diff --git a/.coveragerc b/.coveragerc index 61b95053..be61ad50 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,11 +1,14 @@ [run] source = - adherents + activity + member note theme omit = - adherents/tests/*.py - adherents/migrations/*.py + activity/tests/*.py + activity/migrations/*.py + member/tests/*.py + member/migrations/*.py note/tests/*.py note/migrations/*.py theme/tests/*.py \ No newline at end of file diff --git a/.gitignore b/.gitignore index 3bc22e7b..5d8adc55 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,6 @@ settings_local.py env/ venv/ db.sqlite3 + +# Ignore migrations during first phase dev +migrations/ diff --git a/adherents/__init__.py b/activity/__init__.py similarity index 71% rename from adherents/__init__.py rename to activity/__init__.py index c1217d13..75df9e1f 100644 --- a/adherents/__init__.py +++ b/activity/__init__.py @@ -2,4 +2,4 @@ # Copyright (C) 2018-2019 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later -default_app_config = 'adherents.apps.AdherentsConfig' +default_app_config = 'activity.apps.ActivityConfig' diff --git a/activity/admin.py b/activity/admin.py new file mode 100644 index 00000000..1efe272c --- /dev/null +++ b/activity/admin.py @@ -0,0 +1,33 @@ +# -*- mode: python; coding: utf-8 -*- +# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from django.contrib import admin + +from .models import Activity, ActivityType, Guest + + +class ActivityAdmin(admin.ModelAdmin): + """ + Admin customisation for Activity + """ + list_display = ('name', 'activity_type', 'organizer') + list_filter = ('activity_type',) + search_fields = ['name', 'organizer__name'] + + # Organize activities by start date + date_hierarchy = 'date_start' + ordering = ['-date_start'] + + +class ActivityTypeAdmin(admin.ModelAdmin): + """ + Admin customisation for ActivityType + """ + list_display = ('name', 'can_invite', 'guest_entry_fee') + + +# Register your models here. +admin.site.register(Activity, ActivityAdmin) +admin.site.register(ActivityType, ActivityTypeAdmin) +admin.site.register(Guest) diff --git a/adherents/apps.py b/activity/apps.py similarity index 71% rename from adherents/apps.py rename to activity/apps.py index 05e36034..29990f1b 100644 --- a/adherents/apps.py +++ b/activity/apps.py @@ -6,6 +6,6 @@ from django.apps import AppConfig from django.utils.translation import gettext_lazy as _ -class AdherentsConfig(AppConfig): - name = 'adherents' - verbose_name = _('adherents') +class ActivityConfig(AppConfig): + name = 'activity' + verbose_name = _('activity') diff --git a/activity/locale/fr/LC_MESSAGES/django.po b/activity/locale/fr/LC_MESSAGES/django.po new file mode 100644 index 00000000..dc8f2db2 --- /dev/null +++ b/activity/locale/fr/LC_MESSAGES/django.po @@ -0,0 +1,74 @@ +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2019-07-16 13:45+0200\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#: apps.py:11 models.py:61 +msgid "activity" +msgstr "activité" + +#: models.py:12 models.py:29 +msgid "name" +msgstr "nom" + +#: models.py:16 +msgid "can invite" +msgstr "peut inviter" + +#: models.py:19 +msgid "guest entry fee" +msgstr "cotisation de l'entrée invité" + +#: models.py:23 +msgid "activity type" +msgstr "type d'activité" + +#: models.py:24 +msgid "activity types" +msgstr "types d'activité" + +#: models.py:33 +msgid "description" +msgstr "description" + +#: models.py:39 +msgid "type" +msgstr "type" + +#: models.py:45 +msgid "organizer" +msgstr "organisateur" + +#: models.py:51 +msgid "attendees club" +msgstr "" + +#: models.py:54 +msgid "start date" +msgstr "date de début" + +#: models.py:57 +msgid "end date" +msgstr "date de fin" + +#: models.py:62 +msgid "activities" +msgstr "activités" + +#: models.py:88 +msgid "guest" +msgstr "invité" + +#: models.py:89 +msgid "guests" +msgstr "invités" diff --git a/adherents/migrations/__init__.py b/activity/migrations/__init__.py similarity index 100% rename from adherents/migrations/__init__.py rename to activity/migrations/__init__.py diff --git a/activity/models.py b/activity/models.py new file mode 100644 index 00000000..2cc81732 --- /dev/null +++ b/activity/models.py @@ -0,0 +1,92 @@ +# -*- mode: python; coding: utf-8 -*- +# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from django.conf import settings +from django.db import models +from django.utils.translation import gettext_lazy as _ + + +class ActivityType(models.Model): + name = models.CharField( + verbose_name=_('name'), + max_length=255, + ) + can_invite = models.BooleanField( + verbose_name=_('can invite'), + ) + guest_entry_fee = models.PositiveIntegerField( + verbose_name=_('guest entry fee'), + ) + + class Meta: + verbose_name = _("activity type") + verbose_name_plural = _("activity types") + + def __str__(self): + return self.name + + +class Activity(models.Model): + name = models.CharField( + verbose_name=_('name'), + max_length=255, + ) + description = models.TextField( + verbose_name=_('description'), + ) + activity_type = models.ForeignKey( + ActivityType, + on_delete=models.PROTECT, + related_name='+', + verbose_name=_('type'), + ) + organizer = models.ForeignKey( + 'member.Club', + on_delete=models.PROTECT, + related_name='+', + verbose_name=_('organizer'), + ) + attendees_club = models.ForeignKey( + 'member.Club', + on_delete=models.PROTECT, + related_name='+', + verbose_name=_('attendees club'), + ) + date_start = models.DateTimeField( + verbose_name=_('start date'), + ) + date_end = models.DateTimeField( + verbose_name=_('end date'), + ) + + class Meta: + verbose_name = _("activity") + verbose_name_plural = _("activities") + + +class Guest(models.Model): + activity = models.ForeignKey( + Activity, + on_delete=models.PROTECT, + related_name='+', + ) + name = models.CharField( + max_length=255, + ) + inviter = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.PROTECT, + related_name='+', + ) + entry = models.DateTimeField( + null=True, + ) + entry_transaction = models.ForeignKey( + 'note.Transaction', + on_delete=models.PROTECT, + ) + + class Meta: + verbose_name = _("guest") + verbose_name_plural = _("guests") diff --git a/adherents/tests/__init__.py b/activity/tests/__init__.py similarity index 100% rename from adherents/tests/__init__.py rename to activity/tests/__init__.py diff --git a/adherents/locale/fr/LC_MESSAGES/django.po b/adherents/locale/fr/LC_MESSAGES/django.po deleted file mode 100644 index cad278eb..00000000 --- a/adherents/locale/fr/LC_MESSAGES/django.po +++ /dev/null @@ -1,51 +0,0 @@ -# SOME DESCRIPTIVE TITLE. -# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER -# This file is distributed under the same license as the PACKAGE package. -# FIRST AUTHOR , YEAR. -# -#, fuzzy -msgid "" -msgstr "" -"Project-Id-Version: PACKAGE VERSION\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2019-07-08 13:45+0200\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: FULL NAME \n" -"Language-Team: LANGUAGE \n" -"Language: \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=(n > 1);\n" - -#: models.py:26 -msgid "phone number" -msgstr "numéro de téléphone" - -#: models.py:30 -msgid "section" -msgstr "section" - -#: models.py:31 -msgid "e.g. \"1A0\", \"9A♥\", \"SAPHIRE\"" -msgstr "e.g. \"1A0\", \"9A♥\", \"SAPHIRE\"" - -#: models.py:35 models.py:36 -msgid "user profile" -msgstr "profil utilisateur" - -#: models.py:52 -msgid "date" -msgstr "date" - -#: models.py:57 -msgid "amount" -msgstr "montant" - -#: models.py:61 -msgid "membership fee" -msgstr "cotisation" - -#: models.py:62 -msgid "membership fees" -msgstr "cotisations" diff --git a/adherents/migrations/0001_initial.py b/adherents/migrations/0001_initial.py deleted file mode 100644 index 32433430..00000000 --- a/adherents/migrations/0001_initial.py +++ /dev/null @@ -1,49 +0,0 @@ -# Generated by Django 2.2.3 on 2019-07-16 07:17 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='Profile', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('avatar', models.ImageField(blank=True, max_length=255, upload_to='', verbose_name='profile picture')), - ('phone_number', models.CharField(blank=True, default='', max_length=50, null=True, verbose_name='phone number')), - ('section', models.CharField(help_text='e.g. "1A0", "9A♥", "SAPHIRE"', max_length=255, verbose_name='section')), - ('genre', models.CharField(blank=True, choices=[(None, 'ND'), ('M', 'M'), ('F', 'F')], max_length=1, null=True)), - ('address', models.TextField(blank=True, null=True)), - ('paid', models.BooleanField(default=False, verbose_name='paid')), - ('is_active', models.BooleanField(default=True, verbose_name='is active')), - ('is_deleted', models.BooleanField(default=False, verbose_name='is deleted')), - ('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='MembershipFee', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('date', models.DateField(max_length=255, verbose_name='date')), - ('amount', models.DecimalField(decimal_places=2, max_digits=5, verbose_name='amount')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)), - ], - options={ - 'verbose_name': 'membership fee', - 'verbose_name_plural': 'membership fees', - }, - ), - ] diff --git a/adherents/models.py b/adherents/models.py deleted file mode 100644 index 947499bd..00000000 --- a/adherents/models.py +++ /dev/null @@ -1,105 +0,0 @@ -# -*- mode: python; coding: utf-8 -*- -# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay -# SPDX-License-Identifier: GPL-3.0-or-later - -from django.conf import settings -from django.contrib.auth.models import User -from django.db import models -from django.db.models.signals import post_save -from django.dispatch import receiver -from django.utils.translation import gettext_lazy as _ - - -class Profile(models.Model): - """ - An user profile - - We do not want to patch the Django Contrib Auth User class - so this model add an user profile with additional information. - """ - GENRES = [ - (None, "ND"), - ("M", "M"), - ("F", "F"), - ] - - user = models.OneToOneField( - settings.AUTH_USER_MODEL, - on_delete=models.CASCADE, - ) - avatar = models.ImageField( - max_length=255, - blank=True, - verbose_name=_('profile picture'), - ) - phone_number = models.CharField( - max_length=50, - blank=True, - null=True, - default='', - verbose_name=_('phone number'), - ) - section = models.CharField( - max_length=255, - verbose_name=_('section'), - help_text=_('e.g. "1A0", "9A♥", "SAPHIRE"'), - ) - genre = models.CharField( - max_length=1, - blank=True, - null=True, - choices=GENRES, - ) - address = models.TextField( - blank=True, - null=True, - ) - paid = models.BooleanField( - verbose_name=_("paid"), - default=False, - ) - is_active = models.BooleanField( - verbose_name=_("is active"), - default=True, - ) - is_deleted = models.BooleanField( - verbose_name=_("is deleted"), - default=False, - ) - - class Meta: - verbose_name = _('user profile') - verbose_name_plural = _('user profile') - - -class MembershipFee(models.Model): - """ - User can become member by paying a membership fee - """ - user = models.ForeignKey( - settings.AUTH_USER_MODEL, - on_delete=models.PROTECT, - ) - date = models.DateField( - max_length=255, - verbose_name=_('date'), - ) - amount = models.DecimalField( - max_digits=5, # Max 999.99 € - decimal_places=2, - verbose_name=_('amount'), - ) - - class Meta: - verbose_name = _('membership fee') - verbose_name_plural = _('membership fees') - - -@receiver(post_save, sender=User) -def save_user_profile(instance, created, **_kwargs): - """ - Hook to save an user profile when an user is updated - """ - if created: - Profile.objects.create(user=instance) - instance.profile.save() diff --git a/member/__init__.py b/member/__init__.py new file mode 100644 index 00000000..ec189d6f --- /dev/null +++ b/member/__init__.py @@ -0,0 +1,5 @@ +# -*- mode: python; coding: utf-8 -*- +# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +default_app_config = 'member.apps.MemberConfig' diff --git a/adherents/admin.py b/member/admin.py similarity index 83% rename from adherents/admin.py rename to member/admin.py index a6918d19..dc595d7e 100644 --- a/adherents/admin.py +++ b/member/admin.py @@ -7,7 +7,7 @@ from django.contrib.auth.admin import UserAdmin from django.contrib.auth.models import User from .forms import CustomUserChangeForm -from .models import Profile +from .models import Club, Membership, Profile, Role class ProfileInline(admin.StackedInline): @@ -33,5 +33,11 @@ class CustomUserAdmin(UserAdmin): return super().get_inline_instances(request, obj) +# Update Django User with profile admin.site.unregister(User) admin.site.register(User, CustomUserAdmin) + +# Add other models +admin.site.register(Club) +admin.site.register(Membership) +admin.site.register(Role) diff --git a/member/apps.py b/member/apps.py new file mode 100644 index 00000000..928c00e4 --- /dev/null +++ b/member/apps.py @@ -0,0 +1,11 @@ +# -*- mode: python; coding: utf-8 -*- +# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from django.apps import AppConfig +from django.utils.translation import gettext_lazy as _ + + +class MemberConfig(AppConfig): + name = 'member' + verbose_name = _('member') diff --git a/adherents/forms.py b/member/forms.py similarity index 99% rename from adherents/forms.py rename to member/forms.py index a387d73c..6cc9ca4e 100644 --- a/adherents/forms.py +++ b/member/forms.py @@ -10,6 +10,7 @@ class CustomUserChangeForm(UserChangeForm): Make first name, last name and email required in the default Django Auth User model """ + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.fields['first_name'].required = True diff --git a/member/locale/fr/LC_MESSAGES/django.po b/member/locale/fr/LC_MESSAGES/django.po new file mode 100644 index 00000000..226234a9 --- /dev/null +++ b/member/locale/fr/LC_MESSAGES/django.po @@ -0,0 +1,116 @@ +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2019-07-16 15:21+0200\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#: apps.py:11 +msgid "member" +msgstr "adhérent" + +#: models.py:24 +msgid "phone number" +msgstr "numéro de téléphone" + +#: models.py:30 +msgid "section" +msgstr "section" + +#: models.py:31 +msgid "e.g. \"1A0\", \"9A♥\", \"SAPHIRE\"" +msgstr "e.g. \"1A0\", \"9A♥\", \"SAPHIRE\"" + +#: models.py:37 +msgid "address" +msgstr "adresse" + +#: models.py:43 +msgid "paid" +msgstr "payé" + +#: models.py:48 models.py:49 +msgid "user profile" +msgstr "profil utilisateur" + +#: models.py:57 models.py:102 +msgid "name" +msgstr "nom" + +#: models.py:62 +msgid "email" +msgstr "courriel" + +#: models.py:67 +msgid "membership fee" +msgstr "cotisation pour adhérer" + +#: models.py:71 +msgid "membership duration" +msgstr "durée de l'adhésion" + +#: models.py:72 +msgid "The longest time a membership can last (NULL = infinite)." +msgstr "La durée maximale d'une adhésion (NULL = infinie)." + +#: models.py:77 +msgid "membership start" +msgstr "début de l'adhésion" + +#: models.py:78 +msgid "How long after January 1st the members can renew their membership." +msgstr "" + +#: models.py:83 +msgid "membership end" +msgstr "fin de l'adhésion" + +#: models.py:84 +msgid "" +"How long the membership can last after January 1st of the next year after " +"members can renew their membership." +msgstr "" + +#: models.py:90 +msgid "club" +msgstr "club" + +#: models.py:91 +msgid "clubs" +msgstr "clubs" + +#: models.py:108 +msgid "role" +msgstr "rôle" + +#: models.py:109 +msgid "roles" +msgstr "rôles" + +#: models.py:126 +msgid "membership starts on" +msgstr "l'adhésion commence le" + +#: models.py:129 +msgid "membership ends on" +msgstr "l'adhésion finie le" + +#: models.py:133 +msgid "fee" +msgstr "cotisation" + +#: models.py:137 +msgid "membership" +msgstr "adhésion" + +#: models.py:138 +msgid "memberships" +msgstr "adhésions" diff --git a/member/migrations/__init__.py b/member/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/member/models.py b/member/models.py new file mode 100644 index 00000000..dbe0ad07 --- /dev/null +++ b/member/models.py @@ -0,0 +1,148 @@ +# -*- mode: python; coding: utf-8 -*- +# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from django.conf import settings +from django.db import models +from django.db.models.signals import post_save +from django.dispatch import receiver +from django.utils.translation import gettext_lazy as _ + + +class Profile(models.Model): + """ + An user profile + + We do not want to patch the Django Contrib Auth User class + so this model add an user profile with additional information. + """ + user = models.OneToOneField( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + ) + phone_number = models.CharField( + verbose_name=_('phone number'), + max_length=50, + blank=True, + null=True, + ) + section = models.CharField( + verbose_name=_('section'), + help_text=_('e.g. "1A0", "9A♥", "SAPHIRE"'), + max_length=255, + blank=True, + null=True, + ) + address = models.CharField( + verbose_name=_('address'), + max_length=255, + blank=True, + null=True, + ) + paid = models.BooleanField( + verbose_name=_("paid"), + default=False, + ) + + class Meta: + verbose_name = _('user profile') + verbose_name_plural = _('user profile') + + +class Club(models.Model): + """ + A student club + """ + name = models.CharField( + verbose_name=_('name'), + max_length=255, + unique=True, + ) + email = models.EmailField( + verbose_name=_('email'), + ) + + # Memberships + membership_fee = models.PositiveIntegerField( + verbose_name=_('membership fee'), + ) + membership_duration = models.DurationField( + null=True, + verbose_name=_('membership duration'), + help_text=_('The longest time a membership can last ' + '(NULL = infinite).'), + ) + membership_start = models.DurationField( + null=True, + verbose_name=_('membership start'), + help_text=_('How long after January 1st the members can renew ' + 'their membership.'), + ) + membership_end = models.DurationField( + null=True, + verbose_name=_('membership end'), + help_text=_('How long the membership can last after January 1st ' + 'of the next year after members can renew their ' + 'membership.'), + ) + + class Meta: + verbose_name = _("club") + verbose_name_plural = _("clubs") + + def __str__(self): + return self.name + + +class Role(models.Model): + """ + Role that an user can have in a club + """ + name = models.CharField( + verbose_name=_('name'), + max_length=255, + unique=True, + ) + + class Meta: + verbose_name = _('role') + verbose_name_plural = _('roles') + + +class Membership(models.Model): + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.PROTECT + ) + club = models.ForeignKey( + Club, + on_delete=models.PROTECT + ) + roles = models.ForeignKey( + Role, + on_delete=models.PROTECT + ) + date_start = models.DateField( + verbose_name=_('membership starts on'), + ) + date_end = models.DateField( + verbose_name=_('membership ends on'), + null=True, + ) + fee = models.PositiveIntegerField( + verbose_name=_('fee'), + ) + + class Meta: + verbose_name = _('membership') + verbose_name_plural = _('memberships') + + +@receiver(post_save, sender=settings.AUTH_USER_MODEL) +def save_user_profile(instance, created, **_kwargs): + """ + Hook to save an user profile when an user is updated + """ + if created: + Profile.objects.create(user=instance) + instance.profile.save() diff --git a/member/tests/__init__.py b/member/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/note/admin.py b/note/admin.py index 122c11e3..e634e607 100644 --- a/note/admin.py +++ b/note/admin.py @@ -1,10 +1,95 @@ +# -*- mode: python; coding: utf-8 -*- +# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + from django.contrib import admin -from .models import NoteClub, NoteSpec, NoteUser -from .models import Alias +from .models.notes import Alias, NoteClub, NoteSpecial, NoteUser +from .models.transactions import MembershipTransaction, Transaction, \ + TransactionTemplate + + +class AliasInlines(admin.TabularInline): + """ + Define user and club aliases when editing their note + """ + extra = 0 + model = Alias + + +class NoteClubAdmin(admin.ModelAdmin): + """ + Admin customisation for NoteClub + """ + inlines = (AliasInlines,) + list_display = ('club', 'balance', 'is_active') + list_filter = ('is_active',) + search_fields = ['club__name'] + + # We can't change club after creation + readonly_fields = ('club',) + + def has_add_permission(self, request): + """ + A club note should not be manually added + """ + return False + + def has_delete_permission(self, request, obj=None): + """ + A club note should not be manually removed + """ + return False + + +class NoteSpecialAdmin(admin.ModelAdmin): + """ + Admin customisation for NoteSpecial + """ + list_display = ('special_type', 'balance', 'is_active') + + +class NoteUserAdmin(admin.ModelAdmin): + """ + Admin customisation for NoteUser + """ + inlines = (AliasInlines,) + list_display = ('user', 'balance', 'is_active') + list_filter = ('is_active',) + search_fields = ['user__username'] + + # Organize note by registration date + date_hierarchy = 'user__date_joined' + ordering = ['-user__date_joined'] + + # We can't change user after creation + readonly_fields = ('user',) + + def has_add_permission(self, request): + """ + An user note should not be manually added + """ + return False + + def has_delete_permission(self, request, obj=None): + """ + An user note should not be manually removed + """ + return False + + +class TransactionTemplateAdmin(admin.ModelAdmin): + """ + Admin customisation for TransactionTemplate + """ + list_display = ('name', 'destination', 'amount', 'template_type') + list_filter = ('destination', 'template_type',) + # Register your models here. -admin.site.register(NoteClub) -admin.site.register(NoteSpec) -admin.site.register(NoteUser) -admin.site.register(Alias) +admin.site.register(NoteClub, NoteClubAdmin) +admin.site.register(NoteSpecial, NoteSpecialAdmin) +admin.site.register(NoteUser, NoteUserAdmin) +admin.site.register(MembershipTransaction) +admin.site.register(Transaction) +admin.site.register(TransactionTemplate, TransactionTemplateAdmin) diff --git a/note/locale/fr/LC_MESSAGES/django.po b/note/locale/fr/LC_MESSAGES/django.po new file mode 100644 index 00000000..97d5f9c7 --- /dev/null +++ b/note/locale/fr/LC_MESSAGES/django.po @@ -0,0 +1,134 @@ +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2019-07-16 15:21+0200\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#: apps.py:11 models/notes.py:39 +msgid "note" +msgstr "note" + +#: models/notes.py:21 +msgid "account balance" +msgstr "solde du compte" + +#: models/notes.py:22 +msgid "in centimes, money credited for this instance" +msgstr "en centimes, argent crédité pour cette instance" + +#: models/notes.py:25 +msgid "active" +msgstr "actif" + +#: models/notes.py:28 +msgid "" +"Designates whether this note should be treated as active. Unselect this " +"instead of deleting notes." +msgstr "" +"Indique si la note est active. Désactiver cela plutôt que supprimer la note." + +#: models/notes.py:33 +msgid "display image" +msgstr "image affichée" + +#: models/notes.py:40 +msgid "notes" +msgstr "notes" + +#: models/notes.py:54 +msgid "one's note" +msgstr "note d'un utilisateur" + +#: models/notes.py:55 +msgid "users note" +msgstr "notes des utilisateurs" + +#: models/notes.py:69 +msgid "club note" +msgstr "note d'un club" + +#: models/notes.py:70 +msgid "clubs notes" +msgstr "notes des clubs" + +#: models/notes.py:83 models/transactions.py:31 models/transactions.py:64 +msgid "type" +msgstr "type" + +#: models/notes.py:89 +msgid "special note" +msgstr "note spéciale" + +#: models/notes.py:90 +msgid "special notes" +msgstr "notes spéciales" + +#: models/notes.py:98 models/transactions.py:18 +msgid "name" +msgstr "nom" + +#: models/notes.py:108 +msgid "alias" +msgstr "alias" + +#: models/notes.py:109 +msgid "aliases" +msgstr "alias" + +#: models/transactions.py:25 models/transactions.py:51 +#: models/transactions.py:54 +msgid "destination" +msgstr "destination" + +#: models/transactions.py:28 models/transactions.py:61 +msgid "amount" +msgstr "montant" + +#: models/transactions.py:36 +msgid "transaction template" +msgstr "modèle de transaction" + +#: models/transactions.py:37 +msgid "transaction templates" +msgstr "modèles de transaction" + +#: models/transactions.py:45 +msgid "source" +msgstr "source" + +#: models/transactions.py:58 +msgid "quantity" +msgstr "quantité" + +#: models/transactions.py:68 +msgid "description" +msgstr "description" + +#: models/transactions.py:71 +msgid "valid" +msgstr "valide" + +#: models/transactions.py:75 +msgid "transaction" +msgstr "transaction" + +#: models/transactions.py:76 +msgid "transactions" +msgstr "transactions" + +#: models/transactions.py:87 +msgid "membership transaction" +msgstr "transaction d'adhésion" + +#: models/transactions.py:88 +msgid "membership transactions" +msgstr "transactions d'adhésion" diff --git a/note/migrations/0001_initial.py b/note/migrations/0001_initial.py deleted file mode 100644 index b694bbf9..00000000 --- a/note/migrations/0001_initial.py +++ /dev/null @@ -1,63 +0,0 @@ -# Generated by Django 2.2.3 on 2019-07-16 07:17 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('contenttypes', '0002_remove_content_type_name'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='NoteClub', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('balance', models.DecimalField(decimal_places=2, default=0, help_text='money credited for this instance', max_digits=8, verbose_name='account balance')), - ('is_active', models.BooleanField(default=True, verbose_name='is active')), - ], - options={ - 'abstract': False, - }, - ), - migrations.CreateModel( - name='NoteSpec', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('balance', models.DecimalField(decimal_places=2, default=0, help_text='money credited for this instance', max_digits=8, verbose_name='account balance')), - ('is_active', models.BooleanField(default=True, verbose_name='is active')), - ('account_type', models.CharField(choices=[('CH', 'bank check'), ('CB', 'credit card'), ('VB', 'bank transfer'), ('CA', 'cash'), ('RB', 'refund')], max_length=2, unique=True)), - ], - options={ - 'abstract': False, - }, - ), - migrations.CreateModel( - name='NoteUser', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('balance', models.DecimalField(decimal_places=2, default=0, help_text='money credited for this instance', max_digits=8, verbose_name='account balance')), - ('is_active', models.BooleanField(default=True, verbose_name='is active')), - ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), - ], - options={ - 'verbose_name': "one's note", - 'verbose_name_plural': 'users note', - }, - ), - migrations.CreateModel( - name='Alias', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('alias', models.TextField(unique=True, verbose_name='alias')), - ('owner_id', models.PositiveIntegerField()), - ('owner_type', models.ForeignKey(limit_choices_to=models.Q(models.Q(('app_label', 'note'), ('model', 'NoteUser')), models.Q(('app_label', 'note'), ('model', 'NoteClub')), _connector='OR'), on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')), - ], - ), - ] diff --git a/note/models.py b/note/models.py deleted file mode 100644 index 8424355d..00000000 --- a/note/models.py +++ /dev/null @@ -1,89 +0,0 @@ -# -*- mode: python; coding: utf-8 -*- -# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay -# SPDX-License-Identifier: GPL-3.0-or-later - -from django.conf import settings -from django.contrib.auth.models import User -from django.db import models - -from django.utils.translation import gettext_lazy as _ -from django.contrib.contenttypes.models import ContentType -from django.contrib.contenttypes.fields import GenericForeignKey - - -class Alias(models.Model): - """ - A alias labels a Note instance, only for user and clubs - """ - alias = models.TextField( - "alias", - unique=True, - blank=False, - null=False, - ) - - # Owner can be linked to an user note or a club note - limit = models.Q(app_label="note", model="NoteUser") | models.Q(app_label="note", model="NoteClub") - owner_id = models.PositiveIntegerField() - owner_type = models.ForeignKey( - ContentType, - on_delete=models.CASCADE, - limit_choices_to=limit - ) - owner = GenericForeignKey('owner_type', 'owner_id') - - -class Note(models.Model): - """ - An abstract model, use to add transactions capabilities to a user - """ - balance = models.DecimalField( - verbose_name=_('account balance'), - help_text=_("money credited for this instance"), - decimal_places=2, # Limit to centimes - max_digits=8, # Limit to 999999,99€ - default=0, - ) - is_active = models.BooleanField( - default=True, - verbose_name=_('is active') - ) - - class Meta: - abstract = True - - -class NoteUser(Note): - """ - A Note associated to an User - """ - user = models.OneToOneField( - settings.AUTH_USER_MODEL, - on_delete=models.CASCADE, - ) - - class Meta: - verbose_name = _("one's note") - verbose_name_plural = _("users note") - - -class NoteSpec(Note): - """ - A Note for special account, where real money enter or leave the system - """ - account_type = models.CharField( - max_length=2, - choices=( - ("CH", _("bank check")), - ("CB", _("credit card")), - ("VB", _("bank transfer")), - ("CA", _("cash")), - ("RB", _("refund")), - ), - unique=True, - ) - - -class NoteClub(Note): - # to be added - pass diff --git a/note/models/__init__.py b/note/models/__init__.py new file mode 100644 index 00000000..b00572ce --- /dev/null +++ b/note/models/__init__.py @@ -0,0 +1,14 @@ +# -*- mode: python; coding: utf-8 -*- +# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from .notes import Alias, Note, NoteClub, NoteSpecial, NoteUser +from .transactions import MembershipTransaction, Transaction, \ + TransactionTemplate + +__all__ = [ + # Notes + 'Alias', 'Note', 'NoteClub', 'NoteSpecial', 'NoteUser', + # Transactions + 'MembershipTransaction', 'Transaction', 'TransactionTemplate', +] diff --git a/note/models/notes.py b/note/models/notes.py new file mode 100644 index 00000000..499678d9 --- /dev/null +++ b/note/models/notes.py @@ -0,0 +1,132 @@ +# -*- mode: python; coding: utf-8 -*- +# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from django.conf import settings +from django.db import models +from django.db.models.signals import post_save +from django.dispatch import receiver +from django.utils.translation import gettext_lazy as _ + +""" +Defines each note types +""" + + +class Note(models.Model): + """ + An model, use to add transactions capabilities + + We do not use an abstract model to simplify the transfer between two notes. + """ + balance = models.IntegerField( + verbose_name=_('account balance'), + help_text=_('in centimes, money credited for this instance'), + default=0, + ) + is_active = models.BooleanField( + _('active'), + default=True, + help_text=_( + 'Designates whether this note should be treated as active. ' + 'Unselect this instead of deleting notes.' + ), + ) + display_image = models.ImageField( + verbose_name=_('display image'), + max_length=255, + blank=True, + ) + + class Meta: + verbose_name = _("note") + verbose_name_plural = _("notes") + + +class NoteUser(Note): + """ + A Note associated to an User + """ + user = models.OneToOneField( + settings.AUTH_USER_MODEL, + on_delete=models.PROTECT, + related_name='note', + ) + + class Meta: + verbose_name = _("one's note") + verbose_name_plural = _("users note") + + +class NoteClub(Note): + """ + A Note associated to a Club + """ + club = models.OneToOneField( + 'member.Club', + on_delete=models.PROTECT, + related_name='note', + ) + + class Meta: + verbose_name = _("club note") + verbose_name_plural = _("clubs notes") + + +class NoteSpecial(Note): + """ + A Note for special account, where real money enter or leave the system + - bank check + - credit card + - bank transfer + - cash + - refund + """ + special_type = models.CharField( + verbose_name=_('type'), + max_length=255, + unique=True, + ) + + class Meta: + verbose_name = _("special note") + verbose_name_plural = _("special notes") + + +class Alias(models.Model): + """ + An alias labels a Note instance, only for user and clubs + """ + name = models.CharField( + verbose_name=_('name'), + max_length=255, + unique=True, + ) + note = models.ForeignKey( + Note, + on_delete=models.PROTECT, + ) + + class Meta: + verbose_name = _("alias") + verbose_name_plural = _("aliases") + + +@receiver(post_save, sender=settings.AUTH_USER_MODEL) +def save_user_note(instance, created, **_kwargs): + """ + Hook to create and save a note when an user is updated + """ + if created: + NoteUser.objects.create(user=instance) + instance.note.save() + + +@receiver(post_save, sender='member.Club') +def save_club_note(instance, created, **_kwargs): + """ + Hook to create and save a note when a club is updated + """ + if created: + NoteClub.objects.create(club=instance) + instance.note.save() diff --git a/note/models/transactions.py b/note/models/transactions.py new file mode 100644 index 00000000..753f4925 --- /dev/null +++ b/note/models/transactions.py @@ -0,0 +1,88 @@ +# -*- mode: python; coding: utf-8 -*- +# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from django.db import models +from django.utils import timezone +from django.utils.translation import gettext_lazy as _ + +from .notes import Note + +""" +Defines transactions +""" + + +class TransactionTemplate(models.Model): + name = models.CharField( + verbose_name=_('name'), + max_length=255, + ) + destination = models.ForeignKey( + Note, + on_delete=models.PROTECT, + related_name='+', # no reverse + verbose_name=_('destination'), + ) + amount = models.PositiveIntegerField( + verbose_name=_('amount'), + ) + template_type = models.CharField( + verbose_name=_('type'), + max_length=31 + ) + + class Meta: + verbose_name = _("transaction template") + verbose_name_plural = _("transaction templates") + + +class Transaction(models.Model): + source = models.ForeignKey( + Note, + on_delete=models.PROTECT, + related_name='+', + verbose_name=_('source'), + ) + destination = models.ForeignKey( + Note, + on_delete=models.PROTECT, + related_name='+', + verbose_name=_('destination'), + ) + datetime = models.DateTimeField( + verbose_name=_('destination'), + default=timezone.now, + ) + quantity = models.PositiveSmallIntegerField( + verbose_name=_('quantity'), + ) + amount = models.PositiveIntegerField( + verbose_name=_('amount'), + ) + transaction_type = models.CharField( + verbose_name=_('type'), + max_length=31, + ) + description = models.TextField( + verbose_name=_('description'), + ) + valid = models.NullBooleanField( + verbose_name=_('valid'), + ) + + class Meta: + verbose_name = _("transaction") + verbose_name_plural = _("transactions") + + +class MembershipTransaction(Transaction): + membership = models.OneToOneField( + 'member.Membership', + on_delete=models.PROTECT, + related_name='transaction', + ) + + class Meta: + verbose_name = _("membership transaction") + verbose_name_plural = _("membership transactions") diff --git a/note_kfet/settings.py b/note_kfet/settings.py index f3d13ccf..a4f837e9 100644 --- a/note_kfet/settings.py +++ b/note_kfet/settings.py @@ -44,9 +44,11 @@ INSTALLED_APPS = [ # External apps 'guardian', + 'reversion', # Note apps - 'adherents', + 'activity', + 'member', 'note', ] diff --git a/requirements.txt b/requirements.txt index 5f3c52ab..6201c93e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,4 @@ Pillow==6.1.0 pytz==2019.1 six==1.12.0 sqlparse==0.3.0 +django-reversion==3.0.3 \ No newline at end of file diff --git a/theme/locale/fr/LC_MESSAGES/django.po b/theme/locale/fr/LC_MESSAGES/django.po index 4163a3da..7b9adc7d 100644 --- a/theme/locale/fr/LC_MESSAGES/django.po +++ b/theme/locale/fr/LC_MESSAGES/django.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2019-07-08 13:45+0200\n" +"POT-Creation-Date: 2019-07-16 12:36+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" diff --git a/tox.ini b/tox.ini index 615a2701..dd2f3efe 100644 --- a/tox.ini +++ b/tox.ini @@ -8,6 +8,7 @@ deps = -r{toxinidir}/requirements.txt coverage commands = + ./manage.py makemigrations coverage run ./manage.py test {posargs} coverage report -m @@ -27,7 +28,7 @@ deps = pyflakes pylint commands = - flake8 note_* + flake8 activity member note pylint . [flake8] @@ -41,7 +42,8 @@ exclude = *.pyc, *.egg-info, .cache, - .eggs + .eggs, + *migrations* max-complexity = 10 import-order-style = google application-import-names = flake8