Skip to main content
Program modules are the core extensibility mechanism in ESP Website. Each module provides a specific feature or workflow, such as student registration, teacher class creation, or admin scheduling.

What Are Program Modules?

A program module is a self-contained component that:
  • Handles a specific workflow (e.g., student registration)
  • Provides one or more views (web pages)
  • Can be enabled/disabled per program
  • Has configurable settings and requirements
  • Follows a consistent architecture
Examples:
  • StudentRegCore - Main student registration page
  • TeacherClassRegModule - Teacher class creation
  • AdminCore - Admin dashboard
  • OnSiteCheckinModule - Student check-in

Module Architecture

Components

Each module consists of:
  1. Handler Class (esp/esp/program/modules/handlers/{name}module.py)
    • Python class inheriting from ProgramModuleObj
    • Defines views, permissions, and configuration
  2. Templates (esp/templates/program/modules/{name}/)
    • HTML templates for the module’s views
  3. Database Record (ProgramModuleObj)
    • Links the module to a specific program
    • Stores configuration (sequence, required, etc.)

File Structure

esp/esp/program/modules/
├── base.py              # Base classes
├── module_ext.py        # Module registration
├── handlers/            # Module implementations
│   ├── studentregcore.py
│   ├── teacherclassregmodule.py
│   └── ... (70+ modules)
├── forms/               # Module-specific forms
└── templates/           # Module-specific templates

esp/templates/program/modules/
├── studentregcore/
│   └── student_reg.html
├── teacherclassregmodule/
│   └── classreg.html
└── ...

Creating a Module

Step 1: Create the Handler Class

Create a file in esp/esp/program/modules/handlers/:
# esp/esp/program/modules/handlers/mymodule.py

from esp.program.modules.base import ProgramModuleObj, needs_student, \
    main_call, aux_call
from esp.utils.web import render_to_response

class MyModule(ProgramModuleObj):
    """Brief description of what this module does."""
    
    # Module metadata
    @classmethod
    def module_properties(cls):
        return {
            'admin_title': 'My Module',
            'link_title': 'My Feature',
            'module_type': 'learn',  # learn, teach, manage, onsite, volunteer
            'seq': 10,  # Display order
            'choosable': 1,  # Can be selected in admin
        }
    
    # Main view (accessed at /{module_type}/{program}/{instance}/mymodule/)
    @main_call
    @needs_student
    def my_main_view(self, request, tl, one, two, module, extra, prog):
        """Main module view."""
        context = {
            'program': prog,
            'user': request.user,
        }
        return render_to_response(
            'program/modules/mymodule/main.html',
            request,
            context
        )
    
    # Auxiliary view (accessed at /{module_type}/{program}/{instance}/mymodule_aux/)
    @aux_call
    @needs_student
    def my_aux_view(self, request, tl, one, two, module, extra, prog):
        """Auxiliary view for additional functionality."""
        # Implementation
        pass
    
    # Check if user has completed this module
    def isCompleted(self):
        """Return True if user has completed required steps."""
        # Check if user has submitted required data
        return MyModuleData.objects.filter(
            user=self.user,
            program=self.program
        ).exists()

# Register the module
from esp.program.modules.module_ext import DBReceipt
MyModule.register()

Step 2: Create Templates

