Merge branch 'beta' into 'master'

Last deployment fixes

See merge request bde/nk20!109
This commit is contained in:
ynerant 2020-09-05 11:21:07 +02:00
commit 863150d200
35 changed files with 2109 additions and 1475 deletions

3
.gitignore vendored
View File

@ -47,6 +47,3 @@ backups/
env/ env/
venv/ venv/
db.sqlite3 db.sqlite3
# Ignore migrations during first phase dev
migrations/

View File

@ -2,6 +2,10 @@ stages:
- test - test
- quality-assurance - quality-assurance
# Also fetch submodules
variables:
GIT_SUBMODULE_STRATEGY: recursive
# Debian Buster # Debian Buster
py37-django22: py37-django22:
stage: test stage: test
@ -15,7 +19,7 @@ py37-django22:
python3-djangorestframework python3-django-cas-server python3-psycopg2 python3-pil python3-djangorestframework python3-django-cas-server python3-psycopg2 python3-pil
python3-babel python3-lockfile python3-pip python3-phonenumbers python3-babel python3-lockfile python3-pip python3-phonenumbers
python3-bs4 python3-setuptools tox 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 script: tox -e py37-django22
# Ubuntu 20.04 # Ubuntu 20.04
@ -33,7 +37,7 @@ py38-django22:
python3-djangorestframework python3-django-cas-server python3-psycopg2 python3-pil python3-djangorestframework python3-django-cas-server python3-psycopg2 python3-pil
python3-babel python3-lockfile python3-pip python3-phonenumbers python3-babel python3-lockfile python3-pip python3-phonenumbers
python3-bs4 python3-setuptools tox 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 script: tox -e py38-django22
linters: linters:

View File

@ -12,7 +12,7 @@ RUN apt-get update && \
python3-babel python3-lockfile python3-pip python3-phonenumbers ipython3 \ python3-babel python3-lockfile python3-pip python3-phonenumbers ipython3 \
python3-bs4 python3-setuptools \ python3-bs4 python3-setuptools \
uwsgi uwsgi-plugin-python3 \ 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 && \ gettext libjs-bootstrap4 fonts-font-awesome && \
rm -rf /var/lib/apt/lists/* rm -rf /var/lib/apt/lists/*

View File

@ -195,7 +195,6 @@ Sinon vous pouvez suivre les étapes décrites ci-dessous.
DJANGO_SECRET_KEY=CHANGE_ME DJANGO_SECRET_KEY=CHANGE_ME
DJANGO_SETTINGS_MODULE="note_kfet.settings DJANGO_SETTINGS_MODULE="note_kfet.settings
NOTE_URL=localhost # URL où accéder à la note NOTE_URL=localhost # URL où accéder à la note
DOMAIN=localhost # note.example.com
CONTACT_EMAIL=tresorerie.bde@localhost CONTACT_EMAIL=tresorerie.bde@localhost
# Le reste n'est utile qu'en production, pour configurer l'envoi des mails # Le reste n'est utile qu'en production, pour configurer l'envoi des mails
NOTE_MAIL=notekfet@localhost NOTE_MAIL=notekfet@localhost
@ -211,7 +210,6 @@ Sinon vous pouvez suivre les étapes décrites ci-dessous.
$ source /env/bin/activate $ source /env/bin/activate
(env)$ ./manage.py check # pas de bêtise qui traine (env)$ ./manage.py check # pas de bêtise qui traine
(env)$ ./manage.py makemigrations
(env)$ ./manage.py migrate (env)$ ./manage.py migrate
7. *Enjoy \o/* 7. *Enjoy \o/*

View File

@ -41,6 +41,7 @@
- texlive-fonts-recommended - texlive-fonts-recommended
- texlive-lang-french - texlive-lang-french
- texlive-latex-base - texlive-latex-base
- texlive-latex-recommended
# WSGI server # WSGI server
- uwsgi - uwsgi

View File

@ -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 - name: Migrate Django database
command: /var/www/note_kfet/env/bin/python manage.py migrate command: /var/www/note_kfet/env/bin/python manage.py migrate
args: args:

View File

@ -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',
},
),
]

View File

@ -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')},
),
]

View File

@ -1,6 +1,7 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
import os
from datetime import timedelta from datetime import timedelta
from threading import Thread 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: if not settings.DEBUG and self.pk and "scripts" in settings.INSTALLED_APPS:
def refresh_activities(): def refresh_activities():
from scripts.management.commands.refresh_activities import Command as RefreshActivitiesCommand from scripts.management.commands.refresh_activities import Command as RefreshActivitiesCommand
RefreshActivitiesCommand.refresh_human_readable_wiki_page("Modification de l'activité " + self.name) # Consider that we can update the wiki iff the WIKI_PASSWORD env var is not empty
RefreshActivitiesCommand.refresh_raw_wiki_page("Modification de l'activité " + self.name) RefreshActivitiesCommand.refresh_human_readable_wiki_page("Modification de l'activité " + self.name,
Thread(daemon=True, target=refresh_activities).start() 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 return ret
def __str__(self): def __str__(self):

View File

@ -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',
},
),
]

View File

@ -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"
}
}
]

View File

@ -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',
},
),
]

View File

@ -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'),
),
]

View File

@ -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),
]

View File

@ -2,22 +2,22 @@
* On form submit, create a new alias * On form submit, create a new alias
*/ */
function create_alias (e) { function create_alias (e) {
// Do not submit HTML form // Do not submit HTML form
e.preventDefault(); e.preventDefault()
// Get data and send to API // Get data and send to API
const formData = new FormData(e.target); const formData = new FormData(e.target)
$.post("/api/note/alias/", { $.post('/api/note/alias/', {
"csrfmiddlewaretoken": formData.get("csrfmiddlewaretoken"), csrfmiddlewaretoken: formData.get('csrfmiddlewaretoken'),
"name": formData.get("name"), name: formData.get('name'),
"note": formData.get("note") note: formData.get('note')
}).done(function () { }).done(function () {
// Reload table // Reload table
$("#alias_table").load(location.pathname + " #alias_table"); $('#alias_table').load(location.pathname + ' #alias_table')
addMsg("Alias ajouté", "success"); addMsg('Alias ajouté', 'success')
}).fail(function (xhr, _textStatus, _error) { }).fail(function (xhr, _textStatus, _error) {
errMsg(xhr.responseJSON); errMsg(xhr.responseJSON)
}); })
} }
/** /**
@ -25,19 +25,19 @@ function create_alias (e) {
* @param Integer button_id Alias id to remove * @param Integer button_id Alias id to remove
*/ */
function delete_button (button_id) { function delete_button (button_id) {
$.ajax({ $.ajax({
url: "/api/note/alias/" + button_id + "/", url: '/api/note/alias/' + button_id + '/',
method: "DELETE", method: 'DELETE',
headers: { "X-CSRFTOKEN": CSRF_TOKEN } headers: { 'X-CSRFTOKEN': CSRF_TOKEN }
}).done(function () { }).done(function () {
addMsg('Alias supprimé', 'success'); addMsg('Alias supprimé', 'success')
$("#alias_table").load(location.pathname + " #alias_table"); $('#alias_table').load(location.pathname + ' #alias_table')
}).fail(function (xhr, _textStatus, _error) { }).fail(function (xhr, _textStatus, _error) {
errMsg(xhr.responseJSON); errMsg(xhr.responseJSON)
}); })
} }
$(document).ready(function () { $(document).ready(function () {
// Attach event // Attach event
document.getElementById("form_alias").addEventListener("submit", create_alias); document.getElementById('form_alias').addEventListener('submit', create_alias)
}) })

View File

