API & Client: display activities on calendar - #2

This commit is contained in:
Sam 2018-06-04 14:38:48 +02:00
parent 179befac71
commit 017d92d7a6
10 changed files with 244 additions and 63 deletions

View File

@ -26,6 +26,7 @@ def get_activities(auth_user_id):
page = 1 if 'page' not in params.keys() else int(params.get('page')) page = 1 if 'page' not in params.keys() else int(params.get('page'))
date_from = params.get('from') date_from = params.get('from')
date_to = params.get('to') date_to = params.get('to')
order = params.get('order')
activities = Activity.query.filter( activities = Activity.query.filter(
Activity.user_id == auth_user_id, Activity.user_id == auth_user_id,
Activity.activity_date >= datetime.strptime(date_from, '%Y-%m-%d') Activity.activity_date >= datetime.strptime(date_from, '%Y-%m-%d')
@ -33,7 +34,9 @@ def get_activities(auth_user_id):
Activity.activity_date <= datetime.strptime(date_to, '%Y-%m-%d') Activity.activity_date <= datetime.strptime(date_to, '%Y-%m-%d')
if date_to else True, if date_to else True,
).order_by( ).order_by(
Activity.activity_date.desc() Activity.activity_date.asc()
if order == 'asc'
else Activity.activity_date.desc()
).paginate( ).paginate(
page, 5, False page, 5, False
).items ).items

View File

@ -344,6 +344,64 @@ def test_get_activities_date_filter_paginate(
assert 'Mon, 20 Mar 2017 00:00:00 GMT' == data['data']['activities'][1]['activity_date'] # noqa assert 'Mon, 20 Mar 2017 00:00:00 GMT' == data['data']['activities'][1]['activity_date'] # noqa
def test_get_activities_order(
app, user_1, sport_1_cycling, seven_activities_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.get(
'/api/activities?order=asc',
headers=dict(
Authorization='Bearer ' + json.loads(
resp_login.data.decode()
)['auth_token']
)
)
data = json.loads(response.data.decode())
assert response.status_code == 200
assert 'success' in data['status']
assert len(data['data']['activities']) == 5
assert 'Mon, 20 Mar 2017 00:00:00 GMT' == data['data']['activities'][0]['activity_date'] # noqa
assert 'Fri, 23 Feb 2018 00:00:00 GMT' == data['data']['activities'][4]['activity_date'] # noqa
def test_get_activities_date_filter_paginate_order(
app, user_1, sport_1_cycling, seven_activities_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.get(
'/api/activities?from=2017-01-01&page=2&order=asc',
headers=dict(
Authorization='Bearer ' + json.loads(
resp_login.data.decode()
)['auth_token']
)
)
data = json.loads(response.data.decode())
assert response.status_code == 200
assert 'success' in data['status']
assert len(data['data']['activities']) == 2
assert 'Sun, 01 Apr 2018 00:00:00 GMT' == data['data']['activities'][0]['activity_date'] # noqa
assert 'Wed, 09 May 2018 00:00:00 GMT' == data['data']['activities'][1]['activity_date'] # noqa
def test_get_an_activity( def test_get_an_activity(
app, user_1, sport_1_cycling, activity_cycling_user_1 app, user_1, sport_1_cycling, activity_cycling_user_1
): ):

View File

@ -1,3 +1,5 @@
import { parse } from 'date-fns'
import mpwoGenericApi from '../mwpoApi' import mpwoGenericApi from '../mwpoApi'
import mpwoApi from '../mwpoApi/activities' import mpwoApi from '../mwpoApi/activities'
import { history } from '../index' import { history } from '../index'
@ -9,6 +11,11 @@ export const pushActivities = activities => ({
activities, activities,
}) })
export const updateCalendar = activities => ({
type: 'UPDATE_CALENDAR',
activities,
})
export const setGpx = gpxContent => ({ export const setGpx = gpxContent => ({
type: 'SET_GPX', type: 'SET_GPX',
gpxContent, gpxContent,
@ -121,3 +128,21 @@ export const getMoreActivities = page => dispatch => mpwoGenericApi
} }
}) })
.catch(error => dispatch(setError(`activities: ${error}`))) .catch(error => dispatch(setError(`activities: ${error}`)))
export const getMonthActivities = (start, end) => dispatch => mpwoGenericApi
.getData('activities', null, null, start, end, 'asc')
.then(ret => {
if (ret.status === 'success') {
if (ret.data.activities.length > 0) {
for (let i = 0; i < ret.data.activities.length; i++) {
ret.data.activities[i].activity_date = parse(
ret.data.activities[i].activity_date
)
}
dispatch(updateCalendar(ret.data.activities))
}
} else {
dispatch(setError(`activities: ${ret.message}`))
}
})
.catch(error => dispatch(setError(`activities: ${error}`)))

View File

