Merge pull request #207 from SamR1/detect-language
Detect navigator language
This commit is contained in:
Vendored
+1
-1
@@ -1 +1 @@
|
||||
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><!--[if IE]><link rel="icon" href="/favicon.ico"><![endif]--><link rel="stylesheet" href="/static/css/fork-awesome.min.css"/><link rel="stylesheet" href="/static/css/leaflet.css"/><title>FitTrackee</title><script defer="defer" src="/static/js/chunk-vendors.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>
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
-2
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2
-2
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -1,2 +1,2 @@
|
||||
"use strict";(self["webpackChunkfittrackee_client"]=self["webpackChunkfittrackee_client"]||[]).push([[193],{9161:function(e,s,t){t.r(s),t.d(s,{default:function(){return A}});t(6699);var a=t(6252),r=t(2262),l=t(3577),o=t(3324),n=t(9996);const c={class:"chart-menu"},i={class:"chart-arrow"},u={class:"time-frames custom-checkboxes-group"},d={class:"time-frames-checkboxes custom-checkboxes"},p=["id","name","checked","onInput"],m={class:"chart-arrow"};var v=(0,a.aZ)({__name:"StatsMenu",emits:["arrowClick","timeFrameUpdate"],setup(e,{emit:s}){const t=(0,r.iH)("month"),o=["week","month","year"];function n(e){t.value=e,s("timeFrameUpdate",e)}return(e,r)=>((0,a.wg)(),(0,a.iD)("div",c,[(0,a._)("div",i,[(0,a._)("i",{class:"fa fa-chevron-left","aria-hidden":"true",onClick:r[0]||(r[0]=e=>s("arrowClick",!0))})]),(0,a._)("div",u,[(0,a._)("div",d,[((0,a.wg)(),(0,a.iD)(a.HY,null,(0,a.Ko)(o,(s=>(0,a._)("div",{class:"time-frame custom-checkbox",key:s},[(0,a._)("label",null,[(0,a._)("input",{type:"radio",id:s,name:s,checked:t.value===s,onInput:e=>n(s)},null,40,p),(0,a._)("span",null,(0,l.zw)(e.$t(`statistics.TIME_FRAMES.${s}`)),1)])]))),64))])]),(0,a._)("div",m,[(0,a._)("i",{class:"fa fa-chevron-right","aria-hidden":"true",onClick:r[1]||(r[1]=e=>s("arrowClick",!1))})])]))}}),k=t(3744);const _=(0,k.Z)(v,[["__scopeId","data-v-22d55de2"]]);var S=_,w=t(631);const f={class:"sports-menu"},h=["id","name","checked","onInput"],U={class:"sport-label"};var b=(0,a.aZ)({__name:"StatsSportsMenu",props:{userSports:null,selectedSportIds:{default:()=>[]}},emits:["selectedSportIdsUpdate"],setup(e,{emit:s}){const t=e,{t:n}=(0,o.QT)(),c=(0,a.f3)("sportColors"),{selectedSportIds:i}=(0,r.BK)(t),u=(0,a.Fl)((()=>(0,w.xH)(t.userSports,n)));function d(e){s("selectedSportIdsUpdate",e)}return(e,s)=>{const t=(0,a.up)("SportImage");return(0,a.wg)(),(0,a.iD)("div",f,[((0,a.wg)(!0),(0,a.iD)(a.HY,null,(0,a.Ko)((0,r.SU)(u),(e=>((0,a.wg)(),(0,a.iD)("label",{type:"checkbox",key:e.id,style:(0,l.j5)({color:e.color?e.color:(0,r.SU)(c)[e.label]})},[(0,a._)("input",{type:"checkbox",id:e.id,name:e.label,checked:(0,r.SU)(i).includes(e.id),onInput:s=>d(e.id)},null,40,h),(0,a.Wm)(t,{"sport-label":e.label,color:e.color},null,8,["sport-label","color"]),(0,a._)("span",U,(0,l.zw)(e.translatedLabel),1)],4)))),128))])}}});const I=b;var g=I,T=t(9318);const y={key:0,id:"user-statistics"};var C=(0,a.aZ)({__name:"index",props:{sports:null,user:null},setup(e){const s=e,{t:t}=(0,o.QT)(),{sports:l,user:c}=(0,r.BK)(s),i=(0,r.iH)("month"),u=(0,r.iH)(v(i.value)),d=(0,a.Fl)((()=>(0,w.xH)(s.sports,t))),p=(0,r.iH)(_(s.sports));function m(e){i.value=e,u.value=v(i.value)}function v(e){return(0,T.aZ)(new Date,e,s.user.weekm)}function k(e){u.value=(0,T.FN)(u.value,e,s.user.weekm)}function _(e){return e.map((e=>e.id))}function f(e){p.value.includes(e)?p.value=p.value.filter((s=>s!==e)):p.value.push(e)}return(0,a.YP)((()=>s.sports),(e=>{p.value=_(e)})),(e,s)=>(0,r.SU)(d)?((0,a.wg)(),(0,a.iD)("div",y,[(0,a.Wm)(S,{onTimeFrameUpdate:m,onArrowClick:k}),(0,a.Wm)(n.Z,{sports:(0,r.SU)(l),user:(0,r.SU)(c),chartParams:u.value,"displayed-sport-ids":p.value,fullStats:!0},null,8,["sports","user","chartParams","displayed-sport-ids"]),(0,a.Wm)(g,{"selected-sport-ids":p.value,"user-sports":(0,r.SU)(l),onSelectedSportIdsUpdate:f},null,8,["selected-sport-ids","user-sports"])])):(0,a.kq)("",!0)}});const F=(0,k.Z)(C,[["__scopeId","data-v-d693c7da"]]);var Z=F,x=t(5630),D=t(8602),H=t(9917);const E={id:"statistics",class:"view"},R={key:0,class:"container"};var W=(0,a.aZ)({__name:"StatisticsView",setup(e){const s=(0,H.o)(),t=(0,a.Fl)((()=>s.getters[D.YN.GETTERS.AUTH_USER_PROFILE])),o=(0,a.Fl)((()=>s.getters[D.O8.GETTERS.SPORTS].filter((e=>t.value.sports_list.includes(e.id)))));return(e,s)=>{const n=(0,a.up)("Card");return(0,a.wg)(),(0,a.iD)("div",E,[(0,r.SU)(t).username?((0,a.wg)(),(0,a.iD)("div",R,[(0,a.Wm)(n,null,{title:(0,a.w5)((()=>[(0,a.Uk)((0,l.zw)(e.$t("statistics.STATISTICS")),1)])),content:(0,a.w5)((()=>[(0,a.Wm)(Z,{class:(0,l.C_)({"stats-disabled":0===(0,r.SU)(t).nb_workouts}),user:(0,r.SU)(t),sports:(0,r.SU)(o)},null,8,["class","user","sports"])])),_:1}),0===(0,r.SU)(t).nb_workouts?((0,a.wg)(),(0,a.j4)(x.Z,{key:0})):(0,a.kq)("",!0)])):(0,a.kq)("",!0)])}}});const P=(0,k.Z)(W,[["__scopeId","data-v-2e341d4e"]]);var A=P}}]);
|
||||
//# sourceMappingURL=statistics.145d19e3.js.map
|
||||
//# sourceMappingURL=statistics.c817d0d3.js.map
|
||||
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
@@ -1,6 +1,7 @@
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
from io import BytesIO
|
||||
from typing import Optional
|
||||
from unittest.mock import MagicMock, Mock, patch
|
||||
|
||||
import pytest
|
||||
@@ -233,7 +234,16 @@ class TestUserRegistration(ApiTestCaseMixin):
|
||||
assert data['status'] == 'success'
|
||||
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()
|
||||
username = self.random_string()
|
||||
email = self.random_email()
|
||||
@@ -245,6 +255,7 @@ class TestUserRegistration(ApiTestCaseMixin):
|
||||
username=username,
|
||||
email=email,
|
||||
password=self.random_string(),
|
||||
language=input_language,
|
||||
)
|
||||
),
|
||||
content_type='application/json',
|
||||
@@ -254,9 +265,18 @@ class TestUserRegistration(ApiTestCaseMixin):
|
||||
assert new_user.email == email
|
||||
assert new_user.password is not None
|
||||
assert new_user.is_active is False
|
||||
assert new_user.language == expected_language
|
||||
|
||||
def test_it_calls_account_confirmation_email_if_payload_is_valid(
|
||||
self, app: Flask, account_confirmation_email_mock: Mock
|
||||
@pytest.mark.parametrize(
|
||||
'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:
|
||||
client = app.test_client()
|
||||
email = self.random_email()
|
||||
@@ -271,6 +291,7 @@ class TestUserRegistration(ApiTestCaseMixin):
|
||||
username=username,
|
||||
email=email,
|
||||
password='12345678',
|
||||
language=input_language,
|
||||
)
|
||||
),
|
||||
content_type='application/json',
|
||||
@@ -279,7 +300,7 @@ class TestUserRegistration(ApiTestCaseMixin):
|
||||
|
||||
account_confirmation_email_mock.send.assert_called_once_with(
|
||||
{
|
||||
'language': 'en',
|
||||
'language': expected_language,
|
||||
'email': email,
|
||||
},
|
||||
{
|
||||
@@ -1227,8 +1248,16 @@ class TestUserPreferencesUpdate(ApiTestCaseMixin):
|
||||
|
||||
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(
|
||||
self, app: Flask, user_1: User
|
||||
self,
|
||||
app: Flask,
|
||||
user_1: User,
|
||||
input_language: Optional[str],
|
||||
expected_language: str,
|
||||
) -> None:
|
||||
client, auth_token = self.get_test_client_and_auth_token(
|
||||
app, user_1.email
|
||||
@@ -1241,7 +1270,7 @@ class TestUserPreferencesUpdate(ApiTestCaseMixin):
|
||||
dict(
|
||||
timezone='America/New_York',
|
||||
weekm=True,
|
||||
language='fr',
|
||||
language=input_language,
|
||||
imperial_units=True,
|
||||
)
|
||||
),
|
||||
@@ -1252,6 +1281,7 @@ class TestUserPreferencesUpdate(ApiTestCaseMixin):
|
||||
data = json.loads(response.data.decode())
|
||||
assert data['status'] == 'success'
|
||||
assert data['message'] == 'user preferences updated'
|
||||
assert data['data']['language'] == expected_language
|
||||
assert data['data'] == jsonify_dict(user_1.serialize(user_1))
|
||||
|
||||
|
||||
@@ -2237,6 +2267,7 @@ class TestResendAccountConfirmationEmail(ApiTestCaseMixin):
|
||||
) -> None:
|
||||
client = app.test_client()
|
||||
expected_token = self.random_string()
|
||||
inactive_user.language = 'fr'
|
||||
|
||||
with patch('secrets.token_urlsafe', return_value=expected_token):
|
||||
client.post(
|
||||
@@ -2248,7 +2279,7 @@ class TestResendAccountConfirmationEmail(ApiTestCaseMixin):
|
||||
|
||||
account_confirmation_email_mock.send.assert_called_once_with(
|
||||
{
|
||||
'language': 'en',
|
||||
'language': inactive_user.language,
|
||||
'email': inactive_user.email,
|
||||
},
|
||||
{
|
||||
|
||||
+17
-10
@@ -2,7 +2,7 @@ import datetime
|
||||
import os
|
||||
import re
|
||||
import secrets
|
||||
from typing import Dict, Tuple, Union
|
||||
from typing import Dict, Optional, Tuple, Union
|
||||
|
||||
import jwt
|
||||
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'
|
||||
|
||||
|
||||
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:
|
||||
if current_app.config['CAN_SEND_EMAILS']:
|
||||
ui_url = current_app.config['UI_URL']
|
||||
@@ -57,7 +64,7 @@ def send_account_confirmation_email(user: User) -> None:
|
||||
),
|
||||
}
|
||||
user_data = {
|
||||
'language': 'en',
|
||||
'language': get_language(user.language),
|
||||
'email': user.email,
|
||||
}
|
||||
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 email: user email
|
||||
:<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 400:
|
||||
@@ -138,6 +147,7 @@ def register_user() -> Union[Tuple[Dict, int], HttpResponse]:
|
||||
username = post_data.get('username')
|
||||
email = post_data.get('email')
|
||||
password = post_data.get('password')
|
||||
language = get_language(post_data.get('language'))
|
||||
|
||||
try:
|
||||
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.timezone = 'Europe/Paris'
|
||||
new_user.confirmation_token = secrets.token_urlsafe(30)
|
||||
new_user.language = language
|
||||
db.session.add(new_user)
|
||||
db.session.commit()
|
||||
|
||||
@@ -661,9 +672,7 @@ def update_user_account(auth_user: User) -> Union[Dict, HttpResponse]:
|
||||
if current_app.config['CAN_SEND_EMAILS']:
|
||||
ui_url = current_app.config['UI_URL']
|
||||
user_data = {
|
||||
'language': (
|
||||
'en' if auth_user.language is None else auth_user.language
|
||||
),
|
||||
'language': get_language(auth_user.language),
|
||||
'email': auth_user.email,
|
||||
}
|
||||
data = {
|
||||
@@ -830,7 +839,7 @@ def edit_user_preferences(auth_user: User) -> Union[Dict, HttpResponse]:
|
||||
return InvalidPayloadErrorResponse()
|
||||
|
||||
imperial_units = post_data.get('imperial_units')
|
||||
language = post_data.get('language')
|
||||
language = get_language(post_data.get('language'))
|
||||
timezone = post_data.get('timezone')
|
||||
weekm = post_data.get('weekm')
|
||||
|
||||
@@ -1189,7 +1198,7 @@ def request_password_reset() -> Union[Dict, HttpResponse]:
|
||||
if user:
|
||||
password_reset_token = user.encode_password_reset_token(user.id)
|
||||
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 = {
|
||||
'expiration_delay': get_readable_duration(
|
||||
current_app.config['PASSWORD_TOKEN_EXPIRATION_SECONDS'],
|
||||
@@ -1280,9 +1289,7 @@ def update_password() -> Union[Dict, HttpResponse]:
|
||||
if current_app.config['CAN_SEND_EMAILS']:
|
||||
password_change_email.send(
|
||||
{
|
||||
'language': (
|
||||
'en' if user.language is None else user.language
|
||||
),
|
||||
'language': get_language(user.language),
|
||||
'email': user.email,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -23,6 +23,7 @@ from fittrackee.responses import (
|
||||
from fittrackee.utils import get_readable_duration
|
||||
from fittrackee.workouts.models import Record, Workout, WorkoutSegment
|
||||
|
||||
from .auth import get_language
|
||||
from .decorators import authenticate, authenticate_as_admin
|
||||
from .exceptions import InvalidEmailException, UserNotFoundException
|
||||
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']:
|
||||
user_language = 'en' if user.language is None else user.language
|
||||
user_language = get_language(user.language)
|
||||
ui_url = current_app.config['UI_URL']
|
||||
if reset_password:
|
||||
user_data = {
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
import { ROOT_STORE } from '@/store/constants'
|
||||
import { TAppConfig } from '@/types/application'
|
||||
import { useStore } from '@/use/useStore'
|
||||
import { localeFromLanguage } from '@/utils/locales'
|
||||
|
||||
const store = useStore()
|
||||
|
||||
@@ -47,7 +48,10 @@
|
||||
const hideScrollBar = 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())
|
||||
|
||||
function updateHideScrollBar(isMenuOpen: boolean) {
|
||||
@@ -74,6 +78,18 @@
|
||||
displayScrollButton.value = false
|
||||
}, 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>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -191,6 +191,9 @@
|
||||
const appConfig: ComputedRef<TAppConfig> = computed(
|
||||
() => store.getters[ROOT_STORE.GETTERS.APP_CONFIG]
|
||||
)
|
||||
const language: ComputedRef<string> = computed(
|
||||
() => store.getters[ROOT_STORE.GETTERS.LANGUAGE]
|
||||
)
|
||||
const registration_disabled: ComputedRef<boolean> = computed(
|
||||
() =>
|
||||
props.action === 'register' && !appConfig.value.is_registration_enabled
|
||||
@@ -245,6 +248,7 @@
|
||||
}
|
||||
)
|
||||
default:
|
||||
formData['language'] = language.value
|
||||
store.dispatch(AUTH_USER_STORE.ACTIONS.LOGIN_OR_REGISTER, {
|
||||
actionType,
|
||||
formData,
|
||||
|
||||
@@ -93,6 +93,7 @@ export interface ILoginRegisterFormData {
|
||||
username: string
|
||||
email: string
|
||||
password: string
|
||||
language?: string
|
||||
}
|
||||
|
||||
export interface ILoginOrRegisterData {
|
||||
|
||||
Reference in New Issue
Block a user