Merge pull request #65 from SamR1/fix-workouts-display

Fix workouts display on calendar
This commit is contained in:
Sam 2021-02-17 14:13:30 +01:00 committed by GitHub
commit 8796e718a6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 134 additions and 58 deletions

View File

@ -42,7 +42,7 @@ Workouts
- Montain Biking - Montain Biking
- Running - Running
- Walking - Walking
- Dashboard with month calendar displaying workouts and record. The week can start on Sunday or Monday (which can be changed in the user settings) - Dashboard with month calendar displaying workouts and record. The week can start on Sunday or Monday (which can be changed in the user settings). The calendar displays up to 100 workouts.
- Workout creation by uploading a gpx file. A workout can even be created without gpx (the user must enter date, time, duration and distance) - Workout creation by uploading a gpx file. A workout can even be created without gpx (the user must enter date, time, duration and distance)
- A workout with a gpx file can be displayed with map, weather (if the DarkSky API key is provided) and charts (speed and elevation). Segments can be displayed - A workout with a gpx file can be displayed with map, weather (if the DarkSky API key is provided) and charts (speed and elevation). Segments can be displayed
- Workout edition and deletion. User can add a note - Workout edition and deletion. User can add a note

View File

@ -249,7 +249,7 @@
<dd class="field-even"><ul class="simple"> <dd class="field-even"><ul class="simple">
<li><p><strong>page</strong> (<em>integer</em>) page if using pagination (default: 1)</p></li> <li><p><strong>page</strong> (<em>integer</em>) page if using pagination (default: 1)</p></li>
<li><p><strong>per_page</strong> (<em>integer</em>) number of workouts per page <li><p><strong>per_page</strong> (<em>integer</em>) number of workouts per page
(default: 5, max: 50)</p></li> (default: 5, max: 100)</p></li>
<li><p><strong>sport_id</strong> (<em>integer</em>) sport id</p></li> <li><p><strong>sport_id</strong> (<em>integer</em>) sport id</p></li>
<li><p><strong>from</strong> (<em>string</em>) start date (format: <code class="docutils literal notranslate"><span class="pre">%Y-%m-%d</span></code>)</p></li> <li><p><strong>from</strong> (<em>string</em>) start date (format: <code class="docutils literal notranslate"><span class="pre">%Y-%m-%d</span></code>)</p></li>
<li><p><strong>to</strong> (<em>string</em>) end date (format: <code class="docutils literal notranslate"><span class="pre">%Y-%m-%d</span></code>)</p></li> <li><p><strong>to</strong> (<em>string</em>) end date (format: <code class="docutils literal notranslate"><span class="pre">%Y-%m-%d</span></code>)</p></li>

View File

@ -191,7 +191,7 @@
</dd> </dd>
</dl> </dl>
</li> </li>
<li><p>Dashboard with month calendar displaying workouts and record. The week can start on Sunday or Monday (which can be changed in the user settings)</p></li> <li><p>Dashboard with month calendar displaying workouts and record. The week can start on Sunday or Monday (which can be changed in the user settings). The calendar displays up to 100 workouts.</p></li>
<li><p>Workout creation by uploading a gpx file. A workout can even be created without gpx (the user must enter date, time, duration and distance)</p></li> <li><p>Workout creation by uploading a gpx file. A workout can even be created without gpx (the user must enter date, time, duration and distance)</p></li>
<li><p>A workout with a gpx file can be displayed with map, weather (if the DarkSky API key is provided) and charts (speed and elevation). Segments can be displayed</p></li> <li><p>A workout with a gpx file can be displayed with map, weather (if the DarkSky API key is provided) and charts (speed and elevation). Segments can be displayed</p></li>
<li><p>Workout edition and deletion. User can add a note</p></li> <li><p>Workout edition and deletion. User can add a note</p></li>

File diff suppressed because one or more lines are too long

View File

