Merge branch 'dev' into ascent_in_ft

This commit is contained in:
Sam 2022-07-23 18:36:51 +02:00
commit 15f4480920
54 changed files with 726 additions and 135 deletions

View File

@ -95,4 +95,4 @@ jobs:
export TEST_APP_URL=http://$(hostname --ip-address):5000 export TEST_APP_URL=http://$(hostname --ip-address):5000
sleep 5 sleep 5
nohup flask worker --processes=1 >> nohup.out 2>&1 & 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

View File

@ -50,11 +50,18 @@ docker-build-all: docker-build docker-build-client
docker-build-client: docker-build-client:
docker-compose -f docker-compose-dev.yml build fittrackee_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-init-db:
docker-compose -f docker-compose-dev.yml exec fittrackee docker/init-database.sh 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-logs:
docker-compose -f docker-compose-dev.yml logs --follow docker-compose -f docker-compose-dev.yml logs --follow
@ -85,6 +92,18 @@ docker-shell:
docker-stop: docker-stop:
docker-compose -f docker-compose-dev.yml 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-up:
docker-compose -f docker-compose-dev.yml up fittrackee 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." echo "Deprecated command, will be removed in a next version. Use 'user-set-admin' instead."
$(FTCLI) users update $(USERNAME) --set-admin true $(FTCLI) users update $(USERNAME) --set-admin true
test-all: test-client test-python
test-e2e: test-e2e:
$(PYTEST) e2e --driver firefox $(PYTEST_ARGS) $(PYTEST) e2e --driver firefox $(PYTEST_ARGS)
test-all: test-client test-python
test-e2e-client: test-e2e-client:
E2E_ARGS=client $(PYTEST) e2e --driver firefox $(PYTEST_ARGS) E2E_ARGS=client $(PYTEST) e2e --driver firefox $(PYTEST_ARGS)

View File

@ -12,6 +12,8 @@ services:
- POSTGRES_PASSWORD=postgres - POSTGRES_PASSWORD=postgres
volumes: volumes:
- ./data/db:/var/lib/postgresql/data - ./data/db:/var/lib/postgresql/data
networks:
- fittrackee-net
fittrackee: fittrackee:
container_name: fittrackee container_name: fittrackee
@ -24,14 +26,12 @@ services:
- fittrackee-db - fittrackee-db
- redis - redis
- mail - mail
links:
- fittrackee-db
- redis
- mail
volumes: volumes:
- .:/usr/src/app - .:/usr/src/app
- ./data/workouts:/usr/src/app/workouts - ./data/workouts:/usr/src/app/workouts
- ./data/uploads:/usr/src/app/uploads - ./data/uploads:/usr/src/app/uploads
networks:
- fittrackee-net
fittrackee_client: fittrackee_client:
container_name: fittrackee_client container_name: fittrackee_client
@ -57,6 +57,8 @@ services:
hostname: redis hostname: redis
ports: ports:
- "6379:6379" - "6379:6379"
networks:
- fittrackee-net
mail: mail:
container_name: fittrackee-mailhog container_name: fittrackee-mailhog
@ -64,3 +66,14 @@ services:
ports: ports:
- "1025:1025" - "1025:1025"
- "8025:8025" - "8025:8025"
networks:
- fittrackee-net
selenium:
image: selenium/standalone-firefox:latest
hostname: selenium
privileged: true
shm_size: 2g
networks:
fittrackee-net:

View File

@ -2,7 +2,7 @@
set -e set -e
cd /usr/src/app cd /usr/src/app
source .env.docker source .env
ftcli db drop ftcli db drop
ftcli db upgrade ftcli db upgrade

8
docker/lint-python.sh Executable file
View File

@ -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

View File

@ -2,6 +2,6 @@
set -e set -e
cd /usr/src/app cd /usr/src/app
source .env.docker source .env
flask worker --processes=$WORKERS_PROCESSES >> dramatiq.log 2>&1 flask worker --processes=$WORKERS_PROCESSES >> dramatiq.log 2>&1

View File

@ -2,6 +2,6 @@
set -e set -e
cd /usr/src/app cd /usr/src/app
source .env.docker source .env
ftcli users update $1 --set-admin true ftcli users update $1 --set-admin true

View File

@ -2,6 +2,6 @@
set -e set -e
cd /usr/src/app cd /usr/src/app
source .env.docker source .env
/bin/bash /bin/bash

8
docker/test-e2e.sh Executable file
View File

@ -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 $*

7
docker/test-python.sh Executable file
View File

@ -0,0 +1,7 @@
#!/bin/bash
set -e
cd /usr/src/app
source .env
pytest fittrackee $*

View File

@ -45,6 +45,7 @@ Workouts
- User records by sports: - User records by sports:
- average speed - average speed
- farthest distance - farthest distance
- highest ascent (**new in 0.6.11**, can be hidden, see user preferences)
- longest duration - longest duration
- maximum speed - maximum speed
@ -71,6 +72,7 @@ Account & preferences
- A user can reset his password (*new in 0.3.0*) - 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 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 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*): - A user can set sport preferences (*new in 0.5.0*):
- change sport color (used for sport image and charts) - change sport color (used for sport image and charts)
- can override stopped speed threshold (for next uploaded gpx files) - can override stopped speed threshold (for next uploaded gpx files)

View File

@ -710,16 +710,22 @@ Installation
For evaluation purposes, docker files are available, installing **FitTrackee** from **sources**. 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 .. code-block:: bash
$ git clone https://github.com/SamR1/FitTrackee.git $ git clone https://github.com/SamR1/FitTrackee.git
$ cd FitTrackee $ cd FitTrackee
$ cp .env.docker .env $ 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) 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:: .. note::
Some environment variables need to be updated like `UI_URL` 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

View File

