README.me update + minor changes
This commit is contained in:
parent
7d410956b5
commit
1c206de590
17
README.md
17
README.md
@ -1,4 +1,5 @@
|
|||||||
# mpwo
|
# mpwo
|
||||||
|
**A simple self-hosted workout/activity tracker.**
|
||||||
|
|
||||||
[![Python Version](https://img.shields.io/badge/python-3.6-brightgreen.svg)](https://python.org)
|
[![Python Version](https://img.shields.io/badge/python-3.6-brightgreen.svg)](https://python.org)
|
||||||
[![Flask Version](https://img.shields.io/badge/flask-1.0-brightgreen.svg)](http://flask.pocoo.org/)
|
[![Flask Version](https://img.shields.io/badge/flask-1.0-brightgreen.svg)](http://flask.pocoo.org/)
|
||||||
@ -7,8 +8,22 @@
|
|||||||
[![Codacy Badge](https://api.codacy.com/project/badge/Coverage/45d64b31e37e4890a239b8298e66a011)](https://www.codacy.com/app/SamR1/mpwo?utm_source=github.com&utm_medium=referral&utm_content=SamR1/mpwo&utm_campaign=Badge_Coverage)<sup><sup>1</sup></sup>
|
[![Codacy Badge](https://api.codacy.com/project/badge/Coverage/45d64b31e37e4890a239b8298e66a011)](https://www.codacy.com/app/SamR1/mpwo?utm_source=github.com&utm_medium=referral&utm_content=SamR1/mpwo&utm_campaign=Badge_Coverage)<sup><sup>1</sup></sup>
|
||||||
[![Build Status](https://travis-ci.org/SamR1/mpwo.svg?branch=master)](https://travis-ci.org/SamR1/mpwo)
|
[![Build Status](https://travis-ci.org/SamR1/mpwo.svg?branch=master)](https://travis-ci.org/SamR1/mpwo)
|
||||||
|
|
||||||
Self hosted workout/activity tracker written with Flask and React (_work in progress_).
|
---
|
||||||
|
|
||||||
|
This application allows you to track your sports activity from gpx files and keep your data on your own server.
|
||||||
|
Several mobile apps can store workout data locally and export it into a gpx file.
|
||||||
|
Examples (for Android):
|
||||||
|
* [Runner Up](https://github.com/jonasoreland/runnerup) (GPL v3)
|
||||||
|
* [ForRunners](https://github.com/brvier/ForRunners) (GPL v3)
|
||||||
|
* [AlpineQuest](https://www.alpinequest.net/) (Proprietary)
|
||||||
|
...
|
||||||
|
|
||||||
|
**Still under development (not ready for production).**
|
||||||
|
(see [Wiki](https://github.com/SamR1/mpwo/wiki) for more informations)
|
||||||
|
|
||||||
|
![mpwo screenshot](docs/images/mpwo_screenshot-01.png)
|
||||||
|
|
||||||
|
![mpwo screenshot](docs/images/mpwo_screenshot-02.png)
|
||||||
---
|
---
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
|
BIN
docs/images/mpwo_screenshot-01.png
Normal file
BIN
docs/images/mpwo_screenshot-01.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 406 KiB |
BIN
docs/images/mpwo_screenshot-02.png
Normal file
BIN
docs/images/mpwo_screenshot-02.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 436 KiB |
@ -262,6 +262,10 @@ def generate_map(map_filepath, map_data):
|
|||||||
|
|
||||||
|
|
||||||
def get_map_hash(map_filepath):
|
def get_map_hash(map_filepath):
|
||||||
|
"""
|
||||||
|
md5 hash is used as id instead of activity id, to retrieve map image
|
||||||
|
(maps are sensitive data)
|
||||||
|
"""
|
||||||
md5 = hashlib.md5()
|
md5 = hashlib.md5()
|
||||||
with open(map_filepath, 'rb') as f:
|
with open(map_filepath, 'rb') as f:
|
||||||
for chunk in iter(lambda: f.read(128 * md5.block_size), b''):
|
for chunk in iter(lambda: f.read(128 * md5.block_size), b''):
|
||||||
|
@ -35,7 +35,6 @@ test('should throw an error if the user is not registered', async t => {
|
|||||||
// assert user registration failed
|
// assert user registration failed
|
||||||
await t
|
await t
|
||||||
.expect(Selector('H1').withText('Login').exists).ok()
|
.expect(Selector('H1').withText('Login').exists).ok()
|
||||||
.expect(Selector('H1').withText('Dashboard').exists).notOk()
|
|
||||||
.expect(Selector('code').withText(
|
.expect(Selector('code').withText(
|
||||||
'Invalid credentials.').exists).ok()
|
'Invalid credentials.').exists).ok()
|
||||||
})
|
})
|
||||||
@ -62,7 +61,6 @@ test('should allow a user to login', async t => {
|
|||||||
// assert user is redirected to '/'
|
// assert user is redirected to '/'
|
||||||
await t
|
await t
|
||||||
.expect(Selector('H1').withText('Login').exists).notOk()
|
.expect(Selector('H1').withText('Login').exists).notOk()
|
||||||
.expect(Selector('H1').withText('Dashboard').exists).ok()
|
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -78,7 +76,6 @@ test('should throw an error if the email is invalid', async t => {
|
|||||||
// assert user registration failed
|
// assert user registration failed
|
||||||
await t
|
await t
|
||||||
.expect(Selector('H1').withText('Login').exists).ok()
|
.expect(Selector('H1').withText('Login').exists).ok()
|
||||||
.expect(Selector('H1').withText('Dashboard').exists).notOk()
|
|
||||||
.expect(Selector('code').withText(
|
.expect(Selector('code').withText(
|
||||||
'Invalid credentials.').exists).ok()
|
'Invalid credentials.').exists).ok()
|
||||||
})
|
})
|
||||||
@ -95,7 +92,6 @@ test('should throw an error if the password is invalid', async t => {
|
|||||||
// assert user registration failed
|
// assert user registration failed
|
||||||
await t
|
await t
|
||||||
.expect(Selector('H1').withText('Login').exists).ok()
|
.expect(Selector('H1').withText('Login').exists).ok()
|
||||||
.expect(Selector('H1').withText('Dashboard').exists).notOk()
|
|
||||||
.expect(Selector('code').withText(
|
.expect(Selector('code').withText(
|
||||||
'Invalid credentials.').exists).ok()
|
'Invalid credentials.').exists).ok()
|
||||||
})
|
})
|
||||||
|
@ -37,7 +37,6 @@ test('should allow a user to register', async t => {
|
|||||||
|
|
||||||
// assert user is redirected to '/'
|
// assert user is redirected to '/'
|
||||||
await t
|
await t
|
||||||
.expect(Selector('H1').withText('Dashboard').exists).ok()
|
|
||||||
.expect(Selector('H1').withText('Register').exists).notOk()
|
.expect(Selector('H1').withText('Register').exists).notOk()
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -55,7 +54,6 @@ test('should throw an error if the username is taken', async t => {
|
|||||||
// assert user registration failed
|
// assert user registration failed
|
||||||
await t
|
await t
|
||||||
.expect(Selector('H1').withText('Register').exists).ok()
|
.expect(Selector('H1').withText('Register').exists).ok()
|
||||||
.expect(Selector('H1').withText('Dashboard').exists).notOk()
|
|
||||||
.expect(Selector('code').withText(
|
.expect(Selector('code').withText(
|
||||||
'Sorry. That user already exists.').exists).ok()
|
'Sorry. That user already exists.').exists).ok()
|
||||||
})
|
})
|
||||||
@ -76,7 +74,6 @@ test('should throw an error if the email is taken', async t => {
|
|||||||
// assert user registration failed
|
// assert user registration failed
|
||||||
await t
|
await t
|
||||||
.expect(Selector('H1').withText('Register').exists).ok()
|
.expect(Selector('H1').withText('Register').exists).ok()
|
||||||
.expect(Selector('H1').withText('Dashboard').exists).notOk()
|
|
||||||
.expect(Selector('code').withText(
|
.expect(Selector('code').withText(
|
||||||
'Sorry. That user already exists.').exists).ok()
|
'Sorry. That user already exists.').exists).ok()
|
||||||
})
|
})
|
||||||
@ -97,7 +94,6 @@ test('should throw an error if the username is too short', async t => {
|
|||||||
// assert user registration failed
|
// assert user registration failed
|
||||||
await t
|
await t
|
||||||
.expect(Selector('H1').withText('Register').exists).ok()
|
.expect(Selector('H1').withText('Register').exists).ok()
|
||||||
.expect(Selector('H1').withText('Dashboard').exists).notOk()
|
|
||||||
.expect(Selector('code').withText(
|
.expect(Selector('code').withText(
|
||||||
'Username: 3 to 12 characters required.').exists).ok()
|
'Username: 3 to 12 characters required.').exists).ok()
|
||||||
})
|
})
|
||||||
@ -118,7 +114,6 @@ test('should throw an error if the user is too long', async t => {
|
|||||||
// assert user registration failed
|
// assert user registration failed
|
||||||
await t
|
await t
|
||||||
.expect(Selector('H1').withText('Register').exists).ok()
|
.expect(Selector('H1').withText('Register').exists).ok()
|
||||||
.expect(Selector('H1').withText('Dashboard').exists).notOk()
|
|
||||||
.expect(Selector('code').withText(
|
.expect(Selector('code').withText(
|
||||||
'Username: 3 to 12 characters required.').exists).ok()
|
'Username: 3 to 12 characters required.').exists).ok()
|
||||||
})
|
})
|
||||||
@ -139,7 +134,6 @@ test('should throw an error if the email is invalid', async t => {
|
|||||||
// assert user registration failed
|
// assert user registration failed
|
||||||
await t
|
await t
|
||||||
.expect(Selector('H1').withText('Register').exists).ok()
|
.expect(Selector('H1').withText('Register').exists).ok()
|
||||||
.expect(Selector('H1').withText('Dashboard').exists).notOk()
|
|
||||||
.expect(Selector('code').withText(
|
.expect(Selector('code').withText(
|
||||||
'Valid email must be provided.').exists).ok()
|
'Valid email must be provided.').exists).ok()
|
||||||
})
|
})
|
||||||
@ -158,7 +152,6 @@ test('should throw an error if passwords don\'t match', async t => {
|
|||||||
// assert user registration failed
|
// assert user registration failed
|
||||||
await t
|
await t
|
||||||
.expect(Selector('H1').withText('Register').exists).ok()
|
.expect(Selector('H1').withText('Register').exists).ok()
|
||||||
.expect(Selector('H1').withText('Dashboard').exists).notOk()
|
|
||||||
.expect(Selector('code').withText(
|
.expect(Selector('code').withText(
|
||||||
'Password and password confirmation don\'t match.').exists).ok()
|
'Password and password confirmation don\'t match.').exists).ok()
|
||||||
})
|
})
|
||||||
@ -179,7 +172,6 @@ test('should throw an error if the password is too short', async t => {
|
|||||||
// assert user registration failed
|
// assert user registration failed
|
||||||
await t
|
await t
|
||||||
.expect(Selector('H1').withText('Register').exists).ok()
|
.expect(Selector('H1').withText('Register').exists).ok()
|
||||||
.expect(Selector('H1').withText('Dashboard').exists).notOk()
|
|
||||||
.expect(Selector('code').withText(
|
.expect(Selector('code').withText(
|
||||||
'Password: 8 characters required.').exists).ok()
|
'Password: 8 characters required.').exists).ok()
|
||||||
})
|
})
|
||||||
|
@ -146,6 +146,10 @@ input, textarea {
|
|||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dashboard {
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
.sport-img {
|
.sport-img {
|
||||||
max-width: 35px;
|
max-width: 35px;
|
||||||
max-height: 35px;
|
max-height: 35px;
|
||||||
|
@ -28,10 +28,6 @@ export default function ActivityCard (props) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="col">
|
<div className="col">
|
||||||
<p>
|
|
||||||
<i className="fa fa-calendar" aria-hidden="true" />{' '}
|
|
||||||
Start at {activity.activity_date}
|
|
||||||
</p>
|
|
||||||
<p>
|
<p>
|
||||||
<i className="fa fa-clock-o" aria-hidden="true" />{' '}
|
<i className="fa fa-clock-o" aria-hidden="true" />{' '}
|
||||||
Duration: {activity.duration}
|
Duration: {activity.duration}
|
||||||
|
@ -33,12 +33,11 @@ class DashBoard extends React.Component {
|
|||||||
<Helmet>
|
<Helmet>
|
||||||
<title>mpwo - Dashboard</title>
|
<title>mpwo - Dashboard</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<h1 className="page-title">Dashboard</h1>
|
|
||||||
{message ? (
|
{message ? (
|
||||||
<code>{message}</code>
|
<code>{message}</code>
|
||||||
) : (
|
) : (
|
||||||
(activities.length > 0 && sports.length > 0) ? (
|
(activities.length > 0 && sports.length > 0) ? (
|
||||||
<div className="container">
|
<div className="container dashboard">
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col-md-4">
|
<div className="col-md-4">
|
||||||
<Records records={records} sports={sports} />
|
<Records records={records} sports={sports} />
|
||||||
|
Loading…
Reference in New Issue
Block a user