@ -42,7 +42,7 @@ Workouts
- Montain Biking - Montain Biking
- Running - Running
- Walking - Walking
- Dashboard with month calendar displaying workouts and record. The week can start on Sunday or Monday (which can be changed in the user settings) - Dashboard with month calendar displaying workouts and record. The week can start on Sunday or Monday (which can be changed in the user settings). The calendar displays up to 100 workouts.
- Workout creation by uploading a gpx file. A workout can even be created without gpx (the user must enter date, time, duration and distance) - Workout creation by uploading a gpx file. A workout can even be created without gpx (the user must enter date, time, duration and distance)
- A workout with a gpx file can be displayed with map, weather (if the DarkSky API key is provided) and charts (speed and elevation). Segments can be displayed - A workout with a gpx file can be displayed with map, weather (if the DarkSky API key is provided) and charts (speed and elevation). Segments can be displayed
- Workout edition and deletion. User can add a note - Workout edition and deletion. User can add a note

View File

@ -1,8 +1,8 @@
{ {
"files": { "files": {
"main.css": "/static/css/main.376b8924.chunk.css", "main.css": "/static/css/main.376b8924.chunk.css",
"main.js": "/static/js/main.b00026d7.chunk.js", "main.js": "/static/js/main.75e5c894.chunk.js",
"main.js.map": "/static/js/main.b00026d7.chunk.js.map", "main.js.map": "/static/js/main.75e5c894.chunk.js.map",
"runtime-main.js": "/static/js/runtime-main.1240af94.js", "runtime-main.js": "/static/js/runtime-main.1240af94.js",
"runtime-main.js.map": "/static/js/runtime-main.1240af94.js.map", "runtime-main.js.map": "/static/js/runtime-main.1240af94.js.map",
"static/js/2.1ec1bf1c.chunk.js": "/static/js/2.1ec1bf1c.chunk.js", "static/js/2.1ec1bf1c.chunk.js": "/static/js/2.1ec1bf1c.chunk.js",
@ -19,6 +19,6 @@
"static/js/runtime-main.1240af94.js", "static/js/runtime-main.1240af94.js",
"static/js/2.1ec1bf1c.chunk.js", "static/js/2.1ec1bf1c.chunk.js",
"static/css/main.376b8924.chunk.css", "static/css/main.376b8924.chunk.css",
"static/js/main.b00026d7.chunk.js" "static/js/main.75e5c894.chunk.js"
] ]
} }

View File

