From 017d92d7a6a0b236ee989ad44a4149dc85f26b1a Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 4 Jun 2018 14:38:48 +0200 Subject: [PATCH] API & Client: display activities on calendar - #2 --- mpwo_api/mpwo_api/activities/activities.py | 5 +- .../tests/test_activities_api_0_get.py | 58 +++++++++++++ mpwo_client/src/actions/activities.js | 25 ++++++ mpwo_client/src/components/App.css | 6 ++ .../src/components/Dashboard/Records.jsx | 73 ++++++++-------- .../src/components/Dashboard/index.jsx | 18 ++-- .../src/components/Others/Calendar.jsx | 87 +++++++++++++++---- mpwo_client/src/mwpoApi/index.js | 19 +++- mpwo_client/src/reducers/index.js | 13 +++ mpwo_client/src/reducers/initial.js | 3 + 10 files changed, 244 insertions(+), 63 deletions(-) diff --git a/mpwo_api/mpwo_api/activities/activities.py b/mpwo_api/mpwo_api/activities/activities.py index 37e194f8..b41ae104 100644 --- a/mpwo_api/mpwo_api/activities/activities.py +++ b/mpwo_api/mpwo_api/activities/activities.py @@ -26,6 +26,7 @@ def get_activities(auth_user_id): page = 1 if 'page' not in params.keys() else int(params.get('page')) date_from = params.get('from') date_to = params.get('to') + order = params.get('order') activities = Activity.query.filter( Activity.user_id == auth_user_id, 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') if date_to else True, ).order_by( - Activity.activity_date.desc() + Activity.activity_date.asc() + if order == 'asc' + else Activity.activity_date.desc() ).paginate( page, 5, False ).items diff --git a/mpwo_api/mpwo_api/tests/test_activities_api_0_get.py b/mpwo_api/mpwo_api/tests/test_activities_api_0_get.py index bcc6cc2e..18eaff0f 100644 --- a/mpwo_api/mpwo_api/tests/test_activities_api_0_get.py +++ b/mpwo_api/mpwo_api/tests/test_activities_api_0_get.py @@ -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 +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( app, user_1, sport_1_cycling, activity_cycling_user_1 ): diff --git a/mpwo_client/src/actions/activities.js b/mpwo_client/src/actions/activities.js index 18ebc5ff..3a334fe4 100644 --- a/mpwo_client/src/actions/activities.js +++ b/mpwo_client/src/actions/activities.js @@ -1,3 +1,5 @@ +import { parse } from 'date-fns' + import mpwoGenericApi from '../mwpoApi' import mpwoApi from '../mwpoApi/activities' import { history } from '../index' @@ -9,6 +11,11 @@ export const pushActivities = activities => ({ activities, }) +export const updateCalendar = activities => ({ + type: 'UPDATE_CALENDAR', + activities, +}) + export const setGpx = gpxContent => ({ type: 'SET_GPX', gpxContent, @@ -121,3 +128,21 @@ export const getMoreActivities = page => dispatch => mpwoGenericApi } }) .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}`))) diff --git a/mpwo_client/src/components/App.css b/mpwo_client/src/components/App.css index 89d6a016..23ddef0d 100644 --- a/mpwo_client/src/components/App.css +++ b/mpwo_client/src/components/App.css @@ -71,6 +71,12 @@ input, textarea { margin-top: 20px; } +.activity-sport { + margin-right: 1px; + max-width: 20px; + max-height: 20px; +} + .add-activity { margin-top: 50px; } diff --git a/mpwo_client/src/components/Dashboard/Records.jsx b/mpwo_client/src/components/Dashboard/Records.jsx index 1f16c187..90d2e5d7 100644 --- a/mpwo_client/src/components/Dashboard/Records.jsx +++ b/mpwo_client/src/components/Dashboard/Records.jsx @@ -23,42 +23,45 @@ export default function RecordsCard (props) { Personal records
- {Object.keys(recordsBySport).map(sportLabel => ( - - - - - - - - {recordsBySport[sportLabel].records.map(rec => ( - - - - + {Object.keys(recordsBySport).length === 0 + ? 'No records.' + : (Object.keys(recordsBySport).map(sportLabel => ( +
- {`${sportLabel} - {sportLabel} -
- {rec.record_type} - - {rec.value} - - - {rec.activity_date} - -
+ + + - ))} - -
+ {`${sportLabel} + {sportLabel} +
- ))} + + + {recordsBySport[sportLabel].records.map(rec => ( + + + {rec.record_type} + + + {rec.value} + + + + {rec.activity_date} + + + + ))} + + )) + ) + }
) diff --git a/mpwo_client/src/components/Dashboard/index.jsx b/mpwo_client/src/components/Dashboard/index.jsx index 4229637e..80e06468 100644 --- a/mpwo_client/src/components/Dashboard/index.jsx +++ b/mpwo_client/src/components/Dashboard/index.jsx @@ -37,22 +37,25 @@ class DashBoard extends React.Component { {message ? ( {message} ) : ( - (activities.length > 0 && sports.length > 0) ? ( + (activities && sports.length > 0) && (
-
- {activities.map(activity => ( + + {activities.length > 0 ? ( + activities.map(activity => ( - ))} + />) + )) : ( + 'No activities. Upload one !' + )} {!paginationEnd &&
- ) : ( - 'No activities for now' - ))} + ) + )}
) } diff --git a/mpwo_client/src/components/Others/Calendar.jsx b/mpwo_client/src/components/Others/Calendar.jsx index 1426cd44..7d7caa0d 100644 --- a/mpwo_client/src/components/Others/Calendar.jsx +++ b/mpwo_client/src/components/Others/Calendar.jsx @@ -2,15 +2,32 @@ // source: https://blog.flowandform.agency/create-a-custom-calendar-in-react-3df1bfd0b728 import dateFns from 'date-fns' 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) { super(props, context) + const calendarDate = new Date() 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() { const dateFormat = 'MMM YYYY' return ( @@ -51,10 +68,15 @@ export default class Calendar extends React.Component { return
{days}
} + filterActivities(day) { + const { activities } = this.props + return activities + .filter(act => dateFns.isSameDay(act.activity_date, day)) + } + renderCells() { - const { currentMonth, selectedDate } = this.state - const monthStart = dateFns.startOfMonth(currentMonth) - const monthEnd = dateFns.endOfMonth(monthStart) + const { monthStart, monthEnd } = this.state + const { sports } = this.props const startDate = dateFns.startOfWeek(monthStart) const endDate = dateFns.endOfWeek(monthEnd) @@ -68,16 +90,21 @@ export default class Calendar extends React.Component { while (day <= endDate) { for (let i = 0; i < 7; i++) { formattedDate = dateFns.format(day, dateFormat) + const dayActivities = this.filterActivities(day) days.push( -
+
{formattedDate} + {dayActivities.map(act => ( + + s.id === act.sport_id) + .map(s => s.img)} + alt="activity sport logo" + /> + + ))}
) day = dateFns.addDays(day, 1) @@ -92,16 +119,24 @@ export default class Calendar extends React.Component { return
{rows}
} - handleNextMonth () { + updateStateDate (calendarDate) { + const { start, end } = getStartAndEndMonth(calendarDate) 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 () { - this.setState({ - currentMonth: dateFns.subMonths(this.state.currentMonth, 1) - }) + const calendarDate = dateFns.subMonths(this.state.currentMonth, 1) + this.updateStateDate(calendarDate) } 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) diff --git a/mpwo_client/src/mwpoApi/index.js b/mpwo_client/src/mwpoApi/index.js index e0f15506..a7c1c7a2 100644 --- a/mpwo_client/src/mwpoApi/index.js +++ b/mpwo_client/src/mwpoApi/index.js @@ -2,13 +2,30 @@ import { apiUrl, createRequest } from '../utils' 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}` if (id) { url = `${url}/${id}` } else if (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 = { url: url, method: 'GET', diff --git a/mpwo_client/src/reducers/index.js b/mpwo_client/src/reducers/index.js index 7aa56998..08c1aa51 100644 --- a/mpwo_client/src/reducers/index.js +++ b/mpwo_client/src/reducers/index.js @@ -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) => { switch (action.type) { case 'SET_CHART_DATA': @@ -181,6 +193,7 @@ const user = (state = initial.user, action) => { const reducers = combineReducers({ activities, + calendarActivities, chartData, formData, formProfile, diff --git a/mpwo_client/src/reducers/initial.js b/mpwo_client/src/reducers/initial.js index d8a5004c..b1771c4d 100644 --- a/mpwo_client/src/reducers/initial.js +++ b/mpwo_client/src/reducers/initial.js @@ -41,6 +41,9 @@ export default { activities: { ...emptyData, }, + calendarActivities: { + ...emptyData, + }, chartData: [], // check if storing gpx content is OK gpx: null,