Google Summer of Code 2026 - Final Report

Contributor: Supriyo Chowdhury (@Supriyo760)
Organization: Learning Unlimited
Repository: learning-unlimited/ESP-Website
Mentor: Will Gearty (@willgearty)
Program Duration: May - August 2026 (12 Weeks + Community Bonding)
Project Title: New Module Management UI 


Table of Contents

  1. Project Goals and Background
  2. The Proposed Timeline
  3. What I Did - A Week-by-Week Account
  4. The Current State of the Project
  5. What is Left to Do (Future Work)
  6. Challenges and Important Things I Learned
  7. Acknowledgements

1. Project Goals and Background

What is ESP-Website?

ESP-Website is the open-source Django web application that powers educational enrichment programs run by Learning Unlimited, a non-profit organization dedicated to making university-level educational programs accessible to high school students across the United States.

Universities like MIT, Stanford, Yale, and the University of Chicago all use this platform to run their own versions of these programs - MIT Splash, Stanford Splash, Yale Splash, and Chicago Spark, to name a few. These programs teach thousands of students every single year, with courses ranging from mathematics and computer science to music, creative writing, and philosophy, all taught by undergraduate and graduate student volunteers.

The ESP-Website platform handles every aspect of running these programs. It manages teacher registrations, where teachers propose classes they want to teach. It handles student registrations, where students sign up and select classes they want to attend. It manages payment processing so programs can collect financial aid information and application fees. It coordinates scheduling, so hundreds of classes are organized across rooms and timeslots. It coordinates volunteers, manages communications, and generates printable schedules, nametags, and reports. Every touchpoint between a program administrator and a student or teacher runs through this platform.

At the very center of this platform is a system called Program Modules. These are independently toggleable Django handler classes that administrators enable or disable on a per-program basis. Each module represents one step in the registration workflow that users must complete. By selecting which modules to enable for a given program, administrators assemble the exact registration experience they want - no two programs necessarily work the same way.

What Are Program Modules?

To understand why this GSoC project matters, it helps to understand exactly how Program Modules work. When a student visits a program page for the first time, the ESP-Website application looks up a list of enabled modules for that program and that user role (student, teacher, volunteer, or administrator). It then presents those modules in sequence, guiding the user through each required step.

For a student, this sequence might look like: complete your profile, acknowledge the Code of Conduct, fill out financial aid information, select your classes, and confirm your enrollment. For a teacher, the sequence might be: complete your profile, propose the classes you want to teach, log your availability, and acknowledge the instructor terms. Each of those steps is a separate module.

Each module is backed by a database record called a ProgramModuleObj. This record stores the per-program configuration for that module - things like the sequence number (seq), which determines the order in which it appears, a flag indicating whether the module is required (required), and a link title override (link_title) that changes how the module appears in navigation menus. And, after this GSoC project, it now also stores start and end dates (start_date, end_date) that define exactly when the module is open and accessible to users.

The Core Problems I Was Hired to Solve

When I began the community bonding period and started auditing the ESP-Website codebase, I found that the Program Module system had accumulated several structural limitations over its many years of development. These limitations were causing real, practical problems for program administrators running live programs every semester.

The first and most fundamental problem was that modules had no temporal dimension at all. A module was either enabled or disabled - there was no concept of scheduling a module to open at a specific date and time and automatically close at another. This was a serious operational problem. Imagine you are running MIT Splash. You want student class registration to open at exactly 10:00 AM on a Saturday morning for all enrolled students, and you want it to close at 11:59 PM on Sunday night. To do this, an administrator would have to manually log into the system and toggle each relevant module at exactly the right time. If they were late by even a few minutes, or if they toggled the wrong module, students could either be locked out of registration or find it open when it should not be. For large programs with dozens of modules, this was an operational nightmare and a constant source of stress.

The second major problem was that the permission system and the module system were completely disconnected. ESP-Website has a Permission model that stores deadlines for various operations - for example, a record that says "student registration is open from date X to date Y." But these permission records were maintained entirely separately from the module configuration. Changing a permission deadline did not affect whether the corresponding module was accessible, and changing a module's configuration did not update the corresponding permission record. Administrators had to update both systems separately, and over time, the two systems would inevitably drift out of sync with each other.

The third problem was the monolithic RegProfileModule handler. This single Python class handled both student and teacher registration profile flows inside one file, using a series of conditional checks based on the current user's role to decide what to display. This violated the well-known Single Responsibility Principle in software design, making the code harder to test, harder to reason about, and harder to extend. As the module permission system became increasingly important (with each module declaring which permission types it controls), having one handler that returned different permission types depending on who was asking became a structural liability.

The fourth problem was the absence of any visual scheduling interface. All module management was done through Django Admin, a general-purpose administrative interface that was never designed for scheduling. There was no timeline view, no visual representation of when modules would be open or closed, and no way to see at a glance how the temporal structure of a program's registration workflow was organized.

The fifth and final problem was that new programs launched with undated permissions. When an administrator created a new program through the program creation form, all of the permissions associated with that program started with no dates at all. This meant every permission was effectively open to all users immediately, the moment the program was created. Administrators had to manually configure start and end dates for every permission after program creation, and if they forgot even one, users could access things they should not be able to access.

My Project Goals

My GSoC proposal was designed to solve all five of these problems systematically, building from the most foundational changes first and working toward the most visible and user-facing changes last. The project was structured into eight phases spread across twelve weeks, with each phase building directly on the work completed in the previous one.

