Closed shnizzedy closed 4 years ago
I think rather than a freeform JSON Object as a single schedule
parameter, it would be useful to constrain the shape and contents of the schedule, either by specifying how that Object should exist or by passing different keys separately.
Can anyone think of what the advantages would be of having a freeform json object as a schedule parameter?
I can start prototyping an implementation that uses the Veutify v2 calendar component instead of dayspan-vuetify BUT I would simply be attempting as close to a one-for-one replacement as possible. I'm not in a position to make strategic schema or architectural decisions since I only starting looking at this a couple of days ago. It is worth mentioning that scheduling seems to be the most complex and fragile aspect of the ML Admin Panel web app. Rewriting it a week or two before launch is not ideal. I'm trying to take a measure twice cut once approach.
@curtpw, in standup yesterday, @henryrossiter suggested we better define what we're trying to do, I added on that we should do so in writing, and @odziem added that our back-and-forth on https://github.com/ChildMindInstitute/mindlogger-app-backend/issues/204 was a useful iterative design-and-documentation thread. This issue is intended to be the measuring twice you mentioned. Updating the calendar component is a separate, related issue.
Can anyone think of what the advantages would be of having a freeform json object as a schedule parameter?
@binarybottle ― advantages to whom? The question is a classic balancing act between structure and flexibility. In this case, as far as I can tell, the advantages to a freeform JSON Object are:
beneficiary | advantage | cost |
---|---|---|
developer, manager, coordinator, user | time to launch | stability, reliability, meeting expectations, control |
manager, coordinator, user | extreme flexibility | nonfunctional / unimplemented functionality |
developer, user support | setting schedule should rarely throw exceptions | silent failures, unknown / unknowable user requirements, pain points |
To be clear, I think a freeform JSON Object (what we have now) is better than nothing (what we had before) but worse than a well-defined structure.
In this morning's standup, @odziem, @binarybottle, @henryrossiter, @hotavocado, and I discussed how to handle timezones.
Here are some in-use-at-the-moment snippets to help until we document what we want:
const setSchedule = ({ apiHost, token, id, data }) => axios({
method: 'put',
url: `${apiHost}/${id}/constraints`,
headers: {
'Girder-Token': token,
},
data,
});
saveSchedule() {
const scheduleForm = new FormData();
if (this.currentApplet && this.currentApplet.applet && this.currentApplet.applet.schedule) {
this.dialog = true;
this.saveSuccess = false;
this.saveError = false;
this.loading = true;
const schedule = this.currentApplet.applet.schedule;
scheduleForm.set('schedule', JSON.stringify(schedule || {}));
api.setSchedule({
apiHost: this.$store.state.backend,
id: this.currentApplet.applet._id,
token: this.$store.state.auth.authToken.token,
data: scheduleForm,
}).then(() => {
console.log('success');
this.loading = false;
this.saveSuccess = true;
}).catch((e) => {
this.errorMessage = `Save Unsuccessful. ${e}`;
console.log('fail');
this.loading = false;
this.saveError = true;
});
}
},
setSchedule(state, schedule) {
if (!_.isEmpty(state.currentApplet)) {
// TODO: this sucks.
const idx = _.findIndex(state.allApplets,
a => a.applet['skos:prefLabel'] == state.currentApplet.applet['skos:prefLabel']);
if (idx > -1) {
state.allApplets[idx].applet.schedule = schedule;
state.currentApplet = state.allApplets[idx];
}
// update this in the copy too.
//state.currentApplet = {...state.currentApplet, schedule };
}
},
export const dateParser = (schedule) => {
const output = {};
schedule.events.forEach((e) => {
const uri = e.data.URI;
if (!output[uri]) {
output[uri] = {
notificationDateTimes: [],
};
}
const eventSchedule = Parse.schedule(e.schedule);
const now = Day.fromDate(new Date());
const lastScheduled = getLastScheduled(eventSchedule, now);
const nextScheduled = getNextScheduled(eventSchedule, now);
const notifications = R.pathOr([], ['data', 'notifications'], e);
const dateTimes = getScheduledNotifications(eventSchedule, now, notifications);
let lastScheduledResponse = lastScheduled;
if (output[uri].lastScheduledResponse && lastScheduled) {
lastScheduledResponse = moment.max(
moment(output[uri].lastScheduledResponse),
moment(lastScheduled),
);
}
let nextScheduledResponse = nextScheduled;
if (output[uri].nextScheduledResponse && nextScheduled) {
nextScheduledResponse = moment.min(
moment(output[uri].nextScheduledResponse),
moment(nextScheduled),
);
}
output[uri] = {
lastScheduledResponse,
nextScheduledResponse,
// TODO: only append unique datetimes when multiple events scheduled for same activity/URI
notificationDateTimes: output[uri].notificationDateTimes.concat(dateTimes),
};
});
return output;
};
// Attach some info to each activity
export const appletsSelector = createSelector(
R.path(['applets', 'applets']),
responseScheduleSelector,
(applets, responseSchedule) => applets.map((applet) => {
let scheduledDateTimesByActivity = {};
// applet.schedule, if defined, has an events key.
// events is a list of objects.
// the events[idx].data.URI points to the specific activity's schema.
if (applet.schedule) {
scheduledDateTimesByActivity = dateParser(applet.schedule);
}
const extraInfoActivities = applet.activities.map((act) => {
const scheduledDateTimes = scheduledDateTimesByActivity[act.schema];
const nextScheduled = R.pathOr(null, ['nextScheduledResponse'], scheduledDateTimes);
const lastScheduled = R.pathOr(null, ['lastScheduledResponse'], scheduledDateTimes);
const lastResponse = R.path([applet.id, act.id, 'lastResponse'], responseSchedule);
return {
...act,
appletId: applet.id,
appletShortName: applet.name,
appletName: applet.name,
appletSchema: applet.schema,
appletSchemaVersion: applet.schemaVersion,
lastScheduledTimestamp: lastScheduled,
lastResponseTimestamp: lastResponse,
nextScheduledTimestamp: nextScheduled,
isOverdue: lastScheduled && moment(lastResponse) < moment(lastScheduled),
// also add in our parsed notifications...
notification: R.prop('notificationDateTimes', scheduledDateTimes),
};
});
return {
...applet,
activities: extraInfoActivities,
};
}),
);
export const scheduleNotifications = (activities) => {
PushNotificationIOS.setApplicationIconBadgeNumber(1);
PushNotification.cancelAllLocalNotifications();
// const now = moment();
// const lookaheadDate = moment().add(1, 'month');
const notifications = [];
for (let i = 0; i < activities.length; i += 1) {
const activity = activities[i];
const scheduleDateTimes = activity.notification || [];
// /* below is for easy debugging.
// every 30 seconds a notification will appear for an applet.
// */
// const scheduleDateTimes = [];
// for (i = 0; i < 50; i += 1) {
// const foo = new Date();
// foo.setSeconds(foo.getSeconds() + i * 30);
// scheduleDateTimes.push(moment(foo));
// }
// console.log('activity', activity);
// /* end easy debugging section */
scheduleDateTimes.forEach((dateTime) => {
const ugctime = new Date(dateTime.valueOf());
notifications.push({
timestamp: ugctime,
niceTime: dateTime.format(),
activityId: activity.id,
activityName: activity.name.en,
appletName: activity.appletName,
activity: JSON.stringify(activity),
});
});
}
// Sort notifications by timestamp
notifications.sort((a, b) => a.timestamp - b.timestamp);
// Schedule the notifications
notifications.forEach((notification) => {
PushNotification.localNotificationSchedule({
message: `Please perform activity: ${notification.activityName}`,
date: new Date(notification.timestamp),
group: notification.activityName,
vibrate: true,
userInfo: {
id: notification.activityId,
activity: notification.activity,
},
// android only (notification.activity is already stringified)
data: notification.activity,
});
});
return notifications;
};
def setConstraints(self, folder, activity, schedule, **kwargs):
thisUser = self.getCurrentUser()
applet = jsonld_expander.formatLdObject(
_setConstraints(folder, activity, schedule, thisUser),
'applet',
thisUser,
refreshCache=True
)
thread = threading.Thread(
target=AppletModel().updateUserCacheAllUsersAllRoles,
args=(applet, thisUser)
)
thread.start()
return(applet)
For applet 5e1f4f47720011229c1a15d2
on https://api.mindlogger.org/api/v1, my attempt to manually set a schedule ⅓ worked. I realized https://github.com/ChildMindInstitute/mindlogger-admin/issues/71 may have been the issue with it working; I think the schedule page on admin was saving the incorrect activity keys. I've updated to match the URLs of the activities actually contained in the applet. For the sake of documentation, here's what is currently set, which I intend to reflect the following schedule:
activity | scheduled time | notified if not completed time |
---|---|---|
EMA Assessment (Morning) | daily, 8:00 AM | 9:00 AM |
EMA Assessment (Mid Day) | daily, 2:00 PM | 3:00 PM |
EMA Assessment (Night) | daily, 7:00 PM | 8:00 PM |
{
"around": 1578805200000,
"events": [{
"data": {
"URI": "https://raw.githubusercontent.com/hotavocado/HBN_EMA_NIMH2/master/activities/morning_set/morning_set_schema",
"busy": true,
"calendar": "",
"color": "#F44336",
"description": "",
"forecolor": "#ffffff",
"icon": "",
"location": "",
"notifications": [{
"end": null,
"notifyIfIncomplete": true,
"random": false,
"start": "09:00"
}],
"title": "EMA Assessment (Morning)",
"useNotifications": true
},
"schedule": {
"times": [
"08"
]
}
},
{
"data": {
"URI": "https://raw.githubusercontent.com/hotavocado/HBN_EMA_NIMH2/master/activities/day_set/day_set_schema",
"busy": true,
"calendar": "",
"color": "#1976d2",
"description": "",
"forecolor": "#ffffff",
"icon": "",
"location": "",
"notifications": [{
"end": null,
"notifyIfIncomplete": true,
"random": false,
"start": "15:00"
}],
"title": "EMA Assessment (Mid Day)",
"useNotifications": true
},
"schedule": {
"times": [
"14"
]
}
},
{
"data": {
"URI": "https://raw.githubusercontent.com/hotavocado/HBN_EMA_NIMH2/master/activities/evening_set/evening_set_schema",
"busy": true,
"calendar": "",
"color": "#1976d2",
"description": "",
"forecolor": "#ffffff",
"icon": "",
"location": "",
"notifications": [{
"end": null,
"notifyIfIncomplete": true,
"random": false,
"start": "20:00"
}],
"title": "EMA Assessment (Night)",
"useNotifications": true
},
"schedule": {
"times": [
"19"
]
}
}
],
"eventsOutside": true,
"fill": false,
"listTimes": true,
"minimumSize": 0,
"repeatCovers": true,
"size": 1,
"type": 1,
"updateColumns": true,
"updateRows": true
}
Moved to #737
Is your feature request related to a problem? Please describe. We don't
― https://github.com/ChildMindInstitute/mindlogger-app-backend/issues/204#issuecomment-548123955
― https://github.com/ChildMindInstitute/mindlogger-admin/issues/19
Describe the solution you'd like As @henryrossiter suggested, we should define our requirements and choose and design components based on those requirements.
Describe alternatives you've considered To date, we've chosen components (here specifically dayspan-veutify@0.4.0) and worked around their formats.
Additional context Current relevant endpoints include:
GET /schedule
Response Body
PUT /applet/{id}/constraints
Related
37