API & Client: display activities on calendar - #2
This commit is contained in:
parent
179befac71
commit
017d92d7a6
@ -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
|
||||
|
@ -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
|
||||
):
|
||||
|
@ -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}`)))
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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>
|
||||
)
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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',
|
||||
|
@ -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,
|
||||
|
@ -41,6 +41,9 @@ export default {
|
||||
activities: {
|
||||
...emptyData,
|
||||
},
|
||||
calendarActivities: {
|
||||
...emptyData,
|
||||
},
|
||||
chartData: [],
|
||||
// check if storing gpx content is OK
|
||||
gpx: null,
|
||||
|
Loading…
Reference in New Issue
Block a user