Merge branch 'app_member' into 'master'

App member

See merge request bde/nk20!5
This commit is contained in:
Pierre-antoine Comby 2019-08-15 19:58:32 +00:00
commit 6779b9dac2
9 changed files with 428 additions and 15 deletions

View File

@ -6,7 +6,15 @@ from django.contrib.auth.forms import UserChangeForm, UserCreationForm
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django import forms from django import forms
from .models import Profile, Club from .models import Profile, Club, Membership
from django.utils.translation import gettext_lazy as _
from crispy_forms.helper import FormHelper
from crispy_forms import layout, bootstrap
from crispy_forms.bootstrap import InlineField, FormActions, StrictButton, Div, Field
from crispy_forms.layout import Layout
class ProfileForm(forms.ModelForm): class ProfileForm(forms.ModelForm):
""" """
@ -21,3 +29,33 @@ class ClubForm(forms.ModelForm):
class Meta: class Meta:
model = Club model = Club
fields ='__all__' fields ='__all__'
class AddMembersForm(forms.Form):
class Meta:
fields = ('',)
class MembershipForm(forms.ModelForm):
class Meta:
model = Membership
fields = ('user','roles','date_start')
MemberFormSet = forms.modelformset_factory(Membership,
form=MembershipForm,
extra=2,
can_delete=True)
class FormSetHelper(FormHelper):
def __init__(self,*args,**kwargs):
super().__init__(*args,**kwargs)
self.form_tag = False
self.form_method = 'POST'
self.form_class='form-inline'
# self.template = 'bootstrap/table_inline_formset.html'
self.layout = Layout(
Div(
Div('user',css_class='col-sm-2'),
Div('roles',css_class='col-sm-2'),
Div('date_start',css_class='col-sm-2'),
css_class="row formset-row",
)
)

View File

@ -7,7 +7,7 @@ from django.db import models
from django.db.models.signals import post_save from django.db.models.signals import post_save
from django.dispatch import receiver from django.dispatch import receiver
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.urls import reverse from django.urls import reverse, reverse_lazy
class Profile(models.Model): class Profile(models.Model):
""" """
@ -96,7 +96,7 @@ class Club(models.Model):
return self.name return self.name
def get_absolute_url(self): def get_absolute_url(self):
return reverse('member:club_detail', args=(self.pk,)) return reverse_lazy('member:club_detail', args=(self.pk,))
class Role(models.Model): class Role(models.Model):

14
apps/member/tables.py Normal file
View File

@ -0,0 +1,14 @@
#!/usr/bin/env python
import django_tables2 as tables
from .models import Club
class ClubTable(tables.Table):
class Meta:
attrs = {'class':'table table-bordered table-condensed table-striped table-hover'}
model = Club
template_name = 'django_tables2/bootstrap.html'
fields= ('id','name','email')
row_attrs = {'class':'table-row',
'data-href': lambda record: record.pk }

View File

@ -13,6 +13,7 @@ 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/create/',views.ClubCreateView.as_view(),name="club_create"), path('club/create/',views.ClubCreateView.as_view(),name="club_create"),
path('user/<int:pk>',views.UserDetailView.as_view(),name="user_detail") path('user/<int:pk>',views.UserDetailView.as_view(),name="user_detail")
] ]

View File

@ -11,8 +11,12 @@ from django.contrib.auth.forms import UserCreationForm
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.db.models import Q from django.db.models import Q
from .models import Profile, Club from django_tables2.views import SingleTableView
from .forms import ProfileForm, ClubForm
from .models import Profile, Club, Membership
from .forms import ProfileForm, ClubForm,MembershipForm, MemberFormSet,FormSetHelper
from .tables import ClubTable
from note.models.transactions import Transaction from note.models.transactions import Transaction
class UserCreateView(CreateView): class UserCreateView(CreateView):
@ -26,7 +30,7 @@ class UserCreateView(CreateView):
second_form = UserCreationForm second_form = UserCreationForm
def get_context_data(self,**kwargs): def get_context_data(self,**kwargs):
context = super(SignUp,self).get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context["user_form"] = self.second_form context["user_form"] = self.second_form
return context return context
@ -63,14 +67,46 @@ class ClubCreateView(LoginRequiredMixin,CreateView):
def form_valid(self,form): def form_valid(self,form):
return super().form_valid(form) return super().form_valid(form)
class ClubListView(LoginRequiredMixin,ListView): class ClubListView(LoginRequiredMixin,SingleTableView):
""" """
List TransactionsTemplates List existing tables
""" """
model = Club model = Club
form_class = ClubForm table_class = ClubTable
class ClubDetailView(LoginRequiredMixin,DetailView): class ClubDetailView(LoginRequiredMixin,DetailView):
"""
"""
model = Club model = Club
context_object_name="club"
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))
context['history_list'] = club_transactions
club_member = \
Membership.objects.all().filter(club=club)
#TODO: consider only valid Membership
context['member_list'] = club_member
return context
class ClubAddMemberView(LoginRequiredMixin,CreateView):
model = Membership
form_class = MembershipForm
template_name = 'member/add_members.html'
def get_context_data(self,**kwargs):
context = super().get_context_data(**kwargs)
context['formset'] = MemberFormSet()
context['helper'] = FormSetHelper()
return context
def post(self,request,*args,**kwargs):
formset = MembershipFormset(request.POST)
if formset.is_valid():
return self.form_valid(formset)
else:
return self.form_invalid(formset)
def form_valid(self,formset):
formset.save()
return super().form_valid(formset)

