API & Client - user can delete his account - fix #17
This commit is contained in:
parent
3d01eadc71
commit
f249b09146
@ -117,6 +117,15 @@ def user_2():
|
|||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def user_2_admin():
|
||||||
|
user = User(username='toto', email='toto@toto.com', password='87654321')
|
||||||
|
user.admin = True
|
||||||
|
db.session.add(user)
|
||||||
|
db.session.commit()
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
def user_3():
|
def user_3():
|
||||||
user = User(username='sam', email='sam@test.com', password='12345678')
|
user = User(username='sam', email='sam@test.com', password='12345678')
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import json
|
import json
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
from fittrackee_api.users.models import User
|
from fittrackee_api.users.models import User
|
||||||
|
|
||||||
@ -409,3 +410,186 @@ def test_it_returns_error_if_user_can_not_change_admin_rights(
|
|||||||
assert response.status_code == 403
|
assert response.status_code == 403
|
||||||
assert 'error' in data['status']
|
assert 'error' in data['status']
|
||||||
assert 'You do not have permissions.' in data['message']
|
assert 'You do not have permissions.' in data['message']
|
||||||
|
|
||||||
|
|
||||||
|
def test_user_can_delete_its_own_account(app, user_1):
|
||||||
|
client = app.test_client()
|
||||||
|
resp_login = client.post(
|
||||||
|
'/api/auth/login',
|
||||||
|
data=json.dumps(dict(email='test@test.com', password='12345678')),
|
||||||
|
content_type='application/json',
|
||||||
|
)
|
||||||
|
response = client.delete(
|
||||||
|
'/api/users/test',
|
||||||
|
headers=dict(
|
||||||
|
Authorization='Bearer '
|
||||||
|
+ json.loads(resp_login.data.decode())['auth_token']
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 204
|
||||||
|
|
||||||
|
|
||||||
|
def test_user_with_activity_can_delete_its_own_account(
|
||||||
|
app, user_1, sport_1_cycling, gpx_file
|
||||||
|
):
|
||||||
|
client = app.test_client()
|
||||||
|
resp_login = client.post(
|
||||||
|
'/api/auth/login',
|
||||||
|
data=json.dumps(dict(email='test@test.com', password='12345678')),
|
||||||
|
content_type='application/json',
|
||||||
|
)
|
||||||
|
client.post(
|
||||||
|
'/api/activities',
|
||||||
|
data=dict(
|
||||||
|
file=(BytesIO(str.encode(gpx_file)), 'example.gpx'),
|
||||||
|
data='{"sport_id": 1}',
|
||||||
|
),
|
||||||
|
headers=dict(
|
||||||
|
content_type='multipart/form-data',
|
||||||
|
Authorization='Bearer '
|
||||||
|
+ json.loads(resp_login.data.decode())['auth_token'],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
response = client.delete(
|
||||||
|
'/api/users/test',
|
||||||
|
headers=dict(
|
||||||
|
Authorization='Bearer '
|
||||||
|
+ json.loads(resp_login.data.decode())['auth_token']
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 204
|
||||||
|
|
||||||
|
|
||||||
|
def test_user_with_picture_can_delete_its_own_account(
|
||||||
|
app, user_1, sport_1_cycling, gpx_file
|
||||||
|
):
|
||||||
|
client = app.test_client()
|
||||||
|
resp_login = client.post(
|
||||||
|
'/api/auth/login',
|
||||||
|
data=json.dumps(dict(email='test@test.com', password='12345678')),
|
||||||
|
content_type='application/json',
|
||||||
|
)
|
||||||
|
client.post(
|
||||||
|
'/api/auth/picture',
|
||||||
|
data=dict(file=(BytesIO(b'avatar'), 'avatar.png')),
|
||||||
|
headers=dict(
|
||||||
|
content_type='multipart/form-data',
|
||||||
|
authorization='Bearer '
|
||||||
|
+ json.loads(resp_login.data.decode())['auth_token'],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
response = client.delete(
|
||||||
|
'/api/users/test',
|
||||||
|
headers=dict(
|
||||||
|
Authorization='Bearer '
|
||||||
|
+ json.loads(resp_login.data.decode())['auth_token']
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 204
|
||||||
|
|
||||||
|
|
||||||
|
def test_user_can_not_delete_another_user_account(app, user_1, user_2):
|
||||||
|
client = app.test_client()
|
||||||
|
resp_login = client.post(
|
||||||
|
'/api/auth/login',
|
||||||
|
data=json.dumps(dict(email='test@test.com', password='12345678')),
|
||||||
|
content_type='application/json',
|
||||||
|
)
|
||||||
|
response = client.delete(
|
||||||
|
'/api/users/toto',
|
||||||
|
headers=dict(
|
||||||
|
Authorization='Bearer '
|
||||||
|
+ json.loads(resp_login.data.decode())['auth_token']
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
data = json.loads(response.data.decode())
|
||||||
|
assert response.status_code == 403
|
||||||
|
assert 'error' in data['status']
|
||||||
|
assert 'You do not have permissions.' in data['message']
|
||||||
|
|
||||||
|
|
||||||
|
def test_it_returns_error_when_deleting_non_existing_user(app, user_1):
|
||||||
|
client = app.test_client()
|
||||||
|
resp_login = client.post(
|
||||||
|
'/api/auth/login',
|
||||||
|
data=json.dumps(dict(email='test@test.com', password='12345678')),
|
||||||
|
content_type='application/json',
|
||||||
|
)
|
||||||
|
response = client.delete(
|
||||||
|
'/api/users/not_existing',
|
||||||
|
headers=dict(
|
||||||
|
Authorization='Bearer '
|
||||||
|
+ json.loads(resp_login.data.decode())['auth_token']
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
data = json.loads(response.data.decode())
|
||||||
|
assert response.status_code == 404
|
||||||
|
assert 'not found' in data['status']
|
||||||
|
assert 'User does not exist.' in data['message']
|
||||||
|
|
||||||
|
|
||||||
|
def test_admin_can_delete_another_user_account(app, user_1_admin, user_2):
|
||||||
|
client = app.test_client()
|
||||||
|
resp_login = client.post(
|
||||||
|
'/api/auth/login',
|
||||||
|
data=json.dumps(dict(email='admin@example.com', password='12345678')),
|
||||||
|
content_type='application/json',
|
||||||
|
)
|
||||||
|
response = client.delete(
|
||||||
|
'/api/users/toto',
|
||||||
|
headers=dict(
|
||||||
|
Authorization='Bearer '
|
||||||
|
+ json.loads(resp_login.data.decode())['auth_token']
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 204
|
||||||
|
|
||||||
|
|
||||||
|
def test_admin_can_delete_its_own_account(app, user_1_admin, user_2_admin):
|
||||||
|
client = app.test_client()
|
||||||
|
resp_login = client.post(
|
||||||
|
'/api/auth/login',
|
||||||
|
data=json.dumps(dict(email='admin@example.com', password='12345678')),
|
||||||
|
content_type='application/json',
|
||||||
|
)
|
||||||
|
response = client.delete(
|
||||||
|
'/api/users/admin',
|
||||||
|
headers=dict(
|
||||||
|
Authorization='Bearer '
|
||||||
|
+ json.loads(resp_login.data.decode())['auth_token']
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 204
|
||||||
|
|
||||||
|
|
||||||
|
def test_admin_can_not_delete_its_own_account_if_no_other_admin(
|
||||||
|
app, user_1_admin, user_2
|
||||||
|
):
|
||||||
|
client = app.test_client()
|
||||||
|
resp_login = client.post(
|
||||||
|
'/api/auth/login',
|
||||||
|
data=json.dumps(dict(email='admin@example.com', password='12345678')),
|
||||||
|
content_type='application/json',
|
||||||
|
)
|
||||||
|
response = client.delete(
|
||||||
|
'/api/users/admin',
|
||||||
|
headers=dict(
|
||||||
|
Authorization='Bearer '
|
||||||
|
+ json.loads(resp_login.data.decode())['auth_token']
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
data = json.loads(response.data.decode())
|
||||||
|
assert response.status_code == 403
|
||||||
|
assert 'error' in data['status']
|
||||||
|
assert (
|
||||||
|
'You can not delete your account, no other user has admin rights.'
|
||||||
|
in data['message']
|
||||||
|
)
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
|
||||||
from fittrackee_api import appLog, db
|
from fittrackee_api import appLog, db
|
||||||
from flask import Blueprint, jsonify, request, send_file
|
from flask import Blueprint, jsonify, request, send_file
|
||||||
from sqlalchemy import exc
|
from sqlalchemy import exc
|
||||||
|
|
||||||
from ..activities.utils_files import get_absolute_file_path
|
from ..activities.utils_files import get_absolute_file_path
|
||||||
from .models import User
|
from .models import Activity, User
|
||||||
from .utils import authenticate, authenticate_as_admin
|
from .utils import authenticate, authenticate_as_admin
|
||||||
|
|
||||||
users_blueprint = Blueprint('users', __name__)
|
users_blueprint = Blueprint('users', __name__)
|
||||||
@ -270,7 +273,7 @@ def update_user(auth_user_id, user_name):
|
|||||||
}
|
}
|
||||||
|
|
||||||
:param integer auth_user_id: authenticate user id (from JSON Web Token)
|
:param integer auth_user_id: authenticate user id (from JSON Web Token)
|
||||||
:param integer user_name: user name
|
:param string user_name: user name
|
||||||
|
|
||||||
:<json boolean admin: does the user have administrator rights
|
:<json boolean admin: does the user have administrator rights
|
||||||
|
|
||||||
@ -316,6 +319,111 @@ def update_user(auth_user_id, user_name):
|
|||||||
return jsonify(response_object), code
|
return jsonify(response_object), code
|
||||||
|
|
||||||
|
|
||||||
|
@users_blueprint.route('/users/<user_name>', methods=['DELETE'])
|
||||||
|
@authenticate
|
||||||
|
def delete_activity(auth_user_id, user_name):
|
||||||
|
"""
|
||||||
|
Delete a user account
|
||||||
|
- a user can only delete his own account
|
||||||
|
- an admin can delete all accounts except his account if he's the only
|
||||||
|
one admin
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
DELETE /api/users/john_doe HTTP/1.1
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 204 NO CONTENT
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
:param integer auth_user_id: authenticate user id (from JSON Web Token)
|
||||||
|
:param string user_name: user name
|
||||||
|
|
||||||
|
:reqheader Authorization: OAuth 2.0 Bearer Token
|
||||||
|
|
||||||
|
:statuscode 204: user account deleted
|
||||||
|
:statuscode 401:
|
||||||
|
- Provide a valid auth token.
|
||||||
|
- Signature expired. Please log in again.
|
||||||
|
- Invalid token. Please log in again.
|
||||||
|
:statuscode 403:
|
||||||
|
- You do not have permissions.
|
||||||
|
- You can not delete your account, no other user has admin rights.
|
||||||
|
:statuscode 404:
|
||||||
|
- User does not exist.
|
||||||
|
:statuscode 500: Error. Please try again or contact the administrator.
|
||||||
|
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
auth_user = User.query.filter_by(id=auth_user_id).first()
|
||||||
|
user = User.query.filter_by(username=user_name).first()
|
||||||
|
if user:
|
||||||
|
if user.id != auth_user_id and not auth_user.admin:
|
||||||
|
response_object = {
|
||||||
|
'status': 'error',
|
||||||
|
'message': 'You do not have permissions.',
|
||||||
|
}
|
||||||
|
return response_object, 403
|
||||||
|
if (
|
||||||
|
user.admin is True
|
||||||
|
and User.query.filter_by(admin=True).count() == 1
|
||||||
|
):
|
||||||
|
response_object = {
|
||||||
|
'status': 'error',
|
||||||
|
'message': (
|
||||||
|
'You can not delete your account, '
|
||||||
|
'no other user has admin rights.'
|
||||||
|
),
|
||||||
|
}
|
||||||
|
return response_object, 403
|
||||||
|
for activity in Activity.query.filter_by(user_id=user.id).all():
|
||||||
|
db.session.delete(activity)
|
||||||
|
db.session.flush()
|
||||||
|
user_picture = user.picture
|
||||||
|
db.session.delete(user)
|
||||||
|
db.session.commit()
|
||||||
|
if user_picture:
|
||||||
|
picture_path = get_absolute_file_path(user.picture)
|
||||||
|
if os.path.isfile(picture_path):
|
||||||
|
os.remove(picture_path)
|
||||||
|
shutil.rmtree(
|
||||||
|
get_absolute_file_path(f'activities/{user.id}'),
|
||||||
|
ignore_errors=True,
|
||||||
|
)
|
||||||
|
shutil.rmtree(
|
||||||
|
get_absolute_file_path(f'pictures/{user.id}'),
|
||||||
|
ignore_errors=True,
|
||||||
|
)
|
||||||
|
response_object = {'status': 'no content'}
|
||||||
|
code = 204
|
||||||
|
else:
|
||||||
|
response_object = {
|
||||||
|
'status': 'not found',
|
||||||
|
'message': 'User does not exist.',
|
||||||
|
}
|
||||||
|
code = 404
|
||||||
|
except (
|
||||||
|
exc.IntegrityError,
|
||||||
|
exc.OperationalError,
|
||||||
|
ValueError,
|
||||||
|
OSError,
|
||||||
|
) as e:
|
||||||
|
db.session.rollback()
|
||||||
|
appLog.error(e)
|
||||||
|
response_object = {
|
||||||
|
'status': 'error',
|
||||||
|
'message': 'Error. Please try again or contact the administrator.',
|
||||||
|
}
|
||||||
|
code = 500
|
||||||
|
return jsonify(response_object), code
|
||||||
|
|
||||||
|
|
||||||
@users_blueprint.route('/ping', methods=['GET'])
|
@users_blueprint.route('/ping', methods=['GET'])
|
||||||
def ping_pong():
|
def ping_pong():
|
||||||
""" health check endpoint
|
""" health check endpoint
|
||||||
|
@ -2,7 +2,7 @@ import FitTrackeeGenericApi from '../fitTrackeeApi'
|
|||||||
import FitTrackeeApi from '../fitTrackeeApi/auth'
|
import FitTrackeeApi from '../fitTrackeeApi/auth'
|
||||||
import { history } from '../index'
|
import { history } from '../index'
|
||||||
import { generateIds } from '../utils'
|
import { generateIds } from '../utils'
|
||||||
import { getOrUpdateData, updateLanguage } from './index'
|
import { getOrUpdateData, setError, updateLanguage } from './index'
|
||||||
|
|
||||||
const AuthError = message => ({ type: 'AUTH_ERROR', message })
|
const AuthError = message => ({ type: 'AUTH_ERROR', message })
|
||||||
|
|
||||||
@ -133,3 +133,15 @@ export const deletePicture = () => dispatch =>
|
|||||||
.catch(error => {
|
.catch(error => {
|
||||||
throw error
|
throw error
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const deleteUser = username => dispatch =>
|
||||||
|
FitTrackeeGenericApi.deleteData('users', username)
|
||||||
|
.then(ret => {
|
||||||
|
if (ret.status === 204) {
|
||||||
|
dispatch(logout())
|
||||||
|
history.push('/')
|
||||||
|
} else {
|
||||||
|
ret.json().then(r => dispatch(setError(`${r.message}`)))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => dispatch(setError(`user|${error}`)))
|
||||||
|
@ -91,7 +91,7 @@ class ActivityDisplay extends React.Component {
|
|||||||
<div className="container">
|
<div className="container">
|
||||||
{displayModal && (
|
{displayModal && (
|
||||||
<CustomModal
|
<CustomModal
|
||||||
title={t('activities:Confirmation')}
|
title={t('common:Confirmation')}
|
||||||
text={t(
|
text={t(
|
||||||
'activities:Are you sure you want to delete this activity?'
|
'activities:Are you sure you want to delete this activity?'
|
||||||
)}
|
)}
|
||||||
|
@ -6,15 +6,17 @@ import { connect } from 'react-redux'
|
|||||||
import TimezonePicker from 'react-timezone'
|
import TimezonePicker from 'react-timezone'
|
||||||
|
|
||||||
import Message from '../Common/Message'
|
import Message from '../Common/Message'
|
||||||
import { handleProfileFormSubmit } from '../../actions/user'
|
import { deleteUser, handleProfileFormSubmit } from '../../actions/user'
|
||||||
import { history } from '../../index'
|
import { history } from '../../index'
|
||||||
import { languages } from '../NavBar/LanguageDropdown'
|
import { languages } from '../NavBar/LanguageDropdown'
|
||||||
|
import CustomModal from '../Common/CustomModal'
|
||||||
|
|
||||||
class ProfileEdit extends React.Component {
|
class ProfileEdit extends React.Component {
|
||||||
constructor(props, context) {
|
constructor(props, context) {
|
||||||
super(props, context)
|
super(props, context)
|
||||||
this.state = {
|
this.state = {
|
||||||
formData: {},
|
formData: {},
|
||||||
|
displayModal: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,9 +53,22 @@ class ProfileEdit extends React.Component {
|
|||||||
this.setState(formData)
|
this.setState(formData)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
displayModal(value) {
|
||||||
|
this.setState(prevState => ({
|
||||||
|
...prevState,
|
||||||
|
displayModal: value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { onHandleProfileFormSubmit, message, t, user } = this.props
|
const {
|
||||||
const { formData } = this.state
|
message,
|
||||||
|
onDeleteUser,
|
||||||
|
onHandleProfileFormSubmit,
|
||||||
|
t,
|
||||||
|
user,
|
||||||
|
} = this.props
|
||||||
|
const { displayModal, formData } = this.state
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
@ -62,6 +77,20 @@ class ProfileEdit extends React.Component {
|
|||||||
<Message message={message} t={t} />
|
<Message message={message} t={t} />
|
||||||
{formData.isAuthenticated && (
|
{formData.isAuthenticated && (
|
||||||
<div className="container">
|
<div className="container">
|
||||||
|
{displayModal && (
|
||||||
|
<CustomModal
|
||||||
|
title={t('common:Confirmation')}
|
||||||
|
text={t(
|
||||||
|
'user:Are you sure you want to delete your account? ' +
|
||||||
|
'All data will be deleted, this cannot be undone.'
|
||||||
|
)}
|
||||||
|
confirm={() => {
|
||||||
|
onDeleteUser(user.username)
|
||||||
|
this.displayModal(false)
|
||||||
|
}}
|
||||||
|
close={() => this.displayModal(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<h1 className="page-title">{t('user:Profile Edition')}</h1>
|
<h1 className="page-title">{t('user:Profile Edition')}</h1>
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col-md-2" />
|
<div className="col-md-2" />
|
||||||
@ -242,6 +271,11 @@ class ProfileEdit extends React.Component {
|
|||||||
className="btn btn-primary btn-lg btn-block"
|
className="btn btn-primary btn-lg btn-block"
|
||||||
value={t('common:Submit')}
|
value={t('common:Submit')}
|
||||||
/>
|
/>
|
||||||
|
<input
|
||||||
|
className="btn btn-danger btn-lg btn-block"
|
||||||
|
onClick={() => this.displayModal(true)}
|
||||||
|
defaultValue={t('user:Delete my account')}
|
||||||
|
/>
|
||||||
<input
|
<input
|
||||||
type="submit"
|
type="submit"
|
||||||
className="btn btn-secondary btn-lg btn-block"
|
className="btn btn-secondary btn-lg btn-block"
|
||||||
@ -271,6 +305,9 @@ export default withTranslation()(
|
|||||||
user: state.user,
|
user: state.user,
|
||||||
}),
|
}),
|
||||||
dispatch => ({
|
dispatch => ({
|
||||||
|
onDeleteUser: username => {
|
||||||
|
dispatch(deleteUser(username))
|
||||||
|
},
|
||||||
onHandleProfileFormSubmit: formData => {
|
onHandleProfileFormSubmit: formData => {
|
||||||
dispatch(handleProfileFormSubmit(formData))
|
dispatch(handleProfileFormSubmit(formData))
|
||||||
},
|
},
|
||||||
|
@ -8,7 +8,6 @@
|
|||||||
"Ascent": "Ascent",
|
"Ascent": "Ascent",
|
||||||
"Average speed": "Average speed",
|
"Average speed": "Average speed",
|
||||||
"Chart": "Chart",
|
"Chart": "Chart",
|
||||||
"Confirmation": "Confirmation",
|
|
||||||
"data from gpx, without any cleaning": "data from gpx, without any cleaning",
|
"data from gpx, without any cleaning": "data from gpx, without any cleaning",
|
||||||
"Date": "Date",
|
"Date": "Date",
|
||||||
"Delete activity": "Delete activity",
|
"Delete activity": "Delete activity",
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
"Add workout": "Add workout",
|
"Add workout": "Add workout",
|
||||||
"Back": "Back",
|
"Back": "Back",
|
||||||
"Cancel": "Cancel",
|
"Cancel": "Cancel",
|
||||||
|
"Confirmation": "Confirmation",
|
||||||
"Dashboard": "Dashboard",
|
"Dashboard": "Dashboard",
|
||||||
"Edit": "Edit",
|
"Edit": "Edit",
|
||||||
"day": "day",
|
"day": "day",
|
||||||
|
@ -30,5 +30,6 @@
|
|||||||
"statistics": "statistiques",
|
"statistics": "statistiques",
|
||||||
"User does not exist.": "User does not exist.",
|
"User does not exist.": "User does not exist.",
|
||||||
"Valid email must be provided.\n": "Valid email must be provided.",
|
"Valid email must be provided.\n": "Valid email must be provided.",
|
||||||
|
"You can not delete your account, no other user has admin rights.": "You can not delete your account, no other user has admin rights.",
|
||||||
"You do not have permissions.": "You do not have permissions."
|
"You do not have permissions.": "You do not have permissions."
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
{
|
{
|
||||||
"Admin": "Admin",
|
"Admin": "Admin",
|
||||||
|
"Are you sure you want to delete your account? All data will be deleted, this cannot be undone.": "Are you sure you want to delete your account? All data will be deleted, this cannot be undone.",
|
||||||
"Bio": "Bio",
|
"Bio": "Bio",
|
||||||
"Birth Date": "Birth Date",
|
"Birth Date": "Birth Date",
|
||||||
|
"Delete my account": "Delete my account",
|
||||||
"Delete picture": "Delete picture",
|
"Delete picture": "Delete picture",
|
||||||
"Edit Profile": "Edit Profile",
|
"Edit Profile": "Edit Profile",
|
||||||
"Email": "Email",
|
"Email": "Email",
|
||||||
|
@ -8,7 +8,6 @@
|
|||||||
"Ascent": "Dénivelé positif",
|
"Ascent": "Dénivelé positif",
|
||||||
"Average speed": "Vitesse moyenne",
|
"Average speed": "Vitesse moyenne",
|
||||||
"Chart": "Analyse",
|
"Chart": "Analyse",
|
||||||
"Confirmation": "Confirmation",
|
|
||||||
"data from gpx, without any cleaning": "données issues du fichier gpx, sans correction",
|
"data from gpx, without any cleaning": "données issues du fichier gpx, sans correction",
|
||||||
"Date": "Date",
|
"Date": "Date",
|
||||||
"Delete activity": "Supprimer l'activité",
|
"Delete activity": "Supprimer l'activité",
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
"Add workout": "Ajouter une activité",
|
"Add workout": "Ajouter une activité",
|
||||||
"Back": "Revenir à la page précédente",
|
"Back": "Revenir à la page précédente",
|
||||||
"Cancel": "Annuler",
|
"Cancel": "Annuler",
|
||||||
|
"Confirmation": "Confirmation",
|
||||||
"Dashboard": "Tableau de Bord",
|
"Dashboard": "Tableau de Bord",
|
||||||
"Edit": "Modifier",
|
"Edit": "Modifier",
|
||||||
"day": "jour",
|
"day": "jour",
|
||||||
|
@ -30,5 +30,6 @@
|
|||||||
"statistics": "statistics",
|
"statistics": "statistics",
|
||||||
"User does not exist.": "L'utilisateur n'existe pas.",
|
"User does not exist.": "L'utilisateur n'existe pas.",
|
||||||
"Valid email must be provided.\n": "L'email fourni n'est pas valide.",
|
"Valid email must be provided.\n": "L'email fourni n'est pas valide.",
|
||||||
|
"You can not delete your account, no other user has admin rights.": "Vous ne pouvez pas supprimer votre compte, aucun autre utilisateur n'a des droits d'administration.",
|
||||||
"You do not have permissions.": "Vous n'avez pas les permissions nécessaires."
|
"You do not have permissions.": "Vous n'avez pas les permissions nécessaires."
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
{
|
{
|
||||||
"Admin": "Admin",
|
"Admin": "Admin",
|
||||||
|
"Are you sure you want to delete your account? All data will be deleted, this cannot be undone.": "Etes-vous sûr de vouloir supprimer votre compte ? Toutes les données seront définitivement effacés.",
|
||||||
"Bio": "Bio",
|
"Bio": "Bio",
|
||||||
"Birth Date": "Date de naissance",
|
"Birth Date": "Date de naissance",
|
||||||
|
"Delete my account": "Supprimer mon compte",
|
||||||
"Delete picture": "Supprimer l'image",
|
"Delete picture": "Supprimer l'image",
|
||||||
"Edit Profile": "Editer le profil",
|
"Edit Profile": "Editer le profil",
|
||||||
"Email": "Email",
|
"Email": "Email",
|
||||||
|
Loading…
Reference in New Issue
Block a user