Thank you for the tips you’ve given me.
I started working on this integration and I came up with a first draft of part of the architecture & implementation. If you’re willing to give me some feedback based on your experience, which is clearly greater than mine, I’d appreciate it a lot.
The integration framework I designed is composed of three main parts:
- remote twin resources
- integration classes & the registry
- controller classes
I’ll give a brief outline of what each one is and does.
Remote twin resouces
These are models that represent resources on third party services, (e.g. Classroom) which are paired with resources on my LMS.
Here’s the base abstract model: sai_evo_backend/models.py at classroom_integration · Evo-Learning-project/sai_evo_backend · GitHub
The base fields include an id to the remote resource and a json field to store additional data about the remote resource that could be useful. For example, when a Google Classroom course is paired with a course on my LMS, an additional piece of information that I’m interested in keeping is the permalink to the Classroom course, which may be displayed in the UI for the user’s convenience.
Subclasses of this base model add a foreign key to a resource on my LMS. For example, the GoogleClassroomCourseTwin
model is the key integration between a course on my platform and one on Classroom.
These models help with keeping track of what resources have a paired resource over at Classroom, and makes it easy to dispatch any actions related to my models, as well as reflect any updates/deletes on the remote resources
Integration classes & the registry
I created an abstract base class named BaseEvoIntegration
which contains a series of handler methods which can be called when certain actions happen: when an exam is published, when a participation to an exam is turned in, when a lesson is published, and so on.
The GoogleClassroonIntegration
subclass implements these methods in a specific way for Google Classroom. These integration classes know about the remote service they’re interacting with, about user credentials, and they are responsible for mainly dispatching actions that affect the remote Google Classroom resources, such as creating a coursework item on Classroom when an exam is published on my LMS.
Here’s an example handler method which is called when an exam is published on my LMS:
def on_exam_published(self, user: User, exam: Event):
course_id = self.get_classroom_course_id_from_evo_course(exam.course)
service = self.get_service(user)
exam_url = exam.get_absolute_url()
coursework_payload = get_assignment_payload(
title=exam.name,
description=messages.EXAM_PUBLISHED,
exam_url=exam_url,
)
results = (
service.courses()
.courseWork()
.create(
courseId=course_id,
body=coursework_payload,
)
.execute()
)
As I mentioned earlier in this thread, I wanted to use Django lifecycle hooks to dispatch actions, and I wanted it to be something that the models can just “fire and forget,” without knowing the details of any integrations. So, in order to decouple models from all the integration stuff, I created a registry class.
Here’s its dispatch
method:
def dispatch(self, action_name: str, course: "Course", **kwargs):
integrations = self.get_enabled_integrations_for(course)
# loop over all the integrations enabled for the given course, and
# for each of them, if the dispatched action is supported, schedule
# the corresponding handler to be run with the given arguments
for integration_cls in integrations:
integration = integration_cls()
# check the current integration supports the dispatched action
if action_name in integration.get_available_actions():
method = getattr(
integration, integration_cls.ACTION_HANDLER_PREFIX + action_name
)
self.schedule_integration_method_execution(method, **kwargs)
else:
logger.warning(
f"{str(integration_cls)} doesn't support action {action_name}"
)
For now, get_enabled_integrations_for
just checks whether there is a Classroom twin resource for the passed course in order to determine whether the Classoom integration is enabled for it—this is because that’s the only type of integration we have for now, of course.
action_name
is expected to be a string that’s the name of a method on the integration class minus the on_
prefix. So if I get an exam_published
action name, I’ll call a method named on_exam_published
on the integration class.
Controller classess
As I progressed with my implementation, I reliazed not all operations could be achieved via handlers that could be called from models at specific timings.
For example, all of the above methods assume a twin resource for a course could exist, but—how is it created? Another example is roster syncing: putting aside the pub-sub notifications from Classroom which are paid, if I wanted to go the free route, one way would be to periodically poll enrolled students from the Classroom API and create enrollment model instances for each one of them who isn’t already enrolled on my LMS. The key differences between these actions and the ones found on the integration classes are: (1) they aren’t called as handlers from lifecycle hooks—in fact, they can be called by the user itself via the REST API of my application, or they may be scheduled as periodic tasks, (2) they have a different set of responsabilities—they can create, update, and delete models on my application.
So I developed a controller class which exposes some methods that essentially combine primitives from the integration classes and also use methods of model managers and other pieces of business logic of my application. Some of these methods will be called in views of my REST API, others will be used as periodic tasks, and others could possibly be called in other ways.
Here’s an example from the controller class for Google Classroom:
def associate_evo_course_to_classroom_course(
self,
requesting_user: User,
course: Course,
classroom_course_id: str,
) -> GoogleClassroomCourseTwin:
# fetch Google Classroom course using given id
classroom_course = GoogleClassroomIntegration().get_course_by_id(
requesting_user,
classroom_course_id,
)
# create a twin resource that links the given course to the
# specified classroom course
twin_course = GoogleClassroomCourseTwin(
course=course,
remote_object_id=classroom_course_id,
)
twin_course.set_remote_object(classroom_course)
twin_course.save()
return twin_course
This will fetch the remote Classroom course and create a twin resource associating it to the specificed course. Notice the call to set_remote_object
: this is to supply the twin model with a dict representing the remote object, which will allow it to fill up the data
JSON field I mentioned earlier.
So this is a rough description of what I have in mind for now. There’s still a lot of missing details which I’ll have to work on, such as for example authentication and error handling.
Overall, how does it look so far?