@ -1 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no"><meta name="theme-color" content="#000000"><link rel="manifest" href="/manifest.json"><link rel="shortcut icon" href="/favicon.ico"><link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous"><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/fork-awesome@1.1.7/css/fork-awesome.min.css" integrity="sha256-gsmEoJAws/Kd3CjuOQzLie5Q3yshhvmo7YNtBG7aaEY=" crossorigin="anonymous"><link rel="stylesheet" href="https://cdn.jsdelivr.net/foundation-icons/3.0/foundation-icons.min.css"><link rel="stylesheet" href="https://unpkg.com/leaflet@1.6.0/dist/leaflet.css" integrity="sha512-xwE/Az9zrjBIphAcBb3F6JVqxf46+CDLwfLMHloNu6KEQCAWi6HcDUbeOfBIptF7tcCzusKFjFw2yuvEpDL9wQ==" crossorigin=""><title>FitTrackee</title><link href="/static/css/main.376b8924.chunk.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div><script src="https://code.jquery.com/jquery-3.4.1.slim.min.js" integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n" crossorigin="anonymous"></script><script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script><script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script><script type="text/javascript">$(document).ready((function(){$("li.nav-item").click((function(){$("button.navbar-toggler").toggleClass("collapsed"),$("#navbarSupportedContent").toggleClass("show")}))}))</script><script>!function(e){function t(t){for(var n,i,l=t[0],f=t[1],a=t[2],p=0,s=[];p<l.length;p++)i=l[p],Object.prototype.hasOwnProperty.call(o,i)&&o[i]&&s.push(o[i][0]),o[i]=0;for(n in f)Object.prototype.hasOwnProperty.call(f,n)&&(e[n]=f[n]);for(c&&c(t);s.length;)s.shift()();return u.push.apply(u,a||[]),r()}function r(){for(var e,t=0;t<u.length;t++){for(var r=u[t],n=!0,l=1;l<r.length;l++){var f=r[l];0!==o[f]&&(n=!1)}n&&(u.splice(t--,1),e=i(i.s=r[0]))}return e}var n={},o={1:0},u=[];function i(t){if(n[t])return n[t].exports;var r=n[t]={i:t,l:!1,exports:{}};return e[t].call(r.exports,r,r.exports,i),r.l=!0,r.exports}i.m=e,i.c=n,i.d=function(e,t,r){i.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},i.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},i.t=function(e,t){if(1&t&&(e=i(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(i.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var n in e)i.d(r,n,function(t){return e[t]}.bind(null,n));return r},i.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return i.d(t,"a",t),t},i.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},i.p="/";var l=this.webpackJsonpfittrackee_client=this.webpackJsonpfittrackee_client||[],f=l.push.bind(l);l.push=t,l=l.slice();for(var a=0;a<l.length;a++)t(l[a]);var c=f;r()}([])</script><script src="/static/js/2.1ec1bf1c.chunk.js"></script><script src="/static/js/main.b00026d7.chunk.js"></script></body></html> <!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no"><meta name="theme-color" content="#000000"><link rel="manifest" href="/manifest.json"><link rel="shortcut icon" href="/favicon.ico"><link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous"><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/fork-awesome@1.1.7/css/fork-awesome.min.css" integrity="sha256-gsmEoJAws/Kd3CjuOQzLie5Q3yshhvmo7YNtBG7aaEY=" crossorigin="anonymous"><link rel="stylesheet" href="https://cdn.jsdelivr.net/foundation-icons/3.0/foundation-icons.min.css"><link rel="stylesheet" href="https://unpkg.com/leaflet@1.6.0/dist/leaflet.css" integrity="sha512-xwE/Az9zrjBIphAcBb3F6JVqxf46+CDLwfLMHloNu6KEQCAWi6HcDUbeOfBIptF7tcCzusKFjFw2yuvEpDL9wQ==" crossorigin=""><title>FitTrackee</title><link href="/static/css/main.376b8924.chunk.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div><script src="https://code.jquery.com/jquery-3.4.1.slim.min.js" integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n" crossorigin="anonymous"></script><script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script><script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script><script type="text/javascript">$(document).ready((function(){$("li.nav-item").click((function(){$("button.navbar-toggler").toggleClass("collapsed"),$("#navbarSupportedContent").toggleClass("show")}))}))</script><script>!function(e){function t(t){for(var n,i,l=t[0],f=t[1],a=t[2],p=0,s=[];p<l.length;p++)i=l[p],Object.prototype.hasOwnProperty.call(o,i)&&o[i]&&s.push(o[i][0]),o[i]=0;for(n in f)Object.prototype.hasOwnProperty.call(f,n)&&(e[n]=f[n]);for(c&&c(t);s.length;)s.shift()();return u.push.apply(u,a||[]),r()}function r(){for(var e,t=0;t<u.length;t++){for(var r=u[t],n=!0,l=1;l<r.length;l++){var f=r[l];0!==o[f]&&(n=!1)}n&&(u.splice(t--,1),e=i(i.s=r[0]))}return e}var n={},o={1:0},u=[];function i(t){if(n[t])return n[t].exports;var r=n[t]={i:t,l:!1,exports:{}};return e[t].call(r.exports,r,r.exports,i),r.l=!0,r.exports}i.m=e,i.c=n,i.d=function(e,t,r){i.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},i.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},i.t=function(e,t){if(1&t&&(e=i(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(i.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var n in e)i.d(r,n,function(t){return e[t]}.bind(null,n));return r},i.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return i.d(t,"a",t),t},i.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},i.p="/";var l=this.webpackJsonpfittrackee_client=this.webpackJsonpfittrackee_client||[],f=l.push.bind(l);l.push=t,l=l.slice();for(var a=0;a<l.length;a++)t(l[a]);var c=f;r()}([])</script><script src="/static/js/2.1ec1bf1c.chunk.js"></script><script src="/static/js/main.75e5c894.chunk.js"></script></body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,5 @@
import json import json
from unittest.mock import patch
from uuid import uuid4 from uuid import uuid4
from flask import Flask from flask import Flask
@ -278,7 +279,8 @@ class TestGetWorkoutsWithPagination:
in data['message'] in data['message']
) )
def test_it_gets_5_workouts_per_page( @patch('fittrackee.workouts.workouts.MAX_WORKOUTS_PER_PAGE', 6)
def test_it_gets_max_workouts_per_page_if_per_page_exceeds_max(
self, self,
app: Flask, app: Flask,
user_1: User, user_1: User,
@ -303,17 +305,18 @@ class TestGetWorkoutsWithPagination:
data = json.loads(response.data.decode()) data = json.loads(response.data.decode())
assert response.status_code == 200 assert response.status_code == 200
assert 'success' in data['status'] assert 'success' in data['status']
assert len(data['data']['workouts']) == 7 assert len(data['data']['workouts']) == 6
assert ( assert (
'Wed, 09 May 2018 00:00:00 GMT' 'Wed, 09 May 2018 00:00:00 GMT'
== data['data']['workouts'][0]['workout_date'] == data['data']['workouts'][0]['workout_date']
) )
assert ( assert (
'Mon, 20 Mar 2017 00:00:00 GMT' 'Thu, 01 Jun 2017 00:00:00 GMT'
== data['data']['workouts'][6]['workout_date'] == data['data']['workouts'][5]['workout_date']
) )
def test_it_gets_3_workouts_per_page( @patch('fittrackee.workouts.workouts.MAX_WORKOUTS_PER_PAGE', 6)
def test_it_gets_given_number_of_workouts_per_page(
self, self,
app: Flask, app: Flask,
user_1: User, user_1: User,
@ -349,6 +352,113 @@ class TestGetWorkoutsWithPagination:
) )
class TestGetWorkoutsWithOrder:
def test_it_gets_workouts_with_default_order(
self,
app: Flask,
user_1: User,
sport_1_cycling: Sport,
seven_workouts_user_1: Workout,
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
data=json.dumps(dict(email='test@test.com', password='12345678')),
content_type='application/json',
)
response = client.get(
'/api/workouts',
headers=dict(
Authorization='Bearer '
+ json.loads(resp_login.data.decode())['auth_token']
),
)
data = json.loads(response.data.decode())
assert response.status_code == 200
assert 'success' in data['status']
assert len(data['data']['workouts']) == 5
assert (
'Wed, 09 May 2018 00:00:00 GMT'
== data['data']['workouts'][0]['workout_date']
)
assert (
'Mon, 01 Jan 2018 00:00:00 GMT'
== data['data']['workouts'][4]['workout_date']
)
def test_it_gets_workouts_with_ascending_order(
self,
app: Flask,
user_1: User,
sport_1_cycling: Sport,
seven_workouts_user_1: Workout,
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
data=json.dumps(dict(email='test@test.com', password='12345678')),
content_type='application/json',
)
response = client.get(
'/api/workouts?order=asc',
headers=dict(
Authorization='Bearer '
+ json.loads(resp_login.data.decode())['auth_token']
),
)
data = json.loads(response.data.decode())
assert response.status_code == 200
assert 'success' in data['status']
assert len(data['data']['workouts']) == 5
assert (
'Mon, 20 Mar 2017 00:00:00 GMT'
== data['data']['workouts'][0]['workout_date']
)
assert (
'Fri, 23 Feb 2018 00:00:00 GMT'
== data['data']['workouts'][4]['workout_date']
)
def test_it_gets_workouts_with_descending_order(
self,
app: Flask,
user_1: User,
sport_1_cycling: Sport,
seven_workouts_user_1: Workout,
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
data=json.dumps(dict(email='test@test.com', password='12345678')),
content_type='application/json',
)
response = client.get(
'/api/workouts?order=desc',
headers=dict(
Authorization='Bearer '
+ json.loads(resp_login.data.decode())['auth_token']
),
)
data = json.loads(response.data.decode())
assert response.status_code == 200
assert 'success' in data['status']
assert len(data['data']['workouts']) == 5
assert (
'Wed, 09 May 2018 00:00:00 GMT'
== data['data']['workouts'][0]['workout_date']
)
assert (
'Mon, 01 Jan 2018 00:00:00 GMT'
== data['data']['workouts'][4]['workout_date']
)
class TestGetWorkoutsWithFilters: class TestGetWorkoutsWithFilters:
def test_it_gets_workouts_with_date_filter( def test_it_gets_workouts_with_date_filter(
self, self,
@ -487,41 +597,6 @@ class TestGetWorkoutsWithFilters:
== data['data']['workouts'][1]['workout_date'] == data['data']['workouts'][1]['workout_date']
) )
def test_it_gets_workouts_with_ascending_order(
self,
app: Flask,
user_1: User,
sport_1_cycling: Sport,
seven_workouts_user_1: Workout,
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
data=json.dumps(dict(email='test@test.com', password='12345678')),
content_type='application/json',
)
response = client.get(
'/api/workouts?order=asc',
headers=dict(
Authorization='Bearer '
+ json.loads(resp_login.data.decode())['auth_token']
),
)
data = json.loads(response.data.decode())
assert response.status_code == 200
assert 'success' in data['status']
assert len(data['data']['workouts']) == 5
assert (
'Mon, 20 Mar 2017 00:00:00 GMT'
== data['data']['workouts'][0]['workout_date']
)
assert (
'Fri, 23 Feb 2018 00:00:00 GMT'
== data['data']['workouts'][4]['workout_date']
)
def test_it_gets_workouts_with_distance_filter( def test_it_gets_workouts_with_distance_filter(
self, self,
app: Flask, app: Flask,

View File

@ -44,7 +44,8 @@ from .utils_id import decode_short_id
workouts_blueprint = Blueprint('workouts', __name__) workouts_blueprint = Blueprint('workouts', __name__)
WORKOUTS_PER_PAGE = 5 DEFAULT_WORKOUTS_PER_PAGE = 5
MAX_WORKOUTS_PER_PAGE = 100
@workouts_blueprint.route('/workouts', methods=['GET']) @workouts_blueprint.route('/workouts', methods=['GET'])
@ -168,7 +169,7 @@ def get_workouts(auth_user_id: int) -> Union[Dict, HttpResponse]:
:query integer page: page if using pagination (default: 1) :query integer page: page if using pagination (default: 1)
:query integer per_page: number of workouts per page :query integer per_page: number of workouts per page
(default: 5, max: 50) (default: 5, max: 100)
:query integer sport_id: sport id :query integer sport_id: sport id
:query string from: start date (format: ``%Y-%m-%d``) :query string from: start date (format: ``%Y-%m-%d``)
:query string to: end date (format: ``%Y-%m-%d``) :query string to: end date (format: ``%Y-%m-%d``)
@ -219,10 +220,10 @@ def get_workouts(auth_user_id: int) -> Union[Dict, HttpResponse]:
per_page = ( per_page = (
int(params.get('per_page')) int(params.get('per_page'))
if params.get('per_page') if params.get('per_page')
else WORKOUTS_PER_PAGE else DEFAULT_WORKOUTS_PER_PAGE
) )
if per_page > 50: if per_page > MAX_WORKOUTS_PER_PAGE:
per_page = 50 per_page = MAX_WORKOUTS_PER_PAGE
workouts = ( workouts = (
Workout.query.filter( Workout.query.filter(
Workout.user_id == auth_user_id, Workout.user_id == auth_user_id,

View File

@ -179,7 +179,7 @@ export const getMonthWorkouts = (from, to) => dispatch =>
FitTrackeeGenericApi.getData('workouts', { FitTrackeeGenericApi.getData('workouts', {
from, from,
to, to,
order: 'asc', order: 'desc',
per_page: 100, per_page: 100,
}) })
.then(ret => { .then(ret => {