Merge branch 'feature/freedefaults' into develop
This commit is contained in:
@@ -2482,13 +2482,13 @@ def interactive_flex_chart2(id=0,promember=0,
|
||||
title="Max Distance",callback=callback)
|
||||
callback.args["maxdist"] = slider_dist_max
|
||||
|
||||
layout = layoutrow([layoutcolumn([slider_spm_min,
|
||||
layout = layoutrow([layoutcolumn([annotation,
|
||||
slider_spm_min,
|
||||
slider_spm_max,
|
||||
slider_dist_min,
|
||||
slider_dist_max,
|
||||
slider_work_min,
|
||||
slider_work_max,
|
||||
annotation,
|
||||
],
|
||||
),
|
||||
plot])
|
||||
@@ -2499,6 +2499,213 @@ def interactive_flex_chart2(id=0,promember=0,
|
||||
|
||||
return [script,div,js_resources,css_resources,workstrokesonly]
|
||||
|
||||
def thumbnails_set(r,id,favorites):
|
||||
charts = []
|
||||
|
||||
columns = [f.xparam for f in favorites]
|
||||
columns += [f.yparam1 for f in favorites]
|
||||
columns += [f.yparam2 for f in favorites]
|
||||
|
||||
columns += ['time']
|
||||
rowdata = dataprep.getsmallrowdata_db(columns,ids=[id],doclean=True)
|
||||
rowdata.dropna(axis=1,how='all',inplace=True)
|
||||
|
||||
if rowdata.empty:
|
||||
return [
|
||||
{'script':"",
|
||||
'div':"",
|
||||
'notes':""
|
||||
}]
|
||||
else:
|
||||
try:
|
||||
rowdata.sort_values(by='time',ascending=True,inplace=True)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
l = len(rowdata)
|
||||
maxlength = 50
|
||||
if l > maxlength:
|
||||
bins = np.linspace(rowdata['time'].min(),rowdata['time'].max(),maxlength)
|
||||
groups = rowdata.groupby(np.digitize(rowdata['time'],bins))
|
||||
rowdata = groups.mean()
|
||||
|
||||
for f in favorites:
|
||||
workstrokesonly = not f.reststrokes
|
||||
script,div = thumbnail_flex_chart(
|
||||
rowdata,
|
||||
id=id,
|
||||
xparam=f.xparam,
|
||||
yparam1=f.yparam1,
|
||||
yparam2=f.yparam2,
|
||||
plottype=f.plottype,
|
||||
)
|
||||
|
||||
|
||||
charts.append({
|
||||
'script':script,
|
||||
'div':div,
|
||||
'notes':f.notes})
|
||||
|
||||
return charts
|
||||
|
||||
|
||||
def thumbnail_flex_chart(rowdata,id=0,promember=0,
|
||||
xparam='time',
|
||||
yparam1='pace',
|
||||
yparam2='hr',
|
||||
plottype='line',
|
||||
workstrokesonly=False):
|
||||
|
||||
|
||||
try:
|
||||
tests = rowdata[yparam2]
|
||||
except KeyError:
|
||||
yparam2 = 'None'
|
||||
|
||||
try:
|
||||
tests = rowdata[yparam1]
|
||||
except KeyError:
|
||||
yparam1 = 'None'
|
||||
|
||||
|
||||
|
||||
try:
|
||||
tseconds = rowdata.ix[:,'time']
|
||||
except KeyError:
|
||||
return '','No time data - cannot make flex plot','',''
|
||||
|
||||
|
||||
try:
|
||||
rowdata['x1'] = rowdata.ix[:,xparam]
|
||||
except KeyError:
|
||||
rowdata['x1'] = 0*rowdata.ix[:,'time']
|
||||
|
||||
try:
|
||||
rowdata['y1'] = rowdata.ix[:,yparam1]
|
||||
except KeyError:
|
||||
rowdata['y1'] = 0*rowdata.ix[:,'time']
|
||||
|
||||
if yparam2 != 'None':
|
||||
try:
|
||||
rowdata['y2'] = rowdata.ix[:,yparam2]
|
||||
except KeyError:
|
||||
rowdata['y2'] = 0*rowdata.ix[:,'time']
|
||||
else:
|
||||
rowdata['y2'] = rowdata['y1']
|
||||
|
||||
if xparam=='time':
|
||||
xaxmax = tseconds.max()
|
||||
xaxmin = tseconds.min()
|
||||
elif xparam=='distance' or xparam=='cumdist':
|
||||
xaxmax = rowdata['x1'].max()
|
||||
xaxmin = rowdata['x1'].min()
|
||||
else:
|
||||
xaxmax = yaxmaxima[xparam]
|
||||
xaxmin = yaxminima[xparam]
|
||||
|
||||
x_axis_type = 'linear'
|
||||
y_axis_type = 'linear'
|
||||
if xparam == 'time':
|
||||
x_axis_type = 'datetime'
|
||||
|
||||
if yparam1 == 'pace':
|
||||
y_axis_type = 'datetime'
|
||||
y1mean = rowdata.ix[:,'pace'].mean()
|
||||
|
||||
|
||||
rowdata['xname'] = axlabels[xparam]
|
||||
rowdata['yname1'] = axlabels[yparam1]
|
||||
if yparam2 != 'None':
|
||||
rowdata['yname2'] = axlabels[yparam2]
|
||||
else:
|
||||
rowdata['yname2'] = axlabels[yparam1]
|
||||
|
||||
|
||||
# prepare data
|
||||
source = ColumnDataSource(
|
||||
rowdata
|
||||
)
|
||||
|
||||
|
||||
sizing_mode = 'fixed' # 'scale_width' also looks nice with this example
|
||||
plot = Figure(x_axis_type=x_axis_type,y_axis_type=y_axis_type,
|
||||
plot_width=200,plot_height=150,
|
||||
)
|
||||
|
||||
|
||||
|
||||
plot.toolbar.logo = None
|
||||
plot.toolbar_location = None
|
||||
#plot.yaxis.visible = False
|
||||
plot.xaxis.axis_label_text_font_size = "7pt"
|
||||
plot.yaxis.axis_label_text_font_size = "7pt"
|
||||
plot.xaxis.major_label_text_font_size = "7pt"
|
||||
plot.yaxis.major_label_text_font_size = "7pt"
|
||||
|
||||
if plottype=='line':
|
||||
plot.line('x1','y1',source=source)
|
||||
elif plottype=='scatter':
|
||||
plot.scatter('x1','y1',source=source,fill_alpha=0.4,
|
||||
line_color=None)
|
||||
|
||||
plot.xaxis.axis_label = axlabels[xparam]
|
||||
plot.yaxis.axis_label = axlabels[yparam1]
|
||||
|
||||
|
||||
|
||||
yrange1 = Range1d(start=yaxminima[yparam1],end=yaxmaxima[yparam1])
|
||||
plot.y_range = yrange1
|
||||
|
||||
if (xparam != 'time') and (xparam != 'distance') and (xparam != 'cumdist'):
|
||||
xrange1 = Range1d(start=yaxminima[xparam],end=yaxmaxima[xparam])
|
||||
plot.x_range = xrange1
|
||||
|
||||
if xparam == 'time':
|
||||
xrange1 = Range1d(start=xaxmin,end=xaxmax)
|
||||
plot.x_range = xrange1
|
||||
plot.xaxis[0].formatter = DatetimeTickFormatter(
|
||||
hours = ["%H"],
|
||||
minutes = ["%M"],
|
||||
seconds = ["%S"],
|
||||
days = ["0"],
|
||||
months = [""],
|
||||
years = [""]
|
||||
)
|
||||
|
||||
|
||||
if yparam1 == 'pace':
|
||||
plot.yaxis[0].formatter = DatetimeTickFormatter(
|
||||
seconds = ["%S"],
|
||||
minutes = ["%M"]
|
||||
)
|
||||
|
||||
|
||||
if yparam2 != 'None':
|
||||
yrange2 = Range1d(start=yaxminima[yparam2],end=yaxmaxima[yparam2])
|
||||
plot.extra_y_ranges["yax2"] = yrange2
|
||||
#= {"yax2": yrange2}
|
||||
|
||||
if plottype=='line':
|
||||
plot.line('x1','y2',color="red",y_range_name="yax2",
|
||||
source=source)
|
||||
|
||||
elif plottype=='scatter':
|
||||
plot.scatter('x1','y2',source=source,
|
||||
fill_alpha=0.4,
|
||||
line_color=None,color="red",y_range_name="yax2")
|
||||
|
||||
plot.add_layout(LinearAxis(y_range_name="yax2",
|
||||
axis_label=axlabels[yparam2],
|
||||
major_label_text_font_size="7pt",
|
||||
axis_label_text_font_size="7pt",
|
||||
),'right',
|
||||
)
|
||||
|
||||
|
||||
script, div = components(plot)
|
||||
|
||||
return [script,div]
|
||||
|
||||
|
||||
def interactive_bar_chart(id=0,promember=0):
|
||||
# check if valid ID exists (workout exists)
|
||||
|
||||
@@ -219,7 +219,7 @@ rowingmetrics = (
|
||||
'ax_max': 15,
|
||||
'default': 0,
|
||||
'mode':'both',
|
||||
'type': 'pro'}),
|
||||
'type': 'basic'}),
|
||||
|
||||
)
|
||||
|
||||
@@ -236,3 +236,68 @@ axlabels = {ax[0]:ax[1] for ax in axes}
|
||||
yaxminima = {ax[0]:ax[2] for ax in axes}
|
||||
|
||||
yaxmaxima = {ax[0]:ax[3] for ax in axes}
|
||||
|
||||
defaultfavoritecharts = (
|
||||
{
|
||||
'yparam1':'pace',
|
||||
'yparam2':'spm',
|
||||
'xparam':'time',
|
||||
'plottype':'line',
|
||||
'workouttype':'both',
|
||||
'reststrokes':True,
|
||||
'notes':"""This chart shows your pace and stroke rate versus time. """,
|
||||
},
|
||||
{
|
||||
'yparam1':'pace',
|
||||
'yparam2':'hr',
|
||||
'xparam':'time',
|
||||
'plottype':'line',
|
||||
'workouttype':'both',
|
||||
'reststrokes':True,
|
||||
'notes':"""This chart shows your pace and heart rate versus time.
|
||||
Heart rate values will be shown only when it is in your data, i.e.
|
||||
in case you recorded your heart rate during your workout""",
|
||||
},
|
||||
{
|
||||
'yparam1':'distanceperstroke',
|
||||
'yparam2':'hr',
|
||||
'xparam':'time',
|
||||
'plottype':'line',
|
||||
'workouttype':'otw',
|
||||
'reststrokes':True,
|
||||
'notes':"""This chart shows the Distance covered per stroke, and your
|
||||
heart rate versus time. """,
|
||||
},
|
||||
{
|
||||
'yparam1':'strokeenergy',
|
||||
'yparam2':'hr',
|
||||
'xparam':'time',
|
||||
'plottype':'line',
|
||||
'workouttype':'ote',
|
||||
'reststrokes':True,
|
||||
'notes':"""This chart shows the Work per Stroke and your heart rate
|
||||
plotted versus time. """,
|
||||
},
|
||||
{
|
||||
'yparam1':'distanceperstroke',
|
||||
'yparam2':'None',
|
||||
'xparam':'spm',
|
||||
'plottype':'scatter',
|
||||
'workouttype':'otw',
|
||||
'reststrokes':True,
|
||||
'notes':"""This chart shows the Distance per Stroke versus stroke
|
||||
stroke rate. You should see a steady decline of the Distance per Stroke
|
||||
as you increase stroke rate. Typical values are > 10m for steady state
|
||||
dropping to 8m for race pace in the single.""",
|
||||
},
|
||||
{
|
||||
'yparam1':'strokeenergy',
|
||||
'yparam2':'None',
|
||||
'xparam':'spm',
|
||||
'plottype':'line',
|
||||
'workouttype':'ote',
|
||||
'reststrokes':True,
|
||||
'notes':"""This chart shows the Work per Stroke versus Stroke Rate.
|
||||
This value should be fairly constant across all stroke rates.""",
|
||||
},
|
||||
)
|
||||
|
||||
@@ -298,6 +298,10 @@ class Rower(models.Model):
|
||||
defaulttimezone = models.CharField(default='UTC',max_length=100,
|
||||
choices=timezones,
|
||||
verbose_name='Default Time Zone')
|
||||
|
||||
# Show flex chart notes
|
||||
showfavoritechartnotes = models.BooleanField(default=True,
|
||||
verbose_name='Show Notes for Favorite Charts')
|
||||
|
||||
def __str__(self):
|
||||
return self.user.username
|
||||
@@ -344,6 +348,8 @@ class FavoriteChart(models.Model):
|
||||
default='both',
|
||||
verbose_name='Workout Type')
|
||||
reststrokes = models.BooleanField(default=True,verbose_name="Incl. Rest")
|
||||
notes = models.CharField(max_length=300,verbose_name='Chart Notes',
|
||||
default='Flex Chart Notes',blank=True)
|
||||
user = models.ForeignKey(Rower)
|
||||
|
||||
|
||||
@@ -351,7 +357,11 @@ class FavoriteForm(ModelForm):
|
||||
class Meta:
|
||||
model = FavoriteChart
|
||||
fields = ['xparam','yparam1','yparam2',
|
||||
'plottype','workouttype','reststrokes']
|
||||
'plottype','workouttype','reststrokes','notes']
|
||||
# widgets = {
|
||||
# 'notes': forms.Textarea,
|
||||
# }
|
||||
|
||||
|
||||
# To generate favorite chart forms on the fly
|
||||
class BaseFavoriteFormSet(BaseFormSet):
|
||||
@@ -828,7 +838,8 @@ class RowerPowerZonesForm(ModelForm):
|
||||
class AccountRowerForm(ModelForm):
|
||||
class Meta:
|
||||
model = Rower
|
||||
fields = ['weightcategory','getemailnotifications','defaulttimezone']
|
||||
fields = ['weightcategory','getemailnotifications',
|
||||
'defaulttimezone','showfavoritechartnotes']
|
||||
|
||||
class UserForm(ModelForm):
|
||||
class Meta:
|
||||
|
||||
@@ -5,6 +5,11 @@
|
||||
{% block content %}
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<div class="grid_12 alpha">
|
||||
<div class="grid_4 fav-form-header">
|
||||
<p><input type="submit" value="Update Favorites" class="button green small"/></p>
|
||||
</div>
|
||||
</div>
|
||||
{{ favorites_formset.management_form }}
|
||||
|
||||
{% for favorites_form in favorites_formset %}
|
||||
@@ -18,11 +23,6 @@
|
||||
<div class="grid_12 alpha">
|
||||
<p> </p>
|
||||
</div>
|
||||
<div class="grid_12 alpha">
|
||||
<div class="grid_2">
|
||||
<p><input type="submit" value="Update Favorites" class="button green small"/></p>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
||||
@@ -37,4 +37,5 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
{% load staticfiles %}
|
||||
{% load rowerfilters %}
|
||||
|
||||
{% block title %} Flexible Plot {% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<script type="text/javascript" src="/static/js/bokeh-0.12.3.min.js"></script>
|
||||
<script async="true" type="text/javascript">
|
||||
Bokeh.set_log_level("info");
|
||||
</script>
|
||||
|
||||
{{ interactiveplot |safe }}
|
||||
|
||||
<script>
|
||||
// Set things up to resize the plot on a window resize. You can play with
|
||||
// the arguments of resize_width_height() to change the plot's behavior.
|
||||
var plot_resize_setup = function () {
|
||||
var plotid = Object.keys(Bokeh.index)[0]; // assume we have just one plot
|
||||
var plot = Bokeh.index[plotid];
|
||||
var plotresizer = function() {
|
||||
// arguments: use width, use height, maintain aspect ratio
|
||||
plot.resize_width_height(true, true, false);
|
||||
};
|
||||
window.addEventListener('resize', plotresizer);
|
||||
plotresizer();
|
||||
};
|
||||
window.addEventListener('load', plot_resize_setup);
|
||||
</script>
|
||||
<style>
|
||||
/* Need this to get the page in "desktop mode"; not having an infinite height.*/
|
||||
html, body {height: 100%; margin:5px;}
|
||||
</style>
|
||||
|
||||
<div id="navigation" class="grid_12 alpha">
|
||||
{% if user.is_authenticated and mayedit %}
|
||||
<div class="grid_2 alpha">
|
||||
<p>
|
||||
<a class="button gray small" href="/rowers/workout/{{ id }}/edit">Edit Workout</a>
|
||||
</p>
|
||||
</div>
|
||||
<div class="grid_2 suffix_8 omega">
|
||||
<p>
|
||||
<a class="button gray small" href="/rowers/workout/{{ id }}/advanced">Advanced Edit</a>
|
||||
</p>
|
||||
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
|
||||
<p> </p>
|
||||
|
||||
<div id="plotbuttons" class="grid_12 alpha">
|
||||
|
||||
|
||||
<div id="x-axis" class="grid_6 alpha">
|
||||
<div class="grid_2 alpha">
|
||||
<a class="button blue small" href="/rowers/workout/{{ id }}/flexchart2/time/{{ yparam1 }}/{{ yparam2 }}">Time</a>
|
||||
</div>
|
||||
<div class="grid_2">
|
||||
<a class="button blue small" href="/rowers/workout/{{ id }}/flexchart2/distance/{{ yparam1 }}/{{ yparam2 }}">Distance</a>
|
||||
</div>
|
||||
<div class="grid_2 omega">
|
||||
<a class="button blue small"
|
||||
href="/rowers/workout/{{ id }}/flexchart2/{{ xparam }}/{{ yparam2 }}/{{ yparam1 }}">Swap Y axes</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="y-axis" class="grid_6 omega">
|
||||
<div class="grid_1 prefix_2 alpha">
|
||||
<a class="button blue small" href="/rowers/workout/{{ id }}/flexchart2/{{ xparam }}/{{ yparam1 }}/pace">Pace</a>
|
||||
</div>
|
||||
<div class="grid_1">
|
||||
<a class="button blue small" href="/rowers/workout/{{ id }}/flexchart2/{{ xparam }}/{{ yparam1 }}/hr">HR</a>
|
||||
</div>
|
||||
<div class="grid_1">
|
||||
<a class="button blue small" href="/rowers/workout/{{ id }}/flexchart2/{{ xparam }}/{{ yparam1 }}/spm">SPM</a>
|
||||
</div>
|
||||
<div class="grid_1 omega">
|
||||
<a class="button blue small" href="/rowers/workout/{{ id }}/flexchart2/{{ xparam }}/{{ yparam1 }}/power">Power</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div id="theplot" class="grid_12 alpha">
|
||||
|
||||
|
||||
{{ the_div|safe }}
|
||||
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@@ -1,210 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
{% load staticfiles %}
|
||||
{% load rowerfilters %}
|
||||
{% load tz %}
|
||||
|
||||
{% block title %} Flexible Plot {% endblock %}
|
||||
|
||||
{% localtime on %}
|
||||
{% block content %}
|
||||
|
||||
{{ js_res | safe }}
|
||||
{{ css_res| safe }}
|
||||
|
||||
<script type="text/javascript" src="/static/js/bokeh-0.12.3.min.js"></script>
|
||||
<script type="text/javascript" src="/static/js/bokeh-widgets-0.12.3.min.js"></script>
|
||||
<script async="true" type="text/javascript">
|
||||
Bokeh.set_log_level("info");
|
||||
</script>
|
||||
|
||||
{{ the_script |safe }}
|
||||
|
||||
|
||||
<style>
|
||||
/* Need this to get the page in "desktop mode"; not having an infinite height.*/
|
||||
html, body {height: 100%; margin:5px;}
|
||||
</style>
|
||||
|
||||
<div id="navigation" class="grid_12 alpha">
|
||||
{% if user.is_authenticated and mayedit %}
|
||||
<div class="grid_2 alpha">
|
||||
<p>
|
||||
<a class="button gray small" href="/rowers/workout/{{ id }}/edit">Edit Workout</a>
|
||||
</p>
|
||||
</div>
|
||||
<div class="grid_2 suffix_8 omega">
|
||||
<p>
|
||||
<a class="button gray small" href="/rowers/workout/{{ id }}/advanced">Advanced Edit</a>
|
||||
</p>
|
||||
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
|
||||
<p> </p>
|
||||
|
||||
<div id="plotbuttons" class="grid_12 alpha">
|
||||
|
||||
|
||||
<div id="x-axis" class="grid_6 alpha">
|
||||
<div class="grid_2 alpha dropdown">
|
||||
<button class="grid_2 alpha button blue small dropbtn">X-axis</button>
|
||||
<div class="dropdown-content">
|
||||
<a class="button blue small alpha" href="/rowers/workout/{{ id }}/flexchart/time/{{ yparam1 }}/{{ yparam2 }}/{{ plottype }}">Time</a>
|
||||
<a class="button blue small alpha" href="/rowers/workout/{{ id }}/flexchart/distance/{{ yparam1 }}/{{ yparam2 }}/{{ plottype }}">Distance</a>
|
||||
{% if promember %}
|
||||
<a class="button blue small alpha" href="/rowers/workout/{{ id }}/flexchart/power/{{ yparam1 }}/{{ yparam2 }}/scatter">Power</a>
|
||||
<a class="button blue small alpha" href="/rowers/workout/{{ id }}/flexchart/hr/{{ yparam1 }}/{{ yparam2 }}/scatter">HR</a>
|
||||
<a class="button blue small alpha" href="/rowers/workout/{{ id }}/flexchart/spm/{{ yparam1 }}/{{ yparam2 }}/scatter">SPM</a>
|
||||
<a class="button blue small alpha" href="/rowers/workout/{{ id }}/flexchart/peakforce/{{ yparam1 }}/{{ yparam2 }}/scatter">Peak Force</a>
|
||||
<a class="button blue small alpha" href="/rowers/workout/{{ id }}/flexchart/averageforce/{{ yparam1 }}/{{ yparam2 }}/scatter">Average Force</a>
|
||||
<a class="button blue small alpha" href="/rowers/workout/{{ id }}/flexchart/forceratio/{{ yparam1 }}/{{ yparam2 }}/scatter">Average/Peak force ratio</a>
|
||||
<a class="button blue small alpha" href="/rowers/workout/{{ id }}/flexchart/drivelength/{{ yparam1 }}/{{ yparam2 }}/scatter">Drive Length</a>
|
||||
<a class="button blue small alpha" href="/rowers/workout/{{ id }}/flexchart/driveenergy/{{ yparam1 }}/{{ yparam2 }}/scatter">Work per Stroke</a>
|
||||
<a class="button blue small alpha" href="/rowers/workout/{{ id }}/flexchart/drivespeed/{{ yparam1 }}/{{ yparam2 }}/scatter">Drive Speed</a>
|
||||
{% else %}
|
||||
<a class="button rosy small" href="/rowers/promembership">Power (Pro)</a>
|
||||
<a class="button rosy small" href="/rowers/promembership">HR (Pro)</a>
|
||||
<a class="button rosy small" href="/rowers/promembership">SPM (Pro)</a>
|
||||
<a class="button rosy small" href="/rowers/promembership">Peak Force (Pro)</a>
|
||||
<a class="button rosy small" href="/rowers/promembership">Average Force (Pro)</a>
|
||||
<a class="button rosy small" href="/rowers/promembership">Average/Peak Force Ratio (Pro)</a>
|
||||
<a class="button rosy small" href="/rowers/promembership">Drive Length (Pro)</a>
|
||||
<a class="button rosy small" href="/rowers/promembership">Work per Stroke (Pro)</a>
|
||||
<a class="button rosy small" href="/rowers/promembership">Drive Speed (Pro)</a>
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid_2 dropdown">
|
||||
<button class="grid_2 alpha button blue small dropbtn">Left</button>
|
||||
<div class="dropdown-content">
|
||||
<a class="button blue small" href="/rowers/workout/{{ id }}/flexchart/{{ xparam }}/pace/{{ yparam2 }}/{{ plottype }}">Pace</a>
|
||||
<a class="button blue small" href="/rowers/workout/{{ id }}/flexchart/{{ xparam }}/hr/{{ yparam2 }}/{{ plottype }}">HR</a>
|
||||
<a class="button blue small" href="/rowers/workout/{{ id }}/flexchart/{{ xparam }}/spm/{{ yparam2 }}/{{ plottype }}">SPM</a>
|
||||
<a class="button blue small" href="/rowers/workout/{{ id }}/flexchart/{{ xparam }}/power/{{ yparam2 }}/{{ plottype }}">Power</a>
|
||||
{% if promember %}
|
||||
<a class="button blue small" href="/rowers/workout/{{ id }}/flexchart/{{ xparam }}/peakforce/{{ yparam2 }}/{{ plottype }}">Peak Force</a>
|
||||
<a class="button blue small" href="/rowers/workout/{{ id }}/flexchart/{{ xparam }}/averageforce/{{ yparam2 }}/{{ plottype }}">Average Force</a>
|
||||
<a class="button blue small" href="/rowers/workout/{{ id }}/flexchart/{{ xparam }}/forceratio/{{ yparam2 }}/{{ plottype }}">Average/Peak Force Ratio</a>
|
||||
<a class="button blue small" href="/rowers/workout/{{ id }}/flexchart/{{ xparam }}/drivelength/{{ yparam2 }}/{{ plottype }}">Drive Length</a>
|
||||
<a class="button blue small" href="/rowers/workout/{{ id }}/flexchart/{{ xparam }}/driveenergy/{{ yparam2 }}/{{ plottype }}">Work per Stroke</a>
|
||||
<a class="button blue small" href="/rowers/workout/{{ id }}/flexchart/{{ xparam }}/drivespeed/{{ yparam2 }}/{{ plottype }}">Drive Speed</a>
|
||||
<a class="button blue small" href="/rowers/workout/{{ id }}/flexchart/{{ xparam }}/rhythm/{{ yparam2 }}/{{ plottype }}">Stroke Rhythm</a>
|
||||
{% else %}
|
||||
<a class="button rosy small" href="/rowers/promembership">Peak Force (Pro)</a>
|
||||
<a class="button rosy small" href="/rowers/promembership">Average Force (Pro)</a>
|
||||
<a class="button rosy small" href="/rowers/promembership">Average/Peak Force Ratio (Pro)</a>
|
||||
<a class="button rosy small" href="/rowers/promembership">Drive Length (Pro)</a>
|
||||
<a class="button rosy small" href="/rowers/promembership">Work per Stroke (Pro)</a>
|
||||
<a class="button rosy small" href="/rowers/promembership">Drive Speed (Pro)</a>
|
||||
<a class="button rosy small" href="/rowers/promembership">Stroke Rhytm (Pro)</a>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid_2 dropdown omega">
|
||||
<button class="grid_2 alpha button blue small dropbtn">Right</button>
|
||||
<div class="dropdown-content">
|
||||
<a class="button blue small" href="/rowers/workout/{{ id }}/flexchart/{{ xparam }}/{{ yparam1 }}/hr/{{ plottype }}">HR</a>
|
||||
<a class="button blue small" href="/rowers/workout/{{ id }}/flexchart/{{ xparam }}/{{ yparam1 }}/spm/{{ plottype }}">SPM</a>
|
||||
<a class="button blue small" href="/rowers/workout/{{ id }}/flexchart/{{ xparam }}/{{ yparam1 }}/power/{{ plottype }}">Power</a>
|
||||
{% if promember %}
|
||||
<a class="button blue small" href="/rowers/workout/{{ id }}/flexchart/{{ xparam }}/{{ yparam1 }}/peakforce/{{ plottype }}">Peak Force</a>
|
||||
<a class="button blue small" href="/rowers/workout/{{ id }}/flexchart/{{ xparam }}/{{ yparam1 }}/averageforce/{{ plottype }}">Average Force</a>
|
||||
<a class="button blue small" href="/rowers/workout/{{ id }}/flexchart/{{ xparam }}/{{ yparam1 }}/forceratio/{{ plottype }}">Average/Peak Force Ratio</a>
|
||||
<a class="button blue small" href="/rowers/workout/{{ id }}/flexchart/{{ xparam }}/{{ yparam1 }}/drivelength/{{ plottype }}">Drive Length</a>
|
||||
<a class="button blue small" href="/rowers/workout/{{ id }}/flexchart/{{ xparam }}/{{ yparam1 }}/driveenergy/{{ plottype }}">Work per Stroke</a>
|
||||
<a class="button blue small" href="/rowers/workout/{{ id }}/flexchart/{{ xparam }}/{{ yparam1 }}/drivespeed/{{ plottype }}">Drive Speed</a>
|
||||
<a class="button blue small" href="/rowers/workout/{{ id }}/flexchart/{{ xparam }}/{{ yparam1 }}/rhythm/{{ plottype }}">Drive Rhythm</a>
|
||||
{% else %}
|
||||
<a class="button rosy small" href="/rowers/promembership">Peak Force (Pro)</a>
|
||||
<a class="button rosy small" href="/rowers/promembership">Average Force (Pro)</a>
|
||||
<a class="button rosy small" href="/rowers/promembership">Average/Peak Force Ratio (Pro)</a>
|
||||
<a class="button rosy small" href="/rowers/promembership">Drive Length (Pro)</a>
|
||||
<a class="button rosy small" href="/rowers/promembership">Work per Stroke (Pro)</a>
|
||||
<a class="button rosy small" href="/rowers/promembership">Drive Speed (Pro)</a>
|
||||
<a class="button rosy small" href="/rowers/promembership">Drive Rhythm (Pro)</a>
|
||||
{% endif %}
|
||||
|
||||
<a class="button blue small" href="/rowers/workout/{{ id }}/flexchart/{{ xparam }}/{{ yparam1 }}/None/{{ plottype }}">None</a>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div id="y-axis" class="grid_6 omega">
|
||||
<div class="grid_2 alpha tooltip">
|
||||
<form enctype="multipart/form-data" action="{{ formloc }}" method="post">
|
||||
{% csrf_token %}
|
||||
{% if workstrokesonly %}
|
||||
<input type="hidden" name="workstrokesonly" value="True">
|
||||
<input class="grid_2 alpha button blue small" value="Remove Rest Strokes" type="Submit">
|
||||
{% else %}
|
||||
<input class="grid_2 alpha button blue small" type="hidden" name="workstrokesonly" value="False">
|
||||
<input class="grid_2 alpha button blue small" value="Include Rest Strokes" type="Submit">
|
||||
{% endif %}
|
||||
</form>
|
||||
<span class="tooltiptext">If your data source allows, this will show or hide strokes taken during rest intervals.</span>
|
||||
</div>
|
||||
<div class="grid_2">
|
||||
<a class="button blue small" href="/rowers/workout/{{ id }}/flexchart/{{ xparam }}/{{ yparam1 }}/{{ yparam2 }}/line">Line Plot</a>
|
||||
</div>
|
||||
<div class="grid_2 omega">
|
||||
<a class="button blue small" href="/rowers/workout/{{ id }}/flexchart/{{ xparam }}/{{ yparam1 }}/{{ yparam2 }}/scatter">Scatter Plot</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div id="theplot" class="grid_12 alpha">
|
||||
|
||||
|
||||
{{ the_div|safe }}
|
||||
|
||||
</div>
|
||||
|
||||
{% if user.rower.rowerplan == 'pro' or user.rower.rowerplan == 'coach' %}
|
||||
<div id="favorites" class="grid_12 alpha">
|
||||
<div class="grid_2 suffix_4 alpha">
|
||||
{% if maxfav >= 0 %}
|
||||
<a class="button gray small" href="/rowers/me/favoritecharts">Manage Favorites</a>
|
||||
{% else %}
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="grid_1">
|
||||
{% if favoritenr > 0 %}
|
||||
<a class="button blue small" href="/rowers/workout/{{ id }}/flexchart?favoritechart={{ favoritenr|add:-1 }}"><</a>
|
||||
{% else %}
|
||||
<a class="button blue small" href="/rowers/workout/{{ id }}/flexchart?favoritechart={{ maxfav }}"><</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="grid_2">
|
||||
<form enctype="multipart/form-data" action="{{ formloc }}" method="post">
|
||||
{% csrf_token %}
|
||||
<input class="grid_2 alpha button blue small" type="hidden" name="savefavorite" value="True">
|
||||
{% if workstrokesonly %}
|
||||
<input type="hidden" name="workstrokesonlysave" value="False">
|
||||
{% else %}
|
||||
<input type="hidden" name="workstrokesonlysave" value="True">
|
||||
{% endif %}
|
||||
<input class="grid_2 alpha button blue small" value="Make Favorite" type="Submit">
|
||||
</form>
|
||||
</div>
|
||||
<div class="grid_1">
|
||||
{% if favoritenr < maxfav %}
|
||||
<a class="button blue small" href="/rowers/workout/{{ id }}/flexchart?favoritechart={{ favoritenr|add:1 }}">></a>
|
||||
{% else %}
|
||||
<a class="button blue small" href="/rowers/workout/{{ id }}/flexchart?favoritechart=0">></a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
{% endlocaltime %}
|
||||
@@ -151,7 +151,6 @@
|
||||
|
||||
</div>
|
||||
|
||||
{% if user.rower.rowerplan == 'pro' or user.rower.rowerplan == 'coach' %}
|
||||
<div id="favorites" class="grid_12 alpha">
|
||||
<div class="grid_2 suffix_4 alpha">
|
||||
{% if maxfav >= 0 %}
|
||||
@@ -187,7 +186,6 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
{% endlocaltime %}
|
||||
|
||||
14
rowers/templates/flexthumbnails.html
Normal file
14
rowers/templates/flexthumbnails.html
Normal file
@@ -0,0 +1,14 @@
|
||||
{% if charts %}
|
||||
<h2>Flex Charts</h2>
|
||||
{% for chart in charts %}
|
||||
<div class="grid_3 alpha">
|
||||
<big>{{ forloop.counter }}</big>
|
||||
<div class="grid_3 tooltip">
|
||||
<a href="/rowers/workout/{{ workout.id }}/flexchart?favoritechart={{ forloop.counter |add:"-1" }}">
|
||||
{{ chart.div | safe }}
|
||||
</a>
|
||||
<span class="tooltiptext">{{ chart.notes }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
18
rowers/templates/panel_editbuttons.html
Normal file
18
rowers/templates/panel_editbuttons.html
Normal file
@@ -0,0 +1,18 @@
|
||||
<div class="grid_2 alpha">
|
||||
<h2>Navigation</h2>
|
||||
</div>
|
||||
<div class="grid_2 alpha">
|
||||
<p>
|
||||
<a class="button gray small" href="/rowers/workout/{{ workout.id }}/edit">Edit Workout</a>
|
||||
</p>
|
||||
</div>
|
||||
<div class="grid_2 alpha">
|
||||
<p>
|
||||
<a class="button gray small" href="/rowers/workout/{{ workout.id }}/advanced">Advanced</a>
|
||||
</p>
|
||||
</div>
|
||||
<div class="grid_2 alpha">
|
||||
<p>
|
||||
<a class="button gray small" href="/rowers/workout/{{ workout.id }}/editintervals">Edit Intervals</a>
|
||||
</p>
|
||||
</div>
|
||||
11
rowers/templates/panel_statcharts.html
Normal file
11
rowers/templates/panel_statcharts.html
Normal file
@@ -0,0 +1,11 @@
|
||||
{% if statcharts %}
|
||||
<h2>Static Charts</h2>
|
||||
{% for graph in statcharts %}
|
||||
<div id="thumb-container" class="grid_3 alpha">
|
||||
<a href="/rowers/graph/{{ graph.id }}/">
|
||||
<img src="/{{ graph.filename }}"
|
||||
onerror="this.src='/static/img/waiting.png'"
|
||||
alt="{{ graph.filename }}" width="180" height="150"></a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
23
rowers/templates/panel_staticchart.html
Normal file
23
rowers/templates/panel_staticchart.html
Normal file
@@ -0,0 +1,23 @@
|
||||
<div class="grid_2 alpha">
|
||||
<h2>Create Charts</h2>
|
||||
</div>
|
||||
<div class="grid_2 alpha">
|
||||
<p>
|
||||
<a class="button blue small" href="/rowers/workout/{{ workout.id }}/addtimeplot">Add Time Plot</a>
|
||||
</p>
|
||||
</div>
|
||||
<div class="grid_2 alpha">
|
||||
<p>
|
||||
<a class="button blue small" href="/rowers/workout/{{ workout.id }}/adddistanceplot">Add Distance Plot</a>
|
||||
</p>
|
||||
</div>
|
||||
<div class="grid_2 alpha">
|
||||
<p>
|
||||
<a class="button blue small" href="/rowers/workout/{{ workout.id }}/addpiechart">Add Pie Chart</a>
|
||||
</p>
|
||||
</div>
|
||||
<div class="grid_2 alpha">
|
||||
<p>
|
||||
<a class="button blue small" href="/rowers/workout/{{ workout.id }}/addpowerpiechart">Add Power Pie Chart</a>
|
||||
</p>
|
||||
</div>
|
||||
5
rowers/templates/panel_stats.html
Normal file
5
rowers/templates/panel_stats.html
Normal file
@@ -0,0 +1,5 @@
|
||||
<div class="grid_2 alpha">
|
||||
<p>
|
||||
<a class="button gray small" href="/rowers/workout/{{ workout.id }}/stats">Workout Stats</a>
|
||||
</p>
|
||||
</div>
|
||||
6
rowers/templates/panel_summary.html
Normal file
6
rowers/templates/panel_summary.html
Normal file
@@ -0,0 +1,6 @@
|
||||
<h2>Summary</h2>
|
||||
<p>
|
||||
<pre>
|
||||
{{ workout.summary }}
|
||||
</pre>
|
||||
</p>
|
||||
56
rowers/templates/workflow.html
Normal file
56
rowers/templates/workflow.html
Normal file
@@ -0,0 +1,56 @@
|
||||
{% extends "base.html" %}
|
||||
{% load staticfiles %}
|
||||
{% load rowerfilters %}
|
||||
{% load tz %}
|
||||
|
||||
|
||||
{% get_current_timezone as TIME_ZONE %}
|
||||
|
||||
{% block title %}{{ workout.name }}{% endblock %}
|
||||
{% block og_title %}{{ workout.name }}{% endblock %}
|
||||
{% block description %}{{ workout.name }}
|
||||
{{ workout.date }} - {{ workout.distance }}m - {{ workout.duration |durationprint:"%H:%M:%S.%f" }}{% endblock %}
|
||||
{% block og_description %}{{ workout.name }}
|
||||
{{ workout.date }} - {{ workout.distance }}m - {{ workout.duration |durationprint:"%H:%M:%S.%f" }}{% endblock %}
|
||||
|
||||
{% block meta %}
|
||||
<script type="text/javascript" src="/static/js/bokeh-0.12.3.min.js"></script>
|
||||
<script async="true" type="text/javascript">
|
||||
Bokeh.set_log_level("info");
|
||||
</script>
|
||||
|
||||
{% for chart in charts %}
|
||||
{{ chart.script |safe }}
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div id="page" class="grid_12 alpha">
|
||||
<div class="grid_10 prefix_2 alpha">
|
||||
<h1>{{ workout.name }}</h1>
|
||||
</div>
|
||||
<div id="leftpanel" class="grid_2 alpha">
|
||||
{% block left_panel %}
|
||||
{% for templateName in leftTemplates %}
|
||||
{% include templateName %}
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
<div class="grid_2 alpha">
|
||||
<p>Click on the thumbnails to view the full chart</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="middlepanel" class="grid_9">
|
||||
{% block middle_panel %}
|
||||
{% for templateName in middleTemplates %}
|
||||
<div class="grid_9">
|
||||
{% include templateName %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
<div id="rightpanel" class="grid_1">
|
||||
{% block right_panel %}
|
||||
<p> </p>
|
||||
{% endblock %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -327,6 +327,7 @@ urlpatterns = [
|
||||
url(r'^legal', TemplateView.as_view(template_name='legal.html'),name='legal'),
|
||||
url(r'^register$',views.rower_register_view),
|
||||
url(r'^register/thankyou/$', TemplateView.as_view(template_name='registerthankyou.html'), name='registerthankyou'),
|
||||
url(r'^workout/(?P<id>\d+)/workflow$',views.workout_workflow_view),
|
||||
url(r'^workout/(?P<id>\d+)/flexchart/(?P<xparam>\w+.*)/(?P<yparam1>\w+.*)/(?P<yparam2>\w+.*)/(?P<plottype>\w+)/$',views.workout_flexchart3_view),
|
||||
url(r'^workout/(?P<id>\d+)/flexchart/(?P<xparam>\w+.*)/(?P<yparam1>\w+.*)/(?P<yparam2>\w+.*)/(?P<plottype>\w+.*)$',views.workout_flexchart3_view),
|
||||
url(r'^workout/(?P<id>\d+)/flexchart/(?P<xparam>\w+.*)/(?P<yparam1>\w+.*)/(?P<yparam2>\w+.*)$',views.workout_flexchart3_view),
|
||||
|
||||
@@ -49,7 +49,7 @@ from rowers.models import (
|
||||
WorkoutComment,WorkoutCommentForm,RowerExportForm,
|
||||
)
|
||||
from rowers.models import FavoriteForm,BaseFavoriteFormSet,SiteAnnouncement
|
||||
from rowers.metrics import rowingmetrics
|
||||
from rowers.metrics import rowingmetrics,defaultfavoritecharts
|
||||
import rowers.uploads as uploads
|
||||
from django.forms.formsets import formset_factory
|
||||
import StringIO
|
||||
@@ -324,6 +324,22 @@ def ispromember(user):
|
||||
result = False
|
||||
return result
|
||||
|
||||
# More User/Rower utils
|
||||
def add_defaultfavorites(r):
|
||||
for c in defaultfavoritecharts:
|
||||
f = FavoriteChart(user=r,
|
||||
yparam1=c['yparam1'],
|
||||
yparam2=c['yparam2'],
|
||||
xparam=c['xparam'],
|
||||
plottype=c['plottype'],
|
||||
workouttype=c['workouttype'],
|
||||
reststrokes=c['reststrokes'],
|
||||
notes=c['notes'])
|
||||
|
||||
f.save()
|
||||
return 1
|
||||
|
||||
|
||||
# User registration
|
||||
def rower_register_view(request):
|
||||
if request.method == 'POST':
|
||||
@@ -344,6 +360,9 @@ def rower_register_view(request):
|
||||
|
||||
therower.save()
|
||||
|
||||
# create default favorite charts
|
||||
add_defaultfavorites(therower)
|
||||
|
||||
# Create Sample workout
|
||||
f = 'media/testdata.csv.gz'
|
||||
timestr = strftime("%Y%m%d-%H%M%S")
|
||||
@@ -5827,7 +5846,62 @@ def workout_comparison_view2(request,id1=0,id2=0,xparam='distance',
|
||||
'promember':promember,
|
||||
})
|
||||
|
||||
|
||||
|
||||
# Flex thumbnails
|
||||
@login_required()
|
||||
def workout_workflow_view(request,id):
|
||||
request.session['referer'] = absolute(request)['PATH']
|
||||
request.session[translation.LANGUAGE_SESSION_KEY] = USER_LANGUAGE
|
||||
try:
|
||||
row = Workout.objects.get(id=id)
|
||||
except Workout.DoesNotExist:
|
||||
raise Http404("Workout doesn't exist")
|
||||
|
||||
r = getrower(request.user)
|
||||
result = request.user.is_authenticated() and ispromember(request.user)
|
||||
if result:
|
||||
promember=1
|
||||
if request.user == row.user.user:
|
||||
mayedit=1
|
||||
|
||||
workouttype = 'ote'
|
||||
if row.workouttype in ('water','coastal'):
|
||||
workouttype = 'otw'
|
||||
|
||||
try:
|
||||
favorites = FavoriteChart.objects.filter(user=r,
|
||||
workouttype__in=[workouttype,'both']).order_by("id")
|
||||
maxfav = len(favorites)-1
|
||||
except:
|
||||
favorites = None
|
||||
maxfav = 0
|
||||
|
||||
charts = []
|
||||
|
||||
if favorites:
|
||||
charts = thumbnails_set(r,id,favorites)
|
||||
|
||||
statcharts = GraphImage.objects.filter(workout=row)
|
||||
|
||||
# This will be user configurable in the future
|
||||
middleTemplates = ['panel_statcharts.html','flexthumbnails.html',
|
||||
'panel_summary.html']
|
||||
leftTemplates = [
|
||||
'panel_editbuttons.html',
|
||||
'panel_stats.html',
|
||||
'panel_staticchart.html',
|
||||
]
|
||||
|
||||
return render(request,
|
||||
'workflow.html',
|
||||
{
|
||||
'middleTemplates':middleTemplates,
|
||||
'leftTemplates':leftTemplates,
|
||||
'charts':charts,
|
||||
'workout':row,
|
||||
'statcharts':statcharts,
|
||||
})
|
||||
|
||||
# The famous flex chart
|
||||
def workout_flexchart3_view(request,*args,**kwargs):
|
||||
|
||||
@@ -5959,12 +6033,6 @@ def workout_flexchart3_view(request,*args,**kwargs):
|
||||
css_resources = ""
|
||||
|
||||
|
||||
# script = res[0]
|
||||
# div = res[1]
|
||||
# js_resources = res[2]
|
||||
# css_resources = res[3]
|
||||
|
||||
|
||||
axchoicesbasic = {ax[0]:ax[1] for ax in axes if ax[4]=='basic'}
|
||||
axchoicespro = {ax[0]:ax[1] for ax in axes if ax[4]=='pro'}
|
||||
noylist = ["time","distance"]
|
||||
@@ -6745,7 +6813,10 @@ def workout_add_chart_view(request,id,plotnr=1):
|
||||
imagename=imagename)
|
||||
|
||||
|
||||
url = "/rowers/workout/"+str(w.id)+"/edit"
|
||||
try:
|
||||
url = request.session['referer']
|
||||
except KeyError:
|
||||
url = "/rowers/workout/"+str(w.id)+"/edit"
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
|
||||
@@ -8258,7 +8329,7 @@ def workout_summary_edit_view(request,id,message="",successmessage=""
|
||||
})
|
||||
|
||||
# Page where user can manage his favorite charts
|
||||
@user_passes_test(ispromember,login_url="/rowers/me/edit",redirect_field_name=None)
|
||||
@login_required()
|
||||
def rower_favoritecharts_view(request):
|
||||
message = ''
|
||||
successmessage = ''
|
||||
|
||||
@@ -62,11 +62,28 @@ h1 {
|
||||
font-weight: normal;
|
||||
/* padding-top: 20px; */
|
||||
text-align: left;
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
h2 {
|
||||
/* padding-top: 20px; */
|
||||
/* padding-top: 20px; */
|
||||
font-weight: normal;
|
||||
text-align: left;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
h3 {
|
||||
/* padding-top: 20px; */
|
||||
font-weight: normal;
|
||||
text-align: left;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
h3 {
|
||||
/* padding-top: 20px; */
|
||||
font-weight: normal;
|
||||
text-align: left;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
p {
|
||||
|
||||
Reference in New Issue
Block a user