@ -347,6 +347,7 @@ character “_” allowed</p></li>
<span class="w"> </span><span class="nt">&quot;bio&quot;</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">&quot;bio&quot;</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">&quot;birth_date&quot;</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">&quot;birth_date&quot;</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">&quot;created_at&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;Sun, 14 Jul 2019 14:09:58 GMT&quot;</span><span class="p">,</span><span class="w"></span> <span class="w"> </span><span class="nt">&quot;created_at&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;Sun, 14 Jul 2019 14:09:58 GMT&quot;</span><span class="p">,</span><span class="w"></span>
<span class="w"> </span><span class="nt">&quot;display_ascent&quot;</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">&quot;email&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;sam@example.com&quot;</span><span class="p">,</span><span class="w"></span> <span class="w"> </span><span class="nt">&quot;email&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;sam@example.com&quot;</span><span class="p">,</span><span class="w"></span>
<span class="w"> </span><span class="nt">&quot;first_name&quot;</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">&quot;first_name&quot;</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">&quot;imperial_units&quot;</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w"></span> <span class="w"> </span><span class="nt">&quot;imperial_units&quot;</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">&quot;workout_id&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;hvYBqYBRa7wwXpaStWR4V2&quot;</span><span class="w"></span> <span class="w"> </span><span class="nt">&quot;workout_id&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;hvYBqYBRa7wwXpaStWR4V2&quot;</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="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">&quot;id&quot;</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">&quot;record_type&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;HA&quot;</span><span class="p">,</span><span class="w"></span>
<span class="w"> </span><span class="nt">&quot;sport_id&quot;</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">&quot;user&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;Sam&quot;</span><span class="p">,</span><span class="w"></span>
<span class="w"> </span><span class="nt">&quot;value&quot;</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">&quot;workout_date&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;Sun, 07 Jul 2019 08:00:00 GMT&quot;</span><span class="p">,</span><span class="w"></span>
<span class="w"> </span><span class="nt">&quot;workout_id&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;hvYBqYBRa7wwXpaStWR4V2&quot;</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">&quot;id&quot;</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">&quot;id&quot;</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">&quot;record_type&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;LD&quot;</span><span class="p">,</span><span class="w"></span> <span class="w"> </span><span class="nt">&quot;record_type&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;LD&quot;</span><span class="p">,</span><span class="w"></span>
<span class="w"> </span><span class="nt">&quot;sport_id&quot;</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">&quot;sport_id&quot;</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">&quot;bio&quot;</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">&quot;bio&quot;</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">&quot;birth_date&quot;</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">&quot;birth_date&quot;</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">&quot;created_at&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;Sun, 14 Jul 2019 14:09:58 GMT&quot;</span><span class="p">,</span><span class="w"></span> <span class="w"> </span><span class="nt">&quot;created_at&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;Sun, 14 Jul 2019 14:09:58 GMT&quot;</span><span class="p">,</span><span class="w"></span>
<span class="w"> </span><span class="nt">&quot;display_ascent&quot;</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">&quot;email&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;sam@example.com&quot;</span><span class="p">,</span><span class="w"></span> <span class="w"> </span><span class="nt">&quot;email&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;sam@example.com&quot;</span><span class="p">,</span><span class="w"></span>
<span class="w"> </span><span class="nt">&quot;first_name&quot;</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">&quot;first_name&quot;</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">&quot;imperial_units&quot;</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w"></span> <span class="w"> </span><span class="nt">&quot;imperial_units&quot;</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">&quot;workout_id&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;hvYBqYBRa7wwXpaStWR4V2&quot;</span><span class="w"></span> <span class="w"> </span><span class="nt">&quot;workout_id&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;hvYBqYBRa7wwXpaStWR4V2&quot;</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="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">&quot;id&quot;</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">&quot;record_type&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;HA&quot;</span><span class="p">,</span><span class="w"></span>
<span class="w"> </span><span class="nt">&quot;sport_id&quot;</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">&quot;user&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;Sam&quot;</span><span class="p">,</span><span class="w"></span>
<span class="w"> </span><span class="nt">&quot;value&quot;</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">&quot;workout_date&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;Sun, 07 Jul 2019 08:00:00 GMT&quot;</span><span class="p">,</span><span class="w"></span>
<span class="w"> </span><span class="nt">&quot;workout_id&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;hvYBqYBRa7wwXpaStWR4V2&quot;</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">&quot;id&quot;</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">&quot;id&quot;</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">&quot;record_type&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;LD&quot;</span><span class="p">,</span><span class="w"></span> <span class="w"> </span><span class="nt">&quot;record_type&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;LD&quot;</span><span class="p">,</span><span class="w"></span>
<span class="w"> </span><span class="nt">&quot;sport_id&quot;</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">&quot;sport_id&quot;</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">&quot;bio&quot;</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">&quot;bio&quot;</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">&quot;birth_date&quot;</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">&quot;birth_date&quot;</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">&quot;created_at&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;Sun, 14 Jul 2019 14:09:58 GMT&quot;</span><span class="p">,</span><span class="w"></span> <span class="w"> </span><span class="nt">&quot;created_at&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;Sun, 14 Jul 2019 14:09:58 GMT&quot;</span><span class="p">,</span><span class="w"></span>
<span class="w"> </span><span class="nt">&quot;display_ascent&quot;</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">&quot;email&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;sam@example.com&quot;</span><span class="p">,</span><span class="w"></span> <span class="w"> </span><span class="nt">&quot;email&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;sam@example.com&quot;</span><span class="p">,</span><span class="w"></span>
<span class="w"> </span><span class="nt">&quot;first_name&quot;</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">&quot;first_name&quot;</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">&quot;imperial_units&quot;</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w"></span> <span class="w"> </span><span class="nt">&quot;imperial_units&quot;</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">&quot;workout_id&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;hvYBqYBRa7wwXpaStWR4V2&quot;</span><span class="w"></span> <span class="w"> </span><span class="nt">&quot;workout_id&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;hvYBqYBRa7wwXpaStWR4V2&quot;</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="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">&quot;id&quot;</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">&quot;record_type&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;HA&quot;</span><span class="p">,</span><span class="w"></span>
<span class="w"> </span><span class="nt">&quot;sport_id&quot;</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">&quot;user&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;Sam&quot;</span><span class="p">,</span><span class="w"></span>
<span class="w"> </span><span class="nt">&quot;value&quot;</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">&quot;workout_date&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;Sun, 07 Jul 2019 08:00:00 GMT&quot;</span><span class="p">,</span><span class="w"></span>
<span class="w"> </span><span class="nt">&quot;workout_id&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;hvYBqYBRa7wwXpaStWR4V2&quot;</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">&quot;id&quot;</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">&quot;id&quot;</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">&quot;record_type&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;LD&quot;</span><span class="p">,</span><span class="w"></span> <span class="w"> </span><span class="nt">&quot;record_type&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;LD&quot;</span><span class="p">,</span><span class="w"></span>
<span class="w"> </span><span class="nt">&quot;sport_id&quot;</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">&quot;sport_id&quot;</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"> <dl class="field-list simple">
<dt class="field-odd">Request JSON Object<span class="colon">:</span></dt> <dt class="field-odd">Request JSON Object<span class="colon">:</span></dt>
<dd class="field-odd"><ul class="simple"> <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>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>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> </ul>
</dd> </dd>
<dt class="field-even">Request Headers<span class="colon">:</span></dt> <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">&quot;bio&quot;</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">&quot;bio&quot;</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">&quot;birth_date&quot;</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">&quot;birth_date&quot;</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">&quot;created_at&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;Sun, 14 Jul 2019 14:09:58 GMT&quot;</span><span class="p">,</span><span class="w"></span> <span class="w"> </span><span class="nt">&quot;created_at&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;Sun, 14 Jul 2019 14:09:58 GMT&quot;</span><span class="p">,</span><span class="w"></span>
<span class="w"> </span><span class="nt">&quot;display_ascent&quot;</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">&quot;email&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;sam@example.com&quot;</span><span class="p">,</span><span class="w"></span> <span class="w"> </span><span class="nt">&quot;email&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;sam@example.com&quot;</span><span class="p">,</span><span class="w"></span>
<span class="w"> </span><span class="nt">&quot;first_name&quot;</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">&quot;first_name&quot;</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">&quot;imperial_units&quot;</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w"></span> <span class="w"> </span><span class="nt">&quot;imperial_units&quot;</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">&quot;workout_id&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;hvYBqYBRa7wwXpaStWR4V2&quot;</span><span class="w"></span> <span class="w"> </span><span class="nt">&quot;workout_id&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;hvYBqYBRa7wwXpaStWR4V2&quot;</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="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">&quot;id&quot;</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">&quot;record_type&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;HA&quot;</span><span class="p">,</span><span class="w"></span>
<span class="w"> </span><span class="nt">&quot;sport_id&quot;</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">&quot;user&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;Sam&quot;</span><span class="p">,</span><span class="w"></span>
<span class="w"> </span><span class="nt">&quot;value&quot;</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">&quot;workout_date&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;Sun, 07 Jul 2019 08:00:00 GMT&quot;</span><span class="p">,</span><span class="w"></span>
<span class="w"> </span><span class="nt">&quot;workout_id&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;hvYBqYBRa7wwXpaStWR4V2&quot;</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">&quot;id&quot;</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">&quot;id&quot;</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">&quot;record_type&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;LD&quot;</span><span class="p">,</span><span class="w"></span> <span class="w"> </span><span class="nt">&quot;record_type&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;LD&quot;</span><span class="p">,</span><span class="w"></span>
<span class="w"> </span><span class="nt">&quot;sport_id&quot;</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">&quot;sport_id&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w"></span>

