API & Client - display uploaded files size in admin + minor changes

This commit is contained in:
Sam 2020-05-01 20:18:19 +02:00
parent abca323936
commit 5aa1d77553
12 changed files with 119 additions and 39 deletions

View File

@ -7,7 +7,7 @@ from sqlalchemy import func
from ..users.models import User from ..users.models import User
from ..users.utils import authenticate, authenticate_as_admin from ..users.utils import authenticate, authenticate_as_admin
from .models import Activity, Sport from .models import Activity, Sport
from .utils import get_datetime_with_tz from .utils import get_datetime_with_tz, get_upload_dir_size
from .utils_format import convert_timedelta_to_integer from .utils_format import convert_timedelta_to_integer
stats_blueprint = Blueprint('stats', __name__) stats_blueprint = Blueprint('stats', __name__)
@ -376,6 +376,7 @@ def get_application_stats(auth_user_id):
'activities': nb_activities, 'activities': nb_activities,
'sports': nb_sports, 'sports': nb_sports,
'users': nb_users, 'users': nb_users,
'uploads_dir_size': get_upload_dir_size(),
}, },
} }
return jsonify(response_object), 200 return jsonify(response_object), 200

View File

@ -338,3 +338,13 @@ def process_files(auth_user_id, activity_data, activity_file, folders):
return [process_one_gpx_file(common_params, filename)] return [process_one_gpx_file(common_params, filename)]
else: else:
return process_zip_archive(common_params, folders['extract_dir']) return process_zip_archive(common_params, folders['extract_dir'])
def get_upload_dir_size():
upload_path = get_absolute_file_path('')
total_size = 0
for dir_path, _, filenames in os.walk(upload_path):
for f in filenames:
fp = os.path.join(dir_path, f)
total_size += os.path.getsize(fp)
return total_size

View File