@ -71,6 +71,12 @@ input, textarea {
margin-top: 20px; margin-top: 20px;
} }
.activity-sport {
margin-right: 1px;
max-width: 20px;
max-height: 20px;
}
.add-activity { .add-activity {
margin-top: 50px; margin-top: 50px;
} }

View File

@ -23,42 +23,45 @@ export default function RecordsCard (props) {
Personal records Personal records
</div> </div>
<div className="card-body"> <div className="card-body">
{Object.keys(recordsBySport).map(sportLabel => ( {Object.keys(recordsBySport).length === 0
<table ? 'No records.'
className="table table-borderless record-table" : (Object.keys(recordsBySport).map(sportLabel => (
key={sportLabel} <table
> className="table table-borderless record-table"
<thead> key={sportLabel}
<tr> >
<th colSpan="3"> <thead>
<img <tr>
alt={`${sportLabel} logo`} <th colSpan="3">
className="record-logo" <img
src={recordsBySport[sportLabel].img} alt={`${sportLabel} logo`}
/> className="record-logo"
{sportLabel} src={recordsBySport[sportLabel].img}
</th> />
</tr> {sportLabel}
</thead> </th>
<tbody>
{recordsBySport[sportLabel].records.map(rec => (
<tr key={rec.id}>
<td>
{rec.record_type}
</td>
<td>
{rec.value}
</td>
<td>
<Link to={`/activities/${rec.activity_id}`}>
{rec.activity_date}
</Link>
</td>
</tr> </tr>
))} </thead>
</tbody> <tbody>
</table> {recordsBySport[sportLabel].records.map(rec => (
))} <tr key={rec.id}>
<td>
{rec.record_type}
</td>
<td>
{rec.value}
</td>
<td>
<Link to={`/activities/${rec.activity_id}`}>
{rec.activity_date}
</Link>
</td>
</tr>
))}
</tbody>
</table>))
)
}
</div> </div>
</div> </div>
) )

View File

@ -37,22 +37,25 @@ class DashBoard extends React.Component {
{message ? ( {message ? (
<code>{message}</code> <code>{message}</code>
) : ( ) : (
(activities.length > 0 && sports.length > 0) ? ( (activities && sports.length > 0) && (
<div className="container dashboard"> <div className="container dashboard">
<div className="row"> <div className="row">
<div className="col-md-4"> <div className="col-md-4">
<Calendar />
<Records records={records} sports={sports} /> <Records records={records} sports={sports} />
<Statistics /> <Statistics />
</div> </div>
<div className="col-md-8"> <div className="col-md-8">
{activities.map(activity => ( <Calendar />
{activities.length > 0 ? (
activities.map(activity => (
<ActivityCard <ActivityCard
activity={activity} activity={activity}
key={activity.id} key={activity.id}
sports={sports} sports={sports}
/> />)
))} )) : (
'No activities. Upload one !'
)}
{!paginationEnd && {!paginationEnd &&
<input <input
type="submit" type="submit"
@ -67,9 +70,8 @@ class DashBoard extends React.Component {
</div> </div>
</div> </div>
</div> </div>
) : ( )
'No activities for now' )}
))}
</div> </div>
) )
} }

View File

