Merge pull request #110 from SamR1/sport-preferences
Add user sport preferences
This commit is contained in:
commit
d60bf52505
@ -7,6 +7,7 @@
|
||||
#### New Features
|
||||
|
||||
* [#91](https://github.com/SamR1/FitTrackee/issues/91) - Display elevation chart with min and max altitude of workout
|
||||
* [#90](https://github.com/SamR1/FitTrackee/issues/90) - Add user sports preferences
|
||||
* [#18](https://github.com/SamR1/FitTrackee/issues/18) - Better UI
|
||||
|
||||
#### Bugs Fixed
|
||||
@ -22,6 +23,8 @@
|
||||
* [#98/#109](https://github.com/SamR1/FitTrackee/pull/109) - Added stopped_speed_threshold to support slow movement
|
||||
* [#84/#93](https://github.com/SamR1/FitTrackee/pull/93) - Add elevation data and new sports
|
||||
|
||||
In this release 5 issue were closed.
|
||||
|
||||
|
||||
## Version 0.4.9 (2021/07/16)
|
||||
|
||||
|
@ -8,6 +8,8 @@ Authentication
|
||||
auth.logout_user,
|
||||
auth.get_authenticated_user_profile,
|
||||
auth.edit_user,
|
||||
auth.edit_user_preferences,
|
||||
auth.edit_user_sport_preferences,
|
||||
auth.edit_picture,
|
||||
auth.del_picture,
|
||||
auth.request_password_reset,
|
||||
|
@ -7,6 +7,7 @@
|
||||
#### New Features
|
||||
|
||||
* [#91](https://github.com/SamR1/FitTrackee/issues/91) - Display elevation chart with min and max altitude of workout
|
||||
* [#90](https://github.com/SamR1/FitTrackee/issues/90) - Add user sports preferences
|
||||
* [#18](https://github.com/SamR1/FitTrackee/issues/18) - Better UI
|
||||
|
||||
#### Bugs Fixed
|
||||
@ -22,6 +23,8 @@
|
||||
* [#98/#109](https://github.com/SamR1/FitTrackee/pull/109) - Added stopped_speed_threshold to support slow movement
|
||||
* [#84/#93](https://github.com/SamR1/FitTrackee/pull/93) - Add elevation data and new sports
|
||||
|
||||
In this release 5 issue were closed.
|
||||
|
||||
|
||||
## Version 0.4.9 (2021/07/16)
|
||||
|
||||
|
@ -31,10 +31,21 @@ Administration
|
||||
|
||||
- enable or disable a sport (a sport can be disabled even if workout with this sport exists)
|
||||
|
||||
Account
|
||||
^^^^^^^
|
||||
Account & preferences
|
||||
^^^^^^^^^^^^^^^^^^^^^
|
||||
- A user can create, update and deleted his account
|
||||
- A user can reset his password (*new in 0.3.0*)
|
||||
- A user can set language, timezone and first day of week.
|
||||
- A user can set sport preferences (*new in 0.5.0*):
|
||||
- change sport color (used for sport image and charts)
|
||||
- can override stopped speed threshold (for next uploaded gpx files)
|
||||
- disable/enable a sport.
|
||||
|
||||
.. note::
|
||||
| If a sport is disabled by an administrator, it can not be enabled by a user. In this case, it will only appear in preferences if it has user's workouts and the user can only change sport color.
|
||||
| A disabled sport (by admin or user) will not appear in dropdown when **adding a workout**.
|
||||
| A workout with a disabled sport will still be displayed in the application.
|
||||
|
||||
|
||||
|
||||
Workouts
|
||||
@ -51,29 +62,33 @@ Workouts
|
||||
- Skiing (Cross Country) (**new in 0.5.0**)
|
||||
- Trail (**new in 0.5.0**)
|
||||
- Walking
|
||||
- (*new in 0.5.0*) Stopped speed threshold used by `gpxpy <https://github.com/tkrajina/gpxpy>`_ is not the default one (0.1 km/h instead of 1 km/h) for the following sports:
|
||||
- (*new in 0.5.0*) Stopped speed threshold used by `gpxpy <https://github.com/tkrajina/gpxpy>`_ is not the default one for the following sports (0.1 km/h instead of 1 km/h):
|
||||
- Hiking
|
||||
- Skiing (Cross Country)
|
||||
- Trail
|
||||
- Walking
|
||||
- Dashboard with month calendar displaying workouts and record. The week can start on Sunday or Monday (which can be changed in the user settings). The calendar displays up to 100 workouts.
|
||||
- Workout creation by uploading a gpx file. A workout can even be created without gpx (the user must enter date, time, duration and distance)
|
||||
- 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
|
||||
|
||||
.. note::
|
||||
It can be overridden in user preferences.
|
||||
|
||||
- Dashboard with month calendar displaying workouts and record. The week can start on Sunday or Monday (which can be changed in the user preferences). The calendar displays up to 100 workouts.
|
||||
- Workout creation by uploading a gpx file. A workout can even be created without gpx (the user must enter date, time, duration and distance).
|
||||
- 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
|
||||
- Workouts list and filter
|
||||
- Workouts list and filter. Only sports with workouts are displayed in sport dropdown.
|
||||
|
||||
.. note::
|
||||
for now, only the owner of the workout can see it.
|
||||
For now, only the owner of the workout can see it.
|
||||
|
||||
Translations
|
||||
^^^^^^^^^^^^
|
||||
FitTrackee is available in English and French (which can be saved in the user settings).
|
||||
FitTrackee is available in English and French (which can be saved in the user preferences).
|
||||
|
||||
|
||||
Dashboard
|
||||
|
@ -516,6 +516,179 @@
|
||||
</dl>
|
||||
</dd></dl>
|
||||
|
||||
<dl class="http post">
|
||||
<dt class="sig sig-object http" id="post--api-auth-profile-edit-preferences">
|
||||
<span class="sig-name descname"><span class="pre">POST</span> </span><span class="sig-name descname"><span class="pre">/api/auth/profile/edit/preferences</span></span><a class="headerlink" href="#post--api-auth-profile-edit-preferences" title="Permalink to this definition">¶</a></dt>
|
||||
<dd><p>edit authenticated user preferences</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/auth/profile/edit/preferences</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>
|
||||
<p><strong>Example response</strong>:</p>
|
||||
<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">"data"</span><span class="p">:</span> <span class="p">{</span>
|
||||
<span class="nt">"admin"</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
|
||||
<span class="nt">"bio"</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
|
||||
<span class="nt">"birth_date"</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
|
||||
<span class="nt">"created_at"</span><span class="p">:</span> <span class="s2">"Sun, 14 Jul 2019 14:09:58 GMT"</span><span class="p">,</span>
|
||||
<span class="nt">"email"</span><span class="p">:</span> <span class="s2">"sam@example.com"</span><span class="p">,</span>
|
||||
<span class="nt">"first_name"</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
|
||||
<span class="nt">"language"</span><span class="p">:</span> <span class="s2">"en"</span><span class="p">,</span>
|
||||
<span class="nt">"last_name"</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
|
||||
<span class="nt">"location"</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
|
||||
<span class="nt">"nb_sports"</span><span class="p">:</span> <span class="mi">3</span><span class="p">,</span>
|
||||
<span class="nt">"nb_workouts"</span><span class="p">:</span> <span class="mi">6</span><span class="p">,</span>
|
||||
<span class="nt">"picture"</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
|
||||
<span class="nt">"records"</span><span class="p">:</span> <span class="p">[</span>
|
||||
<span class="p">{</span>
|
||||
<span class="nt">"id"</span><span class="p">:</span> <span class="mi">9</span><span class="p">,</span>
|
||||
<span class="nt">"record_type"</span><span class="p">:</span> <span class="s2">"AS"</span><span class="p">,</span>
|
||||
<span class="nt">"sport_id"</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
|
||||
<span class="nt">"user"</span><span class="p">:</span> <span class="s2">"sam"</span><span class="p">,</span>
|
||||
<span class="nt">"value"</span><span class="p">:</span> <span class="mi">18</span><span class="p">,</span>
|
||||
<span class="nt">"workout_date"</span><span class="p">:</span> <span class="s2">"Sun, 07 Jul 2019 08:00:00 GMT"</span><span class="p">,</span>
|
||||
<span class="nt">"workout_id"</span><span class="p">:</span> <span class="s2">"hvYBqYBRa7wwXpaStWR4V2"</span>
|
||||
<span class="p">},</span>
|
||||
<span class="p">{</span>
|
||||
<span class="nt">"id"</span><span class="p">:</span> <span class="mi">10</span><span class="p">,</span>
|
||||
<span class="nt">"record_type"</span><span class="p">:</span> <span class="s2">"FD"</span><span class="p">,</span>
|
||||
<span class="nt">"sport_id"</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
|
||||
<span class="nt">"user"</span><span class="p">:</span> <span class="s2">"sam"</span><span class="p">,</span>
|
||||
<span class="nt">"value"</span><span class="p">:</span> <span class="mi">18</span><span class="p">,</span>
|
||||
<span class="nt">"workout_date"</span><span class="p">:</span> <span class="s2">"Sun, 07 Jul 2019 08:00:00 GMT"</span><span class="p">,</span>
|
||||
<span class="nt">"workout_id"</span><span class="p">:</span> <span class="s2">"hvYBqYBRa7wwXpaStWR4V2"</span>
|
||||
<span class="p">},</span>
|
||||
<span class="p">{</span>
|
||||
<span class="nt">"id"</span><span class="p">:</span> <span class="mi">11</span><span class="p">,</span>
|
||||
<span class="nt">"record_type"</span><span class="p">:</span> <span class="s2">"LD"</span><span class="p">,</span>
|
||||
<span class="nt">"sport_id"</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
|
||||
<span class="nt">"user"</span><span class="p">:</span> <span class="s2">"sam"</span><span class="p">,</span>
|
||||
<span class="nt">"value"</span><span class="p">:</span> <span class="s2">"1:01:00"</span><span class="p">,</span>
|
||||
<span class="nt">"workout_date"</span><span class="p">:</span> <span class="s2">"Sun, 07 Jul 2019 08:00:00 GMT"</span><span class="p">,</span>
|
||||
<span class="nt">"workout_id"</span><span class="p">:</span> <span class="s2">"hvYBqYBRa7wwXpaStWR4V2"</span>
|
||||
<span class="p">},</span>
|
||||
<span class="p">{</span>
|
||||
<span class="nt">"id"</span><span class="p">:</span> <span class="mi">12</span><span class="p">,</span>
|
||||
<span class="nt">"record_type"</span><span class="p">:</span> <span class="s2">"MS"</span><span class="p">,</span>
|
||||
<span class="nt">"sport_id"</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
|
||||
<span class="nt">"user"</span><span class="p">:</span> <span class="s2">"sam"</span><span class="p">,</span>
|
||||
<span class="nt">"value"</span><span class="p">:</span> <span class="mi">18</span><span class="p">,</span>
|
||||
<span class="nt">"workout_date"</span><span class="p">:</span> <span class="s2">"Sun, 07 Jul 2019 08:00:00 GMT"</span><span class="p">,</span>
|
||||
<span class="nt">"workout_id"</span><span class="p">:</span> <span class="s2">"hvYBqYBRa7wwXpaStWR4V2"</span>
|
||||
<span class="p">}</span>
|
||||
<span class="p">],</span>
|
||||
<span class="nt">"sports_list"</span><span class="p">:</span> <span class="p">[</span>
|
||||
<span class="mi">1</span><span class="p">,</span>
|
||||
<span class="mi">4</span><span class="p">,</span>
|
||||
<span class="mi">6</span>
|
||||
<span class="p">],</span>
|
||||
<span class="nt">"timezone"</span><span class="p">:</span> <span class="s2">"Europe/Paris"</span><span class="p">,</span>
|
||||
<span class="nt">"total_distance"</span><span class="p">:</span> <span class="mf">67.895</span><span class="p">,</span>
|
||||
<span class="nt">"total_duration"</span><span class="p">:</span> <span class="s2">"6:50:27"</span><span class="p">,</span>
|
||||
<span class="nt">"username"</span><span class="p">:</span> <span class="nt">"sam"</span>
|
||||
<span class="nt">"weekm"</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
|
||||
<span class="p">},</span>
|
||||
<span class="nt">"message"</span><span class="p">:</span> <span class="s2">"user preferences updated"</span><span class="p">,</span>
|
||||
<span class="nt">"status"</span><span class="p">:</span> <span class="s2">"success"</span>
|
||||
<span class="p">}</span>
|
||||
</pre></div>
|
||||
</div>
|
||||
<dl class="field-list simple">
|
||||
<dt class="field-odd">Request JSON Object</dt>
|
||||
<dd class="field-odd"><ul class="simple">
|
||||
<li><p><strong>timezone</strong> (<em>string</em>) – user time zone</p></li>
|
||||
<li><p><strong>weekm</strong> (<em>string</em>) – does week start on Monday?</p></li>
|
||||
<li><p><strong>language</strong> (<em>string</em>) – language preferences</p></li>
|
||||
</ul>
|
||||
</dd>
|
||||
<dt class="field-even">Request Headers</dt>
|
||||
<dd class="field-even"><ul class="simple">
|
||||
<li><p><span><a class="reference external" href="https://tools.ietf.org/html/rfc7235#section-4.2">Authorization</a></span> – OAuth 2.0 Bearer Token</p></li>
|
||||
</ul>
|
||||
</dd>
|
||||
<dt class="field-odd">Status Codes</dt>
|
||||
<dd class="field-odd"><ul class="simple">
|
||||
<li><p><span><a class="reference external" href="https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.2.1">200 OK</a></span> – user preferences updated</p></li>
|
||||
<li><p><span><a class="reference external" href="https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.1">400 Bad Request</a></span> – <ul>
|
||||
<li><p>invalid payload</p></li>
|
||||
<li><p>password: password and password confirmation don’t match</p></li>
|
||||
</ul>
|
||||
</p></li>
|
||||
<li><p><span><a class="reference external" href="https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.2">401 Unauthorized</a></span> – <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><span><a class="reference external" href="https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.5.1">500 Internal Server Error</a></span> – error, please try again or contact the administrator</p></li>
|
||||
</ul>
|
||||
</dd>
|
||||
</dl>
|
||||
</dd></dl>
|
||||
|
||||
<dl class="http post">
|
||||
<dt class="sig sig-object http" id="post--api-auth-profile-edit-sports">
|
||||
<span class="sig-name descname"><span class="pre">POST</span> </span><span class="sig-name descname"><span class="pre">/api/auth/profile/edit/sports</span></span><a class="headerlink" href="#post--api-auth-profile-edit-sports" title="Permalink to this definition">¶</a></dt>
|
||||
<dd><p>edit authenticated user sport preferences</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/auth/profile/edit/sports</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>
|
||||
<p><strong>Example response</strong>:</p>
|
||||
<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">"data"</span><span class="p">:</span> <span class="p">{</span>
|
||||
<span class="nt">"color"</span><span class="p">:</span> <span class="s2">"#000000"</span><span class="p">,</span>
|
||||
<span class="nt">"is_active"</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
|
||||
<span class="nt">"sport_id"</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
|
||||
<span class="nt">"stopped_speed_threshold"</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
|
||||
<span class="nt">"user_id"</span><span class="p">:</span> <span class="mi">1</span>
|
||||
<span class="p">},</span>
|
||||
<span class="nt">"message"</span><span class="p">:</span> <span class="s2">"user sport preferences updated"</span><span class="p">,</span>
|
||||
<span class="nt">"status"</span><span class="p">:</span> <span class="s2">"success"</span>
|
||||
<span class="p">}</span>
|
||||
</pre></div>
|
||||
</div>
|
||||
<dl class="field-list simple">
|
||||
<dt class="field-odd">Request JSON Object</dt>
|
||||
<dd class="field-odd"><ul class="simple">
|
||||
<li><p><strong>color</strong> (<em>string</em>) – valid hexadecimal color</p></li>
|
||||
<li><p><strong>is_active</strong> (<em>boolean</em>) – is sport available when adding a workout</p></li>
|
||||
<li><p><strong>stopped_speed_threshold</strong> (<em>float</em>) – stopped speed threshold used by gpxpy</p></li>
|
||||
</ul>
|
||||
</dd>
|
||||
<dt class="field-even">Request Headers</dt>
|
||||
<dd class="field-even"><ul class="simple">
|
||||
<li><p><span><a class="reference external" href="https://tools.ietf.org/html/rfc7235#section-4.2">Authorization</a></span> – OAuth 2.0 Bearer Token</p></li>
|
||||
</ul>
|
||||
</dd>
|
||||
<dt class="field-odd">Status Codes</dt>
|
||||
<dd class="field-odd"><ul class="simple">
|
||||
<li><p><span><a class="reference external" href="https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.2.1">200 OK</a></span> – user preferences updated</p></li>
|
||||
<li><p><span><a class="reference external" href="https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.1">400 Bad Request</a></span> – <ul>
|
||||
<li><p>invalid payload</p></li>
|
||||
<li><p>invalid hexadecimal color</p></li>
|
||||
</ul>
|
||||
</p></li>
|
||||
<li><p><span><a class="reference external" href="https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.2">401 Unauthorized</a></span> – <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><span><a class="reference external" href="https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.5.1">500 Internal Server Error</a></span> – error, please try again or contact the administrator</p></li>
|
||||
</ul>
|
||||
</dd>
|
||||
</dl>
|
||||
</dd></dl>
|
||||
|
||||
<dl class="http post">
|
||||
<dt class="sig sig-object http" id="post--api-auth-picture">
|
||||
<span class="sig-name descname"><span class="pre">POST</span> </span><span class="sig-name descname"><span class="pre">/api/auth/picture</span></span><a class="headerlink" href="#post--api-auth-picture" title="Permalink to this definition">¶</a></dt>
|
||||
|
@ -147,40 +147,52 @@
|
||||
<span class="nt">"data"</span><span class="p">:</span> <span class="p">{</span>
|
||||
<span class="nt">"sports"</span><span class="p">:</span> <span class="p">[</span>
|
||||
<span class="p">{</span>
|
||||
<span class="nt">"color"</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
|
||||
<span class="nt">"id"</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
|
||||
<span class="nt">"img"</span><span class="p">:</span> <span class="s2">"/img/sports/cycling-sport.png"</span><span class="p">,</span>
|
||||
<span class="nt">"is_active"</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
|
||||
<span class="nt">"label"</span><span class="p">:</span> <span class="s2">"Cycling (Sport)"</span>
|
||||
<span class="nt">"is_active_for_user"</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
|
||||
<span class="nt">"label"</span><span class="p">:</span> <span class="s2">"Cycling (Sport)"</span><span class="p">,</span>
|
||||
<span class="nt">"stopped_speed_threshold"</span><span class="p">:</span> <span class="mi">1</span>
|
||||
<span class="p">},</span>
|
||||
<span class="p">{</span>
|
||||
<span class="nt">"color"</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
|
||||
<span class="nt">"id"</span><span class="p">:</span> <span class="mi">2</span><span class="p">,</span>
|
||||
<span class="nt">"img"</span><span class="p">:</span> <span class="s2">"/img/sports/cycling-transport.png"</span><span class="p">,</span>
|
||||
<span class="nt">"is_active"</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
|
||||
<span class="nt">"label"</span><span class="p">:</span> <span class="s2">"Cycling (Transport)"</span>
|
||||
<span class="nt">"is_active_for_user"</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
|
||||
<span class="nt">"label"</span><span class="p">:</span> <span class="s2">"Cycling (Transport)"</span><span class="p">,</span>
|
||||
<span class="nt">"stopped_speed_threshold"</span><span class="p">:</span> <span class="mi">1</span>
|
||||
<span class="p">},</span>
|
||||
<span class="p">{</span>
|
||||
<span class="nt">"color"</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
|
||||
<span class="nt">"id"</span><span class="p">:</span> <span class="mi">3</span><span class="p">,</span>
|
||||
<span class="nt">"img"</span><span class="p">:</span> <span class="s2">"/img/sports/hiking.png"</span><span class="p">,</span>
|
||||
<span class="nt">"is_active"</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
|
||||
<span class="nt">"label"</span><span class="p">:</span> <span class="s2">"Hiking"</span>
|
||||
<span class="nt">"is_active_for_user"</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
|
||||
<span class="nt">"label"</span><span class="p">:</span> <span class="s2">"Hiking"</span><span class="p">,</span>
|
||||
<span class="nt">"stopped_speed_threshold"</span><span class="p">:</span> <span class="mf">0.1</span>
|
||||
<span class="p">},</span>
|
||||
<span class="p">{</span>
|
||||
<span class="nt">"color"</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
|
||||
<span class="nt">"id"</span><span class="p">:</span> <span class="mi">4</span><span class="p">,</span>
|
||||
<span class="nt">"img"</span><span class="p">:</span> <span class="s2">"/img/sports/mountain-biking.png"</span><span class="p">,</span>
|
||||
<span class="nt">"is_active"</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
|
||||
<span class="nt">"label"</span><span class="p">:</span> <span class="s2">"Mountain Biking"</span>
|
||||
<span class="nt">"is_active_for_user"</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
|
||||
<span class="nt">"label"</span><span class="p">:</span> <span class="s2">"Mountain Biking"</span><span class="p">,</span>
|
||||
<span class="nt">"stopped_speed_threshold"</span><span class="p">:</span> <span class="mi">1</span>
|
||||
<span class="p">},</span>
|
||||
<span class="p">{</span>
|
||||
<span class="nt">"color"</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
|
||||
<span class="nt">"id"</span><span class="p">:</span> <span class="mi">5</span><span class="p">,</span>
|
||||
<span class="nt">"img"</span><span class="p">:</span> <span class="s2">"/img/sports/running.png"</span><span class="p">,</span>
|
||||
<span class="nt">"is_active"</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
|
||||
<span class="nt">"label"</span><span class="p">:</span> <span class="s2">"Running"</span>
|
||||
<span class="nt">"is_active_for_user"</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
|
||||
<span class="nt">"label"</span><span class="p">:</span> <span class="s2">"Running"</span><span class="p">,</span>
|
||||
<span class="nt">"stopped_speed_threshold"</span><span class="p">:</span> <span class="mf">0.1</span>
|
||||
<span class="p">},</span>
|
||||
<span class="p">{</span>
|
||||
<span class="nt">"color"</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
|
||||
<span class="nt">"id"</span><span class="p">:</span> <span class="mi">6</span><span class="p">,</span>
|
||||
<span class="nt">"img"</span><span class="p">:</span> <span class="s2">"/img/sports/walking.png"</span><span class="p">,</span>
|
||||
<span class="nt">"is_active"</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
|
||||
<span class="nt">"label"</span><span class="p">:</span> <span class="s2">"Walking"</span>
|
||||
<span class="nt">"is_active_for_user"</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
|
||||
<span class="nt">"label"</span><span class="p">:</span> <span class="s2">"Walking"</span><span class="p">,</span>
|
||||
<span class="nt">"stopped_speed_threshold"</span><span class="p">:</span> <span class="mf">0.1</span>
|
||||
<span class="p">}</span>
|
||||
<span class="p">]</span>
|
||||
<span class="p">},</span>
|
||||
@ -198,46 +210,58 @@
|
||||
<span class="nt">"data"</span><span class="p">:</span> <span class="p">{</span>
|
||||
<span class="nt">"sports"</span><span class="p">:</span> <span class="p">[</span>
|
||||
<span class="p">{</span>
|
||||
<span class="nt">"color"</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
|
||||
<span class="nt">"has_workouts"</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
|
||||
<span class="nt">"id"</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
|
||||
<span class="nt">"img"</span><span class="p">:</span> <span class="s2">"/img/sports/cycling-sport.png"</span><span class="p">,</span>
|
||||
<span class="nt">"is_active"</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
|
||||
<span class="nt">"label"</span><span class="p">:</span> <span class="s2">"Cycling (Sport)"</span>
|
||||
<span class="nt">"is_active_for_user"</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
|
||||
<span class="nt">"label"</span><span class="p">:</span> <span class="s2">"Cycling (Sport)"</span><span class="p">,</span>
|
||||
<span class="nt">"stopped_speed_threshold"</span><span class="p">:</span> <span class="mi">1</span>
|
||||
<span class="p">},</span>
|
||||
<span class="p">{</span>
|
||||
<span class="nt">"color"</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
|
||||
<span class="nt">"has_workouts"</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
|
||||
<span class="nt">"id"</span><span class="p">:</span> <span class="mi">2</span><span class="p">,</span>
|
||||
<span class="nt">"img"</span><span class="p">:</span> <span class="s2">"/img/sports/cycling-transport.png"</span><span class="p">,</span>
|
||||
<span class="nt">"is_active"</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
|
||||
<span class="nt">"label"</span><span class="p">:</span> <span class="s2">"Cycling (Transport)"</span>
|
||||
<span class="nt">"is_active_for_user"</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
|
||||
<span class="nt">"label"</span><span class="p">:</span> <span class="s2">"Cycling (Transport)"</span><span class="p">,</span>
|
||||
<span class="nt">"stopped_speed_threshold"</span><span class="p">:</span> <span class="mi">1</span>
|
||||
<span class="p">},</span>
|
||||
<span class="p">{</span>
|
||||
<span class="nt">"color"</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
|
||||
<span class="nt">"has_workouts"</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
|
||||
<span class="nt">"id"</span><span class="p">:</span> <span class="mi">3</span><span class="p">,</span>
|
||||
<span class="nt">"img"</span><span class="p">:</span> <span class="s2">"/img/sports/hiking.png"</span><span class="p">,</span>
|
||||
<span class="nt">"is_active"</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
|
||||
<span class="nt">"label"</span><span class="p">:</span> <span class="s2">"Hiking"</span>
|
||||
<span class="nt">"is_active_for_user"</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
|
||||
<span class="nt">"label"</span><span class="p">:</span> <span class="s2">"Hiking"</span><span class="p">,</span>
|
||||
<span class="nt">"stopped_speed_threshold"</span><span class="p">:</span> <span class="mf">0.1</span>
|
||||
<span class="p">},</span>
|
||||
<span class="p">{</span>
|
||||
<span class="nt">"color"</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
|
||||
<span class="nt">"has_workouts"</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
|
||||
<span class="nt">"id"</span><span class="p">:</span> <span class="mi">4</span><span class="p">,</span>
|
||||
<span class="nt">"img"</span><span class="p">:</span> <span class="s2">"/img/sports/mountain-biking.png"</span><span class="p">,</span>
|
||||
<span class="nt">"is_active"</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
|
||||
<span class="nt">"label"</span><span class="p">:</span> <span class="s2">"Mountain Biking"</span>
|
||||
<span class="nt">"is_active_for_user"</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
|
||||
<span class="nt">"label"</span><span class="p">:</span> <span class="s2">"Mountain Biking"</span><span class="p">,</span>
|
||||
<span class="nt">"stopped_speed_threshold"</span><span class="p">:</span> <span class="mi">1</span>
|
||||
<span class="p">},</span>
|
||||
<span class="p">{</span>
|
||||
<span class="nt">"color"</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
|
||||
<span class="nt">"has_workouts"</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
|
||||
<span class="nt">"id"</span><span class="p">:</span> <span class="mi">5</span><span class="p">,</span>
|
||||
<span class="nt">"img"</span><span class="p">:</span> <span class="s2">"/img/sports/running.png"</span><span class="p">,</span>
|
||||
<span class="nt">"is_active"</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
|
||||
<span class="nt">"label"</span><span class="p">:</span> <span class="s2">"Running"</span>
|
||||
<span class="nt">"is_active_for_user"</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
|
||||
<span class="nt">"label"</span><span class="p">:</span> <span class="s2">"Running"</span><span class="p">,</span>
|
||||
<span class="nt">"stopped_speed_threshold"</span><span class="p">:</span> <span class="mf">0.1</span>
|
||||
<span class="p">},</span>
|
||||
<span class="p">{</span>
|
||||
<span class="nt">"color"</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
|
||||
<span class="nt">"has_workouts"</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
|
||||
<span class="nt">"id"</span><span class="p">:</span> <span class="mi">6</span><span class="p">,</span>
|
||||
<span class="nt">"img"</span><span class="p">:</span> <span class="s2">"/img/sports/walking.png"</span><span class="p">,</span>
|
||||
<span class="nt">"is_active"</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
|
||||
<span class="nt">"label"</span><span class="p">:</span> <span class="s2">"Walking"</span>
|
||||
<span class="nt">"is_active_for_user"</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
|
||||
<span class="nt">"label"</span><span class="p">:</span> <span class="s2">"Walking"</span><span class="p">,</span>
|
||||
<span class="nt">"stopped_speed_threshold"</span><span class="p">:</span> <span class="mf">0.1</span>
|
||||
<span class="p">}</span>
|
||||
<span class="p">]</span>
|
||||
<span class="p">},</span>
|
||||
@ -290,10 +314,12 @@
|
||||
<span class="nt">"data"</span><span class="p">:</span> <span class="p">{</span>
|
||||
<span class="nt">"sports"</span><span class="p">:</span> <span class="p">[</span>
|
||||
<span class="p">{</span>
|
||||
<span class="nt">"color"</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
|
||||
<span class="nt">"id"</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
|
||||
<span class="nt">"img"</span><span class="p">:</span> <span class="s2">"/img/sports/cycling-sport.png"</span><span class="p">,</span>
|
||||
<span class="nt">"is_active"</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
|
||||
<span class="nt">"label"</span><span class="p">:</span> <span class="s2">"Cycling (Sport)"</span>
|
||||
<span class="nt">"is_active_for_user"</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
|
||||
<span class="nt">"label"</span><span class="p">:</span> <span class="s2">"Cycling (Sport)"</span><span class="p">,</span>
|
||||
<span class="nt">"stopped_speed_threshold"</span><span class="p">:</span> <span class="mi">1</span>
|
||||
<span class="p">}</span>
|
||||
<span class="p">]</span>
|
||||
<span class="p">},</span>
|
||||
@ -311,11 +337,13 @@
|
||||
<span class="nt">"data"</span><span class="p">:</span> <span class="p">{</span>
|
||||
<span class="nt">"sports"</span><span class="p">:</span> <span class="p">[</span>
|
||||
<span class="p">{</span>
|
||||
<span class="nt">"color"</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
|
||||
<span class="nt">"has_workouts"</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
|
||||
<span class="nt">"id"</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
|
||||
<span class="nt">"img"</span><span class="p">:</span> <span class="s2">"/img/sports/cycling-sport.png"</span><span class="p">,</span>
|
||||
<span class="nt">"is_active"</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
|
||||
<span class="nt">"label"</span><span class="p">:</span> <span class="s2">"Cycling (Sport)"</span>
|
||||
<span class="nt">"is_active_for_user"</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
|
||||
<span class="nt">"label"</span><span class="p">:</span> <span class="s2">"Cycling (Sport)"</span><span class="p">,</span>
|
||||
<span class="nt">"stopped_speed_threshold"</span><span class="p">:</span> <span class="mi">1</span>
|
||||
<span class="p">}</span>
|
||||
<span class="p">]</span>
|
||||
<span class="p">},</span>
|
||||
@ -385,11 +413,13 @@ Authenticated user must be an admin</p>
|
||||
<span class="nt">"data"</span><span class="p">:</span> <span class="p">{</span>
|
||||
<span class="nt">"sports"</span><span class="p">:</span> <span class="p">[</span>
|
||||
<span class="p">{</span>
|
||||
<span class="nt">"color"</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
|
||||
<span class="nt">"has_workouts"</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
|
||||
<span class="nt">"id"</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
|
||||
<span class="nt">"img"</span><span class="p">:</span> <span class="s2">"/img/sports/cycling-sport.png"</span><span class="p">,</span>
|
||||
<span class="nt">"is_active"</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
|
||||
<span class="nt">"label"</span><span class="p">:</span> <span class="s2">"Cycling (Sport)"</span>
|
||||
<span class="nt">"is_active_for_user"</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
|
||||
<span class="nt">"label"</span><span class="p">:</span> <span class="s2">"Cycling (Sport)"</span><span class="p">,</span>
|
||||
<span class="nt">"stopped_speed_threshold"</span><span class="p">:</span> <span class="mi">1</span>
|
||||
<span class="p">}</span>
|
||||
<span class="p">]</span>
|
||||
<span class="p">},</span>
|
||||
|
@ -278,6 +278,7 @@
|
||||
<h4>New Features<a class="headerlink" href="#new-features" title="Permalink to this headline">¶</a></h4>
|
||||
<ul class="simple">
|
||||
<li><p><a class="reference external" href="https://github.com/SamR1/FitTrackee/issues/91">#91</a> - Display elevation chart with min and max altitude of workout</p></li>
|
||||
<li><p><a class="reference external" href="https://github.com/SamR1/FitTrackee/issues/90">#90</a> - Add user sports preferences</p></li>
|
||||
<li><p><a class="reference external" href="https://github.com/SamR1/FitTrackee/issues/18">#18</a> - Better UI</p></li>
|
||||
</ul>
|
||||
</section>
|
||||
@ -300,6 +301,7 @@
|
||||
<li><p><a class="reference external" href="https://github.com/SamR1/FitTrackee/pull/109">#98/#109</a> - Added stopped_speed_threshold to support slow movement</p></li>
|
||||
<li><p><a class="reference external" href="https://github.com/SamR1/FitTrackee/pull/93">#84/#93</a> - Add elevation data and new sports</p></li>
|
||||
</ul>
|
||||
<p>In this release 5 issue were closed.</p>
|
||||
</section>
|
||||
</section>
|
||||
<section id="version-0-4-9-2021-07-16">
|
||||
|
@ -80,7 +80,7 @@
|
||||
<li><a class="reference internal" href="#">Features</a><ul>
|
||||
<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="#account-preferences">Account & preferences</a></li>
|
||||
<li><a class="reference internal" href="#workouts">Workouts</a></li>
|
||||
<li><a class="reference internal" href="#translations">Translations</a></li>
|
||||
</ul>
|
||||
@ -174,12 +174,30 @@
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
<section id="account">
|
||||
<h3>Account<a class="headerlink" href="#account" title="Permalink to this headline">¶</a></h3>
|
||||
<section id="account-preferences">
|
||||
<h3>Account & preferences<a class="headerlink" href="#account-preferences" title="Permalink to this headline">¶</a></h3>
|
||||
<ul class="simple">
|
||||
<li><p>A user can create, update and deleted his account</p></li>
|
||||
<li><p>A user can reset his password (<em>new in 0.3.0</em>)</p></li>
|
||||
<li><p>A user can set language, timezone and first day of week.</p></li>
|
||||
<li><dl class="simple">
|
||||
<dt>A user can set sport preferences (<em>new in 0.5.0</em>):</dt><dd><ul>
|
||||
<li><p>change sport color (used for sport image and charts)</p></li>
|
||||
<li><p>can override stopped speed threshold (for next uploaded gpx files)</p></li>
|
||||
<li><p>disable/enable a sport.</p></li>
|
||||
</ul>
|
||||
</dd>
|
||||
</dl>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="admonition note">
|
||||
<p class="admonition-title">Note</p>
|
||||
<div class="line-block">
|
||||
<div class="line">If a sport is disabled by an administrator, it can not be enabled by a user. In this case, it will only appear in preferences if it has user’s workouts and the user can only change sport color.</div>
|
||||
<div class="line">A disabled sport (by admin or user) will not appear in dropdown when <strong>adding a workout</strong>.</div>
|
||||
<div class="line">A workout with a disabled sport will still be displayed in the application.</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section id="workouts">
|
||||
<h3>Workouts<a class="headerlink" href="#workouts" title="Permalink to this headline">¶</a></h3>
|
||||
@ -202,7 +220,7 @@
|
||||
</dl>
|
||||
</li>
|
||||
<li><dl class="simple">
|
||||
<dt>(<em>new in 0.5.0</em>) Stopped speed threshold used by <a class="reference external" href="https://github.com/tkrajina/gpxpy">gpxpy</a> is not the default one (0.1 km/h instead of 1 km/h) for the following sports:</dt><dd><ul>
|
||||
<dt>(<em>new in 0.5.0</em>) Stopped speed threshold used by <a class="reference external" href="https://github.com/tkrajina/gpxpy">gpxpy</a> is not the default one for the following sports (0.1 km/h instead of 1 km/h):</dt><dd><ul>
|
||||
<li><p>Hiking</p></li>
|
||||
<li><p>Skiing (Cross Country)</p></li>
|
||||
<li><p>Trail</p></li>
|
||||
@ -211,10 +229,16 @@
|
||||
</dd>
|
||||
</dl>
|
||||
</li>
|
||||
<li><p>Dashboard with month calendar displaying workouts and record. The week can start on Sunday or Monday (which can be changed in the user settings). The calendar displays up to 100 workouts.</p></li>
|
||||
<li><p>Workout creation by uploading a gpx file. A workout can even be created without gpx (the user must enter date, time, duration and distance)</p></li>
|
||||
<li><p>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>
|
||||
</ul>
|
||||
<div class="admonition note">
|
||||
<p class="admonition-title">Note</p>
|
||||
<p>It can be overridden in user preferences.</p>
|
||||
</div>
|
||||
<ul class="simple">
|
||||
<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 preferences). The calendar displays up to 100 workouts.</p></li>
|
||||
<li><p>Workout creation by uploading a gpx file. A workout can even be created without gpx (the user must enter date, time, duration and distance).</p></li>
|
||||
<li><p>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>
|
||||
@ -226,16 +250,16 @@
|
||||
</dd>
|
||||
</dl>
|
||||
</li>
|
||||
<li><p>Workouts list and filter</p></li>
|
||||
<li><p>Workouts list and filter. Only sports with workouts are displayed in sport dropdown.</p></li>
|
||||
</ul>
|
||||
<div class="admonition note">
|
||||
<p class="admonition-title">Note</p>
|
||||
<p>for now, only the owner of the workout can see it.</p>
|
||||
<p>For now, only the owner of the workout can see it.</p>
|
||||
</div>
|
||||
</section>
|
||||
<section id="translations">
|
||||
<h3>Translations<a class="headerlink" href="#translations" title="Permalink to this headline">¶</a></h3>
|
||||
<p>FitTrackee is available in English and French (which can be saved in the user settings).</p>
|
||||
<p>FitTrackee is available in English and French (which can be saved in the user preferences).</p>
|
||||
</section>
|
||||
</section>
|
||||
<section id="dashboard">
|
||||
|
@ -255,6 +255,16 @@
|
||||
<td>
|
||||
<a href="api/auth.html#post--api-auth-profile-edit"><code class="xref">POST /api/auth/profile/edit</code></a></td><td>
|
||||
<em></em></td></tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>
|
||||
<a href="api/auth.html#post--api-auth-profile-edit-preferences"><code class="xref">POST /api/auth/profile/edit/preferences</code></a></td><td>
|
||||
<em></em></td></tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>
|
||||
<a href="api/auth.html#post--api-auth-profile-edit-sports"><code class="xref">POST /api/auth/profile/edit/sports</code></a></td><td>
|
||||
<em></em></td></tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>
|
||||
|
BIN
docs/objects.inv
BIN
docs/objects.inv
Binary file not shown.
File diff suppressed because one or more lines are too long
@ -8,6 +8,8 @@ Authentication
|
||||
auth.logout_user,
|
||||
auth.get_authenticated_user_profile,
|
||||
auth.edit_user,
|
||||
auth.edit_user_preferences,
|
||||
auth.edit_user_sport_preferences,
|
||||
auth.edit_picture,
|
||||
auth.del_picture,
|
||||
auth.request_password_reset,
|
||||
|
@ -31,10 +31,21 @@ Administration
|
||||
|
||||
- enable or disable a sport (a sport can be disabled even if workout with this sport exists)
|
||||
|
||||
Account
|
||||
^^^^^^^
|
||||
Account & preferences
|
||||
^^^^^^^^^^^^^^^^^^^^^
|
||||
- A user can create, update and deleted his account
|
||||
- A user can reset his password (*new in 0.3.0*)
|
||||
- A user can set language, timezone and first day of week.
|
||||
- A user can set sport preferences (*new in 0.5.0*):
|
||||
- change sport color (used for sport image and charts)
|
||||
- can override stopped speed threshold (for next uploaded gpx files)
|
||||
- disable/enable a sport.
|
||||
|
||||
.. note::
|
||||
| If a sport is disabled by an administrator, it can not be enabled by a user. In this case, it will only appear in preferences if it has user's workouts and the user can only change sport color.
|
||||
| A disabled sport (by admin or user) will not appear in dropdown when **adding a workout**.
|
||||
| A workout with a disabled sport will still be displayed in the application.
|
||||
|
||||
|
||||
|
||||
Workouts
|
||||
@ -51,29 +62,33 @@ Workouts
|
||||
- Skiing (Cross Country) (**new in 0.5.0**)
|
||||
- Trail (**new in 0.5.0**)
|
||||
- Walking
|
||||
- (*new in 0.5.0*) Stopped speed threshold used by `gpxpy <https://github.com/tkrajina/gpxpy>`_ is not the default one (0.1 km/h instead of 1 km/h) for the following sports:
|
||||
- (*new in 0.5.0*) Stopped speed threshold used by `gpxpy <https://github.com/tkrajina/gpxpy>`_ is not the default one for the following sports (0.1 km/h instead of 1 km/h):
|
||||
- Hiking
|
||||
- Skiing (Cross Country)
|
||||
- Trail
|
||||
- Walking
|
||||
- Dashboard with month calendar displaying workouts and record. The week can start on Sunday or Monday (which can be changed in the user settings). The calendar displays up to 100 workouts.
|
||||
- Workout creation by uploading a gpx file. A workout can even be created without gpx (the user must enter date, time, duration and distance)
|
||||
- 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
|
||||
|
||||
.. note::
|
||||
It can be overridden in user preferences.
|
||||
|
||||
- Dashboard with month calendar displaying workouts and record. The week can start on Sunday or Monday (which can be changed in the user preferences). The calendar displays up to 100 workouts.
|
||||
- Workout creation by uploading a gpx file. A workout can even be created without gpx (the user must enter date, time, duration and distance).
|
||||
- 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
|
||||
- Workouts list and filter
|
||||
- Workouts list and filter. Only sports with workouts are displayed in sport dropdown.
|
||||
|
||||
.. note::
|
||||
for now, only the owner of the workout can see it.
|
||||
For now, only the owner of the workout can see it.
|
||||
|
||||
Translations
|
||||
^^^^^^^^^^^^
|
||||
FitTrackee is available in English and French (which can be saved in the user settings).
|
||||
FitTrackee is available in English and French (which can be saved in the user preferences).
|
||||
|
||||
|
||||
Dashboard
|
||||
|
2
fittrackee/dist/index.html
vendored
2
fittrackee/dist/index.html
vendored
@ -1 +1 @@
|
||||
<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><!--[if IE]><link rel="icon" href="/favicon.ico"><![endif]--><link rel="stylesheet" href="/static/css/fork-awesome.min.css"><link rel="stylesheet" href="/static/css/leaflet.css"><title>FitTrackee</title><link href="/static/css/admin.04e24276.css" rel="prefetch"><link href="/static/css/main.c790adb1.css" rel="prefetch"><link href="/static/css/main~workouts.66c5ef05.css" rel="prefetch"><link href="/static/css/profile.b52bc193.css" rel="prefetch"><link href="/static/css/reset.bd9657a8.css" rel="prefetch"><link href="/static/css/workouts.d0767062.css" rel="prefetch"><link href="/static/js/admin.2f1d393d.js" rel="prefetch"><link href="/static/js/chunk-2d0c9189.c81458cc.js" rel="prefetch"><link href="/static/js/chunk-2d0cf391.020c75ea.js" rel="prefetch"><link href="/static/js/chunk-2d0da8f3.c8c3e7e8.js" rel="prefetch"><link href="/static/js/chunk-2d2248b6.d84473c1.js" rel="prefetch"><link href="/static/js/chunk-2d22523a.4b710d99.js" rel="prefetch"><link href="/static/js/main.e5da50b8.js" rel="prefetch"><link href="/static/js/main~workouts.a74990d7.js" rel="prefetch"><link href="/static/js/profile.6a786c1d.js" rel="prefetch"><link href="/static/js/reset.518e646f.js" rel="prefetch"><link href="/static/js/workouts.1c22fd12.js" rel="prefetch"><link href="/static/css/app.534b9c5c.css" rel="preload" as="style"><link href="/static/js/app.9ada5ac5.js" rel="preload" as="script"><link href="/static/js/chunk-vendors.71654064.js" rel="preload" as="script"><link href="/static/css/app.534b9c5c.css" rel="stylesheet"><link rel="icon" type="image/png" sizes="32x32" href="/img/icons/favicon-32x32.png"><link rel="icon" type="image/png" sizes="16x16" href="/img/icons/favicon-16x16.png"><link rel="manifest" href="/manifest.json"><meta name="theme-color" content="#4DBA87"><meta name="apple-mobile-web-app-capable" content="no"><meta name="apple-mobile-web-app-status-bar-style" content="default"><meta name="apple-mobile-web-app-title" content="fittrackee_client"><link rel="apple-touch-icon" href="/img/icons/apple-touch-icon-152x152.png"><link rel="mask-icon" href="/img/icons/safari-pinned-tab.svg" color="#4DBA87"><meta name="msapplication-TileImage" content="/img/icons/msapplication-icon-144x144.png"><meta name="msapplication-TileColor" content="#000000"></head><body><noscript><strong>We're sorry but FitTrackee doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div><script src="/static/js/chunk-vendors.71654064.js"></script><script src="/static/js/app.9ada5ac5.js"></script></body></html>
|
||||
<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><!--[if IE]><link rel="icon" href="/favicon.ico"><![endif]--><link rel="stylesheet" href="/static/css/fork-awesome.min.css"><link rel="stylesheet" href="/static/css/leaflet.css"><title>FitTrackee</title><link href="/static/css/admin.babfd43e.css" rel="prefetch"><link href="/static/css/main.7229c1ab.css" rel="prefetch"><link href="/static/css/main~workouts.0edb3403.css" rel="prefetch"><link href="/static/css/profile.05400f70.css" rel="prefetch"><link href="/static/css/reset.46776e72.css" rel="prefetch"><link href="/static/css/workouts.1b0a7916.css" rel="prefetch"><link href="/static/js/admin.2f1d393d.js" rel="prefetch"><link href="/static/js/chunk-2d0c9189.c81458cc.js" rel="prefetch"><link href="/static/js/chunk-2d0cf391.020c75ea.js" rel="prefetch"><link href="/static/js/chunk-2d0da8f3.c8c3e7e8.js" rel="prefetch"><link href="/static/js/chunk-2d2248b6.d84473c1.js" rel="prefetch"><link href="/static/js/chunk-2d22523a.4b710d99.js" rel="prefetch"><link href="/static/js/main.db9cee98.js" rel="prefetch"><link href="/static/js/main~workouts.a74990d7.js" rel="prefetch"><link href="/static/js/profile.62578012.js" rel="prefetch"><link href="/static/js/reset.518e646f.js" rel="prefetch"><link href="/static/js/workouts.d69cf48a.js" rel="prefetch"><link href="/static/css/app.e1e7e23c.css" rel="preload" as="style"><link href="/static/js/app.0f3b3ab5.js" rel="preload" as="script"><link href="/static/js/chunk-vendors.71654064.js" rel="preload" as="script"><link href="/static/css/app.e1e7e23c.css" rel="stylesheet"><link rel="icon" type="image/png" sizes="32x32" href="/img/icons/favicon-32x32.png"><link rel="icon" type="image/png" sizes="16x16" href="/img/icons/favicon-16x16.png"><link rel="manifest" href="/manifest.json"><meta name="theme-color" content="#4DBA87"><meta name="apple-mobile-web-app-capable" content="no"><meta name="apple-mobile-web-app-status-bar-style" content="default"><meta name="apple-mobile-web-app-title" content="fittrackee_client"><link rel="apple-touch-icon" href="/img/icons/apple-touch-icon-152x152.png"><link rel="mask-icon" href="/img/icons/safari-pinned-tab.svg" color="#4DBA87"><meta name="msapplication-TileImage" content="/img/icons/msapplication-icon-144x144.png"><meta name="msapplication-TileColor" content="#000000"></head><body><noscript><strong>We're sorry but FitTrackee doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div><script src="/static/js/chunk-vendors.71654064.js"></script><script src="/static/js/app.0f3b3ab5.js"></script></body></html>
|
@ -64,7 +64,7 @@ self.__precacheManifest = (self.__precacheManifest || []).concat([
|
||||
"url": "/img/workouts/mountains.svg"
|
||||
},
|
||||
{
|
||||
"revision": "04e22b3a72bc3a810319eb7a512e4b23",
|
||||
"revision": "926210f132992651a9543d9c76da25ba",
|
||||
"url": "/index.html"
|
||||
},
|
||||
{
|
||||
@ -76,12 +76,12 @@ self.__precacheManifest = (self.__precacheManifest || []).concat([
|
||||
"url": "/robots.txt"
|
||||
},
|
||||
{
|
||||
"revision": "ca5244fc3bcfc65816b9",
|
||||
"url": "/static/css/admin.04e24276.css"
|
||||
"revision": "33002d1c4452ecd02e50",
|
||||
"url": "/static/css/admin.babfd43e.css"
|
||||
},
|
||||
{
|
||||
"revision": "fe2e967e1efad7b13e67",
|
||||
"url": "/static/css/app.534b9c5c.css"
|
||||
"revision": "4f95d958d90a2ac1b9a0",
|
||||
"url": "/static/css/app.e1e7e23c.css"
|
||||
},
|
||||
{
|
||||
"revision": "82c1118c918377daaa71a320ab8eea42",
|
||||
@ -92,24 +92,24 @@ self.__precacheManifest = (self.__precacheManifest || []).concat([
|
||||
"url": "/static/css/leaflet.css"
|
||||
},
|
||||
{
|
||||
"revision": "d3cf46cfc6340753d540",
|
||||
"url": "/static/css/main.c790adb1.css"
|
||||
"revision": "00c35b353719122c16cd",
|
||||
"url": "/static/css/main.7229c1ab.css"
|
||||
},
|
||||
{
|
||||
"revision": "68364924c988a1f11b42",
|
||||
"url": "/static/css/main~workouts.66c5ef05.css"
|
||||
"revision": "11b770a11a1cd8dae5f4",
|
||||
"url": "/static/css/main~workouts.0edb3403.css"
|
||||
},
|
||||
{
|
||||
"revision": "3438ac3f32223591afd9",
|
||||
"url": "/static/css/profile.b52bc193.css"
|
||||
"revision": "058a877bc4b9cbf8929f",
|
||||
"url": "/static/css/profile.05400f70.css"
|
||||
},
|
||||
{
|
||||
"revision": "688d813785d3c55a7d33",
|
||||
"url": "/static/css/reset.bd9657a8.css"
|
||||
"revision": "8635e7636aa413afd289",
|
||||
"url": "/static/css/reset.46776e72.css"
|
||||
},
|
||||
{
|
||||
"revision": "5e13fc66c78986a630a0",
|
||||
"url": "/static/css/workouts.d0767062.css"
|
||||
"revision": "c78ff76a4bb0919c4b94",
|
||||
"url": "/static/css/workouts.1b0a7916.css"
|
||||
},
|
||||
{
|
||||
"revision": "e719f9244c69e28e7d00e725ca1e280e",
|
||||
@ -192,12 +192,12 @@ self.__precacheManifest = (self.__precacheManifest || []).concat([
|
||||
"url": "/static/img/pt-sans-v9-latin-regular.f1f73e45.svg"
|
||||
},
|
||||
{
|
||||
"revision": "ca5244fc3bcfc65816b9",
|
||||
"revision": "33002d1c4452ecd02e50",
|
||||
"url": "/static/js/admin.2f1d393d.js"
|
||||
},
|
||||
{
|
||||
"revision": "fe2e967e1efad7b13e67",
|
||||
"url": "/static/js/app.9ada5ac5.js"
|
||||
"revision": "4f95d958d90a2ac1b9a0",
|
||||
"url": "/static/js/app.0f3b3ab5.js"
|
||||
},
|
||||
{
|
||||
"revision": "bd7d183c9f68e5f4027d",
|
||||
@ -224,23 +224,23 @@ self.__precacheManifest = (self.__precacheManifest || []).concat([
|
||||
"url": "/static/js/chunk-vendors.71654064.js"
|
||||
},
|
||||
{
|
||||
"revision": "d3cf46cfc6340753d540",
|
||||
"url": "/static/js/main.e5da50b8.js"
|
||||
"revision": "00c35b353719122c16cd",
|
||||
"url": "/static/js/main.db9cee98.js"
|
||||
},
|
||||
{
|
||||
"revision": "68364924c988a1f11b42",
|
||||
"revision": "11b770a11a1cd8dae5f4",
|
||||
"url": "/static/js/main~workouts.a74990d7.js"
|
||||
},
|
||||
{
|
||||
"revision": "3438ac3f32223591afd9",
|
||||
"url": "/static/js/profile.6a786c1d.js"
|
||||
"revision": "058a877bc4b9cbf8929f",
|
||||
"url": "/static/js/profile.62578012.js"
|
||||
},
|
||||
{
|
||||
"revision": "688d813785d3c55a7d33",
|
||||
"revision": "8635e7636aa413afd289",
|
||||
"url": "/static/js/reset.518e646f.js"
|
||||
},
|
||||
{
|
||||
"revision": "5e13fc66c78986a630a0",
|
||||
"url": "/static/js/workouts.1c22fd12.js"
|
||||
"revision": "c78ff76a4bb0919c4b94",
|
||||
"url": "/static/js/workouts.d69cf48a.js"
|
||||
}
|
||||
]);
|
2
fittrackee/dist/service-worker.js
vendored
2
fittrackee/dist/service-worker.js
vendored
@ -14,7 +14,7 @@
|
||||
importScripts("https://storage.googleapis.com/workbox-cdn/releases/4.3.1/workbox-sw.js");
|
||||
|
||||
importScripts(
|
||||
"/precache-manifest.200309f56fa439f700e940d169066203.js"
|
||||
"/precache-manifest.c1f31e9729586ecf3c442890704f31cc.js"
|
||||
);
|
||||
|
||||
workbox.core.setCacheNameDetails({prefix: "fittrackee_client"});
|
||||
|
File diff suppressed because one or more lines are too long
1
fittrackee/dist/static/css/admin.babfd43e.css
vendored
Normal file
1
fittrackee/dist/static/css/admin.babfd43e.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
fittrackee/dist/static/css/app.534b9c5c.css
vendored
1
fittrackee/dist/static/css/app.534b9c5c.css
vendored
File diff suppressed because one or more lines are too long
1
fittrackee/dist/static/css/app.e1e7e23c.css
vendored
Normal file
1
fittrackee/dist/static/css/app.e1e7e23c.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
fittrackee/dist/static/css/main.7229c1ab.css
vendored
Normal file
1
fittrackee/dist/static/css/main.7229c1ab.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
fittrackee/dist/static/css/main.c790adb1.css
vendored
1
fittrackee/dist/static/css/main.c790adb1.css
vendored
File diff suppressed because one or more lines are too long
1
fittrackee/dist/static/css/main~workouts.0edb3403.css
vendored
Normal file
1
fittrackee/dist/static/css/main~workouts.0edb3403.css
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
fittrackee/dist/static/css/profile.05400f70.css
vendored
Normal file
1
fittrackee/dist/static/css/profile.05400f70.css
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
fittrackee/dist/static/css/reset.46776e72.css
vendored
Normal file
1
fittrackee/dist/static/css/reset.46776e72.css
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
fittrackee/dist/static/css/workouts.1b0a7916.css
vendored
Normal file
1
fittrackee/dist/static/css/workouts.1b0a7916.css
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
fittrackee/dist/static/js/app.0f3b3ab5.js
vendored
Normal file
2
fittrackee/dist/static/js/app.0f3b3ab5.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
fittrackee/dist/static/js/app.0f3b3ab5.js.map
vendored
Normal file
1
fittrackee/dist/static/js/app.0f3b3ab5.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
2
fittrackee/dist/static/js/app.9ada5ac5.js
vendored
2
fittrackee/dist/static/js/app.9ada5ac5.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
fittrackee/dist/static/js/main.db9cee98.js
vendored
Normal file
2
fittrackee/dist/static/js/main.db9cee98.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
fittrackee/dist/static/js/main.db9cee98.js.map
vendored
Normal file
1
fittrackee/dist/static/js/main.db9cee98.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
2
fittrackee/dist/static/js/main.e5da50b8.js
vendored
2
fittrackee/dist/static/js/main.e5da50b8.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
fittrackee/dist/static/js/profile.62578012.js
vendored
Normal file
2
fittrackee/dist/static/js/profile.62578012.js
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["profile"],{"0bb3":function(e,t,n){},"36e8":function(e,t,n){"use strict";n.r(t);var c=n("7a23"),r=n("dad5"),o=n("2906"),u=function(e){return Object(c["pushScopeId"])("data-v-d342b648"),e=e(),Object(c["popScopeId"])(),e},a={key:0,id:"profile",class:"container view"},s=u((function(){return Object(c["createElementVNode"])("div",{id:"bottom"},null,-1)})),d=Object(c["defineComponent"])({setup:function(e){var t=Object(o["a"])(),n=Object(c["computed"])((function(){return t.getters[r["a"].GETTERS.AUTH_USER_PROFILE]}));return function(e,t){var r=Object(c["resolveComponent"])("router-view");return Object(c["unref"])(n).username?(Object(c["openBlock"])(),Object(c["createElementBlock"])("div",a,[Object(c["createVNode"])(r,{user:Object(c["unref"])(n)},null,8,["user"]),s])):Object(c["createCommentVNode"])("",!0)}}}),b=(n("6171"),n("6b0d")),i=n.n(b);const f=i()(d,[["__scopeId","data-v-d342b648"]]);t["default"]=f},6171:function(e,t,n){"use strict";n("0bb3")},"9b98":function(e,t,n){"use strict";n("d332")},ad3d:function(e,t,n){"use strict";n.r(t);var c=n("7a23"),r=n("6c02"),o=n("3c44"),u=n("71a7"),a=n("dad5"),s=n("2906"),d={key:0,id:"user",class:"view"},b={class:"box"},i=Object(c["defineComponent"])({setup:function(e){var t=Object(r["c"])(),n=Object(s["a"])(),i=Object(c["computed"])((function(){return n.getters[a["e"].GETTERS.USER]}));return Object(c["onBeforeMount"])((function(){t.params.username&&"string"===typeof t.params.username&&n.dispatch(a["e"].ACTIONS.GET_USER,t.params.username)})),Object(c["onBeforeUnmount"])((function(){n.dispatch(a["e"].ACTIONS.EMPTY_USER)})),function(e,t){return Object(c["unref"])(i).username?(Object(c["openBlock"])(),Object(c["createElementBlock"])("div",d,[Object(c["createVNode"])(o["a"],{user:Object(c["unref"])(i)},null,8,["user"]),Object(c["createElementVNode"])("div",b,[Object(c["createVNode"])(u["a"],{user:Object(c["unref"])(i),"from-admin":!0},null,8,["user"])])])):Object(c["createCommentVNode"])("",!0)}}}),f=(n("9b98"),n("6b0d")),O=n.n(f);const p=O()(i,[["__scopeId","data-v-218f8f1e"]]);t["default"]=p},d332:function(e,t,n){}}]);
|
||||
//# sourceMappingURL=profile.62578012.js.map
|
1
fittrackee/dist/static/js/profile.62578012.js.map
vendored
Normal file
1
fittrackee/dist/static/js/profile.62578012.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -1,2 +0,0 @@
|
||||
(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["profile"],{"36e8":function(e,t,n){"use strict";n.r(t);var c=n("7a23"),r=n("dad5"),o=n("2906"),u={key:0,id:"profile",class:"container view"},a=Object(c["defineComponent"])({setup:function(e){var t=Object(o["a"])(),n=Object(c["computed"])((function(){return t.getters[r["a"].GETTERS.AUTH_USER_PROFILE]}));return function(e,t){var r=Object(c["resolveComponent"])("router-view");return Object(c["unref"])(n).username?(Object(c["openBlock"])(),Object(c["createElementBlock"])("div",u,[Object(c["createVNode"])(r,{user:Object(c["unref"])(n)},null,8,["user"])])):Object(c["createCommentVNode"])("",!0)}}}),s=(n("44ab"),n("6b0d")),b=n.n(s);const i=b()(a,[["__scopeId","data-v-bb090bfa"]]);t["default"]=i},"44ab":function(e,t,n){"use strict";n("4fe6")},"4fe6":function(e,t,n){},"9b98":function(e,t,n){"use strict";n("d332")},ad3d:function(e,t,n){"use strict";n.r(t);var c=n("7a23"),r=n("6c02"),o=n("3c44"),u=n("71a7"),a=n("dad5"),s=n("2906"),b={key:0,id:"user",class:"view"},i={class:"box"},d=Object(c["defineComponent"])({setup:function(e){var t=Object(r["c"])(),n=Object(s["a"])(),d=Object(c["computed"])((function(){return n.getters[a["e"].GETTERS.USER]}));return Object(c["onBeforeMount"])((function(){t.params.username&&"string"===typeof t.params.username&&n.dispatch(a["e"].ACTIONS.GET_USER,t.params.username)})),Object(c["onBeforeUnmount"])((function(){n.dispatch(a["e"].ACTIONS.EMPTY_USER)})),function(e,t){return Object(c["unref"])(d).username?(Object(c["openBlock"])(),Object(c["createElementBlock"])("div",b,[Object(c["createVNode"])(o["a"],{user:Object(c["unref"])(d)},null,8,["user"]),Object(c["createElementVNode"])("div",i,[Object(c["createVNode"])(u["a"],{user:Object(c["unref"])(d),"from-admin":!0},null,8,["user"])])])):Object(c["createCommentVNode"])("",!0)}}}),f=(n("9b98"),n("6b0d")),O=n.n(f);const j=O()(d,[["__scopeId","data-v-218f8f1e"]]);t["default"]=j},d332:function(e,t,n){}}]);
|
||||
//# sourceMappingURL=profile.6a786c1d.js.map
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
fittrackee/dist/static/js/workouts.d69cf48a.js
vendored
Normal file
2
fittrackee/dist/static/js/workouts.d69cf48a.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
fittrackee/dist/static/js/workouts.d69cf48a.js.map
vendored
Normal file
1
fittrackee/dist/static/js/workouts.d69cf48a.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -0,0 +1,76 @@
|
||||
"""add sport preferences
|
||||
|
||||
Revision ID: 080acc8ee956
|
||||
Revises: 9842464bb885
|
||||
Create Date: 2021-11-12 10:20:23.786727
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '080acc8ee956'
|
||||
down_revision = '9842464bb885'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
|
||||
op.drop_constraint('sports_img_key', 'sports', type_='unique')
|
||||
op.drop_column('sports', 'img')
|
||||
|
||||
op.create_table(
|
||||
'users_sports_preferences',
|
||||
sa.Column('user_id', sa.Integer(), nullable=False),
|
||||
sa.Column('sport_id', sa.Integer(), nullable=False),
|
||||
sa.Column('color', sa.String(length=50), nullable=True),
|
||||
sa.Column('is_active', sa.Boolean(), nullable=False),
|
||||
sa.Column(
|
||||
'stopped_speed_threshold',
|
||||
sa.Float(),
|
||||
nullable=False,
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
['sport_id'],
|
||||
['sports.id'],
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
['user_id'],
|
||||
['users.id'],
|
||||
),
|
||||
sa.PrimaryKeyConstraint('user_id', 'sport_id'),
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_table('users_sports_preferences')
|
||||
|
||||
op.add_column(
|
||||
'sports',
|
||||
sa.Column(
|
||||
'img', sa.VARCHAR(length=255), autoincrement=False, nullable=True
|
||||
),
|
||||
)
|
||||
op.create_unique_constraint('sports_img_key', 'sports', ['img'])
|
||||
op.execute(
|
||||
"""
|
||||
UPDATE sports AS s
|
||||
SET img = si.img
|
||||
FROM (VALUES
|
||||
('Cycling (Sport)','/img/sports/cycling-sport.png'),
|
||||
('Cycling (Transport)','/img/sports/cycling-transport.png'),
|
||||
('Hiking','/img/sports/hiking.png'),
|
||||
('Mountain Biking','/img/sports/mountain-biking.png'),
|
||||
('Running','/img/sports/running.png'),
|
||||
('Walking','/img/sports/walking.png'),
|
||||
('Mountain Biking (Electric)','/img/sports/electric-mountain-biking.png'),
|
||||
('Trail','/img/sports/trail.png'),
|
||||
('Skiing (Alpine)','/img/sports/alpine-skiing.png'),
|
||||
('Skiing (Cross Country)','/img/sports/cross-country-skiing.png'),
|
||||
('Rowing','/img/sports/rowing.png')
|
||||
) AS si(label, img)
|
||||
WHERE si.label = s.label;
|
||||
"""
|
||||
)
|
31
fittrackee/tests/fixtures/fixtures_users.py
vendored
31
fittrackee/tests/fixtures/fixtures_users.py
vendored
@ -3,7 +3,8 @@ import datetime
|
||||
import pytest
|
||||
|
||||
from fittrackee import db
|
||||
from fittrackee.users.models import User
|
||||
from fittrackee.users.models import User, UserSportPreference
|
||||
from fittrackee.workouts.models import Sport
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
@ -81,3 +82,31 @@ def user_3() -> User:
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
return user
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def user_sport_1_preference(
|
||||
user_1: User, sport_1_cycling: Sport
|
||||
) -> UserSportPreference:
|
||||
user_sport = UserSportPreference(
|
||||
user_id=user_1.id,
|
||||
sport_id=sport_1_cycling.id,
|
||||
stopped_speed_threshold=sport_1_cycling.stopped_speed_threshold,
|
||||
)
|
||||
db.session.add(user_sport)
|
||||
db.session.commit()
|
||||
return user_sport
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def user_admin_sport_1_preference(
|
||||
user_1_admin: User, sport_1_cycling: Sport
|
||||
) -> UserSportPreference:
|
||||
user_sport = UserSportPreference(
|
||||
user_id=user_1_admin.id,
|
||||
sport_id=sport_1_cycling.id,
|
||||
stopped_speed_threshold=sport_1_cycling.stopped_speed_threshold,
|
||||
)
|
||||
db.session.add(user_sport)
|
||||
db.session.commit()
|
||||
return user_sport
|
||||
|
@ -830,6 +830,186 @@ class TestUserPreferencesUpdate(ApiTestCaseMixin):
|
||||
assert 'error' in data['status']
|
||||
|
||||
|
||||
class TestUserSportPreferencesUpdate(ApiTestCaseMixin):
|
||||
def test_it_returns_error_if_payload_is_empty(
|
||||
self, app: Flask, user_1: User
|
||||
) -> None:
|
||||
client, auth_token = self.get_test_client_and_auth_token(app)
|
||||
|
||||
response = client.post(
|
||||
'/api/auth/profile/edit/sports',
|
||||
content_type='application/json',
|
||||
data=json.dumps(dict()),
|
||||
headers=dict(Authorization=f'Bearer {auth_token}'),
|
||||
)
|
||||
|
||||
data = json.loads(response.data.decode())
|
||||
assert response.status_code == 400
|
||||
assert 'invalid payload' in data['message']
|
||||
assert 'error' in data['status']
|
||||
|
||||
def test_it_returns_error_if_sport_id_is_missing(
|
||||
self, app: Flask, user_1: User
|
||||
) -> None:
|
||||
client, auth_token = self.get_test_client_and_auth_token(app)
|
||||
|
||||
response = client.post(
|
||||
'/api/auth/profile/edit/sports',
|
||||
content_type='application/json',
|
||||
data=json.dumps(dict(is_active=True)),
|
||||
headers=dict(Authorization=f'Bearer {auth_token}'),
|
||||
)
|
||||
|
||||
data = json.loads(response.data.decode())
|
||||
assert data['status'] == 'error'
|
||||
assert data['message'] == 'invalid payload'
|
||||
assert response.status_code == 400
|
||||
|
||||
def test_it_returns_error_if_sport_not_found(
|
||||
self, app: Flask, user_1: User
|
||||
) -> None:
|
||||
client, auth_token = self.get_test_client_and_auth_token(app)
|
||||
|
||||
response = client.post(
|
||||
'/api/auth/profile/edit/sports',
|
||||
content_type='application/json',
|
||||
data=json.dumps(dict(sport_id=1, is_active=True)),
|
||||
headers=dict(Authorization=f'Bearer {auth_token}'),
|
||||
)
|
||||
|
||||
data = json.loads(response.data.decode())
|
||||
assert response.status_code == 404
|
||||
assert 'not found' in data['status']
|
||||
|
||||
def test_it_returns_error_if_payload_contains_only_sport_id(
|
||||
self, app: Flask, user_1: User, sport_1_cycling: Sport
|
||||
) -> None:
|
||||
client, auth_token = self.get_test_client_and_auth_token(app)
|
||||
|
||||
response = client.post(
|
||||
'/api/auth/profile/edit/sports',
|
||||
content_type='application/json',
|
||||
data=json.dumps(dict(sport_id=1)),
|
||||
headers=dict(Authorization=f'Bearer {auth_token}'),
|
||||
)
|
||||
|
||||
data = json.loads(response.data.decode())
|
||||
assert data['status'] == 'error'
|
||||
assert data['message'] == 'invalid payload'
|
||||
assert response.status_code == 400
|
||||
|
||||
def test_it_returns_error_if_color_is_invalid(
|
||||
self, app: Flask, user_1: User, sport_1_cycling: Sport
|
||||
) -> None:
|
||||
client, auth_token = self.get_test_client_and_auth_token(app)
|
||||
|
||||
response = client.post(
|
||||
'/api/auth/profile/edit/sports',
|
||||
content_type='application/json',
|
||||
data=json.dumps(
|
||||
dict(
|
||||
sport_id=sport_1_cycling.id,
|
||||
color='invalid',
|
||||
)
|
||||
),
|
||||
headers=dict(Authorization=f'Bearer {auth_token}'),
|
||||
)
|
||||
|
||||
data = json.loads(response.data.decode())
|
||||
assert data['status'] == 'error'
|
||||
assert data['message'] == 'invalid hexadecimal color'
|
||||
assert response.status_code == 400
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'input_color',
|
||||
['#000000', '#FFF'],
|
||||
)
|
||||
def test_it_updates_sport_color_for_auth_user(
|
||||
self,
|
||||
app: Flask,
|
||||
user_1: User,
|
||||
sport_2_running: Sport,
|
||||
input_color: str,
|
||||
) -> None:
|
||||
client, auth_token = self.get_test_client_and_auth_token(app)
|
||||
|
||||
response = client.post(
|
||||
'/api/auth/profile/edit/sports',
|
||||
content_type='application/json',
|
||||
data=json.dumps(
|
||||
dict(
|
||||
sport_id=sport_2_running.id,
|
||||
color=input_color,
|
||||
)
|
||||
),
|
||||
headers=dict(Authorization=f'Bearer {auth_token}'),
|
||||
)
|
||||
|
||||
data = json.loads(response.data.decode())
|
||||
assert data['status'] == 'success'
|
||||
assert data['message'] == 'user sport preferences updated'
|
||||
assert response.status_code == 200
|
||||
assert data['data']['user_id'] == user_1.id
|
||||
assert data['data']['sport_id'] == sport_2_running.id
|
||||
assert data['data']['color'] == input_color
|
||||
assert data['data']['is_active'] is True
|
||||
assert data['data']['stopped_speed_threshold'] == 0.1
|
||||
|
||||
def test_it_disables_sport_for_auth_user(
|
||||
self, app: Flask, user_1: User, sport_1_cycling: Sport
|
||||
) -> None:
|
||||
client, auth_token = self.get_test_client_and_auth_token(app)
|
||||
|
||||
response = client.post(
|
||||
'/api/auth/profile/edit/sports',
|
||||
content_type='application/json',
|
||||
data=json.dumps(
|
||||
dict(
|
||||
sport_id=sport_1_cycling.id,
|
||||
is_active=False,
|
||||
)
|
||||
),
|
||||
headers=dict(Authorization=f'Bearer {auth_token}'),
|
||||
)
|
||||
|
||||
data = json.loads(response.data.decode())
|
||||
assert data['status'] == 'success'
|
||||
assert data['message'] == 'user sport preferences updated'
|
||||
assert response.status_code == 200
|
||||
assert data['data']['user_id'] == user_1.id
|
||||
assert data['data']['sport_id'] == sport_1_cycling.id
|
||||
assert data['data']['color'] is None
|
||||
assert data['data']['is_active'] is False
|
||||
assert data['data']['stopped_speed_threshold'] == 1
|
||||
|
||||
def test_it_updates_stopped_speed_threshold_for_auth_user(
|
||||
self, app: Flask, user_1: User, sport_1_cycling: Sport
|
||||
) -> None:
|
||||
client, auth_token = self.get_test_client_and_auth_token(app)
|
||||
|
||||
response = client.post(
|
||||
'/api/auth/profile/edit/sports',
|
||||
content_type='application/json',
|
||||
data=json.dumps(
|
||||
dict(
|
||||
sport_id=sport_1_cycling.id,
|
||||
stopped_speed_threshold=0.5,
|
||||
)
|
||||
),
|
||||
headers=dict(Authorization=f'Bearer {auth_token}'),
|
||||
)
|
||||
|
||||
data = json.loads(response.data.decode())
|
||||
assert data['status'] == 'success'
|
||||
assert data['message'] == 'user sport preferences updated'
|
||||
assert response.status_code == 200
|
||||
assert data['data']['user_id'] == user_1.id
|
||||
assert data['data']['sport_id'] == sport_1_cycling.id
|
||||
assert data['data']['color'] is None
|
||||
assert data['data']['is_active']
|
||||
assert data['data']['stopped_speed_threshold'] == 0.5
|
||||
|
||||
|
||||
class TestUserPicture(ApiTestCaseMixin):
|
||||
def test_it_updates_user_picture(self, app: Flask, user_1: User) -> None:
|
||||
client, auth_token = self.get_test_client_and_auth_token(app)
|
||||
|
@ -5,7 +5,7 @@ from unittest.mock import patch
|
||||
|
||||
from flask import Flask
|
||||
|
||||
from fittrackee.users.models import User
|
||||
from fittrackee.users.models import User, UserSportPreference
|
||||
from fittrackee.workouts.models import Sport, Workout
|
||||
|
||||
from ..api_test_case import ApiTestCaseMixin
|
||||
@ -944,6 +944,22 @@ class TestDeleteUser(ApiTestCaseMixin):
|
||||
|
||||
assert response.status_code == 204
|
||||
|
||||
def test_user_with_preferences_can_delete_its_own_account(
|
||||
self,
|
||||
app: Flask,
|
||||
user_1: User,
|
||||
sport_1_cycling: Sport,
|
||||
user_sport_1_preference: UserSportPreference,
|
||||
) -> None:
|
||||
client, auth_token = self.get_test_client_and_auth_token(app)
|
||||
|
||||
response = client.delete(
|
||||
'/api/users/test',
|
||||
headers=dict(Authorization=f'Bearer {auth_token}'),
|
||||
)
|
||||
|
||||
assert response.status_code == 204
|
||||
|
||||
def test_user_with_picture_can_delete_its_own_account(
|
||||
self, app: Flask, user_1: User, sport_1_cycling: Sport, gpx_file: str
|
||||
) -> None:
|
||||
|
@ -1,6 +1,6 @@
|
||||
from flask import Flask
|
||||
|
||||
from fittrackee.users.models import User
|
||||
from fittrackee.users.models import User, UserSportPreference
|
||||
from fittrackee.workouts.models import Sport, Workout
|
||||
|
||||
|
||||
@ -59,3 +59,19 @@ class TestUserModel:
|
||||
== workout_cycling_user_1.short_id
|
||||
)
|
||||
assert serialized_user['records'][0]['workout_date']
|
||||
|
||||
|
||||
class TestUserSportModel:
|
||||
def test_user_model(
|
||||
self,
|
||||
app: Flask,
|
||||
user_1: User,
|
||||
sport_1_cycling: Sport,
|
||||
user_sport_1_preference: UserSportPreference,
|
||||
) -> None:
|
||||
serialized_user_sport = user_sport_1_preference.serialize()
|
||||
assert serialized_user_sport['user_id'] == user_1.id
|
||||
assert serialized_user_sport['sport_id'] == sport_1_cycling.id
|
||||
assert serialized_user_sport['color'] is None
|
||||
assert serialized_user_sport['is_active']
|
||||
assert serialized_user_sport['stopped_speed_threshold'] == 1
|
||||
|
@ -5,7 +5,7 @@ from flask import Flask
|
||||
from gpxpy.gpx import MovingData
|
||||
from werkzeug.datastructures import FileStorage
|
||||
|
||||
from fittrackee.users.models import User
|
||||
from fittrackee.users.models import User, UserSportPreference
|
||||
from fittrackee.workouts.models import Sport
|
||||
from fittrackee.workouts.utils import process_files
|
||||
|
||||
@ -56,3 +56,33 @@ class TestStoppedSpeedThreshold:
|
||||
stopped_speed_threshold=expected_threshold
|
||||
)
|
||||
gpx_track_segment_mock.assert_called_with(expected_threshold)
|
||||
|
||||
def test_it_calls_get_moving_data_with_threshold_depending_from_user_preference( # noqa
|
||||
self,
|
||||
app: Flask,
|
||||
user_1: User,
|
||||
gpx_file_storage: FileStorage,
|
||||
sport_1_cycling: Sport,
|
||||
user_sport_1_preference: UserSportPreference,
|
||||
) -> None:
|
||||
expected_threshold = 0.7
|
||||
user_sport_1_preference.stopped_speed_threshold = expected_threshold
|
||||
with patch(
|
||||
'fittrackee.workouts.utils.get_new_file_path',
|
||||
return_value='/tmp/fitTrackee/uploads/test.png',
|
||||
), patch(
|
||||
'gpxpy.gpx.GPXTrackSegment.get_moving_data',
|
||||
return_value=moving_data,
|
||||
) as gpx_track_segment_mock:
|
||||
|
||||
process_files(
|
||||
auth_user_id=user_1.id,
|
||||
folders=folders,
|
||||
workout_data={'sport_id': sport_1_cycling.id},
|
||||
workout_file=gpx_file_storage,
|
||||
)
|
||||
|
||||
assert gpx_track_segment_mock.call_args_list[0] == call(
|
||||
stopped_speed_threshold=expected_threshold
|
||||
)
|
||||
gpx_track_segment_mock.assert_called_with(expected_threshold)
|
||||
|
@ -2,7 +2,8 @@ import json
|
||||
|
||||
from flask import Flask
|
||||
|
||||
from fittrackee.users.models import User
|
||||
from fittrackee import db
|
||||
from fittrackee.users.models import User, UserSportPreference
|
||||
from fittrackee.workouts.models import Sport, Workout
|
||||
|
||||
from ..api_test_case import ApiTestCaseMixin
|
||||
@ -10,8 +11,10 @@ from ..api_test_case import ApiTestCaseMixin
|
||||
expected_sport_1_cycling_result = {
|
||||
'id': 1,
|
||||
'label': 'Cycling',
|
||||
'img': None,
|
||||
'is_active': True,
|
||||
'is_active_for_user': True,
|
||||
'color': None,
|
||||
'stopped_speed_threshold': 1,
|
||||
}
|
||||
expected_sport_1_cycling_admin_result = expected_sport_1_cycling_result.copy()
|
||||
expected_sport_1_cycling_admin_result['has_workouts'] = False
|
||||
@ -19,8 +22,10 @@ expected_sport_1_cycling_admin_result['has_workouts'] = False
|
||||
expected_sport_2_running_result = {
|
||||
'id': 2,
|
||||
'label': 'Running',
|
||||
'img': None,
|
||||
'is_active': True,
|
||||
'is_active_for_user': True,
|
||||
'color': None,
|
||||
'stopped_speed_threshold': 0.1,
|
||||
}
|
||||
expected_sport_2_running_admin_result = expected_sport_2_running_result.copy()
|
||||
expected_sport_2_running_admin_result['has_workouts'] = False
|
||||
@ -28,8 +33,10 @@ expected_sport_2_running_admin_result['has_workouts'] = False
|
||||
expected_sport_1_cycling_inactive_result = {
|
||||
'id': 1,
|
||||
'label': 'Cycling',
|
||||
'img': None,
|
||||
'is_active': False,
|
||||
'is_active_for_user': False,
|
||||
'color': None,
|
||||
'stopped_speed_threshold': 1,
|
||||
}
|
||||
expected_sport_1_cycling_inactive_admin_result = (
|
||||
expected_sport_1_cycling_inactive_result.copy()
|
||||
@ -111,6 +118,39 @@ class TestGetSports(ApiTestCaseMixin):
|
||||
data['data']['sports'][1] == expected_sport_2_running_admin_result
|
||||
)
|
||||
|
||||
def test_it_gets_sports_with_auth_user_preferences(
|
||||
self,
|
||||
app: Flask,
|
||||
user_1_admin: User,
|
||||
sport_1_cycling: Sport,
|
||||
sport_2_running: Sport,
|
||||
user_admin_sport_1_preference: UserSportPreference,
|
||||
) -> None:
|
||||
user_admin_sport_1_preference.color = '#000000'
|
||||
user_admin_sport_1_preference.stopped_speed_threshold = 0.5
|
||||
user_admin_sport_1_preference.is_active = False
|
||||
db.session.commit()
|
||||
|
||||
client, auth_token = self.get_test_client_and_auth_token(
|
||||
app, as_admin=True
|
||||
)
|
||||
|
||||
response = client.get(
|
||||
'/api/sports',
|
||||
headers=dict(Authorization=f'Bearer {auth_token}'),
|
||||
)
|
||||
|
||||
data = json.loads(response.data.decode())
|
||||
assert response.status_code == 200
|
||||
assert 'success' in data['status']
|
||||
assert len(data['data']['sports']) == 2
|
||||
assert data['data']['sports'][0]['color'] == '#000000'
|
||||
assert data['data']['sports'][0]['stopped_speed_threshold'] == 0.5
|
||||
assert data['data']['sports'][0]['is_active_for_user'] is False
|
||||
assert (
|
||||
data['data']['sports'][1] == expected_sport_2_running_admin_result
|
||||
)
|
||||
|
||||
|
||||
class TestGetSport(ApiTestCaseMixin):
|
||||
def test_it_gets_a_sport(
|
||||
@ -129,6 +169,26 @@ class TestGetSport(ApiTestCaseMixin):
|
||||
assert len(data['data']['sports']) == 1
|
||||
assert data['data']['sports'][0] == expected_sport_1_cycling_result
|
||||
|
||||
def test_it_gets_a_sport_with_preferences(
|
||||
self,
|
||||
app: Flask,
|
||||
user_1: User,
|
||||
sport_1_cycling: Sport,
|
||||
user_sport_1_preference: UserSportPreference,
|
||||
) -> None:
|
||||
client, auth_token = self.get_test_client_and_auth_token(app)
|
||||
|
||||
response = client.get(
|
||||
'/api/sports/1',
|
||||
headers=dict(Authorization=f'Bearer {auth_token}'),
|
||||
)
|
||||
|
||||
data = json.loads(response.data.decode())
|
||||
assert response.status_code == 200
|
||||
assert 'success' in data['status']
|
||||
assert len(data['data']['sports']) == 1
|
||||
assert data['data']['sports'][0] == expected_sport_1_cycling_result
|
||||
|
||||
def test_it_returns_404_if_sport_does_not_exist(
|
||||
self, app: Flask, user_1: User
|
||||
) -> None:
|
||||
@ -205,6 +265,7 @@ class TestUpdateSport(ApiTestCaseMixin):
|
||||
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]['is_active_for_user'] is False
|
||||
assert data['data']['sports'][0]['has_workouts'] is False
|
||||
|
||||
def test_it_enables_a_sport(
|
||||
@ -227,6 +288,7 @@ class TestUpdateSport(ApiTestCaseMixin):
|
||||
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]['is_active_for_user'] is True
|
||||
assert data['data']['sports'][0]['has_workouts'] is False
|
||||
|
||||
def test_it_disables_a_sport_with_workouts(
|
||||
@ -252,6 +314,7 @@ class TestUpdateSport(ApiTestCaseMixin):
|
||||
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]['is_active_for_user'] is False
|
||||
assert data['data']['sports'][0]['has_workouts'] is True
|
||||
|
||||
def test_it_enables_a_sport_with_workouts(
|
||||
@ -278,8 +341,63 @@ class TestUpdateSport(ApiTestCaseMixin):
|
||||
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]['is_active_for_user'] is True
|
||||
assert data['data']['sports'][0]['has_workouts'] is True
|
||||
|
||||
def test_it_disables_a_sport_with_preferences(
|
||||
self,
|
||||
app: Flask,
|
||||
user_1_admin: User,
|
||||
sport_1_cycling: Sport,
|
||||
user_admin_sport_1_preference: UserSportPreference,
|
||||
) -> None:
|
||||
client, auth_token = self.get_test_client_and_auth_token(
|
||||
app, as_admin=True
|
||||
)
|
||||
|
||||
response = client.patch(
|
||||
'/api/sports/1',
|
||||
content_type='application/json',
|
||||
data=json.dumps(dict(is_active=False)),
|
||||
headers=dict(Authorization=f'Bearer {auth_token}'),
|
||||
)
|
||||
|
||||
data = json.loads(response.data.decode())
|
||||
assert response.status_code == 200
|
||||
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]['is_active_for_user'] is False
|
||||
assert data['data']['sports'][0]['is_active_for_user'] is False
|
||||
assert data['data']['sports'][0]['has_workouts'] is False
|
||||
|
||||
def test_it_enables_a_sport_with_preferences(
|
||||
self,
|
||||
app: Flask,
|
||||
user_1_admin: User,
|
||||
sport_1_cycling: Sport,
|
||||
user_admin_sport_1_preference: UserSportPreference,
|
||||
) -> None:
|
||||
sport_1_cycling.is_active = False
|
||||
client, auth_token = self.get_test_client_and_auth_token(
|
||||
app, as_admin=True
|
||||
)
|
||||
|
||||
response = client.patch(
|
||||
'/api/sports/1',
|
||||
content_type='application/json',
|
||||
data=json.dumps(dict(is_active=True)),
|
||||
headers=dict(Authorization=f'Bearer {auth_token}'),
|
||||
)
|
||||
|
||||
data = json.loads(response.data.decode())
|
||||
assert response.status_code == 200
|
||||
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]['is_active_for_user'] is True
|
||||
assert data['data']['sports'][0]['has_workouts'] is False
|
||||
|
||||
def test_returns_error_if_user_has_no_admin_rights(
|
||||
self, app: Flask, user_1: User, sport_1_cycling: Sport
|
||||
) -> None:
|
||||
|
@ -2,7 +2,8 @@ from typing import Dict, Optional
|
||||
|
||||
from flask import Flask
|
||||
|
||||
from fittrackee.users.models import User
|
||||
from fittrackee import db
|
||||
from fittrackee.users.models import User, UserSportPreference
|
||||
from fittrackee.workouts.models import Sport, Workout
|
||||
|
||||
|
||||
@ -15,10 +16,13 @@ class TestSportModel:
|
||||
assert 'Cycling' == sport.label
|
||||
assert '<Sport \'Cycling\'>' == str(sport)
|
||||
|
||||
serialized_sport = sport.serialize(is_admin)
|
||||
serialized_sport = sport.serialize(is_admin=is_admin)
|
||||
assert 1 == serialized_sport['id']
|
||||
assert 'Cycling' == serialized_sport['label']
|
||||
assert serialized_sport['is_active'] is True
|
||||
assert serialized_sport['is_active_for_user'] is True
|
||||
assert serialized_sport['color'] is None
|
||||
assert serialized_sport['stopped_speed_threshold'] == 1
|
||||
return serialized_sport
|
||||
|
||||
def test_sport_model(self, app: Flask, sport_1_cycling: Sport) -> None:
|
||||
@ -44,3 +48,87 @@ class TestSportModel:
|
||||
) -> None:
|
||||
serialized_sport = self.assert_sport_model(sport_1_cycling, True)
|
||||
assert serialized_sport['has_workouts'] is True
|
||||
|
||||
|
||||
class TestSportModelWithPreferences:
|
||||
def test_sport_model_with_color_preference(
|
||||
self,
|
||||
app: Flask,
|
||||
user_1: User,
|
||||
sport_1_cycling: Sport,
|
||||
user_sport_1_preference: UserSportPreference,
|
||||
) -> None:
|
||||
user_sport_1_preference.color = '#00000'
|
||||
|
||||
serialized_sport = sport_1_cycling.serialize(
|
||||
sport_preferences=user_sport_1_preference.serialize()
|
||||
)
|
||||
assert serialized_sport['id'] == 1
|
||||
assert serialized_sport['label'] == 'Cycling'
|
||||
assert serialized_sport['is_active'] is True
|
||||
assert serialized_sport['is_active_for_user'] is True
|
||||
assert serialized_sport['color'] == '#00000'
|
||||
assert serialized_sport['stopped_speed_threshold'] == 1
|
||||
assert 'has_workouts' not in serialized_sport
|
||||
|
||||
def test_sport_model_with_is_active_preference(
|
||||
self,
|
||||
app: Flask,
|
||||
user_1: User,
|
||||
sport_1_cycling: Sport,
|
||||
user_sport_1_preference: UserSportPreference,
|
||||
) -> None:
|
||||
user_sport_1_preference.is_active = False
|
||||
|
||||
serialized_sport = sport_1_cycling.serialize(
|
||||
sport_preferences=user_sport_1_preference.serialize()
|
||||
)
|
||||
assert serialized_sport['id'] == 1
|
||||
assert serialized_sport['label'] == 'Cycling'
|
||||
assert serialized_sport['is_active'] is True
|
||||
assert serialized_sport['is_active_for_user'] is False
|
||||
assert serialized_sport['color'] is None
|
||||
assert serialized_sport['stopped_speed_threshold'] == 1
|
||||
assert 'has_workouts' not in serialized_sport
|
||||
|
||||
def test_inactive_sport_model_with_is_active_preference(
|
||||
self,
|
||||
app: Flask,
|
||||
user_1: User,
|
||||
sport_1_cycling: Sport,
|
||||
user_sport_1_preference: UserSportPreference,
|
||||
) -> None:
|
||||
sport_1_cycling.is_active = False
|
||||
user_sport_1_preference.is_active = True
|
||||
|
||||
serialized_sport = sport_1_cycling.serialize(
|
||||
sport_preferences=user_sport_1_preference.serialize()
|
||||
)
|
||||
assert serialized_sport['id'] == 1
|
||||
assert serialized_sport['label'] == 'Cycling'
|
||||
assert serialized_sport['is_active'] is False
|
||||
assert serialized_sport['is_active_for_user'] is False
|
||||
assert serialized_sport['color'] is None
|
||||
assert serialized_sport['stopped_speed_threshold'] == 1
|
||||
assert 'has_workouts' not in serialized_sport
|
||||
|
||||
def test_sport_model_with_threshold_preference(
|
||||
self,
|
||||
app: Flask,
|
||||
user_1: User,
|
||||
sport_1_cycling: Sport,
|
||||
user_sport_1_preference: UserSportPreference,
|
||||
) -> None:
|
||||
user_sport_1_preference.stopped_speed_threshold = 0.5
|
||||
db.session.commit()
|
||||
|
||||
serialized_sport = sport_1_cycling.serialize(
|
||||
sport_preferences=user_sport_1_preference.serialize()
|
||||
)
|
||||
assert serialized_sport['id'] == 1
|
||||
assert serialized_sport['label'] == 'Cycling'
|
||||
assert serialized_sport['is_active'] is True
|
||||
assert serialized_sport['is_active_for_user'] is True
|
||||
assert serialized_sport['color'] is None
|
||||
assert serialized_sport['stopped_speed_threshold'] == 0.5
|
||||
assert 'has_workouts' not in serialized_sport
|
||||
|
@ -1,5 +1,6 @@
|
||||
import datetime
|
||||
import os
|
||||
import re
|
||||
from typing import Dict, Tuple, Union
|
||||
|
||||
import jwt
|
||||
@ -10,6 +11,7 @@ from werkzeug.utils import secure_filename
|
||||
|
||||
from fittrackee import appLog, bcrypt, db
|
||||
from fittrackee.responses import (
|
||||
DataNotFoundErrorResponse,
|
||||
ForbiddenErrorResponse,
|
||||
HttpResponse,
|
||||
InvalidPayloadErrorResponse,
|
||||
@ -19,15 +21,18 @@ from fittrackee.responses import (
|
||||
)
|
||||
from fittrackee.tasks import reset_password_email
|
||||
from fittrackee.utils import get_readable_duration, verify_extension_and_size
|
||||
from fittrackee.workouts.models import Sport
|
||||
from fittrackee.workouts.utils_files import get_absolute_file_path
|
||||
|
||||
from .decorators import authenticate
|
||||
from .models import User
|
||||
from .models import User, UserSportPreference
|
||||
from .utils import check_passwords, register_controls
|
||||
from .utils_token import decode_user_token
|
||||
|
||||
auth_blueprint = Blueprint('auth', __name__)
|
||||
|
||||
HEX_COLOR_REGEX = regex = "^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$"
|
||||
|
||||
|
||||
@auth_blueprint.route('/auth/register', methods=['POST'])
|
||||
def register_user() -> Union[Tuple[Dict, int], HttpResponse]:
|
||||
@ -677,6 +682,108 @@ def edit_user_preferences(auth_user_id: int) -> Union[Dict, HttpResponse]:
|
||||
return handle_error_and_return_response(e, db=db)
|
||||
|
||||
|
||||
@auth_blueprint.route('/auth/profile/edit/sports', methods=['POST'])
|
||||
@authenticate
|
||||
def edit_user_sport_preferences(
|
||||
auth_user_id: int,
|
||||
) -> Union[Dict, HttpResponse]:
|
||||
"""
|
||||
edit authenticated user sport preferences
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /api/auth/profile/edit/sports HTTP/1.1
|
||||
Content-Type: application/json
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"data": {
|
||||
"color": "#000000",
|
||||
"is_active": true,
|
||||
"sport_id": 1,
|
||||
"stopped_speed_threshold": 1,
|
||||
"user_id": 1
|
||||
},
|
||||
"message": "user sport preferences updated",
|
||||
"status": "success"
|
||||
}
|
||||
|
||||
:<json string color: valid hexadecimal color
|
||||
:<json boolean is_active: is sport available when adding a workout
|
||||
:<json float stopped_speed_threshold: stopped speed threshold used by gpxpy
|
||||
|
||||
:reqheader Authorization: OAuth 2.0 Bearer Token
|
||||
|
||||
:statuscode 200: user preferences updated
|
||||
:statuscode 400:
|
||||
- invalid payload
|
||||
- invalid hexadecimal color
|
||||
:statuscode 401:
|
||||
- provide a valid auth token
|
||||
- signature expired, please log in again
|
||||
- invalid token, please log in again
|
||||
:statuscode 500: error, please try again or contact the administrator
|
||||
|
||||
"""
|
||||
post_data = request.get_json()
|
||||
if (
|
||||
not post_data
|
||||
or 'sport_id' not in post_data
|
||||
or len(post_data.keys()) == 1
|
||||
):
|
||||
return InvalidPayloadErrorResponse()
|
||||
|
||||
sport_id = post_data.get('sport_id')
|
||||
sport = Sport.query.filter_by(id=sport_id).first()
|
||||
if not sport:
|
||||
return DataNotFoundErrorResponse('sports')
|
||||
|
||||
color = post_data.get('color')
|
||||
is_active = post_data.get('is_active')
|
||||
stopped_speed_threshold = post_data.get('stopped_speed_threshold')
|
||||
|
||||
try:
|
||||
user_sport = UserSportPreference.query.filter_by(
|
||||
user_id=auth_user_id,
|
||||
sport_id=sport_id,
|
||||
).first()
|
||||
if not user_sport:
|
||||
user_sport = UserSportPreference(
|
||||
user_id=auth_user_id,
|
||||
sport_id=sport_id,
|
||||
stopped_speed_threshold=sport.stopped_speed_threshold,
|
||||
)
|
||||
db.session.add(user_sport)
|
||||
db.session.flush()
|
||||
if color:
|
||||
if re.match(HEX_COLOR_REGEX, color) is None:
|
||||
return InvalidPayloadErrorResponse('invalid hexadecimal color')
|
||||
user_sport.color = color
|
||||
if is_active is not None:
|
||||
user_sport.is_active = is_active
|
||||
if stopped_speed_threshold:
|
||||
user_sport.stopped_speed_threshold = stopped_speed_threshold
|
||||
db.session.commit()
|
||||
|
||||
return {
|
||||
'status': 'success',
|
||||
'message': 'user sport preferences updated',
|
||||
'data': user_sport.serialize(),
|
||||
}
|
||||
|
||||
# handler errors
|
||||
except (exc.IntegrityError, exc.OperationalError, ValueError) as e:
|
||||
return handle_error_and_return_response(e, db=db)
|
||||
|
||||
|
||||
@auth_blueprint.route('/auth/picture', methods=['POST'])
|
||||
@authenticate
|
||||
def edit_picture(auth_user_id: int) -> Union[Dict, HttpResponse]:
|
||||
|
@ -143,3 +143,41 @@ class User(BaseModel):
|
||||
'total_distance': float(total[0]),
|
||||
'total_duration': str(total[1]),
|
||||
}
|
||||
|
||||
|
||||
class UserSportPreference(BaseModel):
|
||||
__tablename__ = 'users_sports_preferences'
|
||||
|
||||
user_id = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey('users.id'),
|
||||
primary_key=True,
|
||||
)
|
||||
sport_id = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey('sports.id'),
|
||||
primary_key=True,
|
||||
)
|
||||
color = db.Column(db.String(50), nullable=True)
|
||||
is_active = db.Column(db.Boolean, default=True, nullable=False)
|
||||
stopped_speed_threshold = db.Column(db.Float, default=1.0, nullable=False)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
user_id: int,
|
||||
sport_id: int,
|
||||
stopped_speed_threshold: float,
|
||||
) -> None:
|
||||
self.user_id = user_id
|
||||
self.sport_id = sport_id
|
||||
self.is_active = True
|
||||
self.stopped_speed_threshold = stopped_speed_threshold
|
||||
|
||||
def serialize(self) -> Dict:
|
||||
return {
|
||||
'user_id': self.user_id,
|
||||
'sport_id': self.sport_id,
|
||||
'color': self.color,
|
||||
'is_active': self.is_active,
|
||||
'stopped_speed_threshold': self.stopped_speed_threshold,
|
||||
}
|
||||
|
@ -14,10 +14,11 @@ from fittrackee.responses import (
|
||||
UserNotFoundErrorResponse,
|
||||
handle_error_and_return_response,
|
||||
)
|
||||
from fittrackee.workouts.models import Record, Workout, WorkoutSegment
|
||||
from fittrackee.workouts.utils_files import get_absolute_file_path
|
||||
|
||||
from .decorators import authenticate, authenticate_as_admin
|
||||
from .models import User, Workout
|
||||
from .models import User, UserSportPreference
|
||||
|
||||
users_blueprint = Blueprint('users', __name__)
|
||||
|
||||
@ -555,9 +556,15 @@ def delete_user(
|
||||
'no other user has admin rights'
|
||||
)
|
||||
|
||||
for workout in Workout.query.filter_by(user_id=user.id).all():
|
||||
db.session.delete(workout)
|
||||
db.session.flush()
|
||||
db.session.query(UserSportPreference).filter(
|
||||
UserSportPreference.user_id == user.id
|
||||
).delete()
|
||||
db.session.query(Record).filter(Record.user_id == user.id).delete()
|
||||
db.session.query(WorkoutSegment).filter(
|
||||
WorkoutSegment.workout_id == Workout.id, Workout.user_id == user.id
|
||||
).delete(synchronize_session=False)
|
||||
db.session.query(Workout).filter(Workout.user_id == user.id).delete()
|
||||
db.session.flush()
|
||||
user_picture = user.picture
|
||||
db.session.delete(user)
|
||||
db.session.commit()
|
||||
|
@ -70,7 +70,6 @@ class Sport(BaseModel):
|
||||
__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)
|
||||
stopped_speed_threshold = db.Column(db.Float, default=1.0, nullable=False)
|
||||
workouts = db.relationship(
|
||||
@ -86,12 +85,30 @@ class Sport(BaseModel):
|
||||
def __init__(self, label: str) -> None:
|
||||
self.label = label
|
||||
|
||||
def serialize(self, is_admin: Optional[bool] = False) -> Dict:
|
||||
def serialize(
|
||||
self,
|
||||
is_admin: Optional[bool] = False,
|
||||
sport_preferences: Optional[Dict] = None,
|
||||
) -> Dict:
|
||||
serialized_sport = {
|
||||
'id': self.id,
|
||||
'label': self.label,
|
||||
'img': self.img,
|
||||
'is_active': self.is_active,
|
||||
'is_active_for_user': (
|
||||
self.is_active
|
||||
if sport_preferences is None
|
||||
else (sport_preferences['is_active'] and self.is_active)
|
||||
),
|
||||
'color': (
|
||||
None
|
||||
if sport_preferences is None
|
||||
else sport_preferences['color']
|
||||
),
|
||||
'stopped_speed_threshold': (
|
||||
self.stopped_speed_threshold
|
||||
if sport_preferences is None
|
||||
else sport_preferences['stopped_speed_threshold']
|
||||
),
|
||||
}
|
||||
if is_admin:
|
||||
serialized_sport['has_workouts'] = len(self.workouts) > 0
|
||||
|
@ -11,7 +11,7 @@ from fittrackee.responses import (
|
||||
handle_error_and_return_response,
|
||||
)
|
||||
from fittrackee.users.decorators import authenticate, authenticate_as_admin
|
||||
from fittrackee.users.models import User
|
||||
from fittrackee.users.models import User, UserSportPreference
|
||||
|
||||
from .models import Sport
|
||||
|
||||
@ -44,40 +44,52 @@ def get_sports(auth_user_id: int) -> Dict:
|
||||
"data": {
|
||||
"sports": [
|
||||
{
|
||||
"color": null,
|
||||
"id": 1,
|
||||
"img": "/img/sports/cycling-sport.png",
|
||||
"is_active": true,
|
||||
"label": "Cycling (Sport)"
|
||||
"is_active_for_user": true,
|
||||
"label": "Cycling (Sport)",
|
||||
"stopped_speed_threshold": 1
|
||||
},
|
||||
{
|
||||
"color": null,
|
||||
"id": 2,
|
||||
"img": "/img/sports/cycling-transport.png",
|
||||
"is_active": true,
|
||||
"label": "Cycling (Transport)"
|
||||
"is_active_for_user": true,
|
||||
"label": "Cycling (Transport)",
|
||||
"stopped_speed_threshold": 1
|
||||
},
|
||||
{
|
||||
"color": null,
|
||||
"id": 3,
|
||||
"img": "/img/sports/hiking.png",
|
||||
"is_active": true,
|
||||
"label": "Hiking"
|
||||
"is_active_for_user": true,
|
||||
"label": "Hiking",
|
||||
"stopped_speed_threshold": 0.1
|
||||
},
|
||||
{
|
||||
"color": null,
|
||||
"id": 4,
|
||||
"img": "/img/sports/mountain-biking.png",
|
||||
"is_active": true,
|
||||
"label": "Mountain Biking"
|
||||
"is_active_for_user": true,
|
||||
"label": "Mountain Biking",
|
||||
"stopped_speed_threshold": 1
|
||||
},
|
||||
{
|
||||
"color": null,
|
||||
"id": 5,
|
||||
"img": "/img/sports/running.png",
|
||||
"is_active": true,
|
||||
"label": "Running"
|
||||
"is_active_for_user": true,
|
||||
"label": "Running",
|
||||
"stopped_speed_threshold": 0.1
|
||||
},
|
||||
{
|
||||
"color": null,
|
||||
"id": 6,
|
||||
"img": "/img/sports/walking.png",
|
||||
"is_active": true,
|
||||
"label": "Walking"
|
||||
"is_active_for_user": true,
|
||||
"label": "Walking",
|
||||
"stopped_speed_threshold": 0.1
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -95,46 +107,58 @@ def get_sports(auth_user_id: int) -> Dict:
|
||||
"data": {
|
||||
"sports": [
|
||||
{
|
||||
"color": null,
|
||||
"has_workouts": true,
|
||||
"id": 1,
|
||||
"img": "/img/sports/cycling-sport.png",
|
||||
"is_active": true,
|
||||
"label": "Cycling (Sport)"
|
||||
"is_active_for_user": true,
|
||||
"label": "Cycling (Sport)",
|
||||
"stopped_speed_threshold": 1
|
||||
},
|
||||
{
|
||||
"color": null,
|
||||
"has_workouts": false,
|
||||
"id": 2,
|
||||
"img": "/img/sports/cycling-transport.png",
|
||||
"is_active": true,
|
||||
"label": "Cycling (Transport)"
|
||||
"is_active_for_user": true,
|
||||
"label": "Cycling (Transport)",
|
||||
"stopped_speed_threshold": 1
|
||||
},
|
||||
{
|
||||
"color": null,
|
||||
"has_workouts": false,
|
||||
"id": 3,
|
||||
"img": "/img/sports/hiking.png",
|
||||
"is_active": true,
|
||||
"label": "Hiking"
|
||||
"is_active_for_user": true,
|
||||
"label": "Hiking",
|
||||
"stopped_speed_threshold": 0.1
|
||||
},
|
||||
{
|
||||
"color": null,
|
||||
"has_workouts": false,
|
||||
"id": 4,
|
||||
"img": "/img/sports/mountain-biking.png",
|
||||
"is_active": true,
|
||||
"label": "Mountain Biking"
|
||||
"is_active_for_user": true,
|
||||
"label": "Mountain Biking",
|
||||
"stopped_speed_threshold": 1
|
||||
},
|
||||
{
|
||||
"color": null,
|
||||
"has_workouts": false,
|
||||
"id": 5,
|
||||
"img": "/img/sports/running.png",
|
||||
"is_active": true,
|
||||
"label": "Running"
|
||||
"is_active_for_user": true,
|
||||
"label": "Running",
|
||||
"stopped_speed_threshold": 0.1
|
||||
},
|
||||
{
|
||||
"color": null,
|
||||
"has_workouts": false,
|
||||
"id": 6,
|
||||
"img": "/img/sports/walking.png",
|
||||
"is_active": true,
|
||||
"label": "Walking"
|
||||
"is_active_for_user": true,
|
||||
"label": "Walking",
|
||||
"stopped_speed_threshold": 0.1
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -154,9 +178,22 @@ def get_sports(auth_user_id: int) -> Dict:
|
||||
"""
|
||||
user = User.query.filter_by(id=int(auth_user_id)).first()
|
||||
sports = Sport.query.order_by(Sport.id).all()
|
||||
sports_data = []
|
||||
for sport in sports:
|
||||
sport_preferences = UserSportPreference.query.filter_by(
|
||||
user_id=user.id, sport_id=sport.id
|
||||
).first()
|
||||
sports_data.append(
|
||||
sport.serialize(
|
||||
is_admin=user.admin,
|
||||
sport_preferences=sport_preferences.serialize()
|
||||
if sport_preferences
|
||||
else None,
|
||||
)
|
||||
)
|
||||
return {
|
||||
'status': 'success',
|
||||
'data': {'sports': [sport.serialize(user.admin) for sport in sports]},
|
||||
'data': {'sports': sports_data},
|
||||
}
|
||||
|
||||
|
||||
@ -186,10 +223,12 @@ def get_sport(auth_user_id: int, sport_id: int) -> Union[Dict, HttpResponse]:
|
||||
"data": {
|
||||
"sports": [
|
||||
{
|
||||
"color": null,
|
||||
"id": 1,
|
||||
"img": "/img/sports/cycling-sport.png",
|
||||
"is_active": true,
|
||||
"label": "Cycling (Sport)"
|
||||
"is_active_for_user": true,
|
||||
"label": "Cycling (Sport)",
|
||||
"stopped_speed_threshold": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -207,11 +246,13 @@ def get_sport(auth_user_id: int, sport_id: int) -> Union[Dict, HttpResponse]:
|
||||
"data": {
|
||||
"sports": [
|
||||
{
|
||||
"color": null,
|
||||
"has_workouts": false,
|
||||
"id": 1,
|
||||
"img": "/img/sports/cycling-sport.png",
|
||||
"is_active": true,
|
||||
"label": "Cycling (Sport)"
|
||||
"is_active_for_user": true,
|
||||
"label": "Cycling (Sport)",
|
||||
"stopped_speed_threshold": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -248,9 +289,21 @@ def get_sport(auth_user_id: int, sport_id: int) -> Union[Dict, HttpResponse]:
|
||||
user = User.query.filter_by(id=int(auth_user_id)).first()
|
||||
sport = Sport.query.filter_by(id=sport_id).first()
|
||||
if sport:
|
||||
sport_preferences = UserSportPreference.query.filter_by(
|
||||
user_id=user.id, sport_id=sport.id
|
||||
).first()
|
||||
return {
|
||||
'status': 'success',
|
||||
'data': {'sports': [sport.serialize(user.admin)]},
|
||||
'data': {
|
||||
'sports': [
|
||||
sport.serialize(
|
||||
is_admin=user.admin,
|
||||
sport_preferences=sport_preferences.serialize()
|
||||
if sport_preferences
|
||||
else None,
|
||||
)
|
||||
]
|
||||
},
|
||||
}
|
||||
return DataNotFoundErrorResponse('sports')
|
||||
|
||||
@ -284,11 +337,13 @@ def update_sport(
|
||||
"data": {
|
||||
"sports": [
|
||||
{
|
||||
"color": null,
|
||||
"has_workouts": false,
|
||||
"id": 1,
|
||||
"img": "/img/sports/cycling-sport.png",
|
||||
"is_active": false,
|
||||
"label": "Cycling (Sport)"
|
||||
"is_active_for_user": false,
|
||||
"label": "Cycling (Sport)",
|
||||
"stopped_speed_threshold": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -332,15 +387,28 @@ def update_sport(
|
||||
return InvalidPayloadErrorResponse()
|
||||
|
||||
try:
|
||||
user = User.query.filter_by(id=int(auth_user_id)).first()
|
||||
sport = Sport.query.filter_by(id=sport_id).first()
|
||||
if not sport:
|
||||
return DataNotFoundErrorResponse('sports')
|
||||
|
||||
sport.is_active = sport_data.get('is_active')
|
||||
db.session.commit()
|
||||
sport_preferences = UserSportPreference.query.filter_by(
|
||||
user_id=user.id, sport_id=sport.id
|
||||
).first()
|
||||
return {
|
||||
'status': 'success',
|
||||
'data': {'sports': [sport.serialize(True)]},
|
||||
'data': {
|
||||
'sports': [
|
||||
sport.serialize(
|
||||
is_admin=user.admin,
|
||||
sport_preferences=sport_preferences.serialize()
|
||||
if sport_preferences
|
||||
else None,
|
||||
)
|
||||
]
|
||||
},
|
||||
}
|
||||
|
||||
except (exc.IntegrityError, exc.OperationalError, ValueError) as e:
|
||||
|
@ -15,7 +15,7 @@ from werkzeug.datastructures import FileStorage
|
||||
from werkzeug.utils import secure_filename
|
||||
|
||||
from fittrackee import db
|
||||
from fittrackee.users.models import User
|
||||
from fittrackee.users.models import User, UserSportPreference
|
||||
|
||||
from .exceptions import WorkoutException
|
||||
from .models import Sport, Workout, WorkoutSegment
|
||||
@ -400,6 +400,14 @@ def process_files(
|
||||
f"Sport id: {workout_data.get('sport_id')} does not exist",
|
||||
)
|
||||
user = User.query.filter_by(id=auth_user_id).first()
|
||||
sport_preferences = UserSportPreference.query.filter_by(
|
||||
user_id=user.id, sport_id=sport.id
|
||||
).first()
|
||||
stopped_speed_threshold = (
|
||||
sport.stopped_speed_threshold
|
||||
if sport_preferences is None
|
||||
else sport_preferences.stopped_speed_threshold
|
||||
)
|
||||
|
||||
common_params = {
|
||||
'user': user,
|
||||
@ -418,14 +426,14 @@ def process_files(
|
||||
process_one_gpx_file(
|
||||
common_params,
|
||||
filename,
|
||||
sport.stopped_speed_threshold,
|
||||
stopped_speed_threshold,
|
||||
)
|
||||
]
|
||||
else:
|
||||
return process_zip_archive(
|
||||
common_params,
|
||||
folders['extract_dir'],
|
||||
sport.stopped_speed_threshold,
|
||||
stopped_speed_threshold,
|
||||
)
|
||||
|
||||
|
||||
|
@ -24,7 +24,7 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="sport in translatedSports" :key="sport.id">
|
||||
<td class="center-text">
|
||||
<td class="text-center">
|
||||
<span class="cell-heading">id</span>
|
||||
{{ sport.id }}
|
||||
</td>
|
||||
@ -35,6 +35,7 @@
|
||||
<SportImage
|
||||
:title="sport.translatedLabel"
|
||||
:sport-label="sport.label"
|
||||
:color="sport.color"
|
||||
/>
|
||||
</td>
|
||||
<td class="sport-label">
|
||||
@ -43,12 +44,12 @@
|
||||
</span>
|
||||
{{ sport.translatedLabel }}
|
||||
</td>
|
||||
<td class="center-text">
|
||||
<td class="text-center">
|
||||
<span class="cell-heading">
|
||||
{{ $t('admin.SPORTS.TABLE.ACTIVE') }}
|
||||
</span>
|
||||
<i
|
||||
:class="`fa fa${sport.is_active ? '-check' : ''}-square-o`"
|
||||
:class="`fa fa${sport.is_active ? '-check' : ''}`"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</td>
|
||||
@ -127,9 +128,6 @@
|
||||
font-style: italic;
|
||||
padding: 0 $default-padding;
|
||||
}
|
||||
.text-left {
|
||||
text-align: left;
|
||||
}
|
||||
.sport-action {
|
||||
padding-left: $default-padding * 4;
|
||||
}
|
||||
|
@ -63,13 +63,13 @@
|
||||
)
|
||||
}}
|
||||
</td>
|
||||
<td class="center-text">
|
||||
<td class="text-center">
|
||||
<span class="cell-heading">
|
||||
{{ capitalize($t('workouts.WORKOUT', 0)) }}
|
||||
</span>
|
||||
{{ user.nb_workouts }}
|
||||
</td>
|
||||
<td class="center-text">
|
||||
<td class="text-center">
|
||||
<span class="cell-heading">
|
||||
{{ $t('user.ADMIN') }}
|
||||
</span>
|
||||
@ -78,7 +78,7 @@
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</td>
|
||||
<td class="center-text">
|
||||
<td class="text-center">
|
||||
<span class="cell-heading">
|
||||
{{ $t('admin.ACTION') }}
|
||||
</span>
|
||||
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div
|
||||
class="sport-img"
|
||||
:style="{ fill: sportColors[sportLabel] }"
|
||||
:style="{ fill: color ? color : sportColors[sportLabel] }"
|
||||
:title="title ? title : $t(`sports.${sportLabel}.LABEL`)"
|
||||
>
|
||||
<CyclingSport v-if="sportLabel === 'Cycling (Sport)'" />
|
||||
@ -37,12 +37,13 @@
|
||||
|
||||
interface Props {
|
||||
sportLabel: string
|
||||
color: string | null
|
||||
title?: string
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
title: '',
|
||||
})
|
||||
|
||||
const { sportLabel, title } = toRefs(props)
|
||||
const { color, sportLabel, title } = toRefs(props)
|
||||
const sportColors = inject('sportColors')
|
||||
</script>
|
||||
|
@ -5,7 +5,11 @@
|
||||
$router.push({ name: 'Workout', params: { workoutId: workout.id } })
|
||||
"
|
||||
>
|
||||
<SportImage :sport-label="sportLabel" :title="workout.title" />
|
||||
<SportImage
|
||||
:sport-label="sportLabel"
|
||||
:title="workout.title"
|
||||
:color="sportColor"
|
||||
/>
|
||||
<sup>
|
||||
<i
|
||||
v-if="workout.records.length > 0"
|
||||
@ -28,6 +32,7 @@
|
||||
interface Props {
|
||||
workout: IWorkout
|
||||
sportLabel: string
|
||||
sportColor: string | null
|
||||
}
|
||||
const props = defineProps<Props>()
|
||||
|
||||
|
@ -10,6 +10,7 @@
|
||||
:key="index"
|
||||
:workout="workout"
|
||||
:sportLabel="getSportLabel(workout, sports)"
|
||||
:sportColor="getSportColor(workout, sports)"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="donut-display">
|
||||
@ -41,7 +42,7 @@
|
||||
import CalendarWorkoutsChart from '@/components/Dashboard/UserCalendar/CalendarWorkoutsChart.vue'
|
||||
import { ISport } from '@/types/sports'
|
||||
import { IWorkout } from '@/types/workouts'
|
||||
import { getSportLabel, sportIdColors } from '@/utils/sports'
|
||||
import { getSportColor, getSportLabel, sportIdColors } from '@/utils/sports'
|
||||
import { getDonutDatasets } from '@/utils/workouts'
|
||||
|
||||
interface Props {
|
||||
|
@ -2,7 +2,7 @@
|
||||
<div class="records-card">
|
||||
<Card>
|
||||
<template #title>
|
||||
<SportImage :sport-label="records.label" />
|
||||
<SportImage :sport-label="records.label" :color="records.color" />
|
||||
{{ sportTranslatedLabel }}
|
||||
</template>
|
||||
<template #content>
|
||||
|
@ -4,7 +4,7 @@
|
||||
v-for="sport in translatedSports"
|
||||
type="checkbox"
|
||||
:key="sport.id"
|
||||
:style="{ color: sportColors[sport.label] }"
|
||||
:style="{ color: sport.color ? sport.color : sportColors[sport.label] }"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
@ -13,7 +13,7 @@
|
||||
:checked="selectedSportIds.includes(sport.id)"
|
||||
@input="updateSelectedSportIds(sport.id)"
|
||||
/>
|
||||
<SportImage :sport-label="sport.label" />
|
||||
<SportImage :sport-label="sport.label" :color="sport.color" />
|
||||
<span class="sport-label">{{ sport.translatedLabel }}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
@ -47,7 +47,7 @@
|
||||
import { format } from 'date-fns'
|
||||
import { ComputedRef, Ref, computed, ref, toRefs, withDefaults } from 'vue'
|
||||
|
||||
import { AUTH_USER_STORE } from '@/store/constants'
|
||||
import { AUTH_USER_STORE, USERS_STORE } from '@/store/constants'
|
||||
import { IUserProfile } from '@/types/user'
|
||||
import { useStore } from '@/use/useStore'
|
||||
|
||||
@ -81,7 +81,7 @@
|
||||
displayModal.value = value
|
||||
}
|
||||
function deleteUserAccount(username: string) {
|
||||
store.dispatch(AUTH_USER_STORE.ACTIONS.DELETE_ACCOUNT, { username })
|
||||
store.dispatch(USERS_STORE.ACTIONS.DELETE_USER_ACCOUNT, { username })
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -91,9 +91,5 @@
|
||||
.user-bio {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.profile-buttons {
|
||||
display: flex;
|
||||
gap: $default-padding;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -38,13 +38,3 @@
|
||||
props.user.timezone ? props.user.timezone : 'Europe/Paris'
|
||||
)
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/scss/base.scss';
|
||||
#user-preferences {
|
||||
.profile-buttons {
|
||||
display: flex;
|
||||
gap: $default-padding;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -22,7 +22,7 @@
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const { user, tab } = toRefs(props)
|
||||
const tabs = ['PROFILE', 'PREFERENCES']
|
||||
const tabs = ['PROFILE', 'PREFERENCES', 'SPORTS']
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
@ -34,7 +34,7 @@
|
||||
const store = useStore()
|
||||
|
||||
const { user, tab } = toRefs(props)
|
||||
const tabs = ['PROFILE', 'PICTURE', 'PREFERENCES']
|
||||
const tabs = ['PROFILE', 'PICTURE', 'PREFERENCES', 'SPORTS']
|
||||
const loading = computed(
|
||||
() => store.getters[AUTH_USER_STORE.GETTERS.USER_LOADING]
|
||||
)
|
||||
|
@ -38,7 +38,10 @@
|
||||
case 'PICTURE':
|
||||
return '/profile/edit/picture'
|
||||
case 'PREFERENCES':
|
||||
return `/profile${props.edition ? '/edit' : ''}/preferences`
|
||||
case 'SPORTS':
|
||||
return `/profile${
|
||||
props.edition ? '/edit' : ''
|
||||
}/${tab.toLocaleLowerCase()}`
|
||||
default:
|
||||
case 'PROFILE':
|
||||
return `/profile${props.edition ? '/edit' : ''}`
|
||||
|
321
fittrackee_client/src/components/User/UserSportPreferences.vue
Normal file
321
fittrackee_client/src/components/User/UserSportPreferences.vue
Normal file
@ -0,0 +1,321 @@
|
||||
<template>
|
||||
<div id="user-sport-preferences">
|
||||
<div class="responsive-table" v-if="sports.length > 0">
|
||||
<div class="mobile-display">
|
||||
<div v-if="isEdition" class="profile-buttons mobile-display">
|
||||
<button
|
||||
class="cancel"
|
||||
@click.prevent="$router.push('/profile/sports')"
|
||||
>
|
||||
{{ $t('buttons.BACK') }}
|
||||
</button>
|
||||
</div>
|
||||
<div v-else class="profile-buttons">
|
||||
<button @click="$router.push('/profile/edit/sports')">
|
||||
{{ $t('user.PROFILE.EDIT_SPORTS_PREFERENCES') }}
|
||||
</button>
|
||||
<button @click="$router.push('/')">{{ $t('common.HOME') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ $t('user.PROFILE.SPORT.COLOR') }}</th>
|
||||
<th class="text-left">{{ $t('workouts.SPORT', 0) }}</th>
|
||||
<th>{{ $t('workouts.WORKOUT', 0) }}</th>
|
||||
<th>{{ $t('user.PROFILE.SPORT.IS_ACTIVE') }}</th>
|
||||
<th>{{ $t('user.PROFILE.SPORT.STOPPED_SPEED_THRESHOLD') }}</th>
|
||||
<th v-if="isEdition">{{ $t('user.PROFILE.SPORT.ACTION') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="sport in translatedSports" :key="sport.id">
|
||||
<td>
|
||||
<span class="cell-heading">
|
||||
{{ $t('user.PROFILE.SPORT.COLOR') }}
|
||||
</span>
|
||||
<input
|
||||
v-if="isSportInEdition(sport.id)"
|
||||
class="sport-color"
|
||||
type="color"
|
||||
:value="sportPayload.color"
|
||||
@input="updateColor"
|
||||
/>
|
||||
<SportImage
|
||||
v-else
|
||||
:title="sport.translatedLabel"
|
||||
:sport-label="sport.label"
|
||||
:color="sport.color ? sport.color : sportColors[sport.label]"
|
||||
/>
|
||||
</td>
|
||||
<td
|
||||
class="sport-label"
|
||||
:class="{ 'disabled-sport': !sport.is_active }"
|
||||
>
|
||||
<span class="cell-heading">
|
||||
{{ $t('user.PROFILE.SPORT.LABEL') }}
|
||||
</span>
|
||||
{{ sport.translatedLabel }}
|
||||
<span class="disabled-message" v-if="!sport.is_active">
|
||||
({{ $t('user.PROFILE.SPORT.DISABLED_BY_ADMIN') }})
|
||||
</span>
|
||||
<i
|
||||
v-if="loading && isSportInEdition(sport.id)"
|
||||
class="fa fa-refresh fa-spin fa-fw"
|
||||
/>
|
||||
<ErrorMessage
|
||||
:message="errorMessages"
|
||||
v-if="errorMessages && sportPayload.sport_id === sport.id"
|
||||
/>
|
||||
</td>
|
||||
<td
|
||||
class="text-center"
|
||||
:class="{ 'disabled-sport': !sport.is_active }"
|
||||
>
|
||||
<span class="cell-heading">
|
||||
{{ $t('workouts.WORKOUT', 0) }}
|
||||
</span>
|
||||
<i
|
||||
:class="`fa fa${
|
||||
user.sports_list.includes(sport.id) ? '-check' : ''
|
||||
}`"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</td>
|
||||
<td
|
||||
class="text-center"
|
||||
:class="{ 'disabled-sport': !sport.is_active }"
|
||||
>
|
||||
<span class="cell-heading">
|
||||
{{ $t('user.PROFILE.SPORT.IS_ACTIVE') }}
|
||||
</span>
|
||||
<input
|
||||
v-if="isSportInEdition(sport.id) && sport.is_active"
|
||||
type="checkbox"
|
||||
:checked="sport.is_active_for_user"
|
||||
@change="updateIsActive"
|
||||
/>
|
||||
<i
|
||||
v-else
|
||||
:class="`fa fa${sport.is_active_for_user ? '-check' : ''}`"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</td>
|
||||
<td
|
||||
class="text-center"
|
||||
:class="{ 'disabled-sport': !sport.is_active }"
|
||||
>
|
||||
<span class="cell-heading">
|
||||
{{ $t('user.PROFILE.SPORT.STOPPED_SPEED_THRESHOLD') }}
|
||||
</span>
|
||||
<input
|
||||
class="threshold-input"
|
||||
v-if="isSportInEdition(sport.id) && sport.is_active"
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.1"
|
||||
:value="sportPayload.stopped_speed_threshold"
|
||||
@input="updateThreshold"
|
||||
/>
|
||||
<span v-else>
|
||||
{{ sport.stopped_speed_threshold }}
|
||||
</span>
|
||||
</td>
|
||||
<td v-if="isEdition" class="action-buttons">
|
||||
<span class="cell-heading">
|
||||
{{ $t('user.PROFILE.SPORT.ACTION') }}
|
||||
</span>
|
||||
<button
|
||||
v-if="sportPayload.sport_id === 0"
|
||||
@click="updateSportInEdition(sport)"
|
||||
>
|
||||
{{ $t('buttons.EDIT') }}
|
||||
</button>
|
||||
<div v-if="isSportInEdition(sport.id)" class="edition-buttons">
|
||||
<button :disabled="loading" @click="updateSport">
|
||||
{{ $t('buttons.SUBMIT') }}
|
||||
</button>
|
||||
<button :disabled="loading" @click="updateSportInEdition(null)">
|
||||
{{ $t('buttons.CANCEL') }}
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div v-if="isEdition" class="profile-buttons">
|
||||
<button class="cancel" @click.prevent="$router.push('/profile/sports')">
|
||||
{{ $t('buttons.BACK') }}
|
||||
</button>
|
||||
</div>
|
||||
<div v-else class="profile-buttons">
|
||||
<button @click="$router.push('/profile/edit/sports')">
|
||||
{{ $t('user.PROFILE.EDIT_SPORTS_PREFERENCES') }}
|
||||
</button>
|
||||
<button @click="$router.push('/')">{{ $t('common.HOME') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ComputedRef, computed, inject, reactive, toRefs, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { AUTH_USER_STORE, ROOT_STORE, SPORTS_STORE } from '@/store/constants'
|
||||
import { ISport, ITranslatedSport } from '@/types/sports'
|
||||
import { IUserProfile, IUserSportPreferencesPayload } from '@/types/user'
|
||||
import { useStore } from '@/use/useStore'
|
||||
import { translateSports } from '@/utils/sports'
|
||||
|
||||
interface Props {
|
||||
user: IUserProfile
|
||||
isEdition: boolean
|
||||
}
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const store = useStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
const { isEdition, user } = toRefs(props)
|
||||
const sportColors = inject('sportColors')
|
||||
const sports: ComputedRef<ISport[]> = computed(
|
||||
() => store.getters[SPORTS_STORE.GETTERS.SPORTS]
|
||||
)
|
||||
const translatedSports: ComputedRef<ITranslatedSport[]> = computed(() =>
|
||||
translateSports(sports.value, t, true, user.value.sports_list)
|
||||
)
|
||||
const loading = computed(
|
||||
() => store.getters[AUTH_USER_STORE.GETTERS.USER_LOADING]
|
||||
)
|
||||
const errorMessages: ComputedRef<string | string[] | null> = computed(
|
||||
() => store.getters[ROOT_STORE.GETTERS.ERROR_MESSAGES]
|
||||
)
|
||||
const sportPayload: IUserSportPreferencesPayload = reactive({
|
||||
sport_id: 0,
|
||||
color: null,
|
||||
is_active: true,
|
||||
stopped_speed_threshold: 1,
|
||||
})
|
||||
|
||||
function updateSportInEdition(sport: ISport | null) {
|
||||
if (sport !== null) {
|
||||
sportPayload.sport_id = sport.id
|
||||
sportPayload.color = sport.color ? sport.color : sportColors[sport.label]
|
||||
sportPayload.is_active = sport.is_active_for_user
|
||||
sportPayload.stopped_speed_threshold = sport.stopped_speed_threshold
|
||||
} else {
|
||||
resetSportPayload()
|
||||
}
|
||||
}
|
||||
function isSportInEdition(sportId: number) {
|
||||
return sportPayload.sport_id === sportId
|
||||
}
|
||||
function updateColor(event: Event & { target: HTMLInputElement }) {
|
||||
sportPayload.color = event.target.value
|
||||
}
|
||||
function updateThreshold(event: Event & { target: HTMLInputElement }) {
|
||||
sportPayload.stopped_speed_threshold = parseFloat(event.target.value)
|
||||
}
|
||||
function updateIsActive(event: Event & { target: HTMLInputElement }) {
|
||||
sportPayload.is_active = event.target.checked
|
||||
}
|
||||
function resetSportPayload() {
|
||||
sportPayload.sport_id = 0
|
||||
sportPayload.color = null
|
||||
sportPayload.is_active = true
|
||||
sportPayload.stopped_speed_threshold = 1
|
||||
store.commit(ROOT_STORE.MUTATIONS.EMPTY_ERROR_MESSAGES)
|
||||
}
|
||||
function updateSport(event: Event) {
|
||||
event.preventDefault()
|
||||
store.dispatch(
|
||||
AUTH_USER_STORE.ACTIONS.UPDATE_USER_SPORT_PREFERENCES,
|
||||
sportPayload
|
||||
)
|
||||
}
|
||||
|
||||
watch(
|
||||
() => loading.value,
|
||||
(newIsLoading) => {
|
||||
if (!newIsLoading && !errorMessages.value) {
|
||||
resetSportPayload()
|
||||
}
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/scss/base.scss';
|
||||
#user-sport-preferences {
|
||||
.sport-img {
|
||||
height: 35px;
|
||||
width: 35px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.sport-color {
|
||||
border: none;
|
||||
margin: 6px 1px 6px 0;
|
||||
padding: 0;
|
||||
width: 40px;
|
||||
}
|
||||
.sport-label {
|
||||
width: 170px;
|
||||
}
|
||||
.disabled-sport {
|
||||
font-style: italic;
|
||||
color: var(--disabled-sport-color);
|
||||
|
||||
.disabled-message {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.cell-heading {
|
||||
font-style: normal;
|
||||
}
|
||||
}
|
||||
.action-buttons {
|
||||
width: 70px;
|
||||
}
|
||||
.edition-buttons {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: $default-padding * 0.5;
|
||||
line-height: 1.3em;
|
||||
|
||||
button {
|
||||
text-align: center;
|
||||
min-width: 80px;
|
||||
}
|
||||
}
|
||||
.threshold-input {
|
||||
padding: $default-padding * 0.5;
|
||||
width: 50px;
|
||||
}
|
||||
.mobile-display {
|
||||
display: none;
|
||||
}
|
||||
div.error-message {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@media screen and (max-width: $small-limit) {
|
||||
.sport-label,
|
||||
.action-buttons {
|
||||
width: 45%;
|
||||
}
|
||||
.edition-buttons {
|
||||
justify-content: center;
|
||||
}
|
||||
.mobile-display {
|
||||
display: flex;
|
||||
margin: $default-margin * 2 0 $default-margin;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: $x-small-limit) {
|
||||
.sport-label,
|
||||
.action-buttons {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -75,7 +75,11 @@
|
||||
"
|
||||
>
|
||||
<div class="img">
|
||||
<SportImage v-if="sport.label" :sport-label="sport.label" />
|
||||
<SportImage
|
||||
v-if="sport.label"
|
||||
:sport-label="sport.label"
|
||||
:color="sport.color"
|
||||
/>
|
||||
</div>
|
||||
<div class="data">
|
||||
<i class="fa fa-clock-o" aria-hidden="true" />
|
||||
|
@ -17,7 +17,7 @@
|
||||
<i class="fa fa-chevron-left" aria-hidden="true" />
|
||||
</div>
|
||||
<div class="workout-card-title">
|
||||
<SportImage :sport-label="sport.label" />
|
||||
<SportImage :sport-label="sport.label" :color="sport.color" />
|
||||
<div class="workout-title-date">
|
||||
<div class="workout-title" v-if="workoutObject.type === 'WORKOUT'">
|
||||
{{ workoutObject.title }}
|
||||
|
@ -46,7 +46,7 @@
|
||||
v-model="workoutForm.sport_id"
|
||||
>
|
||||
<option
|
||||
v-for="sport in translatedSports.filter((s) => s.is_active)"
|
||||
v-for="sport in translatedSports"
|
||||
:value="sport.id"
|
||||
:key="sport.id"
|
||||
>
|
||||
@ -259,7 +259,12 @@
|
||||
|
||||
const { workout, isCreation, loading } = toRefs(props)
|
||||
const translatedSports: ComputedRef<ISport[]> = computed(() =>
|
||||
translateSports(props.sports, t)
|
||||
translateSports(
|
||||
props.sports,
|
||||
t,
|
||||
true,
|
||||
workout.value.id ? [workout.value.sport_id] : null
|
||||
)
|
||||
)
|
||||
const appConfig: ComputedRef<TAppConfig> = computed(
|
||||
() => store.getters[ROOT_STORE.GETTERS.APP_CONFIG]
|
||||
|
@ -49,9 +49,8 @@
|
||||
sports.filter((s) => s.id === workout.sport_id)[0]
|
||||
.translatedLabel
|
||||
"
|
||||
:sport-label="
|
||||
sports.filter((s) => s.id === workout.sport_id)[0].label
|
||||
"
|
||||
:sport-label="getSportLabel(workout, sports)"
|
||||
:color="getSportColor(workout, sports)"
|
||||
/>
|
||||
</td>
|
||||
<td
|
||||
@ -163,6 +162,7 @@
|
||||
import { useStore } from '@/use/useStore'
|
||||
import { getQuery, sortList, workoutsPayloadKeys } from '@/utils/api'
|
||||
import { getDateWithTZ } from '@/utils/dates'
|
||||
import { getSportColor, getSportLabel } from '@/utils/sports'
|
||||
import { defaultOrder } from '@/utils/workouts'
|
||||
|
||||
interface Props {
|
||||
|
@ -23,6 +23,7 @@
|
||||
"BIRTH_DATE": "Birth date",
|
||||
"EDIT": "Edit profile",
|
||||
"EDIT_PREFERENCES": "Edit preferences",
|
||||
"EDIT_SPORTS_PREFERENCES": "Edit sports preferences",
|
||||
"FIRST_NAME": "First name",
|
||||
"FIRST_DAY_OF_WEEK": "First day of week",
|
||||
"LANGUAGE": "Language",
|
||||
@ -36,11 +37,21 @@
|
||||
"PREFERENCES_EDITION": "Preferences edition",
|
||||
"PROFILE_EDITION": "Profile edition",
|
||||
"REGISTRATION_DATE": "Registration date",
|
||||
"SPORTS_EDITION": "Sports preferences edition",
|
||||
"SUNDAY": "Sunday",
|
||||
"TABS": {
|
||||
"PICTURE": "picture",
|
||||
"PREFERENCES": "preferences",
|
||||
"PROFILE": "profile"
|
||||
"PROFILE": "profile",
|
||||
"SPORTS": "sports"
|
||||
},
|
||||
"SPORT": {
|
||||
"ACTION": "action",
|
||||
"COLOR": "color",
|
||||
"DISABLED_BY_ADMIN": "disabled by admin",
|
||||
"IS_ACTIVE": "active",
|
||||
"LABEL": "label",
|
||||
"STOPPED_SPEED_THRESHOLD": "stopped speed threshold"
|
||||
},
|
||||
"TIMEZONE": "Timezone"
|
||||
},
|
||||
|
@ -23,6 +23,7 @@
|
||||
"BIRTH_DATE": "Date de naissance",
|
||||
"EDIT": "Modifier le profil",
|
||||
"EDIT_PREFERENCES": "Modifier les préférences",
|
||||
"EDIT_SPORTS_PREFERENCES": "Modifier les préférences des sports",
|
||||
"FIRST_DAY_OF_WEEK": "Premier jour de la semaine",
|
||||
"FIRST_NAME": "Prénom",
|
||||
"LANGUAGE": "Langue",
|
||||
@ -36,11 +37,21 @@
|
||||
"PREFERENCES_EDITION": "Mise à jour des préférences",
|
||||
"PROFILE_EDITION": "Mise à jour du profil",
|
||||
"REGISTRATION_DATE": "Date d'inscription",
|
||||
"SPORTS_EDITION": "Mise à jour des préférences des sports",
|
||||
"SUNDAY": "Dimanche",
|
||||
"TABS": {
|
||||
"PICTURE": "image",
|
||||
"PREFERENCES": "préférences",
|
||||
"PROFILE": "profil"
|
||||
"PROFILE": "profil",
|
||||
"SPORTS": "sports"
|
||||
},
|
||||
"SPORT": {
|
||||
"ACTION": "action",
|
||||
"COLOR": "couleur",
|
||||
"DISABLED_BY_ADMIN": "désactivé par l'administrateur",
|
||||
"IS_ACTIVE": "actif",
|
||||
"LABEL": "label",
|
||||
"STOPPED_SPEED_THRESHOLD": "seuil de vitesse arrêtée"
|
||||
},
|
||||
"TIMEZONE": "Fuseau horaire"
|
||||
},
|
||||
@ -49,4 +60,4 @@
|
||||
"RESET_PASSWORD": "Réinitialiser votre mot de passe",
|
||||
"USER_PICTURE": "photo de l'utilisateur",
|
||||
"USERNAME": "Nom d'utilisateur"
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ import ProfileEdition from '@/components/User/ProfileEdition/index.vue'
|
||||
import UserInfosEdition from '@/components/User/ProfileEdition/UserInfosEdition.vue'
|
||||
import UserPictureEdition from '@/components/User/ProfileEdition/UserPictureEdition.vue'
|
||||
import UserPreferencesEdition from '@/components/User/ProfileEdition/UserPreferencesEdition.vue'
|
||||
import UserSportPreferences from '@/components/User/UserSportPreferences.vue'
|
||||
import store from '@/store'
|
||||
import { AUTH_USER_STORE } from '@/store/constants'
|
||||
|
||||
@ -101,6 +102,12 @@ const routes: Array<RouteRecordRaw> = [
|
||||
name: 'UserPreferences',
|
||||
component: UserPreferences,
|
||||
},
|
||||
{
|
||||
path: 'sports',
|
||||
name: 'UserSportPreferences',
|
||||
component: UserSportPreferences,
|
||||
props: { isEdition: false },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@ -126,6 +133,12 @@ const routes: Array<RouteRecordRaw> = [
|
||||
name: 'UserPreferencesEdition',
|
||||
component: UserPreferencesEdition,
|
||||
},
|
||||
{
|
||||
path: 'sports',
|
||||
name: 'UserSportPreferencesEdition',
|
||||
component: UserSportPreferences,
|
||||
props: { isEdition: true },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
@ -254,9 +254,12 @@ button {
|
||||
}
|
||||
}
|
||||
|
||||
.center-text {
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
.text-left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.responsive-table {
|
||||
margin-bottom: 15px;
|
||||
@ -337,3 +340,14 @@ button {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.profile-buttons {
|
||||
display: flex;
|
||||
gap: $default-padding;
|
||||
}
|
||||
|
||||
.medium-sport-img {
|
||||
height: 35px;
|
||||
width: 35px;
|
||||
margin: 0 auto;
|
||||
}
|
@ -53,6 +53,7 @@
|
||||
|
||||
--disabled-background-color: #e0e0e0;
|
||||
--disabled-color: #a3a3a3;
|
||||
--disabled-sport-color: #616161;
|
||||
|
||||
--scroll-button-bg-color: rgba(255, 255, 255, .7);
|
||||
|
||||
|
@ -17,6 +17,7 @@ import {
|
||||
IAuthUserState,
|
||||
} from '@/store/modules/authUser/types'
|
||||
import { IRootState } from '@/store/modules/root/types'
|
||||
import { deleteUserAccount } from '@/store/modules/users/actions'
|
||||
import {
|
||||
ILoginOrRegisterData,
|
||||
IUserDeletionPayload,
|
||||
@ -25,6 +26,7 @@ import {
|
||||
IUserPayload,
|
||||
IUserPicturePayload,
|
||||
IUserPreferencesPayload,
|
||||
IUserSportPreferencesPayload,
|
||||
} from '@/types/user'
|
||||
import { handleError } from '@/utils'
|
||||
|
||||
@ -172,6 +174,26 @@ export const actions: ActionTree<IAuthUserState, IRootState> &
|
||||
context.commit(AUTH_USER_STORE.MUTATIONS.UPDATE_USER_LOADING, false)
|
||||
)
|
||||
},
|
||||
[AUTH_USER_STORE.ACTIONS.UPDATE_USER_SPORT_PREFERENCES](
|
||||
context: ActionContext<IAuthUserState, IRootState>,
|
||||
payload: IUserSportPreferencesPayload
|
||||
): void {
|
||||
context.commit(ROOT_STORE.MUTATIONS.EMPTY_ERROR_MESSAGES)
|
||||
context.commit(AUTH_USER_STORE.MUTATIONS.UPDATE_USER_LOADING, true)
|
||||
authApi
|
||||
.post('auth/profile/edit/sports', payload)
|
||||
.then((res) => {
|
||||
if (res.data.status === 'success') {
|
||||
context.dispatch(SPORTS_STORE.ACTIONS.GET_SPORTS)
|
||||
} else {
|
||||
handleError(context, null)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
handleError(context, error)
|
||||
context.commit(AUTH_USER_STORE.MUTATIONS.UPDATE_USER_LOADING, false)
|
||||
})
|
||||
},
|
||||
[AUTH_USER_STORE.ACTIONS.UPDATE_USER_PICTURE](
|
||||
context: ActionContext<IAuthUserState, IRootState>,
|
||||
payload: IUserPicturePayload
|
||||
@ -207,19 +229,7 @@ export const actions: ActionTree<IAuthUserState, IRootState> &
|
||||
context: ActionContext<IAuthUserState, IRootState>,
|
||||
payload: IUserDeletionPayload
|
||||
): void {
|
||||
context.commit(ROOT_STORE.MUTATIONS.EMPTY_ERROR_MESSAGES)
|
||||
authApi
|
||||
.delete(`users/${payload.username}`)
|
||||
.then((res) => {
|
||||
if (res.status === 204) {
|
||||
context
|
||||
.dispatch(AUTH_USER_STORE.ACTIONS.LOGOUT)
|
||||
.then(() => router.push('/'))
|
||||
} else {
|
||||
handleError(context, null)
|
||||
}
|
||||
})
|
||||
.catch((error) => handleError(context, error))
|
||||
deleteUserAccount(context, payload)
|
||||
},
|
||||
[AUTH_USER_STORE.ACTIONS.DELETE_PICTURE](
|
||||
context: ActionContext<IAuthUserState, IRootState>
|
||||
|
@ -10,6 +10,7 @@ export enum AuthUserActions {
|
||||
UPDATE_USER_PICTURE = 'UPDATE_USER_PICTURE',
|
||||
UPDATE_USER_PROFILE = 'UPDATE_USER_PROFILE',
|
||||
UPDATE_USER_PREFERENCES = 'UPDATE_USER_PREFERENCES',
|
||||
UPDATE_USER_SPORT_PREFERENCES = 'UPDATE_USER_SPORT_PREFERENCES',
|
||||
}
|
||||
|
||||
export enum AuthUserGetters {
|
||||
|
@ -16,6 +16,7 @@ import {
|
||||
IUserPayload,
|
||||
IUserPicturePayload,
|
||||
IUserPreferencesPayload,
|
||||
IUserSportPreferencesPayload,
|
||||
} from '@/types/user'
|
||||
|
||||
export interface IAuthUserState {
|
||||
@ -52,6 +53,11 @@ export interface IAuthUserActions {
|
||||
payload: IUserPreferencesPayload
|
||||
): void
|
||||
|
||||
[AUTH_USER_STORE.ACTIONS.UPDATE_USER_SPORT_PREFERENCES](
|
||||
context: ActionContext<IAuthUserState, IRootState>,
|
||||
payload: IUserSportPreferencesPayload
|
||||
): void
|
||||
|
||||
[AUTH_USER_STORE.ACTIONS.UPDATE_USER_PICTURE](
|
||||
context: ActionContext<IAuthUserState, IRootState>,
|
||||
payload: IUserPicturePayload
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { ActionContext, ActionTree } from 'vuex'
|
||||
|
||||
import authApi from '@/api/authApi'
|
||||
import { ROOT_STORE, SPORTS_STORE } from '@/store/constants'
|
||||
import { AUTH_USER_STORE, ROOT_STORE, SPORTS_STORE } from '@/store/constants'
|
||||
import { IRootState } from '@/store/modules/root/types'
|
||||
import { ISportsActions, ISportsState } from '@/store/modules/sports/types'
|
||||
import { ISportPayload } from '@/types/sports'
|
||||
@ -20,6 +20,7 @@ export const actions: ActionTree<ISportsState, IRootState> & ISportsActions = {
|
||||
SPORTS_STORE.MUTATIONS.SET_SPORTS,
|
||||
res.data.data.sports
|
||||
)
|
||||
context.commit(AUTH_USER_STORE.MUTATIONS.UPDATE_USER_LOADING, false)
|
||||
} else {
|
||||
handleError(context, null)
|
||||
}
|
||||
|
@ -1,13 +1,40 @@
|
||||
import { ActionContext, ActionTree } from 'vuex'
|
||||
|
||||
import authApi from '@/api/authApi'
|
||||
import { ROOT_STORE, USERS_STORE } from '@/store/constants'
|
||||
import router from '@/router'
|
||||
import { AUTH_USER_STORE, ROOT_STORE, USERS_STORE } from '@/store/constants'
|
||||
import { IAuthUserState } from '@/store/modules/authUser/types'
|
||||
import { IRootState } from '@/store/modules/root/types'
|
||||
import { IUsersActions, IUsersState } from '@/store/modules/users/types'
|
||||
import { TPaginationPayload } from '@/types/api'
|
||||
import { IAdminUserPayload } from '@/types/user'
|
||||
import { IAdminUserPayload, IUserDeletionPayload } from '@/types/user'
|
||||
import { handleError } from '@/utils'
|
||||
|
||||
export const deleteUserAccount = (
|
||||
context:
|
||||
| ActionContext<IAuthUserState, IRootState>
|
||||
| ActionContext<IUsersState, IRootState>,
|
||||
payload: IUserDeletionPayload
|
||||
): void => {
|
||||
context.commit(ROOT_STORE.MUTATIONS.EMPTY_ERROR_MESSAGES)
|
||||
authApi
|
||||
.delete(`users/${payload.username}`)
|
||||
.then((res) => {
|
||||
if (res.status === 204) {
|
||||
if (payload.fromAdmin) {
|
||||
router.push('/admin/users')
|
||||
} else {
|
||||
context
|
||||
.dispatch(AUTH_USER_STORE.ACTIONS.LOGOUT)
|
||||
.then(() => router.push('/'))
|
||||
}
|
||||
} else {
|
||||
handleError(context, null)
|
||||
}
|
||||
})
|
||||
.catch((error) => handleError(context, error))
|
||||
}
|
||||
|
||||
export const actions: ActionTree<IUsersState, IRootState> & IUsersActions = {
|
||||
[USERS_STORE.ACTIONS.EMPTY_USER](
|
||||
context: ActionContext<IUsersState, IRootState>
|
||||
@ -94,4 +121,13 @@ export const actions: ActionTree<IUsersState, IRootState> & IUsersActions = {
|
||||
context.commit(USERS_STORE.MUTATIONS.UPDATE_USERS_LOADING, false)
|
||||
)
|
||||
},
|
||||
[USERS_STORE.ACTIONS.DELETE_USER_ACCOUNT](
|
||||
context: ActionContext<IUsersState, IRootState>,
|
||||
payload: IUserDeletionPayload
|
||||
): void {
|
||||
deleteUserAccount(context, {
|
||||
username: payload.username,
|
||||
fromAdmin: true,
|
||||
})
|
||||
},
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ export enum UsersActions {
|
||||
GET_USER = 'GET_USER',
|
||||
GET_USERS = 'GET_USERS',
|
||||
UPDATE_USER = 'UPDATE_USER',
|
||||
DELETE_USER_ACCOUNT = 'DELETE_USER_ACCOUNT',
|
||||
}
|
||||
|
||||
export enum UsersGetters {
|
||||
|
@ -8,7 +8,11 @@ import {
|
||||
import { USERS_STORE } from '@/store/constants'
|
||||
import { IRootState } from '@/store/modules/root/types'
|
||||
import { IPagination, TPaginationPayload } from '@/types/api'
|
||||
import { IAdminUserPayload, IUserProfile } from '@/types/user'
|
||||
import {
|
||||
IAdminUserPayload,
|
||||
IUserDeletionPayload,
|
||||
IUserProfile,
|
||||
} from '@/types/user'
|
||||
|
||||
export interface IUsersState {
|
||||
user: IUserProfile
|
||||
@ -36,6 +40,10 @@ export interface IUsersActions {
|
||||
context: ActionContext<IUsersState, IRootState>,
|
||||
payload: IAdminUserPayload
|
||||
): void
|
||||
[USERS_STORE.ACTIONS.DELETE_USER_ACCOUNT](
|
||||
context: ActionContext<IUsersState, IRootState>,
|
||||
payload: IUserDeletionPayload
|
||||
): void
|
||||
}
|
||||
|
||||
export interface IUsersGetters {
|
||||
|
@ -1,9 +1,12 @@
|
||||
export interface ISport {
|
||||
color: string | null
|
||||
has_workouts: boolean
|
||||
id: number
|
||||
img: string
|
||||
is_active: boolean
|
||||
is_active_for_user: boolean
|
||||
label: string
|
||||
stopped_speed_threshold: number
|
||||
}
|
||||
|
||||
export interface ITranslatedSport extends ISport {
|
||||
|
@ -45,6 +45,13 @@ export interface IUserPreferencesPayload {
|
||||
weekm: boolean
|
||||
}
|
||||
|
||||
export interface IUserSportPreferencesPayload {
|
||||
sport_id: number
|
||||
color: string | null
|
||||
is_active: boolean
|
||||
stopped_speed_threshold: number
|
||||
}
|
||||
|
||||
export interface IUserPicturePayload {
|
||||
picture: File
|
||||
}
|
||||
@ -61,6 +68,7 @@ export interface IUserPasswordResetPayload {
|
||||
|
||||
export interface IUserDeletionPayload {
|
||||
username: string
|
||||
fromAdmin?: boolean
|
||||
}
|
||||
|
||||
export interface ILoginRegisterFormData {
|
||||
|
@ -27,8 +27,9 @@ export interface IRecord {
|
||||
}
|
||||
|
||||
export interface IRecordsBySport {
|
||||
[key: string]: string | Record<string, string | number>[]
|
||||
[key: string]: string | Record<string, string | number>[] | null
|
||||
label: string
|
||||
color: string | null
|
||||
records: Record<string, string | number>[]
|
||||
}
|
||||
|
||||
|
@ -44,6 +44,7 @@ export const getRecordsBySports = (
|
||||
if (sportList[sport.translatedLabel] === void 0) {
|
||||
sportList[sport.translatedLabel] = {
|
||||
label: sport.label,
|
||||
color: sport.color,
|
||||
records: [],
|
||||
}
|
||||
}
|
||||
|
@ -18,7 +18,10 @@ export const sportColors: Record<string, string> = {
|
||||
|
||||
export const sportIdColors = (sports: ISport[]): Record<number, string> => {
|
||||
const colors: Record<number, string> = {}
|
||||
sports.map((sport) => (colors[sport.id] = sportColors[sport.label]))
|
||||
sports.map(
|
||||
(sport) =>
|
||||
(colors[sport.id] = sport.color ? sport.color : sportColors[sport.label])
|
||||
)
|
||||
return colors
|
||||
}
|
||||
|
||||
@ -35,10 +38,17 @@ const sortSports = (a: ITranslatedSport, b: ITranslatedSport): number => {
|
||||
export const translateSports = (
|
||||
sports: ISport[],
|
||||
t: CallableFunction,
|
||||
onlyActive = false
|
||||
onlyActive = false,
|
||||
userSports: number[] | null = null
|
||||
): ITranslatedSport[] =>
|
||||
sports
|
||||
.filter((sport) => (onlyActive ? sport.is_active : true))
|
||||
.filter((sport) =>
|
||||
onlyActive
|
||||
? userSports === null
|
||||
? sport.is_active_for_user
|
||||
: userSports.includes(sport.id) || sport.is_active
|
||||
: true
|
||||
)
|
||||
.map((sport) => ({
|
||||
...sport,
|
||||
translatedLabel: t(`sports.${sport.label}.LABEL`),
|
||||
@ -50,3 +60,12 @@ export const getSportLabel = (workout: IWorkout, sports: ISport[]): string => {
|
||||
.filter((sport) => sport.id === workout.sport_id)
|
||||
.map((sport) => sport.label)[0]
|
||||
}
|
||||
|
||||
export const getSportColor = (
|
||||
workout: IWorkout,
|
||||
sports: ISport[]
|
||||
): string | null => {
|
||||
return sports
|
||||
.filter((sport) => sport.id === workout.sport_id)
|
||||
.map((sport) => sport.color)[0]
|
||||
}
|
||||
|
@ -84,7 +84,7 @@ export const getDatasets = (displayedSports: ISport[]): TStatisticsDatasets => {
|
||||
total_descent: [],
|
||||
}
|
||||
displayedSports.map((sport) => {
|
||||
const color = sportColors[sport.label]
|
||||
const color = sport.color ? sport.color : sportColors[sport.label]
|
||||
datasets.nb_workouts.push(getStatisticsChartDataset(sport.label, color))
|
||||
datasets.total_distance.push(getStatisticsChartDataset(sport.label, color))
|
||||
datasets.total_duration.push(getStatisticsChartDataset(sport.label, color))
|
||||
|
@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<div id="profile" class="container view" v-if="authUser.username">
|
||||
<router-view :user="authUser"></router-view>
|
||||
<div id="bottom" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -5,25 +5,34 @@ import { translateSports } from '@/utils/sports'
|
||||
const { t } = createI18n.global
|
||||
export const sports: ISport[] = [
|
||||
{
|
||||
color: null,
|
||||
has_workouts: false,
|
||||
id: 1,
|
||||
img: '/img/sports/cycling-sport.png',
|
||||
is_active: true,
|
||||
is_active_for_user: true,
|
||||
label: 'Cycling (Sport)',
|
||||
stopped_speed_threshold: 1,
|
||||
},
|
||||
{
|
||||
has_workouts: false,
|
||||
color: '#000000',
|
||||
has_workouts: true,
|
||||
id: 2,
|
||||
img: '/img/sports/cycling-transport.png',
|
||||
is_active: false,
|
||||
is_active_for_user: false,
|
||||
label: 'Cycling (Transport)',
|
||||
stopped_speed_threshold: 1,
|
||||
},
|
||||
{
|
||||
color: null,
|
||||
has_workouts: true,
|
||||
id: 3,
|
||||
img: '/img/sports/hiking.png',
|
||||
is_active: true,
|
||||
is_active_for_user: false,
|
||||
label: 'Hiking',
|
||||
stopped_speed_threshold: 0.1,
|
||||
},
|
||||
]
|
||||
|
||||
|
@ -157,6 +157,7 @@ describe('getRecordsBySports', () => {
|
||||
},
|
||||
expected: {
|
||||
'Cycling (Sport)': {
|
||||
color: null,
|
||||
label: 'Cycling (Sport)',
|
||||
records: [
|
||||
{
|
||||
@ -206,6 +207,7 @@ describe('getRecordsBySports', () => {
|
||||
},
|
||||
expected: {
|
||||
'Cycling (Sport)': {
|
||||
color: null,
|
||||
label: 'Cycling (Sport)',
|
||||
records: [
|
||||
{
|
||||
@ -225,6 +227,7 @@ describe('getRecordsBySports', () => {
|
||||
],
|
||||
},
|
||||
'Cycling (Transport)': {
|
||||
color: '#000000',
|
||||
label: 'Cycling (Transport)',
|
||||
records: [
|
||||
{
|
||||
|
@ -7,7 +7,7 @@ import { translateSports } from '@/utils/sports'
|
||||
|
||||
const { t, locale } = createI18n.global
|
||||
|
||||
describe('sortSports', () => {
|
||||
describe('translateSports', () => {
|
||||
const testsParams = [
|
||||
{
|
||||
description: "returns sorted all translated sports (with 'en' locale)",
|
||||
@ -15,59 +15,65 @@ describe('sortSports', () => {
|
||||
sports,
|
||||
locale: 'en',
|
||||
onlyActive: false,
|
||||
userSports: null,
|
||||
},
|
||||
expected: [
|
||||
{
|
||||
color: null,
|
||||
has_workouts: false,
|
||||
id: 1,
|
||||
img: '/img/sports/cycling-sport.png',
|
||||
is_active: true,
|
||||
is_active_for_user: true,
|
||||
label: 'Cycling (Sport)',
|
||||
stopped_speed_threshold: 1,
|
||||
translatedLabel: 'Cycling (Sport)',
|
||||
},
|
||||
{
|
||||
has_workouts: false,
|
||||
color: '#000000',
|
||||
has_workouts: true,
|
||||
id: 2,
|
||||
img: '/img/sports/cycling-transport.png',
|
||||
is_active: false,
|
||||
is_active_for_user: false,
|
||||
label: 'Cycling (Transport)',
|
||||
stopped_speed_threshold: 1,
|
||||
translatedLabel: 'Cycling (Transport)',
|
||||
},
|
||||
{
|
||||
color: null,
|
||||
has_workouts: true,
|
||||
id: 3,
|
||||
img: '/img/sports/hiking.png',
|
||||
is_active: true,
|
||||
is_active_for_user: false,
|
||||
label: 'Hiking',
|
||||
stopped_speed_threshold: 0.1,
|
||||
translatedLabel: 'Hiking',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
description:
|
||||
"returns sorted only active translated sports (with 'en' locales)",
|
||||
"returns sorted only translated sports, active for user (with 'en' locales)",
|
||||
inputParams: {
|
||||
sports,
|
||||
locale: 'en',
|
||||
onlyActive: true,
|
||||
userSports: null,
|
||||
},
|
||||
expected: [
|
||||
{
|
||||
color: null,
|
||||
has_workouts: false,
|
||||
id: 1,
|
||||
img: '/img/sports/cycling-sport.png',
|
||||
is_active: true,
|
||||
is_active_for_user: true,
|
||||
label: 'Cycling (Sport)',
|
||||
stopped_speed_threshold: 1,
|
||||
translatedLabel: 'Cycling (Sport)',
|
||||
},
|
||||
{
|
||||
has_workouts: true,
|
||||
id: 3,
|
||||
img: '/img/sports/hiking.png',
|
||||
is_active: true,
|
||||
label: 'Hiking',
|
||||
translatedLabel: 'Hiking',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@ -76,6 +82,7 @@ describe('sortSports', () => {
|
||||
sports: [],
|
||||
locale: 'en',
|
||||
onlyActive: false,
|
||||
userSports: null,
|
||||
},
|
||||
expected: [],
|
||||
},
|
||||
@ -85,57 +92,63 @@ describe('sortSports', () => {
|
||||
sports,
|
||||
locale: 'fr',
|
||||
onlyActive: false,
|
||||
userSports: [],
|
||||
},
|
||||
expected: [
|
||||
{
|
||||
color: null,
|
||||
has_workouts: true,
|
||||
id: 3,
|
||||
img: '/img/sports/hiking.png',
|
||||
is_active: true,
|
||||
is_active_for_user: false,
|
||||
label: 'Hiking',
|
||||
stopped_speed_threshold: 0.1,
|
||||
translatedLabel: 'Randonnée',
|
||||
},
|
||||
{
|
||||
color: null,
|
||||
has_workouts: false,
|
||||
id: 1,
|
||||
img: '/img/sports/cycling-sport.png',
|
||||
is_active: true,
|
||||
is_active_for_user: true,
|
||||
label: 'Cycling (Sport)',
|
||||
stopped_speed_threshold: 1,
|
||||
translatedLabel: 'Vélo (Sport)',
|
||||
},
|
||||
{
|
||||
has_workouts: false,
|
||||
color: '#000000',
|
||||
has_workouts: true,
|
||||
id: 2,
|
||||
img: '/img/sports/cycling-transport.png',
|
||||
is_active: false,
|
||||
is_active_for_user: false,
|
||||
label: 'Cycling (Transport)',
|
||||
stopped_speed_threshold: 1,
|
||||
translatedLabel: 'Vélo (Transport)',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
description:
|
||||
"returns sorted only active translated sports (with 'fr' locales)",
|
||||
"returns sorted only translated sports, active for user (with 'fr' locales)",
|
||||
inputParams: {
|
||||
sports,
|
||||
locale: 'fr',
|
||||
onlyActive: true,
|
||||
userSports: null,
|
||||
},
|
||||
expected: [
|
||||
{
|
||||
has_workouts: true,
|
||||
id: 3,
|
||||
img: '/img/sports/hiking.png',
|
||||
is_active: true,
|
||||
label: 'Hiking',
|
||||
translatedLabel: 'Randonnée',
|
||||
},
|
||||
{
|
||||
color: null,
|
||||
has_workouts: false,
|
||||
id: 1,
|
||||
img: '/img/sports/cycling-sport.png',
|
||||
is_active: true,
|
||||
is_active_for_user: true,
|
||||
label: 'Cycling (Sport)',
|
||||
stopped_speed_threshold: 1,
|
||||
translatedLabel: 'Vélo (Sport)',
|
||||
},
|
||||
],
|
||||
@ -146,6 +159,209 @@ describe('sortSports', () => {
|
||||
sports: [],
|
||||
locale: 'fr',
|
||||
onlyActive: false,
|
||||
userSports: null,
|
||||
},
|
||||
expected: [],
|
||||
},
|
||||
{
|
||||
description:
|
||||
"returns sorted all translated sports, even with user sports list provided (with 'en' locale)",
|
||||
inputParams: {
|
||||
sports,
|
||||
locale: 'en',
|
||||
onlyActive: false,
|
||||
userSports: [2],
|
||||
},
|
||||
expected: [
|
||||
{
|
||||
color: null,
|
||||
has_workouts: false,
|
||||
id: 1,
|
||||
img: '/img/sports/cycling-sport.png',
|
||||
is_active: true,
|
||||
is_active_for_user: true,
|
||||
label: 'Cycling (Sport)',
|
||||
stopped_speed_threshold: 1,
|
||||
translatedLabel: 'Cycling (Sport)',
|
||||
},
|
||||
{
|
||||
color: '#000000',
|
||||
has_workouts: true,
|
||||
id: 2,
|
||||
img: '/img/sports/cycling-transport.png',
|
||||
is_active: false,
|
||||
is_active_for_user: false,
|
||||
label: 'Cycling (Transport)',
|
||||
stopped_speed_threshold: 1,
|
||||
translatedLabel: 'Cycling (Transport)',
|
||||
},
|
||||
{
|
||||
color: null,
|
||||
has_workouts: true,
|
||||
id: 3,
|
||||
img: '/img/sports/hiking.png',
|
||||
is_active: true,
|
||||
is_active_for_user: false,
|
||||
label: 'Hiking',
|
||||
stopped_speed_threshold: 0.1,
|
||||
translatedLabel: 'Hiking',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
description:
|
||||
"returns sorted only translated sports active on application and sports with user workouts (with 'en' locales)",
|
||||
inputParams: {
|
||||
sports,
|
||||
locale: 'en',
|
||||
onlyActive: true,
|
||||
userSports: [2],
|
||||
},
|
||||
expected: [
|
||||
{
|
||||
color: null,
|
||||
has_workouts: false,
|
||||
id: 1,
|
||||
img: '/img/sports/cycling-sport.png',
|
||||
is_active: true,
|
||||
is_active_for_user: true,
|
||||
label: 'Cycling (Sport)',
|
||||
stopped_speed_threshold: 1,
|
||||
translatedLabel: 'Cycling (Sport)',
|
||||
},
|
||||
{
|
||||
color: '#000000',
|
||||
has_workouts: true,
|
||||
id: 2,
|
||||
img: '/img/sports/cycling-transport.png',
|
||||
is_active: false,
|
||||
is_active_for_user: false,
|
||||
label: 'Cycling (Transport)',
|
||||
stopped_speed_threshold: 1,
|
||||
translatedLabel: 'Cycling (Transport)',
|
||||
},
|
||||
{
|
||||
color: null,
|
||||
has_workouts: true,
|
||||
id: 3,
|
||||
img: '/img/sports/hiking.png',
|
||||
is_active: true,
|
||||
is_active_for_user: false,
|
||||
label: 'Hiking',
|
||||
stopped_speed_threshold: 0.1,
|
||||
translatedLabel: 'Hiking',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
description:
|
||||
"returns empty array, with user sports list provided (with 'en' locale)",
|
||||
inputParams: {
|
||||
sports: [],
|
||||
locale: 'en',
|
||||
onlyActive: false,
|
||||
userSports: null,
|
||||
},
|
||||
expected: [],
|
||||
},
|
||||
{
|
||||
description:
|
||||
"returns sorted all translated sports, even with user sports list provided (with 'fr' locale)",
|
||||
inputParams: {
|
||||
sports,
|
||||
locale: 'fr',
|
||||
onlyActive: false,
|
||||
userSports: [2],
|
||||
},
|
||||
expected: [
|
||||
{
|
||||
color: null,
|
||||
has_workouts: true,
|
||||
id: 3,
|
||||
img: '/img/sports/hiking.png',
|
||||
is_active: true,
|
||||
is_active_for_user: false,
|
||||
label: 'Hiking',
|
||||
stopped_speed_threshold: 0.1,
|
||||
translatedLabel: 'Randonnée',
|
||||
},
|
||||
{
|
||||
color: null,
|
||||
has_workouts: false,
|
||||
id: 1,
|
||||
img: '/img/sports/cycling-sport.png',
|
||||
is_active: true,
|
||||
is_active_for_user: true,
|
||||
label: 'Cycling (Sport)',
|
||||
stopped_speed_threshold: 1,
|
||||
translatedLabel: 'Vélo (Sport)',
|
||||
},
|
||||
{
|
||||
color: '#000000',
|
||||
has_workouts: true,
|
||||
id: 2,
|
||||
img: '/img/sports/cycling-transport.png',
|
||||
is_active: false,
|
||||
is_active_for_user: false,
|
||||
label: 'Cycling (Transport)',
|
||||
stopped_speed_threshold: 1,
|
||||
translatedLabel: 'Vélo (Transport)',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
description:
|
||||
"returns sorted only translated sports active on application and sports with user workouts (with 'fr' locales)",
|
||||
inputParams: {
|
||||
sports,
|
||||
locale: 'fr',
|
||||
onlyActive: true,
|
||||
userSports: [2],
|
||||
},
|
||||
expected: [
|
||||
{
|
||||
color: null,
|
||||
has_workouts: true,
|
||||
id: 3,
|
||||
img: '/img/sports/hiking.png',
|
||||
is_active: true,
|
||||
is_active_for_user: false,
|
||||
label: 'Hiking',
|
||||
stopped_speed_threshold: 0.1,
|
||||
translatedLabel: 'Randonnée',
|
||||
},
|
||||
{
|
||||
color: null,
|
||||
has_workouts: false,
|
||||
id: 1,
|
||||
img: '/img/sports/cycling-sport.png',
|
||||
is_active: true,
|
||||
is_active_for_user: true,
|
||||
label: 'Cycling (Sport)',
|
||||
stopped_speed_threshold: 1,
|
||||
translatedLabel: 'Vélo (Sport)',
|
||||
},
|
||||
{
|
||||
color: '#000000',
|
||||
has_workouts: true,
|
||||
id: 2,
|
||||
img: '/img/sports/cycling-transport.png',
|
||||
is_active: false,
|
||||
is_active_for_user: false,
|
||||
label: 'Cycling (Transport)',
|
||||
stopped_speed_threshold: 1,
|
||||
translatedLabel: 'Vélo (Transport)',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
description:
|
||||
"returns empty array, with user sports list provided (with 'fr' locale)",
|
||||
inputParams: {
|
||||
sports: [],
|
||||
locale: 'fr',
|
||||
onlyActive: false,
|
||||
userSports: [2],
|
||||
},
|
||||
expected: [],
|
||||
},
|
||||
@ -157,7 +373,8 @@ describe('sortSports', () => {
|
||||
translateSports(
|
||||
testParams.inputParams.sports,
|
||||
t,
|
||||
testParams.inputParams.onlyActive
|
||||
testParams.inputParams.onlyActive,
|
||||
testParams.inputParams.userSports
|
||||
),
|
||||
testParams.expected
|
||||
)
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user