@ -968,7 +968,10 @@ def test_get_app_stats_without_activities(app, user_1_admin):
assert response.status_code == 200 assert response.status_code == 200
assert 'success' in data['status'] assert 'success' in data['status']
assert data['data'] == {'activities': 0, 'sports': 0, 'users': 1} assert data['data']['activities'] == 0
assert data['data']['sports'] == 0
assert data['data']['users'] == 1
assert 'uploads_dir_size' in data['data']
def test_get_app_stats_with_activities( def test_get_app_stats_with_activities(
@ -999,7 +1002,10 @@ def test_get_app_stats_with_activities(
assert response.status_code == 200 assert response.status_code == 200
assert 'success' in data['status'] assert 'success' in data['status']
assert data['data'] == {'activities': 3, 'sports': 2, 'users': 3} assert data['data']['activities'] == 3
assert data['data']['sports'] == 2
assert data['data']['users'] == 3
assert 'uploads_dir_size' in data['data']
def test_get_app_stats_no_admin( def test_get_app_stats_no_admin(

View File

@ -3,9 +3,6 @@ import { Link } from 'react-router-dom'
import { Helmet } from 'react-helmet' import { Helmet } from 'react-helmet'
import AdminStats from './AdminStats' import AdminStats from './AdminStats'
import { capitalize } from '../../utils'
const menuItems = ['application', 'sports', 'users']
export default function AdminDashboard(props) { export default function AdminDashboard(props) {
const { t } = props const { t } = props
@ -18,19 +15,51 @@ export default function AdminDashboard(props) {
<div className="card-header">{t('administration:Administration')}</div> <div className="card-header">{t('administration:Administration')}</div>
<div className="card-body"> <div className="card-body">
<AdminStats /> <AdminStats />
<ul className="admin-items"> <br />
{menuItems.map(item => ( <dl className="admin-items">
<li key={item}> <dt>
<Link <Link
to={{ to={{
pathname: `/admin/${item}`, pathname: '/admin/application',
}} }}
> >
{t(`administration:${capitalize(item)}`)} {t('administration:Application')}
</Link> </Link>
</li> </dt>
))} <dd>
</ul> {t(
'administration:Update application configuration ' +
'(maximum number of registered users, maximum files size).'
)}{' '}
</dd>
<br />
<dt>
<Link
to={{
pathname: '/admin/sports',
}}
>
{t('administration:Sports')}
</Link>
</dt>
<dd>{t('administration:Enable/disable sports.')}</dd>
<br />
<dt>
<Link
to={{
pathname: '/admin/users',
}}
>
{t('administration:Users')}
</Link>
</dt>
<dd>
{t(
'administration:Add/remove admin rigths, ' +
'delete user account.'
)}
</dd>
</dl>
</div> </div>
</div> </div>
</> </>

View File

@ -3,6 +3,7 @@ import { withTranslation } from 'react-i18next'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { getAppData } from '../../actions/application' import { getAppData } from '../../actions/application'
import { getFileSize } from '../../utils'
class AdminStats extends React.Component { class AdminStats extends React.Component {
componentDidMount() { componentDidMount() {
@ -11,9 +12,10 @@ class AdminStats extends React.Component {
render() { render() {
const { appStats, t } = this.props const { appStats, t } = this.props
const uploadDirSize = getFileSize(appStats.uploads_dir_size, false)
return ( return (
<div className="row"> <div className="row">
<div className="col-md-4"> <div className="col-lg-3 col-md-6 col-sm-6">
<div className="card activity-card"> <div className="card activity-card">
<div className="card-body row"> <div className="card-body row">
<div className="col-3"> <div className="col-3">
@ -32,7 +34,7 @@ class AdminStats extends React.Component {
</div> </div>
</div> </div>
</div> </div>
<div className="col-md-4"> <div className="col-lg-3 col-md-6 col-sm-6">
<div className="card activity-card"> <div className="card activity-card">
<div className="card-body row"> <div className="card-body row">
<div className="col-3"> <div className="col-3">
@ -49,7 +51,7 @@ class AdminStats extends React.Component {
</div> </div>
</div> </div>
</div> </div>
<div className="col-md-4"> <div className="col-lg-3 col-md-6 col-sm-6">
<div className="card activity-card"> <div className="card activity-card">
<div className="card-body row"> <div className="card-body row">
<div className="col-3"> <div className="col-3">
@ -68,6 +70,21 @@ class AdminStats extends React.Component {
</div> </div>
</div> </div>
</div> </div>
<div className="col-lg-3 col-md-6 col-sm-6">
<div className="card activity-card">
<div className="card-body row">
<div className="col-3">
<i className="fa fa-folder-open fa-3x fa-color" />
</div>
<div className="col-9 text-right">
<div className="huge">{uploadDirSize.size}</div>
<div>
{uploadDirSize.suffix} ({t('administration:uploads')})
</div>
</div>
</div>
</div>
</div>
</div> </div>
) )
} }

View File

@ -96,7 +96,7 @@ class AdminSports extends React.Component {
type="submit" type="submit"
className="btn btn-secondary" className="btn btn-secondary"
onClick={() => history.push('/admin/')} onClick={() => history.push('/admin/')}
value={t('administration:Back')} value={t('common:Back')}
/> />
</div> </div>
</div> </div>

View File

@ -89,8 +89,8 @@ class AdminUsers extends React.Component {
disabled={user.username === authUser.username} disabled={user.username === authUser.username}
value={ value={
user.admin user.admin
? t('administration:Remove') ? t('administration:Remove admin rights')
: t('administration:Add') : t('administration:Add admin rights')
} }
onClick={() => onClick={() =>
updateUser(user.username, !user.admin) updateUser(user.username, !user.admin)
@ -105,7 +105,7 @@ class AdminUsers extends React.Component {
type="submit" type="submit"
className="btn btn-secondary" className="btn btn-secondary"
onClick={() => history.push('/admin/')} onClick={() => history.push('/admin/')}
value={t('administration:Back')} value={t('common:Back')}
/> />
</div> </div>
</div> </div>

View File

@ -13,7 +13,7 @@ export default function UserStatistics(props) {
duration = `${duration.split(':')[0]}h ${duration.split(':')[1]}min` duration = `${duration.split(':')[0]}h ${duration.split(':')[1]}min`
return ( return (
<div className="row"> <div className="row">
<div className="col-md-3"> <div className="col-lg-3 col-md-6 col-sm-6">
<div className="card activity-card"> <div className="card activity-card">
<div className="card-body row"> <div className="card-body row">
<div className="col-3"> <div className="col-3">
@ -30,7 +30,7 @@ export default function UserStatistics(props) {
</div> </div>
</div> </div>
</div> </div>
<div className="col-md-3"> <div className="col-lg-3 col-md-6 col-sm-6">
<div className="card activity-card"> <div className="card activity-card">
<div className="card-body row"> <div className="card-body row">
<div className="col-3"> <div className="col-3">
@ -45,7 +45,7 @@ export default function UserStatistics(props) {
</div> </div>
</div> </div>
</div> </div>
<div className="col-md-3"> <div className="col-lg-3 col-md-6 col-sm-6">
<div className="card activity-card"> <div className="card activity-card">
<div className="card-body row"> <div className="card-body row">
<div className="col-3"> <div className="col-3">
@ -58,7 +58,7 @@ export default function UserStatistics(props) {
</div> </div>
</div> </div>
</div> </div>
<div className="col-md-3"> <div className="col-lg-3 col-md-6 col-sm-6">
<div className="card activity-card"> <div className="card activity-card">
<div className="card-body row"> <div className="card-body row">
<div className="col-3"> <div className="col-3">

View File

@ -8,6 +8,7 @@ import { Link } from 'react-router-dom'
import Message from '../Common/Message' import Message from '../Common/Message'
import { deletePicture, uploadPicture } from '../../actions/user' import { deletePicture, uploadPicture } from '../../actions/user'
import { apiUrl, getFileSize } from '../../utils' import { apiUrl, getFileSize } from '../../utils'
import { history } from '../../index'
function ProfileDetail({ function ProfileDetail({
appConfig, appConfig,
@ -135,6 +136,12 @@ function ProfileDetail({
)}{' '} )}{' '}
</div> </div>
</div> </div>
<input
type="submit"
className="btn btn-secondary"
onClick={() => history.go(-1)}
value={t('common:Back')}
/>
</div> </div>
</div> </div>
</div> </div>

View File

@ -2,13 +2,15 @@
"Actions": "Actions", "Actions": "Actions",
"Active": "Active", "Active": "Active",
"activities exist": "activities exist", "activities exist": "activities exist",
"Add": "Add", "Add admin rights": "Add admin rights",
"Add/remove admin rigths, delete user account.": "Add/remove admin rigths, delete user account.",
"Administration": "Administration", "Administration": "Administration",
"Application": "Application", "Application": "Application",
"Application configuration": "Application configuration", "Application configuration": "Application configuration",
"Back": "Back", "Back": "Back",
"Disable": "Disable", "Disable": "Disable",
"Enable": "Enable", "Enable": "Enable",
"Enable/disable sports.": "Enable/disable sports.",
"FitTrackee administration": "FitTrackee administration", "FitTrackee administration": "FitTrackee administration",
"id": "id", "id": "id",
"Image": "Image", "Image": "Image",
@ -19,8 +21,10 @@
"Max. size of uploaded files (in Mb)": "Max. size of uploaded files (in Mb)", "Max. size of uploaded files (in Mb)": "Max. size of uploaded files (in Mb)",
"Max. size of zip archive": "Max. size of zip archive", "Max. size of zip archive": "Max. size of zip archive",
"Max. size of zip archive (in Mb)": "Max. size of zip archive (in Mb)", "Max. size of zip archive (in Mb)": "Max. size of zip archive (in Mb)",
"Remove": "Remove", "Remove admin rights": "Remove admin rights",
"Sports": "Sports", "Sports": "Sports",
"Update application configuration (maximum number of registered users, maximum files size).": "Update application configuration (maximum number of registered users, maximum files size).",
"uploads": "uploads",
"user": "user", "user": "user",
"Users": "Users", "Users": "Users",
"users": "users" "users": "users"

View File

@ -1,7 +1,8 @@
{ {
"Actions": "Actions", "Actions": "Actions",
"Active": "Active", "Active": "Active",
"Add": "Ajouter", "Add admin rights": "Ajouter des droits d'admin",
"Add/remove admin rigths, delete user account.": "Ajouter/retirer des droits d'adminsitration, supprimer des comptes utilisateurs.",
"Administration": "Administration", "Administration": "Administration",
"activities exist": "des activités existent", "activities exist": "des activités existent",
"Application": "Application", "Application": "Application",
@ -9,6 +10,7 @@
"Back": "Retour", "Back": "Retour",
"Disable": "désactiver", "Disable": "désactiver",
"Enable": "activer", "Enable": "activer",
"Enable/disable sports.": "Activer/désactiver des sports.",
"FitTrackee administration": "Administration de FitTrackee", "FitTrackee administration": "Administration de FitTrackee",
"id": "id", "id": "id",
"Image": "Image", "Image": "Image",
@ -19,8 +21,10 @@
"Max. size of uploaded files (in Mb)": "Taille max. des fichiers (en Mo)", "Max. size of uploaded files (in Mb)": "Taille max. des fichiers (en Mo)",
"Max. size of zip archive": "Taille max. des archives zip", "Max. size of zip archive": "Taille max. des archives zip",
"Max. size of zip archive (in Mb)": "Taille max. des archives zip (en Mo)", "Max. size of zip archive (in Mb)": "Taille max. des archives zip (en Mo)",
"Remove": "Retirer", "Remove admin rights": "Retirer des droits d'admin",
"Sports": "Sports", "Sports": "Sports",
"Update application configuration (maximum number of registered users, maximum files size).": "Configurer l'application (nombre maximum d'utilisateurs inscrits, taille maximale des fichers).",
"uploads": "fichiers",
"user": "user", "user": "user",
"Users": "Utilisateurs", "Users": "Utilisateurs",
"users": "utilisateurs" "users": "utilisateurs"

View File

@ -2,12 +2,14 @@ import { format, parse } from 'date-fns'
import { DateTime } from 'luxon' import { DateTime } from 'luxon'
const suffixes = ['bytes', 'KB', 'MB', 'GB', 'TB'] const suffixes = ['bytes', 'KB', 'MB', 'GB', 'TB']
export const getFileSize = fileSize => { export const getFileSize = (fileSize, asText = true) => {
const i = Math.floor(Math.log(fileSize) / Math.log(1024)) const i = Math.floor(Math.log(fileSize) / Math.log(1024))
return ( if (!fileSize) {
(!fileSize && '0 bytes') || return asText ? '0 bytes' : { size: 0, suffix: 'bytes' }
`${(fileSize / Math.pow(1024, i)).toFixed(1)}${suffixes[i]}` }
) const size = (fileSize / Math.pow(1024, i)).toFixed(1)
const suffix = suffixes[i]
return asText ? `${size}${suffix}` : { size, suffix }
} }
export const getFileSizeInMB = fileSize => { export const getFileSizeInMB = fileSize => {