@ -2,15 +2,32 @@
// source: https://blog.flowandform.agency/create-a-custom-calendar-in-react-3df1bfd0b728 // source: https://blog.flowandform.agency/create-a-custom-calendar-in-react-3df1bfd0b728
import dateFns from 'date-fns' import dateFns from 'date-fns'
import React from 'react' import React from 'react'
import { connect } from 'react-redux'
import { Link } from 'react-router-dom'
export default class Calendar extends React.Component { import { getMonthActivities } from '../../actions/activities'
const getStartAndEndMonth = date => ({
start: dateFns.startOfMonth(date),
end: dateFns.endOfMonth(date),
})
class Calendar extends React.Component {
constructor(props, context) { constructor(props, context) {
super(props, context) super(props, context)
const calendarDate = new Date()
this.state = { this.state = {
currentMonth: new Date(), currentMonth: calendarDate,
monthStart: getStartAndEndMonth(calendarDate).start,
monthEnd: getStartAndEndMonth(calendarDate).end,
} }
} }
componentDidMount() {
this.props.loadMonthActivities(this.state.monthStart, this.state.monthEnd)
}
renderHeader() { renderHeader() {
const dateFormat = 'MMM YYYY' const dateFormat = 'MMM YYYY'
return ( return (
@ -51,10 +68,15 @@ export default class Calendar extends React.Component {
return <div className="days row">{days}</div> return <div className="days row">{days}</div>
} }
filterActivities(day) {
const { activities } = this.props
return activities
.filter(act => dateFns.isSameDay(act.activity_date, day))
}
renderCells() { renderCells() {
const { currentMonth, selectedDate } = this.state const { monthStart, monthEnd } = this.state
const monthStart = dateFns.startOfMonth(currentMonth) const { sports } = this.props
const monthEnd = dateFns.endOfMonth(monthStart)
const startDate = dateFns.startOfWeek(monthStart) const startDate = dateFns.startOfWeek(monthStart)
const endDate = dateFns.endOfWeek(monthEnd) const endDate = dateFns.endOfWeek(monthEnd)
@ -68,16 +90,21 @@ export default class Calendar extends React.Component {
while (day <= endDate) { while (day <= endDate) {
for (let i = 0; i < 7; i++) { for (let i = 0; i < 7; i++) {
formattedDate = dateFns.format(day, dateFormat) formattedDate = dateFns.format(day, dateFormat)
const dayActivities = this.filterActivities(day)
days.push( days.push(
<div <div className="col cell" key={day} >
className={`col cell ${
dateFns.isSameMonth(day, monthStart)
? dateFns.isSameDay(day, selectedDate) ? 'selected' : ''
: 'disabled'
}`}
key={day}
>
<span className="number">{formattedDate}</span> <span className="number">{formattedDate}</span>
{dayActivities.map(act => (
<Link key={act.id} to={`/activities/${act.id}`}>
<img
className="activity-sport"
src={sports
.filter(s => s.id === act.sport_id)
.map(s => s.img)}
alt="activity sport logo"
/>
</Link>
))}
</div> </div>
) )
day = dateFns.addDays(day, 1) day = dateFns.addDays(day, 1)
@ -92,16 +119,24 @@ export default class Calendar extends React.Component {
return <div className="body">{rows}</div> return <div className="body">{rows}</div>
} }
handleNextMonth () { updateStateDate (calendarDate) {
const { start, end } = getStartAndEndMonth(calendarDate)
this.setState({ this.setState({
currentMonth: dateFns.addMonths(this.state.currentMonth, 1) currentMonth: calendarDate,
monthStart: start,
monthEnd: end,
}) })
this.props.loadMonthActivities(start, end)
}
handleNextMonth () {
const calendarDate = dateFns.addMonths(this.state.currentMonth, 1)
this.updateStateDate(calendarDate)
} }
handlePrevMonth () { handlePrevMonth () {
this.setState({ const calendarDate = dateFns.subMonths(this.state.currentMonth, 1)
currentMonth: dateFns.subMonths(this.state.currentMonth, 1) this.updateStateDate(calendarDate)
})
} }
render() { render() {
@ -116,3 +151,19 @@ export default class Calendar extends React.Component {
) )
} }
} }
export default connect(
state => ({
activities: state.calendarActivities.data,
sports: state.sports.data,
}),
dispatch => ({
loadMonthActivities: (start, end) => {
const dateFormat = 'YYYY-MM-DD'
dispatch(getMonthActivities(
dateFns.format(start, dateFormat),
dateFns.format(end, dateFormat),
))
},
})
)(Calendar)

View File

@ -2,13 +2,30 @@ import { apiUrl, createRequest } from '../utils'
export default class MpwoApi { export default class MpwoApi {
static getData(target, id = null, page = null) { static getData(target,
id = null,
page = null,
start = null,
end = null,
order = null) {
let url = `${apiUrl}${target}` let url = `${apiUrl}${target}`
if (id) { if (id) {
url = `${url}/${id}` url = `${url}/${id}`
} else if (page) { } else if (page) {
url = `${url}?page=${page}` url = `${url}?page=${page}`
} }
if (start || end) {
url = `${url}${
page ? '' : '?'
}${
start && `&from=${start}`
}${
end && `&to=${end}`
}`
}
if (order) {
url = `${url}${(page || start || end) ? '' : '?'}${`&order=${order}`}`
}
const params = { const params = {
url: url, url: url,
method: 'GET', method: 'GET',

View File

@ -31,6 +31,18 @@ const activities = (state = initial.activities, action) => {
} }
} }
const calendarActivities = (state = initial.calendarActivities, action) => {
switch (action.type) {
case 'UPDATE_CALENDAR':
return {
...state,
data: action.activities,
}
default:
return handleDataAndError(state, 'calendarActivities', action)
}
}
const chartData = (state = initial.chartData, action) => { const chartData = (state = initial.chartData, action) => {
switch (action.type) { switch (action.type) {
case 'SET_CHART_DATA': case 'SET_CHART_DATA':
@ -181,6 +193,7 @@ const user = (state = initial.user, action) => {
const reducers = combineReducers({ const reducers = combineReducers({
activities, activities,
calendarActivities,
chartData, chartData,
formData, formData,
formProfile, formProfile,

View File

@ -41,6 +41,9 @@ export default {
activities: { activities: {
...emptyData, ...emptyData,
}, },
calendarActivities: {
...emptyData,
},
chartData: [], chartData: [],
// check if storing gpx content is OK // check if storing gpx content is OK
gpx: null, gpx: null,