5

I made a learning management system using Django REST as its backend app. The app has grown quite a bit, so has its user base, and more and more people are showing interest in it.

Something I have realized is that the state-of-art LMS applications have something in common: modularity--people can develop plug-ins, extensions, and easily add features. My app is monolithic and doesn't really allow that, so I'm considering reworking the code base to be more open to extension; especially, I'm trying to fit in a plug-in system.

How would you approach something like that, in Django? Even some general thoughts, not necessarily Django- or Python-specific are very welcome.

I'll give an example of a feature that would need to move from being hard-coded in the core to being something pluggable.

Consider the following model:

class Exercise(TimestampableModel, OrderableModel, LockableModel):
    """
    An Exercise represents a question, coding problem, or other element that can appear inside of an exam.
    """

    MULTIPLE_CHOICE = 1
    OPEN_ANSWER = 2
    CLOZE = 3
    PROGRAMMING = 4

    EXERCISE_TYPES = (
        (MULTIPLE_CHOICE "Multiple choice"),
        (OPEN_ANSWER, "Open answer"),
        (CLOZE, "Cloze"),
        (PROGRAMMING, "Programming exercise"),
    )

    course = models.ForeignKey(
        Course,
        on_delete=models.PROTECT,
        related_name="exercises",
    )
    exercise_type = models.PositiveSmallIntegerField(choices=EXERCISE_TYPES)
    name = models.CharField(max_length=75, blank=True)
    text = models.TextField(blank=True)

Right now, the types of exercises are hard-coded in a field with choices. The Exercise model has some properties and methods that allow things like getting the max attainable score for that exercise or to grade an answer to that exercise. Those methods contain conditionals based on the exercise_type. For example:

def get_max_score(self):
        if self.exercise_type == Exercise.PROGRAMMING:
            return self.testcases.count()
        if self.exercise_type == Exercise.MULTIPLE_CHOICE:
                max_score = (self.choices.all().aggregate(Max("correctness")))[
                    "correctness__max"
                ]
            return max_score
        # ...

Now, let's say I wanted to make programming exercises into a plug-in.

How would you approach this?

The first thing I thought of is there would need to be a layer of indirection for getting the exercise types that are available. Instead of checking for membership to a set of choices for the exercise_type field at db level, the value would have to be validated at run time and follow some convention that tells my app to search for that type inside of a plug-in, so for example there would be some load_exercise_types function somewhere.

Then the business logic would have to be moved to a place that can dynamically call into the correct code for the exercise type.

For example, I could create an ExerciseBusinessLogic abstract base class with a static method from_model_instance---plug-in developers would subclass it and implement the relevant methods (like the get_max_score I showed above), and the model instance would take care of instantiating the correct subclass based on the exercise type.

I took a look at this article which shows a proof of concept about how to implement a simple plug-in system in Django, but it seems limited in what such a plug-in can do; here I'm looking for more complex solutions that would allow adding models, extending existing models with new functionalities, and expanding on the already present core business logic enabling more actions that integrate with whatever else is in the app.

How would you approach this problem?

Samuele B.
  • 481
  • 1
  • 6
  • 29
  • Are you asking how to design a state-of-the-art application without any prior experience? – Florin C. Sep 24 '22 at 10:42
  • Depends on what you mean by no prior experience. The LMS that I designed is used by a couple thousands users as of now, and it's not the first system I design. I'm asking about how, at high level, such an architecture could be extended to support plug-ins – Samuele B. Sep 24 '22 at 11:05
  • The high-level solution is easy: define what behaviour you want to keep stable, implement that, and then describe what behaviour you want to be *pluggable*, and write an interface for it. – Florin C. Sep 24 '22 at 11:18
  • As you might've been able to infer from my post (which, among other things, includes snippets of actual code), I am not discussing things at such a high level. "Write an interface for it" means nothing to me in this context; here we're spanning models, urls, entry points, business logic, and much more. How do you "write an interface" for a Django model? – Samuele B. Sep 24 '22 at 12:31
  • 1
    Not sure why you're investing your time and energy trying to play smartass on SO, but you should know that Python doesn't have interfaces, and certainly Django doesn't. And since my question was pretty idiomatic about Django, I highlighted the nonsensical "write an interface" comment by rhetorically asking how you'd do it with a Django model. If anything, you should be facepalming your own comment – Samuele B. Sep 24 '22 at 19:51

2 Answers2

1

It depends on whether a plugin has only logic (and can work with the same persistent data structure of the base system) or needs to add its own persistent fields.

Logic-only plug-ins

Exercise would define an API that any exercise type plug-in needs to adhere to. For instance, that API would require a get_max_score method with a certain signature and meaning.

  • A plug-in then is a class that can live anywhere.
  • The plug-in user adds that class to an EXERCISE_TYPES Django setting.
  • Exercise reads that setting and collects the type each of them declares.
  • When such a plugged-in type is needed, Exercise instantiates it and calls its functions by delegation.

Alternatively, such plug-in types could simply be subclasses of Exercise.

Logic-plus-persistent-fields plug-ins

This gets more involved. The simplest approach, I think, would be that the plug-in declares its own model M for all additional fields it requires and Exercise uses a GenericForeignKey to store a one-to-one relationship with an M instance. Exercise could then call the appropriate load/store logic as needed.

Lutz Prechelt
  • 36,608
  • 11
  • 63
  • 88
0

Essentially what OP is looking for is How to write reusable apps. That page from the Django doc will guide one in

turning our web-poll into a standalone Python package you can reuse in new projects and share with other people.

If OP wants to learn more about turning a Django app into a package, consider also reading the article Making Your Installable Django App.

Gonçalo Peres
  • 11,752
  • 3
  • 54
  • 83
  • Not really. A reusable app is something you can re-use in any general context, like a "mixin" that adds some functionality that is agnostic with respect to the app it's being used inside of. What I want here is to be able to design plugins specifically for my application, that extend *my* code base and only that. And I'm asking as much about how to make those app as I'm asking about how to design the overall plugin system – Samuele B. Sep 20 '22 at 19:37
  • @SamueleB. If it plugs with others it is able to plug and extend it. The difference is the access. – Gonçalo Peres Sep 20 '22 at 20:54
  • Then, with regards to how to design, the two different links explain how they did it. So both of your concerns are addressed in my answer. – Gonçalo Peres Sep 20 '22 at 21:01