View File

@ -212,6 +212,7 @@
<dt>User records by sports:</dt><dd><ul class="simple"> <dt>User records by sports:</dt><dd><ul class="simple">
<li><p>average speed</p></li> <li><p>average speed</p></li>
<li><p>farthest distance</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>longest duration</p></li>
<li><p>maximum speed</p></li> <li><p>maximum speed</p></li>
</ul> </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 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 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 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"> <li><dl class="simple">
<dt>A user can set sport preferences (<em>new in 0.5.0</em>):</dt><dd><ul> <dt>A user can set sport preferences (<em>new in 0.5.0</em>):</dt><dd><ul>
<li><p>change sport color (used for sport image and charts)</p></li> <li><p>change sport color (used for sport image and charts)</p></li>

View File

@ -996,15 +996,23 @@ server {
</div> </div>
<p>For evaluation purposes, docker files are available, installing <strong>FitTrackee</strong> from <strong>sources</strong>.</p> <p>For evaluation purposes, docker files are available, installing <strong>FitTrackee</strong> from <strong>sources</strong>.</p>
<ul class="simple"> <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> </ul>
<div class="highlight-bash notranslate"><div class="highlight"><pre><span></span>$ git clone https://github.com/SamR1/FitTrackee.git <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 $ <span class="nb">cd</span> FitTrackee
$ cp .env.docker .env $ cp .env.docker .env
$ make docker-build docker-run docker-init $ make docker-build
</pre></div> </pre></div>
</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> <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"> <ul class="simple">
<li><p>To set admin rights to the newly created account, use the following command line:</p></li> <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 class="admonition-title">Note</p>
<p>Some environment variables need to be updated like <cite>UI_URL</cite></p> <p>Some environment variables need to be updated like <cite>UI_URL</cite></p>
</div> </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> </section>
</section> </section>

File diff suppressed because one or more lines are too long

View File

@ -45,6 +45,7 @@ Workouts
- User records by sports: - User records by sports:
- average speed - average speed
- farthest distance - farthest distance
- highest ascent (**new in 0.6.11**, can be hidden, see user preferences)
- longest duration - longest duration
- maximum speed - maximum speed
@ -71,6 +72,7 @@ Account & preferences
- A user can reset his password (*new in 0.3.0*) - 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 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 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*): - A user can set sport preferences (*new in 0.5.0*):
- change sport color (used for sport image and charts) - change sport color (used for sport image and charts)
- can override stopped speed threshold (for next uploaded gpx files) - can override stopped speed threshold (for next uploaded gpx files)

