API - replace 'Activity' with 'Workout' - #58

This commit is contained in:
Sam 2021-01-10 11:16:43 +01:00
parent 24ee5bbcfa
commit 3a80e01cc2
70 changed files with 2746 additions and 2511 deletions

View File

@ -19,6 +19,9 @@ clean-install:
rm -rf .pytest_cache
rm -rf dist/
downgrade-db:
$(FLASK) db downgrade --directory $(MIGRATIONS)
html:
rm -rf docsrc/build
rm -rf docs/*

View File

@ -14,7 +14,7 @@
---
This web application allows you to track your outdoor activities from gpx files and keep your data on your own server.
This web application allows you to track your outdoor activities (workouts) from gpx files and keep your data on your own server.
No mobile app is developed yet, but several existing mobile apps can store workouts data locally and export them into a gpx file.
Examples (for Android):
* [Runner Up](https://github.com/jonasoreland/runnerup) (GPL v3)

View File

@ -1,17 +0,0 @@
Activities
##########
.. autoflask:: fittrackee:create_app()
:endpoints:
activities.get_activities,
activities.get_activity,
activities.get_activity_gpx,
activities.get_activity_chart_data,
activities.get_segment_chart_data,
activities.get_segment_gpx,
activities.get_map,
activities.get_map_tile,
activities.post_activity,
activities.post_activity_no_gpx,
activities.update_activity,
activities.delete_activity

View File

@ -5,10 +5,10 @@ API documentation
:maxdepth: 2
:caption: Endpoints:
activities
auth
configuration
records
sports
stats
users
workouts

View File

@ -3,6 +3,6 @@ Statistics
.. autoflask:: fittrackee:create_app()
:endpoints:
stats.get_activities_by_time,
stats.get_activities_by_sport,
stats.get_workouts_by_time,
stats.get_workouts_by_sport,
stats.get_application_stats

View File

@ -0,0 +1,17 @@
Workouts
##########
.. autoflask:: fittrackee:create_app()
:endpoints:
workouts.get_workouts,
workouts.get_workout,
workouts.get_workout_gpx,
workouts.get_workout_chart_data,
workouts.get_segment_chart_data,
workouts.get_segment_gpx,
workouts.get_map,
workouts.get_map_tile,
workouts.post_workout,
workouts.post_workout_no_gpx,
workouts.update_workout,
workouts.delete_workout

View File

@ -25,7 +25,7 @@ Administration
- **Sports**
- enable or disable a sport (a sport can be disabled even if activity with this sport exists)
- enable or disable a sport (a sport can be disabled even if workout with this sport exists)
Account
^^^^^^^
@ -33,29 +33,29 @@ Account
- A user can reset his password (*new in 0.3.0*)
Activities/Workouts
^^^^^^^^^^^^^^^^^^^
- 6 sports supported:
Workouts
^^^^^^^^
- 6 sports are supported:
- Cycling (Sport)
- Cycling (Transport)
- Hiking
- Montain Biking
- Running
- Walking
- Dashboard with month calendar displaying activities and record. The week can start on Sunday or Monday (which can be changed in the user settings)
- Activity creation by uploading a gpx file. An activity can even be created without gpx (the user must enter date, time, duration and distance)
- An activity 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
- Activity edition and deletion. User can add a note
- Dashboard with month calendar displaying workouts and record. The week can start on Sunday or Monday (which can be changed in the user settings)
- 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
- Workout edition and deletion. User can add a note
- User statistics
- User records by sports:
- average speed
- farest distance
- longest duration
- maximum speed
- Activities list and filter
- Workours list and filter
.. note::
for now, only the owner of the activity can see the activity.
for now, only the owner of the workout can see it.
Translations
^^^^^^^^^^^^
@ -69,16 +69,16 @@ Dashboard
:alt: FitTrackee Dashboard
Activity/workout detail
Workout detail
~~~~~~~~~~~~~~~~~~~~~~~
.. figure:: _images/fittrackee_screenshot-02.png
:alt: FitTrackee Activity
:alt: FitTrackee Workout
Activities/workouts list
~~~~~~~~~~~~~~~~~~~~~~~~
Workouts list
~~~~~~~~~~~~~
.. figure:: _images/fittrackee_screenshot-03.png
:alt: FitTrackee Activities
:alt: FitTrackee Workouts
Statistics

View File

@ -5,8 +5,8 @@
FitTrackee
==========
| This web application allows you to track your outdoor activities from
gpx files and keep your data on your own server.
| This web application allows you to track your outdoor activities (workouts)
from gpx files and keep your data on your own server.
| No mobile app is developed yet, but several existing mobile apps can
store workouts data locally and export them into a gpx file.
| Examples (for Android):

View File

@ -16,7 +16,7 @@
<link rel="index" title="Index" href="../genindex.html" />
<link rel="search" title="Search" href="../search.html" />
<link rel="next" title="Configuration" href="configuration.html" />
<link rel="prev" title="Activities" href="activities.html" />
<link rel="prev" title="API documentation" href="index.html" />
<meta charset='utf-8'>
<meta http-equiv='X-UA-Compatible' content='IE=edge,chrome=1'>
<meta name='viewport' content='width=device-width, initial-scale=1.0, maximum-scale=1'>
@ -86,7 +86,7 @@
<li>
<a href="activities.html" title="Previous Chapter: Activities"><span class="glyphicon glyphicon-chevron-left visible-sm"></span><span class="hidden-sm hidden-tablet">&laquo; Activities</span>
<a href="index.html" title="Previous Chapter: API documentation"><span class="glyphicon glyphicon-chevron-left visible-sm"></span><span class="hidden-sm hidden-tablet">&laquo; API documentation</span>
</a>
</li>
<li>
@ -321,8 +321,8 @@
<span class="nt">&quot;language&quot;</span><span class="p">:</span> <span class="s2">&quot;en&quot;</span><span class="p">,</span>
<span class="nt">&quot;last_name&quot;</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
<span class="nt">&quot;location&quot;</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
<span class="nt">&quot;nb_activities&quot;</span><span class="p">:</span> <span class="mi">6</span><span class="p">,</span>
<span class="nt">&quot;nb_sports&quot;</span><span class="p">:</span> <span class="mi">3</span><span class="p">,</span>
<span class="nt">&quot;nb_workouts&quot;</span><span class="p">:</span> <span class="mi">6</span><span class="p">,</span>
<span class="nt">&quot;picture&quot;</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
<span class="nt">&quot;sports_list&quot;</span><span class="p">:</span> <span class="p">[</span>
<span class="mi">1</span><span class="p">,</span>
@ -383,8 +383,8 @@
<span class="nt">&quot;language&quot;</span><span class="p">:</span> <span class="s2">&quot;en&quot;</span><span class="p">,</span>
<span class="nt">&quot;last_name&quot;</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
<span class="nt">&quot;location&quot;</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
<span class="nt">&quot;nb_activities&quot;</span><span class="p">:</span> <span class="mi">6</span><span class="p">,</span>
<span class="nt">&quot;nb_sports&quot;</span><span class="p">:</span> <span class="mi">3</span><span class="p">,</span>
<span class="nt">&quot;nb_workouts&quot;</span><span class="p">:</span> <span class="mi">6</span><span class="p">,</span>
<span class="nt">&quot;picture&quot;</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
<span class="nt">&quot;sports_list&quot;</span><span class="p">:</span> <span class="p">[</span>
<span class="mi">1</span><span class="p">,</span>

View File

@ -15,7 +15,7 @@
<script src="../_static/doctools.js"></script>
<link rel="index" title="Index" href="../genindex.html" />
<link rel="search" title="Search" href="../search.html" />
<link rel="next" title="Activities" href="activities.html" />
<link rel="next" title="Authentication" href="auth.html" />
<link rel="prev" title="Features" href="../features.html" />
<meta charset='utf-8'>
<meta http-equiv='X-UA-Compatible' content='IE=edge,chrome=1'>
@ -90,7 +90,7 @@
</a>
</li>
<li>
<a href="activities.html" title="Next Chapter: Activities"><span class="glyphicon glyphicon-chevron-right visible-sm"></span><span class="hidden-sm hidden-tablet">Activities &raquo;</span>
<a href="auth.html" title="Next Chapter: Authentication"><span class="glyphicon glyphicon-chevron-right visible-sm"></span><span class="hidden-sm hidden-tablet">Authentication &raquo;</span>
</a>
</li>
@ -129,13 +129,13 @@
<div class="toctree-wrapper compound">
<p class="caption"><span class="caption-text">Endpoints:</span></p>
<ul>
<li class="toctree-l1"><a class="reference internal" href="activities.html">Activities</a></li>
<li class="toctree-l1"><a class="reference internal" href="auth.html">Authentication</a></li>
<li class="toctree-l1"><a class="reference internal" href="configuration.html">Configuration</a></li>
<li class="toctree-l1"><a class="reference internal" href="records.html">Records</a></li>
<li class="toctree-l1"><a class="reference internal" href="sports.html">Sports</a></li>
<li class="toctree-l1"><a class="reference internal" href="stats.html">Statistics</a></li>
<li class="toctree-l1"><a class="reference internal" href="users.html">Users</a></li>
<li class="toctree-l1"><a class="reference internal" href="workouts.html">Workouts</a></li>
</ul>
</div>
</div>

View File

@ -155,40 +155,40 @@
<span class="nt">&quot;data&quot;</span><span class="p">:</span> <span class="p">{</span>
<span class="nt">&quot;records&quot;</span><span class="p">:</span> <span class="p">[</span>
<span class="p">{</span>
<span class="nt">&quot;activity_date&quot;</span><span class="p">:</span> <span class="s2">&quot;Sun, 07 Jul 2019 08:00:00 GMT&quot;</span><span class="p">,</span>
<span class="nt">&quot;activity_id&quot;</span><span class="p">:</span> <span class="s2">&quot;hvYBqYBRa7wwXpaStWR4V2&quot;</span><span class="p">,</span>
<span class="nt">&quot;id&quot;</span><span class="p">:</span> <span class="mi">9</span><span class="p">,</span>
<span class="nt">&quot;record_type&quot;</span><span class="p">:</span> <span class="s2">&quot;AS&quot;</span><span class="p">,</span>
<span class="nt">&quot;sport_id&quot;</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
<span class="nt">&quot;user&quot;</span><span class="p">:</span> <span class="s2">&quot;admin&quot;</span><span class="p">,</span>
<span class="nt">&quot;value&quot;</span><span class="p">:</span> <span class="mi">18</span>
<span class="nt">&quot;value&quot;</span><span class="p">:</span> <span class="mi">18</span><span class="p">,</span>
<span class="nt">&quot;workout_date&quot;</span><span class="p">:</span> <span class="s2">&quot;Sun, 07 Jul 2019 08:00:00 GMT&quot;</span><span class="p">,</span>
<span class="nt">&quot;workout_id&quot;</span><span class="p">:</span> <span class="s2">&quot;hvYBqYBRa7wwXpaStWR4V2&quot;</span>
<span class="p">},</span>
<span class="p">{</span>
<span class="nt">&quot;activity_date&quot;</span><span class="p">:</span> <span class="s2">&quot;Sun, 07 Jul 2019 08:00:00 GMT&quot;</span><span class="p">,</span>
<span class="nt">&quot;activity_id&quot;</span><span class="p">:</span> <span class="s2">&quot;hvYBqYBRa7wwXpaStWR4V2&quot;</span><span class="p">,</span>
<span class="nt">&quot;id&quot;</span><span class="p">:</span> <span class="mi">10</span><span class="p">,</span>
<span class="nt">&quot;record_type&quot;</span><span class="p">:</span> <span class="s2">&quot;FD&quot;</span><span class="p">,</span>
<span class="nt">&quot;sport_id&quot;</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
<span class="nt">&quot;user&quot;</span><span class="p">:</span> <span class="s2">&quot;admin&quot;</span><span class="p">,</span>
<span class="nt">&quot;value&quot;</span><span class="p">:</span> <span class="mi">18</span>
<span class="nt">&quot;value&quot;</span><span class="p">:</span> <span class="mi">18</span><span class="p">,</span>
<span class="nt">&quot;workout_date&quot;</span><span class="p">:</span> <span class="s2">&quot;Sun, 07 Jul 2019 08:00:00 GMT&quot;</span><span class="p">,</span>
<span class="nt">&quot;workout_id&quot;</span><span class="p">:</span> <span class="s2">&quot;hvYBqYBRa7wwXpaStWR4V2&quot;</span>
<span class="p">},</span>
<span class="p">{</span>
<span class="nt">&quot;activity_date&quot;</span><span class="p">:</span> <span class="s2">&quot;Sun, 07 Jul 2019 08:00:00 GMT&quot;</span><span class="p">,</span>
<span class="nt">&quot;activity_id&quot;</span><span class="p">:</span> <span class="s2">&quot;hvYBqYBRa7wwXpaStWR4V2&quot;</span><span class="p">,</span>
<span class="nt">&quot;id&quot;</span><span class="p">:</span> <span class="mi">11</span><span class="p">,</span>
<span class="nt">&quot;record_type&quot;</span><span class="p">:</span> <span class="s2">&quot;LD&quot;</span><span class="p">,</span>
<span class="nt">&quot;sport_id&quot;</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
<span class="nt">&quot;user&quot;</span><span class="p">:</span> <span class="s2">&quot;admin&quot;</span><span class="p">,</span>
<span class="nt">&quot;value&quot;</span><span class="p">:</span> <span class="s2">&quot;1:01:00&quot;</span>
<span class="nt">&quot;value&quot;</span><span class="p">:</span> <span class="s2">&quot;1:01:00&quot;</span><span class="p">,</span>
<span class="nt">&quot;workout_date&quot;</span><span class="p">:</span> <span class="s2">&quot;Sun, 07 Jul 2019 08:00:00 GMT&quot;</span><span class="p">,</span>
<span class="nt">&quot;workout_id&quot;</span><span class="p">:</span> <span class="s2">&quot;hvYBqYBRa7wwXpaStWR4V2&quot;</span>
<span class="p">},</span>
<span class="p">{</span>
<span class="nt">&quot;activity_date&quot;</span><span class="p">:</span> <span class="s2">&quot;Sun, 07 Jul 2019 08:00:00 GMT&quot;</span><span class="p">,</span>
<span class="nt">&quot;activity_id&quot;</span><span class="p">:</span> <span class="s2">&quot;hvYBqYBRa7wwXpaStWR4V2&quot;</span><span class="p">,</span>
<span class="nt">&quot;id&quot;</span><span class="p">:</span> <span class="mi">12</span><span class="p">,</span>
<span class="nt">&quot;record_type&quot;</span><span class="p">:</span> <span class="s2">&quot;MS&quot;</span><span class="p">,</span>
<span class="nt">&quot;sport_id&quot;</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
<span class="nt">&quot;user&quot;</span><span class="p">:</span> <span class="s2">&quot;admin&quot;</span><span class="p">,</span>
<span class="nt">&quot;value&quot;</span><span class="p">:</span> <span class="mi">18</span>
<span class="nt">&quot;value&quot;</span><span class="p">:</span> <span class="mi">18</span><span class="p">,</span>
<span class="nt">&quot;workout_date&quot;</span><span class="p">:</span> <span class="s2">&quot;Sun, 07 Jul 2019 08:00:00 GMT&quot;</span><span class="p">,</span>
<span class="nt">&quot;workout_id&quot;</span><span class="p">:</span> <span class="s2">&quot;hvYBqYBRa7wwXpaStWR4V2&quot;</span>
<span class="p">}</span>
<span class="p">]</span>
<span class="p">},</span>

View File

@ -197,42 +197,42 @@
<span class="nt">&quot;data&quot;</span><span class="p">:</span> <span class="p">{</span>
<span class="nt">&quot;sports&quot;</span><span class="p">:</span> <span class="p">[</span>
<span class="p">{</span>
<span class="nt">&quot;has_activities&quot;</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
<span class="nt">&quot;has_workouts&quot;</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
<span class="nt">&quot;id&quot;</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
<span class="nt">&quot;img&quot;</span><span class="p">:</span> <span class="s2">&quot;/img/sports/cycling-sport.png&quot;</span><span class="p">,</span>
<span class="nt">&quot;is_active&quot;</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
<span class="nt">&quot;label&quot;</span><span class="p">:</span> <span class="s2">&quot;Cycling (Sport)&quot;</span>
<span class="p">},</span>
<span class="p">{</span>
<span class="nt">&quot;has_activities&quot;</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
<span class="nt">&quot;has_workouts&quot;</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
<span class="nt">&quot;id&quot;</span><span class="p">:</span> <span class="mi">2</span><span class="p">,</span>
<span class="nt">&quot;img&quot;</span><span class="p">:</span> <span class="s2">&quot;/img/sports/cycling-transport.png&quot;</span><span class="p">,</span>
<span class="nt">&quot;is_active&quot;</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
<span class="nt">&quot;label&quot;</span><span class="p">:</span> <span class="s2">&quot;Cycling (Transport)&quot;</span>
<span class="p">},</span>
<span class="p">{</span>
<span class="nt">&quot;has_activities&quot;</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
<span class="nt">&quot;has_workouts&quot;</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
<span class="nt">&quot;id&quot;</span><span class="p">:</span> <span class="mi">3</span><span class="p">,</span>
<span class="nt">&quot;img&quot;</span><span class="p">:</span> <span class="s2">&quot;/img/sports/hiking.png&quot;</span><span class="p">,</span>
<span class="nt">&quot;is_active&quot;</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
<span class="nt">&quot;label&quot;</span><span class="p">:</span> <span class="s2">&quot;Hiking&quot;</span>
<span class="p">},</span>
<span class="p">{</span>
<span class="nt">&quot;has_activities&quot;</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
<span class="nt">&quot;has_workouts&quot;</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
<span class="nt">&quot;id&quot;</span><span class="p">:</span> <span class="mi">4</span><span class="p">,</span>
<span class="nt">&quot;img&quot;</span><span class="p">:</span> <span class="s2">&quot;/img/sports/mountain-biking.png&quot;</span><span class="p">,</span>
<span class="nt">&quot;is_active&quot;</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
<span class="nt">&quot;label&quot;</span><span class="p">:</span> <span class="s2">&quot;Mountain Biking&quot;</span>
<span class="p">},</span>
<span class="p">{</span>
<span class="nt">&quot;has_activities&quot;</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
<span class="nt">&quot;has_workouts&quot;</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
<span class="nt">&quot;id&quot;</span><span class="p">:</span> <span class="mi">5</span><span class="p">,</span>
<span class="nt">&quot;img&quot;</span><span class="p">:</span> <span class="s2">&quot;/img/sports/running.png&quot;</span><span class="p">,</span>
<span class="nt">&quot;is_active&quot;</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
<span class="nt">&quot;label&quot;</span><span class="p">:</span> <span class="s2">&quot;Running&quot;</span>
<span class="p">},</span>
<span class="p">{</span>
<span class="nt">&quot;has_activities&quot;</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
<span class="nt">&quot;has_workouts&quot;</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
<span class="nt">&quot;id&quot;</span><span class="p">:</span> <span class="mi">6</span><span class="p">,</span>
<span class="nt">&quot;img&quot;</span><span class="p">:</span> <span class="s2">&quot;/img/sports/walking.png&quot;</span><span class="p">,</span>
<span class="nt">&quot;is_active&quot;</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
@ -310,7 +310,7 @@
<span class="nt">&quot;data&quot;</span><span class="p">:</span> <span class="p">{</span>
<span class="nt">&quot;sports&quot;</span><span class="p">:</span> <span class="p">[</span>
<span class="p">{</span>
<span class="nt">&quot;has_activities&quot;</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
<span class="nt">&quot;has_workouts&quot;</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
<span class="nt">&quot;id&quot;</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
<span class="nt">&quot;img&quot;</span><span class="p">:</span> <span class="s2">&quot;/img/sports/cycling-sport.png&quot;</span><span class="p">,</span>
<span class="nt">&quot;is_active&quot;</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
@ -384,7 +384,7 @@ Authenticated user must be an admin</p>
<span class="nt">&quot;data&quot;</span><span class="p">:</span> <span class="p">{</span>
<span class="nt">&quot;sports&quot;</span><span class="p">:</span> <span class="p">[</span>
<span class="p">{</span>
<span class="nt">&quot;has_activities&quot;</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
<span class="nt">&quot;has_workouts&quot;</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
<span class="nt">&quot;id&quot;</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
<span class="nt">&quot;img&quot;</span><span class="p">:</span> <span class="s2">&quot;/img/sports/cycling-sport.png&quot;</span><span class="p">,</span>
<span class="nt">&quot;is_active&quot;</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>

View File

@ -129,7 +129,7 @@
<dl class="http get">
<dt id="get--api-stats-(user_name)-by_time">
<code class="sig-name descname">GET </code><code class="sig-name descname">/api/stats/</code><span class="sig-paren">(</span><em class="sig-param">user_name</em><span class="sig-paren">)</span><code class="sig-name descname">/by_time</code><a class="headerlink" href="#get--api-stats-(user_name)-by_time" title="Permalink to this definition"></a></dt>
<dd><p>Get activities statistics for a user by time</p>
<dd><p>Get workouts statistics for a user by time</p>
<p><strong>Example requests</strong>:</p>
<ul class="simple">
<li><p>without parameters</p></li>
@ -140,7 +140,8 @@
<ul class="simple">
<li><p>with parameters</p></li>
</ul>
<div class="highlight-http notranslate"><div class="highlight"><pre><span></span><span class="nf">GET</span> <span class="nn">/api/stats/admin/by_time?from=2018-01-01&amp;to=2018-06-30&amp;time=week</span> <span class="kr">HTTP</span><span class="o">/</span><span class="m">1.1</span>
<div class="highlight-http notranslate"><div class="highlight"><pre><span></span><span class="nf">GET</span> <span class="nn">/api/stats/admin/by_time?from=2018-01-01&amp;to=2018-06-30&amp;time=week</span>
<span class="kr">HTTP</span><span class="o">/</span><span class="m">1.1</span>
</pre></div>
</div>
<p><strong>Example responses</strong>:</p>
@ -155,19 +156,19 @@
<span class="nt">&quot;statistics&quot;</span><span class="p">:</span> <span class="p">{</span>
<span class="nt">&quot;2017&quot;</span><span class="p">:</span> <span class="p">{</span>
<span class="nt">&quot;3&quot;</span><span class="p">:</span> <span class="p">{</span>
<span class="nt">&quot;nb_activities&quot;</span><span class="p">:</span> <span class="mi">2</span><span class="p">,</span>
<span class="nt">&quot;nb_workouts&quot;</span><span class="p">:</span> <span class="mi">2</span><span class="p">,</span>
<span class="nt">&quot;total_distance&quot;</span><span class="p">:</span> <span class="mf">15.282</span><span class="p">,</span>
<span class="nt">&quot;total_duration&quot;</span><span class="p">:</span> <span class="mi">12341</span>
<span class="p">}</span>
<span class="p">},</span>
<span class="nt">&quot;2019&quot;</span><span class="p">:</span> <span class="p">{</span>
<span class="nt">&quot;1&quot;</span><span class="p">:</span> <span class="p">{</span>
<span class="nt">&quot;nb_activities&quot;</span><span class="p">:</span> <span class="mi">3</span><span class="p">,</span>
<span class="nt">&quot;nb_workouts&quot;</span><span class="p">:</span> <span class="mi">3</span><span class="p">,</span>
<span class="nt">&quot;total_distance&quot;</span><span class="p">:</span> <span class="mi">47</span><span class="p">,</span>
<span class="nt">&quot;total_duration&quot;</span><span class="p">:</span> <span class="mi">9960</span>
<span class="p">},</span>
<span class="nt">&quot;2&quot;</span><span class="p">:</span> <span class="p">{</span>
<span class="nt">&quot;nb_activities&quot;</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
<span class="nt">&quot;nb_workouts&quot;</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
<span class="nt">&quot;total_distance&quot;</span><span class="p">:</span> <span class="mf">5.613</span><span class="p">,</span>
<span class="nt">&quot;total_duration&quot;</span><span class="p">:</span> <span class="mi">1267</span>
<span class="p">}</span>
@ -179,7 +180,7 @@
</pre></div>
</div>
<ul class="simple">
<li><p>no activities</p></li>
<li><p>no workouts</p></li>
</ul>
<div class="highlight-http notranslate"><div class="highlight"><pre><span></span><span class="kr">HTTP</span><span class="o">/</span><span class="m">1.1</span> <span class="m">200</span> <span class="ne">OK</span>
<span class="na">Content-Type</span><span class="o">:</span> <span class="l">application/json</span>
@ -239,10 +240,10 @@
<dl class="http get">
<dt id="get--api-stats-(user_name)-by_sport">
<code class="sig-name descname">GET </code><code class="sig-name descname">/api/stats/</code><span class="sig-paren">(</span><em class="sig-param">user_name</em><span class="sig-paren">)</span><code class="sig-name descname">/by_sport</code><a class="headerlink" href="#get--api-stats-(user_name)-by_sport" title="Permalink to this definition"></a></dt>
<dd><p>Get activities statistics for a user by sport</p>
<dd><p>Get workouts statistics for a user by sport</p>
<p><strong>Example requests</strong>:</p>
<ul class="simple">
<li><p>without parameters (get stats for all sports with activities)</p></li>
<li><p>without parameters (get stats for all sports with workouts)</p></li>
</ul>
<div class="highlight-http notranslate"><div class="highlight"><pre><span></span><span class="nf">GET</span> <span class="nn">/api/stats/admin/by_sport</span> <span class="kr">HTTP</span><span class="o">/</span><span class="m">1.1</span>
</pre></div>
@ -264,17 +265,17 @@
<span class="nt">&quot;data&quot;</span><span class="p">:</span> <span class="p">{</span>
<span class="nt">&quot;statistics&quot;</span><span class="p">:</span> <span class="p">{</span>
<span class="nt">&quot;1&quot;</span><span class="p">:</span> <span class="p">{</span>
<span class="nt">&quot;nb_activities&quot;</span><span class="p">:</span> <span class="mi">3</span><span class="p">,</span>
<span class="nt">&quot;nb_workouts&quot;</span><span class="p">:</span> <span class="mi">3</span><span class="p">,</span>
<span class="nt">&quot;total_distance&quot;</span><span class="p">:</span> <span class="mi">47</span><span class="p">,</span>
<span class="nt">&quot;total_duration&quot;</span><span class="p">:</span> <span class="mi">9960</span>
<span class="p">},</span>
<span class="nt">&quot;2&quot;</span><span class="p">:</span> <span class="p">{</span>
<span class="nt">&quot;nb_activities&quot;</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
<span class="nt">&quot;nb_workouts&quot;</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
<span class="nt">&quot;total_distance&quot;</span><span class="p">:</span> <span class="mf">5.613</span><span class="p">,</span>
<span class="nt">&quot;total_duration&quot;</span><span class="p">:</span> <span class="mi">1267</span>
<span class="p">},</span>
<span class="nt">&quot;3&quot;</span><span class="p">:</span> <span class="p">{</span>
<span class="nt">&quot;nb_activities&quot;</span><span class="p">:</span> <span class="mi">2</span><span class="p">,</span>
<span class="nt">&quot;nb_workouts&quot;</span><span class="p">:</span> <span class="mi">2</span><span class="p">,</span>
<span class="nt">&quot;total_distance&quot;</span><span class="p">:</span> <span class="mf">15.282</span><span class="p">,</span>
<span class="nt">&quot;total_duration&quot;</span><span class="p">:</span> <span class="mi">12341</span>
<span class="p">}</span>
@ -285,7 +286,7 @@
</pre></div>
</div>
<ul class="simple">
<li><p>no activities</p></li>
<li><p>no workouts</p></li>
</ul>
<div class="highlight-http notranslate"><div class="highlight"><pre><span></span><span class="kr">HTTP</span><span class="o">/</span><span class="m">1.1</span> <span class="m">200</span> <span class="ne">OK</span>
<span class="na">Content-Type</span><span class="o">:</span> <span class="l">application/json</span>
@ -348,10 +349,10 @@
<span class="p">{</span>
<span class="nt">&quot;data&quot;</span><span class="p">:</span> <span class="p">{</span>
<span class="nt">&quot;activities&quot;</span><span class="p">:</span> <span class="mi">3</span><span class="p">,</span>
<span class="nt">&quot;sports&quot;</span><span class="p">:</span> <span class="mi">3</span><span class="p">,</span>
<span class="nt">&quot;uploads_dir_size&quot;</span><span class="p">:</span> <span class="mi">1000</span><span class="p">,</span>
<span class="nt">&quot;users&quot;</span><span class="p">:</span> <span class="mi">2</span><span class="p">,</span>
<span class="nt">&quot;uploads_dir_size&quot;</span><span class="p">:</span> <span class="mi">1000</span>
<span class="nt">&quot;workouts&quot;</span><span class="p">:</span> <span class="mi">3</span><span class="p">,</span>
<span class="p">},</span>
<span class="nt">&quot;status&quot;</span><span class="p">:</span> <span class="s2">&quot;success&quot;</span>
<span class="p">}</span>

View File

@ -15,7 +15,7 @@
<script src="../_static/doctools.js"></script>
<link rel="index" title="Index" href="../genindex.html" />
<link rel="search" title="Search" href="../search.html" />
<link rel="next" title="Troubleshooting" href="../troubleshooting/index.html" />
<link rel="next" title="Workouts" href="workouts.html" />
<link rel="prev" title="Statistics" href="stats.html" />
<meta charset='utf-8'>
<meta http-equiv='X-UA-Compatible' content='IE=edge,chrome=1'>
@ -90,7 +90,7 @@
</a>
</li>
<li>
<a href="../troubleshooting/index.html" title="Next Chapter: Troubleshooting"><span class="glyphicon glyphicon-chevron-right visible-sm"></span><span class="hidden-sm hidden-tablet">Troubleshooting &raquo;</span>
<a href="workouts.html" title="Next Chapter: Workouts"><span class="glyphicon glyphicon-chevron-right visible-sm"></span><span class="hidden-sm hidden-tablet">Workouts &raquo;</span>
</a>
</li>
@ -141,7 +141,7 @@
<ul class="simple">
<li><p>with some query parameters</p></li>
</ul>
<div class="highlight-http notranslate"><div class="highlight"><pre><span></span><span class="nf">GET</span> <span class="nn">/api/users?order_by=activities_count&amp;par_page=5</span> <span class="kr">HTTP</span><span class="o">/</span><span class="m">1.1</span>
<div class="highlight-http notranslate"><div class="highlight"><pre><span></span><span class="nf">GET</span> <span class="nn">/api/users?order_by=workouts_count&amp;par_page=5</span> <span class="kr">HTTP</span><span class="o">/</span><span class="m">1.1</span>
<span class="na">Content-Type</span><span class="o">:</span> <span class="l">application/json</span>
</pre></div>
</div>
@ -162,8 +162,8 @@
<span class="nt">&quot;language&quot;</span><span class="p">:</span> <span class="s2">&quot;en&quot;</span><span class="p">,</span>
<span class="nt">&quot;last_name&quot;</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
<span class="nt">&quot;location&quot;</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
<span class="nt">&quot;nb_activities&quot;</span><span class="p">:</span> <span class="mi">6</span><span class="p">,</span>
<span class="nt">&quot;nb_sports&quot;</span><span class="p">:</span> <span class="mi">3</span><span class="p">,</span>
<span class="nt">&quot;nb_workouts&quot;</span><span class="p">:</span> <span class="mi">6</span><span class="p">,</span>
<span class="nt">&quot;picture&quot;</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
<span class="nt">&quot;sports_list&quot;</span><span class="p">:</span> <span class="p">[</span>
<span class="mi">1</span><span class="p">,</span>
@ -185,8 +185,8 @@
<span class="nt">&quot;language&quot;</span><span class="p">:</span> <span class="s2">&quot;fr&quot;</span><span class="p">,</span>
<span class="nt">&quot;last_name&quot;</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
<span class="nt">&quot;location&quot;</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
<span class="nt">&quot;nb_activities&quot;</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
<span class="nt">&quot;nb_sports&quot;</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
<span class="nt">&quot;nb_workouts&quot;</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
<span class="nt">&quot;picture&quot;</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
<span class="nt">&quot;sports_list&quot;</span><span class="p">:</span> <span class="p">[],</span>
<span class="nt">&quot;timezone&quot;</span><span class="p">:</span> <span class="s2">&quot;Europe/Paris&quot;</span><span class="p">,</span>
@ -212,7 +212,7 @@
<li><p><strong>per_page</strong> (<em>integer</em>) number of users per page (default: 10, max: 50)</p></li>
<li><p><strong>q</strong> (<em>string</em>) query on user name</p></li>
<li><p><strong>order_by</strong> (<em>string</em>) sorting criteria (<code class="docutils literal notranslate"><span class="pre">username</span></code>, <code class="docutils literal notranslate"><span class="pre">created_at</span></code>,
<code class="docutils literal notranslate"><span class="pre">activities_count</span></code>, <code class="docutils literal notranslate"><span class="pre">admin</span></code>)</p></li>
<code class="docutils literal notranslate"><span class="pre">workouts_count</span></code>, <code class="docutils literal notranslate"><span class="pre">admin</span></code>)</p></li>
<li><p><strong>order</strong> (<em>string</em>) sorting order (default: <code class="docutils literal notranslate"><span class="pre">asc</span></code>)</p></li>
</ul>
</dd>
@ -260,8 +260,8 @@
<span class="nt">&quot;language&quot;</span><span class="p">:</span> <span class="s2">&quot;en&quot;</span><span class="p">,</span>
<span class="nt">&quot;last_name&quot;</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
<span class="nt">&quot;location&quot;</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
<span class="nt">&quot;nb_activities&quot;</span><span class="p">:</span> <span class="mi">6</span><span class="p">,</span>
<span class="nt">&quot;nb_sports&quot;</span><span class="p">:</span> <span class="mi">3</span><span class="p">,</span>
<span class="nt">&quot;nb_workouts&quot;</span><span class="p">:</span> <span class="mi">6</span><span class="p">,</span>
<span class="nt">&quot;picture&quot;</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
<span class="nt">&quot;sports_list&quot;</span><span class="p">:</span> <span class="p">[</span>
<span class="mi">1</span><span class="p">,</span>
@ -367,7 +367,7 @@
<span class="nt">&quot;language&quot;</span><span class="p">:</span> <span class="s2">&quot;en&quot;</span><span class="p">,</span>
<span class="nt">&quot;last_name&quot;</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
<span class="nt">&quot;location&quot;</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
<span class="nt">&quot;nb_activities&quot;</span><span class="p">:</span> <span class="mi">6</span><span class="p">,</span>
<span class="nt">&quot;nb_workouts&quot;</span><span class="p">:</span> <span class="mi">6</span><span class="p">,</span>
<span class="nt">&quot;nb_sports&quot;</span><span class="p">:</span> <span class="mi">3</span><span class="p">,</span>
<span class="nt">&quot;picture&quot;</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
<span class="nt">&quot;sports_list&quot;</span><span class="p">:</span> <span class="p">[</span>

View File

@ -4,7 +4,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Activities &#8212; FitTrackee 0.4.2
<title>Workouts &#8212; FitTrackee 0.4.2
documentation</title>
<link rel="stylesheet" href="../_static/pygments.css" type="text/css" />
<link rel="stylesheet" href="../_static/bootstrap-sphinx.css" type="text/css" />
@ -15,8 +15,8 @@
<script src="../_static/doctools.js"></script>
<link rel="index" title="Index" href="../genindex.html" />
<link rel="search" title="Search" href="../search.html" />
<link rel="next" title="Authentication" href="auth.html" />
<link rel="prev" title="API documentation" href="index.html" />
<link rel="next" title="Troubleshooting" href="../troubleshooting/index.html" />
<link rel="prev" title="Users" href="users.html" />
<meta charset='utf-8'>
<meta http-equiv='X-UA-Compatible' content='IE=edge,chrome=1'>
<meta name='viewport' content='width=device-width, initial-scale=1.0, maximum-scale=1'>
@ -76,7 +76,7 @@
<ul class="dropdown-menu localtoc"
role="menu"
aria-labelledby="dLabelLocalToc"><ul>
<li><a class="reference internal" href="#">Activities</a></li>
<li><a class="reference internal" href="#">Workouts</a></li>
</ul>
</ul>
</li>
@ -86,11 +86,11 @@
<li>
<a href="index.html" title="Previous Chapter: API documentation"><span class="glyphicon glyphicon-chevron-left visible-sm"></span><span class="hidden-sm hidden-tablet">&laquo; API documentation</span>
<a href="users.html" title="Previous Chapter: Users"><span class="glyphicon glyphicon-chevron-left visible-sm"></span><span class="hidden-sm hidden-tablet">&laquo; Users</span>
</a>
</li>
<li>
<a href="auth.html" title="Next Chapter: Authentication"><span class="glyphicon glyphicon-chevron-right visible-sm"></span><span class="hidden-sm hidden-tablet">Authentication &raquo;</span>
<a href="../troubleshooting/index.html" title="Next Chapter: Troubleshooting"><span class="glyphicon glyphicon-chevron-right visible-sm"></span><span class="hidden-sm hidden-tablet">Troubleshooting &raquo;</span>
</a>
</li>
@ -100,7 +100,7 @@
<li class="hidden-sm">
<div id="sourcelink">
<a href="../_sources/api/activities.rst.txt"
<a href="../_sources/api/workouts.rst.txt"
rel="nofollow">Source</a>
</div></li>
@ -124,37 +124,36 @@
<div class="row">
<div class="body col-md-12 content" role="main">
<div class="section" id="activities">
<h1>Activities<a class="headerlink" href="#activities" title="Permalink to this headline"></a></h1>
<div class="section" id="workouts">
<h1>Workouts<a class="headerlink" href="#workouts" title="Permalink to this headline"></a></h1>
<dl class="http get">
<dt id="get--api-activities">
<code class="sig-name descname">GET </code><code class="sig-name descname">/api/activities</code><a class="headerlink" href="#get--api-activities" title="Permalink to this definition"></a></dt>
<dd><p>Get activities for the authenticated user.</p>
<dt id="get--api-workouts">
<code class="sig-name descname">GET </code><code class="sig-name descname">/api/workouts</code><a class="headerlink" href="#get--api-workouts" title="Permalink to this definition"></a></dt>
<dd><p>Get workouts for the authenticated user.</p>
<p><strong>Example requests</strong>:</p>
<ul class="simple">
<li><p>without parameters</p></li>
</ul>
<div class="highlight-http notranslate"><div class="highlight"><pre><span></span><span class="nf">GET</span> <span class="nn">/api/activities/</span> <span class="kr">HTTP</span><span class="o">/</span><span class="m">1.1</span>
<div class="highlight-http notranslate"><div class="highlight"><pre><span></span><span class="nf">GET</span> <span class="nn">/api/workouts/</span> <span class="kr">HTTP</span><span class="o">/</span><span class="m">1.1</span>
</pre></div>
</div>
<ul class="simple">
<li><p>with some query parameters</p></li>
</ul>
<div class="highlight-http notranslate"><div class="highlight"><pre><span></span><span class="nf">GET</span> <span class="nn">/api/activities?from=2019-07-02&amp;to=2019-07-31&amp;sport_id=1</span> <span class="kr">HTTP</span><span class="o">/</span><span class="m">1.1</span>
<div class="highlight-http notranslate"><div class="highlight"><pre><span></span><span class="nf">GET</span> <span class="nn">/api/workouts?from=2019-07-02&amp;to=2019-07-31&amp;sport_id=1</span> <span class="kr">HTTP</span><span class="o">/</span><span class="m">1.1</span>
</pre></div>
</div>
<p><strong>Example responses</strong>:</p>
<ul class="simple">
<li><p>returning at least one activity</p></li>
<li><p>returning at least one workout</p></li>
</ul>
<div class="highlight-http notranslate"><div class="highlight"><pre><span></span><span class="kr">HTTP</span><span class="o">/</span><span class="m">1.1</span> <span class="m">200</span> <span class="ne">OK</span>
<span class="na">Content-Type</span><span class="o">:</span> <span class="l">application/json</span>
<span class="p">{</span>
<span class="nt">&quot;data&quot;</span><span class="p">:</span> <span class="p">{</span>
<span class="nt">&quot;activities&quot;</span><span class="p">:</span> <span class="p">[</span>
<span class="nt">&quot;workouts&quot;</span><span class="p">:</span> <span class="p">[</span>
<span class="p">{</span>
<span class="nt">&quot;activity_date&quot;</span><span class="p">:</span> <span class="s2">&quot;Mon, 01 Jan 2018 00:00:00 GMT&quot;</span><span class="p">,</span>
<span class="nt">&quot;ascent&quot;</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
<span class="nt">&quot;ave_speed&quot;</span><span class="p">:</span> <span class="mf">10.0</span><span class="p">,</span>
<span class="nt">&quot;bounds&quot;</span><span class="p">:</span> <span class="p">[],</span>
@ -169,46 +168,46 @@
<span class="nt">&quot;min_alt&quot;</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
<span class="nt">&quot;modification_date&quot;</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
<span class="nt">&quot;moving&quot;</span><span class="p">:</span> <span class="s2">&quot;0:17:04&quot;</span><span class="p">,</span>
<span class="nt">&quot;next_activity&quot;</span><span class="p">:</span> <span class="mi">3</span><span class="p">,</span>
<span class="nt">&quot;next_workout&quot;</span><span class="p">:</span> <span class="mi">3</span><span class="p">,</span>
<span class="nt">&quot;notes&quot;</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
<span class="nt">&quot;pauses&quot;</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
<span class="nt">&quot;previous_activity&quot;</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
<span class="nt">&quot;previous_workout&quot;</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
<span class="nt">&quot;records&quot;</span><span class="p">:</span> <span class="p">[</span>
<span class="p">{</span>
<span class="nt">&quot;activity_date&quot;</span><span class="p">:</span> <span class="s2">&quot;Mon, 01 Jan 2018 00:00:00 GMT&quot;</span><span class="p">,</span>
<span class="nt">&quot;activity_id&quot;</span><span class="p">:</span> <span class="s2">&quot;kjxavSTUrJvoAh2wvCeGEF&quot;</span><span class="p">,</span>
<span class="nt">&quot;id&quot;</span><span class="p">:</span> <span class="mi">4</span><span class="p">,</span>
<span class="nt">&quot;record_type&quot;</span><span class="p">:</span> <span class="s2">&quot;MS&quot;</span><span class="p">,</span>
<span class="nt">&quot;sport_id&quot;</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
<span class="nt">&quot;user&quot;</span><span class="p">:</span> <span class="s2">&quot;admin&quot;</span><span class="p">,</span>
<span class="nt">&quot;value&quot;</span><span class="p">:</span> <span class="mf">10.0</span>
<span class="nt">&quot;value&quot;</span><span class="p">:</span> <span class="mf">10.0</span><span class="p">,</span>
<span class="nt">&quot;workout_date&quot;</span><span class="p">:</span> <span class="s2">&quot;Mon, 01 Jan 2018 00:00:00 GMT&quot;</span><span class="p">,</span>
<span class="nt">&quot;workout_id&quot;</span><span class="p">:</span> <span class="s2">&quot;kjxavSTUrJvoAh2wvCeGEF&quot;</span>
<span class="p">},</span>
<span class="p">{</span>
<span class="nt">&quot;activity_date&quot;</span><span class="p">:</span> <span class="s2">&quot;Mon, 01 Jan 2018 00:00:00 GMT&quot;</span><span class="p">,</span>
<span class="nt">&quot;activity_id&quot;</span><span class="p">:</span> <span class="s2">&quot;kjxavSTUrJvoAh2wvCeGEF&quot;</span><span class="p">,</span>
<span class="nt">&quot;id&quot;</span><span class="p">:</span> <span class="mi">3</span><span class="p">,</span>
<span class="nt">&quot;record_type&quot;</span><span class="p">:</span> <span class="s2">&quot;LD&quot;</span><span class="p">,</span>
<span class="nt">&quot;sport_id&quot;</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
<span class="nt">&quot;user&quot;</span><span class="p">:</span> <span class="s2">&quot;admin&quot;</span><span class="p">,</span>
<span class="nt">&quot;value&quot;</span><span class="p">:</span> <span class="s2">&quot;0:17:04&quot;</span>
<span class="nt">&quot;value&quot;</span><span class="p">:</span> <span class="s2">&quot;0:17:04&quot;</span><span class="p">,</span>
<span class="nt">&quot;workout_date&quot;</span><span class="p">:</span> <span class="s2">&quot;Mon, 01 Jan 2018 00:00:00 GMT&quot;</span><span class="p">,</span>
<span class="nt">&quot;workout_id&quot;</span><span class="p">:</span> <span class="s2">&quot;kjxavSTUrJvoAh2wvCeGEF&quot;</span>
<span class="p">},</span>
<span class="p">{</span>
<span class="nt">&quot;activity_date&quot;</span><span class="p">:</span> <span class="s2">&quot;Mon, 01 Jan 2018 00:00:00 GMT&quot;</span><span class="p">,</span>
<span class="nt">&quot;activity_id&quot;</span><span class="p">:</span> <span class="s2">&quot;kjxavSTUrJvoAh2wvCeGEF&quot;</span><span class="p">,</span>
<span class="nt">&quot;id&quot;</span><span class="p">:</span> <span class="mi">2</span><span class="p">,</span>
<span class="nt">&quot;record_type&quot;</span><span class="p">:</span> <span class="s2">&quot;FD&quot;</span><span class="p">,</span>
<span class="nt">&quot;sport_id&quot;</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
<span class="nt">&quot;user&quot;</span><span class="p">:</span> <span class="s2">&quot;admin&quot;</span><span class="p">,</span>
<span class="nt">&quot;value&quot;</span><span class="p">:</span> <span class="mf">10.0</span>
<span class="nt">&quot;value&quot;</span><span class="p">:</span> <span class="mf">10.0</span><span class="p">,</span>
<span class="nt">&quot;workout_date&quot;</span><span class="p">:</span> <span class="s2">&quot;Mon, 01 Jan 2018 00:00:00 GMT&quot;</span><span class="p">,</span>
<span class="nt">&quot;workout_id&quot;</span><span class="p">:</span> <span class="s2">&quot;kjxavSTUrJvoAh2wvCeGEF&quot;</span>
<span class="p">},</span>
<span class="p">{</span>
<span class="nt">&quot;activity_date&quot;</span><span class="p">:</span> <span class="s2">&quot;Mon, 01 Jan 2018 00:00:00 GMT&quot;</span><span class="p">,</span>
<span class="nt">&quot;activity_id&quot;</span><span class="p">:</span> <span class="s2">&quot;kjxavSTUrJvoAh2wvCeGEF&quot;</span><span class="p">,</span>
<span class="nt">&quot;id&quot;</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
<span class="nt">&quot;record_type&quot;</span><span class="p">:</span> <span class="s2">&quot;AS&quot;</span><span class="p">,</span>
<span class="nt">&quot;sport_id&quot;</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
<span class="nt">&quot;user&quot;</span><span class="p">:</span> <span class="s2">&quot;admin&quot;</span><span class="p">,</span>
<span class="nt">&quot;value&quot;</span><span class="p">:</span> <span class="mf">10.0</span>
<span class="nt">&quot;value&quot;</span><span class="p">:</span> <span class="mf">10.0</span><span class="p">,</span>
<span class="nt">&quot;workout_date&quot;</span><span class="p">:</span> <span class="s2">&quot;Mon, 01 Jan 2018 00:00:00 GMT&quot;</span><span class="p">,</span>
<span class="nt">&quot;workout_id&quot;</span><span class="p">:</span> <span class="s2">&quot;kjxavSTUrJvoAh2wvCeGEF&quot;</span>
<span class="p">}</span>
<span class="p">],</span>
<span class="nt">&quot;segments&quot;</span><span class="p">:</span> <span class="p">[],</span>
@ -217,7 +216,8 @@
<span class="nt">&quot;user&quot;</span><span class="p">:</span> <span class="s2">&quot;admin&quot;</span><span class="p">,</span>
<span class="nt">&quot;weather_end&quot;</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
<span class="nt">&quot;weather_start&quot;</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
<span class="nt">&quot;with_gpx&quot;</span><span class="p">:</span> <span class="kc">false</span>
<span class="nt">&quot;with_gpx&quot;</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
<span class="nt">&quot;workout_date&quot;</span><span class="p">:</span> <span class="s2">&quot;Mon, 01 Jan 2018 00:00:00 GMT&quot;</span>
<span class="p">}</span>
<span class="p">]</span>
<span class="p">},</span>
@ -226,14 +226,14 @@
</pre></div>
</div>
<ul class="simple">
<li><p>returning no activities</p></li>
<li><p>returning no workouts</p></li>
</ul>
<div class="highlight-http notranslate"><div class="highlight"><pre><span></span><span class="kr">HTTP</span><span class="o">/</span><span class="m">1.1</span> <span class="m">200</span> <span class="ne">OK</span>
<span class="na">Content-Type</span><span class="o">:</span> <span class="l">application/json</span>
<span class="p">{</span>
<span class="nt">&quot;data&quot;</span><span class="p">:</span> <span class="p">{</span>
<span class="nt">&quot;activities&quot;</span><span class="p">:</span> <span class="p">[]</span>
<span class="nt">&quot;workouts&quot;</span><span class="p">:</span> <span class="p">[]</span>
<span class="p">},</span>
<span class="nt">&quot;status&quot;</span><span class="p">:</span> <span class="s2">&quot;success&quot;</span>
<span class="p">}</span>
@ -248,7 +248,7 @@
<dt class="field-even">Query Parameters</dt>
<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>per_page</strong> (<em>integer</em>) number of activities per page
<li><p><strong>per_page</strong> (<em>integer</em>) number of workouts per page
(default: 5, max: 50)</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>
@ -285,11 +285,11 @@
</dd></dl>
<dl class="http get">
<dt id="get--api-activities-(string-activity_short_id)">
<code class="sig-name descname">GET </code><code class="sig-name descname">/api/activities/</code><span class="sig-paren">(</span><em class="property">string: </em><em class="sig-param">activity_short_id</em><span class="sig-paren">)</span><a class="headerlink" href="#get--api-activities-(string-activity_short_id)" title="Permalink to this definition"></a></dt>
<dd><p>Get an activity</p>
<dt id="get--api-workouts-(string-workout_short_id)">
<code class="sig-name descname">GET </code><code class="sig-name descname">/api/workouts/</code><span class="sig-paren">(</span><em class="property">string: </em><em class="sig-param">workout_short_id</em><span class="sig-paren">)</span><a class="headerlink" href="#get--api-workouts-(string-workout_short_id)" title="Permalink to this definition"></a></dt>
<dd><p>Get an workout</p>
<p><strong>Example request</strong>:</p>
<div class="highlight-http notranslate"><div class="highlight"><pre><span></span><span class="nf">GET</span> <span class="nn">/api/activities/kjxavSTUrJvoAh2wvCeGEF</span> <span class="kr">HTTP</span><span class="o">/</span><span class="m">1.1</span>
<div class="highlight-http notranslate"><div class="highlight"><pre><span></span><span class="nf">GET</span> <span class="nn">/api/workouts/kjxavSTUrJvoAh2wvCeGEF</span> <span class="kr">HTTP</span><span class="o">/</span><span class="m">1.1</span>
</pre></div>
</div>
<p><strong>Example responses</strong>:</p>
@ -301,9 +301,8 @@
<span class="p">{</span>
<span class="nt">&quot;data&quot;</span><span class="p">:</span> <span class="p">{</span>
<span class="nt">&quot;activities&quot;</span><span class="p">:</span> <span class="p">[</span>
<span class="nt">&quot;workouts&quot;</span><span class="p">:</span> <span class="p">[</span>
<span class="p">{</span>
<span class="nt">&quot;activity_date&quot;</span><span class="p">:</span> <span class="s2">&quot;Sun, 07 Jul 2019 07:00:00 GMT&quot;</span><span class="p">,</span>
<span class="nt">&quot;ascent&quot;</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
<span class="nt">&quot;ave_speed&quot;</span><span class="p">:</span> <span class="mi">16</span><span class="p">,</span>
<span class="nt">&quot;bounds&quot;</span><span class="p">:</span> <span class="p">[],</span>
@ -318,10 +317,10 @@
<span class="nt">&quot;min_alt&quot;</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
<span class="nt">&quot;modification_date&quot;</span><span class="p">:</span> <span class="s2">&quot;Sun, 14 Jul 2019 18:57:22 GMT&quot;</span><span class="p">,</span>
<span class="nt">&quot;moving&quot;</span><span class="p">:</span> <span class="s2">&quot;0:45:00&quot;</span><span class="p">,</span>
<span class="nt">&quot;next_activity&quot;</span><span class="p">:</span> <span class="mi">4</span><span class="p">,</span>
<span class="nt">&quot;notes&quot;</span><span class="p">:</span> <span class="s2">&quot;activity without gpx&quot;</span><span class="p">,</span>
<span class="nt">&quot;next_workout&quot;</span><span class="p">:</span> <span class="mi">4</span><span class="p">,</span>
<span class="nt">&quot;notes&quot;</span><span class="p">:</span> <span class="s2">&quot;workout without gpx&quot;</span><span class="p">,</span>
<span class="nt">&quot;pauses&quot;</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
<span class="nt">&quot;previous_activity&quot;</span><span class="p">:</span> <span class="mi">3</span><span class="p">,</span>
<span class="nt">&quot;previous_workout&quot;</span><span class="p">:</span> <span class="mi">3</span><span class="p">,</span>
<span class="nt">&quot;records&quot;</span><span class="p">:</span> <span class="p">[],</span>
<span class="nt">&quot;segments&quot;</span><span class="p">:</span> <span class="p">[],</span>
<span class="nt">&quot;sport_id&quot;</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
@ -329,7 +328,8 @@
<span class="nt">&quot;user&quot;</span><span class="p">:</span> <span class="s2">&quot;admin&quot;</span><span class="p">,</span>
<span class="nt">&quot;weather_end&quot;</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
<span class="nt">&quot;weather_start&quot;</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
<span class="nt">&quot;with_gpx&quot;</span><span class="p">:</span> <span class="kc">false</span>
<span class="nt">&quot;with_gpx&quot;</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
<span class="nt">&quot;workout_date&quot;</span><span class="p">:</span> <span class="s2">&quot;Sun, 07 Jul 2019 07:00:00 GMT&quot;</span>
<span class="p">}</span>
<span class="p">]</span>
<span class="p">},</span>
@ -345,7 +345,7 @@
<span class="p">{</span>
<span class="nt">&quot;data&quot;</span><span class="p">:</span> <span class="p">{</span>
<span class="nt">&quot;activities&quot;</span><span class="p">:</span> <span class="p">[]</span>
<span class="nt">&quot;workouts&quot;</span><span class="p">:</span> <span class="p">[]</span>
<span class="p">},</span>
<span class="nt">&quot;status&quot;</span><span class="p">:</span> <span class="s2">&quot;not found&quot;</span>
<span class="p">}</span>
@ -355,7 +355,7 @@
<dt class="field-odd">Parameters</dt>
<dd class="field-odd"><ul class="simple">
<li><p><strong>auth_user_id</strong> (<em>integer</em>) authenticate user id (from JSON Web Token)</p></li>
<li><p><strong>activity_short_id</strong> (<em>string</em>) activity short id</p></li>
<li><p><strong>workout_short_id</strong> (<em>string</em>) workout short id</p></li>
</ul>
</dd>
<dt class="field-even">Request Headers</dt>
@ -373,18 +373,18 @@
</ul>
</p></li>
<li><p><a class="reference external" href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.4">403 Forbidden</a> You do not have permissions.</p></li>
<li><p><a class="reference external" href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.5">404 Not Found</a> activity not found</p></li>
<li><p><a class="reference external" href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.5">404 Not Found</a> workout not found</p></li>
</ul>
</dd>
</dl>
</dd></dl>
<dl class="http get">
<dt id="get--api-activities-(string-activity_short_id)-gpx">
<code class="sig-name descname">GET </code><code class="sig-name descname">/api/activities/</code><span class="sig-paren">(</span><em class="property">string: </em><em class="sig-param">activity_short_id</em><span class="sig-paren">)</span><code class="sig-name descname">/gpx</code><a class="headerlink" href="#get--api-activities-(string-activity_short_id)-gpx" title="Permalink to this definition"></a></dt>
<dd><p>Get gpx file for an activity displayed on map with Leaflet</p>
<dt id="get--api-workouts-(string-workout_short_id)-gpx">
<code class="sig-name descname">GET </code><code class="sig-name descname">/api/workouts/</code><span class="sig-paren">(</span><em class="property">string: </em><em class="sig-param">workout_short_id</em><span class="sig-paren">)</span><code class="sig-name descname">/gpx</code><a class="headerlink" href="#get--api-workouts-(string-workout_short_id)-gpx" title="Permalink to this definition"></a></dt>
<dd><p>Get gpx file for an workout displayed on map with Leaflet</p>
<p><strong>Example request</strong>:</p>
<div class="highlight-http notranslate"><div class="highlight"><pre><span></span><span class="nf">GET</span> <span class="nn">/api/activities/kjxavSTUrJvoAh2wvCeGEF/gpx</span> <span class="kr">HTTP</span><span class="o">/</span><span class="m">1.1</span>
<div class="highlight-http notranslate"><div class="highlight"><pre><span></span><span class="nf">GET</span> <span class="nn">/api/workouts/kjxavSTUrJvoAh2wvCeGEF/gpx</span> <span class="kr">HTTP</span><span class="o">/</span><span class="m">1.1</span>
<span class="na">Content-Type</span><span class="o">:</span> <span class="l">application/json</span>
</pre></div>
</div>
@ -405,7 +405,7 @@
<dt class="field-odd">Parameters</dt>
<dd class="field-odd"><ul class="simple">
<li><p><strong>auth_user_id</strong> (<em>integer</em>) authenticate user id (from JSON Web Token)</p></li>
<li><p><strong>activity_short_id</strong> (<em>string</em>) activity short id</p></li>
<li><p><strong>workout_short_id</strong> (<em>string</em>) workout short id</p></li>
</ul>
</dd>
<dt class="field-even">Request Headers</dt>
@ -423,8 +423,8 @@
</ul>
</p></li>
<li><p><a class="reference external" href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.5">404 Not Found</a> <ul>
<li><p>activity not found</p></li>
<li><p>no gpx file for this activity</p></li>
<li><p>workout not found</p></li>
<li><p>no gpx file for this workout</p></li>
</ul>
</p></li>
<li><p><a class="reference external" href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.5.1">500 Internal Server Error</a> </p></li>
@ -434,11 +434,11 @@
</dd></dl>
<dl class="http get">
<dt id="get--api-activities-(string-activity_short_id)-chart_data">
<code class="sig-name descname">GET </code><code class="sig-name descname">/api/activities/</code><span class="sig-paren">(</span><em class="property">string: </em><em class="sig-param">activity_short_id</em><span class="sig-paren">)</span><code class="sig-name descname">/chart_data</code><a class="headerlink" href="#get--api-activities-(string-activity_short_id)-chart_data" title="Permalink to this definition"></a></dt>
<dd><p>Get chart data from an activity gpx file, to display it with Recharts</p>
<dt id="get--api-workouts-(string-workout_short_id)-chart_data">
<code class="sig-name descname">GET </code><code class="sig-name descname">/api/workouts/</code><span class="sig-paren">(</span><em class="property">string: </em><em class="sig-param">workout_short_id</em><span class="sig-paren">)</span><code class="sig-name descname">/chart_data</code><a class="headerlink" href="#get--api-workouts-(string-workout_short_id)-chart_data" title="Permalink to this definition"></a></dt>
<dd><p>Get chart data from an workout gpx file, to display it with Recharts</p>
<p><strong>Example request</strong>:</p>
<div class="highlight-http notranslate"><div class="highlight"><pre><span></span><span class="nf">GET</span> <span class="nn">/api/activities/kjxavSTUrJvoAh2wvCeGEF/chart</span> <span class="kr">HTTP</span><span class="o">/</span><span class="m">1.1</span>
<div class="highlight-http notranslate"><div class="highlight"><pre><span></span><span class="nf">GET</span> <span class="nn">/api/workouts/kjxavSTUrJvoAh2wvCeGEF/chart</span> <span class="kr">HTTP</span><span class="o">/</span><span class="m">1.1</span>
<span class="na">Content-Type</span><span class="o">:</span> <span class="l">application/json</span>
</pre></div>
</div>
@ -478,7 +478,7 @@
<dt class="field-odd">Parameters</dt>
<dd class="field-odd"><ul class="simple">
<li><p><strong>auth_user_id</strong> (<em>integer</em>) authenticate user id (from JSON Web Token)</p></li>
<li><p><strong>activity_short_id</strong> (<em>string</em>) activity short id</p></li>
<li><p><strong>workout_short_id</strong> (<em>string</em>) workout short id</p></li>
</ul>
</dd>
<dt class="field-even">Request Headers</dt>
@ -496,8 +496,8 @@
</ul>
</p></li>
<li><p><a class="reference external" href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.5">404 Not Found</a> <ul>
<li><p>activity not found</p></li>
<li><p>no gpx file for this activity</p></li>
<li><p>workout not found</p></li>
<li><p>no gpx file for this workout</p></li>
</ul>
</p></li>
<li><p><a class="reference external" href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.5.1">500 Internal Server Error</a> </p></li>
@ -507,11 +507,11 @@
</dd></dl>
<dl class="http get">
<dt id="get--api-activities-(string-activity_short_id)-chart_data-segment-(int-segment_id)">
<code class="sig-name descname">GET </code><code class="sig-name descname">/api/activities/</code><span class="sig-paren">(</span><em class="property">string: </em><em class="sig-param">activity_short_id</em><span class="sig-paren">)</span><code class="sig-name descname">/chart_data/segment/</code><span class="sig-paren">(</span><em class="property">int: </em><em class="sig-param">segment_id</em><span class="sig-paren">)</span><a class="headerlink" href="#get--api-activities-(string-activity_short_id)-chart_data-segment-(int-segment_id)" title="Permalink to this definition"></a></dt>
<dd><p>Get chart data from an activity gpx file, to display it with Recharts</p>
<dt id="get--api-workouts-(string-workout_short_id)-chart_data-segment-(int-segment_id)">
<code class="sig-name descname">GET </code><code class="sig-name descname">/api/workouts/</code><span class="sig-paren">(</span><em class="property">string: </em><em class="sig-param">workout_short_id</em><span class="sig-paren">)</span><code class="sig-name descname">/chart_data/segment/</code><span class="sig-paren">(</span><em class="property">int: </em><em class="sig-param">segment_id</em><span class="sig-paren">)</span><a class="headerlink" href="#get--api-workouts-(string-workout_short_id)-chart_data-segment-(int-segment_id)" title="Permalink to this definition"></a></dt>
<dd><p>Get chart data from an workout gpx file, to display it with Recharts</p>
<p><strong>Example request</strong>:</p>
<div class="highlight-http notranslate"><div class="highlight"><pre><span></span><span class="nf">GET</span> <span class="nn">/api/activities/kjxavSTUrJvoAh2wvCeGEF/chart/segment/0</span> <span class="kr">HTTP</span><span class="o">/</span><span class="m">1.1</span>
<div class="highlight-http notranslate"><div class="highlight"><pre><span></span><span class="nf">GET</span> <span class="nn">/api/workouts/kjxavSTUrJvoAh2wvCeGEF/chart/segment/0</span> <span class="kr">HTTP</span><span class="o">/</span><span class="m">1.1</span>
<span class="na">Content-Type</span><span class="o">:</span> <span class="l">application/json</span>
</pre></div>
</div>
@ -551,7 +551,7 @@
<dt class="field-odd">Parameters</dt>
<dd class="field-odd"><ul class="simple">
<li><p><strong>auth_user_id</strong> (<em>integer</em>) authenticate user id (from JSON Web Token)</p></li>
<li><p><strong>activity_short_id</strong> (<em>string</em>) activity short id</p></li>
<li><p><strong>workout_short_id</strong> (<em>string</em>) workout short id</p></li>
<li><p><strong>segment_id</strong> (<em>integer</em>) segment id</p></li>
</ul>
</dd>
@ -563,14 +563,14 @@
<dt class="field-odd">Status Codes</dt>
<dd class="field-odd"><ul class="simple">
<li><p><a class="reference external" href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.2.1">200 OK</a> success</p></li>
<li><p><a class="reference external" href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.1">400 Bad Request</a> no gpx file for this activity</p></li>
<li><p><a class="reference external" href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.1">400 Bad Request</a> no gpx file for this workout</p></li>
<li><p><a class="reference external" href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.2">401 Unauthorized</a> <ul>
<li><p>Provide a valid auth token.</p></li>
<li><p>Signature expired. Please log in again.</p></li>
<li><p>Invalid token. Please log in again.</p></li>
</ul>
</p></li>
<li><p><a class="reference external" href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.5">404 Not Found</a> activity not found</p></li>
<li><p><a class="reference external" href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.5">404 Not Found</a> workout not found</p></li>
<li><p><a class="reference external" href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.5.1">500 Internal Server Error</a> </p></li>
</ul>
</dd>
@ -578,11 +578,11 @@
</dd></dl>
<dl class="http get">
<dt id="get--api-activities-(string-activity_short_id)-gpx-segment-(int-segment_id)">
<code class="sig-name descname">GET </code><code class="sig-name descname">/api/activities/</code><span class="sig-paren">(</span><em class="property">string: </em><em class="sig-param">activity_short_id</em><span class="sig-paren">)</span><code class="sig-name descname">/gpx/segment/</code><span class="sig-paren">(</span><em class="property">int: </em><em class="sig-param">segment_id</em><span class="sig-paren">)</span><a class="headerlink" href="#get--api-activities-(string-activity_short_id)-gpx-segment-(int-segment_id)" title="Permalink to this definition"></a></dt>
<dd><p>Get gpx file for an activity segment displayed on map with Leaflet</p>
<dt id="get--api-workouts-(string-workout_short_id)-gpx-segment-(int-segment_id)">
<code class="sig-name descname">GET </code><code class="sig-name descname">/api/workouts/</code><span class="sig-paren">(</span><em class="property">string: </em><em class="sig-param">workout_short_id</em><span class="sig-paren">)</span><code class="sig-name descname">/gpx/segment/</code><span class="sig-paren">(</span><em class="property">int: </em><em class="sig-param">segment_id</em><span class="sig-paren">)</span><a class="headerlink" href="#get--api-workouts-(string-workout_short_id)-gpx-segment-(int-segment_id)" title="Permalink to this definition"></a></dt>
<dd><p>Get gpx file for an workout segment displayed on map with Leaflet</p>
<p><strong>Example request</strong>:</p>
<div class="highlight-http notranslate"><div class="highlight"><pre><span></span><span class="nf">GET</span> <span class="nn">/api/activities/kjxavSTUrJvoAh2wvCeGEF/gpx/segment/0</span> <span class="kr">HTTP</span><span class="o">/</span><span class="m">1.1</span>
<div class="highlight-http notranslate"><div class="highlight"><pre><span></span><span class="nf">GET</span> <span class="nn">/api/workouts/kjxavSTUrJvoAh2wvCeGEF/gpx/segment/0</span> <span class="kr">HTTP</span><span class="o">/</span><span class="m">1.1</span>
<span class="na">Content-Type</span><span class="o">:</span> <span class="l">application/json</span>
</pre></div>
</div>
@ -603,7 +603,7 @@
<dt class="field-odd">Parameters</dt>
<dd class="field-odd"><ul class="simple">
<li><p><strong>auth_user_id</strong> (<em>integer</em>) authenticate user id (from JSON Web Token)</p></li>
<li><p><strong>activity_short_id</strong> (<em>string</em>) activity short id</p></li>
<li><p><strong>workout_short_id</strong> (<em>string</em>) workout short id</p></li>
<li><p><strong>segment_id</strong> (<em>integer</em>) segment id</p></li>
</ul>
</dd>
@ -615,14 +615,14 @@
<dt class="field-odd">Status Codes</dt>
<dd class="field-odd"><ul class="simple">
<li><p><a class="reference external" href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.2.1">200 OK</a> success</p></li>
<li><p><a class="reference external" href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.1">400 Bad Request</a> no gpx file for this activity</p></li>
<li><p><a class="reference external" href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.1">400 Bad Request</a> no gpx file for this workout</p></li>
<li><p><a class="reference external" href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.2">401 Unauthorized</a> <ul>
<li><p>Provide a valid auth token.</p></li>
<li><p>Signature expired. Please log in again.</p></li>
<li><p>Invalid token. Please log in again.</p></li>
</ul>
</p></li>
<li><p><a class="reference external" href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.5">404 Not Found</a> activity not found</p></li>
<li><p><a class="reference external" href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.5">404 Not Found</a> workout not found</p></li>
<li><p><a class="reference external" href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.5.1">500 Internal Server Error</a> </p></li>
</ul>
</dd>
@ -630,11 +630,11 @@
</dd></dl>
<dl class="http get">
<dt id="get--api-activities-map-(map_id)">
<code class="sig-name descname">GET </code><code class="sig-name descname">/api/activities/map/</code><span class="sig-paren">(</span><em class="sig-param">map_id</em><span class="sig-paren">)</span><a class="headerlink" href="#get--api-activities-map-(map_id)" title="Permalink to this definition"></a></dt>
<dd><p>Get map image for activities with gpx</p>
<dt id="get--api-workouts-map-(map_id)">
<code class="sig-name descname">GET </code><code class="sig-name descname">/api/workouts/map/</code><span class="sig-paren">(</span><em class="sig-param">map_id</em><span class="sig-paren">)</span><a class="headerlink" href="#get--api-workouts-map-(map_id)" title="Permalink to this definition"></a></dt>
<dd><p>Get map image for workouts with gpx</p>
<p><strong>Example request</strong>:</p>
<div class="highlight-http notranslate"><div class="highlight"><pre><span></span><span class="nf">GET</span> <span class="nn">/api/activities/map/fa33f4d996844a5c73ecd1ae24456ab8?1563529507772</span>
<div class="highlight-http notranslate"><div class="highlight"><pre><span></span><span class="nf">GET</span> <span class="nn">/api/workouts/map/fa33f4d996844a5c73ecd1ae24456ab8?1563529507772</span>
<span class="kr">HTTP</span><span class="o">/</span><span class="m">1.1</span>
</pre></div>
</div>
@ -646,7 +646,7 @@
<dl class="field-list simple">
<dt class="field-odd">Parameters</dt>
<dd class="field-odd"><ul class="simple">
<li><p><strong>map_id</strong> (<em>string</em>) activity map id</p></li>
<li><p><strong>map_id</strong> (<em>string</em>) workout map id</p></li>
</ul>
</dd>
<dt class="field-even">Status Codes</dt>
@ -666,11 +666,11 @@
</dd></dl>
<dl class="http get">
<dt id="get--api-activities-map_tile-(s)-(z)-(x)-(y).png">
<code class="sig-name descname">GET </code><code class="sig-name descname">/api/activities/map_tile/</code><span class="sig-paren">(</span><em class="sig-param">s</em><span class="sig-paren">)</span><code class="sig-name descname">/</code><span class="sig-paren">(</span><em class="sig-param">z</em><span class="sig-paren">)</span><code class="sig-name descname">/</code><span class="sig-paren">(</span><em class="sig-param">x</em><span class="sig-paren">)</span><code class="sig-name descname">/</code><span class="sig-paren">(</span><em class="sig-param">y</em><span class="sig-paren">)</span><code class="sig-name descname">.png</code><a class="headerlink" href="#get--api-activities-map_tile-(s)-(z)-(x)-(y).png" title="Permalink to this definition"></a></dt>
<dt id="get--api-workouts-map_tile-(s)-(z)-(x)-(y).png">
<code class="sig-name descname">GET </code><code class="sig-name descname">/api/workouts/map_tile/</code><span class="sig-paren">(</span><em class="sig-param">s</em><span class="sig-paren">)</span><code class="sig-name descname">/</code><span class="sig-paren">(</span><em class="sig-param">z</em><span class="sig-paren">)</span><code class="sig-name descname">/</code><span class="sig-paren">(</span><em class="sig-param">x</em><span class="sig-paren">)</span><code class="sig-name descname">/</code><span class="sig-paren">(</span><em class="sig-param">y</em><span class="sig-paren">)</span><code class="sig-name descname">.png</code><a class="headerlink" href="#get--api-workouts-map_tile-(s)-(z)-(x)-(y).png" title="Permalink to this definition"></a></dt>
<dd><p>Get map tile from tile server.</p>
<p><strong>Example request</strong>:</p>
<div class="highlight-http notranslate"><div class="highlight"><pre><span></span><span class="nf">GET</span> <span class="nn">/api/activities/map_tile/c/13/4109/2930.png</span> <span class="kr">HTTP</span><span class="o">/</span><span class="m">1.1</span>
<div class="highlight-http notranslate"><div class="highlight"><pre><span></span><span class="nf">GET</span> <span class="nn">/api/workouts/map_tile/c/13/4109/2930.png</span> <span class="kr">HTTP</span><span class="o">/</span><span class="m">1.1</span>
</pre></div>
</div>
<p><strong>Example response</strong>:</p>
@ -692,11 +692,11 @@
</dd></dl>
<dl class="http post">
<dt id="post--api-activities">
<code class="sig-name descname">POST </code><code class="sig-name descname">/api/activities</code><a class="headerlink" href="#post--api-activities" title="Permalink to this definition"></a></dt>
<dd><p>Post an activity with a gpx file</p>
<dt id="post--api-workouts">
<code class="sig-name descname">POST </code><code class="sig-name descname">/api/workouts</code><a class="headerlink" href="#post--api-workouts" title="Permalink to this definition"></a></dt>
<dd><p>Post an workout with a gpx file</p>
<p><strong>Example request</strong>:</p>
<div class="highlight-http notranslate"><div class="highlight"><pre><span></span><span class="nf">POST</span> <span class="nn">/api/activities/</span> <span class="kr">HTTP</span><span class="o">/</span><span class="m">1.1</span>
<div class="highlight-http notranslate"><div class="highlight"><pre><span></span><span class="nf">POST</span> <span class="nn">/api/workouts/</span> <span class="kr">HTTP</span><span class="o">/</span><span class="m">1.1</span>
<span class="na">Content-Type</span><span class="o">:</span> <span class="l">multipart/form-data</span>
</pre></div>
</div>
@ -706,9 +706,8 @@
<span class="p">{</span>
<span class="nt">&quot;data&quot;</span><span class="p">:</span> <span class="p">{</span>
<span class="nt">&quot;activities&quot;</span><span class="p">:</span> <span class="p">[</span>
<span class="nt">&quot;workouts&quot;</span><span class="p">:</span> <span class="p">[</span>
<span class="p">{</span>
<span class="nt">&quot;activity_date&quot;</span><span class="p">:</span> <span class="s2">&quot;Mon, 01 Jan 2018 00:00:00 GMT&quot;</span><span class="p">,</span>
<span class="nt">&quot;ascent&quot;</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
<span class="nt">&quot;ave_speed&quot;</span><span class="p">:</span> <span class="mf">10.0</span><span class="p">,</span>
<span class="nt">&quot;bounds&quot;</span><span class="p">:</span> <span class="p">[],</span>
@ -723,46 +722,46 @@
<span class="nt">&quot;min_alt&quot;</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
<span class="nt">&quot;modification_date&quot;</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
<span class="nt">&quot;moving&quot;</span><span class="p">:</span> <span class="s2">&quot;0:17:04&quot;</span><span class="p">,</span>
<span class="nt">&quot;next_activity&quot;</span><span class="p">:</span> <span class="mi">3</span><span class="p">,</span>
<span class="nt">&quot;next_workout&quot;</span><span class="p">:</span> <span class="mi">3</span><span class="p">,</span>
<span class="nt">&quot;notes&quot;</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
<span class="nt">&quot;pauses&quot;</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
<span class="nt">&quot;previous_activity&quot;</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
<span class="nt">&quot;previous_workout&quot;</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
<span class="nt">&quot;records&quot;</span><span class="p">:</span> <span class="p">[</span>
<span class="p">{</span>
<span class="nt">&quot;activity_date&quot;</span><span class="p">:</span> <span class="s2">&quot;Mon, 01 Jan 2018 00:00:00 GMT&quot;</span><span class="p">,</span>
<span class="nt">&quot;activity_id&quot;</span><span class="p">:</span> <span class="s2">&quot;kjxavSTUrJvoAh2wvCeGEF&quot;</span><span class="p">,</span>
<span class="nt">&quot;id&quot;</span><span class="p">:</span> <span class="mi">4</span><span class="p">,</span>
<span class="nt">&quot;record_type&quot;</span><span class="p">:</span> <span class="s2">&quot;MS&quot;</span><span class="p">,</span>
<span class="nt">&quot;sport_id&quot;</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
<span class="nt">&quot;user&quot;</span><span class="p">:</span> <span class="s2">&quot;admin&quot;</span><span class="p">,</span>
<span class="nt">&quot;value&quot;</span><span class="p">:</span> <span class="mf">10.0</span>
<span class="nt">&quot;value&quot;</span><span class="p">:</span> <span class="mf">10.</span><span class="p">,</span>
<span class="nt">&quot;workout_date&quot;</span><span class="p">:</span> <span class="s2">&quot;Mon, 01 Jan 2018 00:00:00 GMT&quot;</span><span class="p">,</span>
<span class="nt">&quot;workout_id&quot;</span><span class="p">:</span> <span class="s2">&quot;kjxavSTUrJvoAh2wvCeGEF&quot;</span>
<span class="p">},</span>
<span class="p">{</span>
<span class="nt">&quot;activity_date&quot;</span><span class="p">:</span> <span class="s2">&quot;Mon, 01 Jan 2018 00:00:00 GMT&quot;</span><span class="p">,</span>
<span class="nt">&quot;activity_id&quot;</span><span class="p">:</span> <span class="s2">&quot;kjxavSTUrJvoAh2wvCeGEF&quot;</span><span class="p">,</span>
<span class="nt">&quot;id&quot;</span><span class="p">:</span> <span class="mi">3</span><span class="p">,</span>
<span class="nt">&quot;record_type&quot;</span><span class="p">:</span> <span class="s2">&quot;LD&quot;</span><span class="p">,</span>
<span class="nt">&quot;sport_id&quot;</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
<span class="nt">&quot;user&quot;</span><span class="p">:</span> <span class="s2">&quot;admin&quot;</span><span class="p">,</span>
<span class="nt">&quot;value&quot;</span><span class="p">:</span> <span class="s2">&quot;0:17:04&quot;</span>
<span class="nt">&quot;value&quot;</span><span class="p">:</span> <span class="s2">&quot;0:17:04&quot;</span><span class="p">,</span>
<span class="nt">&quot;workout_date&quot;</span><span class="p">:</span> <span class="s2">&quot;Mon, 01 Jan 2018 00:00:00 GMT&quot;</span><span class="p">,</span>
<span class="nt">&quot;workout_id&quot;</span><span class="p">:</span> <span class="s2">&quot;kjxavSTUrJvoAh2wvCeGEF&quot;</span><span class="p">,</span>
<span class="p">},</span>
<span class="p">{</span>
<span class="nt">&quot;activity_date&quot;</span><span class="p">:</span> <span class="s2">&quot;Mon, 01 Jan 2018 00:00:00 GMT&quot;</span><span class="p">,</span>
<span class="nt">&quot;activity_id&quot;</span><span class="p">:</span> <span class="s2">&quot;kjxavSTUrJvoAh2wvCeGEF&quot;</span><span class="p">,</span>
<span class="nt">&quot;id&quot;</span><span class="p">:</span> <span class="mi">2</span><span class="p">,</span>
<span class="nt">&quot;record_type&quot;</span><span class="p">:</span> <span class="s2">&quot;FD&quot;</span><span class="p">,</span>
<span class="nt">&quot;sport_id&quot;</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
<span class="nt">&quot;user&quot;</span><span class="p">:</span> <span class="s2">&quot;admin&quot;</span><span class="p">,</span>
<span class="nt">&quot;value&quot;</span><span class="p">:</span> <span class="mf">10.0</span>
<span class="nt">&quot;value&quot;</span><span class="p">:</span> <span class="mf">10.0</span><span class="p">,</span>
<span class="nt">&quot;workout_date&quot;</span><span class="p">:</span> <span class="s2">&quot;Mon, 01 Jan 2018 00:00:00 GMT&quot;</span><span class="p">,</span>
<span class="nt">&quot;workout_id&quot;</span><span class="p">:</span> <span class="s2">&quot;kjxavSTUrJvoAh2wvCeGEF&quot;</span>
<span class="p">},</span>
<span class="p">{</span>
<span class="nt">&quot;activity_date&quot;</span><span class="p">:</span> <span class="s2">&quot;Mon, 01 Jan 2018 00:00:00 GMT&quot;</span><span class="p">,</span>
<span class="nt">&quot;activity_id&quot;</span><span class="p">:</span> <span class="s2">&quot;kjxavSTUrJvoAh2wvCeGEF&quot;</span><span class="p">,</span>
<span class="nt">&quot;id&quot;</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
<span class="nt">&quot;record_type&quot;</span><span class="p">:</span> <span class="s2">&quot;AS&quot;</span><span class="p">,</span>
<span class="nt">&quot;sport_id&quot;</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
<span class="nt">&quot;user&quot;</span><span class="p">:</span> <span class="s2">&quot;admin&quot;</span><span class="p">,</span>
<span class="nt">&quot;value&quot;</span><span class="p">:</span> <span class="mf">10.0</span>
<span class="nt">&quot;value&quot;</span><span class="p">:</span> <span class="mf">10.0</span><span class="p">,</span>
<span class="nt">&quot;workout_date&quot;</span><span class="p">:</span> <span class="s2">&quot;Mon, 01 Jan 2018 00:00:00 GMT&quot;</span><span class="p">,</span>
<span class="nt">&quot;workout_id&quot;</span><span class="p">:</span> <span class="s2">&quot;kjxavSTUrJvoAh2wvCeGEF&quot;</span>
<span class="p">}</span>
<span class="p">],</span>
<span class="nt">&quot;segments&quot;</span><span class="p">:</span> <span class="p">[],</span>
@ -771,7 +770,8 @@
<span class="nt">&quot;user&quot;</span><span class="p">:</span> <span class="s2">&quot;admin&quot;</span><span class="p">,</span>
<span class="nt">&quot;weather_end&quot;</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
<span class="nt">&quot;weather_start&quot;</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
<span class="nt">&quot;with_gpx&quot;</span><span class="p">:</span> <span class="kc">false</span>
<span class="nt">&quot;with_gpx&quot;</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
<span class="nt">&quot;workout_date&quot;</span><span class="p">:</span> <span class="s2">&quot;Mon, 01 Jan 2018 00:00:00 GMT&quot;</span>
<span class="p">}</span>
<span class="p">]</span>
<span class="p">},</span>
@ -798,7 +798,7 @@
</dd>
<dt class="field-even">Status Codes</dt>
<dd class="field-even"><ul class="simple">
<li><p><a class="reference external" href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.2.2">201 Created</a> activity created</p></li>
<li><p><a class="reference external" href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.2.2">201 Created</a> workout created</p></li>
<li><p><a class="reference external" href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.1">400 Bad Request</a> <ul>
<li><p>Invalid payload.</p></li>
<li><p>No file part.</p></li>
@ -820,11 +820,11 @@
</dd></dl>
<dl class="http post">
<dt id="post--api-activities-no_gpx">
<code class="sig-name descname">POST </code><code class="sig-name descname">/api/activities/no_gpx</code><a class="headerlink" href="#post--api-activities-no_gpx" title="Permalink to this definition"></a></dt>
<dd><p>Post an activity without gpx file</p>
<dt id="post--api-workouts-no_gpx">
<code class="sig-name descname">POST </code><code class="sig-name descname">/api/workouts/no_gpx</code><a class="headerlink" href="#post--api-workouts-no_gpx" title="Permalink to this definition"></a></dt>
<dd><p>Post an workout without gpx file</p>
<p><strong>Example request</strong>:</p>
<div class="highlight-http notranslate"><div class="highlight"><pre><span></span><span class="nf">POST</span> <span class="nn">/api/activities/no_gpx</span> <span class="kr">HTTP</span><span class="o">/</span><span class="m">1.1</span>
<div class="highlight-http notranslate"><div class="highlight"><pre><span></span><span class="nf">POST</span> <span class="nn">/api/workouts/no_gpx</span> <span class="kr">HTTP</span><span class="o">/</span><span class="m">1.1</span>
<span class="na">Content-Type</span><span class="o">:</span> <span class="l">application/json</span>
</pre></div>
</div>
@ -834,9 +834,8 @@
<span class="p">{</span>
<span class="nt">&quot;data&quot;</span><span class="p">:</span> <span class="p">{</span>
<span class="nt">&quot;activities&quot;</span><span class="p">:</span> <span class="p">[</span>
<span class="nt">&quot;workouts&quot;</span><span class="p">:</span> <span class="p">[</span>
<span class="p">{</span>
<span class="nt">&quot;activity_date&quot;</span><span class="p">:</span> <span class="s2">&quot;Mon, 01 Jan 2018 00:00:00 GMT&quot;</span><span class="p">,</span>
<span class="nt">&quot;ascent&quot;</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
<span class="nt">&quot;ave_speed&quot;</span><span class="p">:</span> <span class="mf">10.0</span><span class="p">,</span>
<span class="nt">&quot;bounds&quot;</span><span class="p">:</span> <span class="p">[],</span>
@ -850,46 +849,46 @@
<span class="nt">&quot;min_alt&quot;</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
<span class="nt">&quot;modification_date&quot;</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
<span class="nt">&quot;moving&quot;</span><span class="p">:</span> <span class="s2">&quot;0:17:04&quot;</span><span class="p">,</span>
<span class="nt">&quot;next_activity&quot;</span><span class="p">:</span> <span class="mi">3</span><span class="p">,</span>
<span class="nt">&quot;next_workout&quot;</span><span class="p">:</span> <span class="mi">3</span><span class="p">,</span>
<span class="nt">&quot;notes&quot;</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
<span class="nt">&quot;pauses&quot;</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
<span class="nt">&quot;previous_activity&quot;</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
<span class="nt">&quot;previous_workout&quot;</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
<span class="nt">&quot;records&quot;</span><span class="p">:</span> <span class="p">[</span>
<span class="p">{</span>
<span class="nt">&quot;activity_date&quot;</span><span class="p">:</span> <span class="s2">&quot;Mon, 01 Jan 2018 00:00:00 GMT&quot;</span><span class="p">,</span>
<span class="nt">&quot;activity_id&quot;</span><span class="p">:</span> <span class="s2">&quot;kjxavSTUrJvoAh2wvCeGEF&quot;</span><span class="p">,</span>
<span class="nt">&quot;id&quot;</span><span class="p">:</span> <span class="mi">4</span><span class="p">,</span>
<span class="nt">&quot;record_type&quot;</span><span class="p">:</span> <span class="s2">&quot;MS&quot;</span><span class="p">,</span>
<span class="nt">&quot;sport_id&quot;</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
<span class="nt">&quot;user&quot;</span><span class="p">:</span> <span class="s2">&quot;admin&quot;</span><span class="p">,</span>
<span class="nt">&quot;value&quot;</span><span class="p">:</span> <span class="mf">10.0</span>
<span class="nt">&quot;value&quot;</span><span class="p">:</span> <span class="mf">10.</span><span class="p">,</span>
<span class="nt">&quot;workout_date&quot;</span><span class="p">:</span> <span class="s2">&quot;Mon, 01 Jan 2018 00:00:00 GMT&quot;</span><span class="p">,</span>
<span class="nt">&quot;workout_id&quot;</span><span class="p">:</span> <span class="s2">&quot;kjxavSTUrJvoAh2wvCeGEF&quot;</span>
<span class="p">},</span>
<span class="p">{</span>
<span class="nt">&quot;activity_date&quot;</span><span class="p">:</span> <span class="s2">&quot;Mon, 01 Jan 2018 00:00:00 GMT&quot;</span><span class="p">,</span>
<span class="nt">&quot;activity_id&quot;</span><span class="p">:</span> <span class="s2">&quot;kjxavSTUrJvoAh2wvCeGEF&quot;</span><span class="p">,</span>
<span class="nt">&quot;id&quot;</span><span class="p">:</span> <span class="mi">3</span><span class="p">,</span>
<span class="nt">&quot;record_type&quot;</span><span class="p">:</span> <span class="s2">&quot;LD&quot;</span><span class="p">,</span>
<span class="nt">&quot;sport_id&quot;</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
<span class="nt">&quot;user&quot;</span><span class="p">:</span> <span class="s2">&quot;admin&quot;</span><span class="p">,</span>
<span class="nt">&quot;value&quot;</span><span class="p">:</span> <span class="s2">&quot;0:17:04&quot;</span>
<span class="nt">&quot;value&quot;</span><span class="p">:</span> <span class="s2">&quot;0:17:04&quot;</span><span class="p">,</span>
<span class="nt">&quot;workout_date&quot;</span><span class="p">:</span> <span class="s2">&quot;Mon, 01 Jan 2018 00:00:00 GMT&quot;</span><span class="p">,</span>
<span class="nt">&quot;workout_id&quot;</span><span class="p">:</span> <span class="s2">&quot;kjxavSTUrJvoAh2wvCeGEF&quot;</span>
<span class="p">},</span>
<span class="p">{</span>
<span class="nt">&quot;activity_date&quot;</span><span class="p">:</span> <span class="s2">&quot;Mon, 01 Jan 2018 00:00:00 GMT&quot;</span><span class="p">,</span>
<span class="nt">&quot;activity_id&quot;</span><span class="p">:</span> <span class="s2">&quot;kjxavSTUrJvoAh2wvCeGEF&quot;</span><span class="p">,</span>
<span class="nt">&quot;id&quot;</span><span class="p">:</span> <span class="mi">2</span><span class="p">,</span>
<span class="nt">&quot;record_type&quot;</span><span class="p">:</span> <span class="s2">&quot;FD&quot;</span><span class="p">,</span>
<span class="nt">&quot;sport_id&quot;</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
<span class="nt">&quot;user&quot;</span><span class="p">:</span> <span class="s2">&quot;admin&quot;</span><span class="p">,</span>
<span class="nt">&quot;value&quot;</span><span class="p">:</span> <span class="mf">10.0</span>
<span class="nt">&quot;value&quot;</span><span class="p">:</span> <span class="mf">10.0</span><span class="p">,</span>
<span class="nt">&quot;workout_date&quot;</span><span class="p">:</span> <span class="s2">&quot;Mon, 01 Jan 2018 00:00:00 GMT&quot;</span><span class="p">,</span>
<span class="nt">&quot;workout_id&quot;</span><span class="p">:</span> <span class="s2">&quot;kjxavSTUrJvoAh2wvCeGEF&quot;</span>
<span class="p">},</span>
<span class="p">{</span>
<span class="nt">&quot;activity_date&quot;</span><span class="p">:</span> <span class="s2">&quot;Mon, 01 Jan 2018 00:00:00 GMT&quot;</span><span class="p">,</span>
<span class="nt">&quot;activity_id&quot;</span><span class="p">:</span> <span class="s2">&quot;kjxavSTUrJvoAh2wvCeGEF&quot;</span><span class="p">,</span>
<span class="nt">&quot;id&quot;</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
<span class="nt">&quot;record_type&quot;</span><span class="p">:</span> <span class="s2">&quot;AS&quot;</span><span class="p">,</span>
<span class="nt">&quot;sport_id&quot;</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
<span class="nt">&quot;user&quot;</span><span class="p">:</span> <span class="s2">&quot;admin&quot;</span><span class="p">,</span>
<span class="nt">&quot;value&quot;</span><span class="p">:</span> <span class="mf">10.0</span>
<span class="nt">&quot;value&quot;</span><span class="p">:</span> <span class="mf">10.0</span><span class="p">,</span>
<span class="nt">&quot;workout_date&quot;</span><span class="p">:</span> <span class="s2">&quot;Mon, 01 Jan 2018 00:00:00 GMT&quot;</span><span class="p">,</span>
<span class="nt">&quot;workout_id&quot;</span><span class="p">:</span> <span class="s2">&quot;kjxavSTUrJvoAh2wvCeGEF&quot;</span>
<span class="p">}</span>
<span class="p">],</span>
<span class="nt">&quot;segments&quot;</span><span class="p">:</span> <span class="p">[],</span>
@ -899,7 +898,8 @@
<span class="nt">&quot;uuid&quot;</span><span class="p">:</span> <span class="nt">&quot;kjxavSTUrJvoAh2wvCeGEF&quot;</span>
<span class="nt">&quot;weather_end&quot;</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
<span class="nt">&quot;weather_start&quot;</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
<span class="nt">&quot;with_gpx&quot;</span><span class="p">:</span> <span class="kc">false</span>
<span class="nt">&quot;with_gpx&quot;</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
<span class="nt">&quot;workout_date&quot;</span><span class="p">:</span> <span class="s2">&quot;Mon, 01 Jan 2018 00:00:00 GMT&quot;</span>
<span class="p">}</span>
<span class="p">]</span>
<span class="p">},</span>
@ -915,12 +915,12 @@
</dd>
<dt class="field-even">Request JSON Object</dt>
<dd class="field-even"><ul class="simple">
<li><p><strong>activity_date</strong> (<em>string</em>) activity date (format: <code class="docutils literal notranslate"><span class="pre">%Y-%m-%d</span> <span class="pre">%H:%M</span></code>)</p></li>
<li><p><strong>distance</strong> (<em>float</em>) activity distance in km</p></li>
<li><p><strong>duration</strong> (<em>integer</em>) activity duration in seconds</p></li>
<li><p><strong>workout_date</strong> (<em>string</em>) workout date (format: <code class="docutils literal notranslate"><span class="pre">%Y-%m-%d</span> <span class="pre">%H:%M</span></code>)</p></li>
<li><p><strong>distance</strong> (<em>float</em>) workout distance in km</p></li>
<li><p><strong>duration</strong> (<em>integer</em>) workout duration in seconds</p></li>
<li><p><strong>notes</strong> (<em>string</em>) notes (not mandatory)</p></li>
<li><p><strong>sport_id</strong> (<em>integer</em>) activity sport id</p></li>
<li><p><strong>title</strong> (<em>string</em>) activity title</p></li>
<li><p><strong>sport_id</strong> (<em>integer</em>) workout sport id</p></li>
<li><p><strong>title</strong> (<em>string</em>) workout title</p></li>
</ul>
</dd>
<dt class="field-odd">Request Headers</dt>
@ -930,7 +930,7 @@
</dd>
<dt class="field-even">Status Codes</dt>
<dd class="field-even"><ul class="simple">
<li><p><a class="reference external" href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.2.2">201 Created</a> activity created</p></li>
<li><p><a class="reference external" href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.2.2">201 Created</a> workout created</p></li>
<li><p><a class="reference external" href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.1">400 Bad Request</a> invalid payload</p></li>
<li><p><a class="reference external" href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.2">401 Unauthorized</a> <ul>
<li><p>Provide a valid auth token.</p></li>
@ -945,11 +945,11 @@
</dd></dl>
<dl class="http patch">
<dt id="patch--api-activities-(string-activity_short_id)">
<code class="sig-name descname">PATCH </code><code class="sig-name descname">/api/activities/</code><span class="sig-paren">(</span><em class="property">string: </em><em class="sig-param">activity_short_id</em><span class="sig-paren">)</span><a class="headerlink" href="#patch--api-activities-(string-activity_short_id)" title="Permalink to this definition"></a></dt>
<dd><p>Update an activity</p>
<dt id="patch--api-workouts-(string-workout_short_id)">
<code class="sig-name descname">PATCH </code><code class="sig-name descname">/api/workouts/</code><span class="sig-paren">(</span><em class="property">string: </em><em class="sig-param">workout_short_id</em><span class="sig-paren">)</span><a class="headerlink" href="#patch--api-workouts-(string-workout_short_id)" title="Permalink to this definition"></a></dt>
<dd><p>Update an workout</p>
<p><strong>Example request</strong>:</p>
<div class="highlight-http notranslate"><div class="highlight"><pre><span></span><span class="nf">PATCH</span> <span class="nn">/api/activities/1</span> <span class="kr">HTTP</span><span class="o">/</span><span class="m">1.1</span>
<div class="highlight-http notranslate"><div class="highlight"><pre><span></span><span class="nf">PATCH</span> <span class="nn">/api/workouts/1</span> <span class="kr">HTTP</span><span class="o">/</span><span class="m">1.1</span>
<span class="na">Content-Type</span><span class="o">:</span> <span class="l">application/json</span>
</pre></div>
</div>
@ -959,9 +959,8 @@
<span class="p">{</span>
<span class="nt">&quot;data&quot;</span><span class="p">:</span> <span class="p">{</span>
<span class="nt">&quot;activities&quot;</span><span class="p">:</span> <span class="p">[</span>
<span class="nt">&quot;workouts&quot;</span><span class="p">:</span> <span class="p">[</span>
<span class="p">{</span>
<span class="nt">&quot;activity_date&quot;</span><span class="p">:</span> <span class="s2">&quot;Mon, 01 Jan 2018 00:00:00 GMT&quot;</span><span class="p">,</span>
<span class="nt">&quot;ascent&quot;</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
<span class="nt">&quot;ave_speed&quot;</span><span class="p">:</span> <span class="mf">10.0</span><span class="p">,</span>
<span class="nt">&quot;bounds&quot;</span><span class="p">:</span> <span class="p">[],</span>
@ -975,46 +974,46 @@
<span class="nt">&quot;min_alt&quot;</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
<span class="nt">&quot;modification_date&quot;</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
<span class="nt">&quot;moving&quot;</span><span class="p">:</span> <span class="s2">&quot;0:17:04&quot;</span><span class="p">,</span>
<span class="nt">&quot;next_activity&quot;</span><span class="p">:</span> <span class="mi">3</span><span class="p">,</span>
<span class="nt">&quot;next_workout&quot;</span><span class="p">:</span> <span class="mi">3</span><span class="p">,</span>
<span class="nt">&quot;notes&quot;</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
<span class="nt">&quot;pauses&quot;</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
<span class="nt">&quot;previous_activity&quot;</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
<span class="nt">&quot;previous_workout&quot;</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
<span class="nt">&quot;records&quot;</span><span class="p">:</span> <span class="p">[</span>
<span class="p">{</span>
<span class="nt">&quot;activity_date&quot;</span><span class="p">:</span> <span class="s2">&quot;Mon, 01 Jan 2018 00:00:00 GMT&quot;</span><span class="p">,</span>
<span class="nt">&quot;activity_id&quot;</span><span class="p">:</span> <span class="s2">&quot;kjxavSTUrJvoAh2wvCeGEF&quot;</span><span class="p">,</span>
<span class="nt">&quot;id&quot;</span><span class="p">:</span> <span class="mi">4</span><span class="p">,</span>
<span class="nt">&quot;record_type&quot;</span><span class="p">:</span> <span class="s2">&quot;MS&quot;</span><span class="p">,</span>
<span class="nt">&quot;sport_id&quot;</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
<span class="nt">&quot;user&quot;</span><span class="p">:</span> <span class="s2">&quot;admin&quot;</span><span class="p">,</span>
<span class="nt">&quot;value&quot;</span><span class="p">:</span> <span class="mf">10.0</span>
<span class="nt">&quot;value&quot;</span><span class="p">:</span> <span class="mf">10.0</span><span class="p">,</span>
<span class="nt">&quot;workout_date&quot;</span><span class="p">:</span> <span class="s2">&quot;Mon, 01 Jan 2018 00:00:00 GMT&quot;</span><span class="p">,</span>
<span class="nt">&quot;workout_id&quot;</span><span class="p">:</span> <span class="s2">&quot;kjxavSTUrJvoAh2wvCeGEF&quot;</span>
<span class="p">},</span>
<span class="p">{</span>
<span class="nt">&quot;activity_date&quot;</span><span class="p">:</span> <span class="s2">&quot;Mon, 01 Jan 2018 00:00:00 GMT&quot;</span><span class="p">,</span>
<span class="nt">&quot;activity_id&quot;</span><span class="p">:</span> <span class="s2">&quot;kjxavSTUrJvoAh2wvCeGEF&quot;</span><span class="p">,</span>
<span class="nt">&quot;id&quot;</span><span class="p">:</span> <span class="mi">3</span><span class="p">,</span>
<span class="nt">&quot;record_type&quot;</span><span class="p">:</span> <span class="s2">&quot;LD&quot;</span><span class="p">,</span>
<span class="nt">&quot;sport_id&quot;</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
<span class="nt">&quot;user&quot;</span><span class="p">:</span> <span class="s2">&quot;admin&quot;</span><span class="p">,</span>
<span class="nt">&quot;value&quot;</span><span class="p">:</span> <span class="s2">&quot;0:17:04&quot;</span>
<span class="nt">&quot;value&quot;</span><span class="p">:</span> <span class="s2">&quot;0:17:04&quot;</span><span class="p">,</span>
<span class="nt">&quot;workout_date&quot;</span><span class="p">:</span> <span class="s2">&quot;Mon, 01 Jan 2018 00:00:00 GMT&quot;</span><span class="p">,</span>
<span class="nt">&quot;workout_id&quot;</span><span class="p">:</span> <span class="s2">&quot;kjxavSTUrJvoAh2wvCeGEF&quot;</span>
<span class="p">},</span>
<span class="p">{</span>
<span class="nt">&quot;activity_date&quot;</span><span class="p">:</span> <span class="s2">&quot;Mon, 01 Jan 2018 00:00:00 GMT&quot;</span><span class="p">,</span>
<span class="nt">&quot;activity_id&quot;</span><span class="p">:</span> <span class="s2">&quot;kjxavSTUrJvoAh2wvCeGEF&quot;</span><span class="p">,</span>
<span class="nt">&quot;id&quot;</span><span class="p">:</span> <span class="mi">2</span><span class="p">,</span>
<span class="nt">&quot;record_type&quot;</span><span class="p">:</span> <span class="s2">&quot;FD&quot;</span><span class="p">,</span>
<span class="nt">&quot;sport_id&quot;</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
<span class="nt">&quot;user&quot;</span><span class="p">:</span> <span class="s2">&quot;admin&quot;</span><span class="p">,</span>
<span class="nt">&quot;value&quot;</span><span class="p">:</span> <span class="mf">10.0</span>
<span class="nt">&quot;value&quot;</span><span class="p">:</span> <span class="mf">10.0</span><span class="p">,</span>
<span class="nt">&quot;workout_date&quot;</span><span class="p">:</span> <span class="s2">&quot;Mon, 01 Jan 2018 00:00:00 GMT&quot;</span><span class="p">,</span>
<span class="nt">&quot;workout_id&quot;</span><span class="p">:</span> <span class="s2">&quot;kjxavSTUrJvoAh2wvCeGEF&quot;</span><span class="p">,</span>
<span class="p">},</span>
<span class="p">{</span>
<span class="nt">&quot;activity_date&quot;</span><span class="p">:</span> <span class="s2">&quot;Mon, 01 Jan 2018 00:00:00 GMT&quot;</span><span class="p">,</span>
<span class="nt">&quot;activity_id&quot;</span><span class="p">:</span> <span class="s2">&quot;kjxavSTUrJvoAh2wvCeGEF&quot;</span><span class="p">,</span>
<span class="nt">&quot;id&quot;</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
<span class="nt">&quot;record_type&quot;</span><span class="p">:</span> <span class="s2">&quot;AS&quot;</span><span class="p">,</span>
<span class="nt">&quot;sport_id&quot;</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
<span class="nt">&quot;user&quot;</span><span class="p">:</span> <span class="s2">&quot;admin&quot;</span><span class="p">,</span>
<span class="nt">&quot;value&quot;</span><span class="p">:</span> <span class="mf">10.0</span>
<span class="nt">&quot;value&quot;</span><span class="p">:</span> <span class="mf">10.0</span><span class="p">,</span>
<span class="nt">&quot;workout_date&quot;</span><span class="p">:</span> <span class="s2">&quot;Mon, 01 Jan 2018 00:00:00 GMT&quot;</span><span class="p">,</span>
<span class="nt">&quot;workout_id&quot;</span><span class="p">:</span> <span class="s2">&quot;kjxavSTUrJvoAh2wvCeGEF&quot;</span><span class="p">,</span>
<span class="p">}</span>
<span class="p">],</span>
<span class="nt">&quot;segments&quot;</span><span class="p">:</span> <span class="p">[],</span>
@ -1024,7 +1023,8 @@
<span class="nt">&quot;uuid&quot;</span><span class="p">:</span> <span class="nt">&quot;kjxavSTUrJvoAh2wvCeGEF&quot;</span>
<span class="nt">&quot;weather_end&quot;</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
<span class="nt">&quot;weather_start&quot;</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
<span class="nt">&quot;with_gpx&quot;</span><span class="p">:</span> <span class="kc">false</span>
<span class="nt">&quot;with_gpx&quot;</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
<span class="nt">&quot;workout_date&quot;</span><span class="p">:</span> <span class="s2">&quot;Mon, 01 Jan 2018 00:00:00 GMT&quot;</span>
<span class="p">}</span>
<span class="p">]</span>
<span class="p">},</span>
@ -1036,20 +1036,20 @@
<dt class="field-odd">Parameters</dt>
<dd class="field-odd"><ul class="simple">
<li><p><strong>auth_user_id</strong> (<em>integer</em>) authenticate user id (from JSON Web Token)</p></li>
<li><p><strong>activity_short_id</strong> (<em>string</em>) activity short id</p></li>
<li><p><strong>workout_short_id</strong> (<em>string</em>) workout short id</p></li>
</ul>
</dd>
<dt class="field-even">Request JSON Object</dt>
<dd class="field-even"><ul class="simple">
<li><p><strong>activity_date</strong> (<em>string</em>) activity date (format: <code class="docutils literal notranslate"><span class="pre">%Y-%m-%d</span> <span class="pre">%H:%M</span></code>)
(only for activity without gpx)</p></li>
<li><p><strong>distance</strong> (<em>float</em>) activity distance in km
(only for activity without gpx)</p></li>
<li><p><strong>duration</strong> (<em>integer</em>) activity duration in seconds
(only for activity without gpx)</p></li>
<li><p><strong>workout_date</strong> (<em>string</em>) workout date (format: <code class="docutils literal notranslate"><span class="pre">%Y-%m-%d</span> <span class="pre">%H:%M</span></code>)
(only for workout without gpx)</p></li>
<li><p><strong>distance</strong> (<em>float</em>) workout distance in km
(only for workout without gpx)</p></li>
<li><p><strong>duration</strong> (<em>integer</em>) workout duration in seconds
(only for workout without gpx)</p></li>
<li><p><strong>notes</strong> (<em>string</em>) notes</p></li>
<li><p><strong>sport_id</strong> (<em>integer</em>) activity sport id</p></li>
<li><p><strong>title</strong> (<em>string</em>) activity title</p></li>
<li><p><strong>sport_id</strong> (<em>integer</em>) workout sport id</p></li>
<li><p><strong>title</strong> (<em>string</em>) workout title</p></li>
</ul>
</dd>
<dt class="field-odd">Request Headers</dt>
@ -1059,7 +1059,7 @@
</dd>
<dt class="field-even">Status Codes</dt>
<dd class="field-even"><ul class="simple">
<li><p><a class="reference external" href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.2.1">200 OK</a> activity updated</p></li>
<li><p><a class="reference external" href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.2.1">200 OK</a> workout updated</p></li>
<li><p><a class="reference external" href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.1">400 Bad Request</a> invalid payload</p></li>
<li><p><a class="reference external" href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.2">401 Unauthorized</a> <ul>
<li><p>Provide a valid auth token.</p></li>
@ -1067,7 +1067,7 @@
<li><p>Invalid token. Please log in again.</p></li>
</ul>
</p></li>
<li><p><a class="reference external" href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.5">404 Not Found</a> activity not found</p></li>
<li><p><a class="reference external" href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.5">404 Not Found</a> workout not found</p></li>
<li><p><a class="reference external" href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.5.1">500 Internal Server Error</a> </p></li>
</ul>
</dd>
@ -1075,11 +1075,11 @@
</dd></dl>
<dl class="http delete">
<dt id="delete--api-activities-(string-activity_short_id)">
<code class="sig-name descname">DELETE </code><code class="sig-name descname">/api/activities/</code><span class="sig-paren">(</span><em class="property">string: </em><em class="sig-param">activity_short_id</em><span class="sig-paren">)</span><a class="headerlink" href="#delete--api-activities-(string-activity_short_id)" title="Permalink to this definition"></a></dt>
<dd><p>Delete an activity</p>
<dt id="delete--api-workouts-(string-workout_short_id)">
<code class="sig-name descname">DELETE </code><code class="sig-name descname">/api/workouts/</code><span class="sig-paren">(</span><em class="property">string: </em><em class="sig-param">workout_short_id</em><span class="sig-paren">)</span><a class="headerlink" href="#delete--api-workouts-(string-workout_short_id)" title="Permalink to this definition"></a></dt>
<dd><p>Delete an workout</p>
<p><strong>Example request</strong>:</p>
<div class="highlight-http notranslate"><div class="highlight"><pre><span></span><span class="nf">DELETE</span> <span class="nn">/api/activities/kjxavSTUrJvoAh2wvCeGEF</span> <span class="kr">HTTP</span><span class="o">/</span><span class="m">1.1</span>
<div class="highlight-http notranslate"><div class="highlight"><pre><span></span><span class="nf">DELETE</span> <span class="nn">/api/workouts/kjxavSTUrJvoAh2wvCeGEF</span> <span class="kr">HTTP</span><span class="o">/</span><span class="m">1.1</span>
<span class="na">Content-Type</span><span class="o">:</span> <span class="l">application/json</span>
</pre></div>
</div>
@ -1092,7 +1092,7 @@
<dt class="field-odd">Parameters</dt>
<dd class="field-odd"><ul class="simple">
<li><p><strong>auth_user_id</strong> (<em>integer</em>) authenticate user id (from JSON Web Token)</p></li>
<li><p><strong>activity_short_id</strong> (<em>string</em>) activity short id</p></li>
<li><p><strong>workout_short_id</strong> (<em>string</em>) workout short id</p></li>
</ul>
</dd>
<dt class="field-even">Request Headers</dt>
@ -1102,14 +1102,14 @@
</dd>
<dt class="field-odd">Status Codes</dt>
<dd class="field-odd"><ul class="simple">
<li><p><a class="reference external" href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.2.5">204 No Content</a> activity deleted</p></li>
<li><p><a class="reference external" href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.2.5">204 No Content</a> workout deleted</p></li>
<li><p><a class="reference external" href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.2">401 Unauthorized</a> <ul>
<li><p>Provide a valid auth token.</p></li>
<li><p>Signature expired. Please log in again.</p></li>
<li><p>Invalid token. Please log in again.</p></li>
</ul>
</p></li>
<li><p><a class="reference external" href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.5">404 Not Found</a> activity not found</p></li>
<li><p><a class="reference external" href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.5">404 Not Found</a> workout not found</p></li>
<li><p><a class="reference external" href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.5.1">500 Internal Server Error</a> Error. Please try again or contact the administrator.</p></li>
</ul>
</dd>

View File

@ -80,13 +80,13 @@
<li><a class="reference internal" href="#list">List</a><ul>
<li><a class="reference internal" href="#administration">Administration</a></li>
<li><a class="reference internal" href="#account">Account</a></li>
<li><a class="reference internal" href="#activities-workouts">Activities/Workouts</a></li>
<li><a class="reference internal" href="#workouts">Workouts</a></li>
<li><a class="reference internal" href="#translations">Translations</a></li>
</ul>
</li>
<li><a class="reference internal" href="#dashboard">Dashboard</a></li>
<li><a class="reference internal" href="#activity-workout-detail">Activity/workout detail</a></li>
<li><a class="reference internal" href="#activities-workouts-list">Activities/workouts list</a></li>
<li><a class="reference internal" href="#workout-detail">Workout detail</a></li>
<li><a class="reference internal" href="#workouts-list">Workouts list</a></li>
<li><a class="reference internal" href="#statistics">Statistics</a></li>
<li><a class="reference internal" href="#id1">Administration</a></li>
</ul>
@ -164,7 +164,7 @@
</li>
<li><p><strong>Sports</strong></p>
<ul class="simple">
<li><p>enable or disable a sport (a sport can be disabled even if activity with this sport exists)</p></li>
<li><p>enable or disable a sport (a sport can be disabled even if workout with this sport exists)</p></li>
</ul>
</li>
</ul>
@ -176,11 +176,11 @@
<li><p>A user can reset his password (<em>new in 0.3.0</em>)</p></li>
</ul>
</div>
<div class="section" id="activities-workouts">
<h3>Activities/Workouts<a class="headerlink" href="#activities-workouts" title="Permalink to this headline"></a></h3>
<div class="section" id="workouts">
<h3>Workouts<a class="headerlink" href="#workouts" title="Permalink to this headline"></a></h3>
<ul class="simple">
<li><dl class="simple">
<dt>6 sports supported:</dt><dd><ul>
<dt>6 sports are supported:</dt><dd><ul>
<li><p>Cycling (Sport)</p></li>
<li><p>Cycling (Transport)</p></li>
<li><p>Hiking</p></li>
@ -191,10 +191,10 @@
</dd>
</dl>
</li>
<li><p>Dashboard with month calendar displaying activities and record. The week can start on Sunday or Monday (which can be changed in the user settings)</p></li>
<li><p>Activity creation by uploading a gpx file. An activity can even be created without gpx (the user must enter date, time, duration and distance)</p></li>
<li><p>An activity 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>Activity edition and deletion. User can add a note</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)</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>Workout edition and deletion. User can add a note</p></li>
<li><p>User statistics</p></li>
<li><dl class="simple">
<dt>User records by sports:</dt><dd><ul>
@ -206,11 +206,11 @@
</dd>
</dl>
</li>
<li><p>Activities list and filter</p></li>
<li><p>Workours list and filter</p></li>
</ul>
<div class="admonition note">
<p class="admonition-title">Note</p>
<p>for now, only the owner of the activity can see the activity.</p>
<p>for now, only the owner of the workout can see it.</p>
</div>
</div>
<div class="section" id="translations">
@ -224,16 +224,16 @@
<img alt="FitTrackee Dashboard" src="_images/fittrackee_screenshot-01.png" />
</div>
</div>
<div class="section" id="activity-workout-detail">
<h2>Activity/workout detail<a class="headerlink" href="#activity-workout-detail" title="Permalink to this headline"></a></h2>
<div class="section" id="workout-detail">
<h2>Workout detail<a class="headerlink" href="#workout-detail" title="Permalink to this headline"></a></h2>
<div class="figure align-default">
<img alt="FitTrackee Activity" src="_images/fittrackee_screenshot-02.png" />
<img alt="FitTrackee Workout" src="_images/fittrackee_screenshot-02.png" />
</div>
</div>
<div class="section" id="activities-workouts-list">
<h2>Activities/workouts list<a class="headerlink" href="#activities-workouts-list" title="Permalink to this headline"></a></h2>
<div class="section" id="workouts-list">
<h2>Workouts list<a class="headerlink" href="#workouts-list" title="Permalink to this headline"></a></h2>
<div class="figure align-default">
<img alt="FitTrackee Activities" src="_images/fittrackee_screenshot-03.png" />
<img alt="FitTrackee Workouts" src="_images/fittrackee_screenshot-03.png" />
</div>
</div>
<div class="section" id="statistics">

View File

@ -125,46 +125,6 @@
<tr class="pcap"><td></td><td>&#160;</td><td></td></tr>
<tr class="cap" id="cap-/api"><td></td><td>
<strong>/api</strong></td><td></td></tr>
<tr>
<td></td>
<td>
<a href="api/activities.html#get--api-activities"><code class="xref">GET /api/activities</code></a></td><td>
<em></em></td></tr>
<tr>
<td></td>
<td>
<a href="api/activities.html#get--api-activities-(string-activity_short_id)"><code class="xref">GET /api/activities/(string:activity_short_id)</code></a></td><td>
<em></em></td></tr>
<tr>
<td></td>
<td>
<a href="api/activities.html#get--api-activities-(string-activity_short_id)-chart_data"><code class="xref">GET /api/activities/(string:activity_short_id)/chart_data</code></a></td><td>
<em></em></td></tr>
<tr>
<td></td>
<td>
<a href="api/activities.html#get--api-activities-(string-activity_short_id)-chart_data-segment-(int-segment_id)"><code class="xref">GET /api/activities/(string:activity_short_id)/chart_data/segment/(int:segment_id)</code></a></td><td>
<em></em></td></tr>
<tr>
<td></td>
<td>
<a href="api/activities.html#get--api-activities-(string-activity_short_id)-gpx"><code class="xref">GET /api/activities/(string:activity_short_id)/gpx</code></a></td><td>
<em></em></td></tr>
<tr>
<td></td>
<td>
<a href="api/activities.html#get--api-activities-(string-activity_short_id)-gpx-segment-(int-segment_id)"><code class="xref">GET /api/activities/(string:activity_short_id)/gpx/segment/(int:segment_id)</code></a></td><td>
<em></em></td></tr>
<tr>
<td></td>
<td>
<a href="api/activities.html#get--api-activities-map-(map_id)"><code class="xref">GET /api/activities/map/(map_id)</code></a></td><td>
<em></em></td></tr>
<tr>
<td></td>
<td>
<a href="api/activities.html#get--api-activities-map_tile-(s)-(z)-(x)-(y).png"><code class="xref">GET /api/activities/map_tile/(s)/(z)/(x)/(y).png</code></a></td><td>
<em></em></td></tr>
<tr>
<td></td>
<td>
@ -233,12 +193,42 @@
<tr>
<td></td>
<td>
<a href="api/activities.html#post--api-activities"><code class="xref">POST /api/activities</code></a></td><td>
<a href="api/workouts.html#get--api-workouts"><code class="xref">GET /api/workouts</code></a></td><td>
<em></em></td></tr>
<tr>
<td></td>
<td>
<a href="api/activities.html#post--api-activities-no_gpx"><code class="xref">POST /api/activities/no_gpx</code></a></td><td>
<a href="api/workouts.html#get--api-workouts-(string-workout_short_id)"><code class="xref">GET /api/workouts/(string:workout_short_id)</code></a></td><td>
<em></em></td></tr>
<tr>
<td></td>
<td>
<a href="api/workouts.html#get--api-workouts-(string-workout_short_id)-chart_data"><code class="xref">GET /api/workouts/(string:workout_short_id)/chart_data</code></a></td><td>
<em></em></td></tr>
<tr>
<td></td>
<td>
<a href="api/workouts.html#get--api-workouts-(string-workout_short_id)-chart_data-segment-(int-segment_id)"><code class="xref">GET /api/workouts/(string:workout_short_id)/chart_data/segment/(int:segment_id)</code></a></td><td>
<em></em></td></tr>
<tr>
<td></td>
<td>
<a href="api/workouts.html#get--api-workouts-(string-workout_short_id)-gpx"><code class="xref">GET /api/workouts/(string:workout_short_id)/gpx</code></a></td><td>
<em></em></td></tr>
<tr>
<td></td>
<td>
<a href="api/workouts.html#get--api-workouts-(string-workout_short_id)-gpx-segment-(int-segment_id)"><code class="xref">GET /api/workouts/(string:workout_short_id)/gpx/segment/(int:segment_id)</code></a></td><td>
<em></em></td></tr>
<tr>
<td></td>
<td>
<a href="api/workouts.html#get--api-workouts-map-(map_id)"><code class="xref">GET /api/workouts/map/(map_id)</code></a></td><td>
<em></em></td></tr>
<tr>
<td></td>
<td>
<a href="api/workouts.html#get--api-workouts-map_tile-(s)-(z)-(x)-(y).png"><code class="xref">GET /api/workouts/map_tile/(s)/(z)/(x)/(y).png</code></a></td><td>
<em></em></td></tr>
<tr>
<td></td>
@ -273,7 +263,12 @@
<tr>
<td></td>
<td>
<a href="api/activities.html#delete--api-activities-(string-activity_short_id)"><code class="xref">DELETE /api/activities/(string:activity_short_id)</code></a></td><td>
<a href="api/workouts.html#post--api-workouts"><code class="xref">POST /api/workouts</code></a></td><td>
<em></em></td></tr>
<tr>
<td></td>
<td>
<a href="api/workouts.html#post--api-workouts-no_gpx"><code class="xref">POST /api/workouts/no_gpx</code></a></td><td>
<em></em></td></tr>
<tr>
<td></td>
@ -288,7 +283,7 @@
<tr>
<td></td>
<td>
<a href="api/activities.html#patch--api-activities-(string-activity_short_id)"><code class="xref">PATCH /api/activities/(string:activity_short_id)</code></a></td><td>
<a href="api/workouts.html#delete--api-workouts-(string-workout_short_id)"><code class="xref">DELETE /api/workouts/(string:workout_short_id)</code></a></td><td>
<em></em></td></tr>
<tr>
<td></td>
@ -305,6 +300,11 @@
<td>
<a href="api/users.html#patch--api-users-(user_name)"><code class="xref">PATCH /api/users/(user_name)</code></a></td><td>
<em></em></td></tr>
<tr>
<td></td>
<td>
<a href="api/workouts.html#patch--api-workouts-(string-workout_short_id)"><code class="xref">PATCH /api/workouts/(string:workout_short_id)</code></a></td><td>
<em></em></td></tr>
</table>

View File

@ -123,8 +123,8 @@
<div class="section" id="fittrackee">
<h1>FitTrackee<a class="headerlink" href="#fittrackee" title="Permalink to this headline"></a></h1>
<div class="line-block">
<div class="line">This web application allows you to track your outdoor activities from
gpx files and keep your data on your own server.</div>
<div class="line">This web application allows you to track your outdoor activities (workouts)
from gpx files and keep your data on your own server.</div>
<div class="line">No mobile app is developed yet, but several existing mobile apps can
store workouts data locally and export them into a gpx file.</div>
<div class="line">Examples (for Android):</div>
@ -164,20 +164,20 @@ Map</a>.</div>
<li class="toctree-l1"><a class="reference internal" href="features.html">Features</a><ul>
<li class="toctree-l2"><a class="reference internal" href="features.html#list">List</a></li>
<li class="toctree-l2"><a class="reference internal" href="features.html#dashboard">Dashboard</a></li>
<li class="toctree-l2"><a class="reference internal" href="features.html#activity-workout-detail">Activity/workout detail</a></li>
<li class="toctree-l2"><a class="reference internal" href="features.html#activities-workouts-list">Activities/workouts list</a></li>
<li class="toctree-l2"><a class="reference internal" href="features.html#workout-detail">Workout detail</a></li>
<li class="toctree-l2"><a class="reference internal" href="features.html#workouts-list">Workouts list</a></li>
<li class="toctree-l2"><a class="reference internal" href="features.html#statistics">Statistics</a></li>
<li class="toctree-l2"><a class="reference internal" href="features.html#id1">Administration</a></li>
</ul>
</li>
<li class="toctree-l1"><a class="reference internal" href="api/index.html">API documentation</a><ul>
<li class="toctree-l2"><a class="reference internal" href="api/activities.html">Activities</a></li>
<li class="toctree-l2"><a class="reference internal" href="api/auth.html">Authentication</a></li>
<li class="toctree-l2"><a class="reference internal" href="api/configuration.html">Configuration</a></li>
<li class="toctree-l2"><a class="reference internal" href="api/records.html">Records</a></li>
<li class="toctree-l2"><a class="reference internal" href="api/sports.html">Sports</a></li>
<li class="toctree-l2"><a class="reference internal" href="api/stats.html">Statistics</a></li>
<li class="toctree-l2"><a class="reference internal" href="api/users.html">Users</a></li>
<li class="toctree-l2"><a class="reference internal" href="api/workouts.html">Workouts</a></li>
</ul>
</li>
<li class="toctree-l1"><a class="reference internal" href="troubleshooting/index.html">Troubleshooting</a><ul>

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@ -16,7 +16,7 @@
<link rel="index" title="Index" href="../genindex.html" />
<link rel="search" title="Search" href="../search.html" />
<link rel="next" title="Administrator" href="administrator.html" />
<link rel="prev" title="Users" href="../api/users.html" />
<link rel="prev" title="Workouts" href="../api/workouts.html" />
<meta charset='utf-8'>
<meta http-equiv='X-UA-Compatible' content='IE=edge,chrome=1'>
<meta name='viewport' content='width=device-width, initial-scale=1.0, maximum-scale=1'>
@ -86,7 +86,7 @@
<li>
<a href="../api/users.html" title="Previous Chapter: Users"><span class="glyphicon glyphicon-chevron-left visible-sm"></span><span class="hidden-sm hidden-tablet">&laquo; Users</span>
<a href="../api/workouts.html" title="Previous Chapter: Workouts"><span class="glyphicon glyphicon-chevron-left visible-sm"></span><span class="hidden-sm hidden-tablet">&laquo; Workouts</span>
</a>
</li>
<li>

View File

@ -1,17 +0,0 @@
Activities
##########
.. autoflask:: fittrackee:create_app()
:endpoints:
activities.get_activities,
activities.get_activity,
activities.get_activity_gpx,
activities.get_activity_chart_data,
activities.get_segment_chart_data,
activities.get_segment_gpx,
activities.get_map,
activities.get_map_tile,
activities.post_activity,
activities.post_activity_no_gpx,
activities.update_activity,
activities.delete_activity

View File

@ -5,10 +5,10 @@ API documentation
:maxdepth: 2
:caption: Endpoints:
activities
auth
configuration
records
sports
stats
users
workouts

View File

@ -3,6 +3,6 @@ Statistics
.. autoflask:: fittrackee:create_app()
:endpoints:
stats.get_activities_by_time,
stats.get_activities_by_sport,
stats.get_workouts_by_time,
stats.get_workouts_by_sport,
stats.get_application_stats

View File

@ -0,0 +1,17 @@
Workouts
##########
.. autoflask:: fittrackee:create_app()
:endpoints:
workouts.get_workouts,
workouts.get_workout,
workouts.get_workout_gpx,
workouts.get_workout_chart_data,
workouts.get_segment_chart_data,
workouts.get_segment_gpx,
workouts.get_map,
workouts.get_map_tile,
workouts.post_workout,
workouts.post_workout_no_gpx,
workouts.update_workout,
workouts.delete_workout

View File

@ -25,7 +25,7 @@ Administration
- **Sports**
- enable or disable a sport (a sport can be disabled even if activity with this sport exists)
- enable or disable a sport (a sport can be disabled even if workout with this sport exists)
Account
^^^^^^^
@ -33,29 +33,29 @@ Account
- A user can reset his password (*new in 0.3.0*)
Activities/Workouts
^^^^^^^^^^^^^^^^^^^
- 6 sports supported:
Workouts
^^^^^^^^
- 6 sports are supported:
- Cycling (Sport)
- Cycling (Transport)
- Hiking
- Montain Biking
- Running
- Walking
- Dashboard with month calendar displaying activities and record. The week can start on Sunday or Monday (which can be changed in the user settings)
- Activity creation by uploading a gpx file. An activity can even be created without gpx (the user must enter date, time, duration and distance)
- An activity 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
- Activity edition and deletion. User can add a note
- Dashboard with month calendar displaying workouts and record. The week can start on Sunday or Monday (which can be changed in the user settings)
- 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
- Workout edition and deletion. User can add a note
- User statistics
- User records by sports:
- average speed
- farest distance
- longest duration
- maximum speed
- Activities list and filter
- Workours list and filter
.. note::
for now, only the owner of the activity can see the activity.
for now, only the owner of the workout can see it.
Translations
^^^^^^^^^^^^
@ -69,16 +69,16 @@ Dashboard
:alt: FitTrackee Dashboard
Activity/workout detail
Workout detail
~~~~~~~~~~~~~~~~~~~~~~~
.. figure:: _images/fittrackee_screenshot-02.png
:alt: FitTrackee Activity
:alt: FitTrackee Workout
Activities/workouts list
~~~~~~~~~~~~~~~~~~~~~~~~
Workouts list
~~~~~~~~~~~~~
.. figure:: _images/fittrackee_screenshot-03.png
:alt: FitTrackee Activities
:alt: FitTrackee Workouts
Statistics

View File

@ -5,8 +5,8 @@
FitTrackee
==========
| This web application allows you to track your outdoor activities from
gpx files and keep your data on your own server.
| This web application allows you to track your outdoor activities (workouts)
from gpx files and keep your data on your own server.
| No mobile app is developed yet, but several existing mobile apps can
store workouts data locally and export them into a gpx file.
| Examples (for Android):

View File

@ -62,21 +62,21 @@ def create_app() -> Flask:
_, db_app_config = init_config()
update_app_config_from_database(app, db_app_config)
from .activities.activities import activities_blueprint # noqa
from .activities.records import records_blueprint # noqa
from .activities.sports import sports_blueprint # noqa
from .activities.stats import stats_blueprint # noqa
from .application.app_config import config_blueprint # noqa
from .users.auth import auth_blueprint # noqa
from .users.users import users_blueprint # noqa
from .workouts.records import records_blueprint # noqa
from .workouts.sports import sports_blueprint # noqa
from .workouts.stats import stats_blueprint # noqa
from .workouts.workouts import workouts_blueprint # noqa
app.register_blueprint(users_blueprint, url_prefix='/api')
app.register_blueprint(auth_blueprint, url_prefix='/api')
app.register_blueprint(activities_blueprint, url_prefix='/api')
app.register_blueprint(config_blueprint, url_prefix='/api')
app.register_blueprint(records_blueprint, url_prefix='/api')
app.register_blueprint(sports_blueprint, url_prefix='/api')
app.register_blueprint(stats_blueprint, url_prefix='/api')
app.register_blueprint(config_blueprint, url_prefix='/api')
app.register_blueprint(users_blueprint, url_prefix='/api')
app.register_blueprint(workouts_blueprint, url_prefix='/api')
if app.debug:
logging.getLogger('sqlalchemy').setLevel(logging.WARNING)

View File

@ -6,10 +6,10 @@ from typing import Dict, Optional
import gunicorn.app.base
from fittrackee import create_app, db
from fittrackee.activities.models import Activity
from fittrackee.activities.utils import update_activity
from fittrackee.application.utils import init_config
from fittrackee.database_utils import init_database
from fittrackee.workouts.models import Workout
from fittrackee.workouts.utils import update_workout
from flask import Flask
from flask_dramatiq import worker
from flask_migrate import upgrade
@ -68,19 +68,19 @@ def init_data() -> None:
@app.cli.command()
def recalculate() -> None:
print("Starting activities data refresh")
activities = (
Activity.query.filter(Activity.gpx != None) # noqa
.order_by(Activity.activity_date.asc()) # noqa
print("Starting workouts data refresh")
workouts = (
Workout.query.filter(Workout.gpx != None) # noqa
.order_by(Workout.workout_date.asc()) # noqa
.all()
)
if len(activities) == 0:
print('➡️ no activities to upgrade.')
if len(workouts) == 0:
print('➡️ no workouts to upgrade.')
return None
pbar = tqdm(activities)
for activity in pbar:
update_activity(activity)
pbar.set_postfix(activitiy_id=activity.id)
pbar = tqdm(workouts)
for workout in pbar:
update_workout(workout)
pbar.set_postfix(activitiy_id=workout.id)
db.session.commit()

View File

@ -1,419 +0,0 @@
import hashlib
import os
import tempfile
import zipfile
from datetime import datetime, timedelta
from typing import Dict, List, Optional, Tuple, Union
from uuid import UUID
import gpxpy.gpx
import pytz
from fittrackee import appLog, db
from flask import current_app
from sqlalchemy import exc
from staticmap import Line, StaticMap
from werkzeug.datastructures import FileStorage
from werkzeug.utils import secure_filename
from ..users.models import User
from .models import Activity, ActivitySegment, Sport
from .utils_files import get_absolute_file_path
from .utils_gpx import get_gpx_info
class ActivityException(Exception):
def __init__(
self, status: str, message: str, e: Optional[Exception] = None
) -> None:
self.status = status
self.message = message
self.e = e
def get_datetime_with_tz(
timezone: str, activity_date: datetime, gpx_data: Optional[Dict] = None
) -> Tuple[Optional[datetime], datetime]:
"""
Return naive datetime and datetime with user timezone
"""
activity_date_tz = None
if timezone:
user_tz = pytz.timezone(timezone)
utc_tz = pytz.utc
if gpx_data:
# activity date in gpx is in UTC, but in naive datetime
fmt = '%Y-%m-%d %H:%M:%S'
activity_date_string = activity_date.strftime(fmt)
activity_date_tmp = utc_tz.localize(
datetime.strptime(activity_date_string, fmt)
)
activity_date_tz = activity_date_tmp.astimezone(user_tz)
else:
activity_date_tz = user_tz.localize(activity_date)
activity_date = activity_date_tz.astimezone(utc_tz)
# make datetime 'naive' like in gpx file
activity_date = activity_date.replace(tzinfo=None)
return activity_date_tz, activity_date
def update_activity_data(
activity: Union[Activity, ActivitySegment], gpx_data: Dict
) -> Union[Activity, ActivitySegment]:
"""
Update activity or activity segment with data from gpx file
"""
activity.pauses = gpx_data['stop_time']
activity.moving = gpx_data['moving_time']
activity.min_alt = gpx_data['elevation_min']
activity.max_alt = gpx_data['elevation_max']
activity.descent = gpx_data['downhill']
activity.ascent = gpx_data['uphill']
activity.max_speed = gpx_data['max_speed']
activity.ave_speed = gpx_data['average_speed']
return activity
def create_activity(
user: User, activity_data: Dict, gpx_data: Optional[Dict] = None
) -> Activity:
"""
Create Activity from data entered by user and from gpx if a gpx file is
provided
"""
activity_date = (
gpx_data['start']
if gpx_data
else datetime.strptime(
activity_data['activity_date'], '%Y-%m-%d %H:%M'
)
)
activity_date_tz, activity_date = get_datetime_with_tz(
user.timezone, activity_date, gpx_data
)
duration = (
gpx_data['duration']
if gpx_data
else timedelta(seconds=activity_data['duration'])
)
distance = gpx_data['distance'] if gpx_data else activity_data['distance']
title = gpx_data['name'] if gpx_data else activity_data.get('title', '')
new_activity = Activity(
user_id=user.id,
sport_id=activity_data['sport_id'],
activity_date=activity_date,
distance=distance,
duration=duration,
)
new_activity.notes = activity_data.get('notes')
if title is not None and title != '':
new_activity.title = title
else:
sport = Sport.query.filter_by(id=new_activity.sport_id).first()
fmt = "%Y-%m-%d %H:%M:%S"
activity_datetime = (
activity_date_tz.strftime(fmt)
if activity_date_tz
else new_activity.activity_date.strftime(fmt)
)
new_activity.title = f'{sport.label} - {activity_datetime}'
if gpx_data:
new_activity.gpx = gpx_data['filename']
new_activity.bounds = gpx_data['bounds']
update_activity_data(new_activity, gpx_data)
else:
new_activity.moving = duration
new_activity.ave_speed = (
None
if duration.seconds == 0
else float(new_activity.distance) / (duration.seconds / 3600)
)
new_activity.max_speed = new_activity.ave_speed
return new_activity
def create_segment(
activity_id: int, activity_uuid: UUID, segment_data: Dict
) -> ActivitySegment:
"""
Create Activity Segment from gpx data
"""
new_segment = ActivitySegment(
activity_id=activity_id,
activity_uuid=activity_uuid,
segment_id=segment_data['idx'],
)
new_segment.duration = segment_data['duration']
new_segment.distance = segment_data['distance']
update_activity_data(new_segment, segment_data)
return new_segment
def update_activity(activity: Activity) -> Activity:
"""
Update activity data from gpx file
"""
gpx_data, _, _ = get_gpx_info(
get_absolute_file_path(activity.gpx), False, False
)
updated_activity = update_activity_data(activity, gpx_data)
updated_activity.duration = gpx_data['duration']
updated_activity.distance = gpx_data['distance']
db.session.flush()
for segment_idx, segment in enumerate(updated_activity.segments):
segment_data = gpx_data['segments'][segment_idx]
updated_segment = update_activity_data(segment, segment_data)
updated_segment.duration = segment_data['duration']
updated_segment.distance = segment_data['distance']
db.session.flush()
return updated_activity
def edit_activity(
activity: Activity, activity_data: Dict, auth_user_id: int
) -> Activity:
"""
Edit an activity
Note: the gpx file is NOT modified
In a next version, map_data and weather_data will be updated
(case of a modified gpx file, see issue #7)
"""
user = User.query.filter_by(id=auth_user_id).first()
if activity_data.get('refresh'):
activity = update_activity(activity)
if activity_data.get('sport_id'):
activity.sport_id = activity_data.get('sport_id')
if activity_data.get('title'):
activity.title = activity_data.get('title')
if activity_data.get('notes'):
activity.notes = activity_data.get('notes')
if not activity.gpx:
if activity_data.get('activity_date'):
activity_date = datetime.strptime(
activity_data['activity_date'], '%Y-%m-%d %H:%M'
)
_, activity.activity_date = get_datetime_with_tz(
user.timezone, activity_date
)
if activity_data.get('duration'):
activity.duration = timedelta(seconds=activity_data['duration'])
activity.moving = activity.duration
if activity_data.get('distance'):
activity.distance = activity_data['distance']
activity.ave_speed = (
None
if activity.duration.seconds == 0
else float(activity.distance) / (activity.duration.seconds / 3600)
)
activity.max_speed = activity.ave_speed
return activity
def get_file_path(dir_path: str, filename: str) -> str:
"""
Get full path for a file
"""
if not os.path.exists(dir_path):
os.makedirs(dir_path)
file_path = os.path.join(dir_path, filename)
return file_path
def get_new_file_path(
auth_user_id: int,
activity_date: str,
sport: str,
old_filename: Optional[str] = None,
extension: Optional[str] = None,
) -> str:
"""
Generate a file path from user and activity data
"""
if not extension and old_filename:
extension = f".{old_filename.rsplit('.', 1)[1].lower()}"
_, new_filename = tempfile.mkstemp(
prefix=f'{activity_date}_{sport}_', suffix=extension
)
dir_path = os.path.join('activities', str(auth_user_id))
if not os.path.exists(dir_path):
os.makedirs(dir_path)
file_path = os.path.join(dir_path, new_filename.split('/')[-1])
return file_path
def generate_map(map_filepath: str, map_data: List) -> None:
"""
Generate and save map image from map data
"""
m = StaticMap(400, 225, 10)
line = Line(map_data, '#3388FF', 4)
m.add_line(line)
image = m.render()
image.save(map_filepath)
def get_map_hash(map_filepath: str) -> str:
"""
Generate a md5 hash used as id instead of activity id, to retrieve map
image (maps are sensitive data)
"""
md5 = hashlib.md5()
absolute_map_filepath = get_absolute_file_path(map_filepath)
with open(absolute_map_filepath, 'rb') as f:
for chunk in iter(lambda: f.read(128 * md5.block_size), b''):
md5.update(chunk)
return md5.hexdigest()
def process_one_gpx_file(params: Dict, filename: str) -> Activity:
"""
Get all data from a gpx file to create an activity with map image
"""
try:
gpx_data, map_data, weather_data = get_gpx_info(params['file_path'])
auth_user_id = params['user'].id
new_filepath = get_new_file_path(
auth_user_id=auth_user_id,
activity_date=gpx_data['start'],
old_filename=filename,
sport=params['sport_label'],
)
absolute_gpx_filepath = get_absolute_file_path(new_filepath)
os.rename(params['file_path'], absolute_gpx_filepath)
gpx_data['filename'] = new_filepath
map_filepath = get_new_file_path(
auth_user_id=auth_user_id,
activity_date=gpx_data['start'],
extension='.png',
sport=params['sport_label'],
)
absolute_map_filepath = get_absolute_file_path(map_filepath)
generate_map(absolute_map_filepath, map_data)
except (gpxpy.gpx.GPXXMLSyntaxException, TypeError) as e:
raise ActivityException('error', 'Error during gpx file parsing.', e)
except Exception as e:
raise ActivityException('error', 'Error during gpx processing.', e)
try:
new_activity = create_activity(
params['user'], params['activity_data'], gpx_data
)
new_activity.map = map_filepath
new_activity.map_id = get_map_hash(map_filepath)
new_activity.weather_start = weather_data[0]
new_activity.weather_end = weather_data[1]
db.session.add(new_activity)
db.session.flush()
for segment_data in gpx_data['segments']:
new_segment = create_segment(
new_activity.id, new_activity.uuid, segment_data
)
db.session.add(new_segment)
db.session.commit()
return new_activity
except (exc.IntegrityError, ValueError) as e:
raise ActivityException('fail', 'Error during activity save.', e)
def process_zip_archive(common_params: Dict, extract_dir: str) -> List:
"""
Get files from a zip archive and create activities, if number of files
does not exceed defined limit.
"""
with zipfile.ZipFile(common_params['file_path'], "r") as zip_ref:
zip_ref.extractall(extract_dir)
new_activities = []
gpx_files_limit = os.getenv('REACT_APP_GPX_LIMIT_IMPORT', 10)
if (
gpx_files_limit
and isinstance(gpx_files_limit, str)
and gpx_files_limit.isdigit()
):
gpx_files_limit = int(gpx_files_limit)
else:
gpx_files_limit = 10
appLog.warning('GPX limit not configured, set to 10.')
gpx_files_ok = 0
for gpx_file in os.listdir(extract_dir):
if (
'.' in gpx_file
and gpx_file.rsplit('.', 1)[1].lower()
in current_app.config['ACTIVITY_ALLOWED_EXTENSIONS']
):
gpx_files_ok += 1
if gpx_files_ok > gpx_files_limit:
break
file_path = os.path.join(extract_dir, gpx_file)
params = common_params
params['file_path'] = file_path
new_activity = process_one_gpx_file(params, gpx_file)
new_activities.append(new_activity)
return new_activities
def process_files(
auth_user_id: int,
activity_data: Dict,
activity_file: FileStorage,
folders: Dict,
) -> List:
"""
Store gpx file or zip archive and create activities
"""
if activity_file.filename is None:
raise ActivityException('error', 'File has no filename.')
filename = secure_filename(activity_file.filename)
extension = f".{filename.rsplit('.', 1)[1].lower()}"
file_path = get_file_path(folders['tmp_dir'], filename)
sport = Sport.query.filter_by(id=activity_data.get('sport_id')).first()
if not sport:
raise ActivityException(
'error',
f"Sport id: {activity_data.get('sport_id')} does not exist",
)
user = User.query.filter_by(id=auth_user_id).first()
common_params = {
'user': user,
'activity_data': activity_data,
'file_path': file_path,
'sport_label': sport.label,
}
try:
activity_file.save(file_path)
except Exception as e:
raise ActivityException('error', 'Error during activity file save.', e)
if extension == ".gpx":
return [process_one_gpx_file(common_params, filename)]
else:
return process_zip_archive(common_params, folders['extract_dir'])
def get_upload_dir_size() -> int:
"""
Return upload directory size
"""
upload_path = get_absolute_file_path('')
total_size = 0
for dir_path, _, filenames in os.walk(upload_path):
for f in filenames:
fp = os.path.join(dir_path, f)
total_size += os.path.getsize(fp)
return total_size

View File

@ -25,7 +25,7 @@ class BaseConfig:
os.getenv('UPLOAD_FOLDER', current_app.root_path), 'uploads'
)
PICTURE_ALLOWED_EXTENSIONS = {'jpg', 'png', 'gif'}
ACTIVITY_ALLOWED_EXTENSIONS = {'gpx', 'zip'}
WORKOUT_ALLOWED_EXTENSIONS = {'gpx', 'zip'}
TEMPLATES_FOLDER = os.path.join(current_app.root_path, 'email/templates')
UI_URL = os.environ.get('UI_URL')
EMAIL_URL = os.environ.get('EMAIL_URL')

View File

@ -1,10 +1,10 @@
from fittrackee import db
from fittrackee.activities.models import Sport
from fittrackee.application.utils import (
init_config,
update_app_config_from_database,
)
from fittrackee.users.models import User
from fittrackee.workouts.models import Sport
from flask import Flask

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,244 @@
"""rename 'activity' with 'workout'
Revision ID: 4e8597c50064
Revises: 3243cd25eca7
Create Date: 2021-01-09 19:41:26.589237
"""
import os
import sqlalchemy as sa
from alembic import op
from flask import current_app
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = '4e8597c50064'
down_revision = '3243cd25eca7'
branch_labels = None
depends_on = None
def rename_upload_folder(src, dst):
upload_folder = current_app.config['UPLOAD_FOLDER']
src_directory = f'{upload_folder}/{src}'
if os.path.exists(src_directory):
try:
os.rename(src_directory, f'{upload_folder}/{dst}')
except Exception as e:
print(
f'ERROR: can not rename upload folder \'{src}\' to \'{dst}\':'
f'\n {e}.'
f'\n Please rename it manually.')
def upgrade():
op.create_table('workouts',
sa.Column('id', sa.Integer(), server_default=sa.text("nextval('workouts_id_seq'::regclass)"), autoincrement=True, nullable=False),
sa.Column('uuid', postgresql.UUID(as_uuid=True), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=False),
sa.Column('sport_id', sa.Integer(), nullable=False),
sa.Column('title', sa.String(length=255), nullable=True),
sa.Column('gpx', sa.String(length=255), nullable=True),
sa.Column('creation_date', sa.DateTime(), nullable=True),
sa.Column('modification_date', sa.DateTime(), nullable=True),
sa.Column('workout_date', sa.DateTime(), nullable=False),
sa.Column('duration', sa.Interval(), nullable=False),
sa.Column('pauses', sa.Interval(), nullable=True),
sa.Column('moving', sa.Interval(), nullable=True),
sa.Column('distance', sa.Numeric(precision=6, scale=3), nullable=True),
sa.Column('min_alt', sa.Numeric(precision=6, scale=2), nullable=True),
sa.Column('max_alt', sa.Numeric(precision=6, scale=2), nullable=True),
sa.Column('descent', sa.Numeric(precision=7, scale=2), nullable=True),
sa.Column('ascent', sa.Numeric(precision=7, scale=2), nullable=True),
sa.Column('max_speed', sa.Numeric(precision=6, scale=2), nullable=True),
sa.Column('ave_speed', sa.Numeric(precision=6, scale=2), nullable=True),
sa.Column('bounds', postgresql.ARRAY(sa.Float()), nullable=True),
sa.Column('map', sa.String(length=255), nullable=True),
sa.Column('map_id', sa.String(length=50), nullable=True),
sa.Column('weather_start', sa.JSON(), nullable=True),
sa.Column('weather_end', sa.JSON(), nullable=True),
sa.Column('notes', sa.String(length=500), nullable=True),
sa.ForeignKeyConstraint(['sport_id'], ['sports.id'], ),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('uuid')
)
op.execute(
"SELECT setval('workouts_id_seq', (SELECT max(id) FROM activities));"
)
op.create_table('workout_segments',
sa.Column('workout_id', sa.Integer(), nullable=False),
sa.Column('workout_uuid', postgresql.UUID(as_uuid=True), nullable=False),
sa.Column('segment_id', sa.Integer(), nullable=False),
sa.Column('duration', sa.Interval(), nullable=False),
sa.Column('pauses', sa.Interval(), nullable=True),
sa.Column('moving', sa.Interval(), nullable=True),
sa.Column('distance', sa.Numeric(precision=6, scale=3), nullable=True),
sa.Column('min_alt', sa.Numeric(precision=6, scale=2), nullable=True),
sa.Column('max_alt', sa.Numeric(precision=6, scale=2), nullable=True),
sa.Column('descent', sa.Numeric(precision=7, scale=2), nullable=True),
sa.Column('ascent', sa.Numeric(precision=7, scale=2), nullable=True),
sa.Column('max_speed', sa.Numeric(precision=6, scale=2), nullable=True),
sa.Column('ave_speed', sa.Numeric(precision=6, scale=2), nullable=True),
sa.ForeignKeyConstraint(['workout_id'], ['workouts.id'], ),
sa.PrimaryKeyConstraint('workout_id', 'segment_id')
)
op.add_column('records', sa.Column('workout_date', sa.DateTime(), nullable=True))
op.add_column('records', sa.Column('workout_id', sa.Integer(), nullable=True))
op.add_column('records', sa.Column('workout_uuid', postgresql.UUID(as_uuid=True), nullable=True))
op.create_foreign_key('records_workout_id_fkey', 'records', 'workouts', ['workout_id'], ['id'])
op.execute(
"""
INSERT INTO workouts (id, user_id, sport_id, title, gpx,
creation_date, modification_date, workout_date, duration, pauses,
moving, distance, min_alt, max_alt, descent, ascent, max_speed,
ave_speed, bounds, map, map_id, weather_start, weather_end, notes, uuid)
SELECT id, user_id, sport_id, title, REPLACE(gpx, 'activities/', 'workouts/'),
creation_date, modification_date, activity_date, duration, pauses,
moving, distance, min_alt, max_alt, descent, ascent, max_speed,
ave_speed, bounds, REPLACE(map, 'activities/', 'workouts/'), map_id,
weather_start, weather_end, notes, uuid
FROM activities;
"""
)
op.execute(
"""
INSERT INTO workout_segments (workout_id, workout_uuid, segment_id,
duration, pauses, moving, distance, min_alt, max_alt, descent,
ascent, max_speed, ave_speed)
SELECT activity_id, activity_uuid, segment_id,
duration, pauses, moving, distance, min_alt, max_alt, descent,
ascent, max_speed, ave_speed FROM activity_segments;
"""
)
op.execute(
'UPDATE records SET workout_id = activity_id, '
' workout_uuid = activity_uuid, '
' workout_date = activity_date;'
)
op.alter_column('records', 'workout_date', nullable=False)
op.alter_column('records', 'workout_id', nullable=False)
op.alter_column('records', 'workout_uuid', nullable=False)
op.drop_column('records', 'activity_date')
op.drop_column('records', 'activity_id')
op.drop_column('records', 'activity_uuid')
op.drop_table('activity_segments')
op.drop_table('activities')
rename_upload_folder('activities', 'workouts')
def downgrade():
op.create_table('activities',
sa.Column('id', sa.INTEGER(), server_default=sa.text("nextval('activities_id_seq'::regclass)"), autoincrement=True, nullable=False),
sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=False),
sa.Column('sport_id', sa.INTEGER(), autoincrement=False, nullable=False),
sa.Column('title', sa.VARCHAR(length=255), autoincrement=False, nullable=True),
sa.Column('gpx', sa.VARCHAR(length=255), autoincrement=False, nullable=True),
sa.Column('creation_date', postgresql.TIMESTAMP(), autoincrement=False, nullable=True),
sa.Column('modification_date', postgresql.TIMESTAMP(), autoincrement=False, nullable=True),
sa.Column('activity_date', postgresql.TIMESTAMP(), autoincrement=False, nullable=False),
sa.Column('duration', postgresql.INTERVAL(), autoincrement=False, nullable=False),
sa.Column('pauses', postgresql.INTERVAL(), autoincrement=False, nullable=True),
sa.Column('moving', postgresql.INTERVAL(), autoincrement=False, nullable=True),
sa.Column('distance', sa.NUMERIC(precision=6, scale=3), autoincrement=False, nullable=True),
sa.Column('min_alt', sa.NUMERIC(precision=6, scale=2), autoincrement=False, nullable=True),
sa.Column('max_alt', sa.NUMERIC(precision=6, scale=2), autoincrement=False, nullable=True),
sa.Column('descent', sa.NUMERIC(precision=7, scale=2), autoincrement=False, nullable=True),
sa.Column('ascent', sa.NUMERIC(precision=7, scale=2), autoincrement=False, nullable=True),
sa.Column('max_speed', sa.NUMERIC(precision=6, scale=2), autoincrement=False, nullable=True),
sa.Column('ave_speed', sa.NUMERIC(precision=6, scale=2), autoincrement=False, nullable=True),
sa.Column('bounds', postgresql.ARRAY(postgresql.DOUBLE_PRECISION(precision=53)), autoincrement=False, nullable=True),
sa.Column('map', sa.VARCHAR(length=255), autoincrement=False, nullable=True),
sa.Column('map_id', sa.VARCHAR(length=50), autoincrement=False, nullable=True),
sa.Column('weather_end', postgresql.JSON(astext_type=sa.Text()), autoincrement=False, nullable=True),
sa.Column('weather_start', postgresql.JSON(astext_type=sa.Text()), autoincrement=False, nullable=True),
sa.Column('notes', sa.VARCHAR(length=500), autoincrement=False, nullable=True),
sa.Column('uuid', postgresql.UUID(), autoincrement=False, nullable=False),
sa.ForeignKeyConstraint(['sport_id'], ['sports.id'], name='activities_sport_id_fkey'),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], name='activities_user_id_fkey'),
sa.PrimaryKeyConstraint('id', name='activities_pkey'),
sa.UniqueConstraint('uuid', name='activities_uuid_key'),
postgresql_ignore_search_path=False
)
op.execute(
"SELECT setval('activities_id_seq', (SELECT max(id) FROM workouts));"
)
op.create_table('activity_segments',
sa.Column('activity_id', sa.INTEGER(), autoincrement=False, nullable=False),
sa.Column('segment_id', sa.INTEGER(), autoincrement=False, nullable=False),
sa.Column('duration', postgresql.INTERVAL(), autoincrement=False, nullable=False),
sa.Column('pauses', postgresql.INTERVAL(), autoincrement=False, nullable=True),
sa.Column('moving', postgresql.INTERVAL(), autoincrement=False, nullable=True),
sa.Column('distance', sa.NUMERIC(precision=6, scale=3), autoincrement=False, nullable=True),
sa.Column('min_alt', sa.NUMERIC(precision=6, scale=2), autoincrement=False, nullable=True),
sa.Column('max_alt', sa.NUMERIC(precision=6, scale=2), autoincrement=False, nullable=True),
sa.Column('descent', sa.NUMERIC(precision=7, scale=2), autoincrement=False, nullable=True),
sa.Column('ascent', sa.NUMERIC(precision=7, scale=2), autoincrement=False, nullable=True),
sa.Column('max_speed', sa.NUMERIC(precision=6, scale=2), autoincrement=False, nullable=True),
sa.Column('ave_speed', sa.NUMERIC(precision=6, scale=2), autoincrement=False, nullable=True),
sa.Column('activity_uuid', postgresql.UUID(), autoincrement=False, nullable=False),
sa.ForeignKeyConstraint(['activity_id'], ['activities.id'], name='activity_segments_activity_id_fkey'),
sa.PrimaryKeyConstraint('activity_id', 'segment_id', name='activity_segments_pkey')
)
op.execute(
"""
INSERT INTO activities (id, user_id, sport_id, title, gpx,
creation_date, modification_date, activity_date, duration, pauses,
moving, distance, min_alt, max_alt, descent, ascent, max_speed,
ave_speed, bounds, map, map_id, weather_start, weather_end, notes, uuid)
SELECT id, user_id, sport_id, title, REPLACE(gpx, 'workouts/', 'activities/'),
creation_date, modification_date, workout_date, duration, pauses,
moving, distance, min_alt, max_alt, descent, ascent, max_speed,
ave_speed, bounds, REPLACE(map, 'workouts/', 'activities/'), map_id,
weather_start, weather_end, notes, uuid
FROM workouts;
"""
)
op.execute(
"""
INSERT INTO activity_segments (activity_id, activity_uuid, segment_id,
duration, pauses, moving, distance, min_alt, max_alt, descent,
ascent, max_speed, ave_speed)
SELECT workout_id, workout_uuid, segment_id,
duration, pauses, moving, distance, min_alt, max_alt, descent,
ascent, max_speed, ave_speed FROM workout_segments;
"""
)
op.add_column('records', sa.Column('activity_uuid', postgresql.UUID(), autoincrement=False, nullable=True))
op.add_column('records', sa.Column('activity_id', sa.INTEGER(), autoincrement=False, nullable=True))
op.add_column('records', sa.Column('activity_date', postgresql.TIMESTAMP(), autoincrement=False, nullable=True))
op.create_foreign_key('records_activity_id_fkey', 'records', 'activities', ['activity_id'], ['id'])
op.execute(
'UPDATE records SET activity_id = workout_id, '
' activity_uuid = workout_uuid, '
' activity_date = workout_date;'
)
op.alter_column('records', 'activity_date', nullable=False)
op.alter_column('records', 'activity_id', nullable=False)
op.alter_column('records', 'activity_uuid', nullable=False)
op.drop_column('records', 'workout_uuid')
op.drop_column('records', 'workout_id')
op.drop_column('records', 'workout_date')
op.drop_table('workout_segments')
op.drop_table('workouts')
rename_upload_folder('workouts', 'activities')

View File

@ -1,75 +0,0 @@
from uuid import UUID
from fittrackee.activities.models import Activity, Sport
from fittrackee.activities.utils_id import decode_short_id
from fittrackee.users.models import User
from flask import Flask
class TestActivityModel:
def test_activity_model(
self,
app: Flask,
sport_1_cycling: Sport,
user_1: User,
activity_cycling_user_1: Activity,
) -> None:
activity_cycling_user_1.title = 'Test'
assert 1 == activity_cycling_user_1.id
assert activity_cycling_user_1.uuid is not None
assert 1 == activity_cycling_user_1.user_id
assert 1 == activity_cycling_user_1.sport_id
assert '2018-01-01 00:00:00' == str(
activity_cycling_user_1.activity_date
)
assert 10.0 == float(activity_cycling_user_1.distance)
assert '1:00:00' == str(activity_cycling_user_1.duration)
assert 'Test' == activity_cycling_user_1.title
assert '<Activity \'Cycling\' - 2018-01-01 00:00:00>' == str(
activity_cycling_user_1
)
serialized_activity = activity_cycling_user_1.serialize()
assert isinstance(decode_short_id(serialized_activity['id']), UUID)
assert 'test' == serialized_activity['user']
assert 1 == serialized_activity['sport_id']
assert serialized_activity['title'] == 'Test'
assert 'creation_date' in serialized_activity
assert serialized_activity['modification_date'] is not None
assert (
str(serialized_activity['activity_date']) == '2018-01-01 00:00:00'
)
assert serialized_activity['duration'] == '1:00:00'
assert serialized_activity['pauses'] is None
assert serialized_activity['moving'] == '1:00:00'
assert serialized_activity['distance'] == 10.0
assert serialized_activity['max_alt'] is None
assert serialized_activity['descent'] is None
assert serialized_activity['ascent'] is None
assert serialized_activity['max_speed'] == 10.0
assert serialized_activity['ave_speed'] == 10.0
assert serialized_activity['with_gpx'] is False
assert serialized_activity['bounds'] == []
assert serialized_activity['previous_activity'] is None
assert serialized_activity['next_activity'] is None
assert serialized_activity['segments'] == []
assert serialized_activity['records'] != []
assert serialized_activity['map'] is None
assert serialized_activity['weather_start'] is None
assert serialized_activity['weather_end'] is None
assert serialized_activity['notes'] is None
def test_activity_segment_model(
self,
app: Flask,
sport_1_cycling: Sport,
user_1: User,
activity_cycling_user_1: Activity,
activity_cycling_user_1_segment: Activity,
) -> None:
assert (
f'<Segment \'{activity_cycling_user_1_segment.segment_id}\' '
f'for activity \'{activity_cycling_user_1.short_id}\'>'
== str(activity_cycling_user_1_segment)
)

View File

@ -4,10 +4,10 @@ from typing import Generator, Optional
import pytest
from fittrackee import create_app, db
from fittrackee.activities.models import Activity, ActivitySegment, Sport
from fittrackee.application.models import AppConfig
from fittrackee.application.utils import update_app_config_from_database
from fittrackee.users.models import User
from fittrackee.workouts.models import Sport, Workout, WorkoutSegment
os.environ['FLASK_ENV'] = 'testing'
os.environ['APP_SETTINGS'] = 'fittrackee.config.TestingConfig'
@ -180,149 +180,149 @@ def sport_2_running() -> Sport:
@pytest.fixture()
def activity_cycling_user_1() -> Activity:
activity = Activity(
def workout_cycling_user_1() -> Workout:
workout = Workout(
user_id=1,
sport_id=1,
activity_date=datetime.datetime.strptime('01/01/2018', '%d/%m/%Y'),
workout_date=datetime.datetime.strptime('01/01/2018', '%d/%m/%Y'),
distance=10,
duration=datetime.timedelta(seconds=3600),
)
activity.max_speed = 10
activity.ave_speed = 10
activity.moving = activity.duration
db.session.add(activity)
workout.max_speed = 10
workout.ave_speed = 10
workout.moving = workout.duration
db.session.add(workout)
db.session.commit()
return activity
return workout
@pytest.fixture()
def activity_cycling_user_1_segment(
activity_cycling_user_1: Activity,
) -> ActivitySegment:
activity_segment = ActivitySegment(
activity_id=activity_cycling_user_1.id,
activity_uuid=activity_cycling_user_1.uuid,
def workout_cycling_user_1_segment(
workout_cycling_user_1: Workout,
) -> WorkoutSegment:
workout_segment = WorkoutSegment(
workout_id=workout_cycling_user_1.id,
workout_uuid=workout_cycling_user_1.uuid,
segment_id=0,
)
activity_segment.duration = datetime.timedelta(seconds=6000)
activity_segment.moving = activity_segment.duration
activity_segment.distance = 5
db.session.add(activity_segment)
workout_segment.duration = datetime.timedelta(seconds=6000)
workout_segment.moving = workout_segment.duration
workout_segment.distance = 5
db.session.add(workout_segment)
db.session.commit()
return activity_segment
return workout_segment
@pytest.fixture()
def activity_running_user_1() -> Activity:
activity = Activity(
def workout_running_user_1() -> Workout:
workout = Workout(
user_id=1,
sport_id=2,
activity_date=datetime.datetime.strptime('01/04/2018', '%d/%m/%Y'),
workout_date=datetime.datetime.strptime('01/04/2018', '%d/%m/%Y'),
distance=12,
duration=datetime.timedelta(seconds=6000),
)
activity.moving = activity.duration
db.session.add(activity)
workout.moving = workout.duration
db.session.add(workout)
db.session.commit()
return activity
return workout
@pytest.fixture()
def seven_activities_user_1() -> Activity:
activity = Activity(
def seven_workouts_user_1() -> Workout:
workout = Workout(
user_id=1,
sport_id=1,
activity_date=datetime.datetime.strptime('20/03/2017', '%d/%m/%Y'),
workout_date=datetime.datetime.strptime('20/03/2017', '%d/%m/%Y'),
distance=5,
duration=datetime.timedelta(seconds=1024),
)
activity.ave_speed = float(activity.distance) / (1024 / 3600)
activity.moving = activity.duration
db.session.add(activity)
workout.ave_speed = float(workout.distance) / (1024 / 3600)
workout.moving = workout.duration
db.session.add(workout)
db.session.flush()
activity = Activity(
workout = Workout(
user_id=1,
sport_id=1,
activity_date=datetime.datetime.strptime('01/06/2017', '%d/%m/%Y'),
workout_date=datetime.datetime.strptime('01/06/2017', '%d/%m/%Y'),
distance=10,
duration=datetime.timedelta(seconds=3456),
)
activity.ave_speed = float(activity.distance) / (3456 / 3600)
activity.moving = activity.duration
db.session.add(activity)
workout.ave_speed = float(workout.distance) / (3456 / 3600)
workout.moving = workout.duration
db.session.add(workout)
db.session.flush()
activity = Activity(
workout = Workout(
user_id=1,
sport_id=1,
activity_date=datetime.datetime.strptime('01/01/2018', '%d/%m/%Y'),
workout_date=datetime.datetime.strptime('01/01/2018', '%d/%m/%Y'),
distance=10,
duration=datetime.timedelta(seconds=1024),
)
activity.ave_speed = float(activity.distance) / (1024 / 3600)
activity.moving = activity.duration
db.session.add(activity)
workout.ave_speed = float(workout.distance) / (1024 / 3600)
workout.moving = workout.duration
db.session.add(workout)
db.session.flush()
activity = Activity(
workout = Workout(
user_id=1,
sport_id=1,
activity_date=datetime.datetime.strptime('23/02/2018', '%d/%m/%Y'),
workout_date=datetime.datetime.strptime('23/02/2018', '%d/%m/%Y'),
distance=1,
duration=datetime.timedelta(seconds=600),
)
activity.ave_speed = float(activity.distance) / (600 / 3600)
activity.moving = activity.duration
db.session.add(activity)
workout.ave_speed = float(workout.distance) / (600 / 3600)
workout.moving = workout.duration
db.session.add(workout)
db.session.flush()
activity = Activity(
workout = Workout(
user_id=1,
sport_id=1,
activity_date=datetime.datetime.strptime('23/02/2018', '%d/%m/%Y'),
workout_date=datetime.datetime.strptime('23/02/2018', '%d/%m/%Y'),
distance=10,
duration=datetime.timedelta(seconds=1000),
)
activity.ave_speed = float(activity.distance) / (1000 / 3600)
activity.moving = activity.duration
db.session.add(activity)
workout.ave_speed = float(workout.distance) / (1000 / 3600)
workout.moving = workout.duration
db.session.add(workout)
db.session.flush()
activity = Activity(
workout = Workout(
user_id=1,
sport_id=1,
activity_date=datetime.datetime.strptime('01/04/2018', '%d/%m/%Y'),
workout_date=datetime.datetime.strptime('01/04/2018', '%d/%m/%Y'),
distance=8,
duration=datetime.timedelta(seconds=6000),
)
activity.ave_speed = float(activity.distance) / (6000 / 3600)
activity.moving = activity.duration
db.session.add(activity)
workout.ave_speed = float(workout.distance) / (6000 / 3600)
workout.moving = workout.duration
db.session.add(workout)
db.session.flush()
activity = Activity(
workout = Workout(
user_id=1,
sport_id=1,
activity_date=datetime.datetime.strptime('09/05/2018', '%d/%m/%Y'),
workout_date=datetime.datetime.strptime('09/05/2018', '%d/%m/%Y'),
distance=10,
duration=datetime.timedelta(seconds=3000),
)
activity.ave_speed = float(activity.distance) / (3000 / 3600)
activity.moving = activity.duration
db.session.add(activity)
workout.ave_speed = float(workout.distance) / (3000 / 3600)
workout.moving = workout.duration
db.session.add(workout)
db.session.commit()
return activity
return workout
@pytest.fixture()
def activity_cycling_user_2() -> Activity:
activity = Activity(
def workout_cycling_user_2() -> Workout:
workout = Workout(
user_id=2,
sport_id=1,
activity_date=datetime.datetime.strptime('23/01/2018', '%d/%m/%Y'),
workout_date=datetime.datetime.strptime('23/01/2018', '%d/%m/%Y'),
distance=15,
duration=datetime.timedelta(seconds=3600),
)
activity.moving = activity.duration
db.session.add(activity)
workout.moving = workout.duration
db.session.add(workout)
db.session.commit()
return activity
return workout
@pytest.fixture()
@ -332,7 +332,7 @@ def gpx_file() -> str:
'<gpx xmlns:gpxdata="http://www.cluetrust.com/XML/GPXDATA/1/0" xmlns:gpxtpx="http://www.garmin.com/xmlschemas/TrackPointExtension/v1" xmlns:gpxext="http://www.garmin.com/xmlschemas/GpxExtensions/v3" xmlns="http://www.topografix.com/GPX/1/1">' # noqa
' <metadata/>'
' <trk>'
' <name>just an activity</name>'
' <name>just a workout</name>'
' <trkseg>'
' <trkpt lat="44.68095" lon="6.07367">'
' <ele>998</ele>'
@ -580,7 +580,7 @@ def gpx_file_with_segments() -> str:
'<gpx xmlns:gpxdata="http://www.cluetrust.com/XML/GPXDATA/1/0" xmlns:gpxtpx="http://www.garmin.com/xmlschemas/TrackPointExtension/v1" xmlns:gpxext="http://www.garmin.com/xmlschemas/GpxExtensions/v3" xmlns="http://www.topografix.com/GPX/1/1">' # noqa
' <metadata/>'
' <trk>'
' <name>just an activity</name>'
' <name>just a workout</name>'
' <trkseg>'
' <trkpt lat="44.68095" lon="6.07367">'
' <ele>998</ele>'

Binary file not shown.

View File

@ -3,9 +3,9 @@ from datetime import datetime, timedelta
from io import BytesIO
from unittest.mock import Mock, patch
from fittrackee.activities.models import Activity, Sport
from fittrackee.users.models import User
from fittrackee.users.utils_token import get_user_token
from fittrackee.workouts.models import Sport, Workout
from flask import Flask
from freezegun import freeze_time
@ -446,8 +446,8 @@ class TestUserProfile:
assert data['data']['timezone'] is None
assert data['data']['weekm'] is False
assert data['data']['language'] is None
assert data['data']['nb_activities'] == 0
assert data['data']['nb_sports'] == 0
assert data['data']['nb_workouts'] == 0
assert data['data']['sports_list'] == []
assert data['data']['total_distance'] == 0
assert data['data']['total_duration'] == '0:00:00'
@ -484,21 +484,21 @@ class TestUserProfile:
assert data['data']['timezone'] == 'America/New_York'
assert data['data']['weekm'] is False
assert data['data']['language'] == 'en'
assert data['data']['nb_activities'] == 0
assert data['data']['nb_sports'] == 0
assert data['data']['nb_workouts'] == 0
assert data['data']['sports_list'] == []
assert data['data']['total_distance'] == 0
assert data['data']['total_duration'] == '0:00:00'
assert response.status_code == 200
def test_it_returns_user_profile_with_activities(
def test_it_returns_user_profile_with_workouts(
self,
app: Flask,
user_1: User,
sport_1_cycling: Sport,
sport_2_running: Sport,
activity_cycling_user_1: Activity,
activity_running_user_1: Activity,
workout_cycling_user_1: Workout,
workout_running_user_1: Workout,
) -> None:
client = app.test_client()
resp_login = client.post(
@ -521,8 +521,8 @@ class TestUserProfile:
assert data['data']['created_at']
assert not data['data']['admin']
assert data['data']['timezone'] is None
assert data['data']['nb_activities'] == 2
assert data['data']['nb_sports'] == 2
assert data['data']['nb_workouts'] == 2
assert data['data']['sports_list'] == [1, 2]
assert data['data']['total_distance'] == 22
assert data['data']['total_duration'] == '2:40:00'
@ -585,8 +585,8 @@ class TestUserProfileUpdate:
assert data['data']['timezone'] == 'America/New_York'
assert data['data']['weekm'] is True
assert data['data']['language'] == 'fr'
assert data['data']['nb_activities'] == 0
assert data['data']['nb_sports'] == 0
assert data['data']['nb_workouts'] == 0
assert data['data']['sports_list'] == []
assert data['data']['total_distance'] == 0
assert data['data']['total_duration'] == '0:00:00'
@ -636,8 +636,8 @@ class TestUserProfileUpdate:
assert data['data']['timezone'] == 'America/New_York'
assert data['data']['weekm'] is True
assert data['data']['language'] == 'fr'
assert data['data']['nb_activities'] == 0
assert data['data']['nb_sports'] == 0
assert data['data']['nb_workouts'] == 0
assert data['data']['sports_list'] == []
assert data['data']['total_distance'] == 0
assert data['data']['total_duration'] == '0:00:00'

View File

@ -3,13 +3,13 @@ from datetime import datetime, timedelta
from io import BytesIO
from unittest.mock import patch
from fittrackee.activities.models import Activity, Sport
from fittrackee.users.models import User
from fittrackee.workouts.models import Sport, Workout
from flask import Flask
class TestGetUser:
def test_it_gets_single_user_without_activities(
def test_it_gets_single_user_without_workouts(
self, app: Flask, user_1: User, user_2: User
) -> None:
client = app.test_client()
@ -45,20 +45,20 @@ class TestGetUser:
assert user['timezone'] is None
assert user['weekm'] is False
assert user['language'] is None
assert user['nb_activities'] == 0
assert user['nb_sports'] == 0
assert user['nb_workouts'] == 0
assert user['sports_list'] == []
assert user['total_distance'] == 0
assert user['total_duration'] == '0:00:00'
def test_it_gets_single_user_with_activities(
def test_it_gets_single_user_with_workouts(
self,
app: Flask,
user_1: User,
sport_1_cycling: Sport,
sport_2_running: Sport,
activity_cycling_user_1: Activity,
activity_running_user_1: Activity,
workout_cycling_user_1: Workout,
workout_running_user_1: Workout,
) -> None:
client = app.test_client()
resp_login = client.post(
@ -93,8 +93,8 @@ class TestGetUser:
assert user['timezone'] is None
assert user['weekm'] is False
assert user['language'] is None
assert user['nb_activities'] == 2
assert user['nb_sports'] == 2
assert user['nb_workouts'] == 2
assert user['sports_list'] == [1, 2]
assert user['total_distance'] == 22
assert user['total_duration'] == '2:40:00'
@ -158,24 +158,24 @@ class TestGetUsers:
assert data['data']['users'][0]['timezone'] is None
assert data['data']['users'][0]['weekm'] is False
assert data['data']['users'][0]['language'] is None
assert data['data']['users'][0]['nb_activities'] == 0
assert data['data']['users'][0]['nb_sports'] == 0
assert data['data']['users'][0]['nb_workouts'] == 0
assert data['data']['users'][0]['sports_list'] == []
assert data['data']['users'][0]['total_distance'] == 0
assert data['data']['users'][0]['total_duration'] == '0:00:00'
assert data['data']['users'][1]['timezone'] is None
assert data['data']['users'][1]['weekm'] is False
assert data['data']['users'][1]['language'] is None
assert data['data']['users'][1]['nb_activities'] == 0
assert data['data']['users'][1]['nb_sports'] == 0
assert data['data']['users'][1]['nb_workouts'] == 0
assert data['data']['users'][1]['sports_list'] == []
assert data['data']['users'][1]['total_distance'] == 0
assert data['data']['users'][1]['total_duration'] == '0:00:00'
assert data['data']['users'][2]['timezone'] is None
assert data['data']['users'][2]['weekm'] is True
assert data['data']['users'][2]['language'] is None
assert data['data']['users'][2]['nb_activities'] == 0
assert data['data']['users'][2]['nb_sports'] == 0
assert data['data']['users'][2]['nb_workouts'] == 0
assert data['data']['users'][2]['sports_list'] == []
assert data['data']['users'][2]['total_distance'] == 0
assert data['data']['users'][2]['total_duration'] == '0:00:00'
@ -187,17 +187,17 @@ class TestGetUsers:
'total': 3,
}
def test_it_gets_users_list_with_activities(
def test_it_gets_users_list_with_workouts(
self,
app: Flask,
user_1: User,
user_2: User,
user_3: User,
sport_1_cycling: Sport,
activity_cycling_user_1: Activity,
workout_cycling_user_1: Workout,
sport_2_running: Sport,
activity_running_user_1: Activity,
activity_cycling_user_2: Activity,
workout_running_user_1: Workout,
workout_cycling_user_2: Workout,
) -> None:
client = app.test_client()
resp_login = client.post(
@ -229,22 +229,22 @@ class TestGetUsers:
assert 'sam@test.com' in data['data']['users'][2]['email']
assert data['data']['users'][0]['timezone'] is None
assert data['data']['users'][0]['weekm'] is False
assert data['data']['users'][0]['nb_activities'] == 2
assert data['data']['users'][0]['nb_sports'] == 2
assert data['data']['users'][0]['nb_workouts'] == 2
assert data['data']['users'][0]['sports_list'] == [1, 2]
assert data['data']['users'][0]['total_distance'] == 22.0
assert data['data']['users'][0]['total_duration'] == '2:40:00'
assert data['data']['users'][1]['timezone'] is None
assert data['data']['users'][1]['weekm'] is False
assert data['data']['users'][1]['nb_activities'] == 1
assert data['data']['users'][1]['nb_sports'] == 1
assert data['data']['users'][1]['nb_workouts'] == 1
assert data['data']['users'][1]['sports_list'] == [1]
assert data['data']['users'][1]['total_distance'] == 15
assert data['data']['users'][1]['total_duration'] == '1:00:00'
assert data['data']['users'][2]['timezone'] is None
assert data['data']['users'][2]['weekm'] is True
assert data['data']['users'][2]['nb_activities'] == 0
assert data['data']['users'][2]['nb_sports'] == 0
assert data['data']['users'][2]['nb_workouts'] == 0
assert data['data']['users'][2]['sports_list'] == []
assert data['data']['users'][2]['total_distance'] == 0
assert data['data']['users'][2]['total_duration'] == '0:00:00'
@ -745,14 +745,14 @@ class TestGetUsers:
'total': 3,
}
def test_it_gets_users_list_ordered_by_activities_count(
def test_it_gets_users_list_ordered_by_workouts_count(
self,
app: Flask,
user_1: User,
user_2: User,
user_3: User,
sport_1_cycling: Sport,
activity_cycling_user_2: Activity,
workout_cycling_user_2: Workout,
) -> None:
client = app.test_client()
resp_login = client.post(
@ -762,7 +762,7 @@ class TestGetUsers:
)
response = client.get(
'/api/users?order_by=activities_count',
'/api/users?order_by=workouts_count',
headers=dict(
Authorization='Bearer '
+ json.loads(resp_login.data.decode())['auth_token']
@ -774,11 +774,11 @@ class TestGetUsers:
assert 'success' in data['status']
assert len(data['data']['users']) == 3
assert 'test' in data['data']['users'][0]['username']
assert 0 == data['data']['users'][0]['nb_activities']
assert 0 == data['data']['users'][0]['nb_workouts']
assert 'sam' in data['data']['users'][1]['username']
assert 0 == data['data']['users'][1]['nb_activities']
assert 0 == data['data']['users'][1]['nb_workouts']
assert 'toto' in data['data']['users'][2]['username']
assert 1 == data['data']['users'][2]['nb_activities']
assert 1 == data['data']['users'][2]['nb_workouts']
assert data['pagination'] == {
'has_next': False,
'has_prev': False,
@ -787,14 +787,14 @@ class TestGetUsers:
'total': 3,
}
def test_it_gets_users_list_ordered_by_activities_count_ascending(
def test_it_gets_users_list_ordered_by_workouts_count_ascending(
self,
app: Flask,
user_1: User,
user_2: User,
user_3: User,
sport_1_cycling: Sport,
activity_cycling_user_2: Activity,
workout_cycling_user_2: Workout,
) -> None:
client = app.test_client()
resp_login = client.post(
@ -804,7 +804,7 @@ class TestGetUsers:
)
response = client.get(
'/api/users?order_by=activities_count&order=asc',
'/api/users?order_by=workouts_count&order=asc',
headers=dict(
Authorization='Bearer '
+ json.loads(resp_login.data.decode())['auth_token']
@ -816,11 +816,11 @@ class TestGetUsers:
assert 'success' in data['status']
assert len(data['data']['users']) == 3
assert 'test' in data['data']['users'][0]['username']
assert 0 == data['data']['users'][0]['nb_activities']
assert 0 == data['data']['users'][0]['nb_workouts']
assert 'sam' in data['data']['users'][1]['username']
assert 0 == data['data']['users'][1]['nb_activities']
assert 0 == data['data']['users'][1]['nb_workouts']
assert 'toto' in data['data']['users'][2]['username']
assert 1 == data['data']['users'][2]['nb_activities']
assert 1 == data['data']['users'][2]['nb_workouts']
assert data['pagination'] == {
'has_next': False,
'has_prev': False,
@ -829,14 +829,14 @@ class TestGetUsers:
'total': 3,
}
def test_it_gets_users_list_ordered_by_activities_count_descending(
def test_it_gets_users_list_ordered_by_workouts_count_descending(
self,
app: Flask,
user_1: User,
user_2: User,
user_3: User,
sport_1_cycling: Sport,
activity_cycling_user_2: Activity,
workout_cycling_user_2: Workout,
) -> None:
client = app.test_client()
resp_login = client.post(
@ -846,7 +846,7 @@ class TestGetUsers:
)
response = client.get(
'/api/users?order_by=activities_count&order=desc',
'/api/users?order_by=workouts_count&order=desc',
headers=dict(
Authorization='Bearer '
+ json.loads(resp_login.data.decode())['auth_token']
@ -858,11 +858,11 @@ class TestGetUsers:
assert 'success' in data['status']
assert len(data['data']['users']) == 3
assert 'toto' in data['data']['users'][0]['username']
assert 1 == data['data']['users'][0]['nb_activities']
assert 1 == data['data']['users'][0]['nb_workouts']
assert 'test' in data['data']['users'][1]['username']
assert 0 == data['data']['users'][1]['nb_activities']
assert 0 == data['data']['users'][1]['nb_workouts']
assert 'sam' in data['data']['users'][2]['username']
assert 0 == data['data']['users'][2]['nb_activities']
assert 0 == data['data']['users'][2]['nb_workouts']
assert data['pagination'] == {
'has_next': False,
'has_prev': False,
@ -1156,7 +1156,7 @@ class TestDeleteUser:
assert response.status_code == 204
def test_user_with_activity_can_delete_its_own_account(
def test_user_with_workout_can_delete_its_own_account(
self, app: Flask, user_1: User, sport_1_cycling: Sport, gpx_file: str
) -> None:
client = app.test_client()
@ -1166,7 +1166,7 @@ class TestDeleteUser:
content_type='application/json',
)
client.post(
'/api/activities',
'/api/workouts',
data=dict(
file=(BytesIO(str.encode(gpx_file)), 'example.gpx'),
data='{"sport_id": 1}',

View File

@ -19,8 +19,8 @@ class TestUserModel:
assert serialized_user['timezone'] is None
assert serialized_user['weekm'] is False
assert serialized_user['language'] is None
assert serialized_user['nb_activities'] == 0
assert serialized_user['nb_sports'] == 0
assert serialized_user['nb_workouts'] == 0
assert serialized_user['total_distance'] == 0
assert serialized_user['total_duration'] == '0:00:00'

View File

@ -1,7 +1,7 @@
import json
from fittrackee.activities.models import Activity, Sport
from fittrackee.users.models import User
from fittrackee.workouts.models import Sport, Workout
from flask import Flask
@ -13,8 +13,8 @@ class TestGetRecords:
user_2: User,
sport_1_cycling: Sport,
sport_2_running: Sport,
activity_cycling_user_1: Activity,
activity_cycling_user_2: Activity,
workout_cycling_user_1: Workout,
workout_cycling_user_2: Workout,
) -> None:
client = app.test_client()
resp_login = client.post(
@ -37,64 +37,64 @@ class TestGetRecords:
assert (
'Mon, 01 Jan 2018 00:00:00 GMT'
== data['data']['records'][0]['activity_date']
== data['data']['records'][0]['workout_date']
)
assert 'test' == data['data']['records'][0]['user']
assert sport_1_cycling.id == data['data']['records'][0]['sport_id']
assert (
activity_cycling_user_1.short_id
== data['data']['records'][0]['activity_id']
workout_cycling_user_1.short_id
== data['data']['records'][0]['workout_id']
)
assert 'AS' == data['data']['records'][0]['record_type']
assert 'value' in data['data']['records'][0]
assert (
'Mon, 01 Jan 2018 00:00:00 GMT'
== data['data']['records'][1]['activity_date']
== data['data']['records'][1]['workout_date']
)
assert 'test' == data['data']['records'][1]['user']
assert sport_1_cycling.id == data['data']['records'][1]['sport_id']
assert (
activity_cycling_user_1.short_id
== data['data']['records'][1]['activity_id']
workout_cycling_user_1.short_id
== data['data']['records'][1]['workout_id']
)
assert 'FD' == data['data']['records'][1]['record_type']
assert 'value' in data['data']['records'][1]
assert (
'Mon, 01 Jan 2018 00:00:00 GMT'
== data['data']['records'][2]['activity_date']
== data['data']['records'][2]['workout_date']
)
assert 'test' == data['data']['records'][2]['user']
assert sport_1_cycling.id == data['data']['records'][2]['sport_id']
assert (
activity_cycling_user_1.short_id
== data['data']['records'][2]['activity_id']
workout_cycling_user_1.short_id
== data['data']['records'][2]['workout_id']
)
assert 'LD' == data['data']['records'][2]['record_type']
assert 'value' in data['data']['records'][2]
assert (
'Mon, 01 Jan 2018 00:00:00 GMT'
== data['data']['records'][3]['activity_date']
== data['data']['records'][3]['workout_date']
) # noqa
assert 'test' == data['data']['records'][3]['user']
assert sport_1_cycling.id == data['data']['records'][3]['sport_id']
assert (
activity_cycling_user_1.short_id
== data['data']['records'][3]['activity_id']
workout_cycling_user_1.short_id
== data['data']['records'][3]['workout_id']
)
assert 'MS' == data['data']['records'][3]['record_type']
assert 'value' in data['data']['records'][3]
def test_it_gets_no_records_if_user_has_no_activity(
def test_it_gets_no_records_if_user_has_no_workout(
self,
app: Flask,
user_1: User,
user_2: User,
sport_1_cycling: Sport,
sport_2_running: Sport,
activity_cycling_user_2: Activity,
workout_cycling_user_2: Workout,
) -> None:
client = app.test_client()
resp_login = client.post(
@ -115,7 +115,7 @@ class TestGetRecords:
assert 'success' in data['status']
assert len(data['data']['records']) == 0
def test_it_gets_no_records_if_activity_has_zero_value(
def test_it_gets_no_records_if_workout_has_zero_value(
self,
app: Flask,
user_1: User,
@ -129,15 +129,15 @@ class TestGetRecords:
content_type='application/json',
)
client.post(
'/api/activities/no_gpx',
'/api/workouts/no_gpx',
content_type='application/json',
data=json.dumps(
dict(
sport_id=1,
duration=0,
activity_date='2018-05-14 14:05',
workout_date='2018-05-14 14:05',
distance=0,
title='Activity test',
title='Workout test',
)
),
headers=dict(
@ -158,7 +158,7 @@ class TestGetRecords:
assert 'success' in data['status']
assert len(data['data']['records']) == 0
def test_it_gets_updated_records_after_activities_post_and_patch(
def test_it_gets_updated_records_after_workouts_post_and_patch(
self, app: Flask, user_1: User, sport_1_cycling: Sport
) -> None:
client = app.test_client()
@ -168,15 +168,15 @@ class TestGetRecords:
content_type='application/json',
)
response = client.post(
'/api/activities/no_gpx',
'/api/workouts/no_gpx',
content_type='application/json',
data=json.dumps(
dict(
sport_id=1,
duration=3600,
activity_date='2018-05-14 14:05',
workout_date='2018-05-14 14:05',
distance=7,
title='Activity test 1',
title='Workout test 1',
)
),
headers=dict(
@ -185,7 +185,7 @@ class TestGetRecords:
),
)
data = json.loads(response.data.decode())
activity_1_short_id = data['data']['activities'][0]['id']
workout_1_short_id = data['data']['workouts'][0]['id']
response = client.get(
'/api/records',
headers=dict(
@ -201,56 +201,56 @@ class TestGetRecords:
assert (
'Mon, 14 May 2018 14:05:00 GMT'
== data['data']['records'][0]['activity_date']
== data['data']['records'][0]['workout_date']
) # noqa
assert 'test' == data['data']['records'][0]['user']
assert sport_1_cycling.id == data['data']['records'][0]['sport_id']
assert activity_1_short_id == data['data']['records'][0]['activity_id']
assert workout_1_short_id == data['data']['records'][0]['workout_id']
assert 'AS' == data['data']['records'][0]['record_type']
assert 7.0 == data['data']['records'][0]['value']
assert (
'Mon, 14 May 2018 14:05:00 GMT'
== data['data']['records'][1]['activity_date']
== data['data']['records'][1]['workout_date']
) # noqa
assert 'test' == data['data']['records'][1]['user']
assert sport_1_cycling.id == data['data']['records'][1]['sport_id']
assert activity_1_short_id == data['data']['records'][1]['activity_id']
assert workout_1_short_id == data['data']['records'][1]['workout_id']
assert 'FD' == data['data']['records'][1]['record_type']
assert 7.0 == data['data']['records'][1]['value']
assert (
'Mon, 14 May 2018 14:05:00 GMT'
== data['data']['records'][2]['activity_date']
== data['data']['records'][2]['workout_date']
) # noqa
assert 'test' == data['data']['records'][2]['user']
assert sport_1_cycling.id == data['data']['records'][2]['sport_id']
assert activity_1_short_id == data['data']['records'][2]['activity_id']
assert workout_1_short_id == data['data']['records'][2]['workout_id']
assert 'LD' == data['data']['records'][2]['record_type']
assert '1:00:00' == data['data']['records'][2]['value']
assert (
'Mon, 14 May 2018 14:05:00 GMT'
== data['data']['records'][3]['activity_date']
== data['data']['records'][3]['workout_date']
) # noqa
assert 'test' == data['data']['records'][3]['user']
assert sport_1_cycling.id == data['data']['records'][3]['sport_id']
assert activity_1_short_id == data['data']['records'][3]['activity_id']
assert workout_1_short_id == data['data']['records'][3]['workout_id']
assert 'MS' == data['data']['records'][3]['record_type']
assert 7.0 == data['data']['records'][3]['value']
# Post activity with lower duration (same sport)
# Post workout with lower duration (same sport)
# => 2 new records: Average speed and Max speed
response = client.post(
'/api/activities/no_gpx',
'/api/workouts/no_gpx',
content_type='application/json',
data=json.dumps(
dict(
sport_id=1,
duration=3000,
activity_date='2018-05-15 14:05',
workout_date='2018-05-15 14:05',
distance=7,
title='Activity test 2',
title='Workout test 2',
)
),
headers=dict(
@ -259,7 +259,7 @@ class TestGetRecords:
),
)
data = json.loads(response.data.decode())
activity_2_short_id = data['data']['activities'][0]['id']
workout_2_short_id = data['data']['workouts'][0]['id']
response = client.get(
'/api/records',
headers=dict(
@ -275,55 +275,55 @@ class TestGetRecords:
assert (
'Tue, 15 May 2018 14:05:00 GMT'
== data['data']['records'][0]['activity_date']
== data['data']['records'][0]['workout_date']
) # noqa
assert 'test' == data['data']['records'][0]['user']
assert sport_1_cycling.id == data['data']['records'][0]['sport_id']
assert activity_2_short_id == data['data']['records'][0]['activity_id']
assert workout_2_short_id == data['data']['records'][0]['workout_id']
assert 'AS' == data['data']['records'][0]['record_type']
assert 8.4 == data['data']['records'][0]['value']
assert (
'Mon, 14 May 2018 14:05:00 GMT'
== data['data']['records'][1]['activity_date']
== data['data']['records'][1]['workout_date']
) # noqa
assert 'test' == data['data']['records'][1]['user']
assert sport_1_cycling.id == data['data']['records'][1]['sport_id']
assert activity_1_short_id == data['data']['records'][1]['activity_id']
assert workout_1_short_id == data['data']['records'][1]['workout_id']
assert 'FD' == data['data']['records'][1]['record_type']
assert 7.0 == data['data']['records'][1]['value']
assert (
'Mon, 14 May 2018 14:05:00 GMT'
== data['data']['records'][2]['activity_date']
== data['data']['records'][2]['workout_date']
) # noqa
assert 'test' == data['data']['records'][2]['user']
assert sport_1_cycling.id == data['data']['records'][2]['sport_id']
assert activity_1_short_id == data['data']['records'][2]['activity_id']
assert workout_1_short_id == data['data']['records'][2]['workout_id']
assert 'LD' == data['data']['records'][2]['record_type']
assert '1:00:00' == data['data']['records'][2]['value']
assert (
'Tue, 15 May 2018 14:05:00 GMT'
== data['data']['records'][0]['activity_date']
== data['data']['records'][0]['workout_date']
) # noqa
assert 'test' == data['data']['records'][0]['user']
assert sport_1_cycling.id == data['data']['records'][0]['sport_id']
assert activity_2_short_id == data['data']['records'][0]['activity_id']
assert workout_2_short_id == data['data']['records'][0]['workout_id']
assert 'MS' == data['data']['records'][3]['record_type']
assert 8.4 == data['data']['records'][3]['value']
# Post activity with no new records
# Post workout with no new records
response = client.post(
'/api/activities/no_gpx',
'/api/workouts/no_gpx',
content_type='application/json',
data=json.dumps(
dict(
sport_id=1,
duration=3500,
activity_date='2018-05-16 14:05',
workout_date='2018-05-16 14:05',
distance=6.5,
title='Activity test 3',
title='Workout test 3',
)
),
headers=dict(
@ -332,7 +332,7 @@ class TestGetRecords:
),
)
data = json.loads(response.data.decode())
activity_3_short_id = data['data']['activities'][0]['id']
workout_3_short_id = data['data']['workouts'][0]['id']
response = client.get(
'/api/records',
headers=dict(
@ -348,48 +348,48 @@ class TestGetRecords:
assert (
'Tue, 15 May 2018 14:05:00 GMT'
== data['data']['records'][0]['activity_date']
== data['data']['records'][0]['workout_date']
) # noqa
assert 'test' == data['data']['records'][0]['user']
assert sport_1_cycling.id == data['data']['records'][0]['sport_id']
assert activity_2_short_id == data['data']['records'][0]['activity_id']
assert workout_2_short_id == data['data']['records'][0]['workout_id']
assert 'AS' == data['data']['records'][0]['record_type']
assert 8.4 == data['data']['records'][0]['value']
assert (
'Mon, 14 May 2018 14:05:00 GMT'
== data['data']['records'][1]['activity_date']
== data['data']['records'][1]['workout_date']
) # noqa
assert 'test' == data['data']['records'][1]['user']
assert sport_1_cycling.id == data['data']['records'][1]['sport_id']
assert activity_1_short_id == data['data']['records'][1]['activity_id']
assert workout_1_short_id == data['data']['records'][1]['workout_id']
assert 'FD' == data['data']['records'][1]['record_type']
assert 7.0 == data['data']['records'][1]['value']
assert (
'Mon, 14 May 2018 14:05:00 GMT'
== data['data']['records'][2]['activity_date']
== data['data']['records'][2]['workout_date']
) # noqa
assert 'test' == data['data']['records'][2]['user']
assert sport_1_cycling.id == data['data']['records'][2]['sport_id']
assert activity_1_short_id == data['data']['records'][2]['activity_id']
assert workout_1_short_id == data['data']['records'][2]['workout_id']
assert 'LD' == data['data']['records'][2]['record_type']
assert '1:00:00' == data['data']['records'][2]['value']
assert (
'Tue, 15 May 2018 14:05:00 GMT'
== data['data']['records'][0]['activity_date']
== data['data']['records'][0]['workout_date']
) # noqa
assert 'test' == data['data']['records'][0]['user']
assert sport_1_cycling.id == data['data']['records'][0]['sport_id']
assert activity_2_short_id == data['data']['records'][0]['activity_id']
assert workout_2_short_id == data['data']['records'][0]['workout_id']
assert 'MS' == data['data']['records'][3]['record_type']
assert 8.4 == data['data']['records'][3]['value']
# Edit last activity
# Edit last workout
# 1 new record: Longest duration
client.patch(
f'/api/activities/{activity_3_short_id}',
f'/api/workouts/{workout_3_short_id}',
content_type='application/json',
data=json.dumps(dict(duration=4000)),
headers=dict(
@ -412,47 +412,47 @@ class TestGetRecords:
assert (
'Tue, 15 May 2018 14:05:00 GMT'
== data['data']['records'][0]['activity_date']
== data['data']['records'][0]['workout_date']
) # noqa
assert 'test' == data['data']['records'][0]['user']
assert sport_1_cycling.id == data['data']['records'][0]['sport_id']
assert activity_2_short_id == data['data']['records'][0]['activity_id']
assert workout_2_short_id == data['data']['records'][0]['workout_id']
assert 'AS' == data['data']['records'][0]['record_type']
assert 8.4 == data['data']['records'][0]['value']
assert (
'Mon, 14 May 2018 14:05:00 GMT'
== data['data']['records'][1]['activity_date']
== data['data']['records'][1]['workout_date']
) # noqa
assert 'test' == data['data']['records'][1]['user']
assert sport_1_cycling.id == data['data']['records'][1]['sport_id']
assert activity_1_short_id == data['data']['records'][1]['activity_id']
assert workout_1_short_id == data['data']['records'][1]['workout_id']
assert 'FD' == data['data']['records'][1]['record_type']
assert 7.0 == data['data']['records'][1]['value']
assert (
'Wed, 16 May 2018 14:05:00 GMT'
== data['data']['records'][2]['activity_date']
== data['data']['records'][2]['workout_date']
) # noqa
assert 'test' == data['data']['records'][2]['user']
assert sport_1_cycling.id == data['data']['records'][2]['sport_id']
assert activity_3_short_id == data['data']['records'][2]['activity_id']
assert workout_3_short_id == data['data']['records'][2]['workout_id']
assert 'LD' == data['data']['records'][2]['record_type']
assert '1:06:40' == data['data']['records'][2]['value']
assert (
'Tue, 15 May 2018 14:05:00 GMT'
== data['data']['records'][0]['activity_date']
== data['data']['records'][0]['workout_date']
) # noqa
assert 'test' == data['data']['records'][0]['user']
assert sport_1_cycling.id == data['data']['records'][0]['sport_id']
assert activity_2_short_id == data['data']['records'][0]['activity_id']
assert workout_2_short_id == data['data']['records'][0]['workout_id']
assert 'MS' == data['data']['records'][3]['record_type']
assert 8.4 == data['data']['records'][3]['value']
# delete activity 2 => AS and MS record update
# delete workout 2 => AS and MS record update
client.delete(
f'/api/activities/{activity_2_short_id}',
f'/api/workouts/{workout_2_short_id}',
headers=dict(
Authorization='Bearer '
+ json.loads(resp_login.data.decode())['auth_token']
@ -473,56 +473,56 @@ class TestGetRecords:
assert (
'Mon, 14 May 2018 14:05:00 GMT'
== data['data']['records'][0]['activity_date']
== data['data']['records'][0]['workout_date']
) # noqa
assert 'test' == data['data']['records'][0]['user']
assert sport_1_cycling.id == data['data']['records'][0]['sport_id']
assert activity_1_short_id == data['data']['records'][0]['activity_id']
assert workout_1_short_id == data['data']['records'][0]['workout_id']
assert 'AS' == data['data']['records'][0]['record_type']
assert 7.0 == data['data']['records'][0]['value']
assert (
'Mon, 14 May 2018 14:05:00 GMT'
== data['data']['records'][1]['activity_date']
== data['data']['records'][1]['workout_date']
) # noqa
assert 'test' == data['data']['records'][1]['user']
assert sport_1_cycling.id == data['data']['records'][1]['sport_id']
assert activity_1_short_id == data['data']['records'][1]['activity_id']
assert workout_1_short_id == data['data']['records'][1]['workout_id']
assert 'FD' == data['data']['records'][1]['record_type']
assert 7.0 == data['data']['records'][1]['value']
assert (
'Wed, 16 May 2018 14:05:00 GMT'
== data['data']['records'][2]['activity_date']
== data['data']['records'][2]['workout_date']
) # noqa
assert 'test' == data['data']['records'][2]['user']
assert sport_1_cycling.id == data['data']['records'][2]['sport_id']
assert activity_3_short_id == data['data']['records'][2]['activity_id']
assert workout_3_short_id == data['data']['records'][2]['workout_id']
assert 'LD' == data['data']['records'][2]['record_type']
assert '1:06:40' == data['data']['records'][2]['value']
assert (
'Mon, 14 May 2018 14:05:00 GMT'
== data['data']['records'][3]['activity_date']
== data['data']['records'][3]['workout_date']
) # noqa
assert 'test' == data['data']['records'][3]['user']
assert sport_1_cycling.id == data['data']['records'][3]['sport_id']
assert activity_1_short_id == data['data']['records'][3]['activity_id']
assert workout_1_short_id == data['data']['records'][3]['workout_id']
assert 'MS' == data['data']['records'][3]['record_type']
assert 7.0 == data['data']['records'][3]['value']
# add an activity with the same data as activity 1 except with a
# add a workout with the same data as workout 1 except with a
# later date => no change in record
response = client.post(
'/api/activities/no_gpx',
'/api/workouts/no_gpx',
content_type='application/json',
data=json.dumps(
dict(
sport_id=1,
duration=3600,
activity_date='2018-05-20 14:05',
workout_date='2018-05-20 14:05',
distance=7,
title='Activity test 4',
title='Workout test 4',
)
),
headers=dict(
@ -531,7 +531,7 @@ class TestGetRecords:
),
)
data = json.loads(response.data.decode())
activity_4_short_id = data['data']['activities'][0]['id']
workout_4_short_id = data['data']['workouts'][0]['id']
response = client.get(
'/api/records',
headers=dict(
@ -547,58 +547,58 @@ class TestGetRecords:
assert (
'Mon, 14 May 2018 14:05:00 GMT'
== data['data']['records'][0]['activity_date']
== data['data']['records'][0]['workout_date']
) # noqa
assert 'test' == data['data']['records'][0]['user']
assert sport_1_cycling.id == data['data']['records'][0]['sport_id']
assert activity_1_short_id == data['data']['records'][0]['activity_id']
assert workout_1_short_id == data['data']['records'][0]['workout_id']
assert 'AS' == data['data']['records'][0]['record_type']
assert 7.0 == data['data']['records'][0]['value']
assert (
'Mon, 14 May 2018 14:05:00 GMT'
== data['data']['records'][1]['activity_date']
== data['data']['records'][1]['workout_date']
) # noqa
assert 'test' == data['data']['records'][1]['user']
assert sport_1_cycling.id == data['data']['records'][1]['sport_id']
assert activity_1_short_id == data['data']['records'][1]['activity_id']
assert workout_1_short_id == data['data']['records'][1]['workout_id']
assert 'FD' == data['data']['records'][1]['record_type']
assert 7.0 == data['data']['records'][1]['value']
assert (
'Wed, 16 May 2018 14:05:00 GMT'
== data['data']['records'][2]['activity_date']
== data['data']['records'][2]['workout_date']
) # noqa
assert 'test' == data['data']['records'][2]['user']
assert sport_1_cycling.id == data['data']['records'][2]['sport_id']
assert activity_3_short_id == data['data']['records'][2]['activity_id']
assert workout_3_short_id == data['data']['records'][2]['workout_id']
assert 'LD' == data['data']['records'][2]['record_type']
assert '1:06:40' == data['data']['records'][2]['value']
assert (
'Mon, 14 May 2018 14:05:00 GMT'
== data['data']['records'][3]['activity_date']
== data['data']['records'][3]['workout_date']
) # noqa
assert 'test' == data['data']['records'][3]['user']
assert sport_1_cycling.id == data['data']['records'][3]['sport_id']
assert activity_1_short_id == data['data']['records'][3]['activity_id']
assert workout_1_short_id == data['data']['records'][3]['workout_id']
assert 'MS' == data['data']['records'][3]['record_type']
assert 7.0 == data['data']['records'][3]['value']
# add an activity with the same data as activity 1 except with
# add a workout with the same data as workout 1 except with
# an earlier date
# => record update (activity 5 replace activity 1)
# => record update (workout 5 replace workout 1)
response = client.post(
'/api/activities/no_gpx',
'/api/workouts/no_gpx',
content_type='application/json',
data=json.dumps(
dict(
sport_id=1,
duration=3600,
activity_date='2018-05-14 08:05',
workout_date='2018-05-14 08:05',
distance=7,
title='Activity test 5',
title='Workout test 5',
)
),
headers=dict(
@ -607,7 +607,7 @@ class TestGetRecords:
),
)
data = json.loads(response.data.decode())
activity_5_short_id = data['data']['activities'][0]['id']
workout_5_short_id = data['data']['workouts'][0]['id']
response = client.get(
'/api/records',
headers=dict(
@ -623,68 +623,68 @@ class TestGetRecords:
assert (
'Mon, 14 May 2018 08:05:00 GMT'
== data['data']['records'][0]['activity_date']
== data['data']['records'][0]['workout_date']
) # noqa
assert 'test' == data['data']['records'][0]['user']
assert sport_1_cycling.id == data['data']['records'][0]['sport_id']
assert activity_5_short_id == data['data']['records'][0]['activity_id']
assert workout_5_short_id == data['data']['records'][0]['workout_id']
assert 'AS' == data['data']['records'][0]['record_type']
assert 7.0 == data['data']['records'][0]['value']
assert (
'Mon, 14 May 2018 08:05:00 GMT'
== data['data']['records'][1]['activity_date']
== data['data']['records'][1]['workout_date']
) # noqa
assert 'test' == data['data']['records'][1]['user']
assert sport_1_cycling.id == data['data']['records'][1]['sport_id']
assert activity_5_short_id == data['data']['records'][1]['activity_id']
assert workout_5_short_id == data['data']['records'][1]['workout_id']
assert 'FD' == data['data']['records'][1]['record_type']
assert 7.0 == data['data']['records'][1]['value']
assert (
'Wed, 16 May 2018 14:05:00 GMT'
== data['data']['records'][2]['activity_date']
== data['data']['records'][2]['workout_date']
) # noqa
assert 'test' == data['data']['records'][2]['user']
assert sport_1_cycling.id == data['data']['records'][2]['sport_id']
assert activity_3_short_id == data['data']['records'][2]['activity_id']
assert workout_3_short_id == data['data']['records'][2]['workout_id']
assert 'LD' == data['data']['records'][2]['record_type']
assert '1:06:40' == data['data']['records'][2]['value']
assert (
'Mon, 14 May 2018 08:05:00 GMT'
== data['data']['records'][3]['activity_date']
== data['data']['records'][3]['workout_date']
) # noqa
assert 'test' == data['data']['records'][3]['user']
assert sport_1_cycling.id == data['data']['records'][3]['sport_id']
assert activity_5_short_id == data['data']['records'][3]['activity_id']
assert workout_5_short_id == data['data']['records'][3]['workout_id']
assert 'MS' == data['data']['records'][3]['record_type']
assert 7.0 == data['data']['records'][3]['value']
# delete all activities - no more records
# delete all workouts - no more records
client.delete(
f'/api/activities/{activity_1_short_id}',
f'/api/workouts/{workout_1_short_id}',
headers=dict(
Authorization='Bearer '
+ json.loads(resp_login.data.decode())['auth_token']
),
)
client.delete(
f'/api/activities/{activity_3_short_id}',
f'/api/workouts/{workout_3_short_id}',
headers=dict(
Authorization='Bearer '
+ json.loads(resp_login.data.decode())['auth_token']
),
)
client.delete(
f'/api/activities/{activity_4_short_id}',
f'/api/workouts/{workout_4_short_id}',
headers=dict(
Authorization='Bearer '
+ json.loads(resp_login.data.decode())['auth_token']
),
)
client.delete(
f'/api/activities/{activity_5_short_id}',
f'/api/workouts/{workout_5_short_id}',
headers=dict(
Authorization='Bearer '
+ json.loads(resp_login.data.decode())['auth_token']
@ -717,15 +717,15 @@ class TestGetRecords:
content_type='application/json',
)
response = client.post(
'/api/activities/no_gpx',
'/api/workouts/no_gpx',
content_type='application/json',
data=json.dumps(
dict(
sport_id=1,
duration=3600,
activity_date='2018-05-14 14:05',
workout_date='2018-05-14 14:05',
distance=7,
title='Activity test 1',
title='Workout test 1',
)
),
headers=dict(
@ -734,17 +734,17 @@ class TestGetRecords:
),
)
data = json.loads(response.data.decode())
activity_1_short_id = data['data']['activities'][0]['id']
workout_1_short_id = data['data']['workouts'][0]['id']
response = client.post(
'/api/activities/no_gpx',
'/api/workouts/no_gpx',
content_type='application/json',
data=json.dumps(
dict(
sport_id=2,
duration=3600,
activity_date='2018-05-16 16:05',
workout_date='2018-05-16 16:05',
distance=20,
title='Activity test 2',
title='Workout test 2',
)
),
headers=dict(
@ -753,17 +753,17 @@ class TestGetRecords:
),
)
data = json.loads(response.data.decode())
activity_2_short_id = data['data']['activities'][0]['id']
workout_2_short_id = data['data']['workouts'][0]['id']
client.post(
'/api/activities/no_gpx',
'/api/workouts/no_gpx',
content_type='application/json',
data=json.dumps(
dict(
sport_id=1,
duration=3000,
activity_date='2018-05-17 17:05',
workout_date='2018-05-17 17:05',
distance=3,
title='Activity test 3',
title='Workout test 3',
)
),
headers=dict(
@ -772,15 +772,15 @@ class TestGetRecords:
),
)
response = client.post(
'/api/activities/no_gpx',
'/api/workouts/no_gpx',
content_type='application/json',
data=json.dumps(
dict(
sport_id=2,
duration=3000,
activity_date='2018-05-18 18:05',
workout_date='2018-05-18 18:05',
distance=10,
title='Activity test 4',
title='Workout test 4',
)
),
headers=dict(
@ -789,7 +789,7 @@ class TestGetRecords:
),
)
data = json.loads(response.data.decode())
activity_4_short_id = data['data']['activities'][0]['id']
workout_4_short_id = data['data']['workouts'][0]['id']
response = client.get(
'/api/records',
headers=dict(
@ -805,86 +805,86 @@ class TestGetRecords:
assert (
'Mon, 14 May 2018 14:05:00 GMT'
== data['data']['records'][0]['activity_date']
== data['data']['records'][0]['workout_date']
) # noqa
assert 'test' == data['data']['records'][0]['user']
assert sport_1_cycling.id == data['data']['records'][0]['sport_id']
assert activity_1_short_id == data['data']['records'][0]['activity_id']
assert workout_1_short_id == data['data']['records'][0]['workout_id']
assert 'AS' == data['data']['records'][0]['record_type']
assert 7.0 == data['data']['records'][0]['value']
assert (
'Mon, 14 May 2018 14:05:00 GMT'
== data['data']['records'][1]['activity_date']
== data['data']['records'][1]['workout_date']
) # noqa
assert 'test' == data['data']['records'][1]['user']
assert sport_1_cycling.id == data['data']['records'][1]['sport_id']
assert activity_1_short_id == data['data']['records'][1]['activity_id']
assert workout_1_short_id == data['data']['records'][1]['workout_id']
assert 'FD' == data['data']['records'][1]['record_type']
assert 7.0 == data['data']['records'][1]['value']
assert (
'Mon, 14 May 2018 14:05:00 GMT'
== data['data']['records'][2]['activity_date']
== data['data']['records'][2]['workout_date']
) # noqa
assert 'test' == data['data']['records'][2]['user']
assert sport_1_cycling.id == data['data']['records'][2]['sport_id']
assert activity_1_short_id == data['data']['records'][2]['activity_id']
assert workout_1_short_id == data['data']['records'][2]['workout_id']
assert 'LD' == data['data']['records'][2]['record_type']
assert '1:00:00' == data['data']['records'][2]['value']
assert (
'Mon, 14 May 2018 14:05:00 GMT'
== data['data']['records'][3]['activity_date']
== data['data']['records'][3]['workout_date']
) # noqa
assert 'test' == data['data']['records'][3]['user']
assert sport_1_cycling.id == data['data']['records'][3]['sport_id']
assert activity_1_short_id == data['data']['records'][3]['activity_id']
assert workout_1_short_id == data['data']['records'][3]['workout_id']
assert 'MS' == data['data']['records'][3]['record_type']
assert 7.0 == data['data']['records'][3]['value']
assert (
'Wed, 16 May 2018 16:05:00 GMT'
== data['data']['records'][4]['activity_date']
== data['data']['records'][4]['workout_date']
) # noqa
assert 'test' == data['data']['records'][4]['user']
assert sport_2_running.id == data['data']['records'][4]['sport_id']
assert activity_2_short_id == data['data']['records'][4]['activity_id']
assert workout_2_short_id == data['data']['records'][4]['workout_id']
assert 'AS' == data['data']['records'][4]['record_type']
assert 20.0 == data['data']['records'][4]['value']
assert (
'Wed, 16 May 2018 16:05:00 GMT'
== data['data']['records'][5]['activity_date']
== data['data']['records'][5]['workout_date']
) # noqa
assert 'test' == data['data']['records'][5]['user']
assert sport_2_running.id == data['data']['records'][5]['sport_id']
assert activity_2_short_id == data['data']['records'][5]['activity_id']
assert workout_2_short_id == data['data']['records'][5]['workout_id']
assert 'FD' == data['data']['records'][5]['record_type']
assert 20.0 == data['data']['records'][5]['value']
assert (
'Wed, 16 May 2018 16:05:00 GMT'
== data['data']['records'][6]['activity_date']
== data['data']['records'][6]['workout_date']
) # noqa
assert 'test' == data['data']['records'][6]['user']
assert sport_2_running.id == data['data']['records'][6]['sport_id']
assert activity_2_short_id == data['data']['records'][6]['activity_id']
assert workout_2_short_id == data['data']['records'][6]['workout_id']
assert 'LD' == data['data']['records'][6]['record_type']
assert '1:00:00' == data['data']['records'][6]['value']
assert (
'Wed, 16 May 2018 16:05:00 GMT'
== data['data']['records'][7]['activity_date']
== data['data']['records'][7]['workout_date']
) # noqa
assert 'test' == data['data']['records'][7]['user']
assert sport_2_running.id == data['data']['records'][7]['sport_id']
assert activity_2_short_id == data['data']['records'][7]['activity_id']
assert workout_2_short_id == data['data']['records'][7]['workout_id']
assert 'MS' == data['data']['records'][7]['record_type']
assert 20.0 == data['data']['records'][7]['value']
client.patch(
f'/api/activities/{activity_2_short_id}',
f'/api/workouts/{workout_2_short_id}',
content_type='application/json',
data=json.dumps(dict(sport_id=1)),
headers=dict(
@ -907,80 +907,80 @@ class TestGetRecords:
assert (
'Wed, 16 May 2018 16:05:00 GMT'
== data['data']['records'][0]['activity_date']
== data['data']['records'][0]['workout_date']
) # noqa
assert 'test' == data['data']['records'][0]['user']
assert sport_1_cycling.id == data['data']['records'][0]['sport_id']
assert activity_2_short_id == data['data']['records'][0]['activity_id']
assert workout_2_short_id == data['data']['records'][0]['workout_id']
assert 'AS' == data['data']['records'][0]['record_type']
assert 20.0 == data['data']['records'][0]['value']
assert (
'Wed, 16 May 2018 16:05:00 GMT'
== data['data']['records'][1]['activity_date']
== data['data']['records'][1]['workout_date']
) # noqa
assert 'test' == data['data']['records'][1]['user']
assert sport_1_cycling.id == data['data']['records'][1]['sport_id']
assert activity_2_short_id == data['data']['records'][1]['activity_id']
assert workout_2_short_id == data['data']['records'][1]['workout_id']
assert 'FD' == data['data']['records'][1]['record_type']
assert 20.0 == data['data']['records'][1]['value']
assert (
'Mon, 14 May 2018 14:05:00 GMT'
== data['data']['records'][2]['activity_date']
== data['data']['records'][2]['workout_date']
) # noqa
assert 'test' == data['data']['records'][2]['user']
assert sport_1_cycling.id == data['data']['records'][2]['sport_id']
assert activity_1_short_id == data['data']['records'][2]['activity_id']
assert workout_1_short_id == data['data']['records'][2]['workout_id']
assert 'LD' == data['data']['records'][2]['record_type']
assert '1:00:00' == data['data']['records'][2]['value']
assert (
'Wed, 16 May 2018 16:05:00 GMT'
== data['data']['records'][3]['activity_date']
== data['data']['records'][3]['workout_date']
) # noqa
assert 'test' == data['data']['records'][3]['user']
assert sport_1_cycling.id == data['data']['records'][3]['sport_id']
assert activity_2_short_id == data['data']['records'][3]['activity_id']
assert workout_2_short_id == data['data']['records'][3]['workout_id']
assert 'MS' == data['data']['records'][3]['record_type']
assert 20.0 == data['data']['records'][3]['value']
assert (
'Fri, 18 May 2018 18:05:00 GMT'
== data['data']['records'][4]['activity_date']
== data['data']['records'][4]['workout_date']
) # noqa
assert 'test' == data['data']['records'][4]['user']
assert sport_2_running.id == data['data']['records'][4]['sport_id']
assert activity_4_short_id == data['data']['records'][4]['activity_id']
assert workout_4_short_id == data['data']['records'][4]['workout_id']
assert 'AS' == data['data']['records'][4]['record_type']
assert 12.0 == data['data']['records'][4]['value']
assert (
'Fri, 18 May 2018 18:05:00 GMT'
== data['data']['records'][5]['activity_date']
== data['data']['records'][5]['workout_date']
) # noqa
assert 'test' == data['data']['records'][5]['user']
assert sport_2_running.id == data['data']['records'][5]['sport_id']
assert activity_4_short_id == data['data']['records'][5]['activity_id']
assert workout_4_short_id == data['data']['records'][5]['workout_id']
assert 'FD' == data['data']['records'][5]['record_type']
assert 10.0 == data['data']['records'][5]['value']
assert (
'Fri, 18 May 2018 18:05:00 GMT'
== data['data']['records'][6]['activity_date']
== data['data']['records'][6]['workout_date']
) # noqa
assert 'test' == data['data']['records'][6]['user']
assert sport_2_running.id == data['data']['records'][6]['sport_id']
assert activity_4_short_id == data['data']['records'][6]['activity_id']
assert workout_4_short_id == data['data']['records'][6]['workout_id']
assert 'LD' == data['data']['records'][6]['record_type']
assert '0:50:00' == data['data']['records'][6]['value']
assert (
'Fri, 18 May 2018 18:05:00 GMT'
== data['data']['records'][7]['activity_date']
== data['data']['records'][7]['workout_date']
) # noqa
assert 'test' == data['data']['records'][7]['user']
assert sport_2_running.id == data['data']['records'][7]['sport_id']
assert activity_4_short_id == data['data']['records'][7]['activity_id']
assert workout_4_short_id == data['data']['records'][7]['workout_id']
assert 'MS' == data['data']['records'][7]['record_type']
assert 12.0 == data['data']['records'][7]['value']

View File

@ -1,7 +1,7 @@
import datetime
from fittrackee.activities.models import Activity, Record, Sport
from fittrackee.users.models import User
from fittrackee.workouts.models import Record, Sport, Workout
from flask import Flask
@ -11,27 +11,27 @@ class TestRecordModel:
app: Flask,
user_1: User,
sport_1_cycling: Sport,
activity_cycling_user_1: Activity,
workout_cycling_user_1: Workout,
) -> None:
record_ld = Record.query.filter_by(
user_id=activity_cycling_user_1.user_id,
sport_id=activity_cycling_user_1.sport_id,
user_id=workout_cycling_user_1.user_id,
sport_id=workout_cycling_user_1.sport_id,
record_type='LD',
).first()
assert 'test' == record_ld.user.username
assert 1 == record_ld.sport_id
assert 1 == record_ld.activity_id
assert 1 == record_ld.workout_id
assert 'LD' == record_ld.record_type
assert '2018-01-01 00:00:00' == str(record_ld.activity_date)
assert '2018-01-01 00:00:00' == str(record_ld.workout_date)
assert '<Record Cycling - LD - 2018-01-01>' == str(record_ld)
record_serialize = record_ld.serialize()
assert 'id' in record_serialize
assert 'user' in record_serialize
assert 'sport_id' in record_serialize
assert 'activity_id' in record_serialize
assert 'workout_id' in record_serialize
assert 'record_type' in record_serialize
assert 'activity_date' in record_serialize
assert 'workout_date' in record_serialize
assert 'value' in record_serialize
def test_record_model_with_none_value(
@ -39,19 +39,19 @@ class TestRecordModel:
app: Flask,
user_1: User,
sport_1_cycling: Sport,
activity_cycling_user_1: Activity,
workout_cycling_user_1: Workout,
) -> None:
record_ld = Record.query.filter_by(
user_id=activity_cycling_user_1.user_id,
sport_id=activity_cycling_user_1.sport_id,
user_id=workout_cycling_user_1.user_id,
sport_id=workout_cycling_user_1.sport_id,
record_type='LD',
).first()
record_ld.value = None
assert 'test' == record_ld.user.username
assert 1 == record_ld.sport_id
assert 1 == record_ld.activity_id
assert 1 == record_ld.workout_id
assert 'LD' == record_ld.record_type
assert '2018-01-01 00:00:00' == str(record_ld.activity_date)
assert '2018-01-01 00:00:00' == str(record_ld.workout_date)
assert '<Record Cycling - LD - 2018-01-01>' == str(record_ld)
assert record_ld.value is None
@ -63,11 +63,11 @@ class TestRecordModel:
app: Flask,
user_1: User,
sport_1_cycling: Sport,
activity_cycling_user_1: Activity,
workout_cycling_user_1: Workout,
) -> None:
record_as = Record.query.filter_by(
user_id=activity_cycling_user_1.user_id,
sport_id=activity_cycling_user_1.sport_id,
user_id=workout_cycling_user_1.user_id,
sport_id=workout_cycling_user_1.sport_id,
record_type='AS',
).first()
@ -84,11 +84,11 @@ class TestRecordModel:
app: Flask,
user_1: User,
sport_1_cycling: Sport,
activity_cycling_user_1: Activity,
workout_cycling_user_1: Workout,
) -> None:
record_fd = Record.query.filter_by(
user_id=activity_cycling_user_1.user_id,
sport_id=activity_cycling_user_1.sport_id,
user_id=workout_cycling_user_1.user_id,
sport_id=workout_cycling_user_1.sport_id,
record_type='FD',
).first()
@ -105,11 +105,11 @@ class TestRecordModel:
app: Flask,
user_1: User,
sport_1_cycling: Sport,
activity_cycling_user_1: Activity,
workout_cycling_user_1: Workout,
) -> None:
record_ld = Record.query.filter_by(
user_id=activity_cycling_user_1.user_id,
sport_id=activity_cycling_user_1.sport_id,
user_id=workout_cycling_user_1.user_id,
sport_id=workout_cycling_user_1.sport_id,
record_type='LD',
).first()
@ -126,11 +126,11 @@ class TestRecordModel:
app: Flask,
user_1: User,
sport_1_cycling: Sport,
activity_cycling_user_1: Activity,
workout_cycling_user_1: Workout,
) -> None:
record_ld = Record.query.filter_by(
user_id=activity_cycling_user_1.user_id,
sport_id=activity_cycling_user_1.sport_id,
user_id=workout_cycling_user_1.user_id,
sport_id=workout_cycling_user_1.sport_id,
record_type='LD',
).first()
record_ld.value = datetime.timedelta(seconds=0)
@ -148,11 +148,11 @@ class TestRecordModel:
app: Flask,
user_1: User,
sport_1_cycling: Sport,
activity_cycling_user_1: Activity,
workout_cycling_user_1: Workout,
) -> None:
record_ms = Record.query.filter_by(
user_id=activity_cycling_user_1.user_id,
sport_id=activity_cycling_user_1.sport_id,
user_id=workout_cycling_user_1.user_id,
sport_id=workout_cycling_user_1.sport_id,
record_type='MS',
).first()

View File

@ -1,7 +1,7 @@
import json
from fittrackee.activities.models import Activity, Sport
from fittrackee.users.models import User
from fittrackee.workouts.models import Sport, Workout
from flask import Flask
expected_sport_1_cycling_result = {
@ -11,7 +11,7 @@ expected_sport_1_cycling_result = {
'is_active': True,
}
expected_sport_1_cycling_admin_result = expected_sport_1_cycling_result.copy()
expected_sport_1_cycling_admin_result['has_activities'] = False
expected_sport_1_cycling_admin_result['has_workouts'] = False
expected_sport_2_running_result = {
'id': 2,
@ -20,7 +20,7 @@ expected_sport_2_running_result = {
'is_active': True,
}
expected_sport_2_running_admin_result = expected_sport_2_running_result.copy()
expected_sport_2_running_admin_result['has_activities'] = False
expected_sport_2_running_admin_result['has_workouts'] = False
expected_sport_1_cycling_inactive_result = {
'id': 1,
@ -31,7 +31,7 @@ expected_sport_1_cycling_inactive_result = {
expected_sport_1_cycling_inactive_admin_result = (
expected_sport_1_cycling_inactive_result.copy()
)
expected_sport_1_cycling_inactive_admin_result['has_activities'] = False
expected_sport_1_cycling_inactive_admin_result['has_workouts'] = False
class TestGetSports:
@ -266,7 +266,7 @@ class TestUpdateSport:
assert 'success' in data['status']
assert len(data['data']['sports']) == 1
assert data['data']['sports'][0]['is_active'] is False
assert data['data']['sports'][0]['has_activities'] is False
assert data['data']['sports'][0]['has_workouts'] is False
def test_it_enables_a_sport(
self, app: Flask, user_1_admin: User, sport_1_cycling: Sport
@ -296,14 +296,14 @@ class TestUpdateSport:
assert 'success' in data['status']
assert len(data['data']['sports']) == 1
assert data['data']['sports'][0]['is_active'] is True
assert data['data']['sports'][0]['has_activities'] is False
assert data['data']['sports'][0]['has_workouts'] is False
def test_it_disables_a_sport_with_activities(
def test_it_disables_a_sport_with_workouts(
self,
app: Flask,
user_1_admin: User,
sport_1_cycling: Sport,
activity_cycling_user_1: Activity,
workout_cycling_user_1: Workout,
) -> None:
client = app.test_client()
resp_login = client.post(
@ -329,14 +329,14 @@ class TestUpdateSport:
assert 'success' in data['status']
assert len(data['data']['sports']) == 1
assert data['data']['sports'][0]['is_active'] is False
assert data['data']['sports'][0]['has_activities'] is True
assert data['data']['sports'][0]['has_workouts'] is True
def test_it_enables_a_sport_with_activities(
def test_it_enables_a_sport_with_workouts(
self,
app: Flask,
user_1_admin: User,
sport_1_cycling: Sport,
activity_cycling_user_1: Activity,
workout_cycling_user_1: Workout,
) -> None:
sport_1_cycling.is_active = False
client = app.test_client()
@ -363,7 +363,7 @@ class TestUpdateSport:
assert 'success' in data['status']
assert len(data['data']['sports']) == 1
assert data['data']['sports'][0]['is_active'] is True
assert data['data']['sports'][0]['has_activities'] is True
assert data['data']['sports'][0]['has_workouts'] is True
def test_returns_error_if_user_has_no_admin_rights(
self, app: Flask, user_1: User, sport_1_cycling: Sport

View File

@ -1,7 +1,7 @@
from typing import Dict, Optional
from fittrackee.activities.models import Activity, Sport
from fittrackee.users.models import User
from fittrackee.workouts.models import Sport, Workout
from flask import Flask
@ -22,24 +22,24 @@ class TestSportModel:
def test_sport_model(self, app: Flask, sport_1_cycling: Sport) -> None:
serialized_sport = self.assert_sport_model(sport_1_cycling)
assert 'has_activities' not in serialized_sport
assert 'has_workouts' not in serialized_sport
def test_sport_model_with_activity(
def test_sport_model_with_workout(
self,
app: Flask,
sport_1_cycling: Sport,
user_1: User,
activity_cycling_user_1: Activity,
workout_cycling_user_1: Workout,
) -> None:
serialized_sport = self.assert_sport_model(sport_1_cycling)
assert 'has_activities' not in serialized_sport
assert 'has_workouts' not in serialized_sport
def test_sport_model_with_activity_as_admin(
def test_sport_model_with_workout_as_admin(
self,
app: Flask,
sport_1_cycling: Sport,
user_1: User,
activity_cycling_user_1: Activity,
workout_cycling_user_1: Workout,
) -> None:
serialized_sport = self.assert_sport_model(sport_1_cycling, True)
assert serialized_sport['has_activities'] is True
assert serialized_sport['has_workouts'] is True

View File

@ -1,12 +1,12 @@
import json
from fittrackee.activities.models import Activity, Sport
from fittrackee.users.models import User
from fittrackee.workouts.models import Sport, Workout
from flask import Flask
class TestGetStatsByTime:
def test_it_gets_no_stats_when_user_has_no_activities(
def test_it_gets_no_stats_when_user_has_no_workouts(
self, app: Flask, user_1: User
) -> None:
client = app.test_client()
@ -58,8 +58,8 @@ class TestGetStatsByTime:
user_1: User,
sport_1_cycling: Sport,
sport_2_running: Sport,
seven_activities_user_1: Activity,
activity_running_user_1: Activity,
seven_workouts_user_1: Workout,
workout_running_user_1: Workout,
) -> None:
client = app.test_client()
resp_login = client.post(
@ -90,8 +90,8 @@ class TestGetStatsByTime:
user_1: User,
sport_1_cycling: Sport,
sport_2_running: Sport,
seven_activities_user_1: Activity,
activity_running_user_1: Activity,
seven_workouts_user_1: Workout,
workout_running_user_1: Workout,
) -> None:
client = app.test_client()
resp_login = client.post(
@ -113,14 +113,14 @@ class TestGetStatsByTime:
assert 'fail' in data['status']
assert 'Invalid time period.' in data['message']
def test_it_gets_stats_by_time_all_activities(
def test_it_gets_stats_by_time_all_workouts(
self,
app: Flask,
user_1: User,
sport_1_cycling: Sport,
sport_2_running: Sport,
seven_activities_user_1: Activity,
activity_running_user_1: Activity,
seven_workouts_user_1: Workout,
workout_running_user_1: Workout,
) -> None:
client = app.test_client()
resp_login = client.post(
@ -143,19 +143,19 @@ class TestGetStatsByTime:
assert data['data']['statistics'] == {
'2017': {
'1': {
'nb_activities': 2,
'nb_workouts': 2,
'total_distance': 15.0,
'total_duration': 4480,
}
},
'2018': {
'1': {
'nb_activities': 5,
'nb_workouts': 5,
'total_distance': 39.0,
'total_duration': 11624,
},
'2': {
'nb_activities': 1,
'nb_workouts': 1,
'total_distance': 12.0,
'total_duration': 6000,
},
@ -168,8 +168,8 @@ class TestGetStatsByTime:
user_1: User,
sport_1_cycling: Sport,
sport_2_running: Sport,
seven_activities_user_1: Activity,
activity_running_user_1: Activity,
seven_workouts_user_1: Workout,
workout_running_user_1: Workout,
) -> None:
client = app.test_client()
resp_login = client.post(
@ -192,12 +192,12 @@ class TestGetStatsByTime:
assert data['data']['statistics'] == {
'2018': {
'1': {
'nb_activities': 1,
'nb_workouts': 1,
'total_distance': 8.0,
'total_duration': 6000,
},
'2': {
'nb_activities': 1,
'nb_workouts': 1,
'total_distance': 12.0,
'total_duration': 6000,
},
@ -210,8 +210,8 @@ class TestGetStatsByTime:
user_1_paris: User,
sport_1_cycling: Sport,
sport_2_running: Sport,
seven_activities_user_1: Activity,
activity_running_user_1: Activity,
seven_workouts_user_1: Workout,
workout_running_user_1: Workout,
) -> None:
client = app.test_client()
resp_login = client.post(
@ -235,12 +235,12 @@ class TestGetStatsByTime:
assert data['data']['statistics'] == {
'2018': {
'1': {
'nb_activities': 1,
'nb_workouts': 1,
'total_distance': 8.0,
'total_duration': 6000,
},
'2': {
'nb_activities': 1,
'nb_workouts': 1,
'total_distance': 12.0,
'total_duration': 6000,
},
@ -253,8 +253,8 @@ class TestGetStatsByTime:
user_1: User,
sport_1_cycling: Sport,
sport_2_running: Sport,
seven_activities_user_1: Activity,
activity_running_user_1: Activity,
seven_workouts_user_1: Workout,
workout_running_user_1: Workout,
) -> None:
client = app.test_client()
resp_login = client.post(
@ -277,19 +277,19 @@ class TestGetStatsByTime:
assert data['data']['statistics'] == {
'2017': {
'1': {
'nb_activities': 2,
'nb_workouts': 2,
'total_distance': 15.0,
'total_duration': 4480,
}
},
'2018': {
'1': {
'nb_activities': 5,
'nb_workouts': 5,
'total_distance': 39.0,
'total_duration': 11624,
},
'2': {
'nb_activities': 1,
'nb_workouts': 1,
'total_distance': 12.0,
'total_duration': 6000,
},
@ -302,8 +302,8 @@ class TestGetStatsByTime:
user_1: User,
sport_1_cycling: Sport,
sport_2_running: Sport,
seven_activities_user_1: Activity,
activity_running_user_1: Activity,
seven_workouts_user_1: Workout,
workout_running_user_1: Workout,
) -> None:
client = app.test_client()
resp_login = client.post(
@ -326,12 +326,12 @@ class TestGetStatsByTime:
assert data['data']['statistics'] == {
'2018': {
'1': {
'nb_activities': 1,
'nb_workouts': 1,
'total_distance': 8.0,
'total_duration': 6000,
},
'2': {
'nb_activities': 1,
'nb_workouts': 1,
'total_distance': 12.0,
'total_duration': 6000,
},
@ -344,8 +344,8 @@ class TestGetStatsByTime:
user_1_paris: User,
sport_1_cycling: Sport,
sport_2_running: Sport,
seven_activities_user_1: Activity,
activity_running_user_1: Activity,
seven_workouts_user_1: Workout,
workout_running_user_1: Workout,
) -> None:
client = app.test_client()
resp_login = client.post(
@ -368,12 +368,12 @@ class TestGetStatsByTime:
assert data['data']['statistics'] == {
'2018': {
'1': {
'nb_activities': 1,
'nb_workouts': 1,
'total_distance': 8.0,
'total_duration': 6000,
},
'2': {
'nb_activities': 1,
'nb_workouts': 1,
'total_distance': 12.0,
'total_duration': 6000,
},
@ -386,8 +386,8 @@ class TestGetStatsByTime:
user_1: User,
sport_1_cycling: Sport,
sport_2_running: Sport,
seven_activities_user_1: Activity,
activity_running_user_1: Activity,
seven_workouts_user_1: Workout,
workout_running_user_1: Workout,
) -> None:
client = app.test_client()
resp_login = client.post(
@ -410,47 +410,47 @@ class TestGetStatsByTime:
assert data['data']['statistics'] == {
'2017-03': {
'1': {
'nb_activities': 1,
'nb_workouts': 1,
'total_distance': 5.0,
'total_duration': 1024,
}
},
'2017-06': {
'1': {
'nb_activities': 1,
'nb_workouts': 1,
'total_distance': 10.0,
'total_duration': 3456,
}
},
'2018-01': {
'1': {
'nb_activities': 1,
'nb_workouts': 1,
'total_distance': 10.0,
'total_duration': 1024,
}
},
'2018-02': {
'1': {
'nb_activities': 2,
'nb_workouts': 2,
'total_distance': 11.0,
'total_duration': 1600,
}
},
'2018-04': {
'1': {
'nb_activities': 1,
'nb_workouts': 1,
'total_distance': 8.0,
'total_duration': 6000,
},
'2': {
'nb_activities': 1,
'nb_workouts': 1,
'total_distance': 12.0,
'total_duration': 6000,
},
},
'2018-05': {
'1': {
'nb_activities': 1,
'nb_workouts': 1,
'total_distance': 10.0,
'total_duration': 3000,
}
@ -463,8 +463,8 @@ class TestGetStatsByTime:
user_1_full: User,
sport_1_cycling: Sport,
sport_2_running: Sport,
seven_activities_user_1: Activity,
activity_running_user_1: Activity,
seven_workouts_user_1: Workout,
workout_running_user_1: Workout,
) -> None:
client = app.test_client()
resp_login = client.post(
@ -487,47 +487,47 @@ class TestGetStatsByTime:
assert data['data']['statistics'] == {
'2017-03': {
'1': {
'nb_activities': 1,
'nb_workouts': 1,
'total_distance': 5.0,
'total_duration': 1024,
}
},
'2017-06': {
'1': {
'nb_activities': 1,
'nb_workouts': 1,
'total_distance': 10.0,
'total_duration': 3456,
}
},
'2018-01': {
'1': {
'nb_activities': 1,
'nb_workouts': 1,
'total_distance': 10.0,
'total_duration': 1024,
}
},
'2018-02': {
'1': {
'nb_activities': 2,
'nb_workouts': 2,
'total_distance': 11.0,
'total_duration': 1600,
}
},
'2018-04': {
'1': {
'nb_activities': 1,
'nb_workouts': 1,
'total_distance': 8.0,
'total_duration': 6000,
},
'2': {
'nb_activities': 1,
'nb_workouts': 1,
'total_distance': 12.0,
'total_duration': 6000,
},
},
'2018-05': {
'1': {
'nb_activities': 1,
'nb_workouts': 1,
'total_distance': 10.0,
'total_duration': 3000,
}
@ -540,8 +540,8 @@ class TestGetStatsByTime:
user_1: User,
sport_1_cycling: Sport,
sport_2_running: Sport,
seven_activities_user_1: Activity,
activity_running_user_1: Activity,
seven_workouts_user_1: Workout,
workout_running_user_1: Workout,
) -> None:
client = app.test_client()
resp_login = client.post(
@ -564,12 +564,12 @@ class TestGetStatsByTime:
assert data['data']['statistics'] == {
'2018-04': {
'1': {
'nb_activities': 1,
'nb_workouts': 1,
'total_distance': 8.0,
'total_duration': 6000,
},
'2': {
'nb_activities': 1,
'nb_workouts': 1,
'total_distance': 12.0,
'total_duration': 6000,
},
@ -582,8 +582,8 @@ class TestGetStatsByTime:
user_1_full: User,
sport_1_cycling: Sport,
sport_2_running: Sport,
seven_activities_user_1: Activity,
activity_running_user_1: Activity,
seven_workouts_user_1: Workout,
workout_running_user_1: Workout,
) -> None:
client = app.test_client()
resp_login = client.post(
@ -606,47 +606,47 @@ class TestGetStatsByTime:
assert data['data']['statistics'] == {
'2017-03-19': {
'1': {
'nb_activities': 1,
'nb_workouts': 1,
'total_distance': 5.0,
'total_duration': 1024,
}
},
'2017-05-28': {
'1': {
'nb_activities': 1,
'nb_workouts': 1,
'total_distance': 10.0,
'total_duration': 3456,
}
},
'2017-12-31': {
'1': {
'nb_activities': 1,
'nb_workouts': 1,
'total_distance': 10.0,
'total_duration': 1024,
}
},
'2018-02-18': {
'1': {
'nb_activities': 2,
'nb_workouts': 2,
'total_distance': 11.0,
'total_duration': 1600,
}
},
'2018-04-01': {
'1': {
'nb_activities': 1,
'nb_workouts': 1,
'total_distance': 8.0,
'total_duration': 6000,
},
'2': {
'nb_activities': 1,
'nb_workouts': 1,
'total_distance': 12.0,
'total_duration': 6000,
},
},
'2018-05-06': {
'1': {
'nb_activities': 1,
'nb_workouts': 1,
'total_distance': 10.0,
'total_duration': 3000,
}
@ -659,8 +659,8 @@ class TestGetStatsByTime:
user_1: User,
sport_1_cycling: Sport,
sport_2_running: Sport,
seven_activities_user_1: Activity,
activity_running_user_1: Activity,
seven_workouts_user_1: Workout,
workout_running_user_1: Workout,
) -> None:
client = app.test_client()
resp_login = client.post(
@ -683,12 +683,12 @@ class TestGetStatsByTime:
assert data['data']['statistics'] == {
'2018-04-01': {
'1': {
'nb_activities': 1,
'nb_workouts': 1,
'total_distance': 8.0,
'total_duration': 6000,
},
'2': {
'nb_activities': 1,
'nb_workouts': 1,
'total_distance': 12.0,
'total_duration': 6000,
},
@ -701,8 +701,8 @@ class TestGetStatsByTime:
user_1: User,
sport_1_cycling: Sport,
sport_2_running: Sport,
seven_activities_user_1: Activity,
activity_running_user_1: Activity,
seven_workouts_user_1: Workout,
workout_running_user_1: Workout,
) -> None:
client = app.test_client()
resp_login = client.post(
@ -725,47 +725,47 @@ class TestGetStatsByTime:
assert data['data']['statistics'] == {
'2017-03-20': {
'1': {
'nb_activities': 1,
'nb_workouts': 1,
'total_distance': 5.0,
'total_duration': 1024,
}
},
'2017-05-29': {
'1': {
'nb_activities': 1,
'nb_workouts': 1,
'total_distance': 10.0,
'total_duration': 3456,
}
},
'2018-01-01': {
'1': {
'nb_activities': 1,
'nb_workouts': 1,
'total_distance': 10.0,
'total_duration': 1024,
}
},
'2018-02-19': {
'1': {
'nb_activities': 2,
'nb_workouts': 2,
'total_distance': 11.0,
'total_duration': 1600,
}
},
'2018-03-26': {
'1': {
'nb_activities': 1,
'nb_workouts': 1,
'total_distance': 8.0,
'total_duration': 6000,
},
'2': {
'nb_activities': 1,
'nb_workouts': 1,
'total_distance': 12.0,
'total_duration': 6000,
},
},
'2018-05-07': {
'1': {
'nb_activities': 1,
'nb_workouts': 1,
'total_distance': 10.0,
'total_duration': 3000,
}
@ -778,8 +778,8 @@ class TestGetStatsByTime:
user_1: User,
sport_1_cycling: Sport,
sport_2_running: Sport,
seven_activities_user_1: Activity,
activity_running_user_1: Activity,
seven_workouts_user_1: Workout,
workout_running_user_1: Workout,
) -> None:
client = app.test_client()
resp_login = client.post(
@ -802,12 +802,12 @@ class TestGetStatsByTime:
assert data['data']['statistics'] == {
'2018-03-26': {
'1': {
'nb_activities': 1,
'nb_workouts': 1,
'total_distance': 8.0,
'total_duration': 6000,
},
'2': {
'nb_activities': 1,
'nb_workouts': 1,
'total_distance': 12.0,
'total_duration': 6000,
},
@ -822,8 +822,8 @@ class TestGetStatsBySport:
user_1: User,
sport_1_cycling: Sport,
sport_2_running: Sport,
seven_activities_user_1: Activity,
activity_running_user_1: Activity,
seven_workouts_user_1: Workout,
workout_running_user_1: Workout,
) -> None:
client = app.test_client()
resp_login = client.post(
@ -845,12 +845,12 @@ class TestGetStatsBySport:
assert 'success' in data['status']
assert data['data']['statistics'] == {
'1': {
'nb_activities': 7,
'nb_workouts': 7,
'total_distance': 54.0,
'total_duration': 16104,
},
'2': {
'nb_activities': 1,
'nb_workouts': 1,
'total_distance': 12.0,
'total_duration': 6000,
},
@ -862,8 +862,8 @@ class TestGetStatsBySport:
user_1: User,
sport_1_cycling: Sport,
sport_2_running: Sport,
seven_activities_user_1: Activity,
activity_running_user_1: Activity,
seven_workouts_user_1: Workout,
workout_running_user_1: Workout,
) -> None:
client = app.test_client()
resp_login = client.post(
@ -885,7 +885,7 @@ class TestGetStatsBySport:
assert 'success' in data['status']
assert data['data']['statistics'] == {
'1': {
'nb_activities': 7,
'nb_workouts': 7,
'total_distance': 54.0,
'total_duration': 16104,
}
@ -897,8 +897,8 @@ class TestGetStatsBySport:
user_1: User,
sport_1_cycling: Sport,
sport_2_running: Sport,
seven_activities_user_1: Activity,
activity_running_user_1: Activity,
seven_workouts_user_1: Workout,
workout_running_user_1: Workout,
) -> None:
client = app.test_client()
resp_login = client.post(
@ -926,8 +926,8 @@ class TestGetStatsBySport:
user_1: User,
sport_1_cycling: Sport,
sport_2_running: Sport,
seven_activities_user_1: Activity,
activity_running_user_1: Activity,
seven_workouts_user_1: Workout,
workout_running_user_1: Workout,
) -> None:
client = app.test_client()
resp_login = client.post(
@ -955,8 +955,8 @@ class TestGetStatsBySport:
user_1: User,
sport_1_cycling: Sport,
sport_2_running: Sport,
seven_activities_user_1: Activity,
activity_running_user_1: Activity,
seven_workouts_user_1: Workout,
workout_running_user_1: Workout,
) -> None:
client = app.test_client()
resp_login = client.post(
@ -983,7 +983,7 @@ class TestGetStatsBySport:
class TestGetAllStats:
def test_it_returns_all_stats_when_users_have_no_activities(
def test_it_returns_all_stats_when_users_have_no_workouts(
self, app: Flask, user_1_admin: User, user_2: User
) -> None:
client = app.test_client()
@ -1006,12 +1006,12 @@ class TestGetAllStats:
data = json.loads(response.data.decode())
assert response.status_code == 200
assert 'success' in data['status']
assert data['data']['activities'] == 0
assert data['data']['workouts'] == 0
assert data['data']['sports'] == 0
assert data['data']['users'] == 2
assert 'uploads_dir_size' in data['data']
def test_it_gets_app_all_stats_with_activities(
def test_it_gets_app_all_stats_with_workouts(
self,
app: Flask,
user_1_admin: User,
@ -1019,9 +1019,9 @@ class TestGetAllStats:
user_3: User,
sport_1_cycling: Sport,
sport_2_running: Sport,
activity_cycling_user_1: Activity,
activity_cycling_user_2: Activity,
activity_running_user_1: Activity,
workout_cycling_user_1: Workout,
workout_cycling_user_2: Workout,
workout_running_user_1: Workout,
) -> None:
client = app.test_client()
resp_login = client.post(
@ -1043,7 +1043,7 @@ class TestGetAllStats:
data = json.loads(response.data.decode())
assert response.status_code == 200
assert 'success' in data['status']
assert data['data']['activities'] == 3
assert data['data']['workouts'] == 3
assert data['data']['sports'] == 2
assert data['data']['users'] == 3
assert 'uploads_dir_size' in data['data']
@ -1056,9 +1056,9 @@ class TestGetAllStats:
user_3: User,
sport_1_cycling: Sport,
sport_2_running: Sport,
activity_cycling_user_1: Activity,
activity_cycling_user_2: Activity,
activity_running_user_1: Activity,
workout_cycling_user_1: Workout,
workout_cycling_user_2: Workout,
workout_running_user_1: Workout,
) -> None:
client = app.test_client()
resp_login = client.post(

View File

@ -1,59 +1,59 @@
import json
from typing import Dict
from fittrackee.activities.models import Activity, Sport
from fittrackee.activities.utils_id import decode_short_id
from fittrackee.users.models import User
from fittrackee.workouts.models import Sport, Workout
from fittrackee.workouts.utils_id import decode_short_id
from flask import Flask
from .utils import get_random_short_id, post_an_activity
from .utils import get_random_short_id, post_an_workout
def assert_activity_data_with_gpx(data: Dict, sport_id: int) -> None:
assert 'creation_date' in data['data']['activities'][0]
def assert_workout_data_with_gpx(data: Dict, sport_id: int) -> None:
assert 'creation_date' in data['data']['workouts'][0]
assert (
'Tue, 13 Mar 2018 12:44:45 GMT'
== data['data']['activities'][0]['activity_date']
== data['data']['workouts'][0]['workout_date']
)
assert 'test' == data['data']['activities'][0]['user']
assert '0:04:10' == data['data']['activities'][0]['duration']
assert data['data']['activities'][0]['ascent'] == 0.4
assert data['data']['activities'][0]['ave_speed'] == 4.61
assert data['data']['activities'][0]['descent'] == 23.4
assert data['data']['activities'][0]['distance'] == 0.32
assert data['data']['activities'][0]['max_alt'] == 998.0
assert data['data']['activities'][0]['max_speed'] == 5.12
assert data['data']['activities'][0]['min_alt'] == 975.0
assert data['data']['activities'][0]['moving'] == '0:04:10'
assert data['data']['activities'][0]['pauses'] is None
assert data['data']['activities'][0]['with_gpx'] is True
assert 'test' == data['data']['workouts'][0]['user']
assert '0:04:10' == data['data']['workouts'][0]['duration']
assert data['data']['workouts'][0]['ascent'] == 0.4
assert data['data']['workouts'][0]['ave_speed'] == 4.61
assert data['data']['workouts'][0]['descent'] == 23.4
assert data['data']['workouts'][0]['distance'] == 0.32
assert data['data']['workouts'][0]['max_alt'] == 998.0
assert data['data']['workouts'][0]['max_speed'] == 5.12
assert data['data']['workouts'][0]['min_alt'] == 975.0
assert data['data']['workouts'][0]['moving'] == '0:04:10'
assert data['data']['workouts'][0]['pauses'] is None
assert data['data']['workouts'][0]['with_gpx'] is True
records = data['data']['activities'][0]['records']
records = data['data']['workouts'][0]['records']
assert len(records) == 4
assert records[0]['sport_id'] == sport_id
assert records[0]['activity_id'] == data['data']['activities'][0]['id']
assert records[0]['workout_id'] == data['data']['workouts'][0]['id']
assert records[0]['record_type'] == 'MS'
assert records[0]['activity_date'] == 'Tue, 13 Mar 2018 12:44:45 GMT'
assert records[0]['workout_date'] == 'Tue, 13 Mar 2018 12:44:45 GMT'
assert records[0]['value'] == 5.12
assert records[1]['sport_id'] == sport_id
assert records[1]['activity_id'] == data['data']['activities'][0]['id']
assert records[1]['workout_id'] == data['data']['workouts'][0]['id']
assert records[1]['record_type'] == 'LD'
assert records[1]['activity_date'] == 'Tue, 13 Mar 2018 12:44:45 GMT'
assert records[1]['workout_date'] == 'Tue, 13 Mar 2018 12:44:45 GMT'
assert records[1]['value'] == '0:04:10'
assert records[2]['sport_id'] == sport_id
assert records[2]['activity_id'] == data['data']['activities'][0]['id']
assert records[2]['workout_id'] == data['data']['workouts'][0]['id']
assert records[2]['record_type'] == 'FD'
assert records[2]['activity_date'] == 'Tue, 13 Mar 2018 12:44:45 GMT'
assert records[2]['workout_date'] == 'Tue, 13 Mar 2018 12:44:45 GMT'
assert records[2]['value'] == 0.32
assert records[3]['sport_id'] == sport_id
assert records[3]['activity_id'] == data['data']['activities'][0]['id']
assert records[3]['workout_id'] == data['data']['workouts'][0]['id']
assert records[3]['record_type'] == 'AS'
assert records[3]['activity_date'] == 'Tue, 13 Mar 2018 12:44:45 GMT'
assert records[3]['workout_date'] == 'Tue, 13 Mar 2018 12:44:45 GMT'
assert records[3]['value'] == 4.61
class TestEditActivityWithGpx:
def test_it_updates_title_for_an_activity_with_gpx(
class TestEditWorkoutWithGpx:
def test_it_updates_title_for_an_workout_with_gpx(
self,
app: Flask,
user_1: User,
@ -61,25 +61,25 @@ class TestEditActivityWithGpx:
sport_2_running: Sport,
gpx_file: str,
) -> None:
token, activity_short_id = post_an_activity(app, gpx_file)
token, workout_short_id = post_an_workout(app, gpx_file)
client = app.test_client()
response = client.patch(
f'/api/activities/{activity_short_id}',
f'/api/workouts/{workout_short_id}',
content_type='application/json',
data=json.dumps(dict(sport_id=2, title="Activity test")),
data=json.dumps(dict(sport_id=2, title="Workout test")),
headers=dict(Authorization=f'Bearer {token}'),
)
data = json.loads(response.data.decode())
assert response.status_code == 200
assert 'success' in data['status']
assert len(data['data']['activities']) == 1
assert sport_2_running.id == data['data']['activities'][0]['sport_id']
assert data['data']['activities'][0]['title'] == 'Activity test'
assert_activity_data_with_gpx(data, sport_2_running.id)
assert len(data['data']['workouts']) == 1
assert sport_2_running.id == data['data']['workouts'][0]['sport_id']
assert data['data']['workouts'][0]['title'] == 'Workout test'
assert_workout_data_with_gpx(data, sport_2_running.id)
def test_it_adds_notes_for_an_activity_with_gpx(
def test_it_adds_notes_for_an_workout_with_gpx(
self,
app: Flask,
user_1: User,
@ -87,11 +87,11 @@ class TestEditActivityWithGpx:
sport_2_running: Sport,
gpx_file: str,
) -> None:
token, activity_short_id = post_an_activity(app, gpx_file)
token, workout_short_id = post_an_workout(app, gpx_file)
client = app.test_client()
response = client.patch(
f'/api/activities/{activity_short_id}',
f'/api/workouts/{workout_short_id}',
content_type='application/json',
data=json.dumps(dict(notes="test notes")),
headers=dict(Authorization=f'Bearer {token}'),
@ -100,11 +100,11 @@ class TestEditActivityWithGpx:
data = json.loads(response.data.decode())
assert response.status_code == 200
assert 'success' in data['status']
assert len(data['data']['activities']) == 1
assert data['data']['activities'][0]['title'] == 'just an activity'
assert data['data']['activities'][0]['notes'] == 'test notes'
assert len(data['data']['workouts']) == 1
assert data['data']['workouts'][0]['title'] == 'just a workout'
assert data['data']['workouts'][0]['notes'] == 'test notes'
def test_it_raises_403_when_editing_an_activity_from_different_user(
def test_it_raises_403_when_editing_an_workout_from_different_user(
self,
app: Flask,
user_1: User,
@ -113,7 +113,7 @@ class TestEditActivityWithGpx:
sport_2_running: Sport,
gpx_file: str,
) -> None:
_, activity_short_id = post_an_activity(app, gpx_file)
_, workout_short_id = post_an_workout(app, gpx_file)
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -122,9 +122,9 @@ class TestEditActivityWithGpx:
)
response = client.patch(
f'/api/activities/{activity_short_id}',
f'/api/workouts/{workout_short_id}',
content_type='application/json',
data=json.dumps(dict(sport_id=2, title="Activity test")),
data=json.dumps(dict(sport_id=2, title="Workout test")),
headers=dict(
Authorization='Bearer '
+ json.loads(resp_login.data.decode())['auth_token']
@ -144,11 +144,11 @@ class TestEditActivityWithGpx:
sport_2_running: Sport,
gpx_file: str,
) -> None:
token, activity_short_id = post_an_activity(app, gpx_file)
token, workout_short_id = post_an_workout(app, gpx_file)
client = app.test_client()
response = client.patch(
f'/api/activities/{activity_short_id}',
f'/api/workouts/{workout_short_id}',
content_type='application/json',
data=json.dumps(dict(sport_id=2)),
headers=dict(Authorization=f'Bearer {token}'),
@ -157,19 +157,19 @@ class TestEditActivityWithGpx:
data = json.loads(response.data.decode())
assert response.status_code == 200
assert 'success' in data['status']
assert len(data['data']['activities']) == 1
assert sport_2_running.id == data['data']['activities'][0]['sport_id']
assert data['data']['activities'][0]['title'] == 'just an activity'
assert_activity_data_with_gpx(data, sport_2_running.id)
assert len(data['data']['workouts']) == 1
assert sport_2_running.id == data['data']['workouts'][0]['sport_id']
assert data['data']['workouts'][0]['title'] == 'just a workout'
assert_workout_data_with_gpx(data, sport_2_running.id)
def test_it_returns_400_if_payload_is_empty(
self, app: Flask, user_1: User, sport_1_cycling: Sport, gpx_file: str
) -> None:
token, activity_short_id = post_an_activity(app, gpx_file)
token, workout_short_id = post_an_workout(app, gpx_file)
client = app.test_client()
response = client.patch(
f'/api/activities/{activity_short_id}',
f'/api/workouts/{workout_short_id}',
content_type='application/json',
data=json.dumps(dict()),
headers=dict(Authorization=f'Bearer {token}'),
@ -183,11 +183,11 @@ class TestEditActivityWithGpx:
def test_it_raises_500_if_sport_does_not_exists(
self, app: Flask, user_1: User, sport_1_cycling: Sport, gpx_file: str
) -> None:
token, activity_short_id = post_an_activity(app, gpx_file)
token, workout_short_id = post_an_workout(app, gpx_file)
client = app.test_client()
response = client.patch(
f'/api/activities/{activity_short_id}',
f'/api/workouts/{workout_short_id}',
content_type='application/json',
data=json.dumps(dict(sport_id=2)),
headers=dict(Authorization=f'Bearer {token}'),
@ -202,16 +202,16 @@ class TestEditActivityWithGpx:
)
class TestEditActivityWithoutGpx:
def test_it_updates_an_activity_wo_gpx(
class TestEditWorkoutWithoutGpx:
def test_it_updates_an_workout_wo_gpx(
self,
app: Flask,
user_1: User,
sport_1_cycling: Sport,
sport_2_running: Sport,
activity_cycling_user_1: Activity,
workout_cycling_user_1: Workout,
) -> None:
activity_short_id = activity_cycling_user_1.short_id
workout_short_id = workout_cycling_user_1.short_id
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -220,15 +220,15 @@ class TestEditActivityWithoutGpx:
)
response = client.patch(
f'/api/activities/{activity_short_id}',
f'/api/workouts/{workout_short_id}',
content_type='application/json',
data=json.dumps(
dict(
sport_id=2,
duration=3600,
activity_date='2018-05-15 15:05',
workout_date='2018-05-15 15:05',
distance=8,
title='Activity test',
title='Workout test',
)
),
headers=dict(
@ -241,62 +241,62 @@ class TestEditActivityWithoutGpx:
assert response.status_code == 200
assert 'success' in data['status']
assert len(data['data']['activities']) == 1
assert 'creation_date' in data['data']['activities'][0]
assert len(data['data']['workouts']) == 1
assert 'creation_date' in data['data']['workouts'][0]
assert (
data['data']['activities'][0]['activity_date']
data['data']['workouts'][0]['workout_date']
== 'Tue, 15 May 2018 15:05:00 GMT'
)
assert data['data']['activities'][0]['user'] == 'test'
assert data['data']['activities'][0]['sport_id'] == sport_2_running.id
assert data['data']['activities'][0]['duration'] == '1:00:00'
assert data['data']['activities'][0]['title'] == 'Activity test'
assert data['data']['activities'][0]['ascent'] is None
assert data['data']['activities'][0]['ave_speed'] == 8.0
assert data['data']['activities'][0]['descent'] is None
assert data['data']['activities'][0]['distance'] == 8.0
assert data['data']['activities'][0]['max_alt'] is None
assert data['data']['activities'][0]['max_speed'] == 8.0
assert data['data']['activities'][0]['min_alt'] is None
assert data['data']['activities'][0]['moving'] == '1:00:00'
assert data['data']['activities'][0]['pauses'] is None
assert data['data']['activities'][0]['with_gpx'] is False
assert data['data']['activities'][0]['map'] is None
assert data['data']['activities'][0]['weather_start'] is None
assert data['data']['activities'][0]['weather_end'] is None
assert data['data']['activities'][0]['notes'] is None
assert data['data']['workouts'][0]['user'] == 'test'
assert data['data']['workouts'][0]['sport_id'] == sport_2_running.id
assert data['data']['workouts'][0]['duration'] == '1:00:00'
assert data['data']['workouts'][0]['title'] == 'Workout test'
assert data['data']['workouts'][0]['ascent'] is None
assert data['data']['workouts'][0]['ave_speed'] == 8.0
assert data['data']['workouts'][0]['descent'] is None
assert data['data']['workouts'][0]['distance'] == 8.0
assert data['data']['workouts'][0]['max_alt'] is None
assert data['data']['workouts'][0]['max_speed'] == 8.0
assert data['data']['workouts'][0]['min_alt'] is None
assert data['data']['workouts'][0]['moving'] == '1:00:00'
assert data['data']['workouts'][0]['pauses'] is None
assert data['data']['workouts'][0]['with_gpx'] is False
assert data['data']['workouts'][0]['map'] is None
assert data['data']['workouts'][0]['weather_start'] is None
assert data['data']['workouts'][0]['weather_end'] is None
assert data['data']['workouts'][0]['notes'] is None
records = data['data']['activities'][0]['records']
records = data['data']['workouts'][0]['records']
assert len(records) == 4
assert records[0]['sport_id'] == sport_2_running.id
assert records[0]['activity_id'] == activity_short_id
assert records[0]['workout_id'] == workout_short_id
assert records[0]['record_type'] == 'MS'
assert records[0]['activity_date'] == 'Tue, 15 May 2018 15:05:00 GMT'
assert records[0]['workout_date'] == 'Tue, 15 May 2018 15:05:00 GMT'
assert records[0]['value'] == 8.0
assert records[1]['sport_id'] == sport_2_running.id
assert records[1]['activity_id'] == activity_short_id
assert records[1]['workout_id'] == workout_short_id
assert records[1]['record_type'] == 'LD'
assert records[1]['activity_date'] == 'Tue, 15 May 2018 15:05:00 GMT'
assert records[1]['workout_date'] == 'Tue, 15 May 2018 15:05:00 GMT'
assert records[1]['value'] == '1:00:00'
assert records[2]['sport_id'] == sport_2_running.id
assert records[2]['activity_id'] == activity_short_id
assert records[2]['workout_id'] == workout_short_id
assert records[2]['record_type'] == 'FD'
assert records[2]['activity_date'] == 'Tue, 15 May 2018 15:05:00 GMT'
assert records[2]['workout_date'] == 'Tue, 15 May 2018 15:05:00 GMT'
assert records[2]['value'] == 8.0
assert records[3]['sport_id'] == sport_2_running.id
assert records[3]['activity_id'] == activity_short_id
assert records[3]['workout_id'] == workout_short_id
assert records[3]['record_type'] == 'AS'
assert records[3]['activity_date'] == 'Tue, 15 May 2018 15:05:00 GMT'
assert records[3]['workout_date'] == 'Tue, 15 May 2018 15:05:00 GMT'
assert records[3]['value'] == 8.0
def test_it_adds_notes_to_an_activity_wo_gpx(
def test_it_adds_notes_to_an_workout_wo_gpx(
self,
app: Flask,
user_1: User,
sport_1_cycling: Sport,
activity_cycling_user_1: Activity,
workout_cycling_user_1: Workout,
) -> None:
activity_short_id = activity_cycling_user_1.short_id
workout_short_id = workout_cycling_user_1.short_id
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -305,7 +305,7 @@ class TestEditActivityWithoutGpx:
)
response = client.patch(
f'/api/activities/{activity_short_id}',
f'/api/workouts/{workout_short_id}',
content_type='application/json',
data=json.dumps(dict(notes='test notes')),
headers=dict(
@ -317,61 +317,61 @@ class TestEditActivityWithoutGpx:
data = json.loads(response.data.decode())
assert response.status_code == 200
assert 'success' in data['status']
assert len(data['data']['activities']) == 1
assert 'creation_date' in data['data']['activities'][0]
assert len(data['data']['workouts']) == 1
assert 'creation_date' in data['data']['workouts'][0]
assert (
data['data']['activities'][0]['activity_date']
data['data']['workouts'][0]['workout_date']
== 'Mon, 01 Jan 2018 00:00:00 GMT'
)
assert data['data']['activities'][0]['user'] == 'test'
assert data['data']['activities'][0]['sport_id'] == sport_1_cycling.id
assert data['data']['activities'][0]['duration'] == '1:00:00'
assert data['data']['activities'][0]['title'] is None
assert data['data']['activities'][0]['ascent'] is None
assert data['data']['activities'][0]['ave_speed'] == 10.0
assert data['data']['activities'][0]['descent'] is None
assert data['data']['activities'][0]['distance'] == 10.0
assert data['data']['activities'][0]['max_alt'] is None
assert data['data']['activities'][0]['max_speed'] == 10.0
assert data['data']['activities'][0]['min_alt'] is None
assert data['data']['activities'][0]['moving'] == '1:00:00'
assert data['data']['activities'][0]['pauses'] is None
assert data['data']['activities'][0]['with_gpx'] is False
assert data['data']['activities'][0]['map'] is None
assert data['data']['activities'][0]['weather_start'] is None
assert data['data']['activities'][0]['weather_end'] is None
assert data['data']['activities'][0]['notes'] == 'test notes'
assert data['data']['workouts'][0]['user'] == 'test'
assert data['data']['workouts'][0]['sport_id'] == sport_1_cycling.id
assert data['data']['workouts'][0]['duration'] == '1:00:00'
assert data['data']['workouts'][0]['title'] is None
assert data['data']['workouts'][0]['ascent'] is None
assert data['data']['workouts'][0]['ave_speed'] == 10.0
assert data['data']['workouts'][0]['descent'] is None
assert data['data']['workouts'][0]['distance'] == 10.0
assert data['data']['workouts'][0]['max_alt'] is None
assert data['data']['workouts'][0]['max_speed'] == 10.0
assert data['data']['workouts'][0]['min_alt'] is None
assert data['data']['workouts'][0]['moving'] == '1:00:00'
assert data['data']['workouts'][0]['pauses'] is None
assert data['data']['workouts'][0]['with_gpx'] is False
assert data['data']['workouts'][0]['map'] is None
assert data['data']['workouts'][0]['weather_start'] is None
assert data['data']['workouts'][0]['weather_end'] is None
assert data['data']['workouts'][0]['notes'] == 'test notes'
records = data['data']['activities'][0]['records']
records = data['data']['workouts'][0]['records']
assert len(records) == 4
assert records[0]['sport_id'] == sport_1_cycling.id
assert records[0]['activity_id'] == activity_short_id
assert records[0]['workout_id'] == workout_short_id
assert records[0]['record_type'] == 'MS'
assert records[0]['activity_date'] == 'Mon, 01 Jan 2018 00:00:00 GMT'
assert records[0]['workout_date'] == 'Mon, 01 Jan 2018 00:00:00 GMT'
assert records[0]['value'] == 10.0
assert records[1]['sport_id'] == sport_1_cycling.id
assert records[1]['activity_id'] == activity_short_id
assert records[1]['workout_id'] == workout_short_id
assert records[1]['record_type'] == 'LD'
assert records[1]['activity_date'] == 'Mon, 01 Jan 2018 00:00:00 GMT'
assert records[1]['workout_date'] == 'Mon, 01 Jan 2018 00:00:00 GMT'
assert records[1]['value'] == '1:00:00'
assert records[2]['sport_id'] == sport_1_cycling.id
assert records[2]['activity_id'] == activity_short_id
assert records[2]['workout_id'] == workout_short_id
assert records[2]['record_type'] == 'FD'
assert records[2]['activity_date'] == 'Mon, 01 Jan 2018 00:00:00 GMT'
assert records[2]['workout_date'] == 'Mon, 01 Jan 2018 00:00:00 GMT'
assert records[2]['value'] == 10.0
assert records[3]['sport_id'] == sport_1_cycling.id
assert records[3]['activity_id'] == activity_short_id
assert records[3]['workout_id'] == workout_short_id
assert records[3]['record_type'] == 'AS'
assert records[3]['activity_date'] == 'Mon, 01 Jan 2018 00:00:00 GMT'
assert records[3]['workout_date'] == 'Mon, 01 Jan 2018 00:00:00 GMT'
assert records[3]['value'] == 10.0
def test_returns_403_when_editing_an_activity_wo_gpx_from_different_user(
def test_returns_403_when_editing_an_workout_wo_gpx_from_different_user(
self,
app: Flask,
user_1: User,
user_2: User,
sport_1_cycling: Sport,
activity_cycling_user_2: Activity,
workout_cycling_user_2: Workout,
) -> None:
client = app.test_client()
resp_login = client.post(
@ -381,15 +381,15 @@ class TestEditActivityWithoutGpx:
)
response = client.patch(
f'/api/activities/{activity_cycling_user_2.short_id}',
f'/api/workouts/{workout_cycling_user_2.short_id}',
content_type='application/json',
data=json.dumps(
dict(
sport_id=2,
duration=3600,
activity_date='2018-05-15 15:05',
workout_date='2018-05-15 15:05',
distance=8,
title='Activity test',
title='Workout test',
)
),
headers=dict(
@ -403,15 +403,15 @@ class TestEditActivityWithoutGpx:
assert 'error' in data['status']
assert 'You do not have permissions.' in data['message']
def test_it_updates_an_activity_wo_gpx_with_timezone(
def test_it_updates_an_workout_wo_gpx_with_timezone(
self,
app: Flask,
user_1_paris: User,
sport_1_cycling: Sport,
sport_2_running: Sport,
activity_cycling_user_1: Activity,
workout_cycling_user_1: Workout,
) -> None:
activity_short_id = activity_cycling_user_1.short_id
workout_short_id = workout_cycling_user_1.short_id
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -420,15 +420,15 @@ class TestEditActivityWithoutGpx:
)
response = client.patch(
f'/api/activities/{activity_short_id}',
f'/api/workouts/{workout_short_id}',
content_type='application/json',
data=json.dumps(
dict(
sport_id=2,
duration=3600,
activity_date='2018-05-15 15:05',
workout_date='2018-05-15 15:05',
distance=8,
title='Activity test',
title='Workout test',
)
),
headers=dict(
@ -440,59 +440,59 @@ class TestEditActivityWithoutGpx:
assert response.status_code == 200
assert 'success' in data['status']
assert len(data['data']['activities']) == 1
assert 'creation_date' in data['data']['activities'][0]
assert len(data['data']['workouts']) == 1
assert 'creation_date' in data['data']['workouts'][0]
assert (
data['data']['activities'][0]['activity_date']
data['data']['workouts'][0]['workout_date']
== 'Tue, 15 May 2018 13:05:00 GMT'
)
assert data['data']['activities'][0]['user'] == 'test'
assert data['data']['activities'][0]['sport_id'] == sport_2_running.id
assert data['data']['activities'][0]['duration'] == '1:00:00'
assert data['data']['activities'][0]['title'] == 'Activity test'
assert data['data']['activities'][0]['ascent'] is None
assert data['data']['activities'][0]['ave_speed'] == 8.0
assert data['data']['activities'][0]['descent'] is None
assert data['data']['activities'][0]['distance'] == 8.0
assert data['data']['activities'][0]['max_alt'] is None
assert data['data']['activities'][0]['max_speed'] == 8.0
assert data['data']['activities'][0]['min_alt'] is None
assert data['data']['activities'][0]['moving'] == '1:00:00'
assert data['data']['activities'][0]['pauses'] is None
assert data['data']['activities'][0]['with_gpx'] is False
assert data['data']['workouts'][0]['user'] == 'test'
assert data['data']['workouts'][0]['sport_id'] == sport_2_running.id
assert data['data']['workouts'][0]['duration'] == '1:00:00'
assert data['data']['workouts'][0]['title'] == 'Workout test'
assert data['data']['workouts'][0]['ascent'] is None
assert data['data']['workouts'][0]['ave_speed'] == 8.0
assert data['data']['workouts'][0]['descent'] is None
assert data['data']['workouts'][0]['distance'] == 8.0
assert data['data']['workouts'][0]['max_alt'] is None
assert data['data']['workouts'][0]['max_speed'] == 8.0
assert data['data']['workouts'][0]['min_alt'] is None
assert data['data']['workouts'][0]['moving'] == '1:00:00'
assert data['data']['workouts'][0]['pauses'] is None
assert data['data']['workouts'][0]['with_gpx'] is False
records = data['data']['activities'][0]['records']
records = data['data']['workouts'][0]['records']
assert len(records) == 4
assert records[0]['sport_id'] == sport_2_running.id
assert records[0]['activity_id'] == activity_short_id
assert records[0]['workout_id'] == workout_short_id
assert records[0]['record_type'] == 'MS'
assert records[0]['activity_date'] == 'Tue, 15 May 2018 13:05:00 GMT'
assert records[0]['workout_date'] == 'Tue, 15 May 2018 13:05:00 GMT'
assert records[0]['value'] == 8.0
assert records[1]['sport_id'] == sport_2_running.id
assert records[1]['activity_id'] == activity_short_id
assert records[1]['workout_id'] == workout_short_id
assert records[1]['record_type'] == 'LD'
assert records[1]['activity_date'] == 'Tue, 15 May 2018 13:05:00 GMT'
assert records[1]['workout_date'] == 'Tue, 15 May 2018 13:05:00 GMT'
assert records[1]['value'] == '1:00:00'
assert records[2]['sport_id'] == sport_2_running.id
assert records[2]['activity_id'] == activity_short_id
assert records[2]['workout_id'] == workout_short_id
assert records[2]['record_type'] == 'FD'
assert records[2]['activity_date'] == 'Tue, 15 May 2018 13:05:00 GMT'
assert records[2]['workout_date'] == 'Tue, 15 May 2018 13:05:00 GMT'
assert records[2]['value'] == 8.0
assert records[3]['sport_id'] == sport_2_running.id
assert records[3]['activity_id'] == activity_short_id
assert records[3]['workout_id'] == workout_short_id
assert records[3]['record_type'] == 'AS'
assert records[3]['activity_date'] == 'Tue, 15 May 2018 13:05:00 GMT'
assert records[3]['workout_date'] == 'Tue, 15 May 2018 13:05:00 GMT'
assert records[3]['value'] == 8.0
def test_it_updates_only_sport_and_distance_an_activity_wo_gpx(
def test_it_updates_only_sport_and_distance_an_workout_wo_gpx(
self,
app: Flask,
user_1: User,
sport_1_cycling: Sport,
sport_2_running: Sport,
activity_cycling_user_1: Activity,
workout_cycling_user_1: Workout,
) -> None:
activity_short_id = activity_cycling_user_1.short_id
workout_short_id = workout_cycling_user_1.short_id
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -501,7 +501,7 @@ class TestEditActivityWithoutGpx:
)
response = client.patch(
f'/api/activities/{activity_short_id}',
f'/api/workouts/{workout_short_id}',
content_type='application/json',
data=json.dumps(dict(sport_id=2, distance=20)),
headers=dict(
@ -513,48 +513,48 @@ class TestEditActivityWithoutGpx:
data = json.loads(response.data.decode())
assert response.status_code == 200
assert 'success' in data['status']
assert len(data['data']['activities']) == 1
assert 'creation_date' in data['data']['activities'][0]
assert len(data['data']['workouts']) == 1
assert 'creation_date' in data['data']['workouts'][0]
assert (
data['data']['activities'][0]['activity_date']
data['data']['workouts'][0]['workout_date']
== 'Mon, 01 Jan 2018 00:00:00 GMT'
)
assert data['data']['activities'][0]['user'] == 'test'
assert data['data']['activities'][0]['sport_id'] == sport_2_running.id
assert data['data']['activities'][0]['duration'] == '1:00:00'
assert data['data']['activities'][0]['title'] is None
assert data['data']['activities'][0]['ascent'] is None
assert data['data']['activities'][0]['ave_speed'] == 20.0
assert data['data']['activities'][0]['descent'] is None
assert data['data']['activities'][0]['distance'] == 20.0
assert data['data']['activities'][0]['max_alt'] is None
assert data['data']['activities'][0]['max_speed'] == 20.0
assert data['data']['activities'][0]['min_alt'] is None
assert data['data']['activities'][0]['moving'] == '1:00:00'
assert data['data']['activities'][0]['pauses'] is None
assert data['data']['activities'][0]['with_gpx'] is False
assert data['data']['workouts'][0]['user'] == 'test'
assert data['data']['workouts'][0]['sport_id'] == sport_2_running.id
assert data['data']['workouts'][0]['duration'] == '1:00:00'
assert data['data']['workouts'][0]['title'] is None
assert data['data']['workouts'][0]['ascent'] is None
assert data['data']['workouts'][0]['ave_speed'] == 20.0
assert data['data']['workouts'][0]['descent'] is None
assert data['data']['workouts'][0]['distance'] == 20.0
assert data['data']['workouts'][0]['max_alt'] is None
assert data['data']['workouts'][0]['max_speed'] == 20.0
assert data['data']['workouts'][0]['min_alt'] is None
assert data['data']['workouts'][0]['moving'] == '1:00:00'
assert data['data']['workouts'][0]['pauses'] is None
assert data['data']['workouts'][0]['with_gpx'] is False
records = data['data']['activities'][0]['records']
records = data['data']['workouts'][0]['records']
assert len(records) == 4
assert records[0]['sport_id'] == sport_2_running.id
assert records[0]['activity_id'] == activity_short_id
assert records[0]['workout_id'] == workout_short_id
assert records[0]['record_type'] == 'MS'
assert records[0]['activity_date'] == 'Mon, 01 Jan 2018 00:00:00 GMT'
assert records[0]['workout_date'] == 'Mon, 01 Jan 2018 00:00:00 GMT'
assert records[0]['value'] == 20.0
assert records[1]['sport_id'] == sport_2_running.id
assert records[1]['activity_id'] == activity_short_id
assert records[1]['workout_id'] == workout_short_id
assert records[1]['record_type'] == 'LD'
assert records[1]['activity_date'] == 'Mon, 01 Jan 2018 00:00:00 GMT'
assert records[1]['workout_date'] == 'Mon, 01 Jan 2018 00:00:00 GMT'
assert records[1]['value'] == '1:00:00'
assert records[2]['sport_id'] == sport_2_running.id
assert records[2]['activity_id'] == activity_short_id
assert records[2]['workout_id'] == workout_short_id
assert records[2]['record_type'] == 'FD'
assert records[2]['activity_date'] == 'Mon, 01 Jan 2018 00:00:00 GMT'
assert records[2]['workout_date'] == 'Mon, 01 Jan 2018 00:00:00 GMT'
assert records[2]['value'] == 20.0
assert records[3]['sport_id'] == sport_2_running.id
assert records[3]['activity_id'] == activity_short_id
assert records[3]['workout_id'] == workout_short_id
assert records[3]['record_type'] == 'AS'
assert records[3]['activity_date'] == 'Mon, 01 Jan 2018 00:00:00 GMT'
assert records[3]['workout_date'] == 'Mon, 01 Jan 2018 00:00:00 GMT'
assert records[3]['value'] == 20.0
def test_it_returns_400_if_payload_is_empty(
@ -562,7 +562,7 @@ class TestEditActivityWithoutGpx:
app: Flask,
user_1: User,
sport_1_cycling: Sport,
activity_cycling_user_1: Activity,
workout_cycling_user_1: Workout,
) -> None:
client = app.test_client()
resp_login = client.post(
@ -572,7 +572,7 @@ class TestEditActivityWithoutGpx:
)
response = client.patch(
f'/api/activities/{activity_cycling_user_1.short_id}',
f'/api/workouts/{workout_cycling_user_1.short_id}',
content_type='application/json',
data=json.dumps(dict()),
headers=dict(
@ -591,7 +591,7 @@ class TestEditActivityWithoutGpx:
app: Flask,
user_1: User,
sport_1_cycling: Sport,
activity_cycling_user_1: Activity,
workout_cycling_user_1: Workout,
) -> None:
client = app.test_client()
resp_login = client.post(
@ -600,13 +600,13 @@ class TestEditActivityWithoutGpx:
content_type='application/json',
)
response = client.patch(
f'/api/activities/{activity_cycling_user_1.short_id}',
f'/api/workouts/{workout_cycling_user_1.short_id}',
content_type='application/json',
data=json.dumps(
dict(
sport_id=1,
duration=3600,
activity_date='15/2018',
workout_date='15/2018',
distance=10,
)
),
@ -625,7 +625,7 @@ class TestEditActivityWithoutGpx:
in data['message']
)
def test_it_returns_404_if_edited_activity_does_not_exists(
def test_it_returns_404_if_edited_workout_does_not_exists(
self, app: Flask, user_1: User, sport_1_cycling: Sport
) -> None:
client = app.test_client()
@ -635,13 +635,13 @@ class TestEditActivityWithoutGpx:
content_type='application/json',
)
response = client.patch(
f'/api/activities/{get_random_short_id()}',
f'/api/workouts/{get_random_short_id()}',
content_type='application/json',
data=json.dumps(
dict(
sport_id=1,
duration=3600,
activity_date='2018-05-15 14:05',
workout_date='2018-05-15 14:05',
distance=10,
)
),
@ -654,11 +654,11 @@ class TestEditActivityWithoutGpx:
data = json.loads(response.data.decode())
assert response.status_code == 404
assert 'not found' in data['status']
assert len(data['data']['activities']) == 0
assert len(data['data']['workouts']) == 0
class TestRefreshActivityWithGpx:
def test_refresh_an_activity_with_gpx(
class TestRefreshWorkoutWithGpx:
def test_refresh_an_workout_with_gpx(
self,
app: Flask,
user_1: User,
@ -666,17 +666,17 @@ class TestRefreshActivityWithGpx:
sport_2_running: Sport,
gpx_file: str,
) -> None:
token, activity_short_id = post_an_activity(app, gpx_file)
activity_uuid = decode_short_id(activity_short_id)
token, workout_short_id = post_an_workout(app, gpx_file)
workout_uuid = decode_short_id(workout_short_id)
client = app.test_client()
# Edit some activity data
activity = Activity.query.filter_by(uuid=activity_uuid).first()
activity.ascent = 1000
activity.min_alt = -100
# Edit some workout data
workout = Workout.query.filter_by(uuid=workout_uuid).first()
workout.ascent = 1000
workout.min_alt = -100
response = client.patch(
f'/api/activities/{activity_short_id}',
f'/api/workouts/{workout_short_id}',
content_type='application/json',
data=json.dumps(dict(refresh=True)),
headers=dict(Authorization=f'Bearer {token}'),
@ -685,7 +685,7 @@ class TestRefreshActivityWithGpx:
assert response.status_code == 200
assert 'success' in data['status']
assert len(data['data']['activities']) == 1
assert 1 == data['data']['activities'][0]['sport_id']
assert 0.4 == data['data']['activities'][0]['ascent']
assert 975.0 == data['data']['activities'][0]['min_alt']
assert len(data['data']['workouts']) == 1
assert 1 == data['data']['workouts'][0]['sport_id']
assert 0.4 == data['data']['workouts'][0]['ascent']
assert 975.0 == data['data']['workouts'][0]['min_alt']

View File

@ -1,34 +1,34 @@
import json
import os
from fittrackee.activities.models import Activity, Sport
from fittrackee.activities.utils import get_absolute_file_path
from fittrackee.users.models import User
from fittrackee.workouts.models import Sport, Workout
from fittrackee.workouts.utils import get_absolute_file_path
from flask import Flask
from .utils import get_random_short_id, post_an_activity
from .utils import get_random_short_id, post_an_workout
def get_gpx_filepath(activity_id: int) -> str:
activity = Activity.query.filter_by(id=activity_id).first()
return activity.gpx
def get_gpx_filepath(workout_id: int) -> str:
workout = Workout.query.filter_by(id=workout_id).first()
return workout.gpx
class TestDeleteActivityWithGpx:
def test_it_deletes_an_activity_with_gpx(
class TestDeleteWorkoutWithGpx:
def test_it_deletes_an_workout_with_gpx(
self, app: Flask, user_1: User, sport_1_cycling: Sport, gpx_file: str
) -> None:
token, activity_short_id = post_an_activity(app, gpx_file)
token, workout_short_id = post_an_workout(app, gpx_file)
client = app.test_client()
response = client.delete(
f'/api/activities/{activity_short_id}',
f'/api/workouts/{workout_short_id}',
headers=dict(Authorization=f'Bearer {token}'),
)
assert response.status_code == 204
def test_it_returns_403_when_deleting_an_activity_from_different_user(
def test_it_returns_403_when_deleting_an_workout_from_different_user(
self,
app: Flask,
user_1: User,
@ -36,7 +36,7 @@ class TestDeleteActivityWithGpx:
sport_1_cycling: Sport,
gpx_file: str,
) -> None:
_, activity_short_id = post_an_activity(app, gpx_file)
_, workout_short_id = post_an_workout(app, gpx_file)
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -45,7 +45,7 @@ class TestDeleteActivityWithGpx:
)
response = client.delete(
f'/api/activities/{activity_short_id}',
f'/api/workouts/{workout_short_id}',
headers=dict(
Authorization='Bearer '
+ json.loads(resp_login.data.decode())['auth_token']
@ -58,7 +58,7 @@ class TestDeleteActivityWithGpx:
assert 'error' in data['status']
assert 'You do not have permissions.' in data['message']
def test_it_returns_404_if_activity_does_not_exist(
def test_it_returns_404_if_workout_does_not_exist(
self, app: Flask, user_1: User
) -> None:
client = app.test_client()
@ -68,7 +68,7 @@ class TestDeleteActivityWithGpx:
content_type='application/json',
)
response = client.delete(
f'/api/activities/{get_random_short_id()}',
f'/api/workouts/{get_random_short_id()}',
headers=dict(
Authorization='Bearer '
+ json.loads(resp_login.data.decode())['auth_token']
@ -78,17 +78,17 @@ class TestDeleteActivityWithGpx:
assert response.status_code == 404
assert 'not found' in data['status']
def test_it_returns_500_when_deleting_an_activity_with_gpx_invalid_file(
def test_it_returns_500_when_deleting_an_workout_with_gpx_invalid_file(
self, app: Flask, user_1: User, sport_1_cycling: Sport, gpx_file: str
) -> None:
token, activity_short_id = post_an_activity(app, gpx_file)
token, workout_short_id = post_an_workout(app, gpx_file)
client = app.test_client()
gpx_filepath = get_gpx_filepath(1)
gpx_filepath = get_absolute_file_path(gpx_filepath)
os.remove(gpx_filepath)
response = client.delete(
f'/api/activities/{activity_short_id}',
f'/api/workouts/{workout_short_id}',
headers=dict(Authorization=f'Bearer {token}'),
)
@ -102,13 +102,13 @@ class TestDeleteActivityWithGpx:
)
class TestDeleteActivityWithoutGpx:
def test_it_deletes_an_activity_wo_gpx(
class TestDeleteWorkoutWithoutGpx:
def test_it_deletes_an_workout_wo_gpx(
self,
app: Flask,
user_1: User,
sport_1_cycling: Sport,
activity_cycling_user_1: Activity,
workout_cycling_user_1: Workout,
) -> None:
client = app.test_client()
resp_login = client.post(
@ -117,7 +117,7 @@ class TestDeleteActivityWithoutGpx:
content_type='application/json',
)
response = client.delete(
f'/api/activities/{activity_cycling_user_1.short_id}',
f'/api/workouts/{workout_cycling_user_1.short_id}',
headers=dict(
Authorization='Bearer '
+ json.loads(resp_login.data.decode())['auth_token']
@ -125,13 +125,13 @@ class TestDeleteActivityWithoutGpx:
)
assert response.status_code == 204
def test_it_returns_403_when_deleting_an_activity_from_different_user(
def test_it_returns_403_when_deleting_an_workout_from_different_user(
self,
app: Flask,
user_1: User,
user_2: User,
sport_1_cycling: Sport,
activity_cycling_user_1: Activity,
workout_cycling_user_1: Workout,
) -> None:
client = app.test_client()
resp_login = client.post(
@ -140,7 +140,7 @@ class TestDeleteActivityWithoutGpx:
content_type='application/json',
)
response = client.delete(
f'/api/activities/{activity_cycling_user_1.short_id}',
f'/api/workouts/{workout_cycling_user_1.short_id}',
headers=dict(
Authorization='Bearer '
+ json.loads(resp_login.data.decode())['auth_token']

View File

@ -0,0 +1,73 @@
from uuid import UUID
from fittrackee.users.models import User
from fittrackee.workouts.models import Sport, Workout
from fittrackee.workouts.utils_id import decode_short_id
from flask import Flask
class TestWorkoutModel:
def test_workout_model(
self,
app: Flask,
sport_1_cycling: Sport,
user_1: User,
workout_cycling_user_1: Workout,
) -> None:
workout_cycling_user_1.title = 'Test'
assert 1 == workout_cycling_user_1.id
assert workout_cycling_user_1.uuid is not None
assert 1 == workout_cycling_user_1.user_id
assert 1 == workout_cycling_user_1.sport_id
assert '2018-01-01 00:00:00' == str(
workout_cycling_user_1.workout_date
)
assert 10.0 == float(workout_cycling_user_1.distance)
assert '1:00:00' == str(workout_cycling_user_1.duration)
assert 'Test' == workout_cycling_user_1.title
assert '<Workout \'Cycling\' - 2018-01-01 00:00:00>' == str(
workout_cycling_user_1
)
serialized_workout = workout_cycling_user_1.serialize()
assert isinstance(decode_short_id(serialized_workout['id']), UUID)
assert 'test' == serialized_workout['user']
assert 1 == serialized_workout['sport_id']
assert serialized_workout['title'] == 'Test'
assert 'creation_date' in serialized_workout
assert serialized_workout['modification_date'] is not None
assert str(serialized_workout['workout_date']) == '2018-01-01 00:00:00'
assert serialized_workout['duration'] == '1:00:00'
assert serialized_workout['pauses'] is None
assert serialized_workout['moving'] == '1:00:00'
assert serialized_workout['distance'] == 10.0
assert serialized_workout['max_alt'] is None
assert serialized_workout['descent'] is None
assert serialized_workout['ascent'] is None
assert serialized_workout['max_speed'] == 10.0
assert serialized_workout['ave_speed'] == 10.0
assert serialized_workout['with_gpx'] is False
assert serialized_workout['bounds'] == []
assert serialized_workout['previous_workout'] is None
assert serialized_workout['next_workout'] is None
assert serialized_workout['segments'] == []
assert serialized_workout['records'] != []
assert serialized_workout['map'] is None
assert serialized_workout['weather_start'] is None
assert serialized_workout['weather_end'] is None
assert serialized_workout['notes'] is None
def test_workout_segment_model(
self,
app: Flask,
sport_1_cycling: Sport,
user_1: User,
workout_cycling_user_1: Workout,
workout_cycling_user_1_segment: Workout,
) -> None:
assert (
f'<Segment \'{workout_cycling_user_1_segment.segment_id}\' '
f'for workout \'{workout_cycling_user_1.short_id}\'>'
== str(workout_cycling_user_1_segment)
)

View File

@ -3,7 +3,7 @@ from io import BytesIO
from typing import Tuple
from uuid import uuid4
from fittrackee.activities.utils_id import encode_uuid
from fittrackee.workouts.utils_id import encode_uuid
from flask import Flask
@ -11,7 +11,7 @@ def get_random_short_id() -> str:
return encode_uuid(uuid4())
def post_an_activity(app: Flask, gpx_file: str) -> Tuple[str, str]:
def post_an_workout(app: Flask, gpx_file: str) -> Tuple[str, str]:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -20,7 +20,7 @@ def post_an_activity(app: Flask, gpx_file: str) -> Tuple[str, str]:
)
token = json.loads(resp_login.data.decode())['auth_token']
response = client.post(
'/api/activities',
'/api/workouts',
data=dict(
file=(BytesIO(str.encode(gpx_file)), 'example.gpx'),
data='{"sport_id": 1}',
@ -30,4 +30,4 @@ def post_an_activity(app: Flask, gpx_file: str) -> Tuple[str, str]:
),
)
data = json.loads(response.data.decode())
return token, data['data']['activities'][0]['id']
return token, data['data']['workouts'][0]['id']

View File

@ -18,7 +18,7 @@ from sqlalchemy import exc, or_
from werkzeug.exceptions import RequestEntityTooLarge
from werkzeug.utils import secure_filename
from ..activities.utils_files import get_absolute_file_path
from ..workouts.utils_files import get_absolute_file_path
from .models import User
from .utils import (
authenticate,
@ -310,8 +310,8 @@ def get_authenticated_user_profile(
"language": "en",
"last_name": null,
"location": null,
"nb_activities": 6,
"nb_sports": 3,
"nb_workouts": 6,
"picture": false,
"sports_list": [
1,
@ -371,8 +371,8 @@ def edit_user(auth_user_id: int) -> Union[Dict, HttpResponse]:
"language": "en",
"last_name": null,
"location": null,
"nb_activities": 6,
"nb_sports": 3,
"nb_workouts": 6,
"picture": false,
"sports_list": [
1,

View File

@ -9,7 +9,7 @@ from sqlalchemy.ext.declarative import DeclarativeMeta
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.sql.expression import select
from ..activities.models import Activity
from ..workouts.models import Workout
from .utils_token import decode_user_token, get_user_token
BaseModel: DeclarativeMeta = db.Model
@ -32,8 +32,8 @@ class User(BaseModel):
timezone = db.Column(db.String(50), nullable=True)
# does the week start Monday?
weekm = db.Column(db.Boolean(50), default=False, nullable=False)
activities = db.relationship(
'Activity', lazy=True, backref=db.backref('user', lazy='joined')
workouts = db.relationship(
'Workout', lazy=True, backref=db.backref('user', lazy='joined')
)
records = db.relationship(
'Record', lazy=True, backref=db.backref('user', lazy='joined')
@ -90,33 +90,33 @@ class User(BaseModel):
return 'Invalid token. Please log in again.'
@hybrid_property
def activities_count(self) -> int:
return Activity.query.filter(Activity.user_id == self.id).count()
def workouts_count(self) -> int:
return Workout.query.filter(Workout.user_id == self.id).count()
@activities_count.expression # type: ignore
def activities_count(self) -> int:
@workouts_count.expression # type: ignore
def workouts_count(self) -> int:
return (
select([func.count(Activity.id)])
.where(Activity.user_id == self.id)
.label('activities_count')
select([func.count(Workout.id)])
.where(Workout.user_id == self.id)
.label('workouts_count')
)
def serialize(self) -> Dict:
sports = []
total = (0, '0:00:00')
if self.activities_count > 0: # type: ignore
if self.workouts_count > 0: # type: ignore
sports = (
db.session.query(Activity.sport_id)
.filter(Activity.user_id == self.id)
.group_by(Activity.sport_id)
.order_by(Activity.sport_id)
db.session.query(Workout.sport_id)
.filter(Workout.user_id == self.id)
.group_by(Workout.sport_id)
.order_by(Workout.sport_id)
.all()
)
total = (
db.session.query(
func.sum(Activity.distance), func.sum(Activity.duration)
func.sum(Workout.distance), func.sum(Workout.duration)
)
.filter(Activity.user_id == self.id)
.filter(Workout.user_id == self.id)
.first()
)
return {
@ -133,8 +133,8 @@ class User(BaseModel):
'timezone': self.timezone,
'weekm': self.weekm,
'language': self.language,
'nb_activities': self.activities_count,
'nb_sports': len(sports),
'nb_workouts': self.workouts_count,
'sports_list': [
sport for sportslist in sports for sport in sportslist
],

View File

@ -14,8 +14,8 @@ from fittrackee.responses import (
from flask import Blueprint, request, send_file
from sqlalchemy import exc
from ..activities.utils_files import get_absolute_file_path
from .models import Activity, User
from ..workouts.utils_files import get_absolute_file_path
from .models import User, Workout
from .utils import authenticate, authenticate_as_admin
users_blueprint = Blueprint('users', __name__)
@ -42,7 +42,7 @@ def get_users(auth_user_id: int) -> Dict:
.. sourcecode:: http
GET /api/users?order_by=activities_count&par_page=5 HTTP/1.1
GET /api/users?order_by=workouts_count&par_page=5 HTTP/1.1
Content-Type: application/json
**Example response**:
@ -65,8 +65,8 @@ def get_users(auth_user_id: int) -> Dict:
"language": "en",
"last_name": null,
"location": null,
"nb_activities": 6,
"nb_sports": 3,
"nb_workouts": 6,
"picture": false,
"sports_list": [
1,
@ -88,8 +88,8 @@ def get_users(auth_user_id: int) -> Dict:
"language": "fr",
"last_name": null,
"location": null,
"nb_activities": 0,
"nb_sports": 0,
"nb_workouts": 0,
"picture": false,
"sports_list": [],
"timezone": "Europe/Paris",
@ -108,7 +108,7 @@ def get_users(auth_user_id: int) -> Dict:
:query integer per_page: number of users per page (default: 10, max: 50)
:query string q: query on user name
:query string order_by: sorting criteria (``username``, ``created_at``,
``activities_count``, ``admin``)
``workouts_count``, ``admin``)
:query string order: sorting order (default: ``asc``)
:reqheader Authorization: OAuth 2.0 Bearer Token
@ -137,11 +137,11 @@ def get_users(auth_user_id: int) -> Dict:
User.username.like('%' + query + '%') if query else True,
)
.order_by(
User.activities_count.asc() # type: ignore
if order_by == 'activities_count' and order == 'asc'
User.workouts_count.asc() # type: ignore
if order_by == 'workouts_count' and order == 'asc'
else True,
User.activities_count.desc() # type: ignore
if order_by == 'activities_count' and order == 'desc'
User.workouts_count.desc() # type: ignore
if order_by == 'workouts_count' and order == 'desc'
else True,
User.username.asc()
if order_by == 'username' and order == 'asc'
@ -212,8 +212,8 @@ def get_single_user(
"language": "en",
"last_name": null,
"location": null,
"nb_activities": 6,
"nb_sports": 3,
"nb_workouts": 6,
"picture": false,
"sports_list": [
1,
@ -328,7 +328,7 @@ def update_user(
"language": "en",
"last_name": null,
"location": null,
"nb_activities": 6,
"nb_workouts": 6,
"nb_sports": 3,
"picture": false,
"sports_list": [
@ -443,8 +443,8 @@ def delete_user(
'no other user has admin rights.'
)
for activity in Activity.query.filter_by(user_id=user.id).all():
db.session.delete(activity)
for workout in Workout.query.filter_by(user_id=user.id).all():
db.session.delete(workout)
db.session.flush()
user_picture = user.picture
db.session.delete(user)
@ -454,7 +454,7 @@ def delete_user(
if os.path.isfile(picture_path):
os.remove(picture_path)
shutil.rmtree(
get_absolute_file_path(f'activities/{user.id}'),
get_absolute_file_path(f'workouts/{user.id}'),
ignore_errors=True,
)
shutil.rmtree(

View File

@ -78,8 +78,8 @@ def verify_extension_and_size(
return InvalidPayloadErrorResponse('No selected file.', 'fail')
allowed_extensions = (
'ACTIVITY_ALLOWED_EXTENSIONS'
if file_type == 'activity'
'WORKOUT_ALLOWED_EXTENSIONS'
if file_type == 'workout'
else 'PICTURE_ALLOWED_EXTENSIONS'
)
@ -158,13 +158,13 @@ def authenticate_as_admin(f: Callable) -> Callable:
return decorated_function
def can_view_activity(
auth_user_id: int, activity_user_id: int
def can_view_workout(
auth_user_id: int, workout_user_id: int
) -> Optional[HttpResponse]:
"""
Return error response if user has no right to view activity
Return error response if user has no right to view workout
"""
if auth_user_id != activity_user_id:
if auth_user_id != workout_user_id:
return ForbiddenErrorResponse()
return None

View File

@ -30,7 +30,7 @@ def update_records(
user_id: int, sport_id: int, connection: Connection, session: Session
) -> None:
record_table = Record.__table__
new_records = Activity.get_user_activity_records(user_id, sport_id)
new_records = Workout.get_user_workout_records(user_id, sport_id)
for record_type, record_data in new_records.items():
if record_data['record_value']:
record = Record.query.filter_by(
@ -45,14 +45,14 @@ def update_records(
.where(record_table.c.id == record.id)
.values(
value=value,
activity_id=record_data['activity'].id,
activity_uuid=record_data['activity'].uuid,
activity_date=record_data['activity'].activity_date,
workout_id=record_data['workout'].id,
workout_uuid=record_data['workout'].uuid,
workout_date=record_data['workout'].workout_date,
)
)
else:
new_record = Record(
activity=record_data['activity'], record_type=record_type
workout=record_data['workout'], record_type=record_type
)
new_record.value = record_data['record_value'] # type: ignore
session.add(new_record)
@ -66,13 +66,13 @@ def update_records(
class Sport(BaseModel):
__tablename__ = "sports"
__tablename__ = 'sports'
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
label = db.Column(db.String(50), unique=True, nullable=False)
img = db.Column(db.String(255), unique=True, nullable=True)
is_active = db.Column(db.Boolean, default=True, nullable=False)
activities = db.relationship(
'Activity', lazy=True, backref=db.backref('sports', lazy='joined')
workouts = db.relationship(
'Workout', lazy=True, backref=db.backref('sports', lazy='joined')
)
records = db.relationship(
'Record', lazy=True, backref=db.backref('sports', lazy='joined')
@ -92,12 +92,12 @@ class Sport(BaseModel):
'is_active': self.is_active,
}
if is_admin:
serialized_sport['has_activities'] = len(self.activities) > 0
serialized_sport['has_workouts'] = len(self.workouts) > 0
return serialized_sport
class Activity(BaseModel):
__tablename__ = "activities"
class Workout(BaseModel):
__tablename__ = 'workouts'
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
uuid = db.Column(
postgresql.UUID(as_uuid=True),
@ -115,7 +115,7 @@ class Activity(BaseModel):
modification_date = db.Column(
db.DateTime, onupdate=datetime.datetime.utcnow
)
activity_date = db.Column(db.DateTime, nullable=False)
workout_date = db.Column(db.DateTime, nullable=False)
duration = db.Column(db.Interval, nullable=False)
pauses = db.Column(db.Interval, nullable=True)
moving = db.Column(db.Interval, nullable=True)
@ -133,32 +133,32 @@ class Activity(BaseModel):
weather_end = db.Column(JSON, nullable=True)
notes = db.Column(db.String(500), nullable=True)
segments = db.relationship(
'ActivitySegment',
'WorkoutSegment',
lazy=True,
cascade='all, delete',
backref=db.backref('activities', lazy='joined', single_parent=True),
backref=db.backref('workouts', lazy='joined', single_parent=True),
)
records = db.relationship(
'Record',
lazy=True,
cascade='all, delete',
backref=db.backref('activities', lazy='joined', single_parent=True),
backref=db.backref('workouts', lazy='joined', single_parent=True),
)
def __str__(self) -> str:
return f'<Activity \'{self.sports.label}\' - {self.activity_date}>'
return f'<Workout \'{self.sports.label}\' - {self.workout_date}>'
def __init__(
self,
user_id: int,
sport_id: int,
activity_date: datetime.datetime,
workout_date: datetime.datetime,
distance: float,
duration: datetime.timedelta,
) -> None:
self.user_id = user_id
self.sport_id = sport_id
self.activity_date = activity_date
self.workout_date = workout_date
self.distance = distance
self.duration = duration
@ -178,78 +178,78 @@ class Activity(BaseModel):
max_speed_from = params.get('max_speed_from') if params else None
max_speed_to = params.get('max_speed_to') if params else None
sport_id = params.get('sport_id') if params else None
previous_activity = (
Activity.query.filter(
Activity.id != self.id,
Activity.user_id == self.user_id,
Activity.sport_id == sport_id if sport_id else True,
Activity.activity_date <= self.activity_date,
Activity.activity_date
previous_workout = (
Workout.query.filter(
Workout.id != self.id,
Workout.user_id == self.user_id,
Workout.sport_id == sport_id if sport_id else True,
Workout.workout_date <= self.workout_date,
Workout.workout_date
>= datetime.datetime.strptime(date_from, '%Y-%m-%d')
if date_from
else True,
Activity.activity_date
Workout.workout_date
<= datetime.datetime.strptime(date_to, '%Y-%m-%d')
if date_to
else True,
Activity.distance >= int(distance_from)
Workout.distance >= int(distance_from)
if distance_from
else True,
Activity.distance <= int(distance_to) if distance_to else True,
Activity.duration >= convert_in_duration(duration_from)
Workout.distance <= int(distance_to) if distance_to else True,
Workout.duration >= convert_in_duration(duration_from)
if duration_from
else True,
Activity.duration <= convert_in_duration(duration_to)
Workout.duration <= convert_in_duration(duration_to)
if duration_to
else True,
Activity.ave_speed >= float(ave_speed_from)
Workout.ave_speed >= float(ave_speed_from)
if ave_speed_from
else True,
Activity.ave_speed <= float(ave_speed_to)
Workout.ave_speed <= float(ave_speed_to)
if ave_speed_to
else True,
Activity.max_speed >= float(max_speed_from)
Workout.max_speed >= float(max_speed_from)
if max_speed_from
else True,
Activity.max_speed <= float(max_speed_to)
Workout.max_speed <= float(max_speed_to)
if max_speed_to
else True,
)
.order_by(Activity.activity_date.desc())
.order_by(Workout.workout_date.desc())
.first()
)
next_activity = (
Activity.query.filter(
Activity.id != self.id,
Activity.user_id == self.user_id,
Activity.sport_id == sport_id if sport_id else True,
Activity.activity_date >= self.activity_date,
Activity.activity_date
next_workout = (
Workout.query.filter(
Workout.id != self.id,
Workout.user_id == self.user_id,
Workout.sport_id == sport_id if sport_id else True,
Workout.workout_date >= self.workout_date,
Workout.workout_date
>= datetime.datetime.strptime(date_from, '%Y-%m-%d')
if date_from
else True,
Activity.activity_date
Workout.workout_date
<= datetime.datetime.strptime(date_to, '%Y-%m-%d')
if date_to
else True,
Activity.distance >= int(distance_from)
Workout.distance >= int(distance_from)
if distance_from
else True,
Activity.distance <= int(distance_to) if distance_to else True,
Activity.duration >= convert_in_duration(duration_from)
Workout.distance <= int(distance_to) if distance_to else True,
Workout.duration >= convert_in_duration(duration_from)
if duration_from
else True,
Activity.duration <= convert_in_duration(duration_to)
Workout.duration <= convert_in_duration(duration_to)
if duration_to
else True,
Activity.ave_speed >= float(ave_speed_from)
Workout.ave_speed >= float(ave_speed_from)
if ave_speed_from
else True,
Activity.ave_speed <= float(ave_speed_to)
Workout.ave_speed <= float(ave_speed_to)
if ave_speed_to
else True,
)
.order_by(Activity.activity_date.asc())
.order_by(Workout.workout_date.asc())
.first()
)
return {
@ -259,7 +259,7 @@ class Activity(BaseModel):
'title': self.title,
'creation_date': self.creation_date,
'modification_date': self.modification_date,
'activity_date': self.activity_date,
'workout_date': self.workout_date,
'duration': str(self.duration) if self.duration else None,
'pauses': str(self.pauses) if self.pauses else None,
'moving': str(self.moving) if self.moving else None,
@ -274,10 +274,10 @@ class Activity(BaseModel):
'bounds': [float(bound) for bound in self.bounds]
if self.bounds
else [], # noqa
'previous_activity': previous_activity.short_id
if previous_activity
'previous_workout': previous_workout.short_id
if previous_workout
else None, # noqa
'next_activity': next_activity.short_id if next_activity else None,
'next_workout': next_workout.short_id if next_workout else None,
'segments': [segment.serialize() for segment in self.segments],
'records': [record.serialize() for record in self.records],
'map': self.map_id if self.map else None,
@ -287,7 +287,7 @@ class Activity(BaseModel):
}
@classmethod
def get_user_activity_records(
def get_user_workout_records(
cls, user_id: int, sport_id: int, as_integer: Optional[bool] = False
) -> Dict:
record_types_columns = {
@ -298,55 +298,53 @@ class Activity(BaseModel):
}
records = {}
for record_type, column in record_types_columns.items():
column_sorted = getattr(getattr(Activity, column), 'desc')()
record_activity = (
Activity.query.filter_by(user_id=user_id, sport_id=sport_id)
.order_by(column_sorted, Activity.activity_date)
column_sorted = getattr(getattr(Workout, column), 'desc')()
record_workout = (
Workout.query.filter_by(user_id=user_id, sport_id=sport_id)
.order_by(column_sorted, Workout.workout_date)
.first()
)
records[record_type] = dict(
record_value=(
getattr(record_activity, column)
if record_activity
else None
getattr(record_workout, column) if record_workout else None
),
activity=record_activity,
workout=record_workout,
)
return records
@listens_for(Activity, 'after_insert')
def on_activity_insert(
mapper: Mapper, connection: Connection, activity: Activity
@listens_for(Workout, 'after_insert')
def on_workout_insert(
mapper: Mapper, connection: Connection, workout: Workout
) -> None:
@listens_for(db.Session, 'after_flush', once=True)
def receive_after_flush(session: Session, context: Any) -> None:
update_records(
activity.user_id, activity.sport_id, connection, session
workout.user_id, workout.sport_id, connection, session
) # noqa
@listens_for(Activity, 'after_update')
def on_activity_update(
mapper: Mapper, connection: Connection, activity: Activity
@listens_for(Workout, 'after_update')
def on_workout_update(
mapper: Mapper, connection: Connection, workout: Workout
) -> None:
if object_session(activity).is_modified(
activity, include_collections=True
if object_session(workout).is_modified(
workout, include_collections=True
): # noqa
@listens_for(db.Session, 'after_flush', once=True)
def receive_after_flush(session: Session, context: Any) -> None:
sports_list = [activity.sport_id]
records = Record.query.filter_by(activity_id=activity.id).all()
sports_list = [workout.sport_id]
records = Record.query.filter_by(workout_id=workout.id).all()
for rec in records:
if rec.sport_id not in sports_list:
sports_list.append(rec.sport_id)
for sport_id in sports_list:
update_records(activity.user_id, sport_id, connection, session)
update_records(workout.user_id, sport_id, connection, session)
@listens_for(Activity, 'after_delete')
def on_activity_delete(
@listens_for(Workout, 'after_delete')
def on_workout_delete(
mapper: Mapper, connection: Connection, old_record: 'Record'
) -> None:
@listens_for(db.Session, 'after_flush', once=True)
@ -357,12 +355,12 @@ def on_activity_delete(
os.remove(get_absolute_file_path(old_record.gpx))
class ActivitySegment(BaseModel):
__tablename__ = "activity_segments"
activity_id = db.Column(
db.Integer, db.ForeignKey('activities.id'), primary_key=True
class WorkoutSegment(BaseModel):
__tablename__ = 'workout_segments'
workout_id = db.Column(
db.Integer, db.ForeignKey('workouts.id'), primary_key=True
)
activity_uuid = db.Column(postgresql.UUID(as_uuid=True), nullable=False)
workout_uuid = db.Column(postgresql.UUID(as_uuid=True), nullable=False)
segment_id = db.Column(db.Integer, primary_key=True)
duration = db.Column(db.Interval, nullable=False)
pauses = db.Column(db.Interval, nullable=True)
@ -378,19 +376,19 @@ class ActivitySegment(BaseModel):
def __str__(self) -> str:
return (
f'<Segment \'{self.segment_id}\' '
f'for activity \'{encode_uuid(self.activity_uuid)}\'>'
f'for workout \'{encode_uuid(self.workout_uuid)}\'>'
)
def __init__(
self, segment_id: int, activity_id: int, activity_uuid: UUID
self, segment_id: int, workout_id: int, workout_uuid: UUID
) -> None:
self.segment_id = segment_id
self.activity_id = activity_id
self.activity_uuid = activity_uuid
self.workout_id = workout_id
self.workout_uuid = workout_uuid
def serialize(self) -> Dict:
return {
'activity_id': encode_uuid(self.activity_uuid),
'workout_id': encode_uuid(self.workout_uuid),
'segment_id': self.segment_id,
'duration': str(self.duration) if self.duration else None,
'pauses': str(self.pauses) if self.pauses else None,
@ -417,28 +415,28 @@ class Record(BaseModel):
sport_id = db.Column(
db.Integer, db.ForeignKey('sports.id'), nullable=False
)
activity_id = db.Column(
db.Integer, db.ForeignKey('activities.id'), nullable=False
workout_id = db.Column(
db.Integer, db.ForeignKey('workouts.id'), nullable=False
)
activity_uuid = db.Column(postgresql.UUID(as_uuid=True), nullable=False)
workout_uuid = db.Column(postgresql.UUID(as_uuid=True), nullable=False)
record_type = db.Column(Enum(*record_types, name="record_types"))
activity_date = db.Column(db.DateTime, nullable=False)
workout_date = db.Column(db.DateTime, nullable=False)
_value = db.Column("value", db.Integer, nullable=True)
def __str__(self) -> str:
return (
f'<Record {self.sports.label} - '
f'{self.record_type} - '
f"{self.activity_date.strftime('%Y-%m-%d')}>"
f"{self.workout_date.strftime('%Y-%m-%d')}>"
)
def __init__(self, activity: Activity, record_type: str) -> None:
self.user_id = activity.user_id
self.sport_id = activity.sport_id
self.activity_id = activity.id
self.activity_uuid = activity.uuid
def __init__(self, workout: Workout, record_type: str) -> None:
self.user_id = workout.user_id
self.sport_id = workout.sport_id
self.workout_id = workout.id
self.workout_uuid = workout.uuid
self.record_type = record_type
self.activity_date = activity.activity_date
self.workout_date = workout.workout_date
@hybrid_property
def value(self) -> Optional[Union[datetime.timedelta, float]]:
@ -467,9 +465,9 @@ class Record(BaseModel):
'id': self.id,
'user': self.user.username,
'sport_id': self.sport_id,
'activity_id': encode_uuid(self.activity_uuid),
'workout_id': encode_uuid(self.workout_uuid),
'record_type': self.record_type,
'activity_date': self.activity_date,
'workout_date': self.workout_date,
'value': value,
}
@ -480,9 +478,9 @@ def on_record_delete(
) -> None:
@listens_for(db.Session, 'after_flush', once=True)
def receive_after_flush(session: Session, context: Any) -> None:
activity = old_record.activities
new_records = Activity.get_user_activity_records(
activity.user_id, activity.sport_id
workout = old_record.workouts
new_records = Workout.get_user_workout_records(
workout.user_id, workout.sport_id
)
for record_type, record_data in new_records.items():
if (
@ -490,7 +488,7 @@ def on_record_delete(
and record_type == old_record.record_type
):
new_record = Record(
activity=record_data['activity'], record_type=record_type
workout=record_data['workout'], record_type=record_type
)
new_record.value = record_data['record_value'] # type: ignore
session.add(new_record)

View File

@ -40,40 +40,40 @@ def get_records(auth_user_id: int) -> Dict:
"data": {
"records": [
{
"activity_date": "Sun, 07 Jul 2019 08:00:00 GMT",
"activity_id": "hvYBqYBRa7wwXpaStWR4V2",
"id": 9,
"record_type": "AS",
"sport_id": 1,
"user": "admin",
"value": 18
"value": 18,
"workout_date": "Sun, 07 Jul 2019 08:00:00 GMT",
"workout_id": "hvYBqYBRa7wwXpaStWR4V2"
},
{
"activity_date": "Sun, 07 Jul 2019 08:00:00 GMT",
"activity_id": "hvYBqYBRa7wwXpaStWR4V2",
"id": 10,
"record_type": "FD",
"sport_id": 1,
"user": "admin",
"value": 18
"value": 18,
"workout_date": "Sun, 07 Jul 2019 08:00:00 GMT",
"workout_id": "hvYBqYBRa7wwXpaStWR4V2"
},
{
"activity_date": "Sun, 07 Jul 2019 08:00:00 GMT",
"activity_id": "hvYBqYBRa7wwXpaStWR4V2",
"id": 11,
"record_type": "LD",
"sport_id": 1,
"user": "admin",
"value": "1:01:00"
"value": "1:01:00",
"workout_date": "Sun, 07 Jul 2019 08:00:00 GMT",
"workout_id": "hvYBqYBRa7wwXpaStWR4V2"
},
{
"activity_date": "Sun, 07 Jul 2019 08:00:00 GMT",
"activity_id": "hvYBqYBRa7wwXpaStWR4V2",
"id": 12,
"record_type": "MS",
"sport_id": 1,
"user": "admin",
"value": 18
"value": 18,
"workout_date": "Sun, 07 Jul 2019 08:00:00 GMT",
"workout_id": "hvYBqYBRa7wwXpaStWR4V2"
}
]
},

View File

@ -94,42 +94,42 @@ def get_sports(auth_user_id: int) -> Dict:
"data": {
"sports": [
{
"has_activities": true,
"has_workouts": true,
"id": 1,
"img": "/img/sports/cycling-sport.png",
"is_active": true,
"label": "Cycling (Sport)"
},
{
"has_activities": false,
"has_workouts": false,
"id": 2,
"img": "/img/sports/cycling-transport.png",
"is_active": true,
"label": "Cycling (Transport)"
},
{
"has_activities": false,
"has_workouts": false,
"id": 3,
"img": "/img/sports/hiking.png",
"is_active": true,
"label": "Hiking"
},
{
"has_activities": false,
"has_workouts": false,
"id": 4,
"img": "/img/sports/mountain-biking.png",
"is_active": true,
"label": "Mountain Biking"
},
{
"has_activities": false,
"has_workouts": false,
"id": 5,
"img": "/img/sports/running.png",
"is_active": true,
"label": "Running"
},
{
"has_activities": false,
"has_workouts": false,
"id": 6,
"img": "/img/sports/walking.png",
"is_active": true,
@ -206,7 +206,7 @@ def get_sport(auth_user_id: int, sport_id: int) -> Union[Dict, HttpResponse]:
"data": {
"sports": [
{
"has_activities": false,
"has_workouts": false,
"id": 1,
"img": "/img/sports/cycling-sport.png",
"is_active": true,
@ -283,7 +283,7 @@ def update_sport(
"data": {
"sports": [
{
"has_activities": false,
"has_workouts": false,
"id": 1,
"img": "/img/sports/cycling-sport.png",
"is_active": false,

View File

@ -14,18 +14,18 @@ from sqlalchemy import func
from ..users.models import User
from ..users.utils import authenticate, authenticate_as_admin
from .models import Activity, Sport
from .models import Sport, Workout
from .utils import get_datetime_with_tz, get_upload_dir_size
from .utils_format import convert_timedelta_to_integer
stats_blueprint = Blueprint('stats', __name__)
def get_activities(
def get_workouts(
user_name: str, filter_type: str
) -> Union[Dict, HttpResponse]:
"""
Return user activities by sport or by time
Return user workouts by sport or by time
"""
try:
user = User.query.filter_by(username=user_name).first()
@ -52,91 +52,89 @@ def get_activities(
if not sport:
return NotFoundErrorResponse('Sport does not exist.')
activities = (
Activity.query.filter(
Activity.user_id == user.id,
Activity.activity_date >= date_from if date_from else True,
Activity.activity_date < date_to + timedelta(seconds=1)
workouts = (
Workout.query.filter(
Workout.user_id == user.id,
Workout.workout_date >= date_from if date_from else True,
Workout.workout_date < date_to + timedelta(seconds=1)
if date_to
else True,
Activity.sport_id == sport_id if sport_id else True,
Workout.sport_id == sport_id if sport_id else True,
)
.order_by(Activity.activity_date.asc())
.order_by(Workout.workout_date.asc())
.all()
)
activities_list_by_sport = {}
activities_list_by_time = {} # type: ignore
for activity in activities:
workouts_list_by_sport = {}
workouts_list_by_time = {} # type: ignore
for workout in workouts:
if filter_type == 'by_sport':
sport_id = activity.sport_id
if sport_id not in activities_list_by_sport:
activities_list_by_sport[sport_id] = {
'nb_activities': 0,
sport_id = workout.sport_id
if sport_id not in workouts_list_by_sport:
workouts_list_by_sport[sport_id] = {
'nb_workouts': 0,
'total_distance': 0.0,
'total_duration': 0,
}
activities_list_by_sport[sport_id]['nb_activities'] += 1
activities_list_by_sport[sport_id]['total_distance'] += float(
activity.distance
workouts_list_by_sport[sport_id]['nb_workouts'] += 1
workouts_list_by_sport[sport_id]['total_distance'] += float(
workout.distance
)
activities_list_by_sport[sport_id][
workouts_list_by_sport[sport_id][
'total_duration'
] += convert_timedelta_to_integer(activity.moving)
] += convert_timedelta_to_integer(workout.moving)
# filter_type == 'by_time'
else:
if time == 'week':
activity_date = activity.activity_date - timedelta(
workout_date = workout.workout_date - timedelta(
days=(
activity.activity_date.isoweekday()
if activity.activity_date.isoweekday() < 7
workout.workout_date.isoweekday()
if workout.workout_date.isoweekday() < 7
else 0
)
)
time_period = datetime.strftime(activity_date, "%Y-%m-%d")
time_period = datetime.strftime(workout_date, "%Y-%m-%d")
elif time == 'weekm': # week start Monday
activity_date = activity.activity_date - timedelta(
days=activity.activity_date.weekday()
workout_date = workout.workout_date - timedelta(
days=workout.workout_date.weekday()
)
time_period = datetime.strftime(activity_date, "%Y-%m-%d")
time_period = datetime.strftime(workout_date, "%Y-%m-%d")
elif time == 'month':
time_period = datetime.strftime(
activity.activity_date, "%Y-%m"
workout.workout_date, "%Y-%m"
)
elif time == 'year' or not time:
time_period = datetime.strftime(
activity.activity_date, "%Y"
)
time_period = datetime.strftime(workout.workout_date, "%Y")
else:
return InvalidPayloadErrorResponse(
'Invalid time period.', 'fail'
)
sport_id = activity.sport_id
if time_period not in activities_list_by_time:
activities_list_by_time[time_period] = {}
if sport_id not in activities_list_by_time[time_period]:
activities_list_by_time[time_period][sport_id] = {
'nb_activities': 0,
sport_id = workout.sport_id
if time_period not in workouts_list_by_time:
workouts_list_by_time[time_period] = {}
if sport_id not in workouts_list_by_time[time_period]:
workouts_list_by_time[time_period][sport_id] = {
'nb_workouts': 0,
'total_distance': 0.0,
'total_duration': 0,
}
activities_list_by_time[time_period][sport_id][
'nb_activities'
workouts_list_by_time[time_period][sport_id][
'nb_workouts'
] += 1
activities_list_by_time[time_period][sport_id][
workouts_list_by_time[time_period][sport_id][
'total_distance'
] += float(activity.distance)
activities_list_by_time[time_period][sport_id][
] += float(workout.distance)
workouts_list_by_time[time_period][sport_id][
'total_duration'
] += convert_timedelta_to_integer(activity.moving)
] += convert_timedelta_to_integer(workout.moving)
return {
'status': 'success',
'data': {
'statistics': activities_list_by_sport
'statistics': workouts_list_by_sport
if filter_type == 'by_sport'
else activities_list_by_time
else workouts_list_by_time
},
}
except Exception as e:
@ -145,11 +143,11 @@ def get_activities(
@stats_blueprint.route('/stats/<user_name>/by_time', methods=['GET'])
@authenticate
def get_activities_by_time(
def get_workouts_by_time(
auth_user_id: int, user_name: str
) -> Union[Dict, HttpResponse]:
"""
Get activities statistics for a user by time
Get workouts statistics for a user by time
**Example requests**:
@ -163,7 +161,8 @@ def get_activities_by_time(
.. sourcecode:: http
GET /api/stats/admin/by_time?from=2018-01-01&to=2018-06-30&time=week HTTP/1.1
GET /api/stats/admin/by_time?from=2018-01-01&to=2018-06-30&time=week
HTTP/1.1
**Example responses**:
@ -179,19 +178,19 @@ def get_activities_by_time(
"statistics": {
"2017": {
"3": {
"nb_activities": 2,
"nb_workouts": 2,
"total_distance": 15.282,
"total_duration": 12341
}
},
"2019": {
"1": {
"nb_activities": 3,
"nb_workouts": 3,
"total_distance": 47,
"total_duration": 9960
},
"2": {
"nb_activities": 1,
"nb_workouts": 1,
"total_distance": 5.613,
"total_duration": 1267
}
@ -201,7 +200,7 @@ def get_activities_by_time(
"status": "success"
}
- no activities
- no workouts
.. sourcecode:: http
@ -238,20 +237,20 @@ def get_activities_by_time(
- User does not exist.
"""
return get_activities(user_name, 'by_time')
return get_workouts(user_name, 'by_time')
@stats_blueprint.route('/stats/<user_name>/by_sport', methods=['GET'])
@authenticate
def get_activities_by_sport(
def get_workouts_by_sport(
auth_user_id: int, user_name: str
) -> Union[Dict, HttpResponse]:
"""
Get activities statistics for a user by sport
Get workouts statistics for a user by sport
**Example requests**:
- without parameters (get stats for all sports with activities)
- without parameters (get stats for all sports with workouts)
.. sourcecode:: http
@ -276,17 +275,17 @@ def get_activities_by_sport(
"data": {
"statistics": {
"1": {
"nb_activities": 3,
"nb_workouts": 3,
"total_distance": 47,
"total_duration": 9960
},
"2": {
"nb_activities": 1,
"nb_workouts": 1,
"total_distance": 5.613,
"total_duration": 1267
},
"3": {
"nb_activities": 2,
"nb_workouts": 2,
"total_distance": 15.282,
"total_duration": 12341
}
@ -295,7 +294,7 @@ def get_activities_by_sport(
"status": "success"
}
- no activities
- no workouts
.. sourcecode:: http
@ -326,7 +325,7 @@ def get_activities_by_sport(
- Sport does not exist.
"""
return get_activities(user_name, 'by_sport')
return get_workouts(user_name, 'by_sport')
@stats_blueprint.route('/stats/all', methods=['GET'])
@ -351,10 +350,10 @@ def get_application_stats(auth_user_id: int) -> Dict:
{
"data": {
"activities": 3,
"sports": 3,
"uploads_dir_size": 1000,
"users": 2,
"uploads_dir_size": 1000
"workouts": 3,
},
"status": "success"
}
@ -371,17 +370,17 @@ def get_application_stats(auth_user_id: int) -> Dict:
:statuscode 403: You do not have permissions.
"""
nb_activities = Activity.query.filter().count()
nb_workouts = Workout.query.filter().count()
nb_users = User.query.filter().count()
nb_sports = (
db.session.query(func.count(Activity.sport_id))
.group_by(Activity.sport_id)
db.session.query(func.count(Workout.sport_id))
.group_by(Workout.sport_id)
.count()
)
return {
'status': 'success',
'data': {
'activities': nb_activities,
'workouts': nb_workouts,
'sports': nb_sports,
'users': nb_users,
'uploads_dir_size': get_upload_dir_size(),

View File

@ -0,0 +1,417 @@
import hashlib
import os
import tempfile
import zipfile
from datetime import datetime, timedelta
from typing import Dict, List, Optional, Tuple, Union
from uuid import UUID
import gpxpy.gpx
import pytz
from fittrackee import appLog, db
from flask import current_app
from sqlalchemy import exc
from staticmap import Line, StaticMap
from werkzeug.datastructures import FileStorage
from werkzeug.utils import secure_filename
from ..users.models import User
from .models import Sport, Workout, WorkoutSegment
from .utils_files import get_absolute_file_path
from .utils_gpx import get_gpx_info
class WorkoutException(Exception):
def __init__(
self, status: str, message: str, e: Optional[Exception] = None
) -> None:
self.status = status
self.message = message
self.e = e
def get_datetime_with_tz(
timezone: str, workout_date: datetime, gpx_data: Optional[Dict] = None
) -> Tuple[Optional[datetime], datetime]:
"""
Return naive datetime and datetime with user timezone
"""
workout_date_tz = None
if timezone:
user_tz = pytz.timezone(timezone)
utc_tz = pytz.utc
if gpx_data:
# workout date in gpx is in UTC, but in naive datetime
fmt = '%Y-%m-%d %H:%M:%S'
workout_date_string = workout_date.strftime(fmt)
workout_date_tmp = utc_tz.localize(
datetime.strptime(workout_date_string, fmt)
)
workout_date_tz = workout_date_tmp.astimezone(user_tz)
else:
workout_date_tz = user_tz.localize(workout_date)
workout_date = workout_date_tz.astimezone(utc_tz)
# make datetime 'naive' like in gpx file
workout_date = workout_date.replace(tzinfo=None)
return workout_date_tz, workout_date
def update_workout_data(
workout: Union[Workout, WorkoutSegment], gpx_data: Dict
) -> Union[Workout, WorkoutSegment]:
"""
Update workout or workout segment with data from gpx file
"""
workout.pauses = gpx_data['stop_time']
workout.moving = gpx_data['moving_time']
workout.min_alt = gpx_data['elevation_min']
workout.max_alt = gpx_data['elevation_max']
workout.descent = gpx_data['downhill']
workout.ascent = gpx_data['uphill']
workout.max_speed = gpx_data['max_speed']
workout.ave_speed = gpx_data['average_speed']
return workout
def create_workout(
user: User, workout_data: Dict, gpx_data: Optional[Dict] = None
) -> Workout:
"""
Create Workout from data entered by user and from gpx if a gpx file is
provided
"""
workout_date = (
gpx_data['start']
if gpx_data
else datetime.strptime(workout_data['workout_date'], '%Y-%m-%d %H:%M')
)
workout_date_tz, workout_date = get_datetime_with_tz(
user.timezone, workout_date, gpx_data
)
duration = (
gpx_data['duration']
if gpx_data
else timedelta(seconds=workout_data['duration'])
)
distance = gpx_data['distance'] if gpx_data else workout_data['distance']
title = gpx_data['name'] if gpx_data else workout_data.get('title', '')
new_workout = Workout(
user_id=user.id,
sport_id=workout_data['sport_id'],
workout_date=workout_date,
distance=distance,
duration=duration,
)
new_workout.notes = workout_data.get('notes')
if title is not None and title != '':
new_workout.title = title
else:
sport = Sport.query.filter_by(id=new_workout.sport_id).first()
fmt = "%Y-%m-%d %H:%M:%S"
workout_datetime = (
workout_date_tz.strftime(fmt)
if workout_date_tz
else new_workout.workout_date.strftime(fmt)
)
new_workout.title = f'{sport.label} - {workout_datetime}'
if gpx_data:
new_workout.gpx = gpx_data['filename']
new_workout.bounds = gpx_data['bounds']
update_workout_data(new_workout, gpx_data)
else:
new_workout.moving = duration
new_workout.ave_speed = (
None
if duration.seconds == 0
else float(new_workout.distance) / (duration.seconds / 3600)
)
new_workout.max_speed = new_workout.ave_speed
return new_workout
def create_segment(
workout_id: int, workout_uuid: UUID, segment_data: Dict
) -> WorkoutSegment:
"""
Create Workout Segment from gpx data
"""
new_segment = WorkoutSegment(
workout_id=workout_id,
workout_uuid=workout_uuid,
segment_id=segment_data['idx'],
)
new_segment.duration = segment_data['duration']
new_segment.distance = segment_data['distance']
update_workout_data(new_segment, segment_data)
return new_segment
def update_workout(workout: Workout) -> Workout:
"""
Update workout data from gpx file
"""
gpx_data, _, _ = get_gpx_info(
get_absolute_file_path(workout.gpx), False, False
)
updated_workout = update_workout_data(workout, gpx_data)
updated_workout.duration = gpx_data['duration']
updated_workout.distance = gpx_data['distance']
db.session.flush()
for segment_idx, segment in enumerate(updated_workout.segments):
segment_data = gpx_data['segments'][segment_idx]
updated_segment = update_workout_data(segment, segment_data)
updated_segment.duration = segment_data['duration']
updated_segment.distance = segment_data['distance']
db.session.flush()
return updated_workout
def edit_workout(
workout: Workout, workout_data: Dict, auth_user_id: int
) -> Workout:
"""
Edit an workout
Note: the gpx file is NOT modified
In a next version, map_data and weather_data will be updated
(case of a modified gpx file, see issue #7)
"""
user = User.query.filter_by(id=auth_user_id).first()
if workout_data.get('refresh'):
workout = update_workout(workout)
if workout_data.get('sport_id'):
workout.sport_id = workout_data.get('sport_id')
if workout_data.get('title'):
workout.title = workout_data.get('title')
if workout_data.get('notes'):
workout.notes = workout_data.get('notes')
if not workout.gpx:
if workout_data.get('workout_date'):
workout_date = datetime.strptime(
workout_data['workout_date'], '%Y-%m-%d %H:%M'
)
_, workout.workout_date = get_datetime_with_tz(
user.timezone, workout_date
)
if workout_data.get('duration'):
workout.duration = timedelta(seconds=workout_data['duration'])
workout.moving = workout.duration
if workout_data.get('distance'):
workout.distance = workout_data['distance']
workout.ave_speed = (
None
if workout.duration.seconds == 0
else float(workout.distance) / (workout.duration.seconds / 3600)
)
workout.max_speed = workout.ave_speed
return workout
def get_file_path(dir_path: str, filename: str) -> str:
"""
Get full path for a file
"""
if not os.path.exists(dir_path):
os.makedirs(dir_path)
file_path = os.path.join(dir_path, filename)
return file_path
def get_new_file_path(
auth_user_id: int,
workout_date: str,
sport: str,
old_filename: Optional[str] = None,
extension: Optional[str] = None,
) -> str:
"""
Generate a file path from user and workout data
"""
if not extension and old_filename:
extension = f".{old_filename.rsplit('.', 1)[1].lower()}"
_, new_filename = tempfile.mkstemp(
prefix=f'{workout_date}_{sport}_', suffix=extension
)
dir_path = os.path.join('workouts', str(auth_user_id))
if not os.path.exists(dir_path):
os.makedirs(dir_path)
file_path = os.path.join(dir_path, new_filename.split('/')[-1])
return file_path
def generate_map(map_filepath: str, map_data: List) -> None:
"""
Generate and save map image from map data
"""
m = StaticMap(400, 225, 10)
line = Line(map_data, '#3388FF', 4)
m.add_line(line)
image = m.render()
image.save(map_filepath)
def get_map_hash(map_filepath: str) -> str:
"""
Generate a md5 hash used as id instead of workout id, to retrieve map
image (maps are sensitive data)
"""
md5 = hashlib.md5()
absolute_map_filepath = get_absolute_file_path(map_filepath)
with open(absolute_map_filepath, 'rb') as f:
for chunk in iter(lambda: f.read(128 * md5.block_size), b''):
md5.update(chunk)
return md5.hexdigest()
def process_one_gpx_file(params: Dict, filename: str) -> Workout:
"""
Get all data from a gpx file to create an workout with map image
"""
try:
gpx_data, map_data, weather_data = get_gpx_info(params['file_path'])
auth_user_id = params['user'].id
new_filepath = get_new_file_path(
auth_user_id=auth_user_id,
workout_date=gpx_data['start'],
old_filename=filename,
sport=params['sport_label'],
)
absolute_gpx_filepath = get_absolute_file_path(new_filepath)
os.rename(params['file_path'], absolute_gpx_filepath)
gpx_data['filename'] = new_filepath
map_filepath = get_new_file_path(
auth_user_id=auth_user_id,
workout_date=gpx_data['start'],
extension='.png',
sport=params['sport_label'],
)
absolute_map_filepath = get_absolute_file_path(map_filepath)
generate_map(absolute_map_filepath, map_data)
except (gpxpy.gpx.GPXXMLSyntaxException, TypeError) as e:
raise WorkoutException('error', 'Error during gpx file parsing.', e)
except Exception as e:
raise WorkoutException('error', 'Error during gpx processing.', e)
try:
new_workout = create_workout(
params['user'], params['workout_data'], gpx_data
)
new_workout.map = map_filepath
new_workout.map_id = get_map_hash(map_filepath)
new_workout.weather_start = weather_data[0]
new_workout.weather_end = weather_data[1]
db.session.add(new_workout)
db.session.flush()
for segment_data in gpx_data['segments']:
new_segment = create_segment(
new_workout.id, new_workout.uuid, segment_data
)
db.session.add(new_segment)
db.session.commit()
return new_workout
except (exc.IntegrityError, ValueError) as e:
raise WorkoutException('fail', 'Error during workout save.', e)
def process_zip_archive(common_params: Dict, extract_dir: str) -> List:
"""
Get files from a zip archive and create workouts, if number of files
does not exceed defined limit.
"""
with zipfile.ZipFile(common_params['file_path'], "r") as zip_ref:
zip_ref.extractall(extract_dir)
new_workouts = []
gpx_files_limit = os.getenv('REACT_APP_GPX_LIMIT_IMPORT', 10)
if (
gpx_files_limit
and isinstance(gpx_files_limit, str)
and gpx_files_limit.isdigit()
):
gpx_files_limit = int(gpx_files_limit)
else:
gpx_files_limit = 10
appLog.warning('GPX limit not configured, set to 10.')
gpx_files_ok = 0
for gpx_file in os.listdir(extract_dir):
if (
'.' in gpx_file
and gpx_file.rsplit('.', 1)[1].lower()
in current_app.config['WORKOUT_ALLOWED_EXTENSIONS']
):
gpx_files_ok += 1
if gpx_files_ok > gpx_files_limit:
break
file_path = os.path.join(extract_dir, gpx_file)
params = common_params
params['file_path'] = file_path
new_workout = process_one_gpx_file(params, gpx_file)
new_workouts.append(new_workout)
return new_workouts
def process_files(
auth_user_id: int,
workout_data: Dict,
workout_file: FileStorage,
folders: Dict,
) -> List:
"""
Store gpx file or zip archive and create workouts
"""
if workout_file.filename is None:
raise WorkoutException('error', 'File has no filename.')
filename = secure_filename(workout_file.filename)
extension = f".{filename.rsplit('.', 1)[1].lower()}"
file_path = get_file_path(folders['tmp_dir'], filename)
sport = Sport.query.filter_by(id=workout_data.get('sport_id')).first()
if not sport:
raise WorkoutException(
'error',
f"Sport id: {workout_data.get('sport_id')} does not exist",
)
user = User.query.filter_by(id=auth_user_id).first()
common_params = {
'user': user,
'workout_data': workout_data,
'file_path': file_path,
'sport_label': sport.label,
}
try:
workout_file.save(file_path)
except Exception as e:
raise WorkoutException('error', 'Error during workout file save.', e)
if extension == ".gpx":
return [process_one_gpx_file(common_params, filename)]
else:
return process_zip_archive(common_params, folders['extract_dir'])
def get_upload_dir_size() -> int:
"""
Return upload directory size
"""
upload_path = get_absolute_file_path('')
total_size = 0
for dir_path, _, filenames in os.walk(upload_path):
for f in filenames:
fp = os.path.join(dir_path, f)
total_size += os.path.getsize(fp)
return total_size

View File

@ -6,7 +6,7 @@ import gpxpy.gpx
from .utils_weather import get_weather
class ActivityGPXException(Exception):
class WorkoutGPXException(Exception):
def __init__(
self, status: str, message: str, e: Optional[Exception] = None
) -> None:
@ -74,7 +74,7 @@ def get_gpx_info(
"""
gpx = open_gpx_file(gpx_file)
if gpx is None:
raise ActivityGPXException('not found', 'No gpx file')
raise WorkoutGPXException('not found', 'No gpx file')
gpx_data = {'name': gpx.tracks[0].name, 'segments': []}
max_speed = 0
@ -153,11 +153,11 @@ def get_gpx_segments(
if segment_id is not None:
segment_index = segment_id - 1
if segment_index > (len(track_segments) - 1):
raise ActivityGPXException(
raise WorkoutGPXException(
'not found', f'No segment with id \'{segment_id}\'', None
)
if segment_index < 0:
raise ActivityGPXException('error', 'Incorrect segment id', None)
raise WorkoutGPXException('error', 'Incorrect segment id', None)
segments = [track_segments[segment_index]]
else:
segments = track_segments