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'))
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

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
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
):

View File

@ -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}`)))

View File

@ -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;
}

View File

@ -23,42 +23,45 @@ export default function RecordsCard (props) {
Personal records
</div>
<div className="card-body">
{Object.keys(recordsBySport).map(sportLabel => (
<table
className="table table-borderless record-table"
key={sportLabel}
>
<thead>
<tr>
<th colSpan="3">
<img
alt={`${sportLabel} logo`}
className="record-logo"
src={recordsBySport[sportLabel].img}
/>
{sportLabel}
</th>
</tr>
</thead>
<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>
{Object.keys(recordsBySport).length === 0
? 'No records.'
: (Object.keys(recordsBySport).map(sportLabel => (
<table
className="table table-borderless record-table"
key={sportLabel}
>
<thead>
<tr>
<th colSpan="3">
<img
alt={`${sportLabel} logo`}
className="record-logo"
src={recordsBySport[sportLabel].img}
/>
{sportLabel}
</th>
</tr>
))}
</tbody>
</table>
))}
</thead>
<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>
))}
</tbody>
</table>))
)
}
</div>
</div>
)

View File

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

View File

@ -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 <div className="days row">{days}</div>
}
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(
<div
className={`col cell ${
dateFns.isSameMonth(day, monthStart)
? dateFns.isSameDay(day, selectedDate) ? 'selected' : ''
: 'disabled'
}`}
key={day}
>
<div className="col cell" key={day} >
<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>
)
day = dateFns.addDays(day, 1)
@ -92,16 +119,24 @@ export default class Calendar extends React.Component {
return <div className="body">{rows}</div>
}
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)

View File

@ -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',

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) => {
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,

View File

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