Merge branch 'feature/workouteditor' into develop
This commit is contained in:
105
rowers/models.py
105
rowers/models.py
@@ -2391,6 +2391,111 @@ regularsessiontypechoices = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
# model for Planned Session (Workout, Challenge, Test)
|
# model for Planned Session (Workout, Challenge, Test)
|
||||||
|
class PlannedSessionStep(models.Model):
|
||||||
|
intensitytypes = (
|
||||||
|
("Active", "Active"),
|
||||||
|
("Rest", "Rest"),
|
||||||
|
("Warmup", "Warmup"),
|
||||||
|
("Cooldown", "Cooldown")
|
||||||
|
)
|
||||||
|
|
||||||
|
durationtypes = (
|
||||||
|
("Distance", "Distance"),
|
||||||
|
("Time", "Time"),
|
||||||
|
('RepeatUntilStepsCmplt','Repeat previous blocks n times')
|
||||||
|
)
|
||||||
|
|
||||||
|
targettypes = (
|
||||||
|
("Speed", "Speed"),
|
||||||
|
("HeartRate", "HeartRate"),
|
||||||
|
("Cadence", "Cadence"),
|
||||||
|
("Power", "Power")
|
||||||
|
)
|
||||||
|
|
||||||
|
manager = models.ForeignKey(User, null=True, on_delete=models.CASCADE)
|
||||||
|
name = models.TextField(default='',max_length=200, blank=True, null=True)
|
||||||
|
type = models.TextField(default='',max_length=200, blank=True, null=True)
|
||||||
|
durationvalue = models.FloatField(default=0, verbose_name="Duration Value")
|
||||||
|
durationtype = models.TextField(default='Time',max_length=200,
|
||||||
|
choices=durationtypes,
|
||||||
|
verbose_name='Duration Type')
|
||||||
|
targetvalue = models.IntegerField(default=0, verbose_name="Target Value")
|
||||||
|
targettype = models.TextField(default='',max_length=200, blank=True, null=True,
|
||||||
|
choices=targettypes, verbose_name="Target Type")
|
||||||
|
targetvaluelow = models.IntegerField(default=0,
|
||||||
|
verbose_name="Target Value Low")
|
||||||
|
targetvaluehigh = models.IntegerField(default=0,
|
||||||
|
verbose_name="Target Value High")
|
||||||
|
intensity = models.TextField(default='',max_length=200, blank=True, null=True,
|
||||||
|
choices=intensitytypes,
|
||||||
|
verbose_name = "Intensity")
|
||||||
|
description = models.TextField(default='',max_length=200, blank=True, null=True)
|
||||||
|
color = models.TextField(default='#ddd',max_length=200)
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if self.intensity == "Warmup":
|
||||||
|
self.color = "#ffcccb"
|
||||||
|
elif self.intensity == "Cooldown":
|
||||||
|
self.color = '#90ee90'
|
||||||
|
elif self.intensity == "Rest":
|
||||||
|
self.color = 'add8e6'
|
||||||
|
if self.durationtype == 'RepeatUntilStepsCmplt':
|
||||||
|
self.color = 'ffffa7'
|
||||||
|
|
||||||
|
self.durationvalue = int(self.durationvalue)
|
||||||
|
|
||||||
|
super(PlannedSessionStep, self).save(*args, **kwargs)
|
||||||
|
|
||||||
|
def asdict(self):
|
||||||
|
d = {
|
||||||
|
'wkt_step_name': self.name,
|
||||||
|
'durationType': self.durationtype,
|
||||||
|
'durationValue': self.durationvalue,
|
||||||
|
'targetType': self.targettype,
|
||||||
|
'targetValue': self.targetvalue,
|
||||||
|
'targetValueLow': self.targetvaluelow,
|
||||||
|
'targetValueHigh': self.targetvaluehigh,
|
||||||
|
'description': self.description,
|
||||||
|
'stepId': self.pk,
|
||||||
|
'intensity': self.intensity,
|
||||||
|
}
|
||||||
|
|
||||||
|
return d
|
||||||
|
|
||||||
|
class StepEditorForm(ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = PlannedSessionStep
|
||||||
|
fields = [
|
||||||
|
'name',
|
||||||
|
#'type',
|
||||||
|
'durationtype',
|
||||||
|
'durationvalue',
|
||||||
|
'targettype',
|
||||||
|
'targetvalue',
|
||||||
|
'targetvaluelow',
|
||||||
|
'targetvaluehigh',
|
||||||
|
'intensity',
|
||||||
|
'description',
|
||||||
|
]
|
||||||
|
|
||||||
|
widgets = {
|
||||||
|
'name': forms.Textarea(attrs={'rows':1, 'cols':50}),
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(StepEditorForm, self).__init__(*args, **kwargs)
|
||||||
|
if self.instance.durationtype == 'Time':
|
||||||
|
self.initial['durationvalue'] = self.instance.durationvalue / 60000
|
||||||
|
elif self.instance.durationtype == 'Distance':
|
||||||
|
self.initial['durationvalue'] = self.instance.durationvalue / 100
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
# conversions
|
||||||
|
if self.instance.durationtype == 'Time':
|
||||||
|
self.instance.durationvalue *= 60000
|
||||||
|
elif self.instance.durationtype == 'Distance':
|
||||||
|
self.instance.durationvalue *= 100
|
||||||
|
return super(StepEditorForm, self).save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class PlannedSession(models.Model):
|
class PlannedSession(models.Model):
|
||||||
|
|||||||
@@ -34,6 +34,8 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block sidebar %}
|
{% block sidebar %}
|
||||||
|
|||||||
@@ -35,6 +35,17 @@
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
</li>
|
</li>
|
||||||
|
<li class="grid_2">
|
||||||
|
<h2>Steps</h2>
|
||||||
|
{% if steps %}
|
||||||
|
<p>{{ steps|safe }}</p>
|
||||||
|
{% else %}
|
||||||
|
No Steps defined
|
||||||
|
{% endif %}
|
||||||
|
<p>
|
||||||
|
<a href="/rowers/plans/stepeditor/{{ thesession.id }}/">Edit Steps (experimental)</a>
|
||||||
|
</p>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
97
rowers/templates/stepedit.html
Normal file
97
rowers/templates/stepedit.html
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
{% extends "newbase.html" %}
|
||||||
|
{% load static %}
|
||||||
|
{% load rowerfilters %}
|
||||||
|
|
||||||
|
{% block title %}Rowsandall Training Plans{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
<h2>Edit {{ step.name }}</h2>
|
||||||
|
<ul class="main-content">
|
||||||
|
<li class="grid_2">
|
||||||
|
<p>
|
||||||
|
WARNING: This is experimental functionality which may not behave as you
|
||||||
|
expect. Does not work on smartphones.
|
||||||
|
</p>
|
||||||
|
</li>
|
||||||
|
<li class="grid_2">
|
||||||
|
<h2>Step Description</h2>
|
||||||
|
{{ stepdescription }}
|
||||||
|
</li>
|
||||||
|
<li class="grid_2">
|
||||||
|
<h2>Add new step</h2>
|
||||||
|
<form enctype="multipart/multipart/form-data" method="post">
|
||||||
|
<table>
|
||||||
|
{{ form.as_table }}
|
||||||
|
</table>
|
||||||
|
{% csrf_token %}
|
||||||
|
<input name="stepsave" type="submit" value="Update">
|
||||||
|
<input name="stepsave_and_return" type="submit" value="Update and Return to Steps">
|
||||||
|
</form>
|
||||||
|
</li>
|
||||||
|
<li class="grid_2">
|
||||||
|
<h2>Explanation</h2>
|
||||||
|
<table width=90% class="listtable shortpadded">
|
||||||
|
<tr>
|
||||||
|
<th>Parameter</th>
|
||||||
|
<th>Regular Step</th>
|
||||||
|
<th>Repeat Step</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Duration Type</th><td>Time or Distance</td><td>Repeat</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Duration Value</th><td>Minutes / Meters</td><td>Block number to start repeat from</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Target Type</th><td>Set a target to hold</td><td>---</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Target Value</th>
|
||||||
|
<td>
|
||||||
|
<div>Power: Zone number (1-10), % of FTP (10-1000)</div>
|
||||||
|
<div>Speed: 1000x target speed in m/s</div>
|
||||||
|
<div>Heart Rate: Zone number (1-10), % of max (10-100); </div>
|
||||||
|
<div>Cadence: Strokes per Minute</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
Number of repetitions
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Target Value Low</th>
|
||||||
|
<td>
|
||||||
|
<div>Power: % of FTP (10-1000)</div>
|
||||||
|
<div>Speed: 1000x target speed in m/s</div>
|
||||||
|
<div>Heart Rate: % of max (10-100); </div>
|
||||||
|
<div>Cadence: Strokes per Minute</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Target Value High</th>
|
||||||
|
<td>
|
||||||
|
<div>Power: % of FTP (10-1000)</div>
|
||||||
|
<div>Speed: 1000x target speed in m/s</div>
|
||||||
|
<div>Heart Rate: % of max (10-100); </div>
|
||||||
|
<div>Cadence: Strokes per Minute</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Intensity</th><td>Warming Up, Active, Rest</td><td>Set to Active</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Description</th><td>Any other text</td><td></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block sidebar %}
|
||||||
|
{% include 'menu_plan.html' %}
|
||||||
|
{% endblock %}
|
||||||
343
rowers/templates/stepeditor.html
Normal file
343
rowers/templates/stepeditor.html
Normal file
@@ -0,0 +1,343 @@
|
|||||||
|
{% extends "newbase.html" %}
|
||||||
|
{% load static %}
|
||||||
|
{% load rowerfilters %}
|
||||||
|
|
||||||
|
{% block title %}Rowsandall Training Plans{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
<h2>Plan Training Steps</h2>
|
||||||
|
<p>
|
||||||
|
WARNING: This is experimental functionality which may not behave as you
|
||||||
|
expect. Does not work on smartphones.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Drag from Library to Training to add a step to the end.
|
||||||
|
Drag on top of a training step to insert after it.
|
||||||
|
Drag out of Training to remove a step.
|
||||||
|
</p>
|
||||||
|
<div class="stepcontainer" id="list">
|
||||||
|
<section class="drop-zone">
|
||||||
|
<h2>Training Steps for {{ ps.name }}</h2>
|
||||||
|
<p>
|
||||||
|
<form onsubmit="event.preventDefault(); saveSession();">
|
||||||
|
<input id="hidden" type="hidden" value="">
|
||||||
|
<input id="savebutton" type="submit" value="Save">
|
||||||
|
</form>
|
||||||
|
</p>
|
||||||
|
{% for step in currentsteps %}
|
||||||
|
<div id="{{ step.id }}" draggable="true" class="trainingstep {{ step.intensity }} {{ step.durationtype }}" >
|
||||||
|
<span id="{{ step.id }}" class="stepid">
|
||||||
|
({{ forloop.counter|add:-1 }})
|
||||||
|
</span>
|
||||||
|
<span id="{{ step.id }}" class="stepcontent">
|
||||||
|
{{ step.name }}
|
||||||
|
{% if step.durationtype == "RepeatUntilStepsCmplt" %}
|
||||||
|
- repeat {{ step.targetvalue }}x from block {{ step.durationvalue|floatformat }}
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
<span id="{{ step.id }}">
|
||||||
|
<a href="/rowers/plans/step/{{ step.id }}/edit/{{ ps.id }}/">
|
||||||
|
<i class="fas fa-pencil-alt fa-fw"></i>
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
<span id="{{ step.id }}">
|
||||||
|
<a href="/rowers/plans/step/{{ step.id }}/delete/?id={{ ps.id }}">
|
||||||
|
<i class="fas fa-trash-alt fa-fw"></i>
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</section>
|
||||||
|
<section class="allcentered">
|
||||||
|
<div class="allcenteredchild">
|
||||||
|
<h1><i class="fas fa-arrow-left fa-fw"></i></h1>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section class="library">
|
||||||
|
<h2>Step Library</h2>
|
||||||
|
{% for step in steps %}
|
||||||
|
<div id="{{ step.id }}" draggable="true" class="trainingstep {{ step.intensity }} {{ step.durationtype}}" >
|
||||||
|
<span id="{{ step.id }}" class="stepid">
|
||||||
|
</span>
|
||||||
|
<span id="{{ step.id }}" class="stepcontent">
|
||||||
|
{{ step.name }}
|
||||||
|
{% if step.durationtype == "RepeatUntilStepsCmplt" %}
|
||||||
|
- repeat {{ step.targetvalue }}x from block {{ step.durationvalue|floatformat }}
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
<span id="{{ step.id }}">
|
||||||
|
<a href="/rowers/plans/step/{{ step.id }}/edit/{{ ps.id }}/">
|
||||||
|
<i class="fas fa-pencil-alt fa-fw"></i>
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
<span id="{{ step.id }}">
|
||||||
|
<a href="/rowers/plans/step/{{ step.id }}/delete/?id={{ ps.id }}">
|
||||||
|
<i class="fas fa-trash-alt fa-fw"></i>
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<ul class="main-content">
|
||||||
|
<li class="grid_2 library">
|
||||||
|
<h2>Add new step</h2>
|
||||||
|
<form enctype="multipart/multipart/form-data" method="post">
|
||||||
|
<table>
|
||||||
|
{{ form.as_table }}
|
||||||
|
</table>
|
||||||
|
{% csrf_token %}
|
||||||
|
<input name="newstep" type="submit" value="Add to Library">
|
||||||
|
</form>
|
||||||
|
</li>
|
||||||
|
<li class="grid_2 library">
|
||||||
|
<h2>Step Information</h2>
|
||||||
|
<div id="stepinfo">
|
||||||
|
<p>Step information</p>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script type='text/javascript'
|
||||||
|
src='https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js'>
|
||||||
|
</script>
|
||||||
|
<script>
|
||||||
|
let csrftoken;
|
||||||
|
let mysteps = {};
|
||||||
|
{% for step in steps %}
|
||||||
|
mysteps["{{ step.id }}"] = { 'name': '{{ step.name }}' };
|
||||||
|
{% endfor %}
|
||||||
|
let descriptions = {}
|
||||||
|
{% for key, value in stepdescriptions.items %}
|
||||||
|
descriptions["{{ key }}"] = "{{ value }}"
|
||||||
|
{% endfor %}
|
||||||
|
$(document).ready(function() {
|
||||||
|
csrftoken = jQuery("[name=csrfmiddlewaretoken]").val();
|
||||||
|
});
|
||||||
|
function csrfSafeMethod(method) {
|
||||||
|
// these HTTP methods do not require CSRF protection
|
||||||
|
return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
|
||||||
|
}
|
||||||
|
$.ajaxSetup({
|
||||||
|
beforeSend: function(xhr, settings) {
|
||||||
|
if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
|
||||||
|
xhr.setRequestHeader("X-CSRFToken", csrftoken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let dragged;
|
||||||
|
let origcolor;
|
||||||
|
|
||||||
|
function saveSession() {
|
||||||
|
|
||||||
|
var list = [];
|
||||||
|
steps = document.querySelector('.drop-zone');
|
||||||
|
steps.childNodes.forEach(function(item) {
|
||||||
|
if (item.className && item.className.includes("trainingstep")) {
|
||||||
|
item.childNodes.forEach(function(child) {
|
||||||
|
if (child.id && child.className == 'stepcontent') {
|
||||||
|
list.push(child.id);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$.ajax({
|
||||||
|
data: JSON.stringify(list),
|
||||||
|
type: 'POST',
|
||||||
|
url: '/rowers/plans/stepadder/{{ ps.id }}/?save=1',
|
||||||
|
error: function(result) {
|
||||||
|
$("#id_waiting").replaceWith(
|
||||||
|
'<div id="id_failed" class="grid_12 alpha message">Your upload failed</div>'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
success: function(result) {
|
||||||
|
console.log(result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
window.location.reload(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveState() {
|
||||||
|
var cntr = 0;
|
||||||
|
var list = [];
|
||||||
|
steps = document.querySelector('.drop-zone');
|
||||||
|
steps.childNodes.forEach(function(item) {
|
||||||
|
if (item.className && item.className.includes("trainingstep")) {
|
||||||
|
item.childNodes.forEach(function(child) {
|
||||||
|
if (child.id && child.className == 'stepcontent') {
|
||||||
|
list.push(child.id);
|
||||||
|
}
|
||||||
|
if (child.className == "stepid") {
|
||||||
|
s = `(${cntr})`
|
||||||
|
child.replaceWith(s);
|
||||||
|
cntr++;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$.ajax({
|
||||||
|
data: JSON.stringify(list),
|
||||||
|
type: 'POST',
|
||||||
|
url: '/rowers/plans/stepadder/{{ ps.id }}/',
|
||||||
|
error: function(result) {
|
||||||
|
$("#id_waiting").replaceWith(
|
||||||
|
'<div id="id_failed" class="grid_12 alpha message">Your upload failed</div>'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
success: function(result) {
|
||||||
|
console.log(result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
function handleDragStart(event) {
|
||||||
|
let target = event.target;
|
||||||
|
if (target) {
|
||||||
|
dragged = target;
|
||||||
|
origcolor = dragged.style.backgroundColor;
|
||||||
|
event.dataTransfer.setData('text', target.id);
|
||||||
|
event.dataTransfer.dropEffect = 'move';
|
||||||
|
// Make it half transparent
|
||||||
|
event.target.style.opacity = .3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDragEnd(event) {
|
||||||
|
if (event.target) {
|
||||||
|
// Reset the transparency
|
||||||
|
event.target.style.opacity = ''; // reset opacity when drag ends
|
||||||
|
items.forEach(function (item) {
|
||||||
|
item.classList.remove('over');
|
||||||
|
});
|
||||||
|
dragged = null;
|
||||||
|
origcolor = null;
|
||||||
|
}
|
||||||
|
saveState();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMouseOver(event) {
|
||||||
|
if (event.preventDefault) {
|
||||||
|
const id = event.target.id;
|
||||||
|
if (id) {
|
||||||
|
var name = mysteps[id]['name'];
|
||||||
|
var txt = descriptions[id];
|
||||||
|
var divstring = `<p>${name}</p><p>${txt}</p>`
|
||||||
|
const div = document.getElementById("stepinfo");
|
||||||
|
div.innerHTML = divstring;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMouseLeave(event) {
|
||||||
|
if (event.preventDefault) {
|
||||||
|
const div = document.getElementById("stepinfo");
|
||||||
|
div.innerHTML = '<p>Hover over a step to get details</p>'
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDragOver(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDragEnter(event) {
|
||||||
|
this.classList.add('over');
|
||||||
|
const target = event.target;
|
||||||
|
overcolor = event.target.style.backgroundColor;
|
||||||
|
|
||||||
|
if (target && dragged) {
|
||||||
|
event.preventDefault();
|
||||||
|
// Set the dropEffect to move
|
||||||
|
event.dataTransfer.dropEffect = 'move'
|
||||||
|
target.style.background = '#1f904e';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDragLeave(event) {
|
||||||
|
event.target.style.backgroundColor = '';
|
||||||
|
|
||||||
|
if (dragged.parentNode.className.includes("drop-zone")) {
|
||||||
|
trash(event);
|
||||||
|
}
|
||||||
|
saveState();
|
||||||
|
}
|
||||||
|
|
||||||
|
function trash(event) {
|
||||||
|
const target = event.target;
|
||||||
|
event.target.style.backgroundColor = '';
|
||||||
|
if (target && dragged) {
|
||||||
|
event.preventDefault();
|
||||||
|
if (dragged.parentNode.className.includes("drop-zone")) {
|
||||||
|
dragged.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDrop(event) {
|
||||||
|
const target = event.target;
|
||||||
|
if (target && dragged) {
|
||||||
|
target.style.backgroundColor = '';
|
||||||
|
event.preventDefault();
|
||||||
|
// Get the id of the target and add the moved element to the target's DOM
|
||||||
|
// dragged.parentNode.removeChild(dragged);
|
||||||
|
if (target.nodeName == "SECTION") {
|
||||||
|
dragged.style.opacity = '';
|
||||||
|
dragged.style.backgroundColor = origcolor;
|
||||||
|
var dropped = dragged.cloneNode(true);
|
||||||
|
dropped.id = 0;
|
||||||
|
target.appendChild(dropped);
|
||||||
|
}
|
||||||
|
if (target.nodeName == 'DIV') {
|
||||||
|
// insert after
|
||||||
|
dragged.style.opacity = '';
|
||||||
|
dragged.style.backgroundColor = origcolor;
|
||||||
|
var dropped = dragged.cloneNode(true);
|
||||||
|
if (target.parentNode.nodeName == 'SECTION') {
|
||||||
|
target.after(dropped);
|
||||||
|
}
|
||||||
|
if (target.parentNode.parentNode.nodeName == 'SECTION') {
|
||||||
|
target.parentNode.after(dropped);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
saveState();
|
||||||
|
}
|
||||||
|
|
||||||
|
function noDrop(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
let dropzone = document.querySelector('.drop-zone')
|
||||||
|
dropzone.addEventListener('dragenter', handleDragEnter);
|
||||||
|
dropzone.addEventListener('dragleave', handleDragLeave);
|
||||||
|
dropzone.addEventListener('drop', handleDrop)
|
||||||
|
dropzone.addEventListener('dragover', handleDragOver);
|
||||||
|
dropzone.addEventListener('dragstart', handleDragStart);
|
||||||
|
dropzone.addEventListener('dragend', handleDragEnd);
|
||||||
|
|
||||||
|
let items = document.querySelectorAll('.stepcontainer .trainingstep');
|
||||||
|
items.forEach(function(item) {
|
||||||
|
item.addEventListener('dragstart', handleDragStart);
|
||||||
|
item.addEventListener('dragend', handleDragEnd);
|
||||||
|
item.addEventListener('dragleave', handleDragLeave);
|
||||||
|
item.addEventListener('drop',noDrop);
|
||||||
|
item.addEventListener('dragenter',noDrop);
|
||||||
|
item.addEventListener('mouseover',handleMouseOver);
|
||||||
|
item.addEventListener('mouseleave',handleMouseLeave);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block sidebar %}
|
||||||
|
{% include 'menu_plan.html' %}
|
||||||
|
{% endblock %}
|
||||||
@@ -123,15 +123,20 @@
|
|||||||
<td> {% if plan.status %} active {% else %} inactive {% endif %}</td>
|
<td> {% if plan.status %} active {% else %} inactive {% endif %}</td>
|
||||||
<td>
|
<td>
|
||||||
{% if request.user.rower == plan.manager %}
|
{% if request.user.rower == plan.manager %}
|
||||||
<a href="/rowers/editplan/{{ plan.id }}">Edit</a>
|
<a href="/rowers/editplan/{{ plan.id }}"><i class="fas fa-pencil-alt fa-fw"></i></a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td> <a href="/rowers/plan/{{ plan.id }}">Plan</a></td>
|
<td> <a href="/rowers/plan/{{ plan.id }}"><i class="fal fa-calendar-alt fa-fw"></i></a></td>
|
||||||
<td>
|
<td>
|
||||||
{% if request.user.rower == plan.manager %}
|
{% if request.user.rower == plan.manager %}
|
||||||
<a href="/rowers/deleteplan/{{ plan.id }}">Delete</a>
|
<a href="/rowers/deleteplan/{{ plan.id }}"><i class="fas fa-trash-alt fa-fw"></i></a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="/rowers/sessions/user/{{ rower.user.id }}?when={{ plan.startdate|date:'Y-m-d' }}/{{ plan.enddate|date:'Y-m-d' }}/">
|
||||||
|
<i class="fas fa-calendar-alt fa-fw"></i>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -875,6 +875,18 @@ urlpatterns = [
|
|||||||
views.rower_create_trainingplan, name='rower_create_trainingplan'),
|
views.rower_create_trainingplan, name='rower_create_trainingplan'),
|
||||||
re_path(r'^plans/$', views.rower_select_instantplan,
|
re_path(r'^plans/$', views.rower_select_instantplan,
|
||||||
name='rower_select_instantplan'),
|
name='rower_select_instantplan'),
|
||||||
|
re_path(r'^plans/step/(?P<id>\d+)/edit/$',
|
||||||
|
views.stepedit, name='stepedit'),
|
||||||
|
re_path(r'^plans/step/(?P<id>\d+)/edit/(?P<psid>\d+)/$',
|
||||||
|
views.stepedit, name='stepedit'),
|
||||||
|
re_path(r'^plans/step/(?P<id>\d+)/delete/$',
|
||||||
|
views.stepdelete, name='stepdelete'),
|
||||||
|
re_path(r'^plans/stepeditor/$',
|
||||||
|
views.stepeditor, name='stepeditor'),
|
||||||
|
re_path(r'^plans/stepeditor/(?P<id>\d+)/$',
|
||||||
|
views.stepeditor, name='stepeditor'),
|
||||||
|
re_path(r'^plans/stepadder/(?P<id>\d+)/$',
|
||||||
|
views.stepadder, name='stepadder'),
|
||||||
re_path(r'^plans/(?P<id>[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12})/$',
|
re_path(r'^plans/(?P<id>[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12})/$',
|
||||||
views.rower_view_instantplan, name='rower_view_instantplan'),
|
views.rower_view_instantplan, name='rower_view_instantplan'),
|
||||||
re_path(r'^buyplan/(?P<id>\d+)/$', views.buy_trainingplan_view,
|
re_path(r'^buyplan/(?P<id>\d+)/$', views.buy_trainingplan_view,
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ from rowers.views.statements import *
|
|||||||
|
|
||||||
import rowers.garmin_stuff as gs
|
import rowers.garmin_stuff as gs
|
||||||
from rowers import credits
|
from rowers import credits
|
||||||
|
from json.decoder import JSONDecodeError
|
||||||
|
from rowers.utils import step_to_string
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@@ -1403,9 +1405,24 @@ def save_plan_yaml(request, userid=0):
|
|||||||
for ps in sps:
|
for ps in sps:
|
||||||
if ps.preferreddate == dd:
|
if ps.preferreddate == dd:
|
||||||
sessionsport = mytypes.fitmapping[ps.sessionsport].capitalize()
|
sessionsport = mytypes.fitmapping[ps.sessionsport].capitalize()
|
||||||
steps = ps.steps
|
if ps.steps:
|
||||||
steps['filename'] = ""
|
steps = ps.steps
|
||||||
workouts.append(steps)
|
steps['filename'] = ""
|
||||||
|
workouts.append(steps)
|
||||||
|
else:
|
||||||
|
if ps.sessionmode == 'distance':
|
||||||
|
ps.interval_string = '{d}m'.format(d=ps.sessionvalue)
|
||||||
|
elif ps.sessionmode == 'time':
|
||||||
|
ps.interval_string = '{d}min'.format(d=ps.sessionvalue)
|
||||||
|
ps.fitfile = ''
|
||||||
|
ps.steps = None
|
||||||
|
ps.save()
|
||||||
|
ps_reload = PlannedSession.objects.get(id=ps.id)
|
||||||
|
steps = ps_reload.steps
|
||||||
|
steps['filename'] = ""
|
||||||
|
steps['workoutName'] = ps.name
|
||||||
|
print(steps)
|
||||||
|
workouts.append(steps)
|
||||||
|
|
||||||
trainingdays.append({'order': i+1, 'workouts': workouts})
|
trainingdays.append({'order': i+1, 'workouts': workouts})
|
||||||
|
|
||||||
@@ -2003,6 +2020,12 @@ def plannedsession_templateedit_view(request, id=0):
|
|||||||
|
|
||||||
sessiontemplates = sessiontemplates | sessiontemplates2
|
sessiontemplates = sessiontemplates | sessiontemplates2
|
||||||
|
|
||||||
|
steps = ''
|
||||||
|
if ps.steps: # pragma: no cover
|
||||||
|
d = ps.steps
|
||||||
|
|
||||||
|
steps = ps_dict_get_description_html(d, short=False)
|
||||||
|
|
||||||
return render(request, 'plannedsessiontemplateedit.html',
|
return render(request, 'plannedsessiontemplateedit.html',
|
||||||
{
|
{
|
||||||
'teams': get_my_teams(request.user),
|
'teams': get_my_teams(request.user),
|
||||||
@@ -2013,6 +2036,7 @@ def plannedsession_templateedit_view(request, id=0):
|
|||||||
'thesession': ps,
|
'thesession': ps,
|
||||||
'sessiontemplates': sessiontemplates,
|
'sessiontemplates': sessiontemplates,
|
||||||
'rower': r,
|
'rower': r,
|
||||||
|
'steps': steps,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@@ -2959,6 +2983,286 @@ def rower_create_trainingplan(request, id=0):
|
|||||||
'old_targets': old_targets,
|
'old_targets': old_targets,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@user_passes_test(can_plan, login_url="/rowers/paidplans",
|
||||||
|
message="This functionality requires a Coach or Self-Coach plan",
|
||||||
|
redirect_field_name=None)
|
||||||
|
def stepadder(request, id=0):
|
||||||
|
is_ajax = request.META.get('HTTP_X_REQUESTED_WITH') == 'XMLHttpRequest'
|
||||||
|
if not is_ajax:
|
||||||
|
return JSONResponse(
|
||||||
|
status=403, data={
|
||||||
|
'status': 'false',
|
||||||
|
'message': 'this view cannot be accessed directly'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
ps = get_object_or_404(PlannedSession, pk=id)
|
||||||
|
|
||||||
|
is_save = request.GET.get('save',0)
|
||||||
|
|
||||||
|
if request.method != 'POST':
|
||||||
|
message = {'status': 'false',
|
||||||
|
'message': 'this view cannot be accessed through GET'}
|
||||||
|
return JSONResponse(status=403, data=message)
|
||||||
|
|
||||||
|
try:
|
||||||
|
json_data = json.loads(request.body)
|
||||||
|
post_data = json_data
|
||||||
|
except (KeyError, JSONDecodeError):
|
||||||
|
q = request.POST
|
||||||
|
post_data = {k: q.getlist(k) if len(
|
||||||
|
q.getlist(k)) > 1 else v for k, v in q.items()}
|
||||||
|
|
||||||
|
# only allow local host
|
||||||
|
hostt = request.get_host().split(':')
|
||||||
|
if hostt[0] not in ['localhost', '127.0.0.1', 'dev.rowsandall.com', 'rowsandall.com']:
|
||||||
|
message = {'status': 'false',
|
||||||
|
'message': 'permission denied for host '+hostt[0]}
|
||||||
|
return JSONResponse(status=403, data=message)
|
||||||
|
|
||||||
|
if ps.steps:
|
||||||
|
filename = ps.steps.get('filename','')
|
||||||
|
sport = ps.steps.get('sport','rowing')
|
||||||
|
else:
|
||||||
|
filename = ''
|
||||||
|
sport = 'rowing'
|
||||||
|
|
||||||
|
steps = {
|
||||||
|
"filename": filename,
|
||||||
|
"sport": sport,
|
||||||
|
"steps": []
|
||||||
|
}
|
||||||
|
|
||||||
|
for nr,id in enumerate(post_data):
|
||||||
|
try:
|
||||||
|
step = PlannedSessionStep.objects.get(id=int(id))
|
||||||
|
# add JSON
|
||||||
|
d = step.asdict()
|
||||||
|
d['stepId'] = nr
|
||||||
|
steps['steps'].append(d)
|
||||||
|
except PlannedSessionStep.DoesNotExist:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if is_save:
|
||||||
|
# save the darn thing
|
||||||
|
ps.steps = steps
|
||||||
|
|
||||||
|
ps.interval_string = ''
|
||||||
|
ps.fitfile = None
|
||||||
|
ps.save()
|
||||||
|
|
||||||
|
return JSONResponse(status=200,data=post_data)
|
||||||
|
|
||||||
|
@user_passes_test(can_plan, login_url="/rowers/paidplans",
|
||||||
|
message="This functionality requires a Coach or Self-Coach plan",
|
||||||
|
redirect_field_name=None)
|
||||||
|
def stepdelete(request, id=0):
|
||||||
|
step = get_object_or_404(PlannedSessionStep, pk=id)
|
||||||
|
|
||||||
|
step.delete()
|
||||||
|
|
||||||
|
backid = request.GET.get('id')
|
||||||
|
|
||||||
|
url = reverse(stepeditor,kwargs={'id':backid})
|
||||||
|
|
||||||
|
return HttpResponseRedirect(url)
|
||||||
|
|
||||||
|
@user_passes_test(can_plan, login_url="/rowers/paidplans",
|
||||||
|
message="This functionality requires a Coach or Self-Coach plan",
|
||||||
|
redirect_field_name=None)
|
||||||
|
def stepedit(request, id=0, psid=0):
|
||||||
|
step = get_object_or_404(PlannedSessionStep, pk=id)
|
||||||
|
try:
|
||||||
|
ps = PlannedSession.objects.get(id=psid)
|
||||||
|
except PlannedSession.DoesNotExist:
|
||||||
|
ps = None
|
||||||
|
|
||||||
|
form = StepEditorForm(instance=step)
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
form = StepEditorForm(request.POST)
|
||||||
|
if form.is_valid():
|
||||||
|
if ps:
|
||||||
|
dd = step.asdict()
|
||||||
|
dd.pop('stepId')
|
||||||
|
for id,ss in enumerate(ps.steps['steps']):
|
||||||
|
ee = ss.copy()
|
||||||
|
ee.pop('stepId')
|
||||||
|
|
||||||
|
if (dd == ee):
|
||||||
|
ss['durationType'] = form.cleaned_data['durationtype']
|
||||||
|
ss['durationValue'] = form.cleaned_data['durationvalue']
|
||||||
|
ss['targetType'] = form.cleaned_data['targettype']
|
||||||
|
ss['targetValue'] = form.cleaned_data['targetvalue']
|
||||||
|
ss['targetValueLow']= form.cleaned_data['targetvaluelow']
|
||||||
|
ss['targetValueHigh'] = form.cleaned_data['targetvaluehigh']
|
||||||
|
ss['intensity'] = form.cleaned_data['intensity']
|
||||||
|
ss['wkt_step_name'] = form.cleaned_data['name']
|
||||||
|
ss['description'] = form.cleaned_data['description']
|
||||||
|
|
||||||
|
if form.cleaned_data['durationtype'] == 'Time':
|
||||||
|
ss['durationValue'] = form.cleaned_data['durationvalue']*60000
|
||||||
|
elif form.cleaned_data['durationtype'] == 'Distance':
|
||||||
|
ss[durationValue] = form.cleaned_data['durationvalue']*100
|
||||||
|
|
||||||
|
ss['durationValue'] = int(ss['durationValue'])
|
||||||
|
ps.fitfile = None
|
||||||
|
ps.interval_string = ""
|
||||||
|
|
||||||
|
ps.save()
|
||||||
|
|
||||||
|
step.durationtype = form.cleaned_data['durationtype']
|
||||||
|
step.durationvalue = form.cleaned_data['durationvalue']
|
||||||
|
step.targettype = form.cleaned_data['targettype']
|
||||||
|
step.targetvalue = form.cleaned_data['targetvalue']
|
||||||
|
step.targetvaluelow = form.cleaned_data['targetvaluelow']
|
||||||
|
step.targetvaluehigh = form.cleaned_data['targetvaluehigh']
|
||||||
|
step.intensity = form.cleaned_data['intensity']
|
||||||
|
step.name = form.cleaned_data['name']
|
||||||
|
step.description = form.cleaned_data['description']
|
||||||
|
|
||||||
|
if step.durationtype == 'Time':
|
||||||
|
step.durationvalue *= 60000
|
||||||
|
elif step.durationtype == 'Distance':
|
||||||
|
step.durationvalue *= 100
|
||||||
|
|
||||||
|
step.save()
|
||||||
|
|
||||||
|
|
||||||
|
if step.durationtype == 'Time':
|
||||||
|
form.fields['durationvalue'].initial = step.durationvalue / 60000
|
||||||
|
elif step.durationtype == 'Distance':
|
||||||
|
form.fields['durationvalue'].initial = step.durationvalue / 100
|
||||||
|
|
||||||
|
|
||||||
|
stepdescription = step_to_string(step.asdict(), short=False)[0]
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
if 'stepsave_and_return' in request.POST:
|
||||||
|
url = reverse('stepeditor',kwargs = {'id': ps.id})
|
||||||
|
return HttpResponseRedirect(url)
|
||||||
|
|
||||||
|
breadcrumbs = [
|
||||||
|
{
|
||||||
|
'url': reverse('template_library_view'),
|
||||||
|
'name': 'Session Templates'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'url': reverse('plannedsession_templateedit_view', kwargs={'id': ps.id}),
|
||||||
|
'name': ps.name
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'url': reverse('stepeditor', kwargs={'id':ps.id}),
|
||||||
|
'name': 'Edit Steps'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'url': reverse('stepedit', kwargs={'psid': ps.id, 'id': step.id}),
|
||||||
|
'name': 'Edit Step'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
return render(request,'stepedit.html',
|
||||||
|
{
|
||||||
|
'step': step,
|
||||||
|
'stepdescription': stepdescription,
|
||||||
|
'form': form,
|
||||||
|
'ps': ps,
|
||||||
|
'breadcrumbs': breadcrumbs,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@user_passes_test(can_plan, login_url="/rowers/paidplans",
|
||||||
|
message="This functionality requires a Coach or Self-Coach plan",
|
||||||
|
redirect_field_name=None)
|
||||||
|
def stepeditor(request, id=0):
|
||||||
|
ps = get_object_or_404(PlannedSession, pk=id)
|
||||||
|
|
||||||
|
currentsteps = []
|
||||||
|
if ps.steps:
|
||||||
|
for step in ps.steps['steps']:
|
||||||
|
durationtype = step.get('durationType','')
|
||||||
|
durationvalue = step.get('durationValue',0)
|
||||||
|
targetvalue = step.get('targetValue',0)
|
||||||
|
targettype = step.get('targetType','')
|
||||||
|
targetvaluelow = step.get('targetValueLow',0)
|
||||||
|
targetvaluehigh = step.get('targetValueHigh',0)
|
||||||
|
intensity = step.get('intensity','Active')
|
||||||
|
|
||||||
|
archived_steps = PlannedSessionStep.objects.filter(
|
||||||
|
manager = request.user,
|
||||||
|
durationtype = durationtype,
|
||||||
|
durationvalue = durationvalue,
|
||||||
|
targetvalue = targetvalue,
|
||||||
|
targettype = targettype,
|
||||||
|
targetvaluelow = targetvaluelow,
|
||||||
|
targetvaluehigh = targetvaluehigh,
|
||||||
|
intensity = intensity,
|
||||||
|
)
|
||||||
|
if not archived_steps.count() and durationvalue != 0:
|
||||||
|
s = PlannedSessionStep(
|
||||||
|
manager = request.user,
|
||||||
|
durationtype = durationtype,
|
||||||
|
durationvalue = durationvalue,
|
||||||
|
targetvalue = targetvalue,
|
||||||
|
targettype = targettype,
|
||||||
|
targetvaluelow = targetvaluelow,
|
||||||
|
targetvaluehigh = targetvaluehigh,
|
||||||
|
intensity = intensity,
|
||||||
|
name = step.get('wkt_step_name','Step')
|
||||||
|
)
|
||||||
|
s.save()
|
||||||
|
else:
|
||||||
|
s = archived_steps[0]
|
||||||
|
currentsteps.append(s)
|
||||||
|
|
||||||
|
|
||||||
|
form = StepEditorForm()
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
form = StepEditorForm(request.POST)
|
||||||
|
if form.is_valid():
|
||||||
|
step = form.save(commit=False)
|
||||||
|
step.manager = request.user
|
||||||
|
step.save()
|
||||||
|
|
||||||
|
|
||||||
|
steps = PlannedSessionStep.objects.filter(
|
||||||
|
manager=request.user,
|
||||||
|
durationtype__in=['Time','Distance','RepeatUntilStepsCmplt']
|
||||||
|
).order_by(
|
||||||
|
'intensity','-durationvalue','durationtype',
|
||||||
|
)
|
||||||
|
|
||||||
|
stepdescriptions = {}
|
||||||
|
|
||||||
|
for step in steps:
|
||||||
|
stepdescriptions[step.id] = step_to_string(step.asdict(), short=False)[0]
|
||||||
|
|
||||||
|
breadcrumbs = [
|
||||||
|
{
|
||||||
|
'url': reverse('template_library_view'),
|
||||||
|
'name': 'Session Templates'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'url': reverse('plannedsession_templateedit_view', kwargs={'id': ps.id}),
|
||||||
|
'name': ps.name
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'url': reverse('stepeditor', kwargs={'id': ps.id}),
|
||||||
|
'name': 'Edit Steps'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
return render(request, 'stepeditor.html',
|
||||||
|
{
|
||||||
|
'steps':steps,
|
||||||
|
'currentsteps': currentsteps,
|
||||||
|
'stepdescriptions': stepdescriptions,
|
||||||
|
'form':form,
|
||||||
|
'ps':ps,
|
||||||
|
'breadcrumbs': breadcrumbs,
|
||||||
|
})
|
||||||
|
|
||||||
@user_passes_test(can_plan, login_url="/rowers/paidplans",
|
@user_passes_test(can_plan, login_url="/rowers/paidplans",
|
||||||
message="This functionality requires a Coach or Self-Coach plan",
|
message="This functionality requires a Coach or Self-Coach plan",
|
||||||
|
|||||||
@@ -151,7 +151,8 @@ from rowers.models import (
|
|||||||
PlannedSessionComment, CoachRequest, CoachOffer,
|
PlannedSessionComment, CoachRequest, CoachOffer,
|
||||||
VideoAnalysis, ShareKey,
|
VideoAnalysis, ShareKey,
|
||||||
StandardCollection, CourseStandard,
|
StandardCollection, CourseStandard,
|
||||||
VirtualRaceFollower, TombStone, InstantPlan
|
VirtualRaceFollower, TombStone, InstantPlan,
|
||||||
|
PlannedSessionStep,
|
||||||
)
|
)
|
||||||
from rowers.models import (
|
from rowers.models import (
|
||||||
RowerPowerForm, RowerHRZonesForm, RowerForm, RowerCPForm, GraphImage, AdvancedWorkoutForm,
|
RowerPowerForm, RowerHRZonesForm, RowerForm, RowerCPForm, GraphImage, AdvancedWorkoutForm,
|
||||||
@@ -166,7 +167,8 @@ from rowers.models import (
|
|||||||
IndoorVirtualRaceResultForm, IndoorVirtualRaceResult,
|
IndoorVirtualRaceResultForm, IndoorVirtualRaceResult,
|
||||||
IndoorVirtualRaceForm, PlannedSessionCommentForm,
|
IndoorVirtualRaceForm, PlannedSessionCommentForm,
|
||||||
Alert, Condition, StaticChartRowerForm,
|
Alert, Condition, StaticChartRowerForm,
|
||||||
FollowerForm, VirtualRaceAthleteForm, InstantPlanForm, DataRowerForm
|
FollowerForm, VirtualRaceAthleteForm, InstantPlanForm, DataRowerForm,
|
||||||
|
StepEditorForm,
|
||||||
)
|
)
|
||||||
from rowers.models import (
|
from rowers.models import (
|
||||||
FavoriteForm, BaseFavoriteFormSet, SiteAnnouncement, BasePlannedSessionFormSet,
|
FavoriteForm, BaseFavoriteFormSet, SiteAnnouncement, BasePlannedSessionFormSet,
|
||||||
|
|||||||
@@ -322,14 +322,70 @@ th.rotate > div > span {
|
|||||||
margin: 2px;
|
margin: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.stepcontainer {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 4fr 1fr 4fr;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trainingstep {
|
||||||
|
border: 3px solid #666;
|
||||||
|
background-color: #ddd;
|
||||||
|
border-radius: .5em;
|
||||||
|
padding: 10px;
|
||||||
|
cursor: move;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trainingstep.over {
|
||||||
|
border: 3px dotted #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trainingstep.Warmup {
|
||||||
|
background-color: #ffcccb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trainingstep.Cooldown {
|
||||||
|
background-color: #90ee90;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trainingstep.Rest {
|
||||||
|
background-color: #add8e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.RepeatUntilStepsCmplt {
|
||||||
|
background-color: #ffffa7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop-zone {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: #D7D7D7;
|
||||||
|
/* color: white; */
|
||||||
|
padding: 10px;
|
||||||
|
padding-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.allcentered {
|
||||||
|
/* Center vertically and horizontally */
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.allcenteredchild {
|
||||||
|
margin-top: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
.divlines {
|
.divlines {
|
||||||
display: block;
|
width: 50px;
|
||||||
overflow-x: hidden;
|
height: 50px;
|
||||||
border-width: 1px 0 0 0;
|
background-color: red;
|
||||||
border-color: #333 #333 #333 #333;
|
/* Center vertically and horizontally */
|
||||||
border-style: solid;
|
position: absolute;
|
||||||
padding: 0px;
|
top: 50%;
|
||||||
margin: 0px;
|
left: 50%;
|
||||||
|
margin: -25px 0 0 -25px; /* Apply negative top and left margins to truly center the element */
|
||||||
}
|
}
|
||||||
|
|
||||||
.workoutcontainer {
|
.workoutcontainer {
|
||||||
|
|||||||
Reference in New Issue
Block a user