The goal was not just to patch individual problems. The goal was to deliver a unified, cohesive module scheduling system where time-windows, permission records, and the administrator's user interface all worked as a single integrated system. An administrator should be able to set a module's dates in one place and have that change automatically propagate everywhere it needed to go, without any manual duplication.


2. The Proposed Timeline

Before the coding period began, I worked with my mentor Will Gearty to produce a detailed week-by-week plan. This plan became the backbone of the entire project - each week had a clear goal, a defined set of deliverables, and a targeted pull request.

Week / Milestone Proposed Goal Key Deliverables Associated PR / Outcome
Community Bonding Architecture audit & setup Deep audit of ProgramModuleObj, ExpirableModel; prototype subclass change; draft JSON API schema Initial contributions & architecture plan
Week 1 ExpirableModel subclass Model change, migration 0054, unit tests PR #5810, PR #5818
Week 2 getModules() time-filtering Time-aware module filtering in request path; cache invalidation tests PR #5834
Week 3 API endpoints GET/POST module_schedule JSON API; authentication tests PR #5841
Week 4 Special module handling & tooling fixes always_enabled / seq_locked hooks; conflict detection API; seed_dummy_data fix PR #5842, PR #5843
Week 5 Frontend foundation Static timeline canvas DOM/CSS; Bootstrap 5 scoped import; tab switching PR #5856
Week 6 (Midterm) Timeline Canvas & Edit UI Interactive timeline display; slide-in Edit Panel; live AJAX updates PR #5856
Week 7 Drag reorder jQuery UI Sortable on rows; keyboard navigation; atomic reorder API PR #5859
Week 8 Module picker integration Collapsible Add Module drawer; dynamic category discovery PR #5881
Week 9 Accessibility full pass ARIA roles on all interactive handles; keyboard navigation; focus trapping PR #5881
Week 10 Permission sync & module refactoring Automatic permission synchronization engine; split RegProfileModule; automated new program permissions PR #5895, PR #5938, PR #5939
Week 11 Developer documentation & code freeze Comprehensive docs in docs/admin/module_schedule.rst; cross-linking; inline comments PR #5941
Week 12 Final review & submission Final rebase; demo preparation; GSoC final report Final Work Submission

3. What I Did - A Week-by-Week Account

Community Bonding Period: Understanding the Codebase

The community bonding period is officially a time for selected GSoC contributors to introduce themselves, set up their development environments, and get comfortable with the codebase before the coding period begins. For me, it became much more than that.

I spent the first days of community bonding doing a thorough architectural audit of the parts of the codebase most relevant to my project: ProgramModuleObj in esp/esp/program/modules/base.py, the ExpirableModel abstract class in esp/esp/utils/expirable_model.py, the Permission model in esp/esp/users/models.py, and the Program model in esp/esp/program/models/__init__.py. I traced every execution path from an incoming HTTP request through module retrieval, permission checking, and response rendering.

I also used the community bonding period to submit several initial contributions to the codebase - bug fixes and minor features that helped me get comfortable with ESP-Website's coding conventions, test patterns, and pull request review process before the main project work began.

By the end of community bonding, I had a complete picture of how the module system worked internally and had drafted the JSON API schema that would form the backbone of Phase 3's work.


Week 1: Making ProgramModuleObj Time-Aware

PR #5810: Phase 1(Week 1): Make ProgramModuleObj inherit from ExpirableModel (Issue #2895) - Merged May 29
PR #5818: Fixes Issue #5815: Align boundary logic in ExpirableModel.is_valid() with valid_objects() - Merged May 30

The very first thing I needed to do was give ProgramModuleObj a temporal dimension - the ability to know not just that it was enabled, but when it was supposed to be active. ESP-Website already had an abstract model designed exactly for this purpose: ExpirableModel.

The ExpirableModel abstract class lives in esp/esp/utils/expirable_model.py and provides two nullable DateTimeField columns: start_date and end_date. It also provides an is_valid(when=None) method that evaluates whether a model instance is "active" at a given timestamp - returning True if the current time falls within the window defined by start_date and end_date, and returning False otherwise. When either field is NULL, it is interpreted as "no boundary" - a NULL start date means the instance has always been active, and a NULL end date means it will never expire.

The change to make ProgramModuleObj inherit from ExpirableModel instead of models.Model was conceptually simple. In practice, it required very careful thought about the migration strategy.

The challenge was this: ExpirableModel declared its start_date field with default=datetime.now. This meant that if I simply added the field to the migration without overriding this default, every single existing ProgramModuleObj row in every live database would be backfilled with the timestamp of when the migration was run. The moment the migration completed, every module in every university's live database would have a start_date equal to that exact past timestamp. For modules that had that timestamp in the future, they would immediately become invisible to users. For modules that had it in the past, they would behave normally. Depending on exactly when the migration ran, this could silently break an unknown number of modules across twenty or more live university deployments.

I identified this problem during development when I noticed that test modules were being created with start_date values equal to datetime.now() rather than NULL. The fix was to explicitly override the start_date field in ProgramModuleObj with null=True, blank=True, default=None, and then write the migration by hand to ensure existing rows received NULL rather than the current timestamp. With NULL dates, is_valid() interprets them as "no boundary" - meaning all existing modules would continue to behave exactly as they always had, with no disruption to any running programs.

This single-line field override is arguably the most important line of code in the entire project. Without it, a migration that looked perfectly correct would have silently corrupted the data of every university using the platform.

I also wired cache invalidation so that saving a ProgramModuleObj (for example, changing its start_date) would immediately invalidate the Memcached cache for that program's module list. This ensured administrators would see their changes take effect instantly, without needing a server restart.

