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.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):

View File

@ -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/<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/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_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>/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>/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/<int:pk>', views.UserDetailView.as_view(), name="user_detail"),
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 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

View File

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

View File

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

View File

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

View File

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

View File

@ -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);
}
/**

View File

@ -7,3 +7,12 @@
{% block profile_content %}
{% include "member/club_tables.html" %}
{% 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>
<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>
</div>
<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>
{% url 'member:club_detail' club.pk as 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>
</div>

View File

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