Flows Documentation¶
Initalization¶
When a user loads the home timetable page, FeatureFlowView
inside of
timetable.utils
is used to handle the request. On initial page load,
the frontend requires some data to initialize the redux state, like
information about the current user, the list of possible semesters for the
school, and the list of student integrations. This initial data is created
inside of the view, and passed in as a single json string in the response
context:
class FeatureFlowView(ValidateSubdomainMixin, APIView): def get(self, request, *args, **kwargs): # ...gather values for init_data init_data = { 'school': self.school, 'currentUser': get_user_dict(self.school, self.student, sem), 'currentSemester': curr_sem_index, 'allSemesters': all_semesters, 'uses12HrTime': self.school in AM_PM_SCHOOLS, 'studentIntegrations': integrations, 'examSupportedSemesters': map(all_semesters.index, final_exams_available.get(self.school, [])), 'featureFlow': dict(feature_flow, name=self.feature_name) } return render(request, 'timetable.html', {'init_data': json.dumps(init_data)})
which makes the init_data
variable accessible in timetable.html. This dumped json string is
then passed to the frontend as a global variable:
<script type="text/javascript"> var initData = "{{init_data|escapejs}}"; </script>
And then parsed inside of the setup()
function in init.jsx
const setup = () => (dispatch) => { initData = JSON.parse(initData); // pass init data into the redux state dispatch({ type: ActionTypes.INIT_STATE, data: initData }); // do other logic with initData... };
In other words, the data that the frontend requires is retrieved/calculated inside
of FeatureFlowView
, and then passed to the frontend as global variable initData
. The frontend
then does any logic it needs based on that data inside of setup()
in init.jsx
. Any
data that needs to be reused later on from initData
should be passed in to the redux state so
that the only global variable uses appear in setup()
.
Feature Flows¶
One such piece of data that is passed to the frontend is a featureFlow
object. This object is
obtained as the return value of .get_feature_flow()
, in addition to a name: self.feature_name
key value pair. In the default implementation, this is just the dictionary {name: None}
:
class FeatureFlowView(ValidateSubdomainMixin, APIView): feature_name = None def get_feature_flow(self, request, *args, **kwargs): return {} def get(self, request, *args, **kwargs): ... feature_flow = self.get_feature_flow(request, *args, **kwargs) init_data = { ... 'featureFlow': dict(feature_flow, name=self.feature_name) } return render(request, 'timetable.html', {'init_data': json.dumps(init_data)})
This feature flow value can be used to store any extra information that the frontend needs for any
endpoints that would require initial data to be loaded. For example, when loading a timetable share
link, the frontend also needs to get data about the timetable that is being shared - instead of making
a request to the backend after page load, this information can be provided by the backend directly
by passing this information in the feature flow. It is easy to write new views that pass different
data and have custom logic by subclassing FeatureFlowView
and overwriting the
get_feature_flow()
method and the .feature_name
class attribute.
Having this data all stored under the key
featureFlow
in init_data
ensures two things. Firstly, it makes explicit that there can only
be one feature flow in play at a time (we can’t load a timetable share link and a course share link
at the same time), and secondly, it allows the frontend to know where to look for any feature data
and act accordingly. In practice, this is done by switching on the name of the feature flow:
const setup = () => (dispatch) => { initData = JSON.parse(initData); dispatch({ type: ActionTypes.INIT_STATE, data: initData }); // do other logic with initData... dispatch(handleFlows(initData.featureFlow)); }; const handleFlows = featureFlow => (dispatch) => { switch (featureFlow.name) { case 'SIGNUP': dispatch(signupModalActions.showSignupModal()); break; case 'USER_ACQ': dispatch(userAcquisitionModalActions.triggerAcquisitionModal()); break; case 'SHARE_TIMETABLE': dispatch(timetablesActions.cachedTimetableLoaded()); dispatch(lockTimetable(featureFlow.sharedTimetable, true, initData.currentUser.isLoggedIn)); break; // ... etc. default: // unexpected feature name break; } };
Example¶
To help understand how feature flows work, let’s go through the code for an example feature flow: course sharing. In order to implement course sharing, we want to create a new view/endpoint that retrieves course data based on the url and passes it to the frontend, which would then update the redux state and dispatch an action to open the course modal.
We start be defining a new endpoint for this feature flow:
re_path(r'course/(?P<code>.+?)/(?P<sem_name>.+?)/(?P<year>.+?)/*$', courses.views.CourseModal.as_view())
Then we create a new FeatureFlowView
for this endpoint which needs to do two things: define
a name for the feature flow, which the frontend look at to determine what action to do, and return
the course data that the frontend needs inside of get_feature_flow()
:
class CourseModal(FeatureFlowView): feature_name = "SHARE_COURSE" def get_feature_flow(self, request, code, sem_name, year): semester, _ = Semester.objects.get_or_create(name=sem_name, year=year) code = code.upper() course = get_object_or_404(Course, school=self.school, code=code) course_json = get_detailed_course_json(self.school, course, semester, self.student) # analytics SharedCourseView.objects.create( student=self.student, shared_course=course, ).save() return {'sharedCourse': course_json, 'semester': semester}
The frontend can now add a new case in handleFlows
to perform logic for this feature flow:
const handleFlows = featureFlow => (dispatch) => { switch (featureFlow.name) { ... case 'SHARE_COURSE': dispatch(setCourseInfo(featureFlow.sharedCourse)); dispatch(fetchCourseClassmates(featureFlow.sharedCourse.id)); break; // ... etc. default: // unexpected feature name break; } };
Shortcuts¶
Some feature flows don’t require any extra data - they simply require the frontend to know that
a feature flow is being run. For example, for the signup feature flow, loading the page at
/signup
should simply open the signup modal, which requires no extra logic or data other than
knowing that it should occur. We could do this by writing a new view:
class SignupModal(FeatureFlowView): feature_name = "SIGNUP"
We do not need to implement .get_feature_flow()
since the frontend doesn’t require any extra
data and the default implementation already returns an empty dictionary. We can simplify this
by simply declaring this view directly inside of the urls file:
re_path(r'^signup/*$/', FeatureFlowView.as_view(feature_name='SIGNUP')
see https://github.com/noahpresler/semesterly/pull/838 for the original pull request implementing feature flows