@ -197,7 +197,7 @@ class TestMemberships(TestCase):
# Create a new membership # Create a new membership
response = self.client.post(reverse("member:club_add_member", args=(club.pk,)), data=dict( response = self.client.post(reverse("member:club_add_member", args=(club.pk,)), data=dict(
user=user.pk, user=user.pk,
date_start="{:%Y-%m-%d}".format(timezone.now().date()), date_start="{:%Y-%m-%d}".format(date.today()),
soge=False, soge=False,
credit_type=NoteSpecial.objects.get(special_type="Espèces").id, credit_type=NoteSpecial.objects.get(special_type="Espèces").id,
credit_amount=4200, credit_amount=4200,
@ -236,7 +236,7 @@ class TestMemberships(TestCase):
# Renew membership # Renew membership
response = self.client.post(reverse("member:club_renew_membership", args=(membership.pk,)), data=dict( response = self.client.post(reverse("member:club_renew_membership", args=(membership.pk,)), data=dict(
user=user.pk, user=user.pk,
date_start="{:%Y-%m-%d}".format(timezone.now().date()), date_start="{:%Y-%m-%d}".format(date.today()),
soge=bde_parent, soge=bde_parent,
credit_type=NoteSpecial.objects.get(special_type="Chèque").id, credit_type=NoteSpecial.objects.get(special_type="Chèque").id,
credit_amount=14242, credit_amount=14242,
@ -265,7 +265,7 @@ class TestMemberships(TestCase):
response = self.client.post(reverse("member:club_add_member", args=(bde.pk,)), data=dict( response = self.client.post(reverse("member:club_add_member", args=(bde.pk,)), data=dict(
user=user.pk, user=user.pk,
date_start="{:%Y-%m-%d}".format(timezone.now().date()), date_start="{:%Y-%m-%d}".format(date.today()),
soge=True, soge=True,
credit_type=NoteSpecial.objects.get(special_type="Virement bancaire").id, credit_type=NoteSpecial.objects.get(special_type="Virement bancaire").id,
credit_amount=(bde.membership_fee_paid + kfet.membership_fee_paid) / 100, credit_amount=(bde.membership_fee_paid + kfet.membership_fee_paid) / 100,

View File

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

View File

@ -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'),
),
]

View File

@ -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),
]

View File

@ -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')},
),
]

@ -1 +1 @@
Subproject commit 525f091b0caddc69cb2da7eba545ab9609bb1bb0 Subproject commit 7479671b3f6c499958d3ad991eb54de3010f4da8

View File

@ -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',
},
),
]

View File

@ -7,13 +7,15 @@
\usepackage[T1]{fontenc} \usepackage[T1]{fontenc}
\usepackage[utf8]{inputenc} \usepackage[utf8]{inputenc}
\usepackage[a4paper]{geometry} \usepackage[a4paper]{geometry}
\usepackage{units} %\usepackage{bera}
\usepackage{bera}
\usepackage{graphicx} \usepackage{graphicx}
\usepackage{fancyhdr} \usepackage{fancyhdr}
\usepackage{fp} \usepackage{fp}
\usepackage{transparent} \usepackage{transparent}
\usepackage{eso-pic} \usepackage{eso-pic}
\usepackage{ifthen}
\DeclareUnicodeCharacter{00B0}{$^\circ$}
\def\TVA{0} % Taux de la TVA \def\TVA{0} % Taux de la TVA

View File

@ -1,5 +1,6 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from unittest import skip
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
@ -143,6 +144,7 @@ class TestInvoices(TestCase):
self.assertRedirects(response, reverse("treasury:invoice_list"), 302, 200) self.assertRedirects(response, reverse("treasury:invoice_list"), 302, 200)
self.assertFalse(Invoice.objects.filter(pk=self.invoice.id).exists()) self.assertFalse(Invoice.objects.filter(pk=self.invoice.id).exists())
@skip("LaTeX is buggy in the CI")
def test_invoice_render_pdf(self): def test_invoice_render_pdf(self):
""" """
Generate the PDF file of an invoice. Generate the PDF file of an invoice.

View File

@ -217,7 +217,9 @@ class InvoiceRenderView(LoginRequiredMixin, View):
).wait() ).wait()
if error: 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 # Display the generated pdf as a HTTP Response
pdf = open("{}/invoice-{}.pdf".format(tmp_dir, pk), 'rb').read() pdf = open("{}/invoice-{}.pdf".format(tmp_dir, pk), 'rb').read()

View File

@ -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')},
),
]

View File

@ -1110,7 +1110,9 @@ class MemberListRenderView(LoginRequiredMixin, View):
).wait() ).wait()
if error: 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 # Display the generated pdf as a HTTP Response
with open("{}/wei-list.pdf".format(tmp_dir), 'rb') as f: with open("{}/wei-list.pdf".format(tmp_dir), 'rb') as f:

View File

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

View File

@ -1,57 +1,53 @@
$(document).ready(function () { $(document).ready(function () {
$(".autocomplete").keyup(function(e) { $('.autocomplete').keyup(function (e) {
let target = $("#" + e.target.id); const target = $('#' + e.target.id)
let prefix = target.attr("id"); const prefix = target.attr('id')
let api_url = target.attr("api_url"); const api_url = target.attr('api_url')
let api_url_suffix = target.attr("api_url_suffix"); let api_url_suffix = target.attr('api_url_suffix')
if (!api_url_suffix) if (!api_url_suffix) { api_url_suffix = '' }
api_url_suffix = ""; let name_field = target.attr('name_field')
let name_field = target.attr("name_field"); if (!name_field) { name_field = 'name' }
if (!name_field) const input = target.val()
name_field = "name"; target.addClass('is-invalid')
let input = target.val(); target.removeClass('is-valid')
target.addClass("is-invalid"); $('#' + prefix + '_reset').removeClass('d-none')
target.removeClass("is-valid");
$("#" + prefix + "_reset").removeClass("d-none");
$.getJSON(api_url + (api_url.includes("?") ? "&" : "?") + "format=json&search=^" + input + api_url_suffix, function(objects) { $.getJSON(api_url + (api_url.includes('?') ? '&' : '?') + 'format=json&search=^' + input + api_url_suffix, function (objects) {
let html = ""; let html = ''
objects.results.forEach(function (obj) { objects.results.forEach(function (obj) {
html += li(prefix + "_" + obj.id, obj[name_field]); html += li(prefix + '_' + obj.id, obj[name_field])
}); })
let results_list = $("#" + prefix + "_list"); const results_list = $('#' + prefix + '_list')
results_list.html(html); results_list.html(html)
objects.results.forEach(function (obj) { objects.results.forEach(function (obj) {
$("#" + prefix + "_" + obj.id).click(function() { $('#' + prefix + '_' + obj.id).click(function () {
target.val(obj[name_field]); target.val(obj[name_field])
$("#" + prefix + "_pk").val(obj.id); $('#' + prefix + '_pk').val(obj.id)
results_list.html(""); results_list.html('')
target.removeClass("is-invalid"); target.removeClass('is-invalid')
target.addClass("is-valid"); target.addClass('is-valid')
if (typeof autocompleted != 'undefined') if (typeof autocompleted !== 'undefined') { autocompleted(obj, prefix) }
autocompleted(obj, prefix) })
});
if (input === obj[name_field]) if (input === obj[name_field]) { $('#' + prefix + '_pk').val(obj.id) }
$("#" + prefix + "_pk").val(obj.id); })
});
if (results_list.children().length === 1 && e.originalEvent.keyCode >= 32) { if (results_list.children().length === 1 && e.originalEvent.keyCode >= 32) {
results_list.children().first().trigger("click"); results_list.children().first().trigger('click')
} }
}); })
}); })
$(".autocomplete-reset").click(function() { $('.autocomplete-reset').click(function () {
let name = $(this).attr("id").replace("_reset", ""); const name = $(this).attr('id').replace('_reset', '')
$("#" + name + "_pk").val(""); $('#' + name + '_pk').val('')
$("#" + name).val(""); $('#' + name).val('')
$("#" + name + "_list").html(""); $('#' + name + '_list').html('')
$(this).addClass("d-none"); $(this).addClass('d-none')
}); })
}); })

