Adding pluggable features to existing apps - Gamification case

I have a medium size Django REST app that I’m looking to add gamification features to.

The application in question is a school webapp where students can create mockup quizzes, participate in exams that teachers publish, and write didactical content that gets voted by other students and teachers.

I want to add some gamification features to make the app more interesting and to incentivize participation and usage of the various features: for example, each student will have a personal “reputation” score, and gain points upon completing certain actions–a student may gain points when completing a quiz with a high score, when submitting some content, or when receiving upvotes to such content.

The tricky part is I want to be able to have this logic be as separate as possible from the existing codebase, for various reasons: separation of concerns, ability to plug the engine in/out if needed, ability to easily deactivate features for certain groups of users, etc.

What I’m looking for here is some software engineering advice that’s also Django-specific. Here’s a high level description of what I’m thinking of doing–I’d like some advice on the approach.

  • create a new gamification app. Here I will have models that describe a change in reputation for a user and possibly other related events. The app should also send notifications when gamification-related events occur
  • from the gamification app, expose a callback-based interface, which the other primary app can call into to dispatch events
  • use the django-lifecycle package to call the callbacks from gamification when triggers occur.

This way, my existing models would only get touched to register the triggers from django-lifecycle (similar to signals). For example, let’s say I want to give students points when they turn in an assignment. Let’s say I have an AssignmentSubmission model to handle assignment submissions. With the added lifecycle hook, it’d look like this:

class AssignmentSubmission(models.Model):
    NOT_TURNED_IN = 0
    TURNED_IN = 1
    STATES = ((NOT_TURNED_IN, 'NOT_TURNED_IN'), (TURNED_IN, 'TURNED_IN'))

    user = models.ForeignKey(user)
    assignment = models.ForeignKey(assignment)
    state = models.PositiveSmallIntegerField(choices=STATES, default=NOT_TURNED_IN)

    @hook(AFTER_UPDATE, when="state", was=NOT_TURNED_IN, is_now=TURNED_IN)
     def on_turn_in(self):
        get_gamification_interface().on_assignment_turn_in(self.user)

The on_assignment_turn_in method might look something like:

def on_assignment_turn_in(user):
    ReputationIncrease.objects.create(user, points=50)
    notifications.notify(user, "You gained 50 points")

This is pretty much just a sketch to give an idea.

I am unsure how get_gamification_interface() would work. Should it return a singleton? Maybe instantiate an object? Or return a class with static methods? I think it’d be best to have a getter like this as opposed to manually importing methods from the gamification app, but maybe it could also create too much overhead.

What’s a good way to handle adding “pluggable” features to a project that are inherently entangled with existing models and business logic while also touching those as little as possible?