Merge pull request #207 from SamR1/detect-language
Detect navigator language
This commit is contained in:
commit
39c8ba3f7b
2
fittrackee/dist/index.html
vendored
2
fittrackee/dist/index.html
vendored
@ -1 +1 @@
|
|||||||
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><!--[if IE]><link rel="icon" href="/favicon.ico"><![endif]--><link rel="stylesheet" href="/static/css/fork-awesome.min.css"/><link rel="stylesheet" href="/static/css/leaflet.css"/><title>FitTrackee</title><script defer="defer" src="/static/js/chunk-vendors.9821bfa6.js"></script><script defer="defer" src="/static/js/app.78bef568.js"></script><link href="/static/css/app.158f462d.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.9821bfa6.js"></script><script defer="defer" src="/static/js/app.8b9fe8b8.js"></script><link href="/static/css/app.4edb0d03.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>
|
2
fittrackee/dist/service-worker.js
vendored
2
fittrackee/dist/service-worker.js
vendored
File diff suppressed because one or more lines are too long
2
fittrackee/dist/service-worker.js.map
vendored
2
fittrackee/dist/service-worker.js.map
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
fittrackee/dist/static/js/app.78bef568.js
vendored
2
fittrackee/dist/static/js/app.78bef568.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
fittrackee/dist/static/js/app.8b9fe8b8.js
vendored
Normal file
2
fittrackee/dist/static/js/app.8b9fe8b8.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
fittrackee/dist/static/js/app.8b9fe8b8.js.map
vendored
Normal file
1
fittrackee/dist/static/js/app.8b9fe8b8.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -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.145d19e3.js.map
|
//# sourceMappingURL=statistics.c817d0d3.js.map
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1,6 +1,7 @@
|
|||||||
import json
|
import json
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
from typing import Optional
|
||||||
from unittest.mock import MagicMock, Mock, patch
|
from unittest.mock import MagicMock, Mock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@ -233,7 +234,16 @@ class TestUserRegistration(ApiTestCaseMixin):
|
|||||||
assert data['status'] == 'success'
|
assert data['status'] == 'success'
|
||||||
assert 'auth_token' not in data
|
assert 'auth_token' not in data
|
||||||
|
|
||||||
def test_it_creates_user_with_inactive_account(self, app: Flask) -> None:
|
@pytest.mark.parametrize(
|
||||||
|
'input_language,expected_language',
|
||||||
|
[('en', 'en'), ('fr', 'fr'), ('invalid', 'en'), (None, 'en')],
|
||||||
|
)
|
||||||
|
def test_it_creates_user_with_inactive_account(
|
||||||
|
self,
|
||||||
|
app: Flask,
|
||||||
|
input_language: Optional[str],
|
||||||
|
expected_language: str,
|
||||||
|
) -> None:
|
||||||
client = app.test_client()
|
client = app.test_client()
|
||||||
username = self.random_string()
|
username = self.random_string()
|
||||||
email = self.random_email()
|
email = self.random_email()
|
||||||
@ -245,6 +255,7 @@ class TestUserRegistration(ApiTestCaseMixin):
|
|||||||
username=username,
|
username=username,
|
||||||
email=email,
|
email=email,
|
||||||
password=self.random_string(),
|
password=self.random_string(),
|
||||||
|
language=input_language,
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
content_type='application/json',
|
content_type='application/json',
|
||||||
@ -254,9 +265,18 @@ class TestUserRegistration(ApiTestCaseMixin):
|
|||||||
assert new_user.email == email
|
assert new_user.email == email
|
||||||
assert new_user.password is not None
|
assert new_user.password is not None
|
||||||
assert new_user.is_active is False
|
assert new_user.is_active is False
|
||||||
|
assert new_user.language == expected_language
|
||||||
|
|
||||||
def test_it_calls_account_confirmation_email_if_payload_is_valid(
|
@pytest.mark.parametrize(
|
||||||
self, app: Flask, account_confirmation_email_mock: Mock
|
'input_language,expected_language',
|
||||||
|
[('en', 'en'), ('fr', 'fr'), ('invalid', 'en'), (None, 'en')],
|
||||||
|
)
|
||||||
|
def test_it_calls_account_confirmation_email_when_payload_is_valid(
|
||||||
|
self,
|
||||||
|
app: Flask,
|
||||||
|
account_confirmation_email_mock: Mock,
|
||||||
|
input_language: Optional[str],
|
||||||
|
expected_language: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
client = app.test_client()
|
client = app.test_client()
|
||||||
email = self.random_email()
|
email = self.random_email()
|
||||||
@ -271,6 +291,7 @@ class TestUserRegistration(ApiTestCaseMixin):
|
|||||||
username=username,
|
username=username,
|
||||||
email=email,
|
email=email,
|
||||||
password='12345678',
|
password='12345678',
|
||||||
|
language=input_language,
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
content_type='application/json',
|
content_type='application/json',
|
||||||
@ -279,7 +300,7 @@ class TestUserRegistration(ApiTestCaseMixin):
|
|||||||
|
|
||||||
account_confirmation_email_mock.send.assert_called_once_with(
|
account_confirmation_email_mock.send.assert_called_once_with(
|
||||||
{
|
{
|
||||||
'language': 'en',
|
'language': expected_language,
|
||||||
'email': email,
|
'email': email,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -1227,8 +1248,16 @@ class TestUserPreferencesUpdate(ApiTestCaseMixin):
|
|||||||
|
|
||||||
self.assert_400(response)
|
self.assert_400(response)
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
'input_language,expected_language',
|
||||||
|
[('en', 'en'), ('fr', 'fr'), ('invalid', 'en'), (None, 'en')],
|
||||||
|
)
|
||||||
def test_it_updates_user_preferences(
|
def test_it_updates_user_preferences(
|
||||||
self, app: Flask, user_1: User
|
self,
|
||||||
|
app: Flask,
|
||||||
|
user_1: User,
|
||||||
|
input_language: Optional[str],
|
||||||
|
expected_language: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
client, auth_token = self.get_test_client_and_auth_token(
|
client, auth_token = self.get_test_client_and_auth_token(
|
||||||
app, user_1.email
|
app, user_1.email
|
||||||
@ -1241,7 +1270,7 @@ class TestUserPreferencesUpdate(ApiTestCaseMixin):
|
|||||||
dict(
|
dict(
|
||||||
timezone='America/New_York',
|
timezone='America/New_York',
|
||||||
weekm=True,
|
weekm=True,
|
||||||
language='fr',
|
language=input_language,
|
||||||
imperial_units=True,
|
imperial_units=True,
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
@ -1252,6 +1281,7 @@ 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']['language'] == expected_language
|
||||||
assert data['data'] == jsonify_dict(user_1.serialize(user_1))
|
assert data['data'] == jsonify_dict(user_1.serialize(user_1))
|
||||||
|
|
||||||
|
|
||||||
@ -2237,6 +2267,7 @@ class TestResendAccountConfirmationEmail(ApiTestCaseMixin):
|
|||||||
) -> None:
|
) -> None:
|
||||||
client = app.test_client()
|
client = app.test_client()
|
||||||
expected_token = self.random_string()
|
expected_token = self.random_string()
|
||||||
|
inactive_user.language = 'fr'
|
||||||
|
|
||||||
with patch('secrets.token_urlsafe', return_value=expected_token):
|
with patch('secrets.token_urlsafe', return_value=expected_token):
|
||||||
client.post(
|
client.post(
|
||||||
@ -2248,7 +2279,7 @@ class TestResendAccountConfirmationEmail(ApiTestCaseMixin):
|
|||||||
|
|
||||||
account_confirmation_email_mock.send.assert_called_once_with(
|
account_confirmation_email_mock.send.assert_called_once_with(
|
||||||
{
|
{
|
||||||
'language': 'en',
|
'language': inactive_user.language,
|
||||||
'email': inactive_user.email,
|
'email': inactive_user.email,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -2,7 +2,7 @@ import datetime
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import secrets
|
import secrets
|
||||||
from typing import Dict, Tuple, Union
|
from typing import Dict, Optional, Tuple, Union
|
||||||
|
|
||||||
import jwt
|
import jwt
|
||||||
from flask import Blueprint, current_app, request
|
from flask import Blueprint, current_app, request
|
||||||
@ -43,6 +43,13 @@ HEX_COLOR_REGEX = regex = "^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$"
|
|||||||
NOT_FOUND_MESSAGE = 'the requested URL was not found on the server'
|
NOT_FOUND_MESSAGE = 'the requested URL was not found on the server'
|
||||||
|
|
||||||
|
|
||||||
|
def get_language(language: Optional[str]) -> str:
|
||||||
|
# Note: some users may not have language preferences set
|
||||||
|
if not language or language not in current_app.config['LANGUAGES']:
|
||||||
|
language = 'en'
|
||||||
|
return language
|
||||||
|
|
||||||
|
|
||||||
def send_account_confirmation_email(user: User) -> None:
|
def send_account_confirmation_email(user: User) -> None:
|
||||||
if current_app.config['CAN_SEND_EMAILS']:
|
if current_app.config['CAN_SEND_EMAILS']:
|
||||||
ui_url = current_app.config['UI_URL']
|
ui_url = current_app.config['UI_URL']
|
||||||
@ -57,7 +64,7 @@ def send_account_confirmation_email(user: User) -> None:
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
user_data = {
|
user_data = {
|
||||||
'language': 'en',
|
'language': get_language(user.language),
|
||||||
'email': user.email,
|
'email': user.email,
|
||||||
}
|
}
|
||||||
account_confirmation_email.send(user_data, email_data)
|
account_confirmation_email.send(user_data, email_data)
|
||||||
@ -106,6 +113,8 @@ def register_user() -> Union[Tuple[Dict, int], HttpResponse]:
|
|||||||
:<json string username: username (3 to 30 characters required)
|
:<json string username: username (3 to 30 characters required)
|
||||||
:<json string email: user email
|
:<json string email: user email
|
||||||
:<json string password: password (8 characters required)
|
:<json string password: password (8 characters required)
|
||||||
|
:<json string lang: user language preferences (if not provided or invalid,
|
||||||
|
fallback to 'en' (english))
|
||||||
|
|
||||||
:statuscode 200: success
|
:statuscode 200: success
|
||||||
:statuscode 400:
|
:statuscode 400:
|
||||||
@ -138,6 +147,7 @@ def register_user() -> Union[Tuple[Dict, int], HttpResponse]:
|
|||||||
username = post_data.get('username')
|
username = post_data.get('username')
|
||||||
email = post_data.get('email')
|
email = post_data.get('email')
|
||||||
password = post_data.get('password')
|
password = post_data.get('password')
|
||||||
|
language = get_language(post_data.get('language'))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
ret = register_controls(username, email, password)
|
ret = register_controls(username, email, password)
|
||||||
@ -165,6 +175,7 @@ def register_user() -> Union[Tuple[Dict, int], HttpResponse]:
|
|||||||
new_user = User(username=username, email=email, password=password)
|
new_user = User(username=username, email=email, password=password)
|
||||||
new_user.timezone = 'Europe/Paris'
|
new_user.timezone = 'Europe/Paris'
|
||||||
new_user.confirmation_token = secrets.token_urlsafe(30)
|
new_user.confirmation_token = secrets.token_urlsafe(30)
|
||||||
|
new_user.language = language
|
||||||
db.session.add(new_user)
|
db.session.add(new_user)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
@ -661,9 +672,7 @@ def update_user_account(auth_user: User) -> Union[Dict, HttpResponse]:
|
|||||||
if current_app.config['CAN_SEND_EMAILS']:
|
if current_app.config['CAN_SEND_EMAILS']:
|
||||||
ui_url = current_app.config['UI_URL']
|
ui_url = current_app.config['UI_URL']
|
||||||
user_data = {
|
user_data = {
|
||||||
'language': (
|
'language': get_language(auth_user.language),
|
||||||
'en' if auth_user.language is None else auth_user.language
|
|
||||||
),
|
|
||||||
'email': auth_user.email,
|
'email': auth_user.email,
|
||||||
}
|
}
|
||||||
data = {
|
data = {
|
||||||
@ -830,7 +839,7 @@ def edit_user_preferences(auth_user: User) -> Union[Dict, HttpResponse]:
|
|||||||
return InvalidPayloadErrorResponse()
|
return InvalidPayloadErrorResponse()
|
||||||
|
|
||||||
imperial_units = post_data.get('imperial_units')
|
imperial_units = post_data.get('imperial_units')
|
||||||
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')
|
||||||
|
|
||||||
@ -1189,7 +1198,7 @@ def request_password_reset() -> Union[Dict, HttpResponse]:
|
|||||||
if user:
|
if user:
|
||||||
password_reset_token = user.encode_password_reset_token(user.id)
|
password_reset_token = user.encode_password_reset_token(user.id)
|
||||||
ui_url = current_app.config['UI_URL']
|
ui_url = current_app.config['UI_URL']
|
||||||
user_language = 'en' if user.language is None else user.language
|
user_language = get_language(user.language)
|
||||||
email_data = {
|
email_data = {
|
||||||
'expiration_delay': get_readable_duration(
|
'expiration_delay': get_readable_duration(
|
||||||
current_app.config['PASSWORD_TOKEN_EXPIRATION_SECONDS'],
|
current_app.config['PASSWORD_TOKEN_EXPIRATION_SECONDS'],
|
||||||
@ -1280,9 +1289,7 @@ def update_password() -> Union[Dict, HttpResponse]:
|
|||||||
if current_app.config['CAN_SEND_EMAILS']:
|
if current_app.config['CAN_SEND_EMAILS']:
|
||||||
password_change_email.send(
|
password_change_email.send(
|
||||||
{
|
{
|
||||||
'language': (
|
'language': get_language(user.language),
|
||||||
'en' if user.language is None else user.language
|
|
||||||
),
|
|
||||||
'email': user.email,
|
'email': user.email,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -23,6 +23,7 @@ from fittrackee.responses import (
|
|||||||
from fittrackee.utils import get_readable_duration
|
from fittrackee.utils import get_readable_duration
|
||||||
from fittrackee.workouts.models import Record, Workout, WorkoutSegment
|
from fittrackee.workouts.models import Record, Workout, WorkoutSegment
|
||||||
|
|
||||||
|
from .auth import get_language
|
||||||
from .decorators import authenticate, authenticate_as_admin
|
from .decorators import authenticate, authenticate_as_admin
|
||||||
from .exceptions import InvalidEmailException, UserNotFoundException
|
from .exceptions import InvalidEmailException, UserNotFoundException
|
||||||
from .models import User, UserSportPreference
|
from .models import User, UserSportPreference
|
||||||
@ -530,7 +531,7 @@ def update_user(auth_user: User, user_name: str) -> Union[Dict, HttpResponse]:
|
|||||||
)
|
)
|
||||||
|
|
||||||
if current_app.config['CAN_SEND_EMAILS']:
|
if current_app.config['CAN_SEND_EMAILS']:
|
||||||
user_language = 'en' if user.language is None else user.language
|
user_language = get_language(user.language)
|
||||||
ui_url = current_app.config['UI_URL']
|
ui_url = current_app.config['UI_URL']
|
||||||
if reset_password:
|
if reset_password:
|
||||||
user_data = {
|
user_data = {
|
||||||
|
@ -35,6 +35,7 @@
|
|||||||
import { ROOT_STORE } from '@/store/constants'
|
import { ROOT_STORE } from '@/store/constants'
|
||||||
import { TAppConfig } from '@/types/application'
|
import { TAppConfig } from '@/types/application'
|
||||||
import { useStore } from '@/use/useStore'
|
import { useStore } from '@/use/useStore'
|
||||||
|
import { localeFromLanguage } from '@/utils/locales'
|
||||||
|
|
||||||
const store = useStore()
|
const store = useStore()
|
||||||
|
|
||||||
@ -47,7 +48,10 @@
|
|||||||
const hideScrollBar = ref(false)
|
const hideScrollBar = ref(false)
|
||||||
const displayScrollButton = ref(false)
|
const displayScrollButton = ref(false)
|
||||||
|
|
||||||
onBeforeMount(() => store.dispatch(ROOT_STORE.ACTIONS.GET_APPLICATION_CONFIG))
|
onBeforeMount(() => {
|
||||||
|
initLanguage()
|
||||||
|
store.dispatch(ROOT_STORE.ACTIONS.GET_APPLICATION_CONFIG)
|
||||||
|
})
|
||||||
onMounted(() => scroll())
|
onMounted(() => scroll())
|
||||||
|
|
||||||
function updateHideScrollBar(isMenuOpen: boolean) {
|
function updateHideScrollBar(isMenuOpen: boolean) {
|
||||||
@ -74,6 +78,18 @@
|
|||||||
displayScrollButton.value = false
|
displayScrollButton.value = false
|
||||||
}, 300)
|
}, 300)
|
||||||
}
|
}
|
||||||
|
function initLanguage() {
|
||||||
|
let language = 'en'
|
||||||
|
try {
|
||||||
|
const navigatorLanguage = navigator.language.split('-')[0]
|
||||||
|
if (navigatorLanguage in localeFromLanguage) {
|
||||||
|
language = navigatorLanguage
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
language = 'en'
|
||||||
|
}
|
||||||
|
store.dispatch(ROOT_STORE.ACTIONS.UPDATE_APPLICATION_LANGUAGE, language)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
@ -191,6 +191,9 @@
|
|||||||
const appConfig: ComputedRef<TAppConfig> = computed(
|
const appConfig: ComputedRef<TAppConfig> = computed(
|
||||||
() => store.getters[ROOT_STORE.GETTERS.APP_CONFIG]
|
() => store.getters[ROOT_STORE.GETTERS.APP_CONFIG]
|
||||||
)
|
)
|
||||||
|
const language: ComputedRef<string> = computed(
|
||||||
|
() => store.getters[ROOT_STORE.GETTERS.LANGUAGE]
|
||||||
|
)
|
||||||
const registration_disabled: ComputedRef<boolean> = computed(
|
const registration_disabled: ComputedRef<boolean> = computed(
|
||||||
() =>
|
() =>
|
||||||
props.action === 'register' && !appConfig.value.is_registration_enabled
|
props.action === 'register' && !appConfig.value.is_registration_enabled
|
||||||
@ -245,6 +248,7 @@
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
default:
|
default:
|
||||||
|
formData['language'] = language.value
|
||||||
store.dispatch(AUTH_USER_STORE.ACTIONS.LOGIN_OR_REGISTER, {
|
store.dispatch(AUTH_USER_STORE.ACTIONS.LOGIN_OR_REGISTER, {
|
||||||
actionType,
|
actionType,
|
||||||
formData,
|
formData,
|
||||||
|
@ -93,6 +93,7 @@ export interface ILoginRegisterFormData {
|
|||||||
username: string
|
username: string
|
||||||
email: string
|
email: string
|
||||||
password: string
|
password: string
|
||||||
|
language?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ILoginOrRegisterData {
|
export interface ILoginOrRegisterData {
|
||||||
|
Loading…
Reference in New Issue
Block a user