Merge branch 'master' into 'tresorerie'

# Conflicts:
#   apps/note/fixtures/initial.json
#   templates/base.html
This commit is contained in:
ynerant 2020-03-25 00:30:14 +01:00
commit 57a01c48a8
12 changed files with 167 additions and 60 deletions

View File

@ -5,6 +5,7 @@ from django.db.models import Q
from django_filters.rest_framework import DjangoFilterBackend from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.filters import OrderingFilter, SearchFilter from rest_framework.filters import OrderingFilter, SearchFilter
from api.viewsets import ReadProtectedModelViewSet, ReadOnlyProtectedModelViewSet from api.viewsets import ReadProtectedModelViewSet, ReadOnlyProtectedModelViewSet
from rest_framework import viewsets
from .serializers import NotePolymorphicSerializer, AliasSerializer, TemplateCategorySerializer, \ from .serializers import NotePolymorphicSerializer, AliasSerializer, TemplateCategorySerializer, \
TransactionTemplateSerializer, TransactionPolymorphicSerializer TransactionTemplateSerializer, TransactionPolymorphicSerializer
@ -81,7 +82,7 @@ class TemplateCategoryViewSet(ReadProtectedModelViewSet):
search_fields = ['$name', ] search_fields = ['$name', ]
class TransactionTemplateViewSet(ReadProtectedModelViewSet): class TransactionTemplateViewSet(viewsets.ModelViewSet):
""" """
REST API View set. REST API View set.
The djangorestframework plugin will get all `TransactionTemplate` objects, serialize it to JSON with the given serializer, The djangorestframework plugin will get all `TransactionTemplate` objects, serialize it to JSON with the given serializer,
@ -89,8 +90,9 @@ class TransactionTemplateViewSet(ReadProtectedModelViewSet):
""" """
queryset = TransactionTemplate.objects.all() queryset = TransactionTemplate.objects.all()
serializer_class = TransactionTemplateSerializer serializer_class = TransactionTemplateSerializer
filter_backends = [DjangoFilterBackend] filter_backends = [SearchFilter, DjangoFilterBackend]
filterset_fields = ['name', 'amount', 'display', 'category', ] filterset_fields = ['name', 'amount', 'display', 'category', ]
search_fields = ['$name', ]
class TransactionViewSet(ReadProtectedModelViewSet): class TransactionViewSet(ReadProtectedModelViewSet):

View File

@ -9,7 +9,7 @@ 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
from .models.transactions import Transaction from .models.transactions import Transaction, TransactionTemplate
from .templatetags.pretty_money import pretty_money from .templatetags.pretty_money import pretty_money
@ -57,6 +57,12 @@ class HistoryTable(tables.Table):
return "" if value else "" return "" if value else ""
# function delete_button(id) provided in template file
DELETE_TEMPLATE = """
<button id="{{ record.pk }}" class="btn btn-danger" onclick="delete_button(this.id)"> {{ delete_trans }}</button>
"""
class AliasTable(tables.Table): class AliasTable(tables.Table):
class Meta: class Meta:
attrs = { attrs = {
@ -69,9 +75,41 @@ class AliasTable(tables.Table):
show_header = False show_header = False
name = tables.Column(attrs={'td': {'class': 'text-center'}}) name = tables.Column(attrs={'td': {'class': 'text-center'}})
# delete = tables.TemplateColumn(template_code=delete_template,
# attrs={'td':{'class': 'col-sm-1'}})
delete = tables.LinkColumn('member:user_alias_delete', delete = tables.LinkColumn('member:user_alias_delete',
args=[A('pk')], args=[A('pk')],
attrs={ attrs={
'td': {'class': 'col-sm-2'}, 'td': {'class': 'col-sm-2'},
'a': {'class': 'btn btn-danger'}}, 'a': {'class': 'btn btn-danger'}},
text='delete', accessor='pk') text='delete', accessor='pk')
class ButtonTable(tables.Table):
class Meta:
attrs = {
'class':
'table table-bordered condensed table-hover'
}
row_attrs = {
'class': lambda record: 'table-row ' + 'table-success' if record.display else 'table-danger',
'id': lambda record: "row-" + str(record.pk),
'data-href': lambda record: record.pk
}
model = TransactionTemplate
edit = tables.LinkColumn('note:template_update',
args=[A('pk')],
attrs={'td': {'class': 'col-sm-1'},
'a': {'class': 'btn btn-primary'}},
text=_('edit'),
accessor='pk')
delete = tables.TemplateColumn(template_code=DELETE_TEMPLATE,
extra_context={"delete_trans": _('delete')},
attrs={'td': {'class': 'col-sm-1'}})
def render_amount(self, value):
return pretty_money(value)

View File

@ -8,7 +8,7 @@ from .models import Note
app_name = 'note' app_name = 'note'
urlpatterns = [ urlpatterns = [
path('transfer/', views.TransactionCreate.as_view(), name='transfer'), path('transfer/', views.TransactionCreateView.as_view(), name='transfer'),
path('buttons/create/', views.TransactionTemplateCreateView.as_view(), name='template_create'), path('buttons/create/', views.TransactionTemplateCreateView.as_view(), name='template_create'),
path('buttons/update/<int:pk>/', views.TransactionTemplateUpdateView.as_view(), name='template_update'), path('buttons/update/<int:pk>/', views.TransactionTemplateUpdateView.as_view(), name='template_update'),
path('buttons/', views.TransactionTemplateListView.as_view(), name='template_list'), path('buttons/', views.TransactionTemplateListView.as_view(), name='template_list'),

View File

@ -6,22 +6,25 @@ from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.db.models import Q from django.db.models import Q
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views.generic import CreateView, ListView, UpdateView from django.views.generic import CreateView, UpdateView
from django_tables2 import SingleTableView from django_tables2 import SingleTableView
from django.urls import reverse_lazy
from permission.backends import PermissionBackend from permission.backends import PermissionBackend
from .forms import TransactionTemplateForm from .forms import TransactionTemplateForm
from .models import Transaction, TransactionTemplate, Alias, RecurrentTransaction, NoteSpecial from .models import Transaction, TransactionTemplate, Alias, RecurrentTransaction, NoteSpecial
from .models.transactions import SpecialTransaction from .models.transactions import SpecialTransaction
from .tables import HistoryTable from .tables import HistoryTable, ButtonTable
class TransactionCreate(LoginRequiredMixin, SingleTableView): class TransactionCreateView(LoginRequiredMixin, SingleTableView):
""" """
Show transfer page View for the creation of Transaction between two note which are not :models:`transactions.RecurrentTransaction`.
e.g. for donation/transfer between people and clubs or for credit/debit with :models:`note.NoteSpecial`
""" """
template_name = "note/transaction_form.html" template_name = "note/transaction_form.html"
model = Transaction
# Transaction history table # Transaction history table
table_class = HistoryTable table_class = HistoryTable
table_pagination = {"per_page": 50} table_pagination = {"per_page": 50}
@ -46,13 +49,14 @@ class TransactionCreate(LoginRequiredMixin, SingleTableView):
class NoteAutocomplete(autocomplete.Select2QuerySetView): class NoteAutocomplete(autocomplete.Select2QuerySetView):
""" """
Auto complete note by aliases Auto complete note by aliases. Used in every search field for note
ex: :view:`ConsoView`, :view:`TransactionCreateView`
""" """
def get_queryset(self): def get_queryset(self):
""" """
Quand une personne cherche un alias, une requête est envoyée sur l'API dédiée à l'auto-complétion. When someone look for an :models:`note.Alias`, a query is sent to the dedicated API.
Cette fonction récupère la requête, et renvoie la liste filtrée des aliases. This function handles the result and return a filtered list of aliases.
""" """
# Un utilisateur non connecté n'a accès à aucune information # Un utilisateur non connecté n'a accès à aucune information
if not self.request.user.is_authenticated: if not self.request.user.is_authenticated:
@ -81,6 +85,10 @@ class NoteAutocomplete(autocomplete.Select2QuerySetView):
return qs return qs
def get_result_label(self, result): def get_result_label(self, result):
"""
Show the selected alias and the username associated
<Alias> (aka. <Username> )
"""
# Gère l'affichage de l'alias dans la recherche # Gère l'affichage de l'alias dans la recherche
res = result.name res = result.name
note_name = str(result.note) note_name = str(result.note)
@ -89,7 +97,9 @@ class NoteAutocomplete(autocomplete.Select2QuerySetView):
return res return res
def get_result_value(self, result): def get_result_value(self, result):
# Le résultat renvoyé doit être l'identifiant de la note, et non de l'alias """
The value used for the transactions will be the id of the Note.
"""
return str(result.note.pk) return str(result.note.pk)
@ -99,14 +109,15 @@ class TransactionTemplateCreateView(LoginRequiredMixin, CreateView):
""" """
model = TransactionTemplate model = TransactionTemplate
form_class = TransactionTemplateForm form_class = TransactionTemplateForm
success_url = reverse_lazy('note:template_list')
class TransactionTemplateListView(LoginRequiredMixin, ListView): class TransactionTemplateListView(LoginRequiredMixin, SingleTableView):
""" """
List TransactionsTemplates List TransactionsTemplates
""" """
model = TransactionTemplate model = TransactionTemplate
form_class = TransactionTemplateForm table_class = ButtonTable
class TransactionTemplateUpdateView(LoginRequiredMixin, UpdateView): class TransactionTemplateUpdateView(LoginRequiredMixin, UpdateView):
@ -114,11 +125,13 @@ class TransactionTemplateUpdateView(LoginRequiredMixin, UpdateView):
""" """
model = TransactionTemplate model = TransactionTemplate
form_class = TransactionTemplateForm form_class = TransactionTemplateForm
success_url = reverse_lazy('note:template_list')
class ConsoView(LoginRequiredMixin, SingleTableView): class ConsoView(LoginRequiredMixin, SingleTableView):
""" """
Consume The Magic View that make people pay their beer and burgers.
(Most of the magic happens in the dark world of Javascript see consos.js)
""" """
template_name = "note/conso_form.html" template_name = "note/conso_form.html"

View File

@ -650,4 +650,4 @@
] ]
} }
} }
] ]

View File

@ -2,6 +2,7 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from rest_framework.permissions import DjangoObjectPermissions from rest_framework.permissions import DjangoObjectPermissions
from .backends import PermissionBackend
SAFE_METHODS = ('HEAD', 'OPTIONS', ) SAFE_METHODS = ('HEAD', 'OPTIONS', )
@ -41,8 +42,8 @@ class StrongDjangoObjectPermissions(DjangoObjectPermissions):
user = request.user user = request.user
perms = self.get_required_object_permissions(request.method, model_cls) perms = self.get_required_object_permissions(request.method, model_cls)
# if not user.has_perms(perms, obj):
if not user.has_perms(perms, obj): if not all(PermissionBackend().has_perm(user, perm, obj) for perm in perms):
# If the user does not have permissions we need to determine if # If the user does not have permissions we need to determine if
# they have read permissions to see 403, or not, and simply see # they have read permissions to see 403, or not, and simply see
# a 404 response. # a 404 response.

1
apps/scripts Submodule

@ -0,0 +1 @@
Subproject commit b9fdced3c2ce34168b8f0d6004a20a69ca16e0de

View File

@ -128,7 +128,6 @@ PASSWORD_HASHERS = [
AUTHENTICATION_BACKENDS = ( AUTHENTICATION_BACKENDS = (
'permission.backends.PermissionBackend', # Custom role-based permission system 'permission.backends.PermissionBackend', # Custom role-based permission system
'cas.backends.CASBackend', # For CAS connections
) )
REST_FRAMEWORK = { REST_FRAMEWORK = {

View File

@ -79,6 +79,9 @@ SPDX-License-Identifier: GPL-3.0-or-later
<a class="nav-link" href="{% url 'note:consos' %}"><i class="fa fa-coffee"></i> {% trans 'Consumptions' %}</a> <a class="nav-link" href="{% url 'note:consos' %}"><i class="fa fa-coffee"></i> {% trans 'Consumptions' %}</a>
</li> </li>
{% endif %} {% endif %}
<li class="nav-item active">
<a class="nav-link" href="{% url 'note:transfer' %}"><i class="fa fa-exchange"></i>{% trans 'Transfer' %} </a>
</li>
{% if "member.club"|not_empty_model_list %} {% if "member.club"|not_empty_model_list %}
<li class="nav-item active"> <li class="nav-item active">
<a class="nav-link" href="{% url 'member:club_list' %}"><i class="fa fa-users"></i> {% trans 'Clubs' %}</a> <a class="nav-link" href="{% url 'member:club_list' %}"><i class="fa fa-users"></i> {% trans 'Clubs' %}</a>
@ -89,14 +92,6 @@ SPDX-License-Identifier: GPL-3.0-or-later
<a class="nav-link" href="#"><i class="fa fa-calendar"></i> {% trans 'Activities' %}</a> <a class="nav-link" href="#"><i class="fa fa-calendar"></i> {% trans 'Activities' %}</a>
</li> </li>
{% endif %} {% endif %}
{% if "note.transactiontemplate"|not_empty_model_change_list %}
<li class="nav-item active">
<a class="nav-link" href="{% url 'note:template_list' %}"><i class="fa fa-coffee"></i> {% trans 'Buttons' %}</a>
</li>
{% endif %}
<li class="nav-item active">
<a class="nav-link" href="{% url 'note:transfer' %}"><i class="fa fa-exchange"></i>{% trans 'Transfer' %} </a>
</li>
{% if "treasury.invoice"|not_empty_model_change_list %} {% if "treasury.invoice"|not_empty_model_change_list %}
<li class="nav-item active"> <li class="nav-item active">
<a class="nav-link" href="{% url 'treasury:invoice_list' %}"><i class="fa fa-money"></i>{% trans 'Treasury' %} </a> <a class="nav-link" href="{% url 'treasury:invoice_list' %}"><i class="fa fa-money"></i>{% trans 'Treasury' %} </a>

View File

@ -33,6 +33,16 @@ SPDX-License-Identifier: GPL-2.0-or-later
</div> </div>
<div class="row"> <div class="row">
<div class="col-xl-4" id="note_infos_div">
<div class="card border-success shadow mb-4">
<img src="/media/pic/default.png"
id="profile_pic" alt="" class="img-fluid rounded mx-auto d-block">
<div class="card-body text-center">
<span id="user_note"></span>
</div>
</div>
</div>
<div class="col-md-4" id="emitters_div" style="display: none;"> <div class="col-md-4" id="emitters_div" style="display: none;">
<div class="card border-success shadow mb-4"> <div class="card border-success shadow mb-4">
<div class="card-header"> <div class="card-header">
@ -50,16 +60,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
</div> </div>
</div> </div>
<div class="col-xl-4" id="note_infos_div">
<div class="card border-success shadow mb-4">
<img src="/media/pic/default.png"
id="profile_pic" alt="" class="img-fluid rounded mx-auto d-block">
<div class="card-body text-center">
<span id="user_note"></span>
</div>
</div>
</div>
{% if "note.notespecial"|not_empty_model_list %} {% if "note.notespecial"|not_empty_model_list %}
<div class="col-md-4" id="external_div" style="display: none;"> <div class="col-md-4" id="external_div" style="display: none;">
<div class="card border-success shadow mb-4"> <div class="card border-success shadow mb-4">
@ -170,8 +170,8 @@ SPDX-License-Identifier: GPL-2.0-or-later
}); });
$("#type_transfer").click(function() { $("#type_transfer").click(function() {
$("#emitters_div").show();
$("#external_div").hide(); $("#external_div").hide();
$("#emitters_div").show();
$("#dests_div").attr('class', 'col-md-4'); $("#dests_div").attr('class', 'col-md-4');
$("#dest_title").text("{% trans "Select receivers" %}"); $("#dest_title").text("{% trans "Select receivers" %}");
}); });

View File

@ -1,23 +1,79 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load pretty_money %} {% load pretty_money %}
{% load i18n %}
{% load render_table from django_tables2 %}
{% block content %} {% block content %}
<div class="row justify-content-center mb-4">
<table class="table"> <div class="col-md-10 text-center">
<tr> <h4>
<td>ID</td><td>Nom</td> {% trans "search button" %}
<td>Destinataire</td> </h4>
<td>Montant</td> <input class="form-control mx-auto w-25" type="text" onkeyup="search_field_moved();return(false);" id="search_field"/>
<td>Catégorie</td> <hr>
</tr> <a class="btn btn-primary text-center my-4" href="{% url 'note:template_create' %}">Créer un bouton</a>
{% for object in object_list %} </div>
<tr> </div>
<td>{{object.pk}}</td> <div class="row justify-content-center">
<td><a href="{{object.get_absolute_url}}">{{ object.name }}</a></td> <div class="col-md-10">
<td>{{ object.destination }}</td> <div class="card card-border shadow">
<td>{{ object.amount | pretty_money }}</td> <div class="card-header text-center">
<td>{{ object.category }}</td> <h5> {% trans "buttons listing "%}</h5>
</tr> </div>
{% endfor %} <div class="card-body px-0 py-0" id="buttons_table">
</table> {% render_table table %}
<a class="btn btn-primary" href="{% url 'note:template_create' %}">Créer un bouton</a> </div>
</div>
</div>
</div>
{% endblock %}
{% block extrajavascript %}
<script>
/* fonction appelée à la fin du timer */
function getInfo() {
var asked = $("#search_field").val();
/* on ne fait la requête que si on a au moins un caractère pour chercher */
var sel = $(".table-row");
if (asked.length >= 1) {
$.getJSON("/api/note/transaction/template/?format=json&search="+asked, function(buttons){
let selected_id = buttons.results.map((a => "#row-"+a.id));
console.log(selected_id.join());
$(".table-row,"+selected_id.join()).show();
$(".table-row").not(selected_id.join()).hide();
});
}else{
// show everything
$('table tr').show();
}
}
var timer;
var timer_on;
/* Fontion appelée quand le texte change (délenche le timer) */
function search_field_moved(secondfield) {
if (timer_on) { // Si le timer a déjà été lancé, on réinitialise le compteur.
clearTimeout(timer);
timer = setTimeout("getInfo(" + secondfield + ")", 300);
}
else { // Sinon, on le lance et on enregistre le fait qu'il tourne.
timer = setTimeout("getInfo(" + secondfield + ")", 300);
timer_on = true;
}
}
// on click of button "delete" , call the API
function delete_button(button_id){
$.ajax({
url:"/api/note/transaction/template/"+button_id+"/",
method:"DELETE",
headers: {"X-CSRFTOKEN": CSRF_TOKEN}
})
.done(function(){
addMsg('{% trans "button successfully deleted "%}','success');
$("#buttons_table").load("{% url 'note:template_list' %} #buttons_table");
})
.fail(function(){
addMsg(' {% trans "Unable to delete button "%} #' + button_id,'danger' )
});
}
</script>
{% endblock %} {% endblock %}

View File

@ -16,11 +16,13 @@ SPDX-License-Identifier: GPL-2.0-or-later
{% endblocktrans %} {% endblocktrans %}
</p> </p>
{% endif %} {% endif %}
{%url 'cas_login' as cas_url %}
{% if cas_url %}
<div class="alert alert-info"> <div class="alert alert-info">
Vous pouvez aussi vous connecter via l'authentification centralisée <a href="{% url 'cas_login' %}">en suivant ce lien.</a> {% trans "You can also register via the central authentification server " %}
<a href="{{ cas_url }}"> {% trans "using this link "%}</a>
</div> </div>
{%endif%}
<form action="{{ app_path }}" method="post" id="login-form">{% csrf_token %} <form action="{{ app_path }}" method="post" id="login-form">{% csrf_token %}
{{ form | crispy }} {{ form | crispy }}
<input type="submit" value="{% trans 'Log in' %}" class="btn btn-primary"> <input type="submit" value="{% trans 'Log in' %}" class="btn btn-primary">