diff --git a/.gitignore b/.gitignore index b31756b..caa056f 100644 --- a/.gitignore +++ b/.gitignore @@ -54,3 +54,5 @@ ansible/host_vars/*.yaml !ansible/host_vars/bde* ansible/hosts +apps/member/migrations +apps/wei/migrations diff --git a/.gitpod.yml b/.gitpod.yml index 6de1ba5..b488382 100644 --- a/.gitpod.yml +++ b/.gitpod.yml @@ -3,39 +3,33 @@ # and commit this file to your remote git repository to share the goodness with others. tasks: + - name: Apt update + init: sudo apt update - name: Apt install - command: | - sudo apt update - sudo apt install --no-install-recommends -y \ + init: sudo apt install --no-install-recommends -y \ ipython3 python3-setuptools python3-venv python3-dev \ texlive-xetex gettext libjs-bootstrap4 fonts-font-awesome git - gp sync-done apt - name : Install requirements - init : gp sync-await apt - command: | - pip3 install -r requirements.txt - gp sync-done pip + init: pip3 install -r requirements.txt - name : Setup env - command: cp .env_example .env - - name: Django Init - init : gp sync-await pip - command: | - python3 manage.py collectstatic --noinput - python3 manage.py compilemessages - python3 manage.py makemigrations - python3 manage.py migrate - python3 manage.py loaddata initial - python3 manage.py shell -c "from django.contrib.auth.models import User; User.objects.create_superuser('admin', 'admin@example.com', 'adminpass')" - gp sync-done django - + init: cp .env_example .env + - name: Django collectstatic + init: python3 manage.py collectstatic --noinput + - name: Django compilemessages + init: python3 manage.py compilemessages + - name: Django makemigrations + init: python3 manage.py makemigrations + - name: Django migrate + init: python3 manage.py migrate + - name: Django loaddata + init: python3 manage.py loaddata initial + - name: Django create dev superuser + init: python3 manage.py shell -c "from django.contrib.auth.models import User; User.objects.create_superuser('admin', 'admin@example.com', 'adminpass')" - name: Django start server - init : gp sync-await django command: python3 manage.py runserver 0.0.0.0:8000 ports: - - name: Web Dev Server - port: 8000 - visibility: public - + - port: 8000 + onOpen: open-preview diff --git a/README.md b/README.md index 06ba125..db695cf 100644 --- a/README.md +++ b/README.md @@ -35,27 +35,11 @@ Bien que cela permette de créer une instance sur toutes les distributions, texlive-bin gettext ttf-font-awesome git $ paru -S bootstrap jquery # AUR only, need manuel install bootstrap4 and popper.js ``` - Pour Arch il faut installer manuellement libjs-bootstrap4, popper.js et jquery par l'AUR : - ```bash - yay -S jquery - ``` - ```bash - mkdir temp - cd temp - curl http://fr.archive.ubuntu.com/ubuntu/pool/universe/p/popper.js/libjs-popper.js_1.16.1+ds-6_all.deb --output libjs-popper.js_1.16.1+ds-6_all.deb - curl http://fr.archive.ubuntu.com/ubuntu/pool/universe/t/twitter-bootstrap4/libjs-bootstrap4_4.6.1+dfsg1-4_all.deb -o libjs-bootstrap4_4.6.1+dfsg1-4_all.deb # Dl paquet Debian - ar vx libjs-bootstrap4_4.6.1+dfsg1-4_all.deb # Unpack deb - ar -xvf data.tar.zst # Extract files - ar vx libjs-popper.js_1.16.1+ds-6_all.deb # Unpack deb - ar -xvf data.tar.zst # Extract files - sudo cp -Rp usr/* /usr # Copy file to correct locations - ``` - Proposer un packaging correct pour Arch de ce paquet serait une bonne idée. 2. **Clonage du dépot** là où vous voulez : ```bash - $ git clone git@github.com:jbdoderlein/notes-ker-lann.git --recursive && cd notes-ker-lann + $ git clone git@gitlab.crans.org:bde/nk20.git --recursive && cd nk20 ``` 3. **Création d'un environment de travail Python décorrélé du système.** @@ -145,13 +129,13 @@ Sinon vous pouvez suivre les étapes décrites ci-dessous. nginx python3-venv git acl ``` -2. **Clonage du dépot** dans `/var/www/notes-ker-lann`, +2. **Clonage du dépot** dans `/var/www/note_kfet`, ```bash - $ sudo mkdir -p /var/www/notes-ker-lann && cd /var/www/notes-ker-lann + $ sudo mkdir -p /var/www/note_kfet && cd /var/www/note_kfet $ sudo chown www-data:www-data . $ sudo chmod g+rwx . - $ sudo -u www-data git clone https://github.com/jbdoderlein/notes-ker-lann.git --recursive + $ sudo -u www-data git clone https://gitlab.crans.org/bde/nk20.git --recursive ``` 3. **Création d'un environment de travail Python décorrélé du système.** @@ -168,19 +152,19 @@ Sinon vous pouvez suivre les étapes décrites ci-dessous. ```bash $ cp nginx_note.conf_example nginx_note.conf - $ sudo ln -sf /var/www/notes-ker-lann/nginx_note.conf /etc/nginx/sites-enabled/ + $ sudo ln -sf /var/www/note_kfet/nginx_note.conf /etc/nginx/sites-enabled/ ``` Si l'on a un emperor (plusieurs instance uwsgi): ```bash - $ sudo ln -sf /var/www/notes-ker-lann/uwsgi_note.ini /etc/uwsgi/sites/ + $ sudo ln -sf /var/www/note_kfet/uwsgi_note.ini /etc/uwsgi/sites/ ``` Sinon si on est dans le cas habituel : ```bash - $ sudo ln -sf /var/www/notes-ker-lann/uwsgi_note.ini /etc/uwsgi/apps-enabled/ + $ sudo ln -sf /var/www/note_kfet/uwsgi_note.ini /etc/uwsgi/apps-enabled/ ``` Le touch-reload est activé par défault, pour redémarrer la note il suffit donc de faire `touch uwsgi_note.ini`. @@ -235,7 +219,7 @@ Sinon vous pouvez suivre les étapes décrites ci-dessous. DJANGO_DB_PASSWORD=CHANGE_ME DJANGO_DB_PORT= DJANGO_SECRET_KEY=CHANGE_ME - DJANGO_SETTINGS_MODULE="notes-ker-lann.settings + DJANGO_SETTINGS_MODULE="note_kfet.settings NOTE_URL=localhost # URL où accéder à la note CONTACT_EMAIL=tresorerie.bde@localhost # Le reste n'est utile qu'en production, pour configurer l'envoi des mails @@ -263,21 +247,21 @@ Il est possible de travailler sur une instance Docker. Pour construire l'image Docker `nk20`, ``` -git clone https://github.com/jbdoderlein/notes-ker-lann.git/ --recursive && cd notes-ker-lann -docker build . -t notes-ker-lann +git clone https://gitlab.crans.org/bde/nk20/ --recursive && cd nk20 +docker build . -t nk20 ``` -Ensuite pour lancer la note de Ker Lann en tant que vous (option `-u`), +Ensuite pour lancer la note Kfet en tant que vous (option `-u`), l'exposer sur son port 80 (option `-p`) et monter le code en écriture (option `-v`), ``` -docker run -it --rm -u $(id -u):$(id -g) -v "$(pwd):/var/www/notes-ker-lann/" -p 80:8080 nk20 +docker run -it --rm -u $(id -u):$(id -g) -v "$(pwd):/var/www/note_kfet/" -p 80:8080 nk20 ``` Si vous souhaitez lancer une commande spéciale, vous pouvez l'ajouter à la fin, par exemple, ``` -docker run -it --rm -u $(id -u):$(id -g) -v "$(pwd):/var/www/notes-ker-lann/" -p 80:8080 nk20 python3 ./manage.py createsuperuser +docker run -it --rm -u $(id -u):$(id -g) -v "$(pwd):/var/www/note_kfet/" -p 80:8080 nk20 python3 ./manage.py createsuperuser ``` #### Avec Docker Compose @@ -287,15 +271,15 @@ On vous conseilles de faire un fichier d'environnement `.env` en prenant exemple Pour par exemple utiliser le Docker de la note Kfet avec Traefik pour réaliser le HTTPS, ```YAML -notes-ker-lann: - build: /chemin/vers/le/code/notes-ker-lann +nk20: + build: /chemin/vers/le/code/nk20 volumes: - - /chemin/vers/le/code/notes-ker-lann:/var/www/notes-ker-lann/ - env_file: /chemin/vers/le/code/notes-ker-lann/.env + - /chemin/vers/le/code/nk20:/var/www/note_kfet/ + env_file: /chemin/vers/le/code/nk20/.env restart: always labels: - - "traefik.http.routers.notes-ker-lann.rule=Host(`ndd.example.com`)" - - "traefik.http.services.notes-ker-lann.loadbalancer.server.port=8080" + - "traefik.http.routers.nk20.rule=Host(`ndd.example.com`)" + - "traefik.http.services.nk20.loadbalancer.server.port=8080" ``` ## Documentation diff --git a/apps/activity/fixtures/initial.json b/apps/activity/fixtures/initial.json index 2ebec84..63c5009 100644 --- a/apps/activity/fixtures/initial.json +++ b/apps/activity/fixtures/initial.json @@ -1 +1,32 @@ -[{"model": "activity.activitytype", "pk": 4, "fields": {"name": "Activit\u00e9 gratuite ouverte", "manage_entries": false, "can_invite": true, "guest_entry_fee": 0}}, {"model": "activity.activitytype", "pk": 5, "fields": {"name": "Soir\u00e9e", "manage_entries": true, "can_invite": false, "guest_entry_fee": 500}}] \ No newline at end of file +[ + { + "model": "activity.activitytype", + "pk": 1, + "fields": { + "name": "Pot", + "manage_entries": true, + "can_invite": true, + "guest_entry_fee": 500 + } + }, + { + "model": "activity.activitytype", + "pk": 2, + "fields": { + "name": "Soir\u00e9e de club", + "manage_entries": false, + "can_invite": false, + "guest_entry_fee": 0 + } + }, + { + "model": "activity.activitytype", + "pk": 3, + "fields": { + "name": "Autre", + "manage_entries": false, + "can_invite": false, + "guest_entry_fee": 0 + } + } +] diff --git a/apps/activity/forms.py b/apps/activity/forms.py index a37a2bf..1ca98ce 100644 --- a/apps/activity/forms.py +++ b/apps/activity/forms.py @@ -20,9 +20,9 @@ from .models import Activity, Guest class ActivityForm(forms.ModelForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - # By default, the BDE club is attended - self.fields["attendees_club"].initial = Club.objects.get(name="BDE") - self.fields["attendees_club"].widget.attrs["placeholder"] = "BDE" + # By default, the Kfet club is attended + self.fields["attendees_club"].initial = Club.objects.get(name="Kfet") + self.fields["attendees_club"].widget.attrs["placeholder"] = "Kfet" clubs = list(Club.objects.filter(PermissionBackend .filter_queryset(get_current_request(), Club, "view")).all()) shuffle(clubs) diff --git a/apps/activity/migrations/0001_initial.py b/apps/activity/migrations/0001_initial.py index 1ead0b8..ba2d68d 100644 --- a/apps/activity/migrations/0001_initial.py +++ b/apps/activity/migrations/0001_initial.py @@ -1,7 +1,7 @@ -# Generated by Django 5.1 on 2024-08-13 09:26 +# Generated by Django 2.2.16 on 2020-09-04 21:41 -import django.utils.timezone from django.db import migrations, models +import django.utils.timezone class Migration(migrations.Migration): @@ -18,7 +18,7 @@ class Migration(migrations.Migration): ('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. BDE.', max_length=255, verbose_name='location')), + ('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')), diff --git a/apps/activity/migrations/0002_initial.py b/apps/activity/migrations/0002_auto_20200904_2341.py similarity index 80% rename from apps/activity/migrations/0002_initial.py rename to apps/activity/migrations/0002_auto_20200904_2341.py index ca40bef..4afa74d 100644 --- a/apps/activity/migrations/0002_initial.py +++ b/apps/activity/migrations/0002_auto_20200904_2341.py @@ -1,8 +1,8 @@ -# Generated by Django 5.1 on 2024-08-13 09:26 +# Generated by Django 2.2.16 on 2020-09-04 21:41 -import django.db.models.deletion from django.conf import settings from django.db import migrations, models +import django.db.models.deletion class Migration(migrations.Migration): @@ -11,16 +11,58 @@ class Migration(migrations.Migration): dependencies = [ ('activity', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), ('member', '0001_initial'), ('note', '0001_initial'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] 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.', on_delete=django.db.models.deletion.PROTECT, related_name='+', to='member.club', verbose_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', @@ -30,53 +72,7 @@ class Migration(migrations.Migration): 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.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='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='note', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='note.noteuser', verbose_name='note'), - ), - 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='guest', - field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.PROTECT, to='activity.guest'), - ), - 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.AlterUniqueTogether( - name='activity', - unique_together={('name', 'date_start', 'date_end')}, + 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', @@ -86,4 +82,8 @@ class Migration(migrations.Migration): name='entry', unique_together={('activity', 'note', 'guest')}, ), + migrations.AlterUniqueTogether( + name='activity', + unique_together={('name', 'date_start', 'date_end')}, + ), ] diff --git a/apps/activity/models.py b/apps/activity/models.py index b9541cb..f8e5fab 100644 --- a/apps/activity/models.py +++ b/apps/activity/models.py @@ -73,7 +73,7 @@ class Activity(models.Model): max_length=255, blank=True, default="", - help_text=_("Place where the activity is organized, eg. BDE."), + help_text=_("Place where the activity is organized, eg. Kfet."), ) activity_type = models.ForeignKey( @@ -102,7 +102,7 @@ class Activity(models.Model): on_delete=models.PROTECT, related_name='+', verbose_name=_('attendees club'), - help_text=_("Club that is authorized to join the activity."), + help_text=_("Club that is authorized to join the activity. Mostly the Kfet club."), ) date_start = models.DateTimeField( diff --git a/apps/activity/templates/activity/activity_entry.html b/apps/activity/templates/activity/activity_entry.html index 22374dd..0286bdb 100644 --- a/apps/activity/templates/activity/activity_entry.html +++ b/apps/activity/templates/activity/activity_entry.html @@ -91,7 +91,7 @@ SPDX-License-Identifier: GPL-3.0-or-later }).done(function () { if (target.hasClass("table-info")) addMsg( - "{% trans "Entry done, but caution: the user is not a member." %}", + "{% trans "Entry done, but caution: the user is not a Kfet member." %}", "warning", 10000); else addMsg("Entry made!", "success", 4000); @@ -126,7 +126,7 @@ SPDX-License-Identifier: GPL-3.0-or-later }).done(function () { if (target.hasClass("table-info")) addMsg( - "{% trans "Entry done, but caution: the user is not a member." %}", + "{% trans "Entry done, but caution: the user is not a Kfet member." %}", "warning", 10000); else addMsg("{% trans "Entry done!" %}", "success", 4000); @@ -150,7 +150,8 @@ SPDX-License-Identifier: GPL-3.0-or-later "source": credit_id, "destination": target.attr('data-inviter'), "last_name": last_name, - "first_name": first_name + "first_name": first_name, + "bank": "" }).done(function () { makeTransaction(); reset(); diff --git a/apps/activity/templates/activity/includes/activity_info.html b/apps/activity/templates/activity/includes/activity_info.html index 8619cf0..a16ad33 100644 --- a/apps/activity/templates/activity/includes/activity_info.html +++ b/apps/activity/templates/activity/includes/activity_info.html @@ -40,7 +40,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
Authorization: Token <TOKEN>
pour pouvoir vous identifier.- La Note Ker Lann implémente également le protocole OAuth2, afin de + La Note Kfet implémente également le protocole OAuth2, afin de permettre à des applications tierces d'interagir avec la Note en récoltant des informations (de connexion par exemple) voir en permettant des modifications à distance, par exemple lorsqu'il - s'agit d'avoir un site marchand sur lequel faire des transactions via la Note Ker Lann. + s'agit d'avoir un site marchand sur lequel faire des transactions via la Note Kfet.
diff --git a/apps/member/templates/member/profile_trust.html b/apps/member/templates/member/profile_trust.html index 696c0d4..bd8d6b5 100644 --- a/apps/member/templates/member/profile_trust.html +++ b/apps/member/templates/member/profile_trust.html @@ -28,7 +28,7 @@ SPDX-License-Identifier: GPL-3.0-or-later {% blocktrans trimmed %} Adding someone as a friend enables them to initiate transactions coming from your account (while keeping your balance positive). This is - designed to simplify using note ker lann transfers to transfer money between + designed to simplify using note kfet transfers to transfer money between users. The intent is that one person can make all transfers for a group of friends without needing additional rights among them. {% endblocktrans %} diff --git a/apps/member/tests/test_memberships.py b/apps/member/tests/test_memberships.py index b329138..a46a23e 100644 --- a/apps/member/tests/test_memberships.py +++ b/apps/member/tests/test_memberships.py @@ -49,7 +49,7 @@ class TestMemberships(TestCase): self.club = Club.objects.create(name="totoclub", parent_club=Club.objects.get(name="BDE")) self.bde_membership = Membership.objects.create(user=self.user, club=Club.objects.get(name="BDE")) self.membership = Membership.objects.create(user=self.user, club=self.club) - self.membership.roles.add(Role.objects.get(name="Pr\u00e9sident\u00b7e")) + self.membership.roles.add(Role.objects.get(name="Bureau de club")) self.membership.save() def test_admin_pages(self): @@ -161,7 +161,7 @@ class TestMemberships(TestCase): response = self.client.get(reverse("member:club_members", args=(self.club.pk,)) + "?search=toto&roles=" + ",".join([str(role.pk) for role in - Role.objects.all()])) + Role.objects.filter(weirole__isnull=True).all()])) self.assertEqual(response.status_code, 200) def test_render_club_add_member(self): @@ -179,17 +179,20 @@ class TestMemberships(TestCase): # We create a club without any parent and one club with parent BDE (that is the club Kfet) for bde_parent in False, True: - club = Club.objects.create( - name="Second club " + ("with BDE" if bde_parent else "without BDE"), - parent_club=None, - email="newclub@example.com", - require_memberships=True, - membership_fee_paid=1000, - membership_fee_unpaid=500, - membership_start=date.today(), - membership_end=date.today() + timedelta(days=366), - membership_duration=366, - ) + if bde_parent: + club = Club.objects.get(name="Kfet") + else: + club = Club.objects.create( + name="Second club " + ("with BDE" if bde_parent else "without BDE"), + parent_club=None, + email="newclub@example.com", + require_memberships=True, + membership_fee_paid=1000, + membership_fee_unpaid=500, + membership_start=date.today(), + membership_end=date.today() + timedelta(days=366), + membership_duration=366, + ) response = self.client.get(reverse("member:club_add_member", args=(club.pk,))) self.assertEqual(response.status_code, 200) @@ -201,7 +204,8 @@ class TestMemberships(TestCase): credit_type=NoteSpecial.objects.get(special_type="Espèces").id, credit_amount=4200, last_name="TOTO", - first_name="Toto" + first_name="Toto", + bank="Le matelas", )) self.assertRedirects(response, user.profile.get_absolute_url(), 302, 200) @@ -219,6 +223,11 @@ class TestMemberships(TestCase): self.assertEqual(response.status_code, 200) bde_membership = self.bde_membership + if bde_parent: + bde_membership = Membership.objects.get(club__name="BDE", user=user) + bde_membership.date_start = date(year=2000, month=1, day=1) + bde_membership.date_end = date(year=2000, month=12, day=31) + bde_membership.save() response = self.client.get(reverse("member:club_renew_membership", args=(bde_membership.pk,))) self.assertEqual(response.status_code, 200) @@ -234,6 +243,7 @@ class TestMemberships(TestCase): credit_amount=14242, last_name="TOTO", first_name="Toto", + bank="Bank", )) self.assertRedirects(response, user.profile.get_absolute_url(), 302, 200) @@ -251,11 +261,11 @@ class TestMemberships(TestCase): response = self.client.post(reverse("member:club_manage_roles", args=(self.membership.pk,)), data=dict( roles=[role.id for role in Role.objects.filter( - Q(name="Trésorier·ère")).all()], + Q(name="Membre de club") | Q(name="Trésorier·ère de club") | Q(name="Bureau de club")).all()], )) self.assertRedirects(response, self.user.profile.get_absolute_url(), 302, 200) self.membership.refresh_from_db() - self.assertEqual(self.membership.roles.count(), 1) + self.assertEqual(self.membership.roles.count(), 3) def test_render_user_list(self): """ @@ -389,7 +399,7 @@ class TestMemberAPI(TestAPI): ) self.bde_membership = Membership.objects.create(user=self.user, club=Club.objects.get(name="BDE")) self.membership = Membership.objects.create(user=self.user, club=self.club) - self.membership.roles.add(Role.objects.get(name="Pr\u00e9sident\u00b7e")) + self.membership.roles.add(Role.objects.get(name="Bureau de club")) self.membership.save() def test_club_api(self): diff --git a/apps/member/views.py b/apps/member/views.py index 2b7a1a1..5605330 100644 --- a/apps/member/views.py +++ b/apps/member/views.py @@ -28,7 +28,7 @@ from permission.models import Role from permission.views import ProtectQuerysetMixin, ProtectedCreateView from .forms import UserForm, ProfileForm, ImageForm, ClubForm, MembershipForm,\ - MembershipRolesForm, AuthenticationForm + CustomAuthenticationForm, MembershipRolesForm from .models import Club, Membership from .tables import ClubTable, UserTable, MembershipTable, ClubManagerTable @@ -37,19 +37,16 @@ class CustomLoginView(LoginView): """ Login view, where the user can select its permission mask. """ - form_class = AuthenticationForm + form_class = CustomAuthenticationForm @transaction.atomic def form_valid(self, form): logout(self.request) self.request.user = form.get_user() _set_current_request(self.request) - self.request.session['permission_mask'] = 42#form.cleaned_data['permission_mask'].rank + self.request.session['permission_mask'] = form.cleaned_data['permission_mask'].rank return super().form_valid(form) -def logout_view(request): - logout(request) - return redirect('index') class UserUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): """ @@ -272,7 +269,7 @@ class ProfileTrustView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): "class": "autocomplete form-control", "id": "trusted", "resetable": True, - "api_url": "/api/note/alias/", + "api_url": "/api/note/alias/?note__polymorphic_ctype__model=noteuser", "name_field": "name", "placeholder": "" } @@ -448,7 +445,7 @@ class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): club.update_membership_dates() # managers list - managers = Membership.objects.filter(club=self.object, roles__name="Pr\u00e9sident\u00b7e", + managers = Membership.objects.filter(club=self.object, roles__name="Bureau de club", date_start__lte=date.today(), date_end__gte=date.today())\ .order_by('user__last_name').all() context["managers"] = ClubManagerTable(data=managers, prefix="managers-") @@ -544,6 +541,11 @@ class ClubUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): def get_queryset(self, **kwargs): qs = super().get_queryset(**kwargs) + + # Don't update a WEI club through this view + if "wei" in settings.INSTALLED_APPS: + qs = qs.filter(weiclub=None) + return qs def get_success_url(self): @@ -595,7 +597,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView): if "club_pk" in self.kwargs: # We create a new membership. club = Club.objects.filter(PermissionBackend.filter_queryset(self.request, Club, "view"))\ - .get(pk=self.kwargs["club_pk"]) + .get(pk=self.kwargs["club_pk"], weiclub=None) form.fields['credit_amount'].initial = club.membership_fee_paid # Ensure that the user is member of the parent club and all its the family tree. c = club @@ -659,7 +661,12 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView): if not credit_type: credit_amount = 0 - if user.note.balance + credit_amount < fee: + if user.note.balance + credit_amount < fee and not Membership.objects.filter( + club__name="Kfet", + user=user, + date_start__lte=date.today(), + date_end__gte=date.today(), + ).exists(): # Users without a valid Kfet membership can't have a negative balance. # TODO Send a notification to the user (with a mail?) to tell her/him to credit her/his note form.add_error('user', @@ -676,7 +683,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView): error = True # Must join the parent club before joining this club, except for the Kfet club where it can be at the same time. - if club.parent_club and not Membership.objects.filter( + if club.name != "Kfet" and club.parent_club and not Membership.objects.filter( user=form.instance.user, club=club.parent_club, date_start__gte=club.parent_club.membership_start, @@ -724,6 +731,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView): credit_amount = form.cleaned_data["credit_amount"] last_name = form.cleaned_data["last_name"] first_name = form.cleaned_data["first_name"] + bank = form.cleaned_data["bank"] if credit_type is None: @@ -747,7 +755,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView): # Now, all is fine, the membership can be created. - if club.name == "BDE" or club.name == "BDA" or club.name == "BDS": + if club.name == "BDE" or club.name == "Kfet": # When we renew the BDE membership, we update the profile section # that should happens at least once a year. user.profile.section = user.profile.section_generated @@ -764,6 +772,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView): reason="Crédit " + credit_type.special_type + " (Adhésion " + club.name + ")", last_name=last_name, first_name=first_name, + bank=bank, valid=True, ) transaction._force_save = True @@ -775,8 +784,9 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView): ret = super().form_valid(form) - member_role = Role.objects.filter(Q(name="Adhérent")).all() - + member_role = Role.objects.filter(Q(name="Adhérent BDE") | Q(name="Membre de club")).all() \ + if club.name == "BDE" else Role.objects.filter(Q(name="Adhérent Kfet") | Q(name="Membre de club")).all() \ + if club.name == "Kfet"else Role.objects.filter(name="Membre de club").all() # Set the same roles as before if old_membership: member_role = member_role.union(old_membership.roles.all()) @@ -809,7 +819,8 @@ class ClubManageRolesView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): form = super().get_form(form_class) club = self.object.club - form.fields['roles'].queryset = Role.objects.filter((Q(for_club__isnull=True) | Q(for_club=club))).all() + form.fields['roles'].queryset = Role.objects.filter(Q(weirole__isnull=not hasattr(club, 'weiclub')) + & (Q(for_club__isnull=True) | Q(for_club=club))).all() return form @@ -855,7 +866,8 @@ class ClubMembersListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableV ).get(pk=self.kwargs["pk"]) context["club"] = club - applicable_roles = Role.objects.filter((Q(for_club__isnull=True) | Q(for_club=club))).all() + applicable_roles = Role.objects.filter(Q(weirole__isnull=not hasattr(club, 'weiclub')) + & (Q(for_club__isnull=True) | Q(for_club=club))).all() context["applicable_roles"] = applicable_roles context["only_active"] = "only_active" not in self.request.GET or self.request.GET["only_active"] != '0' diff --git a/apps/note/api/serializers.py b/apps/note/api/serializers.py index c6f62e3..33bf75b 100644 --- a/apps/note/api/serializers.py +++ b/apps/note/api/serializers.py @@ -160,7 +160,7 @@ class ConsumerSerializer(serializers.ModelSerializer): memberships = Membership.objects.filter( PermissionBackend.filter_queryset(get_current_request(), Membership, "view")).filter( user=obj.note.user, - club=2, # BDA + club=2, # Kfet ).order_by("-date_start") if memberships.exists(): return MembershipSerializer().to_representation(memberships.first()) diff --git a/apps/note/api/urls.py b/apps/note/api/urls.py index 8e3b184..d15e824 100644 --- a/apps/note/api/urls.py +++ b/apps/note/api/urls.py @@ -13,7 +13,7 @@ def register_note_urls(router, path): router.register(path + '/note', NotePolymorphicViewSet) router.register(path + '/alias', AliasViewSet) router.register(path + '/trust', TrustViewSet) - router.register(path + '/consumer', ConsumerViewSet,"consumer") + router.register(path + '/consumer', ConsumerViewSet) router.register(path + '/transaction/category', TemplateCategoryViewSet) router.register(path + '/transaction/transaction', TransactionViewSet) diff --git a/apps/note/api/views.py b/apps/note/api/views.py index 6ee2c00..34ffaf2 100644 --- a/apps/note/api/views.py +++ b/apps/note/api/views.py @@ -160,13 +160,11 @@ class ConsumerViewSet(ReadOnlyProtectedModelViewSet): """ queryset = super().get_queryset().distinct() - # Sqlite doesn't support ORDER BY in subqueries queryset = queryset.order_by("name") \ if settings.DATABASES[queryset.db]["ENGINE"] == 'django.db.backends.postgresql' else queryset alias = self.request.query_params.get("alias", None) - # Check if this is a valid regex. If not, we won't check regex try: re.compile(alias) @@ -176,14 +174,13 @@ class ConsumerViewSet(ReadOnlyProtectedModelViewSet): suffix = '__iregex' if valid_regex else '__istartswith' alias_prefix = '^' if valid_regex else '' queryset = queryset.prefetch_related('note') - + if alias: # We match first an alias if it is matched without normalization, # then if the normalized pattern matches a normalized alias. queryset = queryset.filter( **{f'name{suffix}': alias_prefix + alias} - ) - """.union( + ).union( queryset.filter( Q(**{f'normalized_name{suffix}': alias_prefix + Alias.normalize(alias)}) & ~Q(**{f'name{suffix}': alias_prefix + alias}) @@ -194,12 +191,12 @@ class ConsumerViewSet(ReadOnlyProtectedModelViewSet): & ~Q(**{f'normalized_name{suffix}': alias_prefix + Alias.normalize(alias)}) & ~Q(**{f'name{suffix}': alias_prefix + alias}) ), - all=True)""" + all=True) queryset = queryset if settings.DATABASES[queryset.db]["ENGINE"] == 'django.db.backends.postgresql' \ else queryset.order_by("name") - return queryset#.distinct() + return queryset.distinct() class TemplateCategoryViewSet(ReadProtectedModelViewSet): diff --git a/apps/note/fixtures/initial.json b/apps/note/fixtures/initial.json deleted file mode 100644 index 56be0fa..0000000 --- a/apps/note/fixtures/initial.json +++ /dev/null @@ -1,57 +0,0 @@ -[ - { - "model": "note.templatecategory", - "pk": 1, - "fields": { - "name": "Soft" - } - }, - { - "model": "note.templatecategory", - "pk": 2, - "fields": { - "name": "Alcool" - } - }, - { - "model": "note.templatecategory", - "pk": 3, - "fields": { - "name": "Nourriture" - } - }, - { - "model": "note.templatecategory", - "pk": 4, - "fields": { - "name": "Clubs" - } - }, - { - "model": "note.templatecategory", - "pk": 5, - "fields": { - "name": "Goodies" - } - }, - { - "model": "note.templatecategory", - "pk": 6, - "fields": { - "name": "Autre" - } - }, - { - "model": "note.transactiontemplate", - "pk": 1, - "fields": { - "name": "Test Button", - "destination": 5, - "amount": 1, - "category": 6, - "display": false, - "highlighted": false, - "description": "Test button" - } - } -] \ No newline at end of file diff --git a/apps/note/migrations/0001_initial.py b/apps/note/migrations/0001_initial.py index 2eca3e2..a0ebda3 100644 --- a/apps/note/migrations/0001_initial.py +++ b/apps/note/migrations/0001_initial.py @@ -1,9 +1,9 @@ -# Generated by Django 5.1 on 2024-08-13 09:26 +# Generated by Django 2.2.16 on 2020-09-04 21:41 -import django.db.models.deletion -import django.utils.timezone from django.conf import settings from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone class Migration(migrations.Migration): @@ -11,31 +11,12 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('contenttypes', '0002_remove_content_type_name'), - ('member', '0001_initial'), migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('member', '0001_initial'), + ('contenttypes', '0002_remove_content_type_name'), ] operations = [ - 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='', max_length=255, verbose_name='invalidity reason')), - ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), - ], - options={ - 'verbose_name': 'transaction', - 'verbose_name_plural': 'transactions', - }, - ), migrations.CreateModel( name='Note', fields=[ @@ -45,8 +26,8 @@ class Migration(migrations.Migration): ('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(blank=True, 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='', max_length=255)), - ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), + ('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', @@ -65,23 +46,41 @@ class Migration(migrations.Migration): }, ), migrations.CreateModel( - name='SpecialTransaction', + name='Transaction', 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')), + ('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': 'Special transaction', - 'verbose_name_plural': 'Special transactions', + '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')), - ('club', models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, related_name='note', to='member.club', verbose_name='club')), + ('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', @@ -92,7 +91,7 @@ class Migration(migrations.Migration): 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')), + ('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={ @@ -101,15 +100,41 @@ class Migration(migrations.Migration): }, bases=('note.note',), ), - migrations.AddField( - model_name='transaction', - name='destination', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='note.note', verbose_name='destination'), + 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.AddField( - model_name='transaction', - name='source', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='note.note', verbose_name='source'), + 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', @@ -117,7 +142,7 @@ class Migration(migrations.Migration): ('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, related_name='alias', to='note.note')), + ('note', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='note.Note')), ], options={ 'verbose_name': 'alias', @@ -133,62 +158,14 @@ class Migration(migrations.Migration): ('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')), + ('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.CreateModel( - name='Trust', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('trusted', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='trusted', to='note.note', verbose_name='trusted')), - ('trusting', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='trusting', to='note.note', verbose_name='trusting')), - ], - options={ - 'verbose_name': 'frienship', - 'verbose_name_plural': 'friendships', - }, - ), - 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')), - ('membership', models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, related_name='transaction', to='member.membership')), - ], - options={ - 'verbose_name': 'membership transaction', - 'verbose_name_plural': 'membership transactions', - }, - bases=('note.transaction',), - ), - 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')), - ('template', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='note.transactiontemplate')), - ], - options={ - 'verbose_name': 'recurrent transaction', - 'verbose_name_plural': 'recurrent transactions', - }, - bases=('note.transaction',), - ), - 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')), - ('user', models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, related_name='note', to=settings.AUTH_USER_MODEL, verbose_name='user')), - ], - options={ - 'verbose_name': "one's note", - 'verbose_name_plural': 'users note', - }, - bases=('note.note',), - ), migrations.AddIndex( model_name='transaction', index=models.Index(fields=['created_at'], name='note_transa_created_bea8b1_idx'), @@ -201,6 +178,26 @@ class Migration(migrations.Migration): 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'), @@ -209,8 +206,4 @@ class Migration(migrations.Migration): model_name='alias', index=models.Index(fields=['normalized_name'], name='note_alias_normali_bd52b4_idx'), ), - migrations.AlterUniqueTogether( - name='trust', - unique_together={('trusting', 'trusted')}, - ), ] diff --git a/apps/note/migrations/0002_special_note.py b/apps/note/migrations/0002_create_special_notes.py similarity index 100% rename from apps/note/migrations/0002_special_note.py rename to apps/note/migrations/0002_create_special_notes.py diff --git a/apps/note/migrations/0003_replace_null_by_blank.py b/apps/note/migrations/0003_replace_null_by_blank.py new file mode 100644 index 0000000..21da860 --- /dev/null +++ b/apps/note/migrations/0003_replace_null_by_blank.py @@ -0,0 +1,17 @@ +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('note', '0002_create_special_notes'), + ] + + operations = [ + migrations.RunSQL( + "UPDATE note_note SET inactivity_reason = '' WHERE inactivity_reason IS NULL;" + ), + migrations.RunSQL( + "UPDATE note_transaction SET invalidity_reason = '' WHERE invalidity_reason IS NULL;" + ), + ] diff --git a/apps/note/migrations/0004_remove_null_tag_on_charfields.py b/apps/note/migrations/0004_remove_null_tag_on_charfields.py new file mode 100644 index 0000000..012fc35 --- /dev/null +++ b/apps/note/migrations/0004_remove_null_tag_on_charfields.py @@ -0,0 +1,23 @@ +# Generated by Django 2.2.16 on 2020-09-06 19:17 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('note', '0003_replace_null_by_blank'), + ] + + operations = [ + migrations.AlterField( + model_name='note', + name='inactivity_reason', + field=models.CharField(blank=True, 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='', max_length=255), + ), + migrations.AlterField( + model_name='transaction', + name='invalidity_reason', + field=models.CharField(blank=True, default='', max_length=255, verbose_name='invalidity reason'), + ), + ] diff --git a/apps/note/migrations/0005_auto_20210313_1235.py b/apps/note/migrations/0005_auto_20210313_1235.py new file mode 100644 index 0000000..09696c3 --- /dev/null +++ b/apps/note/migrations/0005_auto_20210313_1235.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2.19 on 2021-03-13 11:35 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('note', '0004_remove_null_tag_on_charfields'), + ] + + operations = [ + migrations.AlterField( + model_name='alias', + name='note', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='alias', to='note.Note'), + ), + ] diff --git a/apps/note/migrations/0006_trust.py b/apps/note/migrations/0006_trust.py new file mode 100644 index 0000000..4ed059f --- /dev/null +++ b/apps/note/migrations/0006_trust.py @@ -0,0 +1,27 @@ +# Generated by Django 2.2.24 on 2021-09-05 19:16 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('note', '0005_auto_20210313_1235'), + ] + + operations = [ + migrations.CreateModel( + name='Trust', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('trusted', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='trusted', to='note.Note', verbose_name='trusted')), + ('trusting', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='trusting', to='note.Note', verbose_name='trusting')), + ], + options={ + 'verbose_name': 'frienship', + 'verbose_name_plural': 'friendships', + 'unique_together': {('trusting', 'trusted')}, + }, + ), + ] diff --git a/apps/note/models/notes.py b/apps/note/models/notes.py index bdf0354..6db9e5f 100644 --- a/apps/note/models/notes.py +++ b/apps/note/models/notes.py @@ -189,7 +189,7 @@ class NoteClub(Note): def send_mail_negative_balance(self): plain_text = render_to_string("note/mails/negative_balance.txt", dict(note=self)) html = render_to_string("note/mails/negative_balance.html", dict(note=self)) - send_mail("[Note Ker Lann] Passage en négatif (club {})".format(self.club.name), plain_text, + send_mail("[Note Kfet] Passage en négatif (club {})".format(self.club.name), plain_text, settings.DEFAULT_FROM_EMAIL, [self.club.email], html_message=html) @@ -219,7 +219,7 @@ class NoteSpecial(Note): class Trust(models.Model): """ - A one-sided trust relationship between two users + A one-sided trust relationship bertween two users If another user considers you as your friend, you can transfer money from them diff --git a/apps/note/models/transactions.py b/apps/note/models/transactions.py index 09c9874..3c6b4c7 100644 --- a/apps/note/models/transactions.py +++ b/apps/note/models/transactions.py @@ -306,6 +306,11 @@ class SpecialTransaction(Transaction): verbose_name=_("first_name"), ) + bank = models.CharField( + max_length=255, + verbose_name=_("bank"), + blank=True, + ) @property def type(self): @@ -341,16 +346,20 @@ class SpecialTransaction(Transaction): credit_type = form.cleaned_data["credit_type"] last_name = form.cleaned_data["last_name"] first_name = form.cleaned_data["first_name"] + bank = form.cleaned_data["bank"] error = False - if not last_name or not first_name: + if not last_name or not first_name or (not bank and credit_type.special_type == "Chèque"): if not last_name: form.add_error('last_name', _("This field is required.")) error = True if not first_name: form.add_error('first_name', _("This field is required.")) error = True + if not bank and credit_type.special_type == "Chèque": + form.add_error('bank', _("This field is required.")) + error = True return not error diff --git a/apps/note/templates/note/amount_input.html b/apps/note/templates/note/amount_input.html index 3da9ac4..d487311 100644 --- a/apps/note/templates/note/amount_input.html +++ b/apps/note/templates/note/amount_input.html @@ -9,12 +9,7 @@ SPDX-License-Identifier: GPL-3.0-or-later name="{{ widget.name }}" {# Other attributes are loaded #} {% for name, value in widget.attrs.items %} - {% if value != False %} - {{ name }} - {% if value != True %} - ="{{ value|stringformat:'s' }}" - {% endif %} - {% endif %} + {% ifnotequal value False %}{{ name }}{% ifnotequal value True %}="{{ value|stringformat:'s' }}"{% endifnotequal %}{% endifnotequal %} {% endfor %}>
Le BDE
- {% trans "Mail generated by the Note Ker Lann on the" %} {% now "j F Y à H:i:s" %}
+ {% trans "Mail generated by the Note Kfet on the" %} {% now "j F Y à H:i:s" %}