View File

@ -710,16 +710,22 @@ Installation
For evaluation purposes, docker files are available, installing **FitTrackee** from **sources**. 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 .. code-block:: bash
$ git clone https://github.com/SamR1/FitTrackee.git $ git clone https://github.com/SamR1/FitTrackee.git
$ cd FitTrackee $ cd FitTrackee
$ cp .env.docker .env $ 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) 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:: .. note::
Some environment variables need to be updated like `UI_URL` 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

View File

@ -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>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -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}}]); "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

File diff suppressed because one or more lines are too long

View File

@ -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")

View File

@ -1272,6 +1272,7 @@ class TestUserPreferencesUpdate(ApiTestCaseMixin):
weekm=True, weekm=True,
language=input_language, language=input_language,
imperial_units=True, imperial_units=True,
display_ascent=False,
) )
), ),
headers=dict(Authorization=f'Bearer {auth_token}'), headers=dict(Authorization=f'Bearer {auth_token}'),
@ -1281,8 +1282,11 @@ class TestUserPreferencesUpdate(ApiTestCaseMixin):
data = json.loads(response.data.decode()) data = json.loads(response.data.decode())
assert data['status'] == 'success' assert data['status'] == 'success'
assert data['message'] == 'user preferences updated' 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']['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): class TestUserSportPreferencesUpdate(ApiTestCaseMixin):

View File