View File

@ -0,0 +1,231 @@
/**
* jQuery Formset 1.3-pre
* @author Stanislaus Madueke (stan DOT madueke AT gmail DOT com)
* @requires jQuery 1.2.6 or later
*
* Copyright (c) 2009, Stanislaus Madueke
* All rights reserved.
*
* Licensed under the New BSD License
* See: http://www.opensource.org/licenses/bsd-license.php
*/
;(function($) {
$.fn.formset = function(opts)
{
var options = $.extend({}, $.fn.formset.defaults, opts),
flatExtraClasses = options.extraClasses.join(' '),
totalForms = $('#id_' + options.prefix + '-TOTAL_FORMS'),
maxForms = $('#id_' + options.prefix + '-MAX_NUM_FORMS'),
minForms = $('#id_' + options.prefix + '-MIN_NUM_FORMS'),
childElementSelector = 'input,select,textarea,label,div',
$$ = $(this),
applyExtraClasses = function(row, ndx) {
if (options.extraClasses) {
row.removeClass(flatExtraClasses);
row.addClass(options.extraClasses[ndx % options.extraClasses.length]);
}
},
updateElementIndex = function(elem, prefix, ndx) {
var idRegex = new RegExp(prefix + '-(\\d+|__prefix__)-'),
replacement = prefix + '-' + ndx + '-';
if (elem.attr("for")) elem.attr("for", elem.attr("for").replace(idRegex, replacement));
if (elem.attr('id')) elem.attr('id', elem.attr('id').replace(idRegex, replacement));
if (elem.attr('name')) elem.attr('name', elem.attr('name').replace(idRegex, replacement));
},
hasChildElements = function(row) {
return row.find(childElementSelector).length > 0;
},
showAddButton = function() {
return maxForms.length == 0 || // For Django versions pre 1.2
(maxForms.val() == '' || (maxForms.val() - totalForms.val() > 0));
},
/**
* Indicates whether delete link(s) can be displayed - when total forms > min forms
*/
showDeleteLinks = function() {
return minForms.length == 0 || // For Django versions pre 1.7
(minForms.val() == '' || (totalForms.val() - minForms.val() > 0));
},
insertDeleteLink = function(row) {
var delCssSelector = $.trim(options.deleteCssClass).replace(/\s+/g, '.'),
addCssSelector = $.trim(options.addCssClass).replace(/\s+/g, '.');
if (row.is('TR')) {
// If the forms are laid out in table rows, insert
// the remove button into the last table cell:
row.children(':last').append('<a class="' + options.deleteCssClass +'" href="javascript:void(0)">' + options.deleteText + '</a>');
} else if (row.is('UL') || row.is('OL')) {
// If they're laid out as an ordered/unordered list,
// insert an <li> after the last list item:
row.append('<li><a class="' + options.deleteCssClass + '" href="javascript:void(0)">' + options.deleteText +'</a></li>');
} else {
// Otherwise, just insert the remove button as the
// last child element of the form's container:
row.append('<a class="' + options.deleteCssClass + '" href="javascript:void(0)">' + options.deleteText +'</a>');
}
// Check if we're under the minimum number of forms - not to display delete link at rendering
if (!showDeleteLinks()){
row.find('a.' + delCssSelector).hide();
}
row.find('a.' + delCssSelector).click(function() {
var row = $(this).parents('.' + options.formCssClass),
del = row.find('input:hidden[id $= "-DELETE"]'),
buttonRow = row.siblings("a." + addCssSelector + ', .' + options.formCssClass + '-add'),
forms;
if (del.length) {
// We're dealing with an inline formset.
// Rather than remove this form from the DOM, we'll mark it as deleted
// and hide it, then let Django handle the deleting:
del.val('on');
row.hide();
forms = $('.' + options.formCssClass).not(':hidden');
} else {
row.remove();
// Update the TOTAL_FORMS count:
forms = $('.' + options.formCssClass).not('.formset-custom-template');
totalForms.val(forms.length);
}
for (var i=0, formCount=forms.length; i<formCount; i++) {
// Apply `extraClasses` to form rows so they're nicely alternating:
applyExtraClasses(forms.eq(i), i);
if (!del.length) {
// Also update names and IDs for all child controls (if this isn't
// a delete-able inline formset) so they remain in sequence:
forms.eq(i).find(childElementSelector).each(function() {
updateElementIndex($(this), options.prefix, i);
});
}
}
// Check if we've reached the minimum number of forms - hide all delete link(s)
if (!showDeleteLinks()){
$('a.' + delCssSelector).each(function(){$(this).hide();});
}
// Check if we need to show the add button:
if (buttonRow.is(':hidden') && showAddButton()) buttonRow.show();
// If a post-delete callback was provided, call it with the deleted form:
if (options.removed) options.removed(row);
return false;
});
};
$$.each(function(i) {
var row = $(this),
del = row.find('input:checkbox[id $= "-DELETE"]');
if (del.length) {
// If you specify "can_delete = True" when creating an inline formset,
// Django adds a checkbox to each form in the formset.
// Replace the default checkbox with a hidden field:
if (del.is(':checked')) {
// If an inline formset containing deleted forms fails validation, make sure
// we keep the forms hidden (thanks for the bug report and suggested fix Mike)
del.before('<input type="hidden" name="' + del.attr('name') +'" id="' + del.attr('id') +'" value="on" />');
row.hide();
} else {
del.before('<input type="hidden" name="' + del.attr('name') +'" id="' + del.attr('id') +'" />');
}
// Hide any labels associated with the DELETE checkbox:
$('label[for="' + del.attr('id') + '"]').hide();
del.remove();
}
if (hasChildElements(row)) {
row.addClass(options.formCssClass);
if (row.is(':visible')) {
insertDeleteLink(row);
applyExtraClasses(row, i);
}
}
});
if ($$.length) {
var hideAddButton = !showAddButton(),
addButton, template;
if (options.formTemplate) {
// If a form template was specified, we'll clone it to generate new form instances:
template = (options.formTemplate instanceof $) ? options.formTemplate : $(options.formTemplate);
template.removeAttr('id').addClass(options.formCssClass + ' formset-custom-template');
template.find(childElementSelector).each(function() {
updateElementIndex($(this), options.prefix, '__prefix__');
});
insertDeleteLink(template);
} else {
// Otherwise, use the last form in the formset; this works much better if you've got
// extra (>= 1) forms (thnaks to justhamade for pointing this out):
template = $('.' + options.formCssClass + ':last').clone(true).removeAttr('id');
template.find('input:hidden[id $= "-DELETE"]').remove();
// Clear all cloned fields, except those the user wants to keep (thanks to brunogola for the suggestion):
template.find(childElementSelector).not(options.keepFieldValues).each(function() {
var elem = $(this);
// If this is a checkbox or radiobutton, uncheck it.
// This fixes Issue 1, reported by Wilson.Andrew.J:
if (elem.is('input:checkbox') || elem.is('input:radio')) {
elem.attr('checked', false);
} else {
elem.val('');
}
});
}
// FIXME: Perhaps using $.data would be a better idea?
options.formTemplate = template;
if ($$.is('TR')) {
// If forms are laid out as table rows, insert the
// "add" button in a new table row:
var numCols = $$.eq(0).children().length, // This is a bit of an assumption :|
buttonRow = $('<tr><td colspan="' + numCols + '"><a class="' + options.addCssClass + '" href="javascript:void(0)">' + options.addText + '</a></tr>')
.addClass(options.formCssClass + '-add');
$$.parent().append(buttonRow);
if (hideAddButton) buttonRow.hide();
addButton = buttonRow.find('a');
} else {
// Otherwise, insert it immediately after the last form:
$$.filter(':last').after('<a class="' + options.addCssClass + '" href="javascript:void(0)">' + options.addText + '</a>');
addButton = $$.filter(':last').next();
if (hideAddButton) addButton.hide();
}
addButton.click(function() {
var formCount = parseInt(totalForms.val()),
row = options.formTemplate.clone(true).removeClass('formset-custom-template'),
buttonRow = $($(this).parents('tr.' + options.formCssClass + '-add').get(0) || this),
delCssSelector = $.trim(options.deleteCssClass).replace(/\s+/g, '.');
applyExtraClasses(row, formCount);
row.insertBefore(buttonRow).show();
row.find(childElementSelector).each(function() {
updateElementIndex($(this), options.prefix, formCount);
});
totalForms.val(formCount + 1);
// Check if we're above the minimum allowed number of forms -> show all delete link(s)
if (showDeleteLinks()){
$('a.' + delCssSelector).each(function(){$(this).show();});
}
// Check if we've exceeded the maximum allowed number of forms:
if (!showAddButton()) buttonRow.hide();
// If a post-add callback was supplied, call it with the added form:
if (options.added) options.added(row);
return false;
});
}
return $$;
};
/* Setup plugin defaults */
$.fn.formset.defaults = {
prefix: 'form', // The form prefix for your django formset
formTemplate: null, // The jQuery selection cloned to generate new form instances
addText: 'add another', // Text for the add link
deleteText: 'remove', // Text for the delete link
addCssClass: '', // CSS class applied to the add link
deleteCssClass: '', // CSS class applied to the delete link
formCssClass: 'dynamic-form', // CSS class applied to each form in a formset
extraClasses: [], // Additional CSS classes, which will be applied to each form in turn
keepFieldValues: '', // jQuery selector for fields whose values should be kept when the form is cloned
added: null, // Function called each time a new form is added
removed: null // Function called each time a form is deleted
};
})(jQuery);