Alongside the main PR #5810, I submitted a companion fix (PR #5818) to address a boundary logic inconsistency that was identified during the review process. The ExpirableModel.is_valid() method had been using strict inequality comparisons (< and >), while the corresponding valid_objects() queryset method used inclusive comparisons (<= and >=). This meant that an object whose start_date or end_date was exactly equal to the "now" timestamp might be returned by valid_objects() but not by is_valid(), or vice versa. The fix aligned both methods to use inclusive boundaries, ensuring consistent and predictable behavior for any module configured with precise opening or closing times.


Week 2: Making Module Filtering Time-Aware

PR #5834: Phase 2(Week 2): Add time-filtering to getModules() for non-admins (Issue #4935) - Merged Jun 3

With ProgramModuleObj now carrying start_date and end_date fields and is_valid() working correctly, the next step was making the platform actually enforce those time windows. The model fields are meaningless unless something in the request-handling pipeline reads them and uses them to decide which modules to show.

The central method responsible for retrieving the list of active modules for a given user is Program.getModules(), defined in esp/esp/program/models/__init__.py. This method is called every time a user loads a program page and is one of the most performance-critical methods in the entire application. It retrieves the list of enabled ProgramModuleObj instances for the user's current role and returns them so the view layer can decide which pages to render or redirect to.

One of the most important architectural decisions of the entire project was deciding exactly where to apply the is_valid() time filter. There were two viable options, and they had very different performance characteristics.

The first option was to apply the filter at the database query level, adding is_valid() filtering directly to the ORM query that fetches modules. This would be the cleanest approach from a pure code perspective - the database would only return currently-active modules, and there would be no need to filter in Python. However, it had a fatal flaw: because "now" changes every second, any cached version of the module list would become stale within seconds of being stored. ESP-Website uses Memcached extensively to cache module lists, and this caching is a major contributor to the platform's ability to handle hundreds of concurrent users during live registration periods. Filtering at the database level would have made this cache essentially worthless.

The second option was to filter at the request handler level, after retrieving the module list from cache. This approach works as follows: the full list of all enabled modules is cached as before. When a request comes in, the cached list is retrieved and then filtered dynamically in Python by calling m.is_valid(when=datetime.now()) for each module. This filtering happens in Python, not in the database, so the cache is never invalidated by the passage of time. The performance overhead is negligible because the number of modules per program is small (typically between 10 and 30), and a simple datetime comparison is very fast.

I chose the second approach. The result is a system where module windows take effect automatically and precisely. An administrator can schedule a module to open at 10:00 AM, and at exactly that second (within one request cycle), the module becomes visible to the appropriate users without any manual intervention. Administrators are explicitly excluded from time-filtering so they can always see and manage all modules regardless of window status.


Week 3: Building the Module Schedule JSON REST API

PR #5841: Phase 3(Week 3): Implement JSON API endpoints for Module Timeline (Issue #2895) - Merged Jun 14

With the backend model and filtering logic in place, the project moved into Phase 3: building the REST API layer that would allow administrators to interact with module schedules programmatically. This API layer was designed to serve two purposes simultaneously - first, to allow administrators to update module schedules through a proper API rather than the generic Django Admin, and second, to serve as the data and interaction layer for the frontend timeline UI that would come in later phases.

I designed and implemented a suite of REST API endpoints under /manage/<program_type>/<program_term>/module_schedule/. All endpoints are protected by @login_required and @admin_required decorators, validated with descriptive JSON error responses, and covered by a dedicated unit test suite.

The first endpoint, a GET request to /module_schedule/, returns a complete serialized view of all ProgramModuleObj records for the program, grouped by module_type (learn, teach, volunteer, manage). For each module, the response includes the start_date, end_date, sequence number, required flag, handler class name, and permission types. This endpoint is what the frontend timeline UI calls to populate its initial display.

The second endpoint, a POST request to /module_schedule/update/, allows administrators to update one or more fields for a specific module object: start_date, end_date, seq, required, required_label, and link_title. When a date field is updated, the endpoint also calls sync_permissions() (implemented in Phase 4) to automatically propagate the change to the corresponding permission records in the database.

The third endpoint, a POST request to /module_schedule/reorder/, accepts an ordered array of module object IDs and atomically updates their sequence values inside a database transaction. The atomic transaction ensures that no partial reorders are committed - either all modules are reordered successfully or none of them are.

The fourth endpoint, a GET request to /module_schedule/conflicts/, scans all active module time windows and returns any pairs whose windows overlap AND whose handler classes have declared each other in a conflicts_with attribute. This enables the system to warn administrators when they configure two mutually exclusive modules to be active at the same time.

The fifth endpoint, a GET request to /module_schedule/preview/?at=<ISO 8601 timestamp>, accepts a timestamp and returns the filtered list of modules that would be active at that point in time. This is the endpoint that powers the "time scrubber" feature in the UI, allowing administrators to preview exactly what students or teachers would see at any future or past moment.


Week 4: Conflict Detection, Lock Flags, Permission Sync, and Tooling Fixes

PR #5842: Phase 4(Week 4): Implement special module lock handling and conflict detection (Issue #3854) - Merged Jun 19
PR #5843: Fix Issue #5793: seed_dummy_data now includes all choosable=1 modules - Merged Jun 17

