Implement a new type of note (see #45)

This commit is contained in:
Yohann D'ANELLO 2020-03-31 01:03:30 +02:00
parent c8854cf45d
commit c384ee02eb
14 changed files with 326 additions and 21 deletions

View File

@ -7,7 +7,8 @@ from crispy_forms.layout import Layout
from django import forms from django import forms
from django.contrib.auth.forms import UserCreationForm, AuthenticationForm from django.contrib.auth.forms import UserCreationForm, AuthenticationForm
from django.contrib.auth.models import User 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 permission.models import PermissionMask
from .models import Profile, Club, Membership from .models import Profile, Club, Membership
@ -47,6 +48,31 @@ class ClubForm(forms.ModelForm):
class Meta: class Meta:
model = Club model = Club
fields = '__all__' 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): class AddMembersForm(forms.Form):

View File

@ -8,13 +8,23 @@ from . import views
app_name = 'member' app_name = 'member'
urlpatterns = [ urlpatterns = [
path('signup/', views.UserCreateView.as_view(), name="signup"), path('signup/', views.UserCreateView.as_view(), name="signup"),
path('club/', views.ClubListView.as_view(), name="club_list"), path('club/', views.ClubListView.as_view(), name="club_list"),
path('club/<int:pk>/', views.ClubDetailView.as_view(), name="club_detail"), path('club/<int:pk>/', views.ClubDetailView.as_view(), name="club_detail"),
path('club/<int:pk>/add_member/', views.ClubAddMemberView.as_view(), name="club_add_member"), path('club/<int:pk>/add_member/', views.ClubAddMemberView.as_view(), name="club_add_member"),
path('club/create/', views.ClubCreateView.as_view(), name="club_create"), path('club/create/', views.ClubCreateView.as_view(), name="club_create"),
path('club/<int:pk>/update', views.ClubUpdateView.as_view(), name="club_update"), path('club/<int:pk>/update/', views.ClubUpdateView.as_view(), name="club_update"),
path('club/<int:pk>/update_pic', views.ClubPictureUpdateView.as_view(), name="club_update_pic"), path('club/<int:pk>/update_pic/', views.ClubPictureUpdateView.as_view(), name="club_update_pic"),
path('club/<int:pk>/aliases', views.ClubAliasView.as_view(), name="club_alias"), path('club/<int:pk>/aliases/', views.ClubAliasView.as_view(), name="club_alias"),
path('club/<int:pk>/linked_notes/', views.ClubLinkedNotesView.as_view(),
name="club_linked_note_list"),
path('club/<int:club_pk>/linked_notes/create/', views.ClubLinkedNoteCreateView.as_view(),
name="club_linked_note_create"),
path('club/<int:club_pk>/linked_notes/<int:pk>/', views.ClubLinkedNoteDetailView.as_view(),
name="club_linked_note_detail"),
path('club/<int:club_pk>/linked_notes/<int:pk>/update/', views.ClubLinkedNoteUpdateView.as_view(),
name="club_linked_note_update"),
path('user/', views.UserListView.as_view(), name="user_list"), path('user/', views.UserListView.as_view(), name="user_list"),
path('user/<int:pk>', views.UserDetailView.as_view(), name="user_detail"), path('user/<int:pk>', views.UserDetailView.as_view(), name="user_detail"),
path('user/<int:pk>/update', views.UserUpdateView.as_view(), name="user_update_profile"), path('user/<int:pk>/update', views.UserUpdateView.as_view(), name="user_update_profile"),

View File

@ -18,13 +18,14 @@ from django_tables2.views import SingleTableView
from rest_framework.authtoken.models import Token from rest_framework.authtoken.models import Token
from note.forms import ImageForm from note.forms import ImageForm
from note.models import Alias, NoteUser from note.models import Alias, NoteUser
from note.models.notes import NoteActivity
from note.models.transactions import Transaction 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 permission.backends import PermissionBackend
from .filters import UserFilter, UserFilterFormHelper from .filters import UserFilter, UserFilterFormHelper
from .forms import SignUpForm, ProfileForm, ClubForm, MembershipForm, MemberFormSet, FormSetHelper, \ from .forms import SignUpForm, ProfileForm, ClubForm, MembershipForm, MemberFormSet, FormSetHelper, \
CustomAuthenticationForm CustomAuthenticationForm, NoteActivityForm
from .models import Club, Membership from .models import Club, Membership
from .tables import ClubTable, UserTable from .tables import ClubTable, UserTable
@ -134,7 +135,8 @@ class UserDetailView(LoginRequiredMixin, DetailView):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
user = context['user_object'] user = context['user_object']
history_list = \ 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) context['history_list'] = HistoryTable(history_list)
club_list = \ club_list = \
Membership.objects.all().filter(user=user).only("club") Membership.objects.all().filter(user=user).only("club")
@ -179,8 +181,8 @@ class ProfileAliasView(LoginRequiredMixin, DetailView):
class PictureUpdateView(LoginRequiredMixin, FormMixin, DetailView): class PictureUpdateView(LoginRequiredMixin, FormMixin, DetailView):
form_class = ImageForm form_class = ImageForm
def get_context_data(self, *args, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(*args, **kwargs) context = super().get_context_data(**kwargs)
context['form'] = self.form_class(self.request.POST, self.request.FILES) context['form'] = self.form_class(self.request.POST, self.request.FILES)
return context return context
@ -290,8 +292,8 @@ class ClubDetailView(LoginRequiredMixin, DetailView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
club = context["club"] club = context["club"]
club_transactions = \ club_transactions = Transaction.objects.all().filter(Q(source=club.note) | Q(destination=club.note))\
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) context['history_list'] = HistoryTable(club_transactions)
club_member = \ club_member = \
Membership.objects.all().filter(club=club) Membership.objects.all().filter(club=club)
@ -317,7 +319,9 @@ class ClubUpdateView(LoginRequiredMixin, UpdateView):
context_object_name = "club" context_object_name = "club"
form_class = ClubForm form_class = ClubForm
template_name = "member/club_form.html" 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): class ClubPictureUpdateView(PictureUpdateView):
@ -361,3 +365,77 @@ class ClubAddMemberView(LoginRequiredMixin, CreateView):
def form_valid(self, formset): def form_valid(self, formset):
formset.save() formset.save()
return super().form_valid(formset) 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

View File

@ -6,7 +6,7 @@ from django.utils.translation import gettext_lazy as _
from polymorphic.admin import PolymorphicChildModelAdmin, \ from polymorphic.admin import PolymorphicChildModelAdmin, \
PolymorphicChildModelFilter, PolymorphicParentModelAdmin 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, \ from .models.transactions import Transaction, TemplateCategory, TransactionTemplate, \
RecurrentTransaction, MembershipTransaction RecurrentTransaction, MembershipTransaction
@ -24,7 +24,7 @@ class NoteAdmin(PolymorphicParentModelAdmin):
""" """
Parent regrouping all note types as children Parent regrouping all note types as children
""" """
child_models = (NoteClub, NoteSpecial, NoteUser) child_models = (NoteClub, NoteSpecial, NoteUser, NoteActivity)
list_filter = ( list_filter = (
PolymorphicChildModelFilter, PolymorphicChildModelFilter,
'is_active', 'is_active',
@ -74,6 +74,14 @@ class NoteSpecialAdmin(PolymorphicChildModelAdmin):
readonly_fields = ('balance',) readonly_fields = ('balance',)
@admin.register(NoteActivity)
class NoteActivityAdmin(PolymorphicChildModelAdmin):
"""
Child for a special note, see NoteAdmin
"""
readonly_fields = ('balance',)
@admin.register(NoteUser) @admin.register(NoteUser)
class NoteUserAdmin(PolymorphicChildModelAdmin): class NoteUserAdmin(PolymorphicChildModelAdmin):
""" """

View File

@ -4,7 +4,7 @@
from rest_framework import serializers from rest_framework import serializers
from rest_polymorphic.serializers import PolymorphicSerializer 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, \ from ..models.transactions import TransactionTemplate, Transaction, MembershipTransaction, TemplateCategory, \
RecurrentTransaction, SpecialTransaction RecurrentTransaction, SpecialTransaction
@ -69,6 +69,22 @@ class NoteUserSerializer(serializers.ModelSerializer):
return str(obj) 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): class AliasSerializer(serializers.ModelSerializer):
""" """
REST API Serializer for Aliases. REST API Serializer for Aliases.
@ -90,7 +106,8 @@ class NotePolymorphicSerializer(PolymorphicSerializer):
Note: NoteSerializer, Note: NoteSerializer,
NoteUser: NoteUserSerializer, NoteUser: NoteUserSerializer,
NoteClub: NoteClubSerializer, NoteClub: NoteClubSerializer,
NoteSpecial: NoteSpecialSerializer NoteSpecial: NoteSpecialSerializer,
NoteActivity: NoteActivitySerializer,
} }
class Meta: class Meta:

View File

@ -4,11 +4,13 @@
import unicodedata import unicodedata
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.validators import RegexValidator from django.core.validators import RegexValidator
from django.db import models from django.db import models
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from polymorphic.models import PolymorphicModel from polymorphic.models import PolymorphicModel
from member.models import Club
""" """
Defines each note types Defines each note types
@ -174,6 +176,40 @@ class NoteSpecial(Note):
return self.special_type 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): class Alias(models.Model):
""" """
points toward a :model:`note.NoteUser` or :model;`note.NoteClub` instance. points toward a :model:`note.NoteUser` or :model;`note.NoteClub` instance.

View File

@ -9,7 +9,7 @@ from django.utils.html import format_html
from django_tables2.utils import A from django_tables2.utils import A
from django.utils.translation import gettext_lazy as _ 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 .models.transactions import Transaction, TransactionTemplate
from .templatetags.pretty_money import pretty_money from .templatetags.pretty_money import pretty_money
@ -121,6 +121,24 @@ class AliasTable(tables.Table):
attrs={'td': {'class': 'col-sm-1'}}) 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 ButtonTable(tables.Table):
class Meta: class Meta:
attrs = { attrs = {

View File

@ -70,7 +70,7 @@ function refreshBalance() {
* @param fun For each found note with the matched alias `alias`, fun(note, alias) is called. * @param fun For each found note with the matched alias `alias`, fun(note, alias) is called.
*/ */
function getMatchedNotes(pattern, fun) { 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);
} }
/** /**

View File

@ -7,3 +7,12 @@
{% block profile_content %} {% block profile_content %}
{% include "member/club_tables.html" %} {% include "member/club_tables.html" %}
{% endblock %} {% endblock %}
{% block extrajavascript %}
<script>
function refreshHistory() {
$("#history_list").load("{% url 'member:club_detail' pk=object.pk %} #history_list");
$("#profile_infos").load("{% url 'member:club_detail' pk=object.pk %} #profile_infos");
}
</script>
{% endblock %}

View File

@ -34,7 +34,10 @@
<dd class="col-xl-6 text-truncate">{{ object.note.alias_set.all|join:", " }}</dd> <dd class="col-xl-6 text-truncate">{{ object.note.alias_set.all|join:", " }}</dd>
<dt class="col-xl-3">{% trans 'email'|capfirst %}</dt> <dt class="col-xl-3">{% trans 'email'|capfirst %}</dt>
<dd class="col-xl-9"><a href="mailto:{{ club.email }}">{{ club.email}}</a></dd> <dd class="col-xl-9"><a href="mailto:{{ club.email }}">{{ club.email }}</a></dd>
<dt class="col-xl-6"><a href="{% url 'member:club_linked_note_list' pk=club.pk %}">{% trans 'linked notes'|capfirst %}</a></dt>
<dd class="col-xl-6 text-truncate">{{ club.linked_notes.all|join:", " }}</dd>
</dl> </dl>
</div> </div>
<div class="card-footer text-center"> <div class="card-footer text-center">
@ -43,6 +46,6 @@
<a class="btn btn-primary btn-sm my-1" href="{% url 'member:club_add_member' pk=club.pk %}"> {% trans "Add roles" %}</a> <a class="btn btn-primary btn-sm my-1" href="{% url 'member:club_add_member' pk=club.pk %}"> {% trans "Add roles" %}</a>
{% url 'member:club_detail' club.pk as club_detail_url %} {% url 'member:club_detail' club.pk as club_detail_url %}
{%if request.get_full_path != club_detail_url %} {%if request.get_full_path != club_detail_url %}
<a class="btn btn-primary btn-sm my-1" href="{{ user_profile_url }}">{% trans 'View Profile' %}</a> <a class="btn btn-primary btn-sm my-1" href="{{ club_detail_url }}">{% trans 'View Profile' %}</a>
{% endif %} </div> {% endif %} </div>
</div> </div>

View File

@ -19,7 +19,7 @@
{% block extrajavascript %} {% block extrajavascript %}
<script> <script>
function refreshhistory() { function refreshHistory() {
$("#history_list").load("{% url 'member:user_detail' pk=object.pk %} #history_list"); $("#history_list").load("{% url 'member:user_detail' pk=object.pk %} #history_list");
$("#profile_infos").load("{% url 'member:user_detail' pk=object.pk %} #profile_infos"); $("#profile_infos").load("{% url 'member:user_detail' pk=object.pk %} #profile_infos");
} }

View File

@ -0,0 +1,57 @@
{% extends "member/noteowner_detail.html" %}
{% load i18n %}
{% load render_table from django_tables2 %}
{% load pretty_money %}
{% block profile_info %}
{% include "member/club_info.html" %}
{% endblock %}
{% block profile_content %}
<div id="activity_info" class="card bg-light shadow">
<div class="card-header text-center">
<h4>{% trans "Linked note:" %} {{ note.note_name }}</h4>
</div>
<div class="card-body" id="profile_infos">
<dl class="row">
<dt class="col-xl-6">{% trans 'attached club'|capfirst %}</dt>
<dd class="col-xl-6"><a href="{% url 'member:club_detail' pk=club.pk %}">{{ club }}</a></dd>
<dt class="col-xl-6">{% trans 'controller'|capfirst %}</dt>
<dd class="col-xl-6"><a href="{% url 'member:user_detail' pk=note.controller.pk %}">{{ note.controller }}</a></dd>
<dt class="col-xl-6">{% trans 'balance'|capfirst %}</dt>
<dd class="col-xl-6">{{ note.balance|pretty_money }}</dd>
</dl>
<div class="card-footer text-center">
<a class="btn btn-primary btn-sm my-1" href="{% url 'member:club_linked_note_update' club_pk=club.pk pk=note.pk %}"> {% trans "Edit" %}</a>
</div>
</div>
<div class="card">
<div class="card-header position-relative" id="historyListHeading">
<a class="btn btn-link stretched-link collapsed font-weight-bold"
data-toggle="collapse" data-target="#historyListCollapse"
aria-expanded="false" aria-controls="historyListCollapse">
<i class="fa fa-euro"></i> {% trans "Transaction history" %}
</a>
</div>
<div id="historyListCollapse" aria-labelledby="historyListHeading" data-parent="#accordionProfile">
<div id="history_list">
{% render_table history_list %}
</div>
</div>
</div>
{% endblock %}
{% block extrajavascript %}
<script>
function refreshHistory() {
$("#history_list").load("{% url 'member:club_linked_note_detail' club_pk=club.pk pk=note.pk %} #history_list");
$("#profile_infos").load("{% url 'member:club_detail' pk=club.pk%} #profile_infos");
}
</script>
{% endblock %}

View File

@ -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 %}
<form method="post">
{% csrf_token %}
{{ form|crispy }}
<button class="btn btn-primary" type="submit">{% trans "Submit" %}</button>
</form>
{% endblock %}

View File

@ -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 %}
<div class="row justify-content-center">
<div class="col-md-10">
<div class="card card-border shadow">
<div class="card-header text-center">
<h5> {% trans "linked notes of club"|capfirst %} {{ club.name }}</h5>
</div>
<div class="card-body px-0 py-0" id="club_table">
{% render_table table %}
</div>
</div>
<a href="{% url 'member:club_linked_note_create' club_pk=club.pk %}">
<button class="btn btn-primary btn-block">{% trans "Add new note" %}</button>
</a>
</div>
</div>
{% endblock %}