Model-based Criteria Subtypes

Model-based Criteria Subtypes

Overview

In this approach, each criterion type is represented as its own Django model, inheriting from a shared base class. These models define the fields required for their evaluation (such as a number of days, grade, etc) and include a method to return matching users. Evaluation is done by calling each model's method during group processing.

This structure allows clear separation between criterion types and their usage, and relies on Django's ORM relationships to manage them. New types are introduced by creating new models and registering them so the system can discover and evaluate them when needed.

This design is inspired by model extension patterns introduced in openedx-learning for content extensibility.

Goals

The model-based approach allows each criterion type to be defined as its own Django model. This could help us:

  • Keep each criterion self-contained, with its own fields and logic

  • Make the system easier to extend by letting developers add new criterion types as new models

  • Handle complex inputs (like lists or structured values) more cleanly

  • Reuse Django features like admin, forms, and validations

  • Improve clarity by making each criterion easy to inspect and reuse logic

The goal here is to explore whether this structure helps keep the system clean and maintainable as it grows.

Model Structure Overview

This section shows the structure of the model-based approach for user group criteria. It includes a high-level diagram and explains how the main parts connect to support extension, evaluation, and configuration.

Diagram

UserGrouping-Option_ Criterion Model Based.drawio(4).png

Core Components

In the model-based approach, each criterion type is a separate Django model. A UserGrouping connects to Criterion entries, each linking to criteria models (e.g., LastLoginCriterion) that define its data and logic. A runtime registry tracks these types, and a central evaluator uses them to compute group membership.

UserGrouping
Represents a named user group (e.g., "Low Progress Students"). Stores metadata such as name, creation date, and update strategy. Each group links to one or more Criterion entries that define who belongs to the group.

Criterion
Acts as the connector between a user group and the criteria used to determine membership. Each Criterion includes shared fields like name, description, etc. It links to one criterion type model (e.g., LastLoginCriterion or CourseProgressCriterion) through a 1:1 relationship. Be aware of unnecessary overhead with the model, so drop if needed.

Criterion Type Models (e.g., LastLoginCriterion, CourseProgressCriterion)
Each criterion type is implemented as its own model. These models define the criterion-specific fields (e.g., number of days or percentage), the operator to apply (e.g., >, <, <>, in, not in, exists,not exists), and any custom validation or evaluation logic. These models are loaded at runtime and used to determine which users match the criterion.

Extension Workflow

Here's what the extension process should look like, based on how the system is expected to work:

  1. Create a model
    Define the criterion model in your plugin. This model will store any fields needed to configure the criteria (e.g., days since last login, enrollment track, etc).

  2. Define the evaluator logic
    Write the logic that will return the list of matching users based on the model's data. This method should be part of the model definition.

  3. Register the new type
    Use the registry to register the new criterion so it becomes available to the system. This lets it show up in admin forms, the UI, etc. In this case, only defining the model will be enough for the plugin system to discover it.

  4. Install the plugin
    Install the plugin where your new criterion is defined.

  5. Run migrations
    Generate and apply the migration for the new model.

Evaluation Workflow

Here's what the evaluation process should look like, based on how the system is expected to work:

  1. Start Evaluation
    The system begins evaluating a specific user group, usually triggered by a refresh request or schedule.

  2. Retrieve User Group and Criteria
    It fetches the UserGrouping and all associated Criterion entries (regardless of their type).

  3. Resolve Each Criterion Type
    Each Criterion instance is linked to a concrete criterion type (e.g., LastLoginCriterion). These criterion models define both the configuration fields and the evaluation logic.

  4. Evaluate Each Criterion
    The evaluator for each criterion type returns a list of user IDs that match the criteria defined by that model (e.g., last login > 10 days). The system calls each model's evaluation method (e.g., get_matching_user_ids()), which returns a set of user IDs that meet the condition.

  5. Combine Results
    Included users are intersected (AND/OR logic). This step is where all the criteria are composed into a single logical predicate. The system ensures that only users who satisfy all included criteria are part of the group.

  6. Update Group Membership
    The resulting list of user IDs is used to update the group membership in the system, replacing the previous state with the newly computed one.

The main difference with other approaches is how each criterion type is accessed, and how the Criterion is used to reference it. In this model, evaluators and values (e.g., “10 days”) are stored in the criterion type itself, making the evaluation process specific to each model.

Pros & Cons

This section highlights the benefits and trade-offs of using a model for each criterion type. While it supports easy extensions and strong validation, it introduces complexity in querying and maintenance over time.

Pros

Advantage

Description

Easy to extend

Developers can extend functionality simply by creating a new Django model subclass with its own fields and evaluation method.

Encapsulated logic and validation

Each criterion stores its data, evaluation, and validation in one place, making it easier to manage and debug.

Strong auditability

Every criteria has its own table and ID, which helps with tracking, debugging, and admin tooling.

Supports advanced use cases

Different criteria can store their own different kinds of data, including lists, ranges, or structured inputs.

Criteria Type responsibility independence

The responsibility of each criterion is of the models, while each group criterion manages the usage of the model (less coupling).

Cons

Drawback

Description / Risk

Operational overhead

Each new criterion requires a model and a migration. Even small changes involve versioning and review, which slows down iteration and increases maintenance effort.

Migrations required per type

Even simple criterias need new models and migrations, adding setup cost for small additions.

Querying is harder

Fetching and evaluating criteria across multiple models requires a more complex implementation that may be more difficult implement and debug.

Plugins must be available

If a plugin with a criterion is uninstalled, related data may become inaccessible or cause errors to the entire instance. So a fallback mechanism should be implemented.

UI must support custom fields

Since each type can define different inputs, the frontend must support dynamic forms and flexible validation.

Overhead for simple criterias

Creating a whole model and migration may be excessive for criterias that only require a basic value or comparison.

Open Q's

  • Should Criterion be reused across multiple user groups?

  • How can we get all criteria from a group? What do we need?

  • Should this kind of data be persistent only for validations? Is this too strict and inflexible?