diff --git a/apps/member/hashers.py b/apps/member/hashers.py new file mode 100644 index 00000000..0c5d010b --- /dev/null +++ b/apps/member/hashers.py @@ -0,0 +1,27 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +import hashlib + +from django.contrib.auth.hashers import PBKDF2PasswordHasher +from django.utils.crypto import constant_time_compare + + +class CustomNK15Hasher(PBKDF2PasswordHasher): + """ + Permet d'importer les mots de passe depuis la Note KFet 2015. + Si un hash de mot de passe est de la forme : + `custom_nk15$$` + où est un entier quelconque (symbolisant normalement un nombre d'itérations) + et le hash du mot de passe dans la Note Kfet 2015, + alors ce hasher va vérifier le mot de passe. + N'ayant pas la priorité (cf note_kfet/settings/base.py), le mot de passe sera + converti automatiquement avec l'algorithme PBKDF2. + """ + algorithm = "custom_nk15" + + def verify(self, password, encoded): + if '|' in encoded: + salt, db_hashed_pass = encoded.split('$')[2].split('|') + return constant_time_compare(hashlib.sha256((salt + password).encode("utf-8")).hexdigest(), db_hashed_pass) + return super().verify(password, encoded) diff --git a/apps/member/management/__init__.py b/apps/member/management/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/member/management/commands/__init__.py b/apps/member/management/commands/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/member/management/commands/import_nk15.py b/apps/member/management/commands/import_nk15.py new file mode 100644 index 00000000..4b8d57aa --- /dev/null +++ b/apps/member/management/commands/import_nk15.py @@ -0,0 +1,216 @@ +#!/usr/env/bin python3 + +from django.core.management.base import BaseCommand +from django.utils import timezone +import psycopg2 as pg +import psycopg2.extras as pge +from django.db import transaction + +import collections + +from django.core.exceptions import ValidationError +from django.db import IntegrityError +from django.contrib.auth.models import User +from note.models import Note, NoteSpecial, NoteUser, NoteClub +from note.models import Alias +from note.models import Transaction, TransactionTemplate, TemplateCategory, TransactionType +from member.models import Profile, Club + +""" +Script d'import de la nk15: +TODO: import aliases +TODO: import transactions +TODO: import adhesion +TODO: import activite +TODO: import + +""" +@transaction.atomic +def import_special(cur): + cur.execute("SELECT * FROM comptes WHERE idbde <0 ORDER BY idbde;") + map_idbde = dict() + for row in cur: + obj,created = NoteSpecial.objects.get_or_create(special_type = row["pseudo"], + balance = row["solde"], + is_active =True) + if created: + obj.save() + map_idbde[row["idbde"]] = obj.pk + + cur.execute("SELECT * FROM comptes WHERE idbde=0;") + res = cur.fetchone() + clubBde, c = Club.objects.get_or_create(pk = 1, + name = "Bde", + email = "bureau.bde@lists.crans.org", + membership_duration = "396 00:00:00", + membership_start = "213 00:00:00", + membership_end = "273 00:00:00", + membership_fee = 5, + ) + clubKfet, c = Club.objects.get_or_create(pk = 2, + name = "Kfet", + email = "tresorerie.bde@lists.crans.org", + membership_duration = "396 00:00:00", + membership_start = "213 00:00:00", + membership_end = "273 00:00:00", + membership_fee = 35, + ) + clubBde.save() + clubKfet.save() + clubBde.note.solde=res["solde"] + map_idbde[0] = clubKfet.note.pk + return map_idbde + + +@transaction.atomic +def import_comptes(cur,map_idbde): + cur.execute("SELECT * FROM comptes WHERE idbde > 0 ORDER BY idbde;") + pkclub = 3 + for row in cur: + if row["type"] == "personne": + #sanitize password + if row["passwd"] != "*|*": + passwd_nk15 = "$".join(["custom_nk15","1",row["passwd"]]) + else: + passwd_nk15 = '' + try: + obj_dict = { + "username": row["pseudo"], + "password": passwd_nk15, + "first_name": row["nom"], + "last_name": row["prenom"], + "email": row["mail"], + "is_active" : False, # temporary + } + user = User.objects.create(**obj_dict) + #sanitize duplicate aliases (nk12) + except ValidationError as e: + if e.code == 'same_alias': + obj_dict["username"] = row["pseudo"]+str(row["idbde"]) + user = User.objects.create(**obj_dict) + else: + raise(e) + else: + pass + obj_dict ={ + "phone_number": row["tel"], + "address": row["adresse"], + "paid": row["normalien"], + "user": user, + } + profile = Profile.objects.create(**obj_dict) + note = user.note + note.balance = row["solde"] + obj_list =[user, profile, note] + else: # club + obj_dict = { + "pk":pkclub, + "name": row["pseudo"], + "email": row["mail"], + "membership_duration": "396 00:00:00", + "membership_start": "213 00:00:00", + "membership_end": "273 00:00:00", + "membership_fee": 0, + } + club,c = Club.objects.get_or_create(**obj_dict) + pkclub +=1 + note = club.note + note.balance = row["solde"] + obj_list = [club,note] + for obj in obj_list: + obj.save() + map_idbde[row["idbde"]] = note.pk + return map_idbde + + +@transaction.atomic +def import_boutons(cur,map_idbde): + cur.execute("SELECT * FROM boutons;") + for row in cur: + cat, created = TemplateCategory.objects.get_or_create(name=row["categorie"]) + obj_dict = { + "pk": row["id"], + "name": row["label"], + "amount": row["montant"], + "destination_id": map_idbde[row["destinataire"]], + "category": cat, + "display" : row["affiche"], + "description": row["description"], + } + try: + with transaction.atomic(): # required for error management + button = TransactionTemplate.objects.create(**obj_dict) + except IntegrityError as e: + if "unique" in e.args[0]: + qs = Club.objects.filter(note__id=map_idbde[row["destinataire"]]).values('name') + note_name = qs[0]["name"] + obj_dict["name"] = ' '.join([obj_dict["name"],note_name]) + button = TransactionTemplate.objects.create(**obj_dict) + else: + raise(e) + if created: + cat.save() + button.save() + + +@transaction.atomic +def import_transaction(cur, map_idbde): + cur.execute("SELECT * FROM transactions;") + for row in cur: + obj_dict = { + "pk":row["id"], + } + +@transaction.atomic +def import_aliases(cur,map_idbde): + cur.execute("SELECT * FROM aliases ORDER by id") + for row in cur: + alias_name = row["alias"] + alias_name_good = (alias_name[:252]+'...') if len(alias_name) > 255 else alias_name + obj_dict = { + "note_id":map_idbde[row["idbde"]], + "name":alias_name_good, + } + try: + with transaction.atomic(): + alias = Alias.objects.create(**obj_dict) + except IntegrityError as e: + if "unique" in e.args[0]: + continue + else: + raise(e) + alias.save() + + +class Command(BaseCommand): + """ + Command for importing the database of NK15. + Need to be run by a user with a registered role in postgres for the database nk15. + """ + def add_arguments(self,parser): + parser.add_argument('-s', '--special', action = 'store_true') + parser.add_argument('-c', '--comptes', action = 'store_true') + parser.add_argument('-b', '--boutons', action = 'store_true') + parser.add_argument('-t', '--transactions', action = 'store_true') + parser.add_argument('-a', '--aliases', action = 'store_true') + + def handle(self, *args, **kwargs): + conn = pg.connect(database="nk15",user="nk15_user") + cur = conn.cursor(cursor_factory = pge.DictCursor) + + if kwargs["special"]: + map_idbde = import_special(cur) + print("Minimal setup created") + + if kwargs["comptes"]: + map_idbde = import_comptes(cur,map_idbde) + print("comptes table imported") + + if kwargs["boutons"]: + import_boutons(cur,map_idbde) + print("boutons table imported") + if kwargs["transactions"]: + import_transaction(cur) + if kwargs["aliases"]: + import_aliases(cur,map_idbde) + print("aliases imported") diff --git a/apps/note/admin.py b/apps/note/admin.py index 3a9721ae..52c1cc17 100644 --- a/apps/note/admin.py +++ b/apps/note/admin.py @@ -7,7 +7,8 @@ from polymorphic.admin import PolymorphicChildModelAdmin, \ PolymorphicChildModelFilter, PolymorphicParentModelAdmin from .models.notes import Alias, Note, NoteClub, NoteSpecial, NoteUser -from .models.transactions import Transaction, TransactionCategory, TransactionTemplate +from .models.transactions import Transaction, TemplateCategory, TransactionTemplate, \ + TemplateTransaction, MembershipTransaction class AliasInlines(admin.TabularInline): @@ -97,13 +98,14 @@ class NoteUserAdmin(PolymorphicChildModelAdmin): @admin.register(Transaction) -class TransactionAdmin(admin.ModelAdmin): +class TransactionAdmin(PolymorphicParentModelAdmin): """ Admin customisation for Transaction """ + child_models = (TemplateTransaction, MembershipTransaction) list_display = ('created_at', 'poly_source', 'poly_destination', - 'quantity', 'amount', 'transaction_type', 'valid') - list_filter = ('transaction_type', 'valid') + 'quantity', 'amount', 'valid') + list_filter = ('valid',) autocomplete_fields = ( 'source', 'destination', @@ -132,7 +134,7 @@ class TransactionAdmin(admin.ModelAdmin): """ if obj: # user is editing an existing object return 'created_at', 'source', 'destination', 'quantity',\ - 'amount', 'transaction_type' + 'amount' return [] @@ -141,8 +143,8 @@ class TransactionTemplateAdmin(admin.ModelAdmin): """ Admin customisation for TransactionTemplate """ - list_display = ('name', 'poly_destination', 'amount', 'template_type') - list_filter = ('template_type', ) + list_display = ('name', 'poly_destination', 'amount', 'category', 'display', ) + list_filter = ('category', 'display') autocomplete_fields = ('destination', ) def poly_destination(self, obj): @@ -154,8 +156,8 @@ class TransactionTemplateAdmin(admin.ModelAdmin): poly_destination.short_description = _('destination') -@admin.register(TransactionCategory) -class TransactionCategoryAdmin(admin.ModelAdmin): +@admin.register(TemplateCategory) +class TemplateCategoryAdmin(admin.ModelAdmin): """ Admin customisation for TransactionTemplate """ diff --git a/apps/note/fixtures/initial.json b/apps/note/fixtures/initial.json index f853d3cb..c0e92bda 100644 --- a/apps/note/fixtures/initial.json +++ b/apps/note/fixtures/initial.json @@ -162,59 +162,59 @@ } }, { - "model": "note.transactioncategory", + "model": "note.templatecategory", "pk": 1, "fields": { "name": "Soft" } }, { - "model": "note.transactioncategory", + "model": "note.templatecategory", "pk": 2, "fields": { "name": "Pulls" } }, { - "model": "note.transactioncategory", + "model": "note.templatecategory", "pk": 3, "fields": { "name": "Gala" } }, { - "model": "note.transactioncategory", + "model": "note.templatecategory", "pk": 4, "fields": { "name": "Clubs" } }, { - "model": "note.transactioncategory", + "model": "note.templatecategory", "pk": 5, "fields": { "name": "Bouffe" } }, { - "model": "note.transactioncategory", + "model": "note.templatecategory", "pk": 6, "fields": { "name": "BDA" } }, { - "model": "note.transactioncategory", + "model": "note.templatecategory", "pk": 7, "fields": { "name": "Autre" } }, { - "model": "note.transactioncategory", + "model": "note.templatecategory", "pk": 8, "fields": { "name": "Alcool" } } -] \ No newline at end of file +] diff --git a/apps/note/forms.py b/apps/note/forms.py index e4fd344c..2f49d88f 100644 --- a/apps/note/forms.py +++ b/apps/note/forms.py @@ -4,7 +4,7 @@ from dal import autocomplete from django import forms -from .models import Transaction, TransactionTemplate +from .models import Transaction, TransactionTemplate, TemplateTransaction class TransactionTemplateForm(forms.ModelForm): @@ -71,12 +71,13 @@ class ConsoForm(forms.ModelForm): name=self.data['button']).get() self.instance.destination = button.destination self.instance.amount = button.amount - self.instance.transaction_type = 'bouton' - self.instance.reason = button.name + self.instance.reason = '{} ({})'.format(button.name, button.category) + self.instance.name = button.name + self.instance.category = button.category super().save(commit) class Meta: - model = Transaction + model = TemplateTransaction fields = ('source', ) # Le champ d'utilisateur est remplacé par un champ d'auto-complétion. diff --git a/apps/note/models/__init__.py b/apps/note/models/__init__.py index 7e6cc310..081b31a7 100644 --- a/apps/note/models/__init__.py +++ b/apps/note/models/__init__.py @@ -3,11 +3,12 @@ from .notes import Alias, Note, NoteClub, NoteSpecial, NoteUser from .transactions import MembershipTransaction, Transaction, \ - TransactionCategory, TransactionTemplate + TemplateCategory, TransactionTemplate, TemplateTransaction __all__ = [ # Notes 'Alias', 'Note', 'NoteClub', 'NoteSpecial', 'NoteUser', # Transactions - 'MembershipTransaction', 'Transaction', 'TransactionCategory', 'TransactionTemplate', + 'MembershipTransaction', 'Transaction', 'TemplateCategory', 'TransactionTemplate', + 'TemplateTransaction', ] diff --git a/apps/note/models/notes.py b/apps/note/models/notes.py index 3b616f0e..3d929bc8 100644 --- a/apps/note/models/notes.py +++ b/apps/note/models/notes.py @@ -64,7 +64,8 @@ class Note(PolymorphicModel): if aliases.exists(): # Alias exists, so check if it is linked to this note if aliases.first().note != self: - raise ValidationError(_('This alias is already taken.')) + raise ValidationError(_('This alias is already taken.'), + code="same_alias") # Save note super().save(*args, **kwargs) @@ -87,7 +88,8 @@ class Note(PolymorphicModel): if aliases.exists(): # Alias exists, so check if it is linked to this note if aliases.first().note != self: - raise ValidationError(_('This alias is already taken.')) + raise ValidationError(_('This alias is already taken.'), + code="same_alias",) else: # Alias does not exist yet, so check if it can exist a = Alias(name=str(self)) @@ -222,16 +224,19 @@ class Alias(models.Model): def clean(self): normalized_name = Alias.normalize(self.name) if len(normalized_name) >= 255: - raise ValidationError(_('Alias too long.')) + raise ValidationError(_('Alias is too long.'), + code='alias_too_long') try: - if self != Alias.objects.get(normalized_name=normalized_name): - raise ValidationError( - _('An alias with a similar name ' - 'already exists.')) + sim_alias = Alias.objects.get(normalized_name=normalized_name) + if self != sim_alias: + raise ValidationError(_('An alias with a similar name already exists:'), + code="same_alias" + ) except Alias.DoesNotExist: pass def delete(self, using=None, keep_parents=False): if self.name == str(self.note): - raise ValidationError(_("You can't delete your main alias.")) + raise ValidationError(_("You can't delete your main alias."), + code="cant_delete_main_alias") return super().delete(using, keep_parents) diff --git a/apps/note/models/transactions.py b/apps/note/models/transactions.py index 042faa16..c99b5538 100644 --- a/apps/note/models/transactions.py +++ b/apps/note/models/transactions.py @@ -5,6 +5,7 @@ from django.db import models from django.utils import timezone from django.utils.translation import gettext_lazy as _ from django.urls import reverse +from polymorphic.models import PolymorphicModel from .notes import Note, NoteClub @@ -13,7 +14,7 @@ Defines transactions """ -class TransactionCategory(models.Model): +class TemplateCategory(models.Model): """ Defined a recurrent transaction category @@ -43,6 +44,7 @@ class TransactionTemplate(models.Model): verbose_name=_('name'), max_length=255, unique=True, + error_messages={'unique':_("A template with this name already exist")}, ) destination = models.ForeignKey( NoteClub, @@ -54,12 +56,19 @@ class TransactionTemplate(models.Model): verbose_name=_('amount'), help_text=_('in centimes'), ) - template_type = models.ForeignKey( - TransactionCategory, + category = models.ForeignKey( + TemplateCategory, on_delete=models.PROTECT, verbose_name=_('type'), max_length=31, ) + display = models.BooleanField( + default = True, + ) + description = models.CharField( + verbose_name=_('description'), + max_length=255, + ) class Meta: verbose_name = _("transaction template") @@ -69,7 +78,7 @@ class TransactionTemplate(models.Model): return reverse('note:template_update', args=(self.pk, )) -class Transaction(models.Model): +class Transaction(PolymorphicModel): """ General transaction between two :model:`note.Note` @@ -100,10 +109,6 @@ class Transaction(models.Model): default=1, ) amount = models.PositiveIntegerField(verbose_name=_('amount'), ) - transaction_type = models.CharField( - verbose_name=_('type'), - max_length=31, - ) reason = models.CharField( verbose_name=_('reason'), max_length=255, @@ -144,6 +149,26 @@ class Transaction(models.Model): return self.amount * self.quantity +class TemplateTransaction(Transaction): + """ + Special type of :model:`note.Transaction` associated to a :model:`note.TransactionTemplate`. + + """ + + template = models.ForeignKey( + TransactionTemplate, + null=True, + on_delete=models.SET_NULL, + ) + category = models.ForeignKey( + TemplateCategory, + on_delete=models.PROTECT, + ) + name = models.CharField( + max_length=255, + ) + + class MembershipTransaction(Transaction): """ Special type of :model:`note.Transaction` associated to a :model:`member.Membership`. diff --git a/apps/note/views.py b/apps/note/views.py index 167ef4f0..75577a2e 100644 --- a/apps/note/views.py +++ b/apps/note/views.py @@ -8,7 +8,7 @@ from django.urls import reverse from django.utils.translation import gettext_lazy as _ from django.views.generic import CreateView, ListView, UpdateView -from .models import Transaction, TransactionTemplate, Alias +from .models import Transaction, TransactionTemplate, Alias, TemplateTransaction from .forms import TransactionForm, TransactionTemplateForm, ConsoForm @@ -129,7 +129,7 @@ class ConsoView(LoginRequiredMixin, CreateView): """ Consume """ - model = Transaction + model = TemplateTransaction template_name = "note/conso_form.html" form_class = ConsoForm @@ -138,8 +138,8 @@ class ConsoView(LoginRequiredMixin, CreateView): Add some context variables in template such as page title """ context = super().get_context_data(**kwargs) - context['transaction_templates'] = TransactionTemplate.objects.all() \ - .order_by('template_type') + context['transaction_templates'] = TransactionTemplate.objects.filter(display=True) \ + .order_by('category') context['title'] = _("Consommations") # select2 compatibility @@ -152,3 +152,4 @@ class ConsoView(LoginRequiredMixin, CreateView): When clicking a button, reload the same page """ return reverse('note:consos') + diff --git a/note_kfet/settings/base.py b/note_kfet/settings/base.py index ab4453e4..a58d4817 100644 --- a/note_kfet/settings/base.py +++ b/note_kfet/settings/base.py @@ -117,6 +117,12 @@ AUTH_PASSWORD_VALIDATORS = [ }, ] +# Use our custom hasher in order to import NK15 passwords +PASSWORD_HASHERS = [ + 'django.contrib.auth.hashers.PBKDF2PasswordHasher', + 'member.hashers.CustomNK15Hasher', +] + # Django Guardian object permissions AUTHENTICATION_BACKENDS = ( diff --git a/templates/note/conso_form.html b/templates/note/conso_form.html index b121ad54..10b06589 100644 --- a/templates/note/conso_form.html +++ b/templates/note/conso_form.html @@ -7,7 +7,7 @@ {% block content %} {# Regroup buttons under categories #} - {% regroup transaction_templates by template_type as template_types %} + {% regroup transaction_templates by category as categories %}
{% csrf_token %} @@ -44,10 +44,10 @@ {# Tabs for button categories #}