diff --git a/apps/member/forms.py b/apps/member/forms.py index e024ee84f8e860955c85f36b3ff3f106ebf5366c..5dabef245827071880a7c485a81cc8210535b547 100644 --- a/apps/member/forms.py +++ b/apps/member/forms.py @@ -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", + ) + ) diff --git a/apps/member/urls.py b/apps/member/urls.py index 7b179b56f6a30da6af5e09554aff4890302c1c1f..39d3d8967ef099a65b7ddd06fdbb5a826b7e67f5 100644 --- a/apps/member/urls.py +++ b/apps/member/urls.py @@ -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") ] diff --git a/apps/member/views.py b/apps/member/views.py index 37c47212d68607d2003464f052d577ec08ca52a2..6408dc5fcfa6930855aabded87baa77106196a2d 100644 --- a/apps/member/views.py +++ b/apps/member/views.py @@ -10,8 +10,8 @@ from django.http import HttpResponseRedirect from django.contrib.auth.forms import UserCreationForm from django.urls import reverse_lazy -from .models import Profile, Club -from .forms import ProfileForm, ClubForm +from .models import Profile, Club, Membership +from .forms import ProfileForm, ClubForm,MembershipForm, MemberFormSet,FormSetHelper class UserCreateView(CreateView): """ @@ -24,7 +24,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 @@ -62,6 +62,25 @@ class ClubListView(LoginRequiredMixin,ListView): form_class = ClubForm class ClubDetailView(LoginRequiredMixin,DetailView): - """ - """ model = Club + +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) diff --git a/static/js/dynamic-formset.js b/static/js/dynamic-formset.js new file mode 100644 index 0000000000000000000000000000000000000000..95406a8cd6322d3d6a9c386cada2ee6361c7c25a --- /dev/null +++ b/static/js/dynamic-formset.js @@ -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: 'btn btn-primary', // CSS class applied to the add link + deleteCssClass: 'btn btn-danger h-50 my-auto', // 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); diff --git a/templates/member/add_members.html b/templates/member/add_members.html new file mode 100644 index 0000000000000000000000000000000000000000..9c50ac3042a2aa763d4ecbca671c2c004d8ff777 --- /dev/null +++ b/templates/member/add_members.html @@ -0,0 +1,23 @@ +{% 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="Save" 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 link', + deleteText: 'remove' + }); +</script> +{% endblock %} diff --git a/templates/member/club_detail.html b/templates/member/club_detail.html index 1e222e2646f9c1ee43ccedee87ab4af3b5170132..ddac22aa8e58cb16d0321aec26f438cc6bb31691 100644 --- a/templates/member/club_detail.html +++ b/templates/member/club_detail.html @@ -3,4 +3,6 @@ {% block content %} <p><a class="btn btn-default" href="{% url 'member:club_list' %}">Clubs</a></p> <h5>{{ object.name }}</h5> + +<a class="btn btn-default" href="{% url 'member:club_add_member' pk=object.pk %}"> Ajouter des membres </a> {% endblock %}