Phase 4 added three substantial capabilities to the module system that were essential for making the scheduling system robust and safe: conflict detection, lock flags for critical modules, and the sync_permissions() method that finally connected the module system to the permission system. During testing of Phase 4 across dev programs, I also identified and resolved an important platform bug in seed_dummy_data.

Conflict Detection

Some registration phases in ESP programs are mutually exclusive by design. The clearest example is lottery-based enrollment (the "Phase Zero" system) versus first-come-first-served open class registration. In a lottery system, students submit class preferences and are assigned to classes algorithmically. In an open registration system, students pick their own classes on a first-come-first-served basis. These two systems cannot run simultaneously - students would receive contradictory instructions about how enrollment works, and the underlying data model would be in an inconsistent state.

To handle this, I introduced a conflicts_with class-level attribute on module handler classes. For example, StudentRegPhaseZero.conflicts_with = ['StudentClassRegModule'] declares that these two modules should never be active at the same time. The /module_schedule/conflicts/ API endpoint then scans all active modules, checks for any declared conflicts between active module pairs, and returns a structured JSON response identifying the conflicting modules and the overlapping time window.

Lock Flags

Some modules must remain accessible at all times regardless of any configured time window. The most important example is the student profile module - students must always be able to access their profile to complete any other step in the workflow. If the student profile module were closed by a misconfigured time window, students would be entirely blocked from the system.

To handle these critical modules, I introduced two class-level handler attributes. The always_enabled = True attribute prevents a module from being hidden by time-filtering even when is_valid() returns False. The seq_locked = True attribute prevents a module from being repositioned in the sequence via drag-and-drop or the reorder API, ensuring that certain modules always appear in a fixed position (for example, the profile module must always appear first at seq = 0).

Bidirectional Permission Sync (sync_permissions)

This was the most architecturally significant piece of Phase 4 and the key to solving the "disconnected permission and module systems" problem that was identified at the start of the project.

The sync_permissions() method is defined on ProgramModuleObj in base.py. When called, it works as follows. First, it retrieves the list of permission types from the module handler class (for example, ['Student/Classes'] for StudentClassRegModule). Second, it determines the correct Django auth Group (Student, Teacher, or Volunteer) by inspecting the prefix of the permission type string, or by falling back to the module's module_type field. Third, it searches for any existing Permission records in the database that match the program, role, and permission type. If a matching record exists, it updates its start_date and end_date to match the module's dates. If no matching record exists, it creates a new one. Finally, if the role cannot be determined or the expected Group does not exist in the database, it logs a non-fatal warning rather than crashing.

This method is called automatically inside the /module_schedule/update/ API endpoint every time a module's start_date or end_date is changed. The result is that from Phase 4 onward, the module schedule and the permission system are no longer two separate things that administrators must maintain in parallel. Updating a module's time window is the single source of truth that automatically keeps everything in sync.

