API & Client: display activities on calendar - #2
This commit is contained in:
		| @@ -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, | ||||
|   | ||||
		Reference in New Issue
	
	Block a user