From c384ee02ebfe24e817655f316f901351739e4b40 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Tue, 31 Mar 2020 01:03:30 +0200 Subject: [PATCH] Implement a new type of note (see #45) --- apps/member/forms.py | 28 +++++++- apps/member/urls.py | 16 ++++- apps/member/views.py | 94 ++++++++++++++++++++++--- apps/note/admin.py | 12 +++- apps/note/api/serializers.py | 21 +++++- apps/note/models/notes.py | 36 ++++++++++ apps/note/tables.py | 20 +++++- static/js/base.js | 2 +- templates/member/club_detail.html | 9 +++ templates/member/club_info.html | 7 +- templates/member/noteowner_detail.html | 2 +- templates/note/noteactivity_detail.html | 57 +++++++++++++++ templates/note/noteactivity_form.html | 16 +++++ templates/note/noteactivity_list.html | 27 +++++++ 14 files changed, 326 insertions(+), 21 deletions(-) create mode 100644 templates/note/noteactivity_detail.html create mode 100644 templates/note/noteactivity_form.html create mode 100644 templates/note/noteactivity_list.html diff --git a/apps/member/forms.py b/apps/member/forms.py index 20f0acfe..d731c10c 100644 --- a/apps/member/forms.py +++ b/apps/member/forms.py @@ -7,7 +7,8 @@ from crispy_forms.layout import Layout from django import forms from django.contrib.auth.forms import UserCreationForm, AuthenticationForm from django.contrib.auth.models import User -from note_kfet.inputs import Autocomplete +from note.models.notes import NoteActivity +from note_kfet.inputs import Autocomplete, AmountInput from permission.models import PermissionMask from .models import Profile, Club, Membership @@ -47,6 +48,31 @@ class ClubForm(forms.ModelForm): class Meta: model = Club fields = '__all__' + widgets = { + "membership_fee": AmountInput() + } + + +class NoteActivityForm(forms.ModelForm): + class Meta: + model = NoteActivity + fields = ('note_name', 'club', 'controller', ) + widgets = { + "club": Autocomplete( + Club, + attrs={ + 'api_url': '/api/members/club/', + } + ), + "controller": Autocomplete( + User, + attrs={ + 'api_url': '/api/user/', + 'name_field': 'username', + 'placeholder': 'Nom ...', + } + ) + } class AddMembersForm(forms.Form): diff --git a/apps/member/urls.py b/apps/member/urls.py index 085a3fec..f1a5c2bd 100644 --- a/apps/member/urls.py +++ b/apps/member/urls.py @@ -8,13 +8,23 @@ from . import views app_name = 'member' urlpatterns = [ path('signup/', views.UserCreateView.as_view(), name="signup"), + path('club/', views.ClubListView.as_view(), name="club_list"), path('club//', views.ClubDetailView.as_view(), name="club_detail"), path('club//add_member/', views.ClubAddMemberView.as_view(), name="club_add_member"), path('club/create/', views.ClubCreateView.as_view(), name="club_create"), - path('club//update', views.ClubUpdateView.as_view(), name="club_update"), - path('club//update_pic', views.ClubPictureUpdateView.as_view(), name="club_update_pic"), - path('club//aliases', views.ClubAliasView.as_view(), name="club_alias"), + path('club//update/', views.ClubUpdateView.as_view(), name="club_update"), + path('club//update_pic/', views.ClubPictureUpdateView.as_view(), name="club_update_pic"), + path('club//aliases/', views.ClubAliasView.as_view(), name="club_alias"), + path('club//linked_notes/', views.ClubLinkedNotesView.as_view(), + name="club_linked_note_list"), + path('club//linked_notes/create/', views.ClubLinkedNoteCreateView.as_view(), + name="club_linked_note_create"), + path('club//linked_notes//', views.ClubLinkedNoteDetailView.as_view(), + name="club_linked_note_detail"), + path('club//linked_notes//update/', views.ClubLinkedNoteUpdateView.as_view(), + name="club_linked_note_update"), + path('user/', views.UserListView.as_view(), name="user_list"), path('user/', views.UserDetailView.as_view(), name="user_detail"), path('user//update', views.UserUpdateView.as_view(), name="user_update_profile"), diff --git a/apps/member/views.py b/apps/member/views.py index 8145b5e9..e8bde67d 100644 --- a/apps/member/views.py +++ b/apps/member/views.py @@ -18,13 +18,14 @@ from django_tables2.views import SingleTableView from rest_framework.authtoken.models import Token from note.forms import ImageForm from note.models import Alias, NoteUser +from note.models.notes import NoteActivity from note.models.transactions import Transaction -from note.tables import HistoryTable, AliasTable +from note.tables import HistoryTable, AliasTable, NoteActivityTable from permission.backends import PermissionBackend from .filters import UserFilter, UserFilterFormHelper from .forms import SignUpForm, ProfileForm, ClubForm, MembershipForm, MemberFormSet, FormSetHelper, \ - CustomAuthenticationForm + CustomAuthenticationForm, NoteActivityForm from .models import Club, Membership from .tables import ClubTable, UserTable @@ -134,7 +135,8 @@ class UserDetailView(LoginRequiredMixin, DetailView): context = super().get_context_data(**kwargs) user = context['user_object'] history_list = \ - Transaction.objects.all().filter(Q(source=user.note) | Q(destination=user.note)).order_by("-id") + Transaction.objects.all().filter(Q(source=user.note) | Q(destination=user.note)).order_by("-id")\ + .filter(PermissionBackend.filter_queryset(self.request.user, Transaction, "view")) context['history_list'] = HistoryTable(history_list) club_list = \ Membership.objects.all().filter(user=user).only("club") @@ -179,8 +181,8 @@ class ProfileAliasView(LoginRequiredMixin, DetailView): class PictureUpdateView(LoginRequiredMixin, FormMixin, DetailView): form_class = ImageForm - def get_context_data(self, *args, **kwargs): - context = super().get_context_data(*args, **kwargs) + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) context['form'] = self.form_class(self.request.POST, self.request.FILES) return context @@ -290,8 +292,8 @@ class ClubDetailView(LoginRequiredMixin, DetailView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) club = context["club"] - club_transactions = \ - Transaction.objects.all().filter(Q(source=club.note) | Q(destination=club.note)) + club_transactions = Transaction.objects.all().filter(Q(source=club.note) | Q(destination=club.note))\ + .filter(PermissionBackend.filter_queryset(self.request.user, Transaction, "view")).order_by('-id') context['history_list'] = HistoryTable(club_transactions) club_member = \ Membership.objects.all().filter(club=club) @@ -317,7 +319,9 @@ class ClubUpdateView(LoginRequiredMixin, UpdateView): context_object_name = "club" form_class = ClubForm template_name = "member/club_form.html" - success_url = reverse_lazy("member:club_detail") + + def get_success_url(self): + return reverse_lazy("member:club_detail", kwargs={"pk": self.object.pk}) class ClubPictureUpdateView(PictureUpdateView): @@ -361,3 +365,77 @@ class ClubAddMemberView(LoginRequiredMixin, CreateView): def form_valid(self, formset): formset.save() return super().form_valid(formset) + + +class ClubLinkedNotesView(LoginRequiredMixin, SingleTableView): + model = NoteActivity + table_class = NoteActivityTable + + def get_queryset(self): + return super().get_queryset().filter(club=self.get_object())\ + .filter(PermissionBackend.filter_queryset(self.request.user, NoteActivity, "view")) + + def get_object(self): + if hasattr(self, 'object'): + return self.object + self.object = Club.objects.get(pk=int(self.kwargs["pk"])) + return self.object + + def get_context_data(self, **kwargs): + ctx = super().get_context_data(**kwargs) + + ctx["object"] = ctx["club"] = self.get_object() + + return ctx + + +class ClubLinkedNoteCreateView(LoginRequiredMixin, CreateView): + model = NoteActivity + form_class = NoteActivityForm + + def get_context_data(self, **kwargs): + ctx = super().get_context_data(**kwargs) + + club = Club.objects.get(pk=self.kwargs["club_pk"]) + ctx["object"] = ctx["club"] = club + ctx["form"].fields["club"].initial = club + + return ctx + + def get_success_url(self): + self.object.refresh_from_db() + return reverse_lazy('member:club_linked_note_detail', + kwargs={"club_pk": self.object.club.pk, "pk": self.object.pk}) + + +class ClubLinkedNoteUpdateView(LoginRequiredMixin, UpdateView): + model = NoteActivity + form_class = NoteActivityForm + + def get_context_data(self, **kwargs): + ctx = super().get_context_data(**kwargs) + + ctx["club"] = Club.objects.get(pk=self.kwargs["club_pk"]) + + return ctx + + def get_success_url(self): + return reverse_lazy('member:club_linked_note_detail', + kwargs={"club_pk": self.object.club.pk, "pk": self.object.pk}) + + +class ClubLinkedNoteDetailView(LoginRequiredMixin, DetailView): + model = NoteActivity + + def get_context_data(self, **kwargs): + ctx = super().get_context_data(**kwargs) + + note = NoteActivity.objects.get(pk=self.kwargs["pk"]) + + transactions = Transaction.objects.all().filter(Q(source=note) | Q(destination=note))\ + .filter(PermissionBackend.filter_queryset(self.request.user, Transaction, "view")).order_by("-id") + ctx['history_list'] = HistoryTable(transactions) + ctx["note"] = note + ctx["club"] = note.club + + return ctx diff --git a/apps/note/admin.py b/apps/note/admin.py index 702d3350..f0dede17 100644 --- a/apps/note/admin.py +++ b/apps/note/admin.py @@ -6,7 +6,7 @@ from django.utils.translation import gettext_lazy as _ from polymorphic.admin import PolymorphicChildModelAdmin, \ PolymorphicChildModelFilter, PolymorphicParentModelAdmin -from .models.notes import Alias, Note, NoteClub, NoteSpecial, NoteUser +from .models.notes import Alias, Note, NoteClub, NoteSpecial, NoteUser, NoteActivity from .models.transactions import Transaction, TemplateCategory, TransactionTemplate, \ RecurrentTransaction, MembershipTransaction @@ -24,7 +24,7 @@ class NoteAdmin(PolymorphicParentModelAdmin): """ Parent regrouping all note types as children """ - child_models = (NoteClub, NoteSpecial, NoteUser) + child_models = (NoteClub, NoteSpecial, NoteUser, NoteActivity) list_filter = ( PolymorphicChildModelFilter, 'is_active', @@ -74,6 +74,14 @@ class NoteSpecialAdmin(PolymorphicChildModelAdmin): readonly_fields = ('balance',) +@admin.register(NoteActivity) +class NoteActivityAdmin(PolymorphicChildModelAdmin): + """ + Child for a special note, see NoteAdmin + """ + readonly_fields = ('balance',) + + @admin.register(NoteUser) class NoteUserAdmin(PolymorphicChildModelAdmin): """ diff --git a/apps/note/api/serializers.py b/apps/note/api/serializers.py index fbd12038..a445fef9 100644 --- a/apps/note/api/serializers.py +++ b/apps/note/api/serializers.py @@ -4,7 +4,7 @@ from rest_framework import serializers from rest_polymorphic.serializers import PolymorphicSerializer -from ..models.notes import Note, NoteClub, NoteSpecial, NoteUser, Alias +from ..models.notes import Note, NoteClub, NoteSpecial, NoteUser, Alias, NoteActivity from ..models.transactions import TransactionTemplate, Transaction, MembershipTransaction, TemplateCategory, \ RecurrentTransaction, SpecialTransaction @@ -69,6 +69,22 @@ class NoteUserSerializer(serializers.ModelSerializer): return str(obj) +class NoteActivitySerializer(serializers.ModelSerializer): + """ + REST API Serializer for User's notes. + The djangorestframework plugin will analyse the model `NoteActivity` and parse all fields in the API. + """ + name = serializers.SerializerMethodField() + + class Meta: + model = NoteActivity + fields = '__all__' + read_only_fields = ('note', 'user', ) + + def get_name(self, obj): + return str(obj) + + class AliasSerializer(serializers.ModelSerializer): """ REST API Serializer for Aliases. @@ -90,7 +106,8 @@ class NotePolymorphicSerializer(PolymorphicSerializer): Note: NoteSerializer, NoteUser: NoteUserSerializer, NoteClub: NoteClubSerializer, - NoteSpecial: NoteSpecialSerializer + NoteSpecial: NoteSpecialSerializer, + NoteActivity: NoteActivitySerializer, } class Meta: diff --git a/apps/note/models/notes.py b/apps/note/models/notes.py index 9282bde9..64ec524f 100644 --- a/apps/note/models/notes.py +++ b/apps/note/models/notes.py @@ -4,11 +4,13 @@ import unicodedata from django.conf import settings +from django.contrib.auth.models import User from django.core.exceptions import ValidationError from django.core.validators import RegexValidator from django.db import models from django.utils.translation import gettext_lazy as _ from polymorphic.models import PolymorphicModel +from member.models import Club """ Defines each note types @@ -174,6 +176,40 @@ class NoteSpecial(Note): return self.special_type +class NoteActivity(Note): + """ + A :model:`note.Note` for accounts that are not attached to a user neither to a club, + that only need to store and transfer money (notes for activities, departments, ...) + """ + + note_name = models.CharField( + verbose_name=_('name'), + max_length=255, + unique=True, + ) + + club = models.ForeignKey( + Club, + on_delete=models.PROTECT, + related_name="linked_notes", + verbose_name=_("club"), + ) + + controller = models.ForeignKey( + User, + on_delete=models.PROTECT, + related_name="+", + verbose_name=_("controller"), + ) + + class Meta: + verbose_name = _("common note") + verbose_name_plural = _("common notes") + + def __str__(self): + return self.note_name + + class Alias(models.Model): """ points toward a :model:`note.NoteUser` or :model;`note.NoteClub` instance. diff --git a/apps/note/tables.py b/apps/note/tables.py index 0d83e3cc..2aba4684 100644 --- a/apps/note/tables.py +++ b/apps/note/tables.py @@ -9,7 +9,7 @@ from django.utils.html import format_html from django_tables2.utils import A from django.utils.translation import gettext_lazy as _ -from .models.notes import Alias +from .models.notes import Alias, NoteActivity from .models.transactions import Transaction, TransactionTemplate from .templatetags.pretty_money import pretty_money @@ -121,6 +121,24 @@ class AliasTable(tables.Table): attrs={'td': {'class': 'col-sm-1'}}) +class NoteActivityTable(tables.Table): + note_name = tables.LinkColumn( + "member:club_linked_note_detail", + args=[A("club.pk"), A("pk")], + ) + + def render_balance(self, value): + return pretty_money(value) + + class Meta: + attrs = { + 'class': 'table table-condensed table-striped table-hover' + } + model = NoteActivity + fields = ('note_name', 'balance',) + template_name = 'django_tables2/bootstrap4.html' + + class ButtonTable(tables.Table): class Meta: attrs = { diff --git a/static/js/base.js b/static/js/base.js index 22d1366a..7febd3d6 100644 --- a/static/js/base.js +++ b/static/js/base.js @@ -70,7 +70,7 @@ function refreshBalance() { * @param fun For each found note with the matched alias `alias`, fun(note, alias) is called. */ function getMatchedNotes(pattern, fun) { - $.getJSON("/api/note/alias/?format=json&alias=" + pattern + "&search=user|club&ordering=normalized_name", fun); + $.getJSON("/api/note/alias/?format=json&alias=" + pattern + "&search=user|club|activity&ordering=normalized_name", fun); } /** diff --git a/templates/member/club_detail.html b/templates/member/club_detail.html index 979c0897..3ad29901 100644 --- a/templates/member/club_detail.html +++ b/templates/member/club_detail.html @@ -7,3 +7,12 @@ {% block profile_content %} {% include "member/club_tables.html" %} {% endblock %} + +{% block extrajavascript %} + +{% endblock %} diff --git a/templates/member/club_info.html b/templates/member/club_info.html index 1c8e8661..907914be 100644 --- a/templates/member/club_info.html +++ b/templates/member/club_info.html @@ -34,7 +34,10 @@
{{ object.note.alias_set.all|join:", " }}
{% trans 'email'|capfirst %}
-
{{ club.email}}
+
{{ club.email }}
+ +
{% trans 'linked notes'|capfirst %}
+
{{ club.linked_notes.all|join:", " }}
diff --git a/templates/member/noteowner_detail.html b/templates/member/noteowner_detail.html index ad329aee..fc781549 100644 --- a/templates/member/noteowner_detail.html +++ b/templates/member/noteowner_detail.html @@ -19,7 +19,7 @@ {% block extrajavascript %} +{% endblock %} diff --git a/templates/note/noteactivity_form.html b/templates/note/noteactivity_form.html new file mode 100644 index 00000000..5088c790 --- /dev/null +++ b/templates/note/noteactivity_form.html @@ -0,0 +1,16 @@ +{% extends "member/noteowner_detail.html" %} + +{% load i18n %} +{% load crispy_forms_tags %} + +{% block profile_info %} +{% include "member/club_info.html" %} +{% endblock %} + +{% block profile_content %} +
+ {% csrf_token %} + {{ form|crispy }} + +
+{% endblock %} diff --git a/templates/note/noteactivity_list.html b/templates/note/noteactivity_list.html new file mode 100644 index 00000000..b4d69536 --- /dev/null +++ b/templates/note/noteactivity_list.html @@ -0,0 +1,27 @@ +{% extends "member/noteowner_detail.html" %} + +{% load i18n %} +{% load render_table from django_tables2 %} + +{% block profile_info %} +{% include "member/club_info.html" %} +{% endblock %} + +{% block profile_content %} +
+
+
+
+
{% trans "linked notes of club"|capfirst %} {{ club.name }}
+
+
+ {% render_table table %} +
+
+ + + + +
+
+{% endblock %}