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'))
|
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
|
||||||
|
@ -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
|
||||||
):
|
):
|
||||||
|
@ -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}`)))
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
)
|
)
|
||||||
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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',
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
Loading…
Reference in New Issue
Block a user