API - add user sport preferences
This commit is contained in:
parent
d434e82671
commit
a4ad40fdc2
@ -8,6 +8,8 @@ Authentication
|
|||||||
auth.logout_user,
|
auth.logout_user,
|
||||||
auth.get_authenticated_user_profile,
|
auth.get_authenticated_user_profile,
|
||||||
auth.edit_user,
|
auth.edit_user,
|
||||||
|
auth.edit_user_preferences,
|
||||||
|
auth.edit_user_sport_preferences,
|
||||||
auth.edit_picture,
|
auth.edit_picture,
|
||||||
auth.del_picture,
|
auth.del_picture,
|
||||||
auth.request_password_reset,
|
auth.request_password_reset,
|
||||||
|
@ -516,6 +516,179 @@
|
|||||||
</dl>
|
</dl>
|
||||||
</dd></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">
|
<dl class="http post">
|
||||||
<dt class="sig sig-object http" id="post--api-auth-picture">
|
<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>
|
<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>
|
||||||
|
@ -255,6 +255,16 @@
|
|||||||
<td>
|
<td>
|
||||||
<a href="api/auth.html#post--api-auth-profile-edit"><code class="xref">POST /api/auth/profile/edit</code></a></td><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>
|
<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>
|
<tr>
|
||||||
<td></td>
|
<td></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.logout_user,
|
||||||
auth.get_authenticated_user_profile,
|
auth.get_authenticated_user_profile,
|
||||||
auth.edit_user,
|
auth.edit_user,
|
||||||
|
auth.edit_user_preferences,
|
||||||
|
auth.edit_user_sport_preferences,
|
||||||
auth.edit_picture,
|
auth.edit_picture,
|
||||||
auth.del_picture,
|
auth.del_picture,
|
||||||
auth.request_password_reset,
|
auth.request_password_reset,
|
||||||
|
@ -0,0 +1,46 @@
|
|||||||
|
"""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.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():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_table('users_sports_preferences')
|
||||||
|
# ### end Alembic commands ###
|
17
fittrackee/tests/fixtures/fixtures_users.py
vendored
17
fittrackee/tests/fixtures/fixtures_users.py
vendored
@ -3,7 +3,8 @@ import datetime
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from fittrackee import db
|
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()
|
@pytest.fixture()
|
||||||
@ -81,3 +82,17 @@ def user_3() -> User:
|
|||||||
db.session.add(user)
|
db.session.add(user)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return user
|
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
|
||||||
|
@ -830,6 +830,186 @@ class TestUserPreferencesUpdate(ApiTestCaseMixin):
|
|||||||
assert 'error' in data['status']
|
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):
|
class TestUserPicture(ApiTestCaseMixin):
|
||||||
def test_it_updates_user_picture(self, app: Flask, user_1: User) -> None:
|
def test_it_updates_user_picture(self, app: Flask, user_1: User) -> None:
|
||||||
client, auth_token = self.get_test_client_and_auth_token(app)
|
client, auth_token = self.get_test_client_and_auth_token(app)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
from flask import Flask
|
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 fittrackee.workouts.models import Sport, Workout
|
||||||
|
|
||||||
|
|
||||||
@ -59,3 +59,19 @@ class TestUserModel:
|
|||||||
== workout_cycling_user_1.short_id
|
== workout_cycling_user_1.short_id
|
||||||
)
|
)
|
||||||
assert serialized_user['records'][0]['workout_date']
|
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
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
from typing import Dict, Tuple, Union
|
from typing import Dict, Tuple, Union
|
||||||
|
|
||||||
import jwt
|
import jwt
|
||||||
@ -10,6 +11,7 @@ from werkzeug.utils import secure_filename
|
|||||||
|
|
||||||
from fittrackee import appLog, bcrypt, db
|
from fittrackee import appLog, bcrypt, db
|
||||||
from fittrackee.responses import (
|
from fittrackee.responses import (
|
||||||
|
DataNotFoundErrorResponse,
|
||||||
ForbiddenErrorResponse,
|
ForbiddenErrorResponse,
|
||||||
HttpResponse,
|
HttpResponse,
|
||||||
InvalidPayloadErrorResponse,
|
InvalidPayloadErrorResponse,
|
||||||
@ -19,15 +21,18 @@ from fittrackee.responses import (
|
|||||||
)
|
)
|
||||||
from fittrackee.tasks import reset_password_email
|
from fittrackee.tasks import reset_password_email
|
||||||
from fittrackee.utils import get_readable_duration, verify_extension_and_size
|
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 fittrackee.workouts.utils_files import get_absolute_file_path
|
||||||
|
|
||||||
from .decorators import authenticate
|
from .decorators import authenticate
|
||||||
from .models import User
|
from .models import User, UserSportPreference
|
||||||
from .utils import check_passwords, register_controls
|
from .utils import check_passwords, register_controls
|
||||||
from .utils_token import decode_user_token
|
from .utils_token import decode_user_token
|
||||||
|
|
||||||
auth_blueprint = Blueprint('auth', __name__)
|
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'])
|
@auth_blueprint.route('/auth/register', methods=['POST'])
|
||||||
def register_user() -> Union[Tuple[Dict, int], HttpResponse]:
|
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)
|
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'])
|
@auth_blueprint.route('/auth/picture', methods=['POST'])
|
||||||
@authenticate
|
@authenticate
|
||||||
def edit_picture(auth_user_id: int) -> Union[Dict, HttpResponse]:
|
def edit_picture(auth_user_id: int) -> Union[Dict, HttpResponse]:
|
||||||
|
@ -40,6 +40,11 @@ class User(BaseModel):
|
|||||||
'Record', lazy=True, backref=db.backref('user', lazy='joined')
|
'Record', lazy=True, backref=db.backref('user', lazy='joined')
|
||||||
)
|
)
|
||||||
language = db.Column(db.String(50), nullable=True)
|
language = db.Column(db.String(50), nullable=True)
|
||||||
|
sport_preferences = db.relationship(
|
||||||
|
'UserSportPreference',
|
||||||
|
lazy=True,
|
||||||
|
backref=db.backref('user', lazy='joined'),
|
||||||
|
)
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f'<User {self.username!r}>'
|
return f'<User {self.username!r}>'
|
||||||
@ -143,3 +148,41 @@ class User(BaseModel):
|
|||||||
'total_distance': float(total[0]),
|
'total_distance': float(total[0]),
|
||||||
'total_duration': str(total[1]),
|
'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,
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user