Merge branch 'import_nk15' into 'master'

Import nk15

See merge request bde/nk20!19
This commit is contained in:
Pierre-antoine Comby 2020-02-24 18:24:22 +01:00
commit 9039c6fba6
13 changed files with 335 additions and 51 deletions

27
apps/member/hashers.py Normal file
View File

@ -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$<NB>$<ENCODED>`
<NB> est un entier quelconque (symbolisant normalement un nombre d'itérations)
et <ENCODED> 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)

View File

View File

@ -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")

View File

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

View File

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

View File

@ -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.

View File

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

View File

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

View File

@ -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`.

View File

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

View File

@ -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 = (

View File

@ -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 %}
<form method="post" onsubmit="window.onbeforeunload=null">
{% csrf_token %}
@ -44,10 +44,10 @@
{# Tabs for button categories #}
<div class="card-header">
<ul class="nav nav-tabs nav-fill card-header-tabs">
{% for template_type in template_types %}
{% for category in categories %}
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#{{ template_type.grouper|slugify }}">
{{ template_type.grouper }}
<a class="nav-link" data-toggle="tab" href="#{{ category.grouper|slugify }}">
{{ category.grouper }}
</a>
</li>
{% endfor %}
@ -57,10 +57,10 @@
{# Tabs content #}
<div class="card-body">
<div class="tab-content">
{% for template_type in template_types %}
<div class="tab-pane" id="{{ template_type.grouper|slugify }}">
{% for category in categories %}
<div class="tab-pane" id="{{ category.grouper|slugify }}">
<div class="d-inline-flex flex-wrap justify-content-center">
{% for button in template_type.list %}
{% for button in category.list %}
<button class="btn btn-outline-dark rounded-0 flex-fill"
name="button" value="{{ button.name }}">
{{ button.name }} ({{ button.amount | pretty_money }})