Merge branch 'dev' into ascent_in_ft
This commit is contained in:
@@ -95,4 +95,4 @@ jobs:
|
||||
export TEST_APP_URL=http://$(hostname --ip-address):5000
|
||||
sleep 5
|
||||
nohup flask worker --processes=1 >> nohup.out 2>&1 &
|
||||
pytest e2e --driver Remote --capability browserName firefox --host selenium --port 4444
|
||||
pytest e2e --driver Remote --capability browserName firefox --selenium-host selenium --selenium-port 4444
|
||||
|
||||
@@ -50,11 +50,18 @@ docker-build-all: docker-build docker-build-client
|
||||
docker-build-client:
|
||||
docker-compose -f docker-compose-dev.yml build fittrackee_client
|
||||
|
||||
docker-init: docker-init-db docker-restart docker-run-workers
|
||||
docker-init: docker-run docker-init-db docker-restart docker-run-workers
|
||||
|
||||
docker-init-db:
|
||||
docker-compose -f docker-compose-dev.yml exec fittrackee docker/init-database.sh
|
||||
|
||||
docker-lint-client:
|
||||
docker-compose -f docker-compose-dev.yml up -d fittrackee_client
|
||||
docker-compose -f docker-compose-dev.yml exec fittrackee_client yarn lint
|
||||
|
||||
docker-lint-python: docker-run
|
||||
docker-compose -f docker-compose-dev.yml exec fittrackee docker/lint-python.sh
|
||||
|
||||
docker-logs:
|
||||
docker-compose -f docker-compose-dev.yml logs --follow
|
||||
|
||||
@@ -85,6 +92,18 @@ docker-shell:
|
||||
docker-stop:
|
||||
docker-compose -f docker-compose-dev.yml stop
|
||||
|
||||
docker-test-client:
|
||||
docker-compose -f docker-compose-dev.yml up -d fittrackee_client
|
||||
docker-compose -f docker-compose-dev.yml exec fittrackee_client yarn test:unit
|
||||
|
||||
# needs a running application
|
||||
docker-test-e2e: docker-run
|
||||
docker-compose -f docker-compose-dev.yml up -d selenium
|
||||
docker-compose -f docker-compose-dev.yml exec fittrackee docker/test-e2e.sh $(PYTEST_ARGS)
|
||||
|
||||
docker-test-python: docker-run
|
||||
docker-compose -f docker-compose-dev.yml exec fittrackee docker/test-python.sh $(PYTEST_ARGS)
|
||||
|
||||
docker-up:
|
||||
docker-compose -f docker-compose-dev.yml up fittrackee
|
||||
|
||||
@@ -184,11 +203,11 @@ set-admin:
|
||||
echo "Deprecated command, will be removed in a next version. Use 'user-set-admin' instead."
|
||||
$(FTCLI) users update $(USERNAME) --set-admin true
|
||||
|
||||
test-all: test-client test-python
|
||||
|
||||
test-e2e:
|
||||
$(PYTEST) e2e --driver firefox $(PYTEST_ARGS)
|
||||
|
||||
test-all: test-client test-python
|
||||
|
||||
test-e2e-client:
|
||||
E2E_ARGS=client $(PYTEST) e2e --driver firefox $(PYTEST_ARGS)
|
||||
|
||||
|
||||
+17
-4
@@ -12,6 +12,8 @@ services:
|
||||
- POSTGRES_PASSWORD=postgres
|
||||
volumes:
|
||||
- ./data/db:/var/lib/postgresql/data
|
||||
networks:
|
||||
- fittrackee-net
|
||||
|
||||
fittrackee:
|
||||
container_name: fittrackee
|
||||
@@ -24,14 +26,12 @@ services:
|
||||
- fittrackee-db
|
||||
- redis
|
||||
- mail
|
||||
links:
|
||||
- fittrackee-db
|
||||
- redis
|
||||
- mail
|
||||
volumes:
|
||||
- .:/usr/src/app
|
||||
- ./data/workouts:/usr/src/app/workouts
|
||||
- ./data/uploads:/usr/src/app/uploads
|
||||
networks:
|
||||
- fittrackee-net
|
||||
|
||||
fittrackee_client:
|
||||
container_name: fittrackee_client
|
||||
@@ -57,6 +57,8 @@ services:
|
||||
hostname: redis
|
||||
ports:
|
||||
- "6379:6379"
|
||||
networks:
|
||||
- fittrackee-net
|
||||
|
||||
mail:
|
||||
container_name: fittrackee-mailhog
|
||||
@@ -64,3 +66,14 @@ services:
|
||||
ports:
|
||||
- "1025:1025"
|
||||
- "8025:8025"
|
||||
networks:
|
||||
- fittrackee-net
|
||||
|
||||
selenium:
|
||||
image: selenium/standalone-firefox:latest
|
||||
hostname: selenium
|
||||
privileged: true
|
||||
shm_size: 2g
|
||||
|
||||
networks:
|
||||
fittrackee-net:
|
||||
@@ -2,7 +2,7 @@
|
||||
set -e
|
||||
cd /usr/src/app
|
||||
|
||||
source .env.docker
|
||||
source .env
|
||||
|
||||
ftcli db drop
|
||||
ftcli db upgrade
|
||||
Executable
+8
@@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
cd /usr/src/app
|
||||
|
||||
source .env
|
||||
|
||||
mypy fittrackee
|
||||
pytest --flake8 --isort --black -m "flake8 or isort or black" fittrackee e2e --ignore=fittrackee/migrations
|
||||
@@ -2,6 +2,6 @@
|
||||
set -e
|
||||
cd /usr/src/app
|
||||
|
||||
source .env.docker
|
||||
source .env
|
||||
|
||||
flask worker --processes=$WORKERS_PROCESSES >> dramatiq.log 2>&1
|
||||
|
||||
+1
-1
@@ -2,6 +2,6 @@
|
||||
set -e
|
||||
cd /usr/src/app
|
||||
|
||||
source .env.docker
|
||||
source .env
|
||||
|
||||
ftcli users update $1 --set-admin true
|
||||
|
||||
+1
-1
@@ -2,6 +2,6 @@
|
||||
set -e
|
||||
cd /usr/src/app
|
||||
|
||||
source .env.docker
|
||||
source .env
|
||||
|
||||
/bin/bash
|
||||
Executable
+8
@@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
cd /usr/src/app
|
||||
|
||||
source .env
|
||||
|
||||
export TEST_APP_URL=http://$(hostname --ip-address):5000
|
||||
pytest e2e --driver Remote --capability browserName firefox --selenium-host selenium --selenium-port 4444 $*
|
||||
Executable
+7
@@ -0,0 +1,7 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
cd /usr/src/app
|
||||
|
||||
source .env
|
||||
|
||||
pytest fittrackee $*
|
||||
@@ -45,6 +45,7 @@ Workouts
|
||||
- User records by sports:
|
||||
- average speed
|
||||
- farthest distance
|
||||
- highest ascent (**new in 0.6.11**, can be hidden, see user preferences)
|
||||
- longest duration
|
||||
- maximum speed
|
||||
|
||||
@@ -71,6 +72,7 @@ Account & preferences
|
||||
- A user can reset his password (*new in 0.3.0*)
|
||||
- A user can change his email address (*new in 0.6.0*)
|
||||
- A user can choose between metric system and imperial system for distance, elevation and speed display (*new in 0.5.0*)
|
||||
- A user can choose to display or hide ascent records and total on Dashboard (*new in 0.6.11*)
|
||||
- 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)
|
||||
|
||||
@@ -710,16 +710,22 @@ Installation
|
||||
|
||||
For evaluation purposes, docker files are available, installing **FitTrackee** from **sources**.
|
||||
|
||||
- To install **FitTrackee** with database initialisation and run the application and dramatiq workers:
|
||||
- To install **FitTrackee**:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ git clone https://github.com/SamR1/FitTrackee.git
|
||||
$ cd FitTrackee
|
||||
$ cp .env.docker .env
|
||||
$ make docker-build docker-run docker-init
|
||||
$ make docker-build
|
||||
|
||||
Open http://localhost:5000 and register.
|
||||
- To initialise database:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ docker-init
|
||||
|
||||
- Open http://localhost:5000 and register.
|
||||
|
||||
Open http://localhost:8025 to access `MailHog interface <https://github.com/mailhog/MailHog>`_ (email testing tool)
|
||||
|
||||
@@ -773,3 +779,12 @@ Open http://localhost:3000
|
||||
|
||||
.. note::
|
||||
Some environment variables need to be updated like `UI_URL`
|
||||
|
||||
- to run lint or tests:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ make docker-lint-client # run lint on javascript files
|
||||
$ make docker-test-client # run unit tests on Client
|
||||
$ make docker-lint-python # run type check and lint on python files
|
||||
$ make docker-test-python # run unit tests on API
|
||||
+43
-2
@@ -347,6 +347,7 @@ character “_” allowed</p></li>
|
||||
<span class="w"> </span><span class="nt">"bio"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"birth_date"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"created_at"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Sun, 14 Jul 2019 14:09:58 GMT"</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"display_ascent"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"email"</span><span class="p">:</span><span class="w"> </span><span class="s2">"sam@example.com"</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"first_name"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"imperial_units"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w"></span>
|
||||
@@ -377,6 +378,15 @@ character “_” allowed</p></li>
|
||||
<span class="w"> </span><span class="nt">"workout_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"hvYBqYBRa7wwXpaStWR4V2"</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="p">},</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="p">{</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"id"</span><span class="p">:</span><span class="w"> </span><span class="mi">13</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"record_type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"HA"</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"sport_id"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"user"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Sam"</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"value"</span><span class="p">:</span><span class="w"> </span><span class="mf">43.97</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"workout_date"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Sun, 07 Jul 2019 08:00:00 GMT"</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"workout_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"hvYBqYBRa7wwXpaStWR4V2"</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="p">},</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="p">{</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"id"</span><span class="p">:</span><span class="w"> </span><span class="mi">11</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"record_type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"LD"</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"sport_id"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w"></span>
|
||||
@@ -449,6 +459,7 @@ character “_” allowed</p></li>
|
||||
<span class="w"> </span><span class="nt">"bio"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"birth_date"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"created_at"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Sun, 14 Jul 2019 14:09:58 GMT"</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"display_ascent"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"email"</span><span class="p">:</span><span class="w"> </span><span class="s2">"sam@example.com"</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"first_name"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"imperial_units"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w"></span>
|
||||
@@ -479,6 +490,15 @@ character “_” allowed</p></li>
|
||||
<span class="w"> </span><span class="nt">"workout_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"hvYBqYBRa7wwXpaStWR4V2"</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="p">},</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="p">{</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"id"</span><span class="p">:</span><span class="w"> </span><span class="mi">13</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"record_type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"HA"</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"sport_id"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"user"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Sam"</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"value"</span><span class="p">:</span><span class="w"> </span><span class="mf">43.97</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"workout_date"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Sun, 07 Jul 2019 08:00:00 GMT"</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"workout_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"hvYBqYBRa7wwXpaStWR4V2"</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="p">},</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="p">{</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"id"</span><span class="p">:</span><span class="w"> </span><span class="mi">11</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"record_type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"LD"</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"sport_id"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w"></span>
|
||||
@@ -566,6 +586,7 @@ character “_” allowed</p></li>
|
||||
<span class="w"> </span><span class="nt">"bio"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"birth_date"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"created_at"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Sun, 14 Jul 2019 14:09:58 GMT"</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"display_ascent"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"email"</span><span class="p">:</span><span class="w"> </span><span class="s2">"sam@example.com"</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"first_name"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"imperial_units"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w"></span>
|
||||
@@ -596,6 +617,15 @@ character “_” allowed</p></li>
|
||||
<span class="w"> </span><span class="nt">"workout_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"hvYBqYBRa7wwXpaStWR4V2"</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="p">},</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="p">{</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"id"</span><span class="p">:</span><span class="w"> </span><span class="mi">13</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"record_type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"HA"</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"sport_id"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"user"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Sam"</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"value"</span><span class="p">:</span><span class="w"> </span><span class="mf">43.97</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"workout_date"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Sun, 07 Jul 2019 08:00:00 GMT"</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"workout_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"hvYBqYBRa7wwXpaStWR4V2"</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="p">},</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="p">{</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"id"</span><span class="p">:</span><span class="w"> </span><span class="mi">11</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"record_type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"LD"</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"sport_id"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w"></span>
|
||||
@@ -633,10 +663,11 @@ character “_” allowed</p></li>
|
||||
<dl class="field-list simple">
|
||||
<dt class="field-odd">Request JSON Object<span class="colon">:</span></dt>
|
||||
<dd class="field-odd"><ul class="simple">
|
||||
<li><p><strong>display_ascent</strong> (<em>boolean</em>) – display highest ascent records and total</p></li>
|
||||
<li><p><strong>imperial_units</strong> (<em>boolean</em>) – display distance in imperial units</p></li>
|
||||
<li><p><strong>language</strong> (<em>string</em>) – language preferences</p></li>
|
||||
<li><p><strong>timezone</strong> (<em>string</em>) – user time zone</p></li>
|
||||
<li><p><strong>weekm</strong> (<em>boolean</em>) – does week start on Monday?</p></li>
|
||||
<li><p><strong>language</strong> (<em>string</em>) – language preferences</p></li>
|
||||
<li><p><strong>imperial_units</strong> (<em>boolean</em>) – display distance in imperial units</p></li>
|
||||
</ul>
|
||||
</dd>
|
||||
<dt class="field-even">Request Headers<span class="colon">:</span></dt>
|
||||
@@ -924,6 +955,7 @@ character “_” allowed</p></li>
|
||||
<span class="w"> </span><span class="nt">"bio"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"birth_date"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"created_at"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Sun, 14 Jul 2019 14:09:58 GMT"</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"display_ascent"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"email"</span><span class="p">:</span><span class="w"> </span><span class="s2">"sam@example.com"</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"first_name"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"imperial_units"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w"></span>
|
||||
@@ -954,6 +986,15 @@ character “_” allowed</p></li>
|
||||
<span class="w"> </span><span class="nt">"workout_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"hvYBqYBRa7wwXpaStWR4V2"</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="p">},</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="p">{</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"id"</span><span class="p">:</span><span class="w"> </span><span class="mi">13</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"record_type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"HA"</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"sport_id"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"user"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Sam"</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"value"</span><span class="p">:</span><span class="w"> </span><span class="mf">43.97</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"workout_date"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Sun, 07 Jul 2019 08:00:00 GMT"</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"workout_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"hvYBqYBRa7wwXpaStWR4V2"</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="p">},</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="p">{</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"id"</span><span class="p">:</span><span class="w"> </span><span class="mi">11</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"record_type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"LD"</span><span class="p">,</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="nt">"sport_id"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w"></span>
|
||||
|
||||
@@ -212,6 +212,7 @@
|
||||
<dt>User records by sports:</dt><dd><ul class="simple">
|
||||
<li><p>average speed</p></li>
|
||||
<li><p>farthest distance</p></li>
|
||||
<li><p>highest ascent (<strong>new in 0.6.11</strong>, can be hidden, see user preferences)</p></li>
|
||||
<li><p>longest duration</p></li>
|
||||
<li><p>maximum speed</p></li>
|
||||
</ul>
|
||||
@@ -248,6 +249,7 @@ A user with an inactive account cannot log in. (<em>new in 0.6.0</em>)</p></li>
|
||||
<li><p>A user can reset his password (<em>new in 0.3.0</em>)</p></li>
|
||||
<li><p>A user can change his email address (<em>new in 0.6.0</em>)</p></li>
|
||||
<li><p>A user can choose between metric system and imperial system for distance, elevation and speed display (<em>new in 0.5.0</em>)</p></li>
|
||||
<li><p>A user can choose to display or hide ascent records and total on Dashboard (<em>new in 0.6.11</em>)</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>
|
||||
|
||||
+20
-3
@@ -996,15 +996,23 @@ server {
|
||||
</div>
|
||||
<p>For evaluation purposes, docker files are available, installing <strong>FitTrackee</strong> from <strong>sources</strong>.</p>
|
||||
<ul class="simple">
|
||||
<li><p>To install <strong>FitTrackee</strong> with database initialisation and run the application and dramatiq workers:</p></li>
|
||||
<li><p>To install <strong>FitTrackee</strong>:</p></li>
|
||||
</ul>
|
||||
<div class="highlight-bash notranslate"><div class="highlight"><pre><span></span>$ git clone https://github.com/SamR1/FitTrackee.git
|
||||
$ <span class="nb">cd</span> FitTrackee
|
||||
$ cp .env.docker .env
|
||||
$ make docker-build docker-run docker-init
|
||||
$ make docker-build
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>Open <a class="reference external" href="http://localhost:5000">http://localhost:5000</a> and register.</p>
|
||||
<ul class="simple">
|
||||
<li><p>To initialise database:</p></li>
|
||||
</ul>
|
||||
<div class="highlight-bash notranslate"><div class="highlight"><pre><span></span>$ docker-init
|
||||
</pre></div>
|
||||
</div>
|
||||
<ul class="simple">
|
||||
<li><p>Open <a class="reference external" href="http://localhost:5000">http://localhost:5000</a> and register.</p></li>
|
||||
</ul>
|
||||
<p>Open <a class="reference external" href="http://localhost:8025">http://localhost:8025</a> to access <a class="reference external" href="https://github.com/mailhog/MailHog">MailHog interface</a> (email testing tool)</p>
|
||||
<ul class="simple">
|
||||
<li><p>To set admin rights to the newly created account, use the following command line:</p></li>
|
||||
@@ -1057,6 +1065,15 @@ $ make docker-build docker-run docker-init
|
||||
<p class="admonition-title">Note</p>
|
||||
<p>Some environment variables need to be updated like <cite>UI_URL</cite></p>
|
||||
</div>
|
||||
<ul class="simple">
|
||||
<li><p>to run lint or tests:</p></li>
|
||||
</ul>
|
||||
<div class="highlight-bash notranslate"><div class="highlight"><pre><span></span>$ make docker-lint-client <span class="c1"># run lint on javascript files</span>
|
||||
$ make docker-test-client <span class="c1"># run unit tests on Client</span>
|
||||
$ make docker-lint-python <span class="c1"># run type check and lint on python files</span>
|
||||
$ make docker-test-python <span class="c1"># run unit tests on API</span>
|
||||
</pre></div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
@@ -45,6 +45,7 @@ Workouts
|
||||
- User records by sports:
|
||||
- average speed
|
||||
- farthest distance
|
||||
- highest ascent (**new in 0.6.11**, can be hidden, see user preferences)
|
||||
- longest duration
|
||||
- maximum speed
|
||||
|
||||
@@ -71,6 +72,7 @@ Account & preferences
|
||||
- A user can reset his password (*new in 0.3.0*)
|
||||
- A user can change his email address (*new in 0.6.0*)
|
||||
- A user can choose between metric system and imperial system for distance, elevation and speed display (*new in 0.5.0*)
|
||||
- A user can choose to display or hide ascent records and total on Dashboard (*new in 0.6.11*)
|
||||
- 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)
|
||||
|
||||
@@ -710,16 +710,22 @@ Installation
|
||||
|
||||
For evaluation purposes, docker files are available, installing **FitTrackee** from **sources**.
|
||||
|
||||
- To install **FitTrackee** with database initialisation and run the application and dramatiq workers:
|
||||
- To install **FitTrackee**:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ git clone https://github.com/SamR1/FitTrackee.git
|
||||
$ cd FitTrackee
|
||||
$ cp .env.docker .env
|
||||
$ make docker-build docker-run docker-init
|
||||
$ make docker-build
|
||||
|
||||
Open http://localhost:5000 and register.
|
||||
- To initialise database:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ docker-init
|
||||
|
||||
- Open http://localhost:5000 and register.
|
||||
|
||||
Open http://localhost:8025 to access `MailHog interface <https://github.com/mailhog/MailHog>`_ (email testing tool)
|
||||
|
||||
@@ -773,3 +779,12 @@ Open http://localhost:3000
|
||||
|
||||
.. note::
|
||||
Some environment variables need to be updated like `UI_URL`
|
||||
|
||||
- to run lint or tests:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ make docker-lint-client # run lint on javascript files
|
||||
$ make docker-test-client # run unit tests on Client
|
||||
$ make docker-lint-python # run type check and lint on python files
|
||||
$ make docker-test-python # run unit tests on API
|
||||
Vendored
+1
-1
@@ -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><script defer="defer" src="/static/js/chunk-vendors.7132edc6.js"></script><script defer="defer" src="/static/js/app.bf1d4e1c.js"></script><link href="/static/css/app.32d0ced1.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></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><script defer="defer" src="/static/js/chunk-vendors.7132edc6.js"></script><script defer="defer" src="/static/js/app.5447516d.js"></script><link href="/static/css/app.f768a44b.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></body></html>
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
-1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+2
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-2
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -1,2 +1,2 @@
|
||||
"use strict";(self["webpackChunkfittrackee_client"]=self["webpackChunkfittrackee_client"]||[]).push([[193],{9161:function(e,s,t){t.r(s),t.d(s,{default:function(){return A}});t(6699);var a=t(6252),r=t(2262),l=t(3577),o=t(3324),n=t(9996);const c={class:"chart-menu"},i={class:"chart-arrow"},u={class:"time-frames custom-checkboxes-group"},d={class:"time-frames-checkboxes custom-checkboxes"},p=["id","name","checked","onInput"],m={class:"chart-arrow"};var v=(0,a.aZ)({__name:"StatsMenu",emits:["arrowClick","timeFrameUpdate"],setup(e,{emit:s}){const t=(0,r.iH)("month"),o=["week","month","year"];function n(e){t.value=e,s("timeFrameUpdate",e)}return(e,r)=>((0,a.wg)(),(0,a.iD)("div",c,[(0,a._)("div",i,[(0,a._)("i",{class:"fa fa-chevron-left","aria-hidden":"true",onClick:r[0]||(r[0]=e=>s("arrowClick",!0))})]),(0,a._)("div",u,[(0,a._)("div",d,[((0,a.wg)(),(0,a.iD)(a.HY,null,(0,a.Ko)(o,(s=>(0,a._)("div",{class:"time-frame custom-checkbox",key:s},[(0,a._)("label",null,[(0,a._)("input",{type:"radio",id:s,name:s,checked:t.value===s,onInput:e=>n(s)},null,40,p),(0,a._)("span",null,(0,l.zw)(e.$t(`statistics.TIME_FRAMES.${s}`)),1)])]))),64))])]),(0,a._)("div",m,[(0,a._)("i",{class:"fa fa-chevron-right","aria-hidden":"true",onClick:r[1]||(r[1]=e=>s("arrowClick",!1))})])]))}}),k=t(3744);const _=(0,k.Z)(v,[["__scopeId","data-v-22d55de2"]]);var S=_,w=t(631);const f={class:"sports-menu"},h=["id","name","checked","onInput"],U={class:"sport-label"};var b=(0,a.aZ)({__name:"StatsSportsMenu",props:{userSports:null,selectedSportIds:{default:()=>[]}},emits:["selectedSportIdsUpdate"],setup(e,{emit:s}){const t=e,{t:n}=(0,o.QT)(),c=(0,a.f3)("sportColors"),{selectedSportIds:i}=(0,r.BK)(t),u=(0,a.Fl)((()=>(0,w.xH)(t.userSports,n)));function d(e){s("selectedSportIdsUpdate",e)}return(e,s)=>{const t=(0,a.up)("SportImage");return(0,a.wg)(),(0,a.iD)("div",f,[((0,a.wg)(!0),(0,a.iD)(a.HY,null,(0,a.Ko)((0,r.SU)(u),(e=>((0,a.wg)(),(0,a.iD)("label",{type:"checkbox",key:e.id,style:(0,l.j5)({color:e.color?e.color:(0,r.SU)(c)[e.label]})},[(0,a._)("input",{type:"checkbox",id:e.id,name:e.label,checked:(0,r.SU)(i).includes(e.id),onInput:s=>d(e.id)},null,40,h),(0,a.Wm)(t,{"sport-label":e.label,color:e.color},null,8,["sport-label","color"]),(0,a._)("span",U,(0,l.zw)(e.translatedLabel),1)],4)))),128))])}}});const I=b;var g=I,T=t(9318);const y={key:0,id:"user-statistics"};var C=(0,a.aZ)({__name:"index",props:{sports:null,user:null},setup(e){const s=e,{t:t}=(0,o.QT)(),{sports:l,user:c}=(0,r.BK)(s),i=(0,r.iH)("month"),u=(0,r.iH)(v(i.value)),d=(0,a.Fl)((()=>(0,w.xH)(s.sports,t))),p=(0,r.iH)(_(s.sports));function m(e){i.value=e,u.value=v(i.value)}function v(e){return(0,T.aZ)(new Date,e,s.user.weekm)}function k(e){u.value=(0,T.FN)(u.value,e,s.user.weekm)}function _(e){return e.map((e=>e.id))}function f(e){p.value.includes(e)?p.value=p.value.filter((s=>s!==e)):p.value.push(e)}return(0,a.YP)((()=>s.sports),(e=>{p.value=_(e)})),(e,s)=>(0,r.SU)(d)?((0,a.wg)(),(0,a.iD)("div",y,[(0,a.Wm)(S,{onTimeFrameUpdate:m,onArrowClick:k}),(0,a.Wm)(n.Z,{sports:(0,r.SU)(l),user:(0,r.SU)(c),chartParams:u.value,"displayed-sport-ids":p.value,fullStats:!0},null,8,["sports","user","chartParams","displayed-sport-ids"]),(0,a.Wm)(g,{"selected-sport-ids":p.value,"user-sports":(0,r.SU)(l),onSelectedSportIdsUpdate:f},null,8,["selected-sport-ids","user-sports"])])):(0,a.kq)("",!0)}});const F=(0,k.Z)(C,[["__scopeId","data-v-d693c7da"]]);var Z=F,x=t(5630),D=t(8602),H=t(9917);const E={id:"statistics",class:"view"},R={key:0,class:"container"};var W=(0,a.aZ)({__name:"StatisticsView",setup(e){const s=(0,H.o)(),t=(0,a.Fl)((()=>s.getters[D.YN.GETTERS.AUTH_USER_PROFILE])),o=(0,a.Fl)((()=>s.getters[D.O8.GETTERS.SPORTS].filter((e=>t.value.sports_list.includes(e.id)))));return(e,s)=>{const n=(0,a.up)("Card");return(0,a.wg)(),(0,a.iD)("div",E,[(0,r.SU)(t).username?((0,a.wg)(),(0,a.iD)("div",R,[(0,a.Wm)(n,null,{title:(0,a.w5)((()=>[(0,a.Uk)((0,l.zw)(e.$t("statistics.STATISTICS")),1)])),content:(0,a.w5)((()=>[(0,a.Wm)(Z,{class:(0,l.C_)({"stats-disabled":0===(0,r.SU)(t).nb_workouts}),user:(0,r.SU)(t),sports:(0,r.SU)(o)},null,8,["class","user","sports"])])),_:1}),0===(0,r.SU)(t).nb_workouts?((0,a.wg)(),(0,a.j4)(x.Z,{key:0})):(0,a.kq)("",!0)])):(0,a.kq)("",!0)])}}});const P=(0,k.Z)(W,[["__scopeId","data-v-2e341d4e"]]);var A=P}}]);
|
||||
//# sourceMappingURL=statistics.1ad194e3.js.map
|
||||
//# sourceMappingURL=statistics.ef50f3c2.js.map
|
||||
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
@@ -0,0 +1,45 @@
|
||||
"""add ascent record
|
||||
|
||||
Revision ID: cd0e6cf83207
|
||||
Revises: 5e3a3a31c432
|
||||
Create Date: 2022-03-22 20:21:13.661883
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'cd0e6cf83207'
|
||||
down_revision = '5e3a3a31c432'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.execute(
|
||||
"""
|
||||
ALTER TYPE record_types ADD VALUE 'HA';
|
||||
"""
|
||||
)
|
||||
|
||||
op.add_column(
|
||||
'users', sa.Column('display_ascent', sa.Boolean(), nullable=True)
|
||||
)
|
||||
op.execute("UPDATE users SET display_ascent = true")
|
||||
op.alter_column('users', 'display_ascent', nullable=False)
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_column('users', 'display_ascent')
|
||||
|
||||
op.execute("DELETE FROM records WHERE record_type = 'HA';")
|
||||
op.execute("ALTER TYPE record_types RENAME TO record_types_old")
|
||||
op.execute("CREATE TYPE record_types AS ENUM('AS', 'FD', 'LD', 'MS')")
|
||||
op.execute(
|
||||
"""
|
||||
ALTER TABLE records ALTER COLUMN record_type TYPE record_types
|
||||
USING record_type::text::record_types
|
||||
"""
|
||||
)
|
||||
op.execute("DROP TYPE record_types_old")
|
||||
@@ -1272,6 +1272,7 @@ class TestUserPreferencesUpdate(ApiTestCaseMixin):
|
||||
weekm=True,
|
||||
language=input_language,
|
||||
imperial_units=True,
|
||||
display_ascent=False,
|
||||
)
|
||||
),
|
||||
headers=dict(Authorization=f'Bearer {auth_token}'),
|
||||
@@ -1281,8 +1282,11 @@ class TestUserPreferencesUpdate(ApiTestCaseMixin):
|
||||
data = json.loads(response.data.decode())
|
||||
assert data['status'] == 'success'
|
||||
assert data['message'] == 'user preferences updated'
|
||||
assert data['data']['display_ascent'] is False
|
||||
assert data['data']['imperial_units'] is True
|
||||
assert data['data']['language'] == expected_language
|
||||
assert data['data'] == jsonify_dict(user_1.serialize(user_1))
|
||||
assert data['data']['timezone'] == 'America/New_York'
|
||||
assert data['data']['weekm'] is True
|
||||
|
||||
|
||||
class TestUserSportPreferencesUpdate(ApiTestCaseMixin):
|
||||
|
||||
@@ -38,6 +38,7 @@ class UserModelAssertMixin:
|
||||
assert 'nb_workouts' in serialized_user
|
||||
assert 'records' in serialized_user
|
||||
assert 'sports_list' in serialized_user
|
||||
assert 'total_ascent' in serialized_user
|
||||
assert 'total_distance' in serialized_user
|
||||
assert 'total_duration' in serialized_user
|
||||
|
||||
@@ -66,6 +67,7 @@ class TestUserSerializeAsAuthUser(UserModelAssertMixin):
|
||||
assert serialized_user['language'] == user_1.language
|
||||
assert serialized_user['timezone'] == user_1.timezone
|
||||
assert serialized_user['weekm'] == user_1.weekm
|
||||
assert serialized_user['display_ascent'] == user_1.display_ascent
|
||||
|
||||
def test_it_returns_workouts_infos(self, app: Flask, user_1: User) -> None:
|
||||
serialized_user = user_1.serialize(user_1)
|
||||
@@ -168,6 +170,46 @@ class TestUserRecords(UserModelAssertMixin):
|
||||
)
|
||||
assert serialized_user['records'][0]['workout_date']
|
||||
|
||||
def test_it_returns_totals_when_user_has_workout_without_ascent(
|
||||
self,
|
||||
app: Flask,
|
||||
user_1: User,
|
||||
sport_1_cycling: Sport,
|
||||
workout_cycling_user_1: Workout,
|
||||
) -> None:
|
||||
serialized_user = user_1.serialize(user_1)
|
||||
assert serialized_user['total_ascent'] == 0
|
||||
assert serialized_user['total_distance'] == 10
|
||||
assert serialized_user['total_duration'] == '1:00:00'
|
||||
|
||||
def test_it_returns_totals_when_user_has_workout_with_ascent(
|
||||
self,
|
||||
app: Flask,
|
||||
user_1: User,
|
||||
sport_1_cycling: Sport,
|
||||
workout_cycling_user_1: Workout,
|
||||
) -> None:
|
||||
workout_cycling_user_1.ascent = 100
|
||||
serialized_user = user_1.serialize(user_1)
|
||||
assert serialized_user['total_ascent'] == 100
|
||||
assert serialized_user['total_distance'] == 10
|
||||
assert serialized_user['total_duration'] == '1:00:00'
|
||||
|
||||
def test_it_returns_totals_when_user_has_mutiple_workouts(
|
||||
self,
|
||||
app: Flask,
|
||||
user_1: User,
|
||||
sport_1_cycling: Sport,
|
||||
sport_2_running: Sport,
|
||||
workout_cycling_user_1: Workout,
|
||||
workout_running_user_1: Workout,
|
||||
) -> None:
|
||||
workout_cycling_user_1.ascent = 100
|
||||
serialized_user = user_1.serialize(user_1)
|
||||
assert serialized_user['total_ascent'] == 100
|
||||
assert serialized_user['total_distance'] == 22
|
||||
assert serialized_user['total_duration'] == '2:40:00'
|
||||
|
||||
|
||||
class TestUserWorkouts(UserModelAssertMixin):
|
||||
def test_it_returns_infos_when_no_workouts(
|
||||
|
||||
@@ -56,7 +56,7 @@ def assert_workout_data_with_gpx(data: Dict) -> None:
|
||||
assert segment['pauses'] is None
|
||||
|
||||
records = data['data']['workouts'][0]['records']
|
||||
assert len(records) == 4
|
||||
assert len(records) == 5
|
||||
assert records[0]['sport_id'] == 1
|
||||
assert records[0]['workout_id'] == data['data']['workouts'][0]['id']
|
||||
assert records[0]['record_type'] == 'MS'
|
||||
@@ -69,14 +69,19 @@ def assert_workout_data_with_gpx(data: Dict) -> None:
|
||||
assert records[1]['value'] == '0:04:10'
|
||||
assert records[2]['sport_id'] == 1
|
||||
assert records[2]['workout_id'] == data['data']['workouts'][0]['id']
|
||||
assert records[2]['record_type'] == 'FD'
|
||||
assert records[2]['record_type'] == 'HA'
|
||||
assert records[2]['value'] == 0.4
|
||||
assert records[2]['workout_date'] == 'Tue, 13 Mar 2018 12:44:45 GMT'
|
||||
assert records[2]['value'] == 0.32
|
||||
assert records[3]['sport_id'] == 1
|
||||
assert records[3]['workout_id'] == data['data']['workouts'][0]['id']
|
||||
assert records[3]['record_type'] == 'AS'
|
||||
assert records[3]['record_type'] == 'FD'
|
||||
assert records[3]['workout_date'] == 'Tue, 13 Mar 2018 12:44:45 GMT'
|
||||
assert records[3]['value'] == 4.61
|
||||
assert records[3]['value'] == 0.32
|
||||
assert records[4]['sport_id'] == 1
|
||||
assert records[4]['workout_id'] == data['data']['workouts'][0]['id']
|
||||
assert records[4]['record_type'] == 'AS'
|
||||
assert records[4]['workout_date'] == 'Tue, 13 Mar 2018 12:44:45 GMT'
|
||||
assert records[4]['value'] == 4.61
|
||||
|
||||
|
||||
def assert_workout_data_with_gpx_segments(data: Dict) -> None:
|
||||
@@ -133,7 +138,7 @@ def assert_workout_data_with_gpx_segments(data: Dict) -> None:
|
||||
assert segment['pauses'] is None
|
||||
|
||||
records = data['data']['workouts'][0]['records']
|
||||
assert len(records) == 4
|
||||
assert len(records) == 5
|
||||
assert records[0]['sport_id'] == 1
|
||||
assert records[0]['workout_id'] == data['data']['workouts'][0]['id']
|
||||
assert records[0]['record_type'] == 'MS'
|
||||
@@ -146,14 +151,18 @@ def assert_workout_data_with_gpx_segments(data: Dict) -> None:
|
||||
assert records[1]['value'] == '0:03:55'
|
||||
assert records[2]['sport_id'] == 1
|
||||
assert records[2]['workout_id'] == data['data']['workouts'][0]['id']
|
||||
assert records[2]['record_type'] == 'FD'
|
||||
assert records[2]['record_type'] == 'HA'
|
||||
assert records[2]['workout_date'] == 'Tue, 13 Mar 2018 12:44:45 GMT'
|
||||
assert records[2]['value'] == 0.3
|
||||
assert records[3]['sport_id'] == 1
|
||||
assert records[3]['workout_id'] == data['data']['workouts'][0]['id']
|
||||
assert records[3]['record_type'] == 'AS'
|
||||
assert records[3]['record_type'] == 'FD'
|
||||
assert records[3]['workout_date'] == 'Tue, 13 Mar 2018 12:44:45 GMT'
|
||||
assert records[3]['value'] == 4.59
|
||||
assert records[3]['value'] == 0.3
|
||||
assert records[4]['sport_id'] == 1
|
||||
assert records[4]['workout_id'] == data['data']['workouts'][0]['id']
|
||||
assert records[4]['record_type'] == 'AS'
|
||||
assert records[4]['workout_date'] == 'Tue, 13 Mar 2018 12:44:45 GMT'
|
||||
assert records[4]['value'] == 4.59
|
||||
|
||||
|
||||
def assert_workout_data_wo_gpx(data: Dict) -> None:
|
||||
@@ -252,6 +261,39 @@ class TestPostWorkoutWithGpx(ApiTestCaseMixin, CallArgsMixin):
|
||||
assert 'just a workout' == data['data']['workouts'][0]['title']
|
||||
assert_workout_data_with_gpx(data)
|
||||
|
||||
def test_it_returns_ha_record_when_a_workout_without_gpx_exists(
|
||||
self,
|
||||
app: Flask,
|
||||
user_1: User,
|
||||
sport_1_cycling: Sport,
|
||||
gpx_file: str,
|
||||
workout_cycling_user_1: Workout,
|
||||
) -> None:
|
||||
client, auth_token = self.get_test_client_and_auth_token(
|
||||
app, user_1.email
|
||||
)
|
||||
|
||||
response = client.post(
|
||||
'/api/workouts',
|
||||
data=dict(
|
||||
file=(BytesIO(str.encode(gpx_file)), 'example.gpx'),
|
||||
data='{"sport_id": 1}',
|
||||
),
|
||||
headers=dict(
|
||||
content_type='multipart/form-data',
|
||||
Authorization=f'Bearer {auth_token}',
|
||||
),
|
||||
)
|
||||
|
||||
data = json.loads(response.data.decode())
|
||||
records = data['data']['workouts'][0]['records']
|
||||
assert len(records) == 1
|
||||
assert records[0]['sport_id'] == 1
|
||||
assert records[0]['workout_id'] == data['data']['workouts'][0]['id']
|
||||
assert records[0]['record_type'] == 'HA'
|
||||
assert records[0]['value'] == 0.4
|
||||
assert records[0]['workout_date'] == 'Tue, 13 Mar 2018 12:44:45 GMT'
|
||||
|
||||
def test_it_creates_workout_with_expecting_gpx_path(
|
||||
self, app: Flask, user_1: User, sport_1_cycling: Sport, gpx_file: str
|
||||
) -> None:
|
||||
|
||||
@@ -32,7 +32,7 @@ def assert_workout_data_with_gpx(data: Dict, sport_id: int) -> None:
|
||||
assert data['data']['workouts'][0]['with_gpx'] is True
|
||||
|
||||
records = data['data']['workouts'][0]['records']
|
||||
assert len(records) == 4
|
||||
assert len(records) == 5
|
||||
assert records[0]['sport_id'] == sport_id
|
||||
assert records[0]['workout_id'] == data['data']['workouts'][0]['id']
|
||||
assert records[0]['record_type'] == 'MS'
|
||||
@@ -45,14 +45,18 @@ def assert_workout_data_with_gpx(data: Dict, sport_id: int) -> None:
|
||||
assert records[1]['value'] == '0:04:10'
|
||||
assert records[2]['sport_id'] == sport_id
|
||||
assert records[2]['workout_id'] == data['data']['workouts'][0]['id']
|
||||
assert records[2]['record_type'] == 'FD'
|
||||
assert records[2]['record_type'] == 'HA'
|
||||
assert records[2]['workout_date'] == 'Tue, 13 Mar 2018 12:44:45 GMT'
|
||||
assert records[2]['value'] == 0.32
|
||||
assert records[3]['sport_id'] == sport_id
|
||||
assert records[3]['workout_id'] == data['data']['workouts'][0]['id']
|
||||
assert records[3]['record_type'] == 'AS'
|
||||
assert records[3]['record_type'] == 'FD'
|
||||
assert records[3]['workout_date'] == 'Tue, 13 Mar 2018 12:44:45 GMT'
|
||||
assert records[3]['value'] == 4.61
|
||||
assert records[3]['value'] == 0.32
|
||||
assert records[4]['sport_id'] == sport_id
|
||||
assert records[4]['workout_id'] == data['data']['workouts'][0]['id']
|
||||
assert records[4]['record_type'] == 'AS'
|
||||
assert records[4]['workout_date'] == 'Tue, 13 Mar 2018 12:44:45 GMT'
|
||||
assert records[4]['value'] == 4.61
|
||||
|
||||
|
||||
class TestEditWorkoutWithGpx(ApiTestCaseMixin):
|
||||
|
||||
@@ -290,6 +290,7 @@ def get_authenticated_user_profile(
|
||||
"bio": null,
|
||||
"birth_date": null,
|
||||
"created_at": "Sun, 14 Jul 2019 14:09:58 GMT",
|
||||
"display_ascent": true,
|
||||
"email": "sam@example.com",
|
||||
"first_name": null,
|
||||
"imperial_units": false,
|
||||
@@ -319,6 +320,15 @@ def get_authenticated_user_profile(
|
||||
"workout_date": "Sun, 07 Jul 2019 08:00:00 GMT",
|
||||
"workout_id": "hvYBqYBRa7wwXpaStWR4V2"
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"record_type": "HA",
|
||||
"sport_id": 1,
|
||||
"user": "Sam",
|
||||
"value": 43.97,
|
||||
"workout_date": "Sun, 07 Jul 2019 08:00:00 GMT",
|
||||
"workout_id": "hvYBqYBRa7wwXpaStWR4V2"
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"record_type": "LD",
|
||||
@@ -390,6 +400,7 @@ def edit_user(auth_user: User) -> Union[Dict, HttpResponse]:
|
||||
"bio": null,
|
||||
"birth_date": null,
|
||||
"created_at": "Sun, 14 Jul 2019 14:09:58 GMT",
|
||||
"display_ascent": true,
|
||||
"email": "sam@example.com",
|
||||
"first_name": null,
|
||||
"imperial_units": false,
|
||||
@@ -419,6 +430,15 @@ def edit_user(auth_user: User) -> Union[Dict, HttpResponse]:
|
||||
"workout_date": "Sun, 07 Jul 2019 08:00:00 GMT",
|
||||
"workout_id": "hvYBqYBRa7wwXpaStWR4V2"
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"record_type": "HA",
|
||||
"sport_id": 1,
|
||||
"user": "Sam",
|
||||
"value": 43.97,
|
||||
"workout_date": "Sun, 07 Jul 2019 08:00:00 GMT",
|
||||
"workout_id": "hvYBqYBRa7wwXpaStWR4V2"
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"record_type": "LD",
|
||||
@@ -546,6 +566,7 @@ def update_user_account(auth_user: User) -> Union[Dict, HttpResponse]:
|
||||
"bio": null,
|
||||
"birth_date": null,
|
||||
"created_at": "Sun, 14 Jul 2019 14:09:58 GMT",
|
||||
"display_ascent": true,
|
||||
"email": "sam@example.com",
|
||||
"first_name": null,
|
||||
"imperial_units": false,
|
||||
@@ -575,6 +596,15 @@ def update_user_account(auth_user: User) -> Union[Dict, HttpResponse]:
|
||||
"workout_date": "Sun, 07 Jul 2019 08:00:00 GMT",
|
||||
"workout_id": "hvYBqYBRa7wwXpaStWR4V2"
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"record_type": "HA",
|
||||
"sport_id": 1,
|
||||
"user": "Sam",
|
||||
"value": 43.97,
|
||||
"workout_date": "Sun, 07 Jul 2019 08:00:00 GMT",
|
||||
"workout_id": "hvYBqYBRa7wwXpaStWR4V2"
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"record_type": "LD",
|
||||
@@ -746,6 +776,7 @@ def edit_user_preferences(auth_user: User) -> Union[Dict, HttpResponse]:
|
||||
"bio": null,
|
||||
"birth_date": null,
|
||||
"created_at": "Sun, 14 Jul 2019 14:09:58 GMT",
|
||||
"display_ascent": true,
|
||||
"email": "sam@example.com",
|
||||
"first_name": null,
|
||||
"imperial_units": false,
|
||||
@@ -775,6 +806,15 @@ def edit_user_preferences(auth_user: User) -> Union[Dict, HttpResponse]:
|
||||
"workout_date": "Sun, 07 Jul 2019 08:00:00 GMT",
|
||||
"workout_id": "hvYBqYBRa7wwXpaStWR4V2"
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"record_type": "HA",
|
||||
"sport_id": 1,
|
||||
"user": "Sam",
|
||||
"value": 43.97,
|
||||
"workout_date": "Sun, 07 Jul 2019 08:00:00 GMT",
|
||||
"workout_id": "hvYBqYBRa7wwXpaStWR4V2"
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"record_type": "LD",
|
||||
@@ -809,10 +849,11 @@ def edit_user_preferences(auth_user: User) -> Union[Dict, HttpResponse]:
|
||||
"status": "success"
|
||||
}
|
||||
|
||||
:<json boolean display_ascent: display highest ascent records and total
|
||||
:<json boolean imperial_units: display distance in imperial units
|
||||
:<json string language: language preferences
|
||||
:<json string timezone: user time zone
|
||||
:<json boolean weekm: does week start on Monday?
|
||||
:<json string language: language preferences
|
||||
:<json boolean imperial_units: display distance in imperial units
|
||||
|
||||
:reqheader Authorization: OAuth 2.0 Bearer Token
|
||||
|
||||
@@ -830,6 +871,7 @@ def edit_user_preferences(auth_user: User) -> Union[Dict, HttpResponse]:
|
||||
# get post data
|
||||
post_data = request.get_json()
|
||||
user_mandatory_data = {
|
||||
'display_ascent',
|
||||
'imperial_units',
|
||||
'language',
|
||||
'timezone',
|
||||
@@ -838,12 +880,14 @@ def edit_user_preferences(auth_user: User) -> Union[Dict, HttpResponse]:
|
||||
if not post_data or not post_data.keys() >= user_mandatory_data:
|
||||
return InvalidPayloadErrorResponse()
|
||||
|
||||
display_ascent = post_data.get('display_ascent')
|
||||
imperial_units = post_data.get('imperial_units')
|
||||
language = get_language(post_data.get('language'))
|
||||
timezone = post_data.get('timezone')
|
||||
weekm = post_data.get('weekm')
|
||||
|
||||
try:
|
||||
auth_user.display_ascent = display_ascent
|
||||
auth_user.imperial_units = imperial_units
|
||||
auth_user.language = language
|
||||
auth_user.timezone = timezone
|
||||
|
||||
@@ -50,6 +50,7 @@ class User(BaseModel):
|
||||
is_active = db.Column(db.Boolean, default=False, nullable=False)
|
||||
email_to_confirm = db.Column(db.String(255), nullable=True)
|
||||
confirmation_token = db.Column(db.String(255), nullable=True)
|
||||
display_ascent = db.Column(db.Boolean, default=True, nullable=False)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f'<User {self.username!r}>'
|
||||
@@ -127,7 +128,7 @@ class User(BaseModel):
|
||||
raise UserNotFoundException()
|
||||
|
||||
sports = []
|
||||
total = (0, '0:00:00')
|
||||
total = (0, '0:00:00', 0)
|
||||
if self.workouts_count > 0: # type: ignore
|
||||
sports = (
|
||||
db.session.query(Workout.sport_id)
|
||||
@@ -138,7 +139,9 @@ class User(BaseModel):
|
||||
)
|
||||
total = (
|
||||
db.session.query(
|
||||
func.sum(Workout.distance), func.sum(Workout.duration)
|
||||
func.sum(Workout.distance),
|
||||
func.sum(Workout.duration),
|
||||
func.sum(Workout.ascent),
|
||||
)
|
||||
.filter(Workout.user_id == self.id)
|
||||
.first()
|
||||
@@ -162,6 +165,7 @@ class User(BaseModel):
|
||||
'sports_list': [
|
||||
sport for sportslist in sports for sport in sportslist
|
||||
],
|
||||
'total_ascent': float(total[2]) if total[2] else 0.0,
|
||||
'total_distance': float(total[0]),
|
||||
'total_duration': str(total[1]),
|
||||
'username': self.username,
|
||||
@@ -170,6 +174,7 @@ class User(BaseModel):
|
||||
serialized_user = {
|
||||
**serialized_user,
|
||||
**{
|
||||
'display_ascent': self.display_ascent,
|
||||
'imperial_units': self.imperial_units,
|
||||
'language': self.language,
|
||||
'timezone': self.timezone,
|
||||
|
||||
@@ -22,6 +22,7 @@ BaseModel: DeclarativeMeta = db.Model
|
||||
record_types = [
|
||||
'AS', # 'Best Average Speed'
|
||||
'FD', # 'Farthest Distance'
|
||||
'HA', # 'Highest Ascent'
|
||||
'LD', # 'Longest Duration'
|
||||
'MS', # 'Max speed'
|
||||
]
|
||||
@@ -319,9 +320,14 @@ class Workout(BaseModel):
|
||||
def get_user_workout_records(
|
||||
cls, user_id: int, sport_id: int, as_integer: Optional[bool] = False
|
||||
) -> Dict:
|
||||
"""
|
||||
Note:
|
||||
Values for ascent are null for workouts without gpx
|
||||
"""
|
||||
record_types_columns = {
|
||||
'AS': 'ave_speed', # 'Average speed'
|
||||
'FD': 'distance', # 'Farthest Distance'
|
||||
'HA': 'ascent', # 'Highest Ascent'
|
||||
'LD': 'moving', # 'Longest Duration'
|
||||
'MS': 'max_speed', # 'Max speed'
|
||||
}
|
||||
@@ -329,7 +335,11 @@ class Workout(BaseModel):
|
||||
for record_type, column in record_types_columns.items():
|
||||
column_sorted = getattr(getattr(Workout, column), 'desc')()
|
||||
record_workout = (
|
||||
Workout.query.filter_by(user_id=user_id, sport_id=sport_id)
|
||||
Workout.query.filter(
|
||||
Workout.user_id == user_id,
|
||||
Workout.sport_id == sport_id,
|
||||
getattr(Workout, column) != None, # noqa
|
||||
)
|
||||
.order_by(column_sorted, Workout.workout_date)
|
||||
.first()
|
||||
)
|
||||
@@ -481,7 +491,7 @@ class Record(BaseModel):
|
||||
return datetime.timedelta(seconds=self._value)
|
||||
elif self.record_type in ['AS', 'MS']:
|
||||
return float(self._value / 100)
|
||||
else: # 'FD'
|
||||
else: # 'FD' or 'HA'
|
||||
return float(self._value / 1000)
|
||||
|
||||
@value.setter # type: ignore
|
||||
@@ -491,7 +501,7 @@ class Record(BaseModel):
|
||||
def serialize(self) -> Dict:
|
||||
if self.value is None:
|
||||
value = None
|
||||
elif self.record_type in ['AS', 'FD', 'MS']:
|
||||
elif self.record_type in ['AS', 'FD', 'HA', 'MS']:
|
||||
value = float(self.value) # type: ignore
|
||||
else: # 'LD'
|
||||
value = str(self.value) # type: ignore
|
||||
|
||||
@@ -42,7 +42,8 @@
|
||||
props.user.records,
|
||||
translateSports(props.sports, t),
|
||||
props.user.timezone,
|
||||
props.user.imperial_units
|
||||
props.user.imperial_units,
|
||||
props.user.display_ascent
|
||||
)
|
||||
)
|
||||
</script>
|
||||
|
||||
@@ -8,7 +8,13 @@
|
||||
<StatCard
|
||||
icon="road"
|
||||
:value="totalDistance"
|
||||
:text="unitTo === 'mi' ? 'miles' : unitTo"
|
||||
:text="distanceUnitTo === 'mi' ? 'miles' : distanceUnitTo"
|
||||
/>
|
||||
<StatCard
|
||||
v-if="user.display_ascent"
|
||||
icon="location-arrow"
|
||||
:value="totalAscent"
|
||||
:text="ascentUnitTo === 'ft' ? 'feet' : ascentUnitTo"
|
||||
/>
|
||||
<StatCard
|
||||
icon="clock-o"
|
||||
@@ -16,6 +22,7 @@
|
||||
:text="totalDuration.duration"
|
||||
/>
|
||||
<StatCard
|
||||
v-if="!user.display_ascent"
|
||||
icon="tags"
|
||||
:value="user.nb_sports"
|
||||
:text="$t('workouts.SPORT', user.nb_sports)"
|
||||
@@ -43,15 +50,23 @@
|
||||
() => props.user.total_duration
|
||||
)
|
||||
const totalDuration = computed(() => get_duration(userTotalDuration))
|
||||
const defaultUnitFrom: TUnit = 'km'
|
||||
const unitTo: TUnit = user.value.imperial_units
|
||||
? units[defaultUnitFrom].defaultTarget
|
||||
: defaultUnitFrom
|
||||
const distanceUnitFrom: TUnit = 'km'
|
||||
const distanceUnitTo: TUnit = user.value.imperial_units
|
||||
? units[distanceUnitFrom].defaultTarget
|
||||
: distanceUnitFrom
|
||||
const totalDistance: ComputedRef<number> = computed(() =>
|
||||
user.value.imperial_units
|
||||
? convertDistance(user.value.total_distance, defaultUnitFrom, unitTo, 2)
|
||||
: parseFloat(user.value.total_distance.toFixed(2))
|
||||
)
|
||||
? convertDistance(user.value.total_distance, distanceUnitFrom, distanceUnitTo, 2)
|
||||
: parseFloat(user.value.total_distance.toFixed(2)))
|
||||
const ascentUnitFrom: TUnit = 'm'
|
||||
const ascentUnitTo: TUnit = user.value.imperial_units
|
||||
? units[ascentUnitFrom].defaultTarget
|
||||
: ascentUnitFrom
|
||||
const totalAscent: ComputedRef<number> = computed(() =>
|
||||
user.value.imperial_units
|
||||
? convertDistance(user.value.total_ascent, ascentUnitFrom, ascentUnitTo, 2)
|
||||
: parseFloat(user.value.total_ascent.toFixed(2)))
|
||||
|
||||
|
||||
function get_duration(total_duration: ComputedRef<string>) {
|
||||
const duration = total_duration.value.match(/day/g)
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
)
|
||||
}}
|
||||
</dd>
|
||||
<dt>{{ $t('user.PROFILE.ASCENT_DATA') }}:</dt>
|
||||
<dd>{{ $t(`common.${display_ascent}`) }}</dd>
|
||||
</dl>
|
||||
<div class="profile-buttons">
|
||||
<button @click="$router.push('/profile/edit/preferences')">
|
||||
@@ -28,11 +30,11 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { IUserProfile } from '@/types/user'
|
||||
import { IAuthUserProfile } from '@/types/user'
|
||||
import { languageLabels } from '@/utils/locales'
|
||||
|
||||
interface Props {
|
||||
user: IUserProfile
|
||||
user: IAuthUserProfile
|
||||
}
|
||||
const props = defineProps<Props>()
|
||||
|
||||
@@ -45,4 +47,7 @@
|
||||
const timezone = computed(() =>
|
||||
props.user.timezone ? props.user.timezone : 'Europe/Paris'
|
||||
)
|
||||
const display_ascent = computed(() =>
|
||||
props.user.display_ascent ? 'DISPLAYED' : 'HIDDEN'
|
||||
)
|
||||
</script>
|
||||
|
||||
@@ -23,34 +23,66 @@
|
||||
@updateTimezone="updateTZ"
|
||||
/>
|
||||
</label>
|
||||
<label class="form-items">
|
||||
<div class="form-items form-checkboxes">
|
||||
<span class="checkboxes-label">
|
||||
{{ $t('user.PROFILE.FIRST_DAY_OF_WEEK') }}
|
||||
<select id="weekm" v-model="userForm.weekm" :disabled="loading">
|
||||
<option
|
||||
v-for="start in weekStart"
|
||||
:value="start.value"
|
||||
:key="start.value"
|
||||
>
|
||||
{{ $t(`user.PROFILE.${start.label}`) }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="form-items">
|
||||
{{ $t('user.PROFILE.UNITS.LABEL') }}
|
||||
<select
|
||||
id="imperial_units"
|
||||
v-model="userForm.imperial_units"
|
||||
</span>
|
||||
<div class="checkboxes">
|
||||
<label v-for="start in weekStart" :key="start.label">
|
||||
<input
|
||||
type="radio"
|
||||
:id="start.label"
|
||||
:name="start.label"
|
||||
:checked="start.value === userForm.weekm"
|
||||
:disabled="loading"
|
||||
>
|
||||
<option
|
||||
v-for="unit in imperialUnits"
|
||||
:value="unit.value"
|
||||
:key="unit.value"
|
||||
>
|
||||
{{ $t(`user.PROFILE.UNITS.${unit.label}`) }}
|
||||
</option>
|
||||
</select>
|
||||
@input="updateWeekM(start.value)"
|
||||
/>
|
||||
<span class="checkbox-label">
|
||||
{{ $t(`user.PROFILE.${start.label}`) }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-items form-checkboxes">
|
||||
<span class="checkboxes-label">
|
||||
{{ $t('user.PROFILE.UNITS.LABEL') }}
|
||||
</span>
|
||||
<div class="checkboxes">
|
||||
<label v-for="unit in imperialUnits" :key="unit.label">
|
||||
<input
|
||||
type="radio"
|
||||
:id="unit.label"
|
||||
:name="unit.label"
|
||||
:checked="unit.value === userForm.imperial_units"
|
||||
:disabled="loading"
|
||||
@input="updateImperialUnit(unit.value)"
|
||||
/>
|
||||
<span class="checkbox-label">
|
||||
{{ $t(`user.PROFILE.UNITS.${unit.label}`) }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-items form-checkboxes">
|
||||
<span class="checkboxes-label">
|
||||
{{ $t('user.PROFILE.ASCENT_DATA') }}
|
||||
</span>
|
||||
<div class="checkboxes">
|
||||
<label v-for="status in ascentData" :key="status.label">
|
||||
<input
|
||||
type="radio"
|
||||
:id="status.label"
|
||||
:name="status.label"
|
||||
:checked="status.value === userForm.display_ascent"
|
||||
:disabled="loading"
|
||||
@input="updateAscentDisplay(status.value)"
|
||||
/>
|
||||
<span class="checkbox-label">
|
||||
{{ $t(`common.${status.label}`) }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-buttons">
|
||||
<button class="confirm" type="submit">
|
||||
{{ $t('buttons.SUBMIT') }}
|
||||
@@ -72,40 +104,51 @@
|
||||
|
||||
import TimezoneDropdown from '@/components/User/ProfileEdition/TimezoneDropdown.vue'
|
||||
import { AUTH_USER_STORE, ROOT_STORE } from '@/store/constants'
|
||||
import { IUserProfile, IUserPreferencesPayload } from '@/types/user'
|
||||
import { IUserPreferencesPayload, IAuthUserProfile } from '@/types/user'
|
||||
import { useStore } from '@/use/useStore'
|
||||
import { availableLanguages } from '@/utils/locales'
|
||||
|
||||
interface Props {
|
||||
user: IUserProfile
|
||||
user: IAuthUserProfile
|
||||
}
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const store = useStore()
|
||||
|
||||
const userForm: IUserPreferencesPayload = reactive({
|
||||
display_ascent: true,
|
||||
imperial_units: false,
|
||||
language: '',
|
||||
timezone: 'Europe/Paris',
|
||||
weekm: false,
|
||||
})
|
||||
const weekStart = [
|
||||
{
|
||||
label: 'MONDAY',
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
label: 'SUNDAY',
|
||||
value: false,
|
||||
},
|
||||
{
|
||||
label: 'MONDAY',
|
||||
value: true,
|
||||
},
|
||||
]
|
||||
const imperialUnits = [
|
||||
{
|
||||
label: 'METRIC',
|
||||
value: false,
|
||||
},
|
||||
{
|
||||
label: 'IMPERIAL',
|
||||
value: true,
|
||||
},
|
||||
]
|
||||
const ascentData = [
|
||||
{
|
||||
label: 'METRIC',
|
||||
label: 'DISPLAYED',
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
label: 'HIDDEN',
|
||||
value: false,
|
||||
},
|
||||
]
|
||||
@@ -122,7 +165,8 @@
|
||||
}
|
||||
})
|
||||
|
||||
function updateUserForm(user: IUserProfile) {
|
||||
function updateUserForm(user: IAuthUserProfile) {
|
||||
userForm.display_ascent = user.display_ascent
|
||||
userForm.imperial_units = user.imperial_units ? user.imperial_units : false
|
||||
userForm.language = user.language ? user.language : 'en'
|
||||
userForm.timezone = user.timezone ? user.timezone : 'Europe/Paris'
|
||||
@@ -134,8 +178,43 @@
|
||||
function updateTZ(value: string) {
|
||||
userForm.timezone = value
|
||||
}
|
||||
function updateAscentDisplay(value: boolean) {
|
||||
userForm.display_ascent = value
|
||||
}
|
||||
function updateImperialUnit(value: boolean) {
|
||||
userForm.imperial_units = value
|
||||
}
|
||||
function updateWeekM(value: boolean) {
|
||||
userForm.weekm = value
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
store.commit(ROOT_STORE.MUTATIONS.EMPTY_ERROR_MESSAGES)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/scss/vars.scss';
|
||||
#user-preferences-edition {
|
||||
.form-items {
|
||||
padding-top: $default-padding * 0.5;
|
||||
}
|
||||
|
||||
.form-checkboxes {
|
||||
.checkboxes-label {
|
||||
font-weight: bold;
|
||||
}
|
||||
.checkboxes {
|
||||
display: flex;
|
||||
gap: $default-padding;
|
||||
flex-wrap: wrap;
|
||||
.checkbox-label {
|
||||
padding-left: $default-padding * 0.5;
|
||||
}
|
||||
label {
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,9 +3,11 @@
|
||||
"CONFIRMATION": "Confirmation",
|
||||
"CONTACT": "contact",
|
||||
"DAY": "day | days",
|
||||
"DISPLAYED": "Displayed",
|
||||
"DOCUMENTATION": "documentation",
|
||||
"HOME": "Home",
|
||||
"HERE": "here",
|
||||
"HIDDEN": "Hidden",
|
||||
"SELECTS": {
|
||||
"ORDER_BY": {
|
||||
"LABEL": "order by"
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
"PASSWORD_UPDATED": "Your password have been updated. Click {0} to log in.",
|
||||
"PROFILE": {
|
||||
"ACCOUNT_EDITION": "Account edition",
|
||||
"ASCENT_DATA": "Ascent-related data (records, total)",
|
||||
"BACK_TO_PROFILE": "Back to profile",
|
||||
"BIO": "Bio",
|
||||
"BIRTH_DATE": "Birth date",
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
"RECORD": "record | records",
|
||||
"RECORD_AS": "Ave. speed",
|
||||
"RECORD_FD": "Farthest distance",
|
||||
"RECORD_HA": "Highest ascent",
|
||||
"RECORD_LD": "Longest duration",
|
||||
"RECORD_MS": "Max. speed",
|
||||
"REMAINING_CHARS": "remaining characters",
|
||||
|
||||
@@ -3,9 +3,11 @@
|
||||
"CONFIRMATION": "Confirmation",
|
||||
"CONTACT": "contact",
|
||||
"DAY": "jour | jours",
|
||||
"DISPLAYED": "Affiché",
|
||||
"DOCUMENTATION": "documentation (en)",
|
||||
"HOME": "Accueil",
|
||||
"HERE": "ici",
|
||||
"HIDDEN": "Masqué",
|
||||
"SELECTS": {
|
||||
"ORDER_BY": {
|
||||
"LABEL": "trier par "
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
"PASSWORD_UPDATED": "Votre mot de passe a été mis à jour. Cliquez {0} pour vous connecter.",
|
||||
"PROFILE": {
|
||||
"ACCOUNT_EDITION": "Mise à jour du compte",
|
||||
"ASCENT_DATA": "Données relatives au dénivelé positif (records, total)",
|
||||
"BACK_TO_PROFILE": "Revenir au profil",
|
||||
"BIO": "Bio",
|
||||
"BIRTH_DATE": "Date de naissance",
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
"RECORD": "record | records",
|
||||
"RECORD_AS": "Vitesse moy.",
|
||||
"RECORD_FD": "Distance la + longue",
|
||||
"RECORD_HA": "Dénivelé positif le + élevé",
|
||||
"RECORD_LD": "Durée la + longue",
|
||||
"RECORD_MS": "Vitesse max.",
|
||||
"REMAINING_CHARS": "nombre de caractères restants ",
|
||||
|
||||
@@ -24,6 +24,7 @@ export interface IUserProfile {
|
||||
}
|
||||
|
||||
export interface IAuthUserProfile extends IUserProfile {
|
||||
display_ascent: boolean
|
||||
imperial_units: boolean
|
||||
language: string | null
|
||||
timezone: string
|
||||
@@ -58,6 +59,7 @@ export interface IAdminUserPayload {
|
||||
}
|
||||
|
||||
export interface IUserPreferencesPayload {
|
||||
display_ascent: boolean
|
||||
imperial_units: boolean
|
||||
language: string
|
||||
timezone: string
|
||||
|
||||
@@ -9,30 +9,47 @@ export const formatRecord = (
|
||||
tz: string,
|
||||
useImperialUnits: boolean
|
||||
): Record<string, string | number> => {
|
||||
const unitFrom: TUnit = 'km'
|
||||
const unitTo: TUnit = useImperialUnits
|
||||
? units[unitFrom].defaultTarget
|
||||
: unitFrom
|
||||
const distanceUnitFrom: TUnit = 'km'
|
||||
const distanceUnitTo: TUnit = useImperialUnits
|
||||
? units[distanceUnitFrom].defaultTarget
|
||||
: distanceUnitFrom
|
||||
const ascentUnitFrom: TUnit = 'm'
|
||||
const ascentUnitTo: TUnit = useImperialUnits
|
||||
? units[ascentUnitFrom].defaultTarget
|
||||
: ascentUnitFrom
|
||||
let value
|
||||
switch (record.record_type) {
|
||||
case 'AS':
|
||||
case 'MS':
|
||||
value = `${convertDistance(
|
||||
+record.value,
|
||||
unitFrom,
|
||||
unitTo,
|
||||
distanceUnitFrom,
|
||||
distanceUnitTo,
|
||||
2
|
||||
)} ${unitTo}/h`
|
||||
)} ${distanceUnitTo}/h`
|
||||
break
|
||||
case 'FD':
|
||||
value = `${convertDistance(+record.value, unitFrom, unitTo, 3)} ${unitTo}`
|
||||
value = `${convertDistance(
|
||||
+record.value,
|
||||
distanceUnitFrom,
|
||||
distanceUnitTo,
|
||||
3
|
||||
)} ${distanceUnitTo}`
|
||||
break
|
||||
case 'HA':
|
||||
value = `${convertDistance(
|
||||
+record.value,
|
||||
ascentUnitFrom,
|
||||
ascentUnitTo,
|
||||
2
|
||||
)} ${ascentUnitTo}`
|
||||
break
|
||||
case 'LD':
|
||||
value = record.value
|
||||
break
|
||||
default:
|
||||
throw new Error(
|
||||
`Invalid record type, expected: "AS", "FD", "LD", "MD", got: "${record.record_type}"`
|
||||
`Invalid record type, expected: "AS", "FD", "HA", "LD", "MD", got: "${record.record_type}"`
|
||||
)
|
||||
}
|
||||
return {
|
||||
@@ -55,9 +72,12 @@ export const getRecordsBySports = (
|
||||
records: IRecord[],
|
||||
translatedSports: ITranslatedSport[],
|
||||
tz: string,
|
||||
useImperialUnits: boolean
|
||||
useImperialUnits: boolean,
|
||||
display_ascent: boolean
|
||||
): IRecordsBySports =>
|
||||
records.reduce((sportList: IRecordsBySports, record) => {
|
||||
records
|
||||
.filter((r) => (display_ascent ? true : r.record_type !== 'HA'))
|
||||
.reduce((sportList: IRecordsBySports, record) => {
|
||||
const sport = translatedSports.find((s) => s.id === record.sport_id)
|
||||
if (sport && sport.label) {
|
||||
if (sportList[sport.translatedLabel] === void 0) {
|
||||
|
||||
@@ -94,6 +94,28 @@ describe('formatRecord', () => {
|
||||
workout_id: 'hvYBqYBRa7wwXpaStWR4V2',
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "return formatted record for 'Highest ascent'",
|
||||
inputParams: {
|
||||
record: {
|
||||
id: 13,
|
||||
record_type: 'HA',
|
||||
sport_id: 1,
|
||||
user: 'admin',
|
||||
value: 100,
|
||||
workout_date: 'Sun, 07 Jul 2019 08:00:00 GMT',
|
||||
workout_id: 'hvYBqYBRa7wwXpaStWR4V2',
|
||||
},
|
||||
timezone: 'Europe/Paris',
|
||||
},
|
||||
expected: {
|
||||
id: 13,
|
||||
record_type: 'HA',
|
||||
value: '100 m',
|
||||
workout_date: '2019/07/07',
|
||||
workout_id: 'hvYBqYBRa7wwXpaStWR4V2',
|
||||
},
|
||||
},
|
||||
]
|
||||
testsParams.map((testParams) => {
|
||||
it(testParams.description, () => {
|
||||
@@ -199,6 +221,28 @@ describe('formatRecord after conversion', () => {
|
||||
workout_id: 'hvYBqYBRa7wwXpaStWR4V2',
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "return formatted record for 'Highest ascent'",
|
||||
inputParams: {
|
||||
record: {
|
||||
id: 13,
|
||||
record_type: 'HA',
|
||||
sport_id: 1,
|
||||
user: 'admin',
|
||||
value: 100,
|
||||
workout_date: 'Sun, 07 Jul 2019 08:00:00 GMT',
|
||||
workout_id: 'hvYBqYBRa7wwXpaStWR4V2',
|
||||
},
|
||||
timezone: 'Europe/Paris',
|
||||
},
|
||||
expected: {
|
||||
id: 13,
|
||||
record_type: 'HA',
|
||||
value: '328.08 ft',
|
||||
workout_date: '2019/07/07',
|
||||
workout_id: 'hvYBqYBRa7wwXpaStWR4V2',
|
||||
},
|
||||
},
|
||||
]
|
||||
testsParams.map((testParams) => {
|
||||
it(testParams.description, () => {
|
||||
@@ -231,7 +275,7 @@ describe('formatRecord (invalid record type)', () => {
|
||||
false
|
||||
)
|
||||
).to.throw(
|
||||
'Invalid record type, expected: "AS", "FD", "LD", "MD", got: "M"'
|
||||
'Invalid record type, expected: "AS", "FD", "HA", "LD", "MD", got: "M"'
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -356,7 +400,8 @@ describe('getRecordsBySports', () => {
|
||||
testParams.input.records,
|
||||
translatedSports,
|
||||
testParams.input.tz,
|
||||
false
|
||||
false,
|
||||
true
|
||||
),
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
@@ -486,6 +531,7 @@ describe('getRecordsBySports after conversion', () => {
|
||||
testParams.input.records,
|
||||
translatedSports,
|
||||
testParams.input.tz,
|
||||
true,
|
||||
true
|
||||
),
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
@@ -495,3 +541,73 @@ describe('getRecordsBySports after conversion', () => {
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
describe('getRecordsBySports with HA record', () => {
|
||||
const testsParams = [
|
||||
{
|
||||
description: 'returns empty object if no records',
|
||||
input: {
|
||||
records: [],
|
||||
tz: 'Europe/Paris',
|
||||
},
|
||||
expected: {},
|
||||
},
|
||||
{
|
||||
description: 'returns records except HA record',
|
||||
input: {
|
||||
records: [
|
||||
{
|
||||
id: 9,
|
||||
record_type: 'AS',
|
||||
sport_id: 1,
|
||||
user: 'admin',
|
||||
value: 18,
|
||||
workout_date: 'Sun, 07 Jul 2019 08:00:00 GMT',
|
||||
workout_id: 'hvYBqYBRa7wwXpaStWR4V2',
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
record_type: 'HA',
|
||||
sport_id: 1,
|
||||
user: 'admin',
|
||||
value: 235,
|
||||
workout_date: 'Sun, 07 Jul 2019 08:00:00 GMT',
|
||||
workout_id: 'hvYBqYBRa7wwXpaStWR4V2',
|
||||
},
|
||||
],
|
||||
tz: 'Europe/Paris',
|
||||
},
|
||||
expected: {
|
||||
'Cycling (Sport)': {
|
||||
color: null,
|
||||
label: 'Cycling (Sport)',
|
||||
records: [
|
||||
{
|
||||
id: 9,
|
||||
record_type: 'AS',
|
||||
value: '18 km/h',
|
||||
workout_date: '2019/07/07',
|
||||
workout_id: 'hvYBqYBRa7wwXpaStWR4V2',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
testsParams.map((testParams) =>
|
||||
it(testParams.description, () => {
|
||||
assert.deepEqual(
|
||||
getRecordsBySports(
|
||||
testParams.input.records,
|
||||
translatedSports,
|
||||
testParams.input.tz,
|
||||
false,
|
||||
false
|
||||
),
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
testParams.expected
|
||||
)
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user