Create templates in esp/templates/program/modules/mymodule/:
{# esp/templates/program/modules/mymodule/main.html #}
{% extends "program/base.html" %}

{% block content %}
<h1>My Module</h1>

<p>Welcome, {{ user.first_name }}!</p>

{# Your module UI here #}

{% endblock %}

Step 3: Register the Module

The module is automatically registered when imported. Ensure your module file is imported in esp/esp/program/modules/module_ext.py or included in the handlers directory.

Step 4: Enable in Admin

Navigate to /admin/program/program/, select your program, and add your module to the “Program modules” field.

Module Properties

The module_properties() method returns a dictionary with:
admin_title
string
required
Title shown in Django admin
Title shown to users in navigation
module_type
string
required
Module category: learn, teach, manage, onsite, volunteer, or json
seq
integer
default:10
Display order (modules shown in ascending order)
choosable
integer
default:0
Whether module can be selected in admin (1 = yes, 0 = no)
required
boolean
default:false
Whether module is required for all programs
required_label
string
Custom requirement text (e.g., “Required for outside teachers”)

View Decorators

Modules use decorators to define views and enforce permissions:

Main Call vs Aux Call

Defines the primary view accessed at:
/{module_type}/{program}/{instance}/{handler_name}/
Example: /learn/Splash/2024/studentreg/

Permission Decorators

Requires user to be a student (in the Student group).
@main_call
@needs_student
def student_view(self, request, tl, one, two, module, extra, prog):
    # Only students can access
Requires user to be a teacher.
@main_call
@needs_teacher
def teacher_view(self, request, tl, one, two, module, extra, prog):
    # Only teachers can access
Requires user to have admin permission for the program.
@main_call
@needs_admin
def admin_view(self, request, tl, one, two, module, extra, prog):
    # Only administrators can access
Requires user to have onsite permission.
@main_call
@needs_onsite
def onsite_view(self, request, tl, one, two, module, extra, prog):
    # Only onsite staff can access
Requires a deadline to be open.
@aux_call
@needs_student
@meets_deadline('/Registration/Start')
def register(self, request, tl, one, two, module, extra, prog):
    # Only accessible after registration deadline

View Method Signature

All module view methods receive these parameters:
def view_method(self, request, tl, one, two, module, extra, prog):
    """
    Args:
        request: HttpRequest object
        tl: Module type ('learn', 'teach', etc.)
        one: Program URL (e.g., 'Splash')
        two: Program instance (e.g., '2024')
        module: Module handler name
        extra: Additional URL path components
        prog: Program object
    """
    pass
Common Usage:
def my_view(self, request, tl, one, two, module, extra, prog):
    user = request.user
    program = prog
    
    # self.user and self.program are also available
    assert user == self.user
    assert program == self.program

Module State Methods

Modules can track user completion:

isCompleted()

def isCompleted(self):
    """Return True if user has completed this module."""
    # Example: Check if profile exists
    from esp.program.models import RegistrationProfile
    return RegistrationProfile.objects.filter(
        user=self.user,
        program=self.program
    ).exists()

students()

def students(self, QObjects=False):
    """Return students who have completed this module."""
    # Example: Students with profiles
    from esp.program.models import RegistrationProfile
    profiles = RegistrationProfile.objects.filter(program=self.program)
    
    if QObjects:
        # Return Q object for filtering
        from django.db.models import Q
        return Q(id__in=profiles.values_list('user', flat=True))
    else:
        # Return user queryset
        from esp.users.models import ESPUser
        return ESPUser.objects.filter(
            id__in=profiles.values_list('user', flat=True)
        )

teachers()

Same as students() but for teachers.

Configuration via Tags

Modules can use the Tag system for configuration:
from esp.tagdict.models import Tag

def my_view(self, request, tl, one, two, module, extra, prog):
    # Get a module-specific setting
    max_items = Tag.getProgramTag('mymodule_max_items', prog, default='5')
    max_items = int(max_items)
    
    # Get a boolean flag
    require_confirmation = Tag.getBooleanTag(
        'mymodule_require_confirmation',
        prog,
        default=False
    )
Administrators can set tags at /admin/tagdict/tag/.

Database Models

If your module needs to store data, create a model:
# esp/esp/program/modules/models.py (or create a separate file)

from django.db import models
from esp.program.models import Program
from esp.users.models import ESPUser

class MyModuleData(models.Model):
    """Data collected by MyModule."""
    user = models.ForeignKey(ESPUser, on_delete=models.CASCADE)
    program = models.ForeignKey(Program, on_delete=models.CASCADE)
    data_field = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)
    
    class Meta:
        unique_together = ('user', 'program')
Then create and run a migration:
python esp/manage.py makemigrations program
python esp/manage.py migrate

Forms

Modules often use Django forms:
# esp/esp/program/modules/forms/mymodule.py

from django import forms

class MyModuleForm(forms.Form):
    field1 = forms.CharField(max_length=100)
    field2 = forms.IntegerField()
    field3 = forms.BooleanField(required=False)
Use in view:
from esp.program.modules.forms.mymodule import MyModuleForm

def my_view(self, request, tl, one, two, module, extra, prog):
    if request.method == 'POST':
        form = MyModuleForm(request.POST)
        if form.is_valid():
            # Process form data
            pass
    else:
        form = MyModuleForm()
    
    context = {'form': form}
    return render_to_response('...', request, context)

Testing Modules

Create tests in esp/esp/program/modules/tests/:
from esp.program.modules.tests import ProgramModuleTest

class MyModuleTest(ProgramModuleTest):
    def test_main_view(self):
        """Test that the main view loads."""
        # Create a student user
        student = self.create_student()
        self.client.login(username=student.username, password='password')
        
        # Access the module
        url = f'/learn/{self.program.url}/{self.program.name}/mymodule/'
        response = self.client.get(url)
        
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, 'My Module')
    
    def test_isCompleted(self):
        """Test completion tracking."""
        student = self.create_student()
        module = self.get_module_instance('MyModule', student)
        
        # Should not be completed initially
        self.assertFalse(module.isCompleted())
        
        # Create data
        MyModuleData.objects.create(
            user=student,
            program=self.program,
            data_field='test'
        )
        
        # Should now be completed
        self.assertTrue(module.isCompleted())