View File

@ -0,0 +1,25 @@
{% extends "base.html" %}
{% load crispy_forms_tags %}
{% load static %}
{% block content %}
<form method="post" action="">
{% csrf_token %}
{% crispy formset helper %}
<div class="form-actions">
<input type="submit" name="submit" value="Add Members" class="btn btn-primary" id="submit-save">
</div>
</form>
<!-- Include formset plugin - including jQuery dependency -->
<script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
<script src="{% static 'js/dynamic-formset.js' %}"></script>
<script>
$('.formset-row').formset({
addText: 'add another', // Text for the add link
deleteText: 'remove', // Text for the delete link
addCssClass: 'btn btn-primary', // CSS class applied to the add link
deleteCssClass: 'btn btn-danger h-50 my-auto',
});
</script>
{% endblock %}

View File

@ -1,6 +1,61 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load static %} {% load static %}
{% load i18n %}
{% load render_table from django_tables2 %}
{% block content %} {% block content %}
<p><a class="btn btn-default" href="{% url 'member:club_list' %}">Clubs</a></p> <p><a class="btn btn-primary" href="{% url 'member:club_list' %}">Clubs</a></p>
<h5>{{ object.name }}</h5> <h3 class="text-center"> Club {{ object.name }}</h3>
{% endblock %} <dl>
<dt>{% trans 'Membership starts on' %}</dt>
<dd>{{ club.membership_start }}</dd>
<dt>{% trans 'Membership ends on' %}</dt>
<dd>{{ club.membership_end }}</dd>
<dt>{% trans 'Membership duration' %}</dt>
<dd>{{ club.membership_duration }}</dd>
<dt> Aliases </dt>
<dd>{{ club.note.aliases_set.all }}</dd>
<dt>{% trans 'balance' %}</dt>
<dd>{{ club.note.balance }}</dd>
</dl>
<div class="btn-group" role="group">
<a class="btn btn-primary" href="{% url 'member:club_add_member' pk=object.pk %}"> Ajouter des membres </a>
<a class="btn btn-primary" href="{% url 'member:club_add_member' pk=object.pk %}"> Modifier les informations </a>
<a class="btn btn-primary" href="{% url 'member:club_add_member' pk=object.pk %}"> Ajouter des roles </a>
</div>
<div class="accordion" id="accordionExample">
<div class="card">
<div class="card-header" id="headingOne">
<h5 class="mb-0">
<button class="btn btn-link" type="button" data-toggle="collapse" data-target="#collapseOne" aria-expanded="true" aria-controls="collapseOne">
<i class="fa fa-users"></i> Membres du club
</button>
</h5>
</div>
<div id="collapseOne" class="collapse show" aria-labelledby="headingOne" data-parent="#accordionExample">
<div class="card-body">
{% render_table member_list %}
</div>
</div>
</div>
<div class="card">
<div class="card-header" id="headingTwo">
<h5 class="mb-0">
<button class="btn btn-link collapsed" type="button" data-toggle="collapse" data-target="#collapseTwo" aria-expanded="false" aria-controls="collapseTwo">
<i class="fa fa-euro"></i> Historique des transactions
</button>
</h5>
</div>
<div id="collapseTwo" class="collapse" aria-labelledby="headingTwo" data-parent="#accordionExample">
<div class="card-body">
{% render_table history_list %}
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -2,7 +2,20 @@
{% load render_table from django_tables2 %} {% load render_table from django_tables2 %}
{% block content %} {% block content %}
{% render_table object_list %} {% render_table table %}
<a class="btn btn-primary" href="{% url 'member:club_create' %}">New Club</a> <a class="btn btn-primary" href="{% url 'member:club_create' %}">New Club</a>
{% endblock %}
{% block javascript %}
{{ block.super }}
<script type="text/javascript">
$(document).ready(function($) {
$(".table-row").click(function() {
window.document.location = $(this).data("href");
});
});
</script>
{% endblock %} {% endblock %}