@ -38,6 +38,7 @@ class UserModelAssertMixin:
assert 'nb_workouts' in serialized_user assert 'nb_workouts' in serialized_user
assert 'records' in serialized_user assert 'records' in serialized_user
assert 'sports_list' in serialized_user assert 'sports_list' in serialized_user
assert 'total_ascent' in serialized_user
assert 'total_distance' in serialized_user assert 'total_distance' in serialized_user
assert 'total_duration' 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['language'] == user_1.language
assert serialized_user['timezone'] == user_1.timezone assert serialized_user['timezone'] == user_1.timezone
assert serialized_user['weekm'] == user_1.weekm 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: def test_it_returns_workouts_infos(self, app: Flask, user_1: User) -> None:
serialized_user = user_1.serialize(user_1) serialized_user = user_1.serialize(user_1)
@ -168,6 +170,46 @@ class TestUserRecords(UserModelAssertMixin):
) )
assert serialized_user['records'][0]['workout_date'] 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): class TestUserWorkouts(UserModelAssertMixin):
def test_it_returns_infos_when_no_workouts( def test_it_returns_infos_when_no_workouts(

View File

@ -56,7 +56,7 @@ def assert_workout_data_with_gpx(data: Dict) -> None:
assert segment['pauses'] is None assert segment['pauses'] is None
records = data['data']['workouts'][0]['records'] records = data['data']['workouts'][0]['records']
assert len(records) == 4 assert len(records) == 5
assert records[0]['sport_id'] == 1 assert records[0]['sport_id'] == 1
assert records[0]['workout_id'] == data['data']['workouts'][0]['id'] assert records[0]['workout_id'] == data['data']['workouts'][0]['id']
assert records[0]['record_type'] == 'MS' 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[1]['value'] == '0:04:10'
assert records[2]['sport_id'] == 1 assert records[2]['sport_id'] == 1
assert records[2]['workout_id'] == data['data']['workouts'][0]['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]['value'] == 0.4
assert records[2]['workout_date'] == 'Tue, 13 Mar 2018 12:44:45 GMT' assert records[2]['workout_date'] == 'Tue, 13 Mar 2018 12:44:45 GMT'
assert records[2]['value'] == 0.32
assert records[3]['sport_id'] == 1 assert records[3]['sport_id'] == 1
assert records[3]['workout_id'] == data['data']['workouts'][0]['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]['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: 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 assert segment['pauses'] is None
records = data['data']['workouts'][0]['records'] records = data['data']['workouts'][0]['records']
assert len(records) == 4 assert len(records) == 5
assert records[0]['sport_id'] == 1 assert records[0]['sport_id'] == 1
assert records[0]['workout_id'] == data['data']['workouts'][0]['id'] assert records[0]['workout_id'] == data['data']['workouts'][0]['id']
assert records[0]['record_type'] == 'MS' 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[1]['value'] == '0:03:55'
assert records[2]['sport_id'] == 1 assert records[2]['sport_id'] == 1
assert records[2]['workout_id'] == data['data']['workouts'][0]['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]['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]['sport_id'] == 1
assert records[3]['workout_id'] == data['data']['workouts'][0]['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]['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: 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 'just a workout' == data['data']['workouts'][0]['title']
assert_workout_data_with_gpx(data) 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( def test_it_creates_workout_with_expecting_gpx_path(
self, app: Flask, user_1: User, sport_1_cycling: Sport, gpx_file: str self, app: Flask, user_1: User, sport_1_cycling: Sport, gpx_file: str
) -> None: ) -> None:

View File

@ -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 assert data['data']['workouts'][0]['with_gpx'] is True
records = data['data']['workouts'][0]['records'] records = data['data']['workouts'][0]['records']
assert len(records) == 4 assert len(records) == 5
assert records[0]['sport_id'] == sport_id assert records[0]['sport_id'] == sport_id
assert records[0]['workout_id'] == data['data']['workouts'][0]['id'] assert records[0]['workout_id'] == data['data']['workouts'][0]['id']
assert records[0]['record_type'] == 'MS' 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[1]['value'] == '0:04:10'
assert records[2]['sport_id'] == sport_id assert records[2]['sport_id'] == sport_id
assert records[2]['workout_id'] == data['data']['workouts'][0]['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]['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]['sport_id'] == sport_id
assert records[3]['workout_id'] == data['data']['workouts'][0]['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]['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): class TestEditWorkoutWithGpx(ApiTestCaseMixin):

View File

@ -290,6 +290,7 @@ def get_authenticated_user_profile(
"bio": null, "bio": null,
"birth_date": null, "birth_date": null,
"created_at": "Sun, 14 Jul 2019 14:09:58 GMT", "created_at": "Sun, 14 Jul 2019 14:09:58 GMT",
"display_ascent": true,
"email": "sam@example.com", "email": "sam@example.com",
"first_name": null, "first_name": null,
"imperial_units": false, "imperial_units": false,
@ -319,6 +320,15 @@ def get_authenticated_user_profile(
"workout_date": "Sun, 07 Jul 2019 08:00:00 GMT", "workout_date": "Sun, 07 Jul 2019 08:00:00 GMT",
"workout_id": "hvYBqYBRa7wwXpaStWR4V2" "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, "id": 11,
"record_type": "LD", "record_type": "LD",
@ -390,6 +400,7 @@ def edit_user(auth_user: User) -> Union[Dict, HttpResponse]:
"bio": null, "bio": null,
"birth_date": null, "birth_date": null,
"created_at": "Sun, 14 Jul 2019 14:09:58 GMT", "created_at": "Sun, 14 Jul 2019 14:09:58 GMT",
"display_ascent": true,
"email": "sam@example.com", "email": "sam@example.com",
"first_name": null, "first_name": null,
"imperial_units": false, "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_date": "Sun, 07 Jul 2019 08:00:00 GMT",
"workout_id": "hvYBqYBRa7wwXpaStWR4V2" "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, "id": 11,
"record_type": "LD", "record_type": "LD",
@ -546,6 +566,7 @@ def update_user_account(auth_user: User) -> Union[Dict, HttpResponse]:
"bio": null, "bio": null,
"birth_date": null, "birth_date": null,
"created_at": "Sun, 14 Jul 2019 14:09:58 GMT", "created_at": "Sun, 14 Jul 2019 14:09:58 GMT",
"display_ascent": true,
"email": "sam@example.com", "email": "sam@example.com",
"first_name": null, "first_name": null,
"imperial_units": false, "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_date": "Sun, 07 Jul 2019 08:00:00 GMT",
"workout_id": "hvYBqYBRa7wwXpaStWR4V2" "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, "id": 11,
"record_type": "LD", "record_type": "LD",
@ -746,6 +776,7 @@ def edit_user_preferences(auth_user: User) -> Union[Dict, HttpResponse]:
"bio": null, "bio": null,
"birth_date": null, "birth_date": null,
"created_at": "Sun, 14 Jul 2019 14:09:58 GMT", "created_at": "Sun, 14 Jul 2019 14:09:58 GMT",
"display_ascent": true,
"email": "sam@example.com", "email": "sam@example.com",
"first_name": null, "first_name": null,
"imperial_units": false, "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_date": "Sun, 07 Jul 2019 08:00:00 GMT",
"workout_id": "hvYBqYBRa7wwXpaStWR4V2" "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, "id": 11,
"record_type": "LD", "record_type": "LD",
@ -809,10 +849,11 @@ def edit_user_preferences(auth_user: User) -> Union[Dict, HttpResponse]:
"status": "success" "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 string timezone: user time zone
:<json boolean weekm: does week start on Monday? :<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 :reqheader Authorization: OAuth 2.0 Bearer Token
@ -830,6 +871,7 @@ def edit_user_preferences(auth_user: User) -> Union[Dict, HttpResponse]:
# get post data # get post data
post_data = request.get_json() post_data = request.get_json()
user_mandatory_data = { user_mandatory_data = {
'display_ascent',
'imperial_units', 'imperial_units',
'language', 'language',
'timezone', '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: if not post_data or not post_data.keys() >= user_mandatory_data:
return InvalidPayloadErrorResponse() return InvalidPayloadErrorResponse()
display_ascent = post_data.get('display_ascent')
imperial_units = post_data.get('imperial_units') imperial_units = post_data.get('imperial_units')
language = get_language(post_data.get('language')) language = get_language(post_data.get('language'))
timezone = post_data.get('timezone') timezone = post_data.get('timezone')
weekm = post_data.get('weekm') weekm = post_data.get('weekm')
try: try:
auth_user.display_ascent = display_ascent
auth_user.imperial_units = imperial_units auth_user.imperial_units = imperial_units
auth_user.language = language auth_user.language = language
auth_user.timezone = timezone auth_user.timezone = timezone

View File

@ -50,6 +50,7 @@ class User(BaseModel):
is_active = db.Column(db.Boolean, default=False, nullable=False) is_active = db.Column(db.Boolean, default=False, nullable=False)
email_to_confirm = db.Column(db.String(255), nullable=True) email_to_confirm = db.Column(db.String(255), nullable=True)
confirmation_token = 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: def __repr__(self) -> str:
return f'<User {self.username!r}>' return f'<User {self.username!r}>'
@ -127,7 +128,7 @@ class User(BaseModel):
raise UserNotFoundException() raise UserNotFoundException()
sports = [] sports = []
total = (0, '0:00:00') total = (0, '0:00:00', 0)
if self.workouts_count > 0: # type: ignore if self.workouts_count > 0: # type: ignore
sports = ( sports = (
db.session.query(Workout.sport_id) db.session.query(Workout.sport_id)
@ -138,7 +139,9 @@ class User(BaseModel):
) )
total = ( total = (
db.session.query( 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) .filter(Workout.user_id == self.id)
.first() .first()
@ -162,6 +165,7 @@ class User(BaseModel):
'sports_list': [ 'sports_list': [
sport for sportslist in sports for sport in sportslist 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_distance': float(total[0]),
'total_duration': str(total[1]), 'total_duration': str(total[1]),
'username': self.username, 'username': self.username,
@ -170,6 +174,7 @@ class User(BaseModel):
serialized_user = { serialized_user = {
**serialized_user, **serialized_user,
**{ **{
'display_ascent': self.display_ascent,
'imperial_units': self.imperial_units, 'imperial_units': self.imperial_units,
'language': self.language, 'language': self.language,
'timezone': self.timezone, 'timezone': self.timezone,

View File

@ -22,6 +22,7 @@ BaseModel: DeclarativeMeta = db.Model
record_types = [ record_types = [
'AS', # 'Best Average Speed' 'AS', # 'Best Average Speed'
'FD', # 'Farthest Distance' 'FD', # 'Farthest Distance'
'HA', # 'Highest Ascent'
'LD', # 'Longest Duration' 'LD', # 'Longest Duration'
'MS', # 'Max speed' 'MS', # 'Max speed'
] ]
@ -319,9 +320,14 @@ class Workout(BaseModel):
def get_user_workout_records( def get_user_workout_records(
cls, user_id: int, sport_id: int, as_integer: Optional[bool] = False cls, user_id: int, sport_id: int, as_integer: Optional[bool] = False
) -> Dict: ) -> Dict:
"""
Note:
Values for ascent are null for workouts without gpx
"""
record_types_columns = { record_types_columns = {
'AS': 'ave_speed', # 'Average speed' 'AS': 'ave_speed', # 'Average speed'
'FD': 'distance', # 'Farthest Distance' 'FD': 'distance', # 'Farthest Distance'
'HA': 'ascent', # 'Highest Ascent'
'LD': 'moving', # 'Longest Duration' 'LD': 'moving', # 'Longest Duration'
'MS': 'max_speed', # 'Max speed' 'MS': 'max_speed', # 'Max speed'
} }
@ -329,7 +335,11 @@ class Workout(BaseModel):
for record_type, column in record_types_columns.items(): for record_type, column in record_types_columns.items():
column_sorted = getattr(getattr(Workout, column), 'desc')() column_sorted = getattr(getattr(Workout, column), 'desc')()
record_workout = ( 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) .order_by(column_sorted, Workout.workout_date)
.first() .first()
) )
@ -481,7 +491,7 @@ class Record(BaseModel):
return datetime.timedelta(seconds=self._value) return datetime.timedelta(seconds=self._value)
elif self.record_type in ['AS', 'MS']: elif self.record_type in ['AS', 'MS']:
return float(self._value / 100) return float(self._value / 100)
else: # 'FD' else: # 'FD' or 'HA'
return float(self._value / 1000) return float(self._value / 1000)
@value.setter # type: ignore @value.setter # type: ignore
@ -491,7 +501,7 @@ class Record(BaseModel):
def serialize(self) -> Dict: def serialize(self) -> Dict:
if self.value is None: if self.value is None:
value = 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 value = float(self.value) # type: ignore
else: # 'LD' else: # 'LD'
value = str(self.value) # type: ignore value = str(self.value) # type: ignore

View File

@ -42,7 +42,8 @@
props.user.records, props.user.records,
translateSports(props.sports, t), translateSports(props.sports, t),
props.user.timezone, props.user.timezone,
props.user.imperial_units props.user.imperial_units,
props.user.display_ascent
) )
) )
</script> </script>

View File

@ -8,7 +8,13 @@
<StatCard <StatCard
icon="road" icon="road"
:value="totalDistance" :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 <StatCard
icon="clock-o" icon="clock-o"
@ -16,6 +22,7 @@
:text="totalDuration.duration" :text="totalDuration.duration"
/> />
<StatCard <StatCard
v-if="!user.display_ascent"
icon="tags" icon="tags"
:value="user.nb_sports" :value="user.nb_sports"
:text="$t('workouts.SPORT', user.nb_sports)" :text="$t('workouts.SPORT', user.nb_sports)"
@ -43,15 +50,23 @@
() => props.user.total_duration () => props.user.total_duration
) )
const totalDuration = computed(() => get_duration(userTotalDuration)) const totalDuration = computed(() => get_duration(userTotalDuration))
const defaultUnitFrom: TUnit = 'km' const distanceUnitFrom: TUnit = 'km'
const unitTo: TUnit = user.value.imperial_units const distanceUnitTo: TUnit = user.value.imperial_units
? units[defaultUnitFrom].defaultTarget ? units[distanceUnitFrom].defaultTarget
: defaultUnitFrom : distanceUnitFrom
const totalDistance: ComputedRef<number> = computed(() => const totalDistance: ComputedRef<number> = computed(() =>
user.value.imperial_units user.value.imperial_units
? convertDistance(user.value.total_distance, defaultUnitFrom, unitTo, 2) ? convertDistance(user.value.total_distance, distanceUnitFrom, distanceUnitTo, 2)
: parseFloat(user.value.total_distance.toFixed(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>) { function get_duration(total_duration: ComputedRef<string>) {
const duration = total_duration.value.match(/day/g) const duration = total_duration.value.match(/day/g)

View File

@ -15,6 +15,8 @@
) )
}} }}
</dd> </dd>
<dt>{{ $t('user.PROFILE.ASCENT_DATA') }}:</dt>
<dd>{{ $t(`common.${display_ascent}`) }}</dd>
</dl> </dl>
<div class="profile-buttons"> <div class="profile-buttons">
<button @click="$router.push('/profile/edit/preferences')"> <button @click="$router.push('/profile/edit/preferences')">
@ -28,11 +30,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue' import { computed } from 'vue'
import { IUserProfile } from '@/types/user' import { IAuthUserProfile } from '@/types/user'
import { languageLabels } from '@/utils/locales' import { languageLabels } from '@/utils/locales'
interface Props { interface Props {
user: IUserProfile user: IAuthUserProfile
} }
const props = defineProps<Props>() const props = defineProps<Props>()
@ -45,4 +47,7 @@
const timezone = computed(() => const timezone = computed(() =>
props.user.timezone ? props.user.timezone : 'Europe/Paris' props.user.timezone ? props.user.timezone : 'Europe/Paris'
) )
const display_ascent = computed(() =>
props.user.display_ascent ? 'DISPLAYED' : 'HIDDEN'
)
</script> </script>

View File

@ -23,34 +23,66 @@
@updateTimezone="updateTZ" @updateTimezone="updateTZ"
/> />
</label> </label>
<label class="form-items"> <div class="form-items form-checkboxes">
<span class="checkboxes-label">
{{ $t('user.PROFILE.FIRST_DAY_OF_WEEK') }} {{ $t('user.PROFILE.FIRST_DAY_OF_WEEK') }}
<select id="weekm" v-model="userForm.weekm" :disabled="loading"> </span>
<option <div class="checkboxes">
v-for="start in weekStart" <label v-for="start in weekStart" :key="start.label">
:value="start.value" <input
:key="start.value" type="radio"
> :id="start.label"
{{ $t(`user.PROFILE.${start.label}`) }} :name="start.label"
</option> :checked="start.value === userForm.weekm"
</select>
</label>
<label class="form-items">
{{ $t('user.PROFILE.UNITS.LABEL') }}
<select
id="imperial_units"
v-model="userForm.imperial_units"
:disabled="loading" :disabled="loading"
> @input="updateWeekM(start.value)"
<option />
v-for="unit in imperialUnits" <span class="checkbox-label">
:value="unit.value" {{ $t(`user.PROFILE.${start.label}`) }}
:key="unit.value" </span>
>
{{ $t(`user.PROFILE.UNITS.${unit.label}`) }}
</option>
</select>
</label> </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"> <div class="form-buttons">
<button class="confirm" type="submit"> <button class="confirm" type="submit">
{{ $t('buttons.SUBMIT') }} {{ $t('buttons.SUBMIT') }}
@ -72,40 +104,51 @@
import TimezoneDropdown from '@/components/User/ProfileEdition/TimezoneDropdown.vue' import TimezoneDropdown from '@/components/User/ProfileEdition/TimezoneDropdown.vue'
import { AUTH_USER_STORE, ROOT_STORE } from '@/store/constants' 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 { useStore } from '@/use/useStore'
import { availableLanguages } from '@/utils/locales' import { availableLanguages } from '@/utils/locales'
interface Props { interface Props {
user: IUserProfile user: IAuthUserProfile
} }
const props = defineProps<Props>() const props = defineProps<Props>()
const store = useStore() const store = useStore()
const userForm: IUserPreferencesPayload = reactive({ const userForm: IUserPreferencesPayload = reactive({
display_ascent: true,
imperial_units: false, imperial_units: false,
language: '', language: '',
timezone: 'Europe/Paris', timezone: 'Europe/Paris',
weekm: false, weekm: false,
}) })
const weekStart = [ const weekStart = [
{
label: 'MONDAY',
value: true,
},
{ {
label: 'SUNDAY', label: 'SUNDAY',
value: false, value: false,
}, },
{
label: 'MONDAY',
value: true,
},
] ]
const imperialUnits = [ const imperialUnits = [
{
label: 'METRIC',
value: false,
},
{ {
label: 'IMPERIAL', label: 'IMPERIAL',
value: true, value: true,
}, },
]
const ascentData = [
{ {
label: 'METRIC', label: 'DISPLAYED',
value: true,
},
{
label: 'HIDDEN',
value: false, 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.imperial_units = user.imperial_units ? user.imperial_units : false
userForm.language = user.language ? user.language : 'en' userForm.language = user.language ? user.language : 'en'
userForm.timezone = user.timezone ? user.timezone : 'Europe/Paris' userForm.timezone = user.timezone ? user.timezone : 'Europe/Paris'
@ -134,8 +178,43 @@
function updateTZ(value: string) { function updateTZ(value: string) {
userForm.timezone = value 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(() => { onUnmounted(() => {
store.commit(ROOT_STORE.MUTATIONS.EMPTY_ERROR_MESSAGES) store.commit(ROOT_STORE.MUTATIONS.EMPTY_ERROR_MESSAGES)
}) })
</script> </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>

View File

@ -3,9 +3,11 @@
"CONFIRMATION": "Confirmation", "CONFIRMATION": "Confirmation",
"CONTACT": "contact", "CONTACT": "contact",
"DAY": "day | days", "DAY": "day | days",
"DISPLAYED": "Displayed",
"DOCUMENTATION": "documentation", "DOCUMENTATION": "documentation",
"HOME": "Home", "HOME": "Home",
"HERE": "here", "HERE": "here",
"HIDDEN": "Hidden",
"SELECTS": { "SELECTS": {
"ORDER_BY": { "ORDER_BY": {
"LABEL": "order by" "LABEL": "order by"

View File

@ -48,6 +48,7 @@
"PASSWORD_UPDATED": "Your password have been updated. Click {0} to log in.", "PASSWORD_UPDATED": "Your password have been updated. Click {0} to log in.",
"PROFILE": { "PROFILE": {
"ACCOUNT_EDITION": "Account edition", "ACCOUNT_EDITION": "Account edition",
"ASCENT_DATA": "Ascent-related data (records, total)",
"BACK_TO_PROFILE": "Back to profile", "BACK_TO_PROFILE": "Back to profile",
"BIO": "Bio", "BIO": "Bio",
"BIRTH_DATE": "Birth date", "BIRTH_DATE": "Birth date",

View File

@ -43,6 +43,7 @@
"RECORD": "record | records", "RECORD": "record | records",
"RECORD_AS": "Ave. speed", "RECORD_AS": "Ave. speed",
"RECORD_FD": "Farthest distance", "RECORD_FD": "Farthest distance",
"RECORD_HA": "Highest ascent",
"RECORD_LD": "Longest duration", "RECORD_LD": "Longest duration",
"RECORD_MS": "Max. speed", "RECORD_MS": "Max. speed",
"REMAINING_CHARS": "remaining characters", "REMAINING_CHARS": "remaining characters",

View File

@ -3,9 +3,11 @@
"CONFIRMATION": "Confirmation", "CONFIRMATION": "Confirmation",
"CONTACT": "contact", "CONTACT": "contact",
"DAY": "jour | jours", "DAY": "jour | jours",
"DISPLAYED": "Affiché",
"DOCUMENTATION": "documentation (en)", "DOCUMENTATION": "documentation (en)",
"HOME": "Accueil", "HOME": "Accueil",
"HERE": "ici", "HERE": "ici",
"HIDDEN": "Masqué",
"SELECTS": { "SELECTS": {
"ORDER_BY": { "ORDER_BY": {
"LABEL": "trier par " "LABEL": "trier par "

View File

@ -48,6 +48,7 @@
"PASSWORD_UPDATED": "Votre mot de passe a été mis à jour. Cliquez {0} pour vous connecter.", "PASSWORD_UPDATED": "Votre mot de passe a été mis à jour. Cliquez {0} pour vous connecter.",
"PROFILE": { "PROFILE": {
"ACCOUNT_EDITION": "Mise à jour du compte", "ACCOUNT_EDITION": "Mise à jour du compte",
"ASCENT_DATA": "Données relatives au dénivelé positif (records, total)",
"BACK_TO_PROFILE": "Revenir au profil", "BACK_TO_PROFILE": "Revenir au profil",
"BIO": "Bio", "BIO": "Bio",
"BIRTH_DATE": "Date de naissance", "BIRTH_DATE": "Date de naissance",

View File

@ -43,6 +43,7 @@
"RECORD": "record | records", "RECORD": "record | records",
"RECORD_AS": "Vitesse moy.", "RECORD_AS": "Vitesse moy.",
"RECORD_FD": "Distance la + longue", "RECORD_FD": "Distance la + longue",
"RECORD_HA": "Dénivelé positif le + élevé",
"RECORD_LD": "Durée la + longue", "RECORD_LD": "Durée la + longue",
"RECORD_MS": "Vitesse max.", "RECORD_MS": "Vitesse max.",
"REMAINING_CHARS": "nombre de caractères restants ", "REMAINING_CHARS": "nombre de caractères restants ",

View File

@ -24,6 +24,7 @@ export interface IUserProfile {
} }
export interface IAuthUserProfile extends IUserProfile { export interface IAuthUserProfile extends IUserProfile {
display_ascent: boolean
imperial_units: boolean imperial_units: boolean
language: string | null language: string | null
timezone: string timezone: string
@ -58,6 +59,7 @@ export interface IAdminUserPayload {
} }
export interface IUserPreferencesPayload { export interface IUserPreferencesPayload {
display_ascent: boolean
imperial_units: boolean imperial_units: boolean
language: string language: string
timezone: string timezone: string

View File

@ -9,30 +9,47 @@ export const formatRecord = (
tz: string, tz: string,
useImperialUnits: boolean useImperialUnits: boolean
): Record<string, string | number> => { ): Record<string, string | number> => {
const unitFrom: TUnit = 'km' const distanceUnitFrom: TUnit = 'km'
const unitTo: TUnit = useImperialUnits const distanceUnitTo: TUnit = useImperialUnits
? units[unitFrom].defaultTarget ? units[distanceUnitFrom].defaultTarget
: unitFrom : distanceUnitFrom
const ascentUnitFrom: TUnit = 'm'
const ascentUnitTo: TUnit = useImperialUnits
? units[ascentUnitFrom].defaultTarget
: ascentUnitFrom
let value let value
switch (record.record_type) { switch (record.record_type) {
case 'AS': case 'AS':
case 'MS': case 'MS':
value = `${convertDistance( value = `${convertDistance(
+record.value, +record.value,
unitFrom, distanceUnitFrom,
unitTo, distanceUnitTo,
2 2
)} ${unitTo}/h` )} ${distanceUnitTo}/h`
break break
case 'FD': 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 break
case 'LD': case 'LD':
value = record.value value = record.value
break break
default: default:
throw new Error( 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 { return {
@ -55,9 +72,12 @@ export const getRecordsBySports = (
records: IRecord[], records: IRecord[],
translatedSports: ITranslatedSport[], translatedSports: ITranslatedSport[],
tz: string, tz: string,
useImperialUnits: boolean useImperialUnits: boolean,
display_ascent: boolean
): IRecordsBySports => ): 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) const sport = translatedSports.find((s) => s.id === record.sport_id)
if (sport && sport.label) { if (sport && sport.label) {
if (sportList[sport.translatedLabel] === void 0) { if (sportList[sport.translatedLabel] === void 0) {

View File

@ -94,6 +94,28 @@ describe('formatRecord', () => {
workout_id: 'hvYBqYBRa7wwXpaStWR4V2', 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) => { testsParams.map((testParams) => {
it(testParams.description, () => { it(testParams.description, () => {
@ -199,6 +221,28 @@ describe('formatRecord after conversion', () => {
workout_id: 'hvYBqYBRa7wwXpaStWR4V2', 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) => { testsParams.map((testParams) => {
it(testParams.description, () => { it(testParams.description, () => {
@ -231,7 +275,7 @@ describe('formatRecord (invalid record type)', () => {
false false
) )
).to.throw( ).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, testParams.input.records,
translatedSports, translatedSports,
testParams.input.tz, testParams.input.tz,
false false,
true
), ),
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
@ -486,6 +531,7 @@ describe('getRecordsBySports after conversion', () => {
testParams.input.records, testParams.input.records,
translatedSports, translatedSports,
testParams.input.tz, testParams.input.tz,
true,
true true
), ),
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // 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
)
})
)
})