seed_dummy_data Auto-Discovery (PR #5843)

While developing and testing Phase 4's conflict detection across test environments, I discovered that the seed_dummy_data management command - which populates a fresh development database with realistic test data - was using a large, manually maintained hard-coded list of module handler names. This list was out of date and missing many modules that had been added to the codebase over time. I replaced the hard-coded list with dynamic auto-discovery using Python's pkgutil and inspect modules to automatically find all handler classes in the esp.program.modules.handlers package and seed records for any module with choosable = 1. This ensured that test programs like Splash Dev and Spark Dev received all available modules and ProgramModuleObj records for testing.


Week 5 and 6 (Midterm): Building the Module Timeline UI

PR #5856: Phase 5(Week 5 and 6): Integrate Module Timeline UI into Django (Issue #3854) - Merged Jul 9

Phases 5 and 6 were where the most visible and user-facing work of the project took place. Up until this point, all of the work had been backend changes - model modifications, API endpoints, and database migrations. Now it was time to build the administrative interface that would let program administrators actually interact with the module scheduling system visually.

I built a new management view at /manage/<program>/module_schedule/ that replaced the legacy module list interface in admincore/modules.html with an interactive, dynamic timeline display.

The frontend is powered by a dedicated JavaScript file, public/media/scripts/program/modules/module_management.js, which fetches live data from the /module_schedule/ API endpoint when the page loads and dynamically renders the module timeline based on the API response. The timeline is styled with a custom CSS file, public/media/styles/module_management.css, which uses CSS custom properties for role-based theming and responsive layout without depending on any external CSS framework.

The interface is split into a student view and a teacher view, with tab navigation allowing administrators to switch between the two. Within each view, modules are displayed as rows in a sidebar list, and for modules with configured time windows, a visual representation of the window is rendered on a horizontal timeline to the right of the sidebar.

The interface includes several interactive features. Clicking any module row opens a slide-in Edit Panel on the right side of the screen, where the administrator can modify the module's start_date, end_date, required flag, link_title, and sequence number. Changes are saved via AJAX calls to the /module_schedule/update/ endpoint and immediately reflected in the UI without a page reload.

A separate Add Module Drawer allows administrators to enable new modules for the program. The drawer categorizes available modules into Student Registration, Teacher Registration, and General categories, sourced dynamically from the backend. When a module is added, its configuration is immediately saved and the timeline updates to reflect the change.

A time-scrubber preview mode lets administrators preview exactly what students or teachers would see at any future or past moment, calling the /module_schedule/preview/?at=<timestamp> endpoint and visually highlighting inactive modules.

During the review process for this PR, my mentor Will Gearty identified and reported several bugs through extensive manual testing, including a case where a module set to open far in the future caused the timeline's horizontal positioning calculation to exceed 100%, stretching the entire browser scrollable container to many times the viewport width. I resolved this with mathematical clamping to keep all block positions safely within the visible range.



Figure 1: The interactive Module Timeline administrative interface displaying Student (Learn) and Teacher (Teach) module tracks.


Figure 2: The slide-in Edit Panel for configuring a module's start date, end date, required flag, link title, and sequence number.


Figure 3: The Add Module drawer, dynamically categorizing and listing available modules for the administrator to enable.


Week 7: Drag-and-Drop Module Reordering

PR #5859: Phase 6(Week 7): Add drag-and-drop module reordering (Issue #3854) - Merged Jul 9

Week 7 added an important usability feature to the timeline interface: the ability to reorder modules by dragging and dropping them within the sidebar list.

I implemented drag-and-drop by initializing jQuery UI's Sortable plugin on the student and teacher module sidebar lists. When an administrator drags a module row to a new position, the JavaScript code handles the drop event to dynamically recalculate sequence values for all affected rows and then sends a POST request to the /module_schedule/reorder/ endpoint with the new ordering. The UI updates optimistically (the new order is shown immediately) and rolls back to the previous order if the server request fails.

Locked modules (those with seq_locked = True) are visually distinguished from draggable modules using a different cursor style and a CSS class that disables interaction. This ensures administrators cannot accidentally move critical position-locked modules like the registration profile module.

On the backend, the reorder API endpoint wraps all sequence updates in a single transaction.atomic() block to ensure that either all sequence updates are committed or none of them are, preventing partial reorders from corrupting the sequence ordering.

A subtle bug was discovered during Will's review: when administrators dragged modules, some of the modules would visually disappear from the list after the drag completed. This was caused by a state management issue in the JavaScript where the in-memory module state was becoming desynchronized from the DOM state after a drag event. I traced this to a problem with how locked modules were being handled during state rebuilding after a successful reorder response, and fixed it by explicitly preserving locked module positions in the client-side state object.


Week 8 - 10: Module Handler Refactoring, Permission Auto-Initialization, and UI Polish

PR #5881: Phase 7: Add module drawer animation, safety locks, and full accessibility pass (Issue #1827) - Merged Jul 24
PR #5895: Phase 8: Automatic permission synchronization and timeline grid clamping (Issue #1827) - Merged Aug 1
PR #5938: Split RegProfileModule handler into StudentRegProfileModule and TeacherRegProfileModule (Issue #5909) - Merged Aug 11
PR #5939: Initial module permissions and dates configuration in new program form (Issue #5916) - Merged Aug 11

Weeks 8 through 10 covered a broad range of work, focusing on full accessibility and UI polish, comprehensive permission synchronization, and two significant architectural bonus features.

Timeline UI Polish: Accessibility and Drawer Refinement (PR #5881)

PR #5881 added full accessibility support and UI polish to the module management interface: keyboard navigation, ARIA roles and labels, focus management (including focus trapping and restoration when panels open and close), an Escape key handler to dismiss modals, and a dynamic reload so module changes appear on the timeline without a full page reload. The "Add Module" configuration drawer was also converted from a bottom-sheet layout to a right-side panel layout to match the Edit Panel's visual language and provide a unified interface.

Full-Scale Permission Synchronization Engine (PR #5895)

PR #5895 delivered the automatic permission synchronization engine at full scale. I added explicit permission_types mappings across all 14 core registration module handlers (including StudentClassRegModule, TeacherClassRegModule, StudentRegPhaseZero, StudentSurveyModule, TeacherSurveyModule, VolunteerSignup, and more), ensuring that sync_permissions() could correctly determine which permission records to update for every module in the system. I also added a unit test test_module_schedule_update_syncs_permissions that verifies permission records are created and updated correctly whenever a module's schedule is changed through the API.

Splitting RegProfileModule (PR #5938)

The regprofilemodule.py handler had been a recognized code smell in the ESP-Website codebase for a long time. It was a single Python class that handled both student and teacher registration profile logic inside one file, using branching conditionals to determine which form to display based on the current user's role. As the module permission system matured and each handler became responsible for declaring exactly which permission types it controlled, having one handler return different permission types for different user roles was a structural problem that could not be left unaddressed.

I refactored this class into two focused, single-responsibility handlers. StudentRegProfileModule in studentregprofilemodule.py handles student registration profile forms, equity outreach cohort targeting, and medical information. It declares permission_types = ['Student/Profile'], seq = 0, and required = True. TeacherRegProfileModule in teacherregprofilemodule.py handles teacher profile forms, biography, and emergency contact information, declaring permission_types = ['Teacher/Profile'], seq = 0, and required = True.

Beyond the code split itself, this change required a carefully designed data migration (0055_split_regprofilemodule.py) that updated every ProgramModule row in the database - remapping the handler string field from the old regprofilemodule.RegProfileModule class path to the appropriate new class path based on each row's module_type. Rows with module_type = 'learn' (student) were mapped to StudentRegProfileModule, while rows with module_type = 'teach' (teacher) were mapped to TeacherRegProfileModule. The migration was written with a complete reverse_func to ensure it was safely reversible if needed, and was tested against edge cases where rows might have unexpected or missing module_type values.

Automating Initial Permission and Date Setup (PR #5939)

Even with the full permission sync system in place, there was still one remaining operational gap: the new program creation workflow. When an administrator created a brand-new program through the program setup form, all the associated permissions started with no dates - meaning they were effectively open to all users immediately. An administrator would still have to manually configure the date range for every permission after program creation.

I implemented a solution in prepare_program() inside esp/esp/program/setup.py. When a new program is created through the standard form, the function now iterates over every module that has been enabled for the program. For each module, it inspects the handler class's permission_types attribute and automatically creates Permission records with correctly assigned start_date and end_date values. The dates are derived from the student and teacher registration date fields that the administrator already filled in during program creation - so no additional input is required from the administrator. If a module's permission type starts with Student or its module_type is learn, it gets the student registration dates. If it starts with Teacher or module_type is teach, it gets the teacher registration dates. Volunteer modules similarly inherit appropriate date ranges.

I also added date inheritance logic in getFromProgModule() in esp/esp/program/modules/base.py. This method initializes a fresh ProgramModuleObj when one does not yet exist for a given program-module combination. After this change, when a fresh ProgramModuleObj is initialized, it looks up the corresponding Permission record for the program and role, and inherits the start_date and end_date from that permission record rather than defaulting to NULL.

The combined result of these changes is that for any program created through the standard form, the administrator fills in the registration dates once during program creation, and those dates automatically flow through to every module permission and every module object. No post-creation configuration is needed.


Week 11: Developer Documentation

PR #5941: docs: Add developer documentation for module schedule and permission dates - Open / Awaiting final review

The final major deliverable of the project was a comprehensive RST developer documentation page for the entire module scheduling system. I wrote approximately 260 lines of documentation in docs/admin/module_schedule.rst and linked it from both docs/admin/program_modules.rst and docs/admin/README.rst.

The documentation covers the full picture from model architecture down to individual API endpoints. It explains the ExpirableModel subclass structure and what each field means, the backward compatibility guarantees (why NULL dates mean "always open"), and the module_type reference table. It explains how Program.getModules() integrates with is_valid() time-filtering, the Memcached caching strategy, and the rationale for applying filtering at the request handler level rather than the database level. It describes how prepare_program() and commit_program() generate initial role permissions during program creation, and how sync_permissions() maintains ongoing consistency between module dates and permission records. The API reference section documents all five REST endpoints with their full URL patterns, HTTP methods, and expected request/response formats. A permission types reference table maps every key handler class to its declared permission_types. The testing section points readers to the relevant unit test files and explains how to set up the auth group fixtures required for permission tests to pass.

Writing this documentation turned out to be significantly more challenging than I expected. Copilot's automated review identified 12 accuracy issues in my first draft - places where I had described planned future features as if they were already implemented, used incorrect method signatures, or made claims about the codebase that were simply not true. Correcting all 12 issues required going back to the source code and carefully verifying every single claim against the actual implementation. The final version of the documentation is accurate, thorough, and will be a valuable resource for any future contributor working on the module scheduling system.


Week 12: Final Review and Submission

The final week of the project was dedicated to code freeze, final rebasing against upstream main, verifying that all CI checks passed across all modified modules, and preparing the final submission documentation. I confirmed that all 11 merged PRs had clean commit histories, all unit tests were passing, and all lint checks were clean. I then prepared this final report and submitted it as my official GSoC 2026 work submission.


4. The Current State of the Project

As of the end of the Google Summer of Code 2026 coding period, the project is in an excellent state. All primary deliverables across Weeks 1 through 10 are fully implemented, tested, reviewed, and merged into the upstream learning-unlimited/ESP-Website repository on GitHub. The developer documentation PR from Week 11 is complete, has 100% passing CI, and has had all automated review comments addressed. It is awaiting final maintainer sign-off.

Fully Complete and Merged

Component Week Status
ExpirableModel integration for ProgramModuleObj Week 1 Merged - PR #5810
ExpirableModel boundary logic fix Week 1 Merged - PR #5818
is_valid() time-filtering in Program.getModules() Week 2 Merged - PR #5834
Module schedule REST API endpoints Week 3 Merged - PR #5841
Conflict detection, lock flags, sync_permissions() Week 4 Merged - PR #5842
seed_dummy_data choosable modules fix Week 4 Merged - PR #5843
Admin Timeline UI integration Week 5 and 6 Merged - PR #5856
Drag-and-drop module reordering Week 7 Merged - PR #5859
Module drawer animation, safety locks, and accessibility Week 8 - 10 Merged - PR #5881
Automatic permission synchronization and timeline grid clamping Week 8 - 10 Merged - PR #5895
Split RegProfileModule into Student and Teacher handlers Week 8 - 10 Merged - PR #5938
Automated module permission and date initialization Week 8 - 10 Merged - PR #5939
RST developer documentation (docs/admin/module_schedule.rst) Week 11 Merged - PR #5941

5. What is Left to Do (Future Work)

All core deliverables of the GSoC 2026 project are fully completed and merged. Two of the stretch goals originally outlined in my proposal (the Conflict Detection Engine in PR #5842 and the "Student Preview" Time Scrubber in PR #5841) were also successfully completed ahead of schedule during the summer.

The following items represent the remaining Post-GSoC Stretch Goals defined in my original proposal, which serve as natural and high-impact next steps for future contributors:

1. Schedule Templates - Save and Reuse

Educational programs like MIT Splash or Stanford Splash often repeat annually or semi-annually with essentially the same module structure and relative timing. Rather than requiring administrators to manually configure module schedules from scratch for each new session, this feature introduces a ModuleScheduleTemplate model. The model stores module schedules as relative time offsets from program_start rather than fixed timestamps. When creating a new program, an administrator can simply select a saved template (e.g. "Standard 6-Week Fall Registration Schedule"), and all module start and end dates will be computed and populated automatically relative to the new program date.

2. Undo / Redo History

To make timeline scheduling even safer and more forgiving, this feature implements a client-side command history stack using the Command Pattern, maintaining up to 20 reversible operations. Every drag-and-drop reorder, time adjustment, or module toggle wraps its AJAX save operation in a command object with explicit undo() and redo() methods. Undo and redo toolbar buttons along with standard keyboard shortcuts (Ctrl+Z and Ctrl+Shift+Z) in the timeline header will allow administrators to immediately revert accidental changes.

3. Per-Module Activity Analytics Panel

To give program administrators real-time visibility into student and teacher engagement, this feature adds a collapsible analytics drawer at the bottom of each timeline row. The drawer displays an hourly activity sparkline (implemented as lightweight CSS bar charts without third-party chart libraries), total user interactions, peak registration hour markers, and completion rates for checklist modules. Analytics data is lazy-loaded on demand when an administrator expands the drawer, keeping initial page load fast.

4. Auto-Notification Hooks on Module Open and Close

When an important registration phase opens or closes, notifying registered users immediately drives timely action. This feature adds optional notification preference toggles to ProgramModuleObj ("Notify students on open", "Notify students on close"). A scheduled Django management command running via cron or Celery Beat monitors upcoming module window transitions and automatically dispatches customized reminder emails using ESP-Website's existing EmailTemplate infrastructure.


6. Challenges and Important Things I Learned

6.1 The Migration Safety Problem - Why One Line of Code Matters

The most technically dangerous moment of the entire summer came during Phase 1, and it illustrates a principle that I will carry with me throughout my entire software engineering career: in a production system, the most dangerous bugs are not the ones that make the system crash loudly. They are the ones that silently corrupt data without any visible error.

After writing the migration to add start_date and end_date to ProgramModuleObj, I was running unit tests and noticed something that seemed minor at first glance: every test module was being created with a start_date equal to datetime.now() at the time of creation, rather than NULL. I traced this to the default=datetime.now on the start_date field inherited from ExpirableModel.

The implications of this in production were severe. Every existing ProgramModuleObj row in every live university database would be backfilled with the migration timestamp as its start_date. If any module had a start_date that appeared to be "in the future" relative to a subsequent request's datetime.now(), that module would return False from is_valid() and become invisible to students. The system would give no error message, no warning, and no indication that anything was wrong. Students would simply find themselves stuck on a step they could not proceed past, with no explanation.

The fix was a single line: overriding start_date in ProgramModuleObj with null=True, blank=True, default=None. This ensured all existing rows received NULL rather than a timestamp, and NULL is interpreted by is_valid() as "no boundary" - meaning the module is always active. One line of code prevented a migration that looked perfectly correct from corrupting the data of twenty or more live university deployments serving thousands of students.

The lesson I take from this is not just "be careful with migrations." It is a deeper principle: when you inherit behavior from an abstract class, you must fully understand what every inherited default means in the specific context of your subclass. A default that is perfectly correct in one context can be catastrophically wrong in another.

6.2 The Caching Architecture Decision - Why Performance Requires Measurement

Deciding exactly where in the stack to apply the is_valid() time filter was one of the most consequential architectural decisions of the project, and it required me to step back from my initial instinct and actually think carefully about the performance implications.

My first instinct was to apply the filter at the database query level. This felt clean and elegant - the database would only return currently-active modules, and the code would be simple. But after thinking about how the caching layer worked, I realized this approach had a fatal flaw. Because "now" changes every second, any cached module list would become stale within seconds of being stored. The whole point of caching is to avoid repeated database queries for data that does not change frequently. A module list that becomes stale every second is effectively uncacheable.

The alternative - filtering in Python at the request handler level after retrieving the cached list - preserves all the benefits of caching. The cache stores the full list of enabled modules, which changes infrequently (only when an administrator enables or disables a module). The dynamic is_valid() filter is applied in Python against that cached list on every request, at a cost of a few microseconds per module. Because the number of modules per program is small, this cost is completely negligible.

This was a lesson in the importance of measuring before optimizing. The "clean" solution was not actually the correct solution, because it would have broken a major performance optimization that the platform depends on during high-concurrency live registration events.

6.3 Refactoring Legacy Code Safely - Treating the Database as a Public API

Splitting RegProfileModule taught me an important lesson about how to think about database schema changes in long-lived production systems.

In a typical software project, when you rename a class or move it to a different module, you update the import statement and move on. In ESP-Website, module handler class names are stored as string values in the ProgramModule.handler database column. This column is how the system knows which Python class to instantiate for a given module. When I renamed the handler classes, I could not just update a Python file - I had to also update every row in the database that referenced the old class name.

This meant writing a data migration that reads each affected row, inspects its module_type to determine whether it should become a StudentRegProfileModule or a TeacherRegProfileModule, and updates the handler field accordingly. The migration also needed a complete reverse_func to restore the original class name if the migration needed to be rolled back.

Writing this migration forced me to think of the database as part of the public API surface of the application. In a web application with a live database, changes to how data is stored or interpreted are just as consequential as changes to any externally visible HTTP endpoint. They must be designed with the same care: explicitly versioned, reversible, tested against edge cases, and documented.

6.4 Open Source Code Review - The Best Learning Tool Available

Before GSoC 2026, I had contributed to open source projects occasionally, but I had never sustained a contribution effort of this scale and intensity. Going through 13 pull requests in 12 weeks, each with multiple rounds of review from both automated tools and an experienced human maintainer, was one of the most educationally valuable experiences of my software development career.

My mentor Will Gearty reviewed every single PR with genuine care and rigor. He caught a test assertion in Phase 4's conflict detection tests that was order-dependent and would produce intermittent failures depending on the order in which the database returned results. He identified a CodeQL security warning in Phase 3's API views where internal model field names were leaking through error response messages - a real information exposure vulnerability. He pointed out a regression in Phase 6 where modules were disappearing from the UI after drag operations, and provided a video recording of the bug to help me reproduce it.

Each of these review rounds taught me something specific. The order-dependent test failure taught me to always sort or otherwise canonicalize test data before making assertions about its order. The information exposure warning taught me to think about what internal information appears in API error messages even in cases that look like simple validation failures. The drag-and-drop regression taught me that optimistic UI updates require careful state synchronization between the client-side in-memory model and the DOM.

The deeper lesson is that code review is not an obstacle to getting your code merged - it is the most efficient way to learn from engineers who have more experience than you. Every comment from Will or from Copilot was an opportunity to see my code through the eyes of someone who has thought deeply about the problems of correctness, security, and long-term maintainability in production software systems.

6.5 Documentation Is Harder Than It Looks

I began work on PR #5941 expecting it to be one of the easier deliverables of the project. After several weeks of complex backend and frontend work, writing documentation felt like it should be relaxing by comparison. I was wrong.

Good technical documentation has a property that is deceptively simple to state and genuinely difficult to achieve: it must be accurate. Not approximately accurate, not mostly accurate, but precisely accurate. Every method signature must match the actual code. Every feature described must actually exist in the codebase. Every workflow described must reflect what the code actually does, not what you intended for it to do or what you planned to implement in a future phase.

Copilot's review of my first draft identified 12 inaccuracies. I had described sync_permissions() as being called automatically by Django Admin when a ProgramModuleObj is saved - this was not true, it is only called by the API endpoint. I had described a ModuleScheduleTemplate model and an automated notification cron system - neither of which exist in the codebase, because they are future work items. I had used the wrong parameter name for is_valid() (writing at_time=None when the actual parameter is when=None). I had described getFromProgModule() as initializing module dates from permission records - a behavior I had implemented for Issue #5916 but that the documentation PR was submitted before that PR was merged.

Fixing all 12 of these required going back through the codebase line by line and verifying every single claim I had made. The final documentation is accurate and valuable. But the process of getting there was a reminder that writing documentation requires the same discipline as writing code - a willingness to check your assumptions against reality rather than writing what you believe to be true.

6.6 Navigating a 20-Year-Old Codebase

ESP-Website has a commit history that goes back to 2006. That means two decades of design decisions, architectural evolutions, temporary workarounds that became permanent fixtures, and features built by contributors who may no longer be involved with the project.

Working in this codebase taught me a habit that I did not have before GSoC: before making any change, trace the full execution path from HTTP request to database and back, including every piece of middleware, caching layer, serialization hook, and template that touches the data you are modifying. Many of the most significant bugs I encountered this summer came from changes that looked perfectly correct in isolation but interacted unexpectedly with components two or three layers removed.

The boundary logic fix in ExpirableModel (PR #5818) is a perfect example. The inconsistency between is_valid() and valid_objects() was a bug that had existed in the codebase for a long time. It would have been easy to miss if I had not been specifically looking for it, and easy to dismiss as unimportant if I had not traced through all the ways both methods are used across Permission, StudentRegistration, and other subclasses.


7. Acknowledgements

I am genuinely grateful to several people and organizations whose support made this project possible.

Will Gearty (@willgearty) deserves the most thanks. Will is the primary maintainer of ESP-Website and served as my mentor throughout the summer. He reviewed every one of my 13 pull requests with genuine care, patience, and rigor. He caught real bugs that I had missed, explained complex parts of the codebase when I was lost, provided detailed written feedback on every PR, and recorded video demonstrations of UI bugs to help me reproduce and fix them. His guidance and mentorship throughout the summer were invaluable, and the quality of the work in this submission is directly attributable to the quality of his feedback.

The Learning Unlimited Community is a remarkable group. The administrators and contributors who rely on ESP-Website to run their programs do so as volunteers, giving their time to make educational enrichment programs accessible to thousands of students every year. Knowing that my work would eventually reach real students at MIT Splash, Stanford Splash, Chicago Spark, Yale Splash, and dozens of similar programs made every difficult debugging session feel purposeful.

Google Summer of Code gave me an extraordinary opportunity. The program provided the structure, the stipend, the community, and most importantly the mentorship connection to Will that made this project possible. GSoC 2026 was one of the most formative experiences of my development as a software engineer. I began the summer as someone who contributed to open source occasionally. I am ending it as someone who has designed, implemented, documented, and shipped a complete feature system for a production platform that serves thousands of real users.


Resource URL
My GitHub Profile github.com/Supriyo760
My Fork github.com/Supriyo760/ESP-Website
Upstream Repository github.com/learning-unlimited/ESP-Website
Primary Feature Branch feature/new-program-module-permissions-5916
Issue #3854 (Module UI) github.com/learning-unlimited/ESP-Website/issues/3854
Issue #2895 (Module Times) github.com/learning-unlimited/ESP-Website/issues/2895
GSoC 2026 Program Page summerofcode.withgoogle.com

Written by Supriyo Chowdhury - August 2026
Google Summer of Code 2026 Contributor, Learning Unlimited / ESP-Website

Comments

Popular posts from this blog

SocraticPair - Collaborative Partner