Common Patterns

Redirect After Success

from django.http import HttpResponseRedirect
from django.urls import reverse

def my_view(self, request, tl, one, two, module, extra, prog):
    if request.method == 'POST':
        # Process form
        if success:
            # Redirect to student reg main page
            return HttpResponseRedirect(
                f'/learn/{one}/{two}/studentreg/'
            )

Check Deadlines

from esp.program.models import Program

def my_view(self, request, tl, one, two, module, extra, prog):
    # Check if deadline is open
    if not prog.deadline_met('/Registration/Start'):
        return render_to_response(
            'errors/deadline_not_met.html',
            request,
            {'deadline_name': 'Student Registration'}
        )

Access Control

from esp.users.models import Permission

def my_view(self, request, tl, one, two, module, extra, prog):
    # Custom permission check
    if not Permission.user_has_perm(request.user, 'V/Administer', prog):
        return render_to_response('errors/need_auth.html', request, {})

Caching Expensive Queries

from argcache import cache_function

@cache_function
def get_available_classes(program_id):
    """Get all available classes (cached)."""
    from esp.program.models import ClassSubject
    return ClassSubject.objects.filter(
        parent_program_id=program_id,
        status__gt=0
    )

def my_view(self, request, tl, one, two, module, extra, prog):
    classes = get_available_classes(prog.id)
See the Cache documentation for details.

Example: Complete Module

Here’s a complete example of a simple module:
# esp/esp/program/modules/handlers/studentnotemodule.py

from django import forms
from esp.program.modules.base import ProgramModuleObj, needs_student, \
    main_call
from esp.utils.web import render_to_response
from django.db import models
from esp.users.models import ESPUser
from esp.program.models import Program

# Model
class StudentNote(models.Model):
    user = models.ForeignKey(ESPUser, on_delete=models.CASCADE)
    program = models.ForeignKey(Program, on_delete=models.CASCADE)
    note = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)
    
    class Meta:
        unique_together = ('user', 'program')

# Form
class StudentNoteForm(forms.Form):
    note = forms.CharField(
        widget=forms.Textarea,
        label="Your note",
        help_text="Share your thoughts about the program."
    )

# Module
class StudentNoteModule(ProgramModuleObj):
    """Allow students to submit a note."""
    
    @classmethod
    def module_properties(cls):
        return {
            'admin_title': 'Student Notes',
            'link_title': 'Submit Note',
            'module_type': 'learn',
            'seq': 50,
            'choosable': 1,
        }
    
    @main_call
    @needs_student
    def note(self, request, tl, one, two, module, extra, prog):
        # Check if note already exists
        try:
            existing_note = StudentNote.objects.get(
                user=request.user,
                program=prog
            )
            initial_data = {'note': existing_note.note}
        except StudentNote.DoesNotExist:
            initial_data = {}
        
        if request.method == 'POST':
            form = StudentNoteForm(request.POST)
            if form.is_valid():
                # Save or update note
                note, created = StudentNote.objects.update_or_create(
                    user=request.user,
                    program=prog,
                    defaults={'note': form.cleaned_data['note']}
                )
                
                context = {
                    'program': prog,
                    'saved': True,
                }
                return render_to_response(
                    'program/modules/studentnotemodule/success.html',
                    request,
                    context
                )
        else:
            form = StudentNoteForm(initial=initial_data)
        
        context = {
            'program': prog,
            'form': form,
        }
        return render_to_response(
            'program/modules/studentnotemodule/note.html',
            request,
            context
        )
    
    def isCompleted(self):
        return StudentNote.objects.filter(
            user=self.user,
            program=self.program
        ).exists()

# Register
StudentNoteModule.register()

Next Steps

Module API Reference

Complete module system API

Student Modules

Reference for student modules

Teacher Modules

Reference for teacher modules

Admin Modules

Reference for admin modules

Build docs developers (and LLMs) love