mirror of
				https://gitlab.crans.org/bde/nk20
				synced 2025-10-31 15:50:03 +01:00 
			
		
		
		
	Merge branch 'master' into logging
This commit is contained in:
		| @@ -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 | ||||
|  | ||||
|   | ||||
| @@ -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'] | ||||
|   | ||||
							
								
								
									
										27
									
								
								apps/member/hashers.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								apps/member/hashers.py
									
									
									
									
									
										Normal 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>` | ||||
|     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) | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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 | ||||
|     """ | ||||
|   | ||||
| @@ -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" | ||||
|         } | ||||
|     } | ||||
| ] | ||||
| ] | ||||
|   | ||||
| @@ -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. | ||||
|   | ||||
| @@ -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', | ||||
| ] | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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`. | ||||
|   | ||||
| @@ -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') | ||||
|  | ||||
|   | ||||
| @@ -118,6 +118,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 = ( | ||||
|   | ||||
| @@ -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' %}"> | ||||
|   | ||||
| @@ -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> | ||||
|   | ||||
| @@ -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> | ||||
|   | ||||
| @@ -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 }}) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user