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 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):
"""
@ -21,3 +29,33 @@ class ClubForm(forms.ModelForm):
class Meta:
model = Club
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.dispatch import receiver
from django.utils.translation import gettext_lazy as _
from django.urls import reverse
from django.urls import reverse, reverse_lazy
class Profile(models.Model):
"""
@ -96,7 +96,7 @@ class Club(models.Model):
return self.name
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):

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('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('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.db.models import Q
from .models import Profile, Club
from .forms import ProfileForm, ClubForm
from django_tables2.views import SingleTableView
from .models import Profile, Club, Membership
from .forms import ProfileForm, ClubForm,MembershipForm, MemberFormSet,FormSetHelper
from .tables import ClubTable
from note.models.transactions import Transaction
class UserCreateView(CreateView):
@ -26,7 +30,7 @@ class UserCreateView(CreateView):
second_form = UserCreationForm
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
return context
@ -63,14 +67,46 @@ class ClubCreateView(LoginRequiredMixin,CreateView):
def form_valid(self,form):
return super().form_valid(form)
class ClubListView(LoginRequiredMixin,ListView):
class ClubListView(LoginRequiredMixin,SingleTableView):
"""
List TransactionsTemplates
List existing tables
"""
model = Club
form_class = ClubForm
table_class = ClubTable
class ClubDetailView(LoginRequiredMixin,DetailView):
"""
"""
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" %}
{% load static %}
{% load i18n %}
{% load render_table from django_tables2 %}
{% block content %}
<p><a class="btn btn-default" href="{% url 'member:club_list' %}">Clubs</a></p>
<h5>{{ object.name }}</h5>
{% endblock %}
<p><a class="btn btn-primary" href="{% url 'member:club_list' %}">Clubs</a></p>
<h3 class="text-center"> Club {{ object.name }}</h3>
<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 %}
{% block content %}
{% render_table object_list %}
{% render_table table %}
<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 %}