Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

βœ¨πŸ“šπŸ“… Define schedule formats #39

Closed
shnizzedy opened this issue Oct 31, 2019 · 10 comments
Closed

βœ¨πŸ“šπŸ“… Define schedule formats #39

shnizzedy opened this issue Oct 31, 2019 · 10 comments
Assignees
Labels
documentation Improvements or additions to documentation MindLoggerbugreports question Further information is requested

Comments

@shnizzedy
Copy link
Member

shnizzedy commented Oct 31, 2019

Is your feature request related to a problem? Please describe.
We don't

I don't have a good grasp of the format of stored scheduling data

― ChildMindInstitute/mindlogger-backend#204 (comment)

The dayspan-vuetify calendar library doesn't support Vuetify 2. We should eventually find a new calendar component so we can update Vuetify.

― ChildMindInstitute/mindlogger-admin#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 [email protected]) and worked around their formats.

Additional context
Current relevant endpoints include:

GET /schedule

Get schedule Array for the logged-in user

Parameter Description Parameter Type Data Type
timezone TheΒ TZ database nameΒ of the timezone to return times in. Times returned in UTC if omitted. query string

Response Body

{
  "applet/5d6ee74a8f66701eb8166011": {
    "activity/5db9bac59f43a2fb26fdf425": {
      "lastResponse": "2019-10-28T16:46:09.281000"
    }
  },
  "applet/5db71b412611b7876bb7a220": {
    "activity/5db9bba49f43a2fb26fdf484": {
      "lastResponse": null
    },
    "activity/5db9bba59f43a2fb26fdf485": {
      "lastResponse": null
    }
  }
}

PUT /applet/{id}/constraints

Set or update schedule information for an activity.

Parameter Description Parameter Type Data Type
id The ID of the document. path string
activity Girder ID (or Array thereof) of the activity/activities to schedule. query string
schedule A JSON object containing schedule information for an activity formData string

Related

@shnizzedy shnizzedy added documentation Improvements or additions to documentation question Further information is requested labels Oct 31, 2019
@shnizzedy
Copy link
Member Author

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.

@binarybottle
Copy link
Member

Can anyone think of what the advantages would be of having a freeform json object as a schedule parameter?

@curtpw
Copy link

curtpw commented Oct 31, 2019

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.

@shnizzedy
Copy link
Member Author

@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 ChildMindInstitute/mindlogger-backend#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.

@shnizzedy
Copy link
Member Author

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.

@shnizzedy
Copy link
Member Author

related: ReproNim/reproschema#46

@shnizzedy
Copy link
Member Author

In this morning's standup, @odziem, @binarybottle, @henryrossiter, @hotavocado, and I discussed how to handle timezones.

  • For now, we're sticking with timezone-encoded dayspan Objects.
  • We'll eventually need to allow editors / managers / coordinators / users to specify whether specific schedules are timezone sensitive (eg, sleep times, safety check-ins) or absolute (eg, insulin injections).
  • I vote for someday migrating to UTC-encoded timestamps and storing a tz database timezone string like pytz's recommendation

@shnizzedy
Copy link
Member Author

shnizzedy commented Jan 16, 2020

Here are some in-use-at-the-moment snippets to help until we document what we want:

https://github.com/ChildMindInstitute/mindlogger-admin/blob/97ed9fd8d9f41969c04934ee5314ae6bdbc1b256/src/Components/Utils/api/api.vue#L22-L29

const setSchedule = ({ apiHost, token, id, data }) => axios({
  method: 'put',
  url: `${apiHost}/${id}/constraints`,
  headers: {
    'Girder-Token': token,
  },
  data,
});

https://github.com/ChildMindInstitute/mindlogger-admin/blob/1d136a8b4ee0448a23862b6085d638493af41c35/src/Steps/SetSchedule.vue#L143-L168

    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;
          });
        }
    },

https://github.com/ChildMindInstitute/mindlogger-admin/blob/f0681f7319db526d317324962def6b4547ab8907/src/State/state.js#L35-L47

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

https://github.com/ChildMindInstitute/mindlogger-app/blob/33e5511d750eed23bc9fd77ee6cf86b85dcf2e85/app/state/applets/applets.selectors.js#L12-L104

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,
    };
  }),
);

https://github.com/ChildMindInstitute/mindlogger-app/blob/26061c7fa06e747125e24f0020f9333b447c8002/app/services/pushNotifications.js#L39-L98

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

https://github.com/ChildMindInstitute/mindlogger-app-backend/blob/76abb94e8214449e1bf19e6fd3718e603631b15f/girderformindlogger/api/v1/applet.py#L425-L438

    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)

@shnizzedy
Copy link
Member Author

For applet 5e1f4f47720011229c1a15d2 on https://api.mindlogger.org/api/v1, my attempt to manually set a schedule β…“ worked. I realized ChildMindInstitute/mindlogger-admin#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
}

@WorldImpex
Copy link
Contributor

Moved to #737

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation Improvements or additions to documentation MindLoggerbugreports question Further information is requested
Projects
None yet
Development

No branches or pull requests

4 participants