diff --git a/rowers/models.py b/rowers/models.py index 685a687f..827c4585 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,95 @@ 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'), + ) + + 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') + xparam = models.CharField(max_length=50,choices=xparams,verbose_name='X') + plottype = models.CharField(max_length=50,choices=plottypes,default='line') + user = models.ForeignKey(Rower) + +class FavoriteForm(ModelForm): + class Meta: + model = FavoriteChart + fields = ['xparam','yparam1','yparam2','plottype'] + +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' + ) + class Workout(models.Model): workouttypes = ( ('water','On-water'), diff --git a/rowers/templates/favoritecharts.html b/rowers/templates/favoritecharts.html new file mode 100644 index 00000000..4a6406a8 --- /dev/null +++ b/rowers/templates/favoritecharts.html @@ -0,0 +1,35 @@ +{% extends "base.html" %} + +{% block title %}Change Favorite Charts{% endblock %} + +{% block content %} +
+ {% csrf_token %} + {{ favorites_formset.management_form }} + + {% for favorites_form in favorites_formset %} +
+

Chart {{ forloop.counter }}

+ + {{ favorites_form.as_table }} +
+
+ {% endfor %} +
+
+

+
+
+
+ + + + + + +{% endblock %} diff --git a/rowers/templates/flexchart3.html b/rowers/templates/flexchart3.html index 67d9716b..1a023259 100644 --- a/rowers/templates/flexchart3.html +++ b/rowers/templates/flexchart3.html @@ -163,6 +163,37 @@ +
+
+ {% if maxfav >= 0 %} + Manage Favorites + {% else %} +   + {% endif %} +
+
+ {% if favoritenr > 0 %} + < + {% else %} +

 

+ {% endif %} +
+
+
+ {% csrf_token %} + + +
+
+
+ {% if favoritenr < maxfav %} + > + {% else %} +

 

+ {% 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..7869f470 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 @@ -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,56 @@ def workout_flexchart3_view(request,id=0,#*args,**kwargs): if request.user == row.user.user: mayedit=1 + + favorites = FavoriteChart.objects.filter(user=r).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'] + else: + if favorites: + yparam2 = favorites[favoritenr].yparam2 + 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 +2686,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 +2704,8 @@ def workout_flexchart3_view(request,id=0,#*args,**kwargs): 'mayedit':mayedit, 'promember':promember, 'workstrokesonly': not workstrokesonly, + 'favoritenr':favoritenr, + 'maxfav':maxfav, }) def testbokeh(request): @@ -4433,6 +4492,56 @@ def workout_summary_edit_view(request,id,message="",successmessage="" }) +@login_required() +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} + for f in favorites] + FavoriteChartFormSet = formset_factory(FavoriteForm,formset=BaseFavoriteFormSet) + + 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') + new_instances.append(FavoriteChart(user=r, + yparam1=yparam1, + yparam2=yparam2, + xparam=xparam, + plottype=plottype)) + 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