From 2d875bdc6934f9c14dfab10f5db5c87940cf9b37 Mon Sep 17 00:00:00 2001 From: Jacob Coffee Date: Sat, 14 Feb 2026 08:35:54 -0600 Subject: [PATCH 1/2] docs: add user documentation for 6 merged features - Feature toggles: new section in configuration.md covering settings, per-conference DB overrides, Python API, and template usage - Pretalx overrides: new section in pretalx-integration.md covering TalkOverride, SpeakerOverride, RoomOverride, SubmissionTypeDefault, and sync interaction - Global ticket capacity: new section in registration-flow.md covering Conference.total_capacity enforcement in cart and checkout, plus TOML bootstrap addition in configuration.md - Financial dashboard: new management-dashboard.md covering revenue summary, order/cart/payment breakdowns, and ticket type sales - Voucher bulk generation: section in management-dashboard.md covering the bulk code form, generated code format, and transactional guarantees - Programs & activities: new programs-activities.md covering travel grant application flow, day detection, grant lifecycle, receipts, payment info, and activity signup/waitlisting Co-Authored-By: Claude Opus 4.6 --- docs/configuration.md | 168 +++++++++++++++++++++++++++++++++++ docs/index.rst | 14 +++ docs/management-dashboard.md | 135 ++++++++++++++++++++++++++++ docs/pretalx-integration.md | 167 ++++++++++++++++++++++++++++++++++ docs/programs-activities.md | 158 ++++++++++++++++++++++++++++++++ docs/registration-flow.md | 57 ++++++++++++ 6 files changed, 699 insertions(+) create mode 100644 docs/management-dashboard.md create mode 100644 docs/programs-activities.md diff --git a/docs/configuration.md b/docs/configuration.md index 14f8032..b640658 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -34,6 +34,17 @@ DJANGO_PROGRAM = { "publisher": "pycon", # default "flight": "sponsors", # default }, + # Feature toggles (all default to True) + "features": { + "registration_enabled": True, + "sponsors_enabled": True, + "travel_grants_enabled": True, + "programs_enabled": True, + "pretalx_sync_enabled": True, + "public_ui_enabled": True, + "manage_ui_enabled": True, + "all_ui_enabled": True, + }, # General "cart_expiry_minutes": 30, # default "pending_order_expiry_minutes": 15, # default @@ -93,6 +104,157 @@ Most conferences can ignore this section entirely. | `currency_symbol` | `str` | `"$"` | Display symbol for the currency. | | `max_grant_amount` | `int` | `3000` | Maximum travel grant amount in the configured currency. | +### Feature toggles + +Feature toggles disable entire modules and UI sections per-conference. Every toggle +defaults to `True` (enabled). Disable a module by setting its flag to `False` in +settings, or override it per-conference through the database. + +#### Settings defaults + +Add a `features` dict to `DJANGO_PROGRAM` in your Django settings: + +```python +DJANGO_PROGRAM = { + "features": { + "registration_enabled": True, + "sponsors_enabled": True, + "travel_grants_enabled": False, # disable travel grants globally + "programs_enabled": True, + "pretalx_sync_enabled": True, + "public_ui_enabled": True, + "manage_ui_enabled": True, + "all_ui_enabled": True, # master switch for all UI + }, + # ... other settings ... +} +``` + +Changing these values requires a server restart. + +#### Available toggles + +**Module toggles** control backend functionality: + +| Key | Default | Controls | +|---|---|---| +| `registration_enabled` | `True` | Ticket types, cart, checkout, orders | +| `sponsors_enabled` | `True` | Sponsor levels, benefits, comp vouchers | +| `travel_grants_enabled` | `True` | Travel grant applications and review | +| `programs_enabled` | `True` | Activities and signups | +| `pretalx_sync_enabled` | `True` | Speaker/talk/room sync from Pretalx | + +**UI toggles** control interface visibility: + +| Key | Default | Controls | +|---|---|---| +| `public_ui_enabled` | `True` | Public-facing conference pages | +| `manage_ui_enabled` | `True` | Organizer management dashboard | +| `all_ui_enabled` | `True` | Master switch -- when `False`, both `public_ui` and `manage_ui` are forced off regardless of their individual values | + +#### Per-conference database overrides + +Each conference can override the global defaults through a `FeatureFlags` row in the +database. These overrides take effect immediately without a server restart. + +The `FeatureFlags` model uses nullable booleans with three states: + +- **`None`** (blank) -- use the default from `DJANGO_PROGRAM["features"]` +- **`True`** -- force the feature on for this conference +- **`False`** -- force the feature off for this conference + +Resolution order for each flag: + +1. If the conference has a `FeatureFlags` row with an explicit `True` or `False`, that value wins. +2. Otherwise, the default from `DJANGO_PROGRAM["features"]` is used. +3. For `public_ui` and `manage_ui`, the `all_ui_enabled` master switch is evaluated first. + +#### Django admin + +Open the Conference admin page. The **Feature flags** inline appears below the +conference fields, grouped into "Module Toggles" and "UI Toggles". Each dropdown +offers three choices: + +- **Default (enabled)** -- inherit from settings +- **Yes -- force ON** -- override to enabled +- **No -- force OFF** -- override to disabled + +Feature flags are also available as a standalone admin model at +**Conference > Feature flags** for a cross-conference overview. + +#### Checking features in Python code + +Use `is_feature_enabled()` to check a toggle at runtime: + +```python +from django_program.features import is_feature_enabled + +# Check against global settings only +if is_feature_enabled("registration"): + ... + +# Check with per-conference DB override +if is_feature_enabled("sponsors", conference=my_conference): + ... +``` + +Raise a 404 when a feature is disabled: + +```python +from django_program.features import require_feature + +def my_view(request): + require_feature("registration", conference=request.conference) + # ... view logic ... +``` + +Guard a class-based view with the `FeatureRequiredMixin`: + +```python +from django_program.features import FeatureRequiredMixin + +class TicketListView(ConferenceMixin, FeatureRequiredMixin, ListView): + required_feature = ("registration", "public_ui") +``` + +The mixin checks all listed features during `dispatch()`. If the view also uses +`ConferenceMixin` (placed before `FeatureRequiredMixin` in the MRO), per-conference +overrides are picked up automatically from `self.conference`. + +#### Using features in templates + +Add the context processor to your `TEMPLATES` setting: + +```python +TEMPLATES = [ + { + "OPTIONS": { + "context_processors": [ + # ... existing processors ... + "django_program.context_processors.program_features", + ], + }, + }, +] +``` + +Then use `program_features` in your templates to conditionally render sections: + +```html+django +{% if program_features.registration_enabled %} + Buy Tickets +{% endif %} + +{% if program_features.sponsors_enabled %} + Our Sponsors +{% endif %} +``` + +The context processor resolves each flag through `is_feature_enabled()`, so master +switch logic and per-conference DB overrides are applied. When the request has a +`conference` attribute (set by middleware or the view), that conference's `FeatureFlags` +row is consulted automatically. + ### Accessing config in code ```python @@ -145,8 +307,14 @@ slug = "pycon-us-2027" # optional, auto-generated from name venue = "Convention Center" # optional pretalx_event_slug = "pycon-us" # optional, for Pretalx sync website_url = "https://..." # optional +total_capacity = 2500 # optional, 0 = unlimited (default) ``` +`total_capacity` sets the maximum number of tickets sold across all ticket types for the +entire conference. Add-ons do not count toward this limit. Set to `0` or omit entirely +for unlimited capacity. See [Global Ticket Capacity](registration-flow.md#global-ticket-capacity) +for enforcement details. + ### Sections (required, at least one) ```toml diff --git a/docs/index.rst b/docs/index.rst index fdeca6c..5b29811 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -41,6 +41,18 @@ Built for PyCon US but designed to work for any conference that uses Django. Speaker and schedule sync architecture, schema regeneration, and the pretalx-client package. + .. grid-item-card:: Programs & Activities + :link: programs-activities + :link-type: doc + + Travel grants, sprints, tutorials, open spaces, and activity signups. + + .. grid-item-card:: Management Dashboard + :link: management-dashboard + :link-type: doc + + Financial overview, voucher bulk generation, and organizer tools. + .. grid-item-card:: API Reference :link: api/index :link-type: doc @@ -103,6 +115,8 @@ Quick Start getting-started/index configuration registration-flow + programs-activities + management-dashboard .. toctree:: :maxdepth: 2 diff --git a/docs/management-dashboard.md b/docs/management-dashboard.md new file mode 100644 index 0000000..93e00a0 --- /dev/null +++ b/docs/management-dashboard.md @@ -0,0 +1,135 @@ +# Management Dashboard + +The management dashboard is the organizer-facing interface for administering a conference. It lives under the `/manage/` URL prefix (configured in your project's `urls.py`) and requires authentication. All conference-scoped pages sit under `/manage//`. + +Access is restricted to users who meet at least one of these criteria: + +- Django superuser +- Holds the `program_conference.change_conference` permission +- Belongs to a designated permission group (the financial dashboard additionally accepts the "Program: Finance & Accounting" group) + +The sidebar organizes features into sections: **Content** (sections, rooms, speakers, talks, schedule, overrides), **Registration** (financial overview, orders, ticket types, add-ons, vouchers), **Sponsors** (levels, sponsors), and **Programs** (activities, travel grants). + +--- + +## Financial Overview + +The financial overview dashboard gives organizers a single-page summary of revenue, orders, payments, and ticket sales for a conference. + +**URL**: `/manage//financial/` + +Navigate to it from the sidebar under **Registration > Financial**. + +### Summary Cards + +Four headline metrics appear at the top of the page: + +| Card | What It Shows | +|---|---| +| **Net Revenue** | Gross revenue from paid orders minus total refunds. Gross revenue is shown beneath when refunds exist. | +| **Total Orders** | Count of all orders in any status. Shows the paid count below when paid orders exist. | +| **Active Carts** | Number of open, non-expired carts. | +| **Credits Outstanding** | Remaining spendable balance of available credits. Shows the total applied amount beneath. | + +Net revenue is calculated as: `SUM(total) of paid orders - SUM(amount) of credits linked to source orders`. Refund amounts come from `Credit` records tied to source orders rather than summing refunded order totals, so partial refunds are counted accurately. + +### Orders by Status + +A table breaking down order counts by status: **Paid**, **Pending**, **Refunded**, **Partial Refund**, and **Cancelled**. + +### Carts by Status + +Counts for each cart state: **Active** (open and not expired), **Checked Out**, **Expired**, and **Abandoned**. + +### Payments by Method + +For each payment method (Stripe, manual, etc.), the dashboard shows the number of payments and total dollar amount collected through that method. + +### Ticket Type Sales + +A per-ticket-type breakdown showing: + +- **Ticket Type** name +- **Price** per unit +- **Sold** count (tickets on paid and partially-refunded orders) +- **Remaining** inventory (or an infinity symbol for uncapped ticket types) +- **Revenue** generated by that ticket type + +### Recent Orders + +The 20 most recent orders across all statuses, showing reference number (linked to the order detail page), user email, status badge, total, and creation date. + +### Active Carts + +All currently open, non-expired carts listing the user, item count, creation time, and expiration time. + +--- + +## Voucher Management + +### Bulk Code Generation + +Generate a batch of voucher codes from a single form instead of creating them one at a time. + +**URL**: `/manage//vouchers/bulk/generate/` + +Navigate to it from the voucher list page or go directly to the URL. After generation, the dashboard redirects to the voucher list with a success message. + +### Form Fields + +| Field | Required | Description | +|---|---|---| +| **Prefix** | Yes | A fixed string prepended to every generated code (max 20 characters). Use something descriptive like `SPEAKER-` or `SPONSOR-`. | +| **Count** | Yes | Number of codes to create. Minimum 1, maximum 500. | +| **Voucher Type** | Yes | The discount model: **Complimentary** (100% off), **Percentage discount**, or **Fixed amount discount**. | +| **Discount Value** | Yes | The percentage (0--100) or fixed dollar amount. Ignored for comp vouchers. Defaults to 0. | +| **Applicable Ticket Types** | No | Restrict these vouchers to specific ticket types. Leave unchecked for all ticket types. Rendered as checkboxes scoped to the current conference. | +| **Applicable Add-ons** | No | Restrict these vouchers to specific add-ons. Leave unchecked for all add-ons. Rendered as checkboxes scoped to the current conference. | +| **Max Uses** | Yes | Maximum number of times each individual code can be redeemed. Defaults to 1. | +| **Valid From** | No | Start of the validity window (datetime picker). Leave blank for immediately valid. | +| **Valid Until** | No | End of the validity window (datetime picker). Leave blank for no expiration. | +| **Unlocks Hidden Tickets** | No | When checked, these vouchers reveal ticket types that are hidden behind a voucher requirement. | + +### Generated Code Format + +Each code follows the pattern `{prefix}{8 random characters}`. The random portion uses uppercase letters and digits (A-Z, 0-9) generated with `secrets.choice` for cryptographic randomness. Codes are checked against existing vouchers for the same conference and prefix before insertion. + +All vouchers in the batch are created in a single database transaction. If any step fails, the entire batch is rolled back and no partial codes are left behind. + +### After Generation + +On success, the form redirects to the voucher list at `/manage//vouchers/` and displays a message confirming how many codes were created and the prefix used. Each generated voucher appears in the standard voucher list where it can be individually edited. + +--- + +## Other Dashboard Features + +The management sidebar exposes several additional views beyond financial reporting and voucher generation: + +**Content** + +- **Dashboard** -- Conference overview with key stats and quick actions. +- **Settings** -- Edit conference name, dates, slug, and configuration. +- **Sections** -- Create and manage conference sections (tracks or categories). +- **Rooms** -- Manage physical or virtual rooms for scheduling. +- **Speakers** -- Browse and view speaker profiles synced from Pretalx. +- **Talks** -- List, filter by submission type, view details, and edit talks. The sidebar shows submission type sub-navigation with counts. +- **Schedule** -- View and edit schedule slots. +- **Overrides** -- Override Pretalx-synced data for talks, speakers, rooms, sponsors, and submission type defaults. + +**Registration** + +- **Orders** -- Full order list with filtering, detail views, and manual payment recording. +- **Ticket Types** -- Create and edit ticket types with pricing, availability windows, and stock limits. +- **Add-ons** -- Manage conference add-ons (t-shirts, tutorials, etc.). +- **Vouchers** -- Individual voucher CRUD alongside the bulk generation tool. + +**Sponsors** + +- **Levels** -- Define sponsorship tiers and their associated benefits. +- **Sponsors** -- Manage individual sponsors and their level assignments. + +**Programs** + +- **Activities** -- Create sprints, tutorials, and open spaces with signup caps. Includes per-activity dashboards with attendee export and waitlist promotion. +- **Travel Grants** -- Review applications, send messages, and record disbursements. Includes receipt review queue with approve/flag workflow. diff --git a/docs/pretalx-integration.md b/docs/pretalx-integration.md index 7c91f21..0639c04 100644 --- a/docs/pretalx-integration.md +++ b/docs/pretalx-integration.md @@ -58,6 +58,173 @@ consumes `PretalxClient` and maps the typed dataclasses into Django ORM models `src/django_program/pretalx/client.py` bridges the workspace package into the Django app's import namespace. +## Overrides + +Pretalx is the source of truth for your schedule, but the real world does not +care about your source of truth. Speakers cancel at the last minute. Rooms get +renamed. A talk moves from Hall A to Hall B because of a fire alarm. The +override system lets organizers patch any synced field locally without touching +the upstream Pretalx record, and those patches survive re-syncs. + +### How Overrides Work + +Each synced model (`Talk`, `Speaker`, `Room`) has a matching one-to-one override +model (`TalkOverride`, `SpeakerOverride`, `RoomOverride`). Override fields +default to blank/null. When a field is populated, it takes priority over the +synced value. When blank, the original synced value shows through. + +The synced models expose `effective_*` properties that handle this resolution: + +```python +talk = Talk.objects.get(pretalx_code="ABCDEF") + +# Returns the synced title unless a TalkOverride sets override_title +talk.effective_title + +# Returns "cancelled" if is_cancelled=True on the override, +# otherwise override_state if set, otherwise the synced state +talk.effective_state +``` + +Use `effective_*` properties in templates and views. Use the bare fields +(`talk.title`, `talk.state`) only when you need the raw synced value. + +### Override Models + +#### TalkOverride + +One-to-one with `Talk`. Overridable fields: + +| Field | Type | Effect | +|------------------------|---------------|--------------------------------------------------| +| `override_title` | `CharField` | Replaces `talk.effective_title` | +| `override_abstract` | `TextField` | Replaces `talk.effective_abstract` | +| `override_state` | `CharField` | Replaces `talk.effective_state` | +| `override_room` | `ForeignKey` | Replaces `talk.effective_room` | +| `override_slot_start` | `DateTimeField` | Replaces `talk.effective_slot_start` | +| `override_slot_end` | `DateTimeField` | Replaces `talk.effective_slot_end` | +| `is_cancelled` | `BooleanField` | Forces `talk.effective_state` to `"cancelled"` | +| `note` | `TextField` | Internal-only note (not displayed to attendees) | + +The `is_cancelled` flag takes priority over `override_state`. If both are set, +the talk shows as cancelled. + +#### SpeakerOverride + +One-to-one with `Speaker`. Overridable fields: + +| Field | Type | Effect | +|------------------------|-------------|-------------------------------------------| +| `override_name` | `CharField` | Replaces `speaker.effective_name` | +| `override_biography` | `TextField` | Replaces `speaker.effective_biography` | +| `override_avatar_url` | `URLField` | Replaces `speaker.effective_avatar_url` | +| `override_email` | `EmailField`| Replaces `speaker.effective_email` | +| `note` | `TextField` | Internal-only note | + +#### RoomOverride + +One-to-one with `Room`. Overridable fields: + +| Field | Type | Effect | +|-------------------------|------------------------|------------------------------------------| +| `override_name` | `CharField` | Replaces `room.effective_name` | +| `override_description` | `TextField` | Replaces `room.effective_description` | +| `override_capacity` | `PositiveIntegerField` | Replaces `room.effective_capacity` | +| `note` | `TextField` | Internal-only note | + +### SubmissionTypeDefault + +`SubmissionTypeDefault` is not an override in the same sense. It provides +fallback room and time-slot values for talks of a given Pretalx submission type +(e.g. "Poster", "Tutorial") that arrive from Pretalx without scheduling data. + +| Field | Type | Purpose | +|----------------------|--------------|--------------------------------------------------| +| `submission_type` | `CharField` | The Pretalx type name to match (case-sensitive) | +| `default_room` | `ForeignKey` | Room assigned to unscheduled talks of this type | +| `default_date` | `DateField` | Date for synthesized slot times | +| `default_start_time` | `TimeField` | Start time combined with `default_date` | +| `default_end_time` | `TimeField` | End time combined with `default_date` | + +Type defaults are applied automatically at the end of `sync_all()` via +`apply_type_defaults()`. They only affect talks where `room` is `None`, so +talks already assigned a room by Pretalx are left untouched. + +### Creating Overrides via Django Admin + +All override models are registered in the Django admin. Navigate to +**Pretalx > Talk Overrides** (or Speaker Overrides, Room Overrides) to create +or edit overrides. + +#### Cancel a talk + +1. Go to **Pretalx > Talk Overrides > Add**. +2. Select the talk from the `talk` field. +3. Check `is_cancelled`. +4. Add a note explaining why (e.g. "Speaker flight cancelled"). +5. Save. The talk's `effective_state` now returns `"cancelled"`. + +#### Move a talk to a different room + +1. Go to **Pretalx > Talk Overrides > Add**. +2. Select the talk. +3. Set `override_room` to the new room. +4. Optionally adjust `override_slot_start` and `override_slot_end` if the time + also changed. +5. Save. + +#### Rename a room on-site + +1. Go to **Pretalx > Room Overrides > Add**. +2. Select the room. +3. Set `override_name` to the correct name (e.g. "Hall B" instead of + "Ballroom 2"). +4. Save. All schedule displays using `room.effective_name` now show the new + name. + +#### Set up defaults for poster sessions + +1. Go to **Pretalx > Submission Type Defaults > Add**. +2. Set `submission_type` to `"Poster"` (must match the Pretalx type exactly). +3. Set `default_room` to the poster hall. +4. Set `default_date`, `default_start_time`, and `default_end_time` for the + poster session window. +5. Save. On the next sync, any poster talks arriving without scheduling data + get assigned to this room and time slot. + +### Overrides and Sync + +Overrides are stored in separate database tables from the synced data. When +`sync_all()` runs, it updates the `Talk`, `Speaker`, and `Room` tables with +fresh data from Pretalx. The override tables are not touched. This means: + +- Overrides persist across syncs. A `TalkOverride` created before a sync is + still there after the sync completes. +- The synced (base) fields get updated to match Pretalx. If Pretalx changes a + talk's title, the `Talk.title` field updates, but `TalkOverride.override_title` + stays as-is. The `effective_title` property still returns the override value. +- Override resolution happens at read time via the `effective_*` properties, not + during sync. There is no merge step. +- Deleting an override restores the synced values instantly. The base data was + never modified. + +The `conference` field on each override is validated against the parent entity's +conference. The admin and model validation both reject overrides that reference +entities from a different conference. + +### The `is_empty` Property + +Each override model has an `is_empty` property that returns `True` when no +override fields carry a value (only the `note` field is populated, or everything +is blank). Use this to identify overrides that were created but never filled in: + +```python +stale = TalkOverride.objects.all() +for override in stale: + if override.is_empty: + override.delete() +``` + ## Schema Regeneration ### Prerequisites diff --git a/docs/programs-activities.md b/docs/programs-activities.md new file mode 100644 index 0000000..f1e9aab --- /dev/null +++ b/docs/programs-activities.md @@ -0,0 +1,158 @@ +# Programs & Activities + +The `django_program.programs` app handles conference activities (sprints, tutorials, open spaces) and travel grant applications. All views are scoped to a conference via the `conference_slug` URL parameter and gated behind feature flags. + +## Travel Grants + +Travel grants follow the PyCon US model: attendees apply for financial assistance, reviewers evaluate and offer grants, and recipients submit receipts for reimbursement. + +### Application flow + +An attendee submits a {class}`~django_program.programs.models.TravelGrant` through the `TravelGrantApplyView`. The application form collects: + +- **Request type** -- ticket only, or ticket plus travel grant. +- **Travel details** -- origin city, international flag, first-time attendee status. +- **Travel plan breakdown** -- airfare and lodging with descriptions and estimated amounts. +- **Requested amount** -- total USD amount requested. Validated against `max_grant_amount` from `DJANGO_PROGRAM` settings (default `3000`). +- **Applicant profile** -- experience level, occupation, community involvement, and reason for the grant. +- **Days attending** -- which conference days the applicant plans to attend (see [Day Detection](#day-detection) below). + +Each user can submit one grant application per conference, enforced by a `unique_together` constraint on `(conference, user)`. + +### Day detection + +The `days_attending` field renders as a set of checkboxes with labels derived from the conference schedule. The form uses a two-tier strategy to build these labels: + +**Tier 1: Schedule-derived days** -- `get_conference_days()` in `django_program.programs.utils` queries `ScheduleSlot` records for the conference. For each unique date, it counts the `submission_type` of linked talks. When one type accounts for more than half the slots on a given day, that type name is appended to the date label: + +``` +Wednesday, May 14 (Tutorial) +Thursday, May 15 (Tutorial) +Friday, May 16 (Talk) +Saturday, May 17 (Talk) +Sunday, May 18 +``` + +**Tier 2: Section-based fallback** -- when no schedule slots exist (e.g., before a Pretalx sync), the form falls back to `_build_day_choices()`. This method iterates conference `Section` records and labels each date with the section name and day number: + +``` +Tutorials Day 1 -- Wednesday, May 14 +Tutorials Day 2 -- Thursday, May 15 +Talks Day 1 -- Friday, May 16 +Talks Day 2 -- Saturday, May 17 +Sprints Day 1 -- Sunday, May 18 +``` + +Days not covered by any section get a generic `Day N` label. + +The selected checkboxes are serialized to a comma-separated string for storage in `TravelGrant.days_attending`. On edit, the stored string is split back into checkbox selections. + +### Grant statuses and lifecycle + +The `TravelGrant.GrantStatus` choices define the full lifecycle: + +| Status | Description | Applicant can... | +|---|---|---| +| `SUBMITTED` | Application received, awaiting review. | Edit, withdraw, send messages. | +| `INFO_NEEDED` | Reviewers requested more information. | Provide info (resets to `SUBMITTED`), edit, withdraw. | +| `OFFERED` | Grant approved and offer extended. | Accept or decline. | +| `NEED_MORE` | Applicant requesting additional funds. | -- | +| `ACCEPTED` | Applicant accepted the offer. | Upload receipts, submit payment info. | +| `REJECTED` | Application denied. | -- | +| `DECLINED` | Applicant declined the offer. | -- | +| `WITHDRAWN` | Applicant withdrew the application. | -- | +| `DISBURSED` | Funds sent to the recipient. | -- | + +State transitions are enforced by property guards on the model (`is_editable`, `is_actionable`, `show_accept_button`, etc.) and checked in each view before allowing the action. + +### Receipts and payment info + +After accepting a grant, the recipient uploads expense receipts through `ReceiptUploadView`: + +- **Receipt types**: `AIRFARE` and `LODGING`. +- **File validation**: PDF, JPG, JPEG, or PNG, max 10 MB. +- **Review workflow**: receipts start as pending, can be approved or flagged by reviewers with a reason. +- **Deletion**: applicants can delete receipts that have not been approved or flagged. + +Payment information is collected via {class}`~django_program.programs.models.PaymentInfo`, a one-to-one model on the grant. Supported payment methods: + +- Zelle +- PayPal +- ACH (Direct Deposit) +- Wire Transfer +- Wise +- Check + +Sensitive fields (bank account numbers, routing numbers, email addresses) are stored using `EncryptedCharField` and `EncryptedTextField`. Method-specific fields are validated in `PaymentInfoForm.clean()` -- selecting PayPal requires a PayPal email, selecting ACH requires bank name, account number, and routing number, and so on. + +A grant is ready for disbursement (`is_ready_for_disbursement`) when its status is `ACCEPTED`, it has a `PaymentInfo` record, and at least one approved receipt exists. + +### Max grant amount + +The maximum requestable amount is set by `DJANGO_PROGRAM["max_grant_amount"]` (default `3000`). The `TravelGrantApplicationForm` validates this during `clean()` when the request type is `TICKET_AND_GRANT`. Configure it in your Django settings: + +```python +DJANGO_PROGRAM = { + "max_grant_amount": 5000, # USD + # ... +} +``` + +### Messaging + +Both applicants and reviewers can exchange messages on a grant via {class}`~django_program.programs.models.TravelGrantMessage`. Each message has a `visible` flag: + +- **Applicant messages** are always visible to reviewers. +- **Reviewer messages** with `visible=True` appear on the applicant's status page. +- **Reviewer messages** with `visible=False` are internal notes, visible only to other reviewers. + +### Admin management + +Reviewers manage grants through the management dashboard (`django_program.manage`). The `TravelGrantForm` in the dashboard exposes `status`, `approved_amount`, `promo_code`, and `reviewer_notes`. The `ReviewerMessageForm` lets reviewers send messages with visibility control. + +The disbursement flow uses `DisbursementForm` to record the actual amount sent and timestamp. + +Permissions are controlled by the `review_travel_grant` custom permission on the `TravelGrant` model and `review_receipt` on the `Receipt` model. + +## Activities + +Activities represent scheduled or drop-in events at the conference: sprints, workshops, tutorials, lightning talks, social events, open spaces, and summits. + +### Activity types + +The {class}`~django_program.programs.models.Activity` model supports these types via `ActivityType`: + +| Type | Label | +|---|---| +| `SPRINT` | Sprint | +| `WORKSHOP` | Workshop | +| `TUTORIAL` | Tutorial | +| `LIGHTNING_TALK` | Lightning Talk | +| `SOCIAL` | Social Event | +| `OPEN_SPACE` | Open Space | +| `SUMMIT` | Summit | +| `OTHER` | Other | + +### Signup caps and waitlisting + +Set `max_participants` on an activity to limit signups. When `max_participants` is `None`, signups are unlimited. + +When an activity reaches capacity, new signups are created with status `WAITLISTED` instead of `CONFIRMED`. Cancelling a confirmed signup automatically promotes the oldest waitlisted signup via `promote_next_waitlisted()`, which runs inside a transaction with `select_for_update()` to prevent race conditions. + +```python +activity.spots_remaining # int | None -- None means unlimited +``` + +Each user can have at most one active (non-cancelled) signup per activity, enforced by a conditional `UniqueConstraint`. + +### Pretalx integration + +Activities can link to Pretalx submission types via the `pretalx_submission_type` field. During a Pretalx sync, all talks matching that submission type are added to the activity's `talks` M2M relationship. The activity detail page then displays linked talks grouped by day with their speakers and schedule slots. + +### API reference + +For full model, form, and view details, see the autodoc reference: + +- {mod}`django_program.programs.models` +- {mod}`django_program.programs.forms` +- {mod}`django_program.programs.views` diff --git a/docs/registration-flow.md b/docs/registration-flow.md index 4419f9a..171ac1f 100644 --- a/docs/registration-flow.md +++ b/docs/registration-flow.md @@ -18,6 +18,63 @@ Before getting into the flow, here are the models involved: | {class}`~django_program.registration.models.Payment` | A financial transaction against an order. Methods: `STRIPE`, `COMP`, `CREDIT`, `MANUAL`. | | {class}`~django_program.registration.models.Credit` | A store credit issued from a refund, applicable to future orders. | +## Global Ticket Capacity + +The `Conference.total_capacity` field sets a hard venue-wide cap on the number of tickets sold across all ticket types. Add-ons do not count toward this cap since they do not consume venue seats. + +### Setting the capacity + +Set `total_capacity` on the Conference model through any of these: + +- **Management dashboard** -- edit the conference and fill in the "Total capacity" field on the `ConferenceForm`. +- **Django admin** -- set the field directly on the Conference admin page. +- **TOML bootstrap** -- add `total_capacity = 2500` to the `[conference]` table in your config file. + +A value of `0` means unlimited (no global cap enforced). This is the default. + +### How enforcement works + +Global capacity is checked at two points in the registration flow: + +1. **Adding a ticket to the cart** -- `add_ticket()` calls `validate_global_capacity()` with the cart's current ticket count plus the new quantity. If the total exceeds the remaining global capacity, a `ValidationError` is raised. + +2. **Checkout** -- `CheckoutService.checkout()` re-validates global capacity for all ticket items in the cart via `_revalidate_global_capacity()`. This catches the case where capacity filled up between adding items and checking out. + +Both paths call the same underlying function in `django_program.registration.services.capacity`: + +```python +from django_program.registration.services.capacity import ( + get_global_remaining, + get_global_sold_count, +) + +# How many tickets have been sold (paid + pending with active hold)? +sold = get_global_sold_count(conference) + +# How many tickets are left? Returns None if capacity is unlimited. +remaining = get_global_remaining(conference) +``` + +### Sold count calculation + +`get_global_sold_count()` counts `OrderLineItem` quantities where: + +- The line item is a **ticket** (not an add-on), identified by `addon__isnull=True`. +- The order status is `PAID`, `PARTIALLY_REFUNDED`, or `PENDING` with `hold_expires_at` still in the future. + +Using `addon__isnull=True` instead of `ticket_type__isnull=False` is deliberate: if a ticket type is deleted (SET_NULL on the FK), its line items are still counted. This prevents overselling after administrative cleanup. + +### Concurrency safety + +`validate_global_capacity()` acquires a row-level lock on the Conference row with `select_for_update()` before reading the sold count. The caller must already be inside a `transaction.atomic` block. This prevents two concurrent requests from both seeing "1 ticket remaining" and both succeeding. + +### Error messages + +When capacity is exceeded, the user sees one of: + +- `"This conference is sold out (venue capacity: 2500)."` -- when zero tickets remain. +- `"Only 12 tickets remaining for this conference (venue capacity: 2500)."` -- when some tickets remain but fewer than requested. + ## Cart Lifecycle The cart service (`django_program.registration.services.cart`) is a collection of stateless functions. No classes to instantiate. From d9b7b7be3c22816c167fcc6f84abbffc65e69c42 Mon Sep 17 00:00:00 2001 From: Jacob Coffee Date: Sat, 14 Feb 2026 08:47:12 -0600 Subject: [PATCH 2/2] docs: address PR review comments on access control and grant prerequisites - Fix management dashboard access control: group-based access applies only to /financial/, not the whole /manage/ area - Clarify travel grant receipt/payment prereqs: requires ACCEPTED status AND non-zero approved_amount set by reviewer Co-Authored-By: Claude Opus 4.6 --- docs/management-dashboard.md | 5 +++-- docs/programs-activities.md | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/management-dashboard.md b/docs/management-dashboard.md index 93e00a0..2780daf 100644 --- a/docs/management-dashboard.md +++ b/docs/management-dashboard.md @@ -2,11 +2,12 @@ The management dashboard is the organizer-facing interface for administering a conference. It lives under the `/manage/` URL prefix (configured in your project's `urls.py`) and requires authentication. All conference-scoped pages sit under `/manage//`. -Access is restricted to users who meet at least one of these criteria: +Access to the standard management views is restricted to users who meet at least one of these criteria: - Django superuser - Holds the `program_conference.change_conference` permission -- Belongs to a designated permission group (the financial dashboard additionally accepts the "Program: Finance & Accounting" group) + +The financial overview dashboard at `/manage//financial/` additionally allows access to users who belong to the "Program: Finance & Accounting" group. The sidebar organizes features into sections: **Content** (sections, rooms, speakers, talks, schedule, overrides), **Registration** (financial overview, orders, ticket types, add-ons, vouchers), **Sponsors** (levels, sponsors), and **Programs** (activities, travel grants). diff --git a/docs/programs-activities.md b/docs/programs-activities.md index f1e9aab..6ef5c68 100644 --- a/docs/programs-activities.md +++ b/docs/programs-activities.md @@ -67,7 +67,7 @@ State transitions are enforced by property guards on the model (`is_editable`, ` ### Receipts and payment info -After accepting a grant, the recipient uploads expense receipts through `ReceiptUploadView`: +After accepting a grant, the recipient uploads expense receipts through `ReceiptUploadView`. Both receipt uploads and payment info submission require the grant to be in `ACCEPTED` status **and** have a non-zero `approved_amount` set by a reviewer. Until a reviewer sets the approved amount, the receipt and payment sections are locked. - **Receipt types**: `AIRFARE` and `LODGING`. - **File validation**: PDF, JPG, JPEG, or PNG, max 10 MB.