View File

@ -1,18 +1,16 @@
// Copyright (C) 2018-2020 by BDE ENS Paris-Saclay // Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
/** /**
* Convert balance in cents to a human readable amount * Convert balance in cents to a human readable amount
* @param value the balance, in cents * @param value the balance, in cents
* @returns {string} * @returns {string}
*/ */
function pretty_money (value) { function pretty_money (value) {
if (value % 100 === 0) if (value % 100 === 0) { return (value < 0 ? '- ' : '') + Math.floor(Math.abs(value) / 100) + ' €' } else {
return (value < 0 ? "- " : "") + Math.floor(Math.abs(value) / 100) + " €"; return (value < 0 ? '- ' : '') + Math.floor(Math.abs(value) / 100) + '.' +
else (Math.abs(value) % 100 < 10 ? '0' : '') + (Math.abs(value) % 100) + ' €'
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. * @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) { function addMsg (msg, alert_type, timeout = -1) {
let msgDiv = $("#messages"); const msgDiv = $('#messages')
let html = msgDiv.html(); let html = msgDiv.html()
let id = Math.floor(10000 * Math.random() + 1); const id = Math.floor(10000 * Math.random() + 1)
html += "<div class=\"alert alert-" + alert_type + " alert-dismissible\">" + 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>" '<button id="close-message-' + id + '" class="close" data-dismiss="alert" href="#"><span aria-hidden="true">×</span></button>' +
+ msg + "</div>\n"; msg + '</div>\n'
msgDiv.html(html); msgDiv.html(html)
if (timeout > 0) { if (timeout > 0) {
setTimeout(function () { setTimeout(function () {
$("#close-message-" + id).click(); $('#close-message-' + id).click()
}, timeout); }, 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. * @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) { function errMsg (errs_obj, timeout = -1) {
for (const err_msg of Object.values(errs_obj)) { for (const err_msg of Object.values(errs_obj)) {
addMsg(err_msg, 'danger', timeout); addMsg(err_msg, 'danger', timeout)
} }
} }
var reloadWithTurbolinks = (function () { var reloadWithTurbolinks = (function () {
var scrollPosition; var scrollPosition
function reload () { function reload () {
scrollPosition = [window.scrollX, window.scrollY]; scrollPosition = [window.scrollX, window.scrollY]
Turbolinks.visit(window.location.toString(), { action: 'replace' }) 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 () { return reload
if (scrollPosition) { })()
window.scrollTo.apply(window, scrollPosition);
scrollPosition = null
}
});
return reload;
})();
/** /**
* Reload the balance of the user on the right top corner * Reload the balance of the user on the right top corner
*/ */
function refreshBalance () { 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. * @param fun For each found note with the matched alias `alias`, fun(note, alias) is called.
*/ */
function getMatchedNotes (pattern, fun) { 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 * Generate a <li> entry with a given id and text
*/ */
function li (id, text, extra_css) { 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 " 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"; (extra_css || '') + '"' + ' id="' + id + '">' + text + '</li>\n'
} }
/** /**
@ -95,24 +93,13 @@ function li (id, text, extra_css) {
* @param note The concerned note. * @param note The concerned note.
*/ */
function displayStyle (note) { function displayStyle (note) {
if (!note) if (!note) { return '' }
return ""; const balance = note.balance
let balance = note.balance; var css = ''
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' }
if (balance < -5000) return css
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 * Render note name and picture
* @param note The note to render * @param note The note to render
@ -121,23 +108,22 @@ function displayStyle (note) {
* @param profile_pic_field * @param profile_pic_field
*/ */
function displayNote (note, alias, user_note_field = null, profile_pic_field = null) { function displayNote (note, alias, user_note_field = null, profile_pic_field = null) {
if (!note.display_image) { if (!note.display_image) {
note.display_image = '/static/member/img/default_picture.png'; note.display_image = '/static/member/img/default_picture.png'
} }
let img = note.display_image; const img = note.display_image
if (alias !== note.name && note.name) if (alias !== note.name && note.name) { alias += ' (aka. ' + note.name + ')' }
alias += " (aka. " + note.name + ")"; if (user_note_field !== null) {
if (user_note_field !== null) { $('#' + user_note_field).removeAttr('class')
$("#" + user_note_field).removeAttr('class'); $('#' + user_note_field).addClass(displayStyle(note))
$("#" + user_note_field).addClass(displayStyle(note)); $('#' + user_note_field).text(alias + (note.balance == null ? '' : (' :\n' + pretty_money(note.balance))))
$("#" + user_note_field).text(alias + (note.balance == null ? "" : (" :\n" + pretty_money(note.balance)))); if (profile_pic_field != null) {
if (profile_pic_field != null) { $('#' + profile_pic_field).attr('src', img)
$("#" + profile_pic_field).attr('src', img); $('#' + profile_pic_field + '_link').attr('href', note.resourcetype === 'NoteUser'
$("#" + profile_pic_field + "_link").attr('href', note.resourcetype === "NoteUser" ? ? '/accounts/user/' + note.user : note.resourcetype === 'NoteClub'
"/accounts/user/" + note.user : note.resourcetype === "NoteClub" ? ? '/accounts/club/' + note.club : '#')
"/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) * (useful in consumptions, put null if not used)
* @returns an anonymous function to be compatible with jQuery events * @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) { function removeNote (d, note_prefix = 'note', notes_display, note_list_id, user_note_field = null, profile_pic_field = null) {
return (function () { return function () {
let new_notes_display = []; const new_notes_display = []
let html = ""; let html = ''
notes_display.forEach(function (disp) { notes_display.forEach(function (disp) {
if (disp.quantity > 1 || disp.id !== d.id) { if (disp.quantity > 1 || disp.id !== d.id) {
disp.quantity -= disp.id === d.id ? 1 : 0; disp.quantity -= disp.id === d.id ? 1 : 0
new_notes_display.push(disp); new_notes_display.push(disp)
html += li(note_prefix + "_" + disp.id, disp.name html += li(note_prefix + '_' + disp.id, disp.name +
+ "<span class=\"badge badge-dark badge-pill\">" + disp.quantity + "</span>", '<span class="badge badge-dark badge-pill">' + disp.quantity + '</span>',
displayStyle(disp.note)); displayStyle(disp.note))
} }
}); })
notes_display.length = 0; notes_display.length = 0
new_notes_display.forEach(function (disp) { new_notes_display.forEach(function (disp) {
notes_display.push(disp); notes_display.push(disp)
}); })
$("#" + note_list_id).html(html); $('#' + note_list_id).html(html)
notes_display.forEach(function (disp) { notes_display.forEach(function (disp) {
let obj = $("#" + note_prefix + "_" + disp.id); const obj = $('#' + note_prefix + '_' + disp.id)
obj.click(removeNote(disp, note_prefix, notes_display, note_list_id, user_note_field, profile_pic_field)); obj.click(removeNote(disp, note_prefix, notes_display, note_list_id, user_note_field, profile_pic_field))
obj.hover(function () { obj.hover(function () {
if (disp.note) if (disp.note) { displayNote(disp.note, disp.name, user_note_field, profile_pic_field) }
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. * the associated note is not displayed.
* Useful for a consumption if the item is selected before. * Useful for a consumption if the item is selected before.
*/ */
function autoCompleteNote (field_id, note_list_id, notes, notes_display, alias_prefix = "alias", 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) { note_prefix = 'note', user_note_field = null, profile_pic_field = null, alias_click = null) {
let field = $("#" + field_id); const field = $('#' + field_id)
// Configure tooltip // Configure tooltip
field.tooltip({ field.tooltip({
html: true, html: true,
placement: 'bottom', placement: 'bottom',
title: 'Loading...', title: 'Loading...',
trigger: 'manual', trigger: 'manual',
container: field.parent(), container: field.parent(),
fallbackPlacement: 'clockwise' fallbackPlacement: 'clockwise'
}); })
// When the user clicks elsewhere, we hide the tooltip // When the user clicks elsewhere, we hide the tooltip
$(document).click(function(e) { $(document).click(function (e) {
if (!e.target.id.startsWith(alias_prefix)) { if (!e.target.id.startsWith(alias_prefix)) {
field.tooltip("hide"); field.tooltip('hide')
} }
}); })
let old_pattern = null; let old_pattern = null
// Clear search on click // Clear search on click
field.click(function () { field.click(function () {
field.tooltip('hide'); field.tooltip('hide')
field.removeClass('is-invalid'); field.removeClass('is-invalid')
field.val(""); field.val('')
old_pattern = ""; old_pattern = ''
}); })
// When the user type "Enter", the first alias is clicked // When the user type "Enter", the first alias is clicked
field.keypress(function (event) { field.keypress(function (event) {
if (event.originalEvent.charCode === 13 && notes.length > 0) { if (event.originalEvent.charCode === 13 && notes.length > 0) {
let li_obj = field.parent().find("ul li").first(); const li_obj = field.parent().find('ul li').first()
displayNote(notes[0], li_obj.text(), user_note_field, profile_pic_field); displayNote(notes[0], li_obj.text(), user_note_field, profile_pic_field)
li_obj.trigger("click"); li_obj.trigger('click')
} }
}); })
// When the user type something, the matched aliases are refreshed // When the user type something, the matched aliases are refreshed
field.keyup(function (e) { field.keyup(function (e) {
field.removeClass('is-invalid'); field.removeClass('is-invalid')
if (e.originalEvent.charCode === 13) if (e.originalEvent.charCode === 13) { return }
return;
let pattern = field.val(); const pattern = field.val()
// If the pattern is not modified, we don't query the API // If the pattern is not modified, we don't query the API
if (pattern === old_pattern) if (pattern === old_pattern) { return }
return; old_pattern = pattern
old_pattern = pattern; notes.length = 0
notes.length = 0;
// get matched Alias with note associated // get matched Alias with note associated
if (pattern === "") { if (pattern === '') {
field.tooltip('hide'); field.tooltip('hide')
notes.length = 0; notes.length = 0
return; return
} }
$.getJSON("/api/note/consumer/?format=json&alias=" + pattern + "&search=user|club", $.getJSON('/api/note/consumer/?format=json&alias=' + pattern + '&search=user|club',
function (consumers) { function (consumers) {
// The response arrived too late, we stop the request // The response arrived too late, we stop the request
if (pattern !== $("#" + field_id).val()) if (pattern !== $('#' + field_id).val()) { return }
return;
// Build tooltip content // Build tooltip content
let aliases_matched_html = '<ul class="list-group list-group-flush">'; let aliases_matched_html = '<ul class="list-group list-group-flush">'
consumers.results.forEach(function (consumer) { consumers.results.forEach(function (consumer) {
let note = consumer.note; const note = consumer.note
note.email_confirmed = consumer.email_confirmed; note.email_confirmed = consumer.email_confirmed
if (consumer.hasOwnProperty("membership") && consumer.membership) if (consumer.hasOwnProperty('membership') && consumer.membership) { note.membership = consumer.membership } else { note.membership = undefined }
note.membership = consumer.membership; const extra_css = displayStyle(note)
else aliases_matched_html += li(alias_prefix + '_' + consumer.id,
note.membership = undefined; consumer.name,
let extra_css = displayStyle(note); extra_css)
aliases_matched_html += li(alias_prefix + '_' + consumer.id, notes.push(note)
consumer.name, })
extra_css); aliases_matched_html += '</ul>'
notes.push(note);
});
aliases_matched_html += '</ul>';
// Show tooltip // Show tooltip
field.attr('data-original-title', aliases_matched_html).tooltip('show'); field.attr('data-original-title', aliases_matched_html).tooltip('show')
consumers.results.forEach(function (consumer) { consumers.results.forEach(function (consumer) {
let consumer_obj = $("#" + alias_prefix + "_" + consumer.id); const consumer_obj = $('#' + alias_prefix + '_' + consumer.id)
consumer_obj.hover(function () { consumer_obj.hover(function () {
displayNote(consumer.note, consumer.name, user_note_field, profile_pic_field) displayNote(consumer.note, consumer.name, user_note_field, profile_pic_field)
}); })
consumer_obj.click(function () { consumer_obj.click(function () {
var disp = null; var disp = null
notes_display.forEach(function (d) { notes_display.forEach(function (d) {
// We compare the alias ids // We compare the alias ids
if (d.id === consumer.id) { if (d.id === consumer.id) {
d.quantity += 1; d.quantity += 1
disp = d; disp = d
} }
}); })
// In the other case, we add a new emitter // In the other case, we add a new emitter
if (disp == null) { if (disp == null) {
disp = { disp = {
name: consumer.name, name: consumer.name,
id: consumer.id, id: consumer.id,
note: consumer.note, note: consumer.note,
quantity: 1 quantity: 1
}; }
notes_display.push(disp); notes_display.push(disp)
} }
// If the function alias_click exists, it is called. If it doesn't return true, then the notes are // 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 // note displayed. Useful for a consumption when a button is already clicked
if (alias_click && !alias_click()) if (alias_click && !alias_click()) { return }
return;
let note_list = $("#" + note_list_id); const note_list = $('#' + note_list_id)
let html = ""; let html = ''
notes_display.forEach(function (disp) { notes_display.forEach(function (disp) {
html += li(note_prefix + "_" + disp.id, html += li(note_prefix + '_' + disp.id,
disp.name disp.name +
+ "<span class=\"badge badge-dark badge-pill\">" '<span class="badge badge-dark badge-pill">' +
+ disp.quantity + "</span>", disp.quantity + '</span>',
displayStyle(disp.note)); displayStyle(disp.note))
}); })
// Emitters are displayed // Emitters are displayed
note_list.html(html); note_list.html(html)
// Update tooltip position // Update tooltip position
field.tooltip('update'); field.tooltip('update')
notes_display.forEach(function (disp) { notes_display.forEach(function (disp) {
let line_obj = $("#" + note_prefix + "_" + disp.id); const line_obj = $('#' + note_prefix + '_' + disp.id)
// Hover an emitter display also the profile picture // Hover an emitter display also the profile picture
line_obj.hover(function () { line_obj.hover(function () {
displayNote(disp.note, disp.name, user_note_field, profile_pic_field); displayNote(disp.note, disp.name, user_note_field, profile_pic_field)
}); })
// When an emitter is clicked, it is removed // When an emitter is clicked, it is removed
line_obj.click(removeNote(disp, note_prefix, notes_display, note_list_id, user_note_field, line_obj.click(removeNote(disp, note_prefix, notes_display, note_list_id, user_note_field,
profile_pic_field)); profile_pic_field))
}); })
}) })
}); })
})// end getJSON alias
});// end getJSON alias })
});
}// end function autocomplete }// end function autocomplete
// When a validate button is clicked, we switch the validation status // When a validate button is clicked, we switch the validation status
function de_validate (id, validated, resourcetype) { function de_validate (id, validated, resourcetype) {
let validate_obj = $("#validate_" + id); const validate_obj = $('#validate_' + id)
if (validate_obj.data("pending")) if (validate_obj.data('pending'))
// The button is already clicked // The button is already clicked
return; { return }
let invalidity_reason = $("#invalidity_reason_" + id).val(); const invalidity_reason = $('#invalidity_reason_' + id).val()
validate_obj.html("<strong style=\"font-size: 16pt;\">⟳</strong>"); validate_obj.html('<strong style="font-size: 16pt;">⟳</strong>')
validate_obj.data("pending", true); validate_obj.data('pending', true)
// Perform a PATCH request to the API in order to update the transaction // Perform a PATCH request to the API in order to update the transaction
// If the user has insufficient rights, an error message will appear // If the user has insufficient rights, an error message will appear
$.ajax({ $.ajax({
"url": "/api/note/transaction/transaction/" + id + "/", url: '/api/note/transaction/transaction/' + id + '/',
type: "PATCH", type: 'PATCH',
dataType: "json", dataType: 'json',
headers: { headers: {
"X-CSRFTOKEN": CSRF_TOKEN 'X-CSRFTOKEN': CSRF_TOKEN
}, },
data: { data: {
"resourcetype": resourcetype, resourcetype: resourcetype,
"valid": !validated, valid: !validated,
"invalidity_reason": invalidity_reason, invalidity_reason: invalidity_reason
}, },
success: function () { success: function () {
refreshBalance(); refreshBalance()
// error if this method doesn't exist. Please define it. // error if this method doesn't exist. Please define it.
refreshHistory(); refreshHistory()
}, },
error: function (err) { error: function (err) {
let errObj = JSON.parse(err.responseText); const errObj = JSON.parse(err.responseText)
let error = errObj["detail"] ? errObj["detail"] : errObj["non_field_errors"]; let error = errObj.detail ? errObj.detail : errObj.non_field_errors
if (!error) if (!error) { error = err.responseText }
error = err.responseText; addMsg('Une erreur est survenue lors de la validation/dévalidation ' +
addMsg("Une erreur est survenue lors de la validation/dévalidation " + 'de cette transaction : ' + error, 'danger')
"de cette transaction : " + error, "danger");
refreshBalance(); refreshBalance()
// error if this method doesn't exist. Please define it. // error if this method doesn't exist. Please define it.
refreshHistory(); refreshHistory()
} }
}); })
} }
/** /**
@ -404,10 +379,10 @@ function de_validate (id, validated, resourcetype) {
* @param wait Debounced milliseconds * @param wait Debounced milliseconds
*/ */
function debounce (callback, wait) { function debounce (callback, wait) {
let timeout; let timeout
return (...args) => { return (...args) => {
const context = this; const context = this
clearTimeout(timeout); clearTimeout(timeout)
timeout = setTimeout(() => callback.apply(context, args), wait); timeout = setTimeout(() => callback.apply(context, args), wait)
}; }
} }

View File

@ -2,95 +2,92 @@
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// When a transaction is performed, lock the interface to prevent spam clicks. // When a transaction is performed, lock the interface to prevent spam clicks.
var LOCK = false; var LOCK = false
/** /**
* Refresh the history table on the consumptions page. * Refresh the history table on the consumptions page.
*/ */
function refreshHistory() { function refreshHistory () {
$("#history").load("/note/consos/ #history"); $('#history').load('/note/consos/ #history')
$("#most_used").load("/note/consos/ #most_used"); $('#most_used').load('/note/consos/ #most_used')
} }
$(document).ready(function() { $(document).ready(function () {
// If hash of a category in the URL, then select this category // If hash of a category in the URL, then select this category
// else select the first one // else select the first one
if (location.hash) { if (location.hash) {
$("a[href='" + location.hash + "']").tab("show"); $("a[href='" + location.hash + "']").tab('show')
} else { } else {
$("a[data-toggle='tab']").first().tab("show"); $("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')
})
// Switching in double consumptions mode should update the layout
$('#double_conso').change(function () {
$('#consos_list_div').removeClass('d-none')
$('#user_select_div').attr('class', 'col-xl-4')
$('#infos_div').attr('class', 'col-sm-5 col-xl-6')
const note_list_obj = $('#note_list')
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')()
})
})
} }
})
// When selecting a category, change URL $('#single_conso').change(function () {
$(document.body).on("click", "a[data-toggle='tab']", function() { $('#consos_list_div').addClass('d-none')
location.hash = this.getAttribute("href"); $('#user_select_div').attr('class', 'col-xl-7')
}); $('#infos_div').attr('class', 'col-sm-5 col-md-4')
// Switching in double consumptions mode should update the layout const consos_list_obj = $('#consos_list')
$("#double_conso").change(function() { if (buttons.length > 0) {
$("#consos_list_div").removeClass('d-none'); if (notes_display.length === 0 && consos_list_obj.text().length > 0) {
$("#user_select_div").attr('class', 'col-xl-4'); $('#note_list').html(consos_list_obj.html())
$("#infos_div").attr('class', 'col-sm-5 col-xl-6'); 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('')
}
}
})
let note_list_obj = $("#note_list"); // Ensure we begin in single consumption. Fix issue with TurboLinks and BootstrapJS
if (buttons.length > 0 && note_list_obj.text().length > 0) { $("label[for='double_conso']").removeClass('active')
$("#consos_list").html(note_list_obj.html());
note_list_obj.html("");
buttons.forEach(function(button) { $('#consume_all').click(consumeAll)
$("#conso_button_" + button.id).click(function() { })
if (LOCK)
return;
removeNote(button, "conso_button", buttons,"consos_list")();
});
});
}
});
$("#single_conso").change(function() { notes = []
$("#consos_list_div").addClass('d-none'); notes_display = []
$("#user_select_div").attr('class', 'col-xl-7'); buttons = []
$("#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("");
}
}
});
// Ensure we begin in single consumption. Fix issue with TurboLinks and BootstrapJS
$("label[for='double_conso']").removeClass('active');
$("#consume_all").click(consumeAll);
});
notes = [];
notes_display = [];
buttons = [];
// When the user searches an alias, we update the auto-completion // When the user searches an alias, we update the auto-completion
autoCompleteNote("note", "note_list", notes, notes_display, autoCompleteNote('note', 'note_list', notes, notes_display,
"alias", "note", "user_note", "profile_pic", function() { 'alias', 'note', 'user_note', 'profile_pic', function () {
if (buttons.length > 0 && $("#single_conso").is(":checked")) { if (buttons.length > 0 && $('#single_conso').is(':checked')) {
consumeAll(); consumeAll()
return false; return false
} }
return true; return true
}); })
/** /**
* Add a transaction from a button. * 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_id The identifier of the button
* @param template_name The name of the button * @param template_name The name of the button
*/ */
function addConso(dest, amount, type, category_id, category_name, template_id, template_name) { function addConso (dest, amount, type, category_id, category_name, template_id, template_name) {
var button = null; var button = null
buttons.forEach(function(b) { buttons.forEach(function (b) {
if (b.id === template_id) { if (b.id === template_id) {
b.quantity += 1; b.quantity += 1
button = b; 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 (button == null) {
if (dc_obj.is(":checked") || notes_display.length === 0) { button = {
let list = dc_obj.is(":checked") ? "consos_list" : "note_list"; id: template_id,
let html = ""; name: template_name,
buttons.forEach(function(button) { dest: dest,
html += li("conso_button_" + button.id, button.name quantity: 1,
+ "<span class=\"badge badge-dark badge-pill\">" + button.quantity + "</span>"); amount: amount,
}); type: type,
category_id: category_id,
$("#" + list).html(html); category_name: category_name
buttons.forEach(function(button) {
$("#conso_button_" + button.id).click(function() {
if (LOCK)
return;
removeNote(button, "conso_button", buttons, list)();
});
});
} }
else buttons.push(button)
consumeAll(); }
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)
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. * Reset the page as its initial state.
*/ */
function reset() { function reset () {
notes_display.length = 0; notes_display.length = 0
notes.length = 0; notes.length = 0
buttons.length = 0; buttons.length = 0
$("#note_list").html(""); $('#note_list').html('')
$("#consos_list").html(""); $('#consos_list').html('')
$("#note").val(""); $('#note').val('')
$("#note").attr("data-original-title", "").tooltip("hide"); $('#note').attr('data-original-title', '').tooltip('hide')
$("#profile_pic").attr("src", "/static/member/img/default_picture.png"); $('#profile_pic').attr('src', '/static/member/img/default_picture.png')
$("#profile_pic_link").attr("href", "#"); $('#profile_pic_link').attr('href', '#')
refreshHistory(); refreshHistory()
refreshBalance(); refreshBalance()
LOCK = false; LOCK = false
} }
/** /**
* Apply all transactions: all notes in `notes` buy each item in `buttons` * Apply all transactions: all notes in `notes` buy each item in `buttons`
*/ */
function consumeAll() { function consumeAll () {
if (LOCK) if (LOCK) { return }
return;
LOCK = true; LOCK = true
let error = false; let error = false
if (notes_display.length === 0) { if (notes_display.length === 0) {
$("#note").addClass('is-invalid'); $('#note').addClass('is-invalid')
$("#note_list").html(li("", "<strong>Ajoutez des émetteurs.</strong>", "text-danger")); $('#note_list').html(li('', '<strong>Ajoutez des émetteurs.</strong>', 'text-danger'))
error = true; error = true
} }
if (buttons.length === 0) { if (buttons.length === 0) {
$("#consos_list").html(li("", "<strong>Ajoutez des consommations.</strong>", "text-danger")); $('#consos_list').html(li('', '<strong>Ajoutez des consommations.</strong>', 'text-danger'))
error = true; error = true
} }
if (error) { if (error) {
LOCK = false; LOCK = false
return; return
} }
notes_display.forEach(function(note_display) { notes_display.forEach(function (note_display) {
buttons.forEach(function(button) { buttons.forEach(function (button) {
consume(note_display.note, note_display.name, button.dest, button.quantity * note_display.quantity, button.amount, 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); 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 category The category id of the button (type: int)
* @param template The button id (type: int) * @param template The button id (type: int)
*/ */
function consume(source, source_alias, dest, quantity, amount, reason, type, category, template) { function consume (source, source_alias, dest, quantity, amount, reason, type, category, template) {
$.post("/api/note/transaction/transaction/", $.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, csrfmiddlewaretoken: CSRF_TOKEN,
"quantity": quantity, quantity: quantity,
"amount": amount, amount: amount,
"reason": reason, reason: reason,
"valid": true, valid: false,
"polymorphic_ctype": type, invalidity_reason: 'Solde insuffisant',
"resourcetype": "RecurrentTransaction", polymorphic_ctype: type,
"source": source.id, resourcetype: 'RecurrentTransaction',
"source_alias": source_alias, source: source,
"destination": dest, source_alias: source_alias,
"template": template destination: dest,
}) template: template
.done(function () { }).done(function () {
if (!isNaN(source.balance)) { reset()
let newBalance = source.balance - quantity * amount; addMsg("La transaction n'a pas pu être validée pour cause de solde insuffisant.", 'danger', 10000)
if (newBalance <= -5000) }).fail(function () {
addMsg("Attention, La transaction depuis la note " + source_alias + " a été réalisée avec " + reset()
"succès, mais la note émettrice " + source_alias + " est en négatif sévère.", errMsg(e.responseJSON)
"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);
});
});
} }

View File

@ -9,241 +9,240 @@
* Licensed under the New BSD License * Licensed under the New BSD License
* See: http://www.opensource.org/licenses/bsd-license.php * See: http://www.opensource.org/licenses/bsd-license.php
*/ */
;(function($) { ;(function ($) {
$.fn.formset = function(opts) $.fn.formset = function (opts) {
{ var options = $.extend({}, $.fn.formset.defaults, opts)
var options = $.extend({}, $.fn.formset.defaults, opts), var flatExtraClasses = options.extraClasses.join(' ')
flatExtraClasses = options.extraClasses.join(' '), var totalForms = $('#id_' + options.prefix + '-TOTAL_FORMS')
totalForms = $('#id_' + options.prefix + '-TOTAL_FORMS'), var maxForms = $('#id_' + options.prefix + '-MAX_NUM_FORMS')
maxForms = $('#id_' + options.prefix + '-MAX_NUM_FORMS'), var minForms = $('#id_' + options.prefix + '-MIN_NUM_FORMS')
minForms = $('#id_' + options.prefix + '-MIN_NUM_FORMS'), var childElementSelector = 'input,select,textarea,label,div'
childElementSelector = 'input,select,textarea,label,div', var $$ = $(this)
$$ = $(this),
applyExtraClasses = function(row, ndx) { var applyExtraClasses = function (row, ndx) {
if (options.extraClasses) { if (options.extraClasses) {
row.removeClass(flatExtraClasses); row.removeClass(flatExtraClasses)
row.addClass(options.extraClasses[ndx % options.extraClasses.length]); row.addClass(options.extraClasses[ndx % options.extraClasses.length])
} }
}, }
updateElementIndex = function(elem, prefix, ndx) { var updateElementIndex = function (elem, prefix, ndx) {
var idRegex = new RegExp(prefix + '-(\\d+|__prefix__)-'), var idRegex = new RegExp(prefix + '-(\\d+|__prefix__)-')
replacement = prefix + '-' + ndx + '-'; var replacement = prefix + '-' + ndx + '-'
if (elem.attr("for")) elem.attr("for", elem.attr("for").replace(idRegex, replacement)); 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('id')) elem.attr('id', elem.attr('id').replace(idRegex, replacement))
if (elem.attr('name')) elem.attr('name', elem.attr('name').replace(idRegex, replacement)); if (elem.attr('name')) elem.attr('name', elem.attr('name').replace(idRegex, replacement))
}, }
hasChildElements = function(row) { var hasChildElements = function (row) {
return row.find(childElementSelector).length > 0; return row.find(childElementSelector).length > 0
}, }
showAddButton = function() { var showAddButton = function () {
return maxForms.length == 0 || // For Django versions pre 1.2 return maxForms.length === 0 || // For Django versions pre 1.2
(maxForms.val() == '' || (maxForms.val() - totalForms.val() > 0)); (maxForms.val() === '' || (maxForms.val() - totalForms.val() > 0))
}, }
/** /**
* Indicates whether delete link(s) can be displayed - when total forms > min forms * Indicates whether delete link(s) can be displayed - when total forms > min forms
*/ */
showDeleteLinks = function() { var showDeleteLinks = function () {
return minForms.length == 0 || // For Django versions pre 1.7 return minForms.length === 0 || // For Django versions pre 1.7
(minForms.val() == '' || (totalForms.val() - minForms.val() > 0)); (minForms.val() === '' || (totalForms.val() - minForms.val() > 0))
}, }
insertDeleteLink = function(row) { var insertDeleteLink = function (row) {
var delCssSelector = $.trim(options.deleteCssClass).replace(/\s+/g, '.'), var delCssSelector = $.trim(options.deleteCssClass).replace(/\s+/g, '.')
addCssSelector = $.trim(options.addCssClass).replace(/\s+/g, '.'); var addCssSelector = $.trim(options.addCssClass).replace(/\s+/g, '.')
var delButtonHTML = '<a class="' + options.deleteCssClass + '" href="javascript:void(0)">' + options.deleteText +'</a>'; var delButtonHTML = '<a class="' + options.deleteCssClass + '" href="javascript:void(0)">' + options.deleteText + '</a>'
if (options.deleteContainerClass) { if (options.deleteContainerClass) {
// If we have a specific container for the remove button, // If we have a specific container for the remove button,
// place it as the last child of that container: // place it as the last child of that container:
row.find('[class*="' + options.deleteContainerClass + '"]').append(delButtonHTML); row.find('[class*="' + options.deleteContainerClass + '"]').append(delButtonHTML)
} else if (row.is('TR')) { } else if (row.is('TR')) {
// If the forms are laid out in table rows, insert // If the forms are laid out in table rows, insert
// the remove button into the last table cell: // the remove button into the last table cell:
row.children('td:last').append(delButtonHTML); row.children('td:last').append(delButtonHTML)
} else if (row.is('UL') || row.is('OL')) { } else if (row.is('UL') || row.is('OL')) {
// If they're laid out as an ordered/unordered list, // If they're laid out as an ordered/unordered list,
// insert an <li> after the last list item: // insert an <li> after the last list item:
row.append('<li>' + delButtonHTML + '</li>'); row.append('<li>' + delButtonHTML + '</li>')
} else { } else {
// Otherwise, just insert the remove button as the // Otherwise, just insert the remove button as the
// last child element of the form's container: // last child element of the form's container:
row.append(delButtonHTML); row.append(delButtonHTML)
} }
// Check if we're under the minimum number of forms - not to display delete link at rendering // Check if we're under the minimum number of forms - not to display delete link at rendering
if (!showDeleteLinks()){ if (!showDeleteLinks()) {
row.find('a.' + delCssSelector).hide(); row.find('a.' + delCssSelector).hide()
} }
row.find('a.' + delCssSelector).click(function() { row.find('a.' + delCssSelector).click(function () {
var row = $(this).parents('.' + options.formCssClass), var row = $(this).parents('.' + options.formCssClass)
del = row.find('input:hidden[id $= "-DELETE"]'), var del = row.find('input:hidden[id $= "-DELETE"]')
buttonRow = row.siblings("a." + addCssSelector + ', .' + options.formCssClass + '-add'), var buttonRow = row.siblings('a.' + addCssSelector + ', .' + options.formCssClass + '-add')
forms; var forms
if (del.length) { if (del.length) {
// We're dealing with an inline formset. // We're dealing with an inline formset.
// Rather than remove this form from the DOM, we'll mark it as deleted // Rather than remove this form from the DOM, we'll mark it as deleted
// and hide it, then let Django handle the deleting: // and hide it, then let Django handle the deleting:
del.val('on'); del.val('on')
row.hide(); row.hide()
forms = $('.' + options.formCssClass).not(':hidden'); forms = $('.' + options.formCssClass).not(':hidden')
} else { } else {
row.remove(); row.remove()
// Update the TOTAL_FORMS count: // Update the TOTAL_FORMS count:
forms = $('.' + options.formCssClass).not('.formset-custom-template'); forms = $('.' + options.formCssClass).not('.formset-custom-template')
totalForms.val(forms.length); 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;
});
} }
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
})
}
return $$; $$.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)
}
}
})
/* Setup plugin defaults */ if ($$.length) {
$.fn.formset.defaults = { var hideAddButton = !showAddButton()
prefix: 'form', // The form prefix for your django formset var addButton; var template
formTemplate: null, // The jQuery selection cloned to generate new form instances if (options.formTemplate) {
addText: 'add another', // Text for the add link // If a form template was specified, we'll clone it to generate new form instances:
deleteText: 'remove', // Text for the delete link template = (options.formTemplate instanceof $) ? options.formTemplate : $(options.formTemplate)
addContainerClass: null, // Container CSS class for the add link template.removeAttr('id').addClass(options.formCssClass + ' formset-custom-template')
deleteContainerClass: null, // Container CSS class for the delete link template.find(childElementSelector).each(function () {
addCssClass: 'add-row', // CSS class applied to the add link updateElementIndex($(this), options.prefix, '__prefix__')
deleteCssClass: 'delete-row', // CSS class applied to the delete link })
formCssClass: 'dynamic-form', // CSS class applied to each form in a formset insertDeleteLink(template)
extraClasses: [], // Additional CSS classes, which will be applied to each form in turn } else {
keepFieldValues: '', // jQuery selector for fields whose values should be kept when the form is cloned // Otherwise, use the last form in the formset; this works much better if you've got
added: null, // Function called each time a new form is added // extra (>= 1) forms (thnaks to justhamade for pointing this out):
removed: null, // Function called each time a form is deleted if (options.hideLastAddForm) $('.' + options.formCssClass + ':last').hide()
hideLastAddForm: false // When set to true, hide last empty add form (becomes visible when clicking on add button) template = $('.' + options.formCssClass + ':last').clone(true).removeAttr('id')
}; template.find('input:hidden[id $= "-DELETE"]').remove()
})(jQuery); // 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)

View File

@ -6,40 +6,40 @@
let cursor = 0 let cursor = 0
const KONAMI_CODE = [38, 38, 40, 40, 37, 39, 37, 39, 66, 65] const KONAMI_CODE = [38, 38, 40, 40, 37, 39, 37, 39, 66, 65]
function afterKonami() { function afterKonami () {
// Load Rythm.js // Load Rythm.js
var rythmScript = document.createElement('script') var rythmScript = document.createElement('script')
rythmScript.setAttribute('src','//unpkg.com/rythm.js@2.2.5/rythm.min.js') rythmScript.setAttribute('src', '//unpkg.com/rythm.js@2.2.5/rythm.min.js')
document.head.appendChild(rythmScript) document.head.appendChild(rythmScript)
rythmScript.addEventListener('load', function() { rythmScript.addEventListener('load', function () {
// Ker-Lyon audio courtesy of @adalan, ker-lyon.fr // Ker-Lyon audio courtesy of @adalan, ker-lyon.fr
const audioElement = new Audio('/static/song/konami.ogg') const audioElement = new Audio('/static/song/konami.ogg')
audioElement.loop = true audioElement.loop = true
audioElement.play() audioElement.play()
const rythm = new Rythm() const rythm = new Rythm()
rythm.connectExternalAudioElement(audioElement) rythm.connectExternalAudioElement(audioElement)
rythm.addRythm('card', 'pulse', 50, 50, { rythm.addRythm('card', 'pulse', 50, 50, {
min: 1, min: 1,
max: 1.1 max: 1.1
}) })
rythm.addRythm('d-flex', 'color', 50, 50, { rythm.addRythm('d-flex', 'color', 50, 50, {
from: [64,64,64], from: [64, 64, 64],
to:[128,64,128] to: [128, 64, 128]
}) })
rythm.addRythm('nav-link', 'jump', 150, 50, { rythm.addRythm('nav-link', 'jump', 150, 50, {
min: 0, min: 0,
max: 10 max: 10
}) })
rythm.start() rythm.start()
}); })
} }
// Register custom event // Register custom event
document.addEventListener('keydown', (e) => { document.addEventListener('keydown', (e) => {
cursor = (e.keyCode == KONAMI_CODE[cursor]) ? cursor + 1 : 0; cursor = (e.keyCode === KONAMI_CODE[cursor]) ? cursor + 1 : 0
if (cursor == KONAMI_CODE.length) { if (cursor === KONAMI_CODE.length) {
afterKonami() afterKonami()
} }
}); })

View File

@ -1,440 +1,422 @@
var LOCK = false; var LOCK = false
sources = []; sources = []
sources_notes_display = []; sources_notes_display = []
dests = []; dests = []
dests_notes_display = []; dests_notes_display = []
function refreshHistory() { function refreshHistory () {
$("#history").load("/note/transfer/ #history"); $('#history').load('/note/transfer/ #history')
} }
function reset(refresh=true) { function reset (refresh = true) {
sources_notes_display.length = 0; sources_notes_display.length = 0
sources.length = 0; sources.length = 0
dests_notes_display.length = 0; dests_notes_display.length = 0
dests.length = 0; dests.length = 0
$("#source_note_list").html(""); $('#source_note_list').html('')
$("#dest_note_list").html(""); $('#dest_note_list').html('')
let source_field = $("#source_note"); const source_field = $('#source_note')
source_field.val(""); source_field.val('')
let event = jQuery.Event("keyup"); const event = jQuery.Event('keyup')
event.originalEvent = {charCode: 97}; event.originalEvent = { charCode: 97 }
source_field.trigger(event); source_field.trigger(event)
source_field.removeClass('is-invalid'); source_field.removeClass('is-invalid')
source_field.attr("data-original-title", "").tooltip("hide"); source_field.attr('data-original-title', '').tooltip('hide')
let dest_field = $("#dest_note"); const dest_field = $('#dest_note')
dest_field.val(""); dest_field.val('')
dest_field.trigger(event); dest_field.trigger(event)
dest_field.removeClass('is-invalid'); dest_field.removeClass('is-invalid')
dest_field.attr("data-original-title", "").tooltip("hide"); dest_field.attr('data-original-title', '').tooltip('hide')
let amount_field = $("#amount"); const amount_field = $('#amount')
amount_field.val(""); amount_field.val('')
amount_field.removeClass('is-invalid'); amount_field.removeClass('is-invalid')
$("#amount-required").html(""); $('#amount-required').html('')
let reason_field = $("#reason"); const reason_field = $('#reason')
reason_field.val(""); reason_field.val('')
reason_field.removeClass('is-invalid'); reason_field.removeClass('is-invalid')
$("#reason-required").html(""); $('#reason-required').html('')
$("#last_name").val(""); $('#last_name').val('')
$("#first_name").val(""); $('#first_name').val('')
$("#bank").val(""); $('#bank').val('')
$("#user_note").val(""); $('#user_note').val('')
$("#profile_pic").attr("src", "/static/member/img/default_picture.png"); $('#profile_pic').attr('src', '/static/member/img/default_picture.png')
$("#profile_pic_link").attr("href", "#"); $('#profile_pic_link').attr('href', '#')
if (refresh) { if (refresh) {
refreshBalance(); refreshBalance()
refreshHistory(); refreshHistory()
} }
LOCK = false; LOCK = false
} }
$(document).ready(function() { $(document).ready(function () {
/** /**
* If we are in credit/debit mode, check that only one note is entered. * 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. * More over, get first name and last name to autocomplete fields.
*/ */
function checkUniqueNote() { function checkUniqueNote () {
if ($("#type_credit").is(":checked") || $("#type_debit").is(":checked")) { if ($('#type_credit').is(':checked') || $('#type_debit').is(':checked')) {
let arr = $("#type_credit").is(":checked") ? dests_notes_display : sources_notes_display; const arr = $('#type_credit').is(':checked') ? dests_notes_display : sources_notes_display
if (arr.length === 0) if (arr.length === 0) { return }
return;
let last = arr[arr.length - 1]; const last = arr[arr.length - 1]
arr.length = 0; arr.length = 0
arr.push(last); arr.push(last)
last.quantity = 1; last.quantity = 1
if (!last.note.user) { if (!last.note.user) {
$.getJSON("/api/note/note/" + last.note.id + "/?format=json", function(note) { $.getJSON('/api/note/note/' + last.note.id + '/?format=json', function (note) {
last.note.user = note.user; last.note.user = note.user
$.getJSON("/api/user/" + last.note.user + "/", function(user) { $.getJSON('/api/user/' + last.note.user + '/', function (user) {
$("#last_name").val(user.last_name); $('#last_name').val(user.last_name)
$("#first_name").val(user.first_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)
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
}
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 () {
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()
}
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/',
{
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
} }
else { }
$.getJSON("/api/user/" + last.note.user + "/", function(user) { addMsg('Le transfert de ' +
$("#last_name").val(user.last_name); pretty_money(source.quantity * dest.quantity * amount) + ' de la note ' + source.name +
$("#first_name").val(user.first_name); ' vers la note ' + dest.name + ' a été fait avec succès !', 'success', 10000)
});
}
}
return true; 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
}
autoCompleteNote("source_note", "source_note_list", sources, sources_notes_display, $.post('/api/note/transaction/transaction/',
"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;
}
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/",
{
"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 + ")";
}
$.post("/api/note/transaction/transaction/",
{ {
"csrfmiddlewaretoken": CSRF_TOKEN, csrfmiddlewaretoken: CSRF_TOKEN,
"quantity": 1, quantity: source.quantity * dest.quantity,
"amount": amount, amount: amount,
"reason": reason, reason: reason,
"valid": true, valid: false,
"polymorphic_ctype": SPECIAL_TRANSFER_POLYMORPHIC_CTYPE, invalidity_reason: 'Solde insuffisant',
"resourcetype": "SpecialTransaction", polymorphic_ctype: TRANSFER_POLYMORPHIC_CTYPE,
"source": source_id, resourcetype: 'Transaction',
"source_alias": sources_notes_display.length ? alias : null, source: source.note.id,
"destination": dest_id, source_alias: source.name,
"destination_alias": dests_notes_display.length ? alias : null, destination: dest.note.id,
"last_name": $("#last_name").val(), destination_alias: dest.name
"first_name": $("#first_name").val(),
"bank": $("#bank").val()
}).done(function () { }).done(function () {
addMsg("Le crédit/retrait a bien été effectué !", "success", 10000); addMsg('Le transfert de ' +
if (user_note.membership && user_note.membership.date_end < new Date().toISOString()) pretty_money(source.quantity * dest.quantity * amount) + ' de la note ' + source.name +
addMsg("Attention : la note " + alias + " n'est plus adhérente.", "danger", 10000); ' vers la note ' + dest.name + ' a échoué : Solde insuffisant', 'danger', 10000)
reset(); reset()
}).fail(function (err) { }).fail(function (err) {
let errObj = JSON.parse(err.responseText); const errObj = JSON.parse(err.responseText)
let error = errObj["detail"] ? errObj["detail"] : errObj["non_field_errors"] let error = errObj.detail ? errObj.detail : errObj.non_field_errors
if (!error) if (!error) { error = err.responseText }
error = err.responseText; addMsg('Le transfert de ' +
addMsg("Le crédit/retrait a échoué : " + error, "danger", 10000); pretty_money(source.quantity * dest.quantity * amount) + ' de la note ' + source.name +
LOCK = false; ' 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
})
}
})

View File

@ -15,7 +15,6 @@ deps =
-r{toxinidir}/requirements.txt -r{toxinidir}/requirements.txt
coverage coverage
commands = commands =
./manage.py makemigrations
coverage run --omit='*migrations*,apps/scripts*' --source=apps,note_kfet ./manage.py test apps/ coverage run --omit='*migrations*,apps/scripts*' --source=apps,note_kfet ./manage.py test apps/
coverage report -m coverage report -m