diff --git a/README.md b/README.md index ad9e62d9e83e1d4a4da2621abf5a97955844d16d..5ae8a3967704fa1d128b6375f5990d51a3a943d4 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ On supposera pour la suite que vous utiliser debian/ubuntu sur un serveur tout n 1. Paquets nécessaires $ sudo apt install nginx python3 python3-pip python3-dev uwsgi - $ sudo apt install uwsgi-plugin-python3 python3-virtualenv git + $ sudo apt install uwsgi-plugin-python3 python3-venv git acl 2. Clonage du dépot @@ -29,8 +29,8 @@ On supposera pour la suite que vous utiliser debian/ubuntu sur un serveur tout n À la racine du projet: - $ virtualenv env - $ source /env/bin/activate + $ python3 -m venv env + $ source env/bin/activate (env)$ pip3 install -r requirements.txt (env)$ deactivate diff --git a/apps/member/forms.py b/apps/member/forms.py index 66844cf4f37b3c0ebedc9625ea9beb9158dd714c..abb35cd9cb2d4493e849ffcfd9315a5f14230c4a 100644 --- a/apps/member/forms.py +++ b/apps/member/forms.py @@ -14,6 +14,11 @@ from crispy_forms.layout import Layout class SignUpForm(UserCreationForm): + def __init__(self,*args,**kwargs): + super().__init__(*args,**kwargs) + self.fields['username'].widget.attrs.pop("autofocus", None) + self.fields['first_name'].widget.attrs.update({"autofocus":"autofocus"}) + class Meta: model = User fields = ['first_name', 'last_name', 'username', 'email'] diff --git a/apps/member/hashers.py b/apps/member/hashers.py new file mode 100644 index 0000000000000000000000000000000000000000..0c5d010b62f22fc45c1140866aad46df05ebae32 --- /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$<NB>$<ENCODED>` + où <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) diff --git a/apps/member/views.py b/apps/member/views.py index 6f982c64f6463a954de664c04640ee20dd7fd9db..6d82a6ccfc58c36b7f49a3bf8052c94edd486609 100644 --- a/apps/member/views.py +++ b/apps/member/views.py @@ -114,12 +114,13 @@ class UserDetailView(LoginRequiredMixin, DetailView): """ Affiche les informations sur un utilisateur, sa note, ses clubs... """ - model = Profile - context_object_name = "profile" + model = User + context_object_name = "user_object" + template_name = "member/profile_detail.html" def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - user = context['profile'].user + user = context['user_object'] history_list = \ Transaction.objects.all().filter(Q(source=user.note) | Q(destination=user.note)) context['history_list'] = HistoryTable(history_list) diff --git a/apps/note/admin.py b/apps/note/admin.py index 3a9721aeee3eaa7c30750ee4a31109a97fb6f5cb..52c1cc1752066221c27782a303e975e497d278cf 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 f853d3cbb0ec8c248162c65bacc033cabdde9fb9..c0e92bda3aa9c6d2d7be0193b3b70b0df3300c0b 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 e4fd344c1672031f4bf57279de23c6d965c32ffc..15cccf2e6d4c6de632d6f8228de6926edf754d96 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): @@ -31,8 +31,6 @@ class TransactionTemplateForm(forms.ModelForm): class TransactionForm(forms.ModelForm): def save(self, commit=True): - self.instance.transaction_type = 'transfert' - super().save(commit) class Meta: @@ -71,12 +69,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 7e6cc310e823e28eea3f8d8e40b8e1d98b87ac7c..081b31a737f6048c562773cbb67c51aca442b8bd 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 3b616f0e754abaee90a917ae0288d0410ce1de77..62811735a0bd7c46df48df4fa90758895ec84cf8 100644 --- a/apps/note/models/notes.py +++ b/apps/note/models/notes.py @@ -27,6 +27,12 @@ class Note(PolymorphicModel): help_text=_('in centimes, money credited for this instance'), default=0, ) + last_negative= models.DateTimeField( + verbose_name=_('last negative date'), + help_text=_('last time the balance was negative'), + null=True, + blank=True, + ) is_active = models.BooleanField( _('active'), default=True, @@ -64,7 +70,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 +94,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 +230,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 042faa161184e058b706d3e478eda816a255e9aa..49fc44b4bc44070cce44c72f55edb4dc90b1b9f4 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,22 @@ 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, + ) + 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 167ef4f0d4c4c8a45c251e7e60af2ebb622b98b1..75577a2e3ced39bbd28b165b5b5d3f6438d4552c 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 1a915d26171e44b59954e4093ebd9ac56d7de557..07a1c86cd89ed7b04ca987d1a5a20cb8da5653ff 100644 --- a/note_kfet/settings/base.py +++ b/note_kfet/settings/base.py @@ -121,6 +121,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/base.html b/templates/base.html index 4b5f9872d7b52a12eeeabf8ea7500833a522d8f3..6814bedfba9b3783255ffe7df3fd3b1a826889db 100644 --- a/templates/base.html +++ b/templates/base.html @@ -59,8 +59,8 @@ SPDX-License-Identifier: GPL-3.0-or-later <nav class="navbar navbar-expand-md navbar-light bg-light fixed-navbar shadow-sm"> <a class="navbar-brand" href="/">{{ request.site.name }}</a> <button class="navbar-toggler" type="button" data-toggle="collapse" - data-target="#navbarNavAltMarkup" - aria-controls="navbarNavAltMarkup" aria-expanded="false" + data-target="#navbarNavDropdown" + aria-controls="navbarNavDropdown" aria-expanded="false" aria-label="Toggle navigation"> <span class="navbar-toggler-icon"></span> </button> @@ -87,7 +87,7 @@ SPDX-License-Identifier: GPL-3.0-or-later </a> <div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdownMenuLink"> - <a class="dropdown-item" href="{% url 'member:user_detail' pk=user.profile.pk %}"> + <a class="dropdown-item" href="{% url 'member:user_detail' pk=user.pk %}"> <i class="fa fa-user"></i> Mon compte </a> <a class="dropdown-item" href="{% url 'logout' %}"> diff --git a/templates/member/profile_detail.html b/templates/member/profile_detail.html index 655f9893271d4c582a087d701b0ad53ae0c1ff62..1f60414b691a25f6a0a852469e1cfa9f72dc57ff 100644 --- a/templates/member/profile_detail.html +++ b/templates/member/profile_detail.html @@ -5,14 +5,14 @@ <div class="row mt-4"> <div class="col-md-3 mb-4"> <div class="card bg-light shadow"> - <img src="{{ object.note.display_image.url }}" class="card-img-top" alt=""> + <img src="{{ object.note.display_image }}" class="card-img-top" alt=""> <div class="card-body"> <dl class="row"> <dt class="col-xl-6">{% trans 'name'|capfirst %}, {% trans 'first name' %}</dt> - <dd class="col-xl-6">{{ object.user.last_name }} {{ object.user.first_name }}</dd> + <dd class="col-xl-6">{{ object.last_name }} {{ object.first_name }}</dd> <dt class="col-xl-6">{% trans 'username'|capfirst %}</dt> - <dd class="col-xl-6">{{ object.user.username }}</dd> + <dd class="col-xl-6">{{ user.username }}</dd> <dt class="col-xl-6">{% trans 'password'|capfirst %}</dt> <dd class="col-xl-6"> @@ -22,19 +22,19 @@ </dd> <dt class="col-xl-6">{% trans 'section'|capfirst %}</dt> - <dd class="col-xl-6">{{ object.section }}</dd> + <dd class="col-xl-6">{{ object.profile.section }}</dd> <dt class="col-xl-6">{% trans 'address'|capfirst %}</dt> - <dd class="col-xl-6">{{ object.address }}</dd> + <dd class="col-xl-6">{{ object.profile.address }}</dd> <dt class="col-xl-6">{% trans 'balance'|capfirst %}</dt> - <dd class="col-xl-6">{{ object.user.note.balance | pretty_money }}</dd> + <dd class="col-xl-6">{{ object.note.balance | pretty_money }}</dd> <dt class="col-xl-6">{% trans 'aliases'|capfirst %}</dt> - <dd class="col-xl-6">{{ object.user.note.alias_set.all|join:", " }}</dd> + <dd class="col-xl-6">{{ object.note.alias_set.all|join:", " }}</dd> </dl> - {% if object.user.pk == user.pk %} + {% if object.pk == user.pk %} <a class="small" href="{% url 'member:auth_token' %}">{% trans 'Manage auth token' %}</a> {% endif %} </div> diff --git a/templates/member/signup.html b/templates/member/signup.html index 4e6f79bcc219239830edcfab4426930e7d064513..e682bd9b8be5c6c327f8b17cdf4d7a7f07a3da96 100644 --- a/templates/member/signup.html +++ b/templates/member/signup.html @@ -10,7 +10,7 @@ {% csrf_token %} {{ form|crispy }} {{ profile_form|crispy }} - <button class="btn btn-link" type="submit"> + <button class="btn btn-success" type="submit"> {% trans "Sign Up" %} </button> </form> diff --git a/templates/note/conso_form.html b/templates/note/conso_form.html index b121ad54d12f970a66f9572194d3b45d8a3376c8..10b06589cfd4434aa6bc79100307901d017b1547 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 %} <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 }})