diff --git a/rowers/models.py b/rowers/models.py
index 685a687f..d25a36b4 100644
--- a/rowers/models.py
+++ b/rowers/models.py
@@ -6,6 +6,7 @@ from django import forms
from django.forms import ModelForm
from django.dispatch import receiver
from django.forms.widgets import SplitDateTimeWidget
+from django.forms.formsets import BaseFormSet
from datetimewidget.widgets import DateTimeWidget
import os
@@ -81,11 +82,115 @@ class Rower(models.Model):
return self.user.username
class FavoriteChart(models.Model):
- yparam1 = models.CharField(max_length=50)
- yparam2 = models.CharField(max_length=50)
- xparam = models.CharField(max_length=50)
- user = models.ForeignKey(Rower)
+ y1params = (
+ ('hr','Heart Rate'),
+ ('pace','Pace'),
+ ('spm','SPM'),
+ ('driveenergy','Work per Stroke'),
+ ('power','Power'),
+ ('drivelength','Drivelength'),
+ ('averageforce','Average Force'),
+ ('peakforce','Peak Force'),
+ ('forceratio','Average/Peak Force Ratio'),
+ ('drivespeed','Drive Speed'),
+ ('wash','Wash'),
+ ('slip','Slip'),
+ ('catch','Catch Angle'),
+ ('finish','Finish Angle'),
+ ('peakforceangle','Peak Force Angle')
+ )
+ y2params = (
+ ('hr','Heart Rate'),
+ ('spm','SPM'),
+ ('driveenergy','Work per Stroke'),
+ ('power','Power'),
+ ('drivelength','Drivelength'),
+ ('averageforce','Average Force'),
+ ('peakforce','Peak Force'),
+ ('forceratio','Average/Peak Force Ratio'),
+ ('drivespeed','Drive Speed'),
+ ('wash','Wash'),
+ ('slip','Slip'),
+ ('catch','Catch Angle'),
+ ('finish','Finish Angle'),
+ ('peakforceangle','Peak Force Angle'),
+ ('None','None')
+ )
+
+ xparams = (
+ ('time','Time'),
+ ('distance','Distance'),
+ ('hr','Heart Rate'),
+ ('spm','SPM'),
+ ('driveenergy','Work per Stroke'),
+ ('power','Power'),
+ ('drivelength','Drivelength'),
+ ('averageforce','Average Force'),
+ ('peakforce','Peak Force'),
+ ('forceratio','Average/Peak Force Ratio'),
+ ('drivespeed','Drive Speed'),
+ ('wash','Wash'),
+ ('slip','Slip'),
+ ('catch','Catch Angle'),
+ ('finish','Finish Angle'),
+ ('peakforceangle','Peak Force Angle'),
+ )
+
+ workouttypechoices = (
+ ('ote','Erg/SkiErg'),
+ ('otw','On The Water'),
+ ('both','both')
+ )
+
+ plottypes = (
+ ('line','Line Chart'),
+ ('scatter','Scatter Chart')
+ )
+
+ yparam1 = models.CharField(max_length=50,choices=y1params,verbose_name='Y1')
+ yparam2 = models.CharField(max_length=50,choices=y2params,verbose_name='Y2',default='None',blank=True)
+ xparam = models.CharField(max_length=50,choices=xparams,verbose_name='X')
+ plottype = models.CharField(max_length=50,choices=plottypes,
+ default='line',
+ verbose_name='Chart Type')
+ workouttype = models.CharField(max_length=50,choices=workouttypechoices,
+ default='both',
+ verbose_name='Workout Type')
+ user = models.ForeignKey(Rower)
+
+class FavoriteForm(ModelForm):
+ class Meta:
+ model = FavoriteChart
+ fields = ['xparam','yparam1','yparam2','plottype','workouttype']
+
+class BaseFavoriteFormSet(BaseFormSet):
+ def clean(self):
+ if any(self.errors):
+ return
+
+ for form in self.forms:
+ if form.cleaned_data:
+ xparam = form.cleaned_data['xparam']
+ yparam1 = form.cleaned_data['yparam1']
+ yparam2 = form.cleaned_data['yparam2']
+ plottype = form.cleaned_data['plottype']
+
+ if not xparam:
+ raise forms.ValidationError(
+ 'Must have x parameter.',
+ code='missing_xparam'
+ )
+
+ if not yparam1:
+ raise forms.ValidationError(
+ 'Must have Y1 parameter.',
+ code='missing_yparam1'
+ )
+
+ if not yparam2:
+ yparam2 = 'None'
+
class Workout(models.Model):
workouttypes = (
('water','On-water'),
diff --git a/rowers/templates/analysis.html b/rowers/templates/analysis.html
index bc78211c..4935c8a1 100644
--- a/rowers/templates/analysis.html
+++ b/rowers/templates/analysis.html
@@ -44,7 +44,7 @@
{% if user.rower.rowerplan == 'pro' %}
Power Histogram
{% else %}
- Power Histogram
+ Power Histogram
{% endif %}
@@ -73,4 +73,4 @@
-{% endblock %}
\ No newline at end of file
+{% endblock %}
diff --git a/rowers/templates/favoritecharts.html b/rowers/templates/favoritecharts.html
new file mode 100644
index 00000000..0ce6512d
--- /dev/null
+++ b/rowers/templates/favoritecharts.html
@@ -0,0 +1,40 @@
+{% extends "base.html" %}
+
+{% block title %}Change Favorite Charts{% endblock %}
+
+{% block content %}
+
+
+
+
+
+
+
+
+
+{% endblock %}
diff --git a/rowers/templates/flexchart3.html b/rowers/templates/flexchart3.html
index 67d9716b..3f6a2dd8 100644
--- a/rowers/templates/flexchart3.html
+++ b/rowers/templates/flexchart3.html
@@ -163,6 +163,38 @@
+{% if user.rower.rowerplan == 'pro' %}
+
+
+
+ {% if favoritenr > 0 %}
+
<
+ {% else %}
+
<
+ {% endif %}
+
+
+
+
+
+ {% if favoritenr < maxfav %}
+
>
+ {% else %}
+
>
+ {% endif %}
+
+
+{% endif %}
{% endblock %}
{% endlocaltime %}
diff --git a/rowers/templates/flexchart3otw.html b/rowers/templates/flexchart3otw.html
index 812361c5..d643a073 100644
--- a/rowers/templates/flexchart3otw.html
+++ b/rowers/templates/flexchart3otw.html
@@ -198,6 +198,38 @@
+{% if user.rower.rowerplan == 'pro' %}
+
+
+
+ {% if favoritenr > 0 %}
+
<
+ {% else %}
+
+ {% endif %}
+
+
+
+
+
+ {% if favoritenr < maxfav %}
+
>
+ {% else %}
+
+ {% endif %}
+
+
+{% endif %}
{% endblock %}
{% endlocaltime %}
diff --git a/rowers/urls.py b/rowers/urls.py
index 1fccd429..7c3d8e06 100644
--- a/rowers/urls.py
+++ b/rowers/urls.py
@@ -146,6 +146,7 @@ urlpatterns = [
url(r'^me/sporttracksauthorize/$',views.rower_sporttracks_authorize),
url(r'^me/sporttracksrefresh/$',views.rower_sporttracks_token_refresh),
url(r'^me/c2refresh/$',views.rower_c2_token_refresh),
+ url(r'^me/favoritecharts/$',views.rower_favoritecharts_view),
url(r'^email/send/$', views.sendmail),
url(r'^email/thankyou/$', TemplateView.as_view(template_name='thankyou.html'), name='thankyou'),
url(r'^email/$', TemplateView.as_view(template_name='email.html'), name='email'),
diff --git a/rowers/views.py b/rowers/views.py
index 4b3cf209..875457bf 100644
--- a/rowers/views.py
+++ b/rowers/views.py
@@ -2,6 +2,7 @@ import time
import operator
from django.views.generic.base import TemplateView
from django.db.models import Q
+from django.db import IntegrityError, transaction
from django.shortcuts import render
from django.http import HttpResponse, HttpResponseRedirect
from django.contrib.auth import authenticate, login, logout
@@ -16,8 +17,10 @@ from django.core.mail import send_mail, BadHeaderError
from rowers.forms import EmailForm, RegistrationForm, RegistrationFormTermsOfService,RegistrationFormUniqueEmail,CNsummaryForm,UpdateWindForm,UpdateStreamForm
from rowers.forms import PredictedPieceForm,DateRangeForm,DeltaDaysForm
from rowers.forms import SummaryStringForm,IntervalUpdateForm,StrokeDataForm
-from rowers.models import Workout, User, Rower, WorkoutForm
+from rowers.models import Workout, User, Rower, WorkoutForm,FavoriteChart
from rowers.models import RowerPowerForm,RowerForm,GraphImage,AdvancedWorkoutForm
+from rowers.models import FavoriteForm,BaseFavoriteFormSet
+from django.forms.formsets import formset_factory
import StringIO
from django.contrib.auth.decorators import login_required,user_passes_test
from time import strftime,strptime,mktime,time,daylight
@@ -1446,7 +1449,7 @@ def histo(request,theuser=0,
promember=1
if not promember:
- return HttpResponseRedirect("/rowers/about/")
+ return HttpResponseRedirect("/rowers/promembership/")
# get all indoor rows of in date range
@@ -1909,7 +1912,7 @@ def workouts_view(request,message='',successmessage='',
except Rower.DoesNotExist:
return HttpResponse("User has no rower instance")
-@user_passes_test(promember,login_url="/login")
+@user_passes_test(promember,login_url="/",redirect_field_name=None)
def workout_comparison_list(request,id=0,message='',successmessage='',
startdatestring="",enddatestring="",
startdate=timezone.now()-datetime.timedelta(days=365),
@@ -2023,7 +2026,7 @@ def workout_view(request,id=0):
return HttpResponse("Workout doesn't exist")
-@user_passes_test(promember,login_url="/login")
+@user_passes_test(promember,login_url="/",redirect_field_name=None)
def workout_undo_smoothenpace_view(request,id=0,message="",successmessage=""):
row = Workout.objects.get(id=id)
if (checkworkoutuser(request.user,row)==False):
@@ -2050,7 +2053,7 @@ def workout_undo_smoothenpace_view(request,id=0,message="",successmessage=""):
-@user_passes_test(promember,login_url="/login")
+@user_passes_test(promember,login_url="/",redirect_field_name=None)
def workout_smoothenpace_view(request,id=0,message="",successmessage=""):
row = Workout.objects.get(id=id)
if (checkworkoutuser(request.user,row)==False):
@@ -2085,7 +2088,7 @@ def workout_smoothenpace_view(request,id=0,message="",successmessage=""):
return HttpResponseRedirect(url)
-@user_passes_test(promember,login_url="/login")
+@user_passes_test(promember,login_url="/",redirect_field_name=None)
def workout_crewnerd_summary_view(request,id=0,message="",successmessage=""):
row = Workout.objects.get(id=id)
if request.method == 'POST':
@@ -2129,7 +2132,7 @@ def workout_crewnerd_summary_view(request,id=0,message="",successmessage=""):
{'form':form,
'id':row.id})
-@user_passes_test(promember,login_url="/login")
+@user_passes_test(promember,login_url="/",redirect_field_name=None)
def workout_downloadwind_view(request,id=0,message="",successmessage=""):
row = Workout.objects.get(id=id)
f1 = row.csvfilename
@@ -2186,7 +2189,7 @@ def workout_downloadwind_view(request,id=0,message="",successmessage=""):
return response
-@user_passes_test(promember,login_url="/login")
+@user_passes_test(promember,login_url="/",redirect_field_name=None)
def workout_wind_view(request,id=0,message="",successmessage=""):
row = Workout.objects.get(id=id)
if (checkworkoutuser(request.user,row)==False):
@@ -2282,7 +2285,7 @@ def workout_wind_view(request,id=0,message="",successmessage=""):
'gmapdiv':gmdiv})
-@user_passes_test(promember,login_url="/login")
+@user_passes_test(promember,login_url="/",redirect_field_name=None)
def workout_stream_view(request,id=0,message="",successmessage=""):
row = Workout.objects.get(id=id)
if (checkworkoutuser(request.user,row)==False):
@@ -2344,7 +2347,7 @@ def workout_stream_view(request,id=0,message="",successmessage=""):
'the_div':div})
-@user_passes_test(promember, login_url="/login")
+@user_passes_test(promember, login_url="/",redirect_field_name=None)
def workout_otwsetpower_view(request,id=0,message="",successmessage=""):
row = Workout.objects.get(id=id)
if (checkworkoutuser(request.user,row)==False):
@@ -2474,7 +2477,7 @@ def workout_geeky_view(request,id=0,message="",successmessage=""):
'interactiveplot':script,
'the_div':div})
-#@user_passes_test(promember,login_url="/login")
+#@user_passes_test(promember,login_url="/",redirect_field_name=None)
@login_required()
def workout_advanced_view(request,id=0,message="",successmessage=""):
row = Workout.objects.get(id=id)
@@ -2573,27 +2576,29 @@ def workout_comparison_view2(request,id1=0,id2=0,xparam='distance',
-def workout_flexchart3_view(request,id=0,#*args,**kwargs):
- xparam='distance',yparam1='pace',
- yparam2='hr',plottype='line',
- promember=0):
+def workout_flexchart3_view(request,*args,**kwargs):
+# xparam='distance',yparam1='pace',
+# yparam2='hr',plottype='line',
+# promember=0):
-# print args
-# try:
-# id = args[0]
-# except:
-# pass
-# if 'xparam' in kwargs:
-# print "found it"
- if request.method == 'POST':
- workstrokesonly = request.POST['workstrokesonly']
- if workstrokesonly == 'True':
- workstrokesonly = True
- else:
- workstrokesonly = False
+
+ try:
+ id = kwargs['id']
+ except KeyError:
+ return HttpResponse("Invalid workout number")
+
+ if 'promember' in kwargs:
+ promember = kwargs['promember']
else:
- workstrokesonly = False
+ promember = 0
+
+ try:
+ favoritenr = int(request.GET['favoritechart'])
+ except:
+ favoritenr = 0
+
+
row = Workout.objects.get(id=id)
promember=0
@@ -2606,6 +2611,66 @@ def workout_flexchart3_view(request,id=0,#*args,**kwargs):
if request.user == row.user.user:
mayedit=1
+
+ workouttype = 'ote'
+ if row.workouttype == 'water':
+ workouttype = 'otw'
+
+
+ favorites = FavoriteChart.objects.filter(user=r,
+ workouttype__in=[workouttype,'both']).order_by("id")
+ maxfav = len(favorites)-1
+
+ if 'xparam' in kwargs:
+ xparam = kwargs['xparam']
+ else:
+ if favorites:
+ xparam = favorites[favoritenr].xparam
+ else:
+ xparam = 'distance'
+
+ if 'yparam1' in kwargs:
+ yparam1 = kwargs['yparam1']
+ else:
+ if favorites:
+ yparam1 = favorites[favoritenr].yparam1
+ else:
+ yparam1 = 'pace'
+
+ if 'yparam2' in kwargs:
+ yparam2 = kwargs['yparam2']
+ if yparam2 == '':
+ yparam2 = 'None'
+ else:
+ if favorites:
+ yparam2 = favorites[favoritenr].yparam2
+ if yparam2 == '':
+ yparam2 = 'None'
+ else:
+ yparam2 = 'hr'
+
+ if 'plottype' in kwargs:
+ plottype = kwargs['plottype']
+ else:
+ if favorites:
+ plottype = favorites[favoritenr].plottype
+ else:
+ plottype = 'line'
+
+ if request.method == 'POST' and 'savefavorite' in request.POST:
+ f = FavoriteChart(user=r,xparam=xparam,
+ yparam1=yparam1,yparam2=yparam2,
+ plottype=plottype)
+ f.save()
+ if request.method == 'POST' and 'workstrokesonly' in request.POST:
+ workstrokesonly = request.POST['workstrokesonly']
+ if workstrokesonly == 'True':
+ workstrokesonly = True
+ else:
+ workstrokesonly = False
+ else:
+ workstrokesonly = False
+
# create interactive plot
res = interactive_flex_chart2(id,xparam=xparam,yparam1=yparam1,
yparam2=yparam2,
@@ -2631,6 +2696,8 @@ def workout_flexchart3_view(request,id=0,#*args,**kwargs):
'mayedit':mayedit,
'promember':promember,
'workstrokesonly': not workstrokesonly,
+ 'favoritenr':favoritenr,
+ 'maxfav':maxfav,
})
else:
return render(request,
@@ -2647,6 +2714,8 @@ def workout_flexchart3_view(request,id=0,#*args,**kwargs):
'mayedit':mayedit,
'promember':promember,
'workstrokesonly': not workstrokesonly,
+ 'favoritenr':favoritenr,
+ 'maxfav':maxfav,
})
def testbokeh(request):
@@ -2740,7 +2809,7 @@ def testbokeh(request):
'css_res':css_resources,
})
-#@user_passes_test(promember,login_url="/login")
+#@user_passes_test(promember,login_url="/",redirect_field_name=None)
def workout_biginteractive_view(request,id=0,message="",successmessage=""):
row = Workout.objects.get(id=id)
# check if user is owner of this workout
@@ -2982,7 +3051,7 @@ def workout_edit_view(request,id=0,message="",successmessage=""):
'workout_form.html',
{'form':form})
-@user_passes_test(promember,login_url="/login")
+@user_passes_test(promember,login_url="/",redirect_field_name=None)
def workout_add_otw_powerplot_view(request,id):
w = Workout.objects.get(id=id)
if (checkworkoutuser(request.user,w)==False):
@@ -3204,7 +3273,7 @@ def workout_add_distanceplot_view(request,id):
url = "/rowers/workout/"+str(w.id)+"/edit"
return HttpResponseRedirect(url)
-@user_passes_test(promember,login_url="/login")
+@user_passes_test(promember,login_url="/",redirect_field_name=None)
def workout_add_distanceplot2_view(request,id):
w = Workout.objects.get(id=id)
if (checkworkoutuser(request.user,w)==False):
@@ -3248,7 +3317,7 @@ def workout_add_distanceplot2_view(request,id):
return HttpResponseRedirect(url)
-@user_passes_test(promember,login_url="/login")
+@user_passes_test(promember,login_url="/",redirect_field_name=None)
def workout_add_timeplot2_view(request,id):
w = Workout.objects.get(id=id)
if (checkworkoutuser(request.user,w)==False):
@@ -4433,6 +4502,59 @@ def workout_summary_edit_view(request,id,message="",successmessage=""
})
+@user_passes_test(promember,login_url="/rowers/me/edit",redirect_field_name=None)
+def rower_favoritecharts_view(request):
+ message = ''
+ successmessage = ''
+ r = Rower.objects.get(user=request.user)
+ favorites = FavoriteChart.objects.filter(user=r).order_by('id')
+ favorites_data = [{'yparam1':f.yparam1,
+ 'yparam2':f.yparam2,
+ 'xparam':f.xparam,
+ 'plottype':f.plottype,
+ 'workouttype':f.workouttype}
+ for f in favorites]
+ FavoriteChartFormSet = formset_factory(FavoriteForm,formset=BaseFavoriteFormSet,extra=0)
+
+ if request.method == 'POST':
+ favorites_formset = FavoriteChartFormSet(request.POST)
+
+ if favorites_formset.is_valid():
+ new_instances = []
+ for favorites_form in favorites_formset:
+ yparam1 = favorites_form.cleaned_data.get('yparam1')
+ yparam2 = favorites_form.cleaned_data.get('yparam2')
+ xparam = favorites_form.cleaned_data.get('xparam')
+ plottype = favorites_form.cleaned_data.get('plottype')
+ workouttype = favorites_form.cleaned_data.get('workouttype')
+ new_instances.append(FavoriteChart(user=r,
+ yparam1=yparam1,
+ yparam2=yparam2,
+ xparam=xparam,
+ plottype=plottype,
+ workouttype=workouttype))
+ try:
+ with transaction.atomic():
+ FavoriteChart.objects.filter(user=r).delete()
+ FavoriteChart.objects.bulk_create(new_instances)
+ successmessage = "You have updated your favorites"
+
+ except IntegrityError:
+ message = "something went wrong"
+
+ else:
+ favorites_formset = FavoriteChartFormSet(initial=favorites_data)
+
+
+ context = {
+ 'favorites_formset':favorites_formset,
+ 'message':message,
+ 'successmessage':successmessage,
+ }
+
+
+
+ return render(request,'favoritecharts.html',context)
@login_required()
def rower_edit_view(request,message=""):
diff --git a/static/js/jquery.formset.js b/static/js/jquery.formset.js
new file mode 100644
index 00000000..9957b77d
--- /dev/null
+++ b/static/js/jquery.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('' + options.deleteText + ' ');
+ } else if (row.is('UL') || row.is('OL')) {
+ // If they're laid out as an ordered/unordered list,
+ // insert an after the last list item:
+ row.append(' ' + options.deleteText +' ');
+ } else {
+ // Otherwise, just insert the remove button as the
+ // last child element of the form's container:
+ row.append('' + options.deleteText +' ');
+ }
+ // 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 ');
+ row.hide();
+ } else {
+ del.before(' ');
+ }
+ // 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 = $('' + options.addText + ' ')
+ .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('' + options.addText + ' ');
+ 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: 'add-row', // CSS class applied to the add link
+ deleteCssClass: 'delete-row', // 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);