feat: add programs app with activities, signups, travel grants, and manage UI#22
feat: add programs app with activities, signups, travel grants, and manage UI#22JacobCoffee merged 8 commits intomainfrom
Conversation
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
543fca0 to
c224f92
Compare
There was a problem hiding this comment.
Pull request overview
This PR adds a comprehensive programs app to manage conference activities (sprints, workshops, tutorials, socials) and travel grant applications. The implementation includes public-facing views for activity discovery and signup, a full management dashboard for organizers, Django admin integration, and complete test coverage.
Changes:
- New
programsapp with Activity, ActivitySignup, and TravelGrant models supporting capacity tracking and grant lifecycle management - Public views at
/<conference_slug>/programs/for activity listing, detail, signup (auth-required), and travel grant applications - Management dashboard integration at
/manage/<conference_slug>/with activity CRUD, grant review, sidebar navigation, and dashboard stat cards - Django admin with inline signups and editable grant status
Reviewed changes
Copilot reviewed 21 out of 22 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
src/django_program/programs/models.py |
Defines Activity, ActivitySignup, and TravelGrant models with capacity tracking and unique constraints |
src/django_program/programs/views.py |
Public-facing views for activity listing/detail/signup and travel grant application |
src/django_program/programs/urls.py |
URL routing for programs app under conference slug |
src/django_program/programs/admin.py |
Django admin configuration with inline signups and list-editable grant status |
src/django_program/programs/templates/ |
Public-facing templates for activities and travel grant form |
src/django_program/manage/views.py |
Activity and travel grant management views with dashboard stats integration |
src/django_program/manage/urls.py |
Management URLs for activity CRUD and grant review |
src/django_program/manage/forms.py |
ActivityForm and TravelGrantForm for management interface |
src/django_program/manage/templates/ |
Management dashboard templates with sidebar nav and stat cards |
src/django_program/programs/migrations/0001_initial.py |
Initial migration creating all three models |
tests/test_programs/ |
Comprehensive test coverage (19 tests) for models and views |
tests/test_manage/test_programs_views.py |
Tests for management views including dashboard integration |
tests/urls.py |
URL configuration for test suite |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
src/django_program/programs/templates/django_program/programs/activity_list.html
Outdated
Show resolved
Hide resolved
src/django_program/manage/templates/django_program/manage/activity_list.html
Outdated
Show resolved
Hide resolved
…anage UI Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix race condition in ActivitySignupView with select_for_update - Fix N+1 queries in activity list views with Count annotation - Refactor TravelGrantApplyView to use ModelForm validation - Add server-side validation for negative amounts and empty fields - Handle duplicate travel grant applications gracefully - Add tests for negative amount, empty fields, and duplicate grants Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
c224f92 to
b57e778
Compare
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 23 out of 24 changed files in this pull request and generated 6 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
src/django_program/programs/templates/django_program/programs/activity_detail.html
Show resolved
Hide resolved
src/django_program/programs/templates/django_program/programs/travel_grant_form.html
Show resolved
Hide resolved
src/django_program/manage/templates/django_program/manage/travel_grant_list.html
Outdated
Show resolved
Hide resolved
src/django_program/programs/templates/django_program/programs/activity_detail.html
Outdated
Show resolved
Hide resolved
src/django_program/programs/templates/django_program/programs/activity_list.html
Outdated
Show resolved
Hide resolved
…t enhancements - Receipt model with file upload, approval/flagging workflow, and admin - PaymentInfo model with encrypted fields (django-fernet-encrypted-fields) - Disbursement lifecycle: accepted → disbursed with amount/date/processor tracking - Receipt upload page with sidebar summary, progress bar, drag-and-drop file input - Payment info page with conditional fields by method, encryption indicators - Manage receipt review queue with approve/flag actions - Manage disbursement UI with amount input on grant review page - Travel grant status page shows receipts/payment/disbursement state - Bootstrap seeding for travel grants with varied statuses - MEDIA_URL/MEDIA_ROOT and FIELD_ENCRYPTION_KEY in dev/test settings - Migrations 0003-0006 for activity enhancements, travel grant model, receipts, disbursement - 893 tests passing Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 43 out of 45 changed files in this pull request and generated 9 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| receipts = grant.receipts.all() | ||
| return render( | ||
| request, | ||
| self.template_name, | ||
| { | ||
| "conference": self.conference, | ||
| "grant": grant, | ||
| "form": form, | ||
| "receipts": receipts, | ||
| }, | ||
| ) |
There was a problem hiding this comment.
On receipt upload validation errors, the template is re-rendered without receipt_total. Since the page displays totals/coverage in both the sidebar and JS, this causes an incorrect $0 total even when receipts already exist. Compute receipt_total in this error path (same as in get()) and include it in the render context.
| """Sum of airfare and lodging breakdown amounts.""" | ||
| return self.travel_plans_airfare_amount + self.travel_plans_lodging_amount |
There was a problem hiding this comment.
travel_plans_total is used as the “total” in both applicant and reviewer UIs, but it currently sums only airfare + lodging and ignores transit/visa (and any other breakdown fields). This makes the displayed totals incorrect. Either include all breakdown amounts in the calculation or rename this property to reflect what it actually sums.
| """Sum of airfare and lodging breakdown amounts.""" | |
| return self.travel_plans_airfare_amount + self.travel_plans_lodging_amount | |
| """Sum of all travel plan breakdown amounts (airfare, lodging, transit, visa, etc.).""" | |
| total = Decimal("0") | |
| for field in self._meta.get_fields(): | |
| if ( | |
| isinstance(field, models.DecimalField) | |
| and field.name.startswith("travel_plans_") | |
| and field.name.endswith("_amount") | |
| ): | |
| value = getattr(self, field.name, None) | |
| if value is not None: | |
| total += value | |
| return total |
| grant = get_object_or_404(TravelGrant, pk=kwargs["pk"], conference=self.conference) | ||
| form = DisbursementForm(request.POST) | ||
| if form.is_valid() and grant.status == TravelGrant.GrantStatus.ACCEPTED: | ||
| grant.status = TravelGrant.GrantStatus.DISBURSED | ||
| grant.disbursed_amount = form.cleaned_data["disbursed_amount"] | ||
| grant.disbursed_at = timezone.now() | ||
| grant.disbursed_by = request.user | ||
| grant.save(update_fields=["status", "disbursed_amount", "disbursed_at", "disbursed_by"]) | ||
| display_name = grant.user.get_full_name() or grant.user.username | ||
| messages.success( | ||
| request, | ||
| f"Grant for {display_name} marked as disbursed (${grant.disbursed_amount}).", | ||
| ) | ||
| else: | ||
| messages.error(request, "Could not process disbursement.") |
There was a problem hiding this comment.
The disbursement endpoint allows marking a grant as disbursed whenever status is accepted, even if required prerequisites (payment info + at least one approved receipt) are missing. Since the model exposes is_ready_for_disbursement, consider enforcing that here (and return a specific error message when not ready) to prevent accidental premature disbursements.
| {% if grant.status == "accepted" %} | ||
| <div class="card" style="margin-bottom: 1.5rem; border-color: #bfdbfe;"> | ||
| <div style="padding: 0.85rem 1.5rem; background: #eff6ff; border-bottom: 1px solid #bfdbfe;"> | ||
| <span style="font-size: 0.78rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: #1e40af;">Disbursement</span> | ||
| </div> | ||
| <div class="card-body"> | ||
| <form method="post" action="{% url 'manage:travel-grant-disburse' conference.slug grant.pk %}"> | ||
| {% csrf_token %} | ||
| <div style="display: flex; align-items: flex-end; gap: 1rem; flex-wrap: wrap;"> | ||
| <div style="flex: 1; min-width: 200px;"> | ||
| <label style="display: block; font-size: 0.72rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: var(--color-text-muted); margin-bottom: 0.35rem;">Disbursement Amount</label> | ||
| <input type="number" name="disbursed_amount" step="0.01" value="{{ grant.approved_amount }}" style="width: 100%; padding: 0.55rem 0.75rem; border: 1px solid var(--color-border); border-radius: var(--radius-sm); font-family: var(--font-mono); font-size: 0.9rem;"> | ||
| </div> | ||
| <button type="submit" class="btn btn--primary" onclick="return confirm('Mark this grant as disbursed? This records that payment has been sent.');"> | ||
| Mark as Disbursed | ||
| </button> | ||
| </div> | ||
| </form> | ||
| </div> | ||
| </div> | ||
| {% endif %} |
There was a problem hiding this comment.
The “Mark as Disbursed” UI is shown for any accepted grant, even if it isn’t ready for disbursement (no payment info / no approved receipts). Consider gating this section on grant.is_ready_for_disbursement (or otherwise clearly indicating missing prerequisites) to prevent accidental disbursement actions.
| pending = ( | ||
| Receipt.objects.filter( | ||
| grant__conference=self.conference, | ||
| approved=False, | ||
| flagged=False, | ||
| ) | ||
| .select_related("grant__user") | ||
| .order_by("?") | ||
| .first() |
There was a problem hiding this comment.
order_by("?") does a full-table random sort and can become very slow as receipt volume grows. Consider selecting a pending receipt using a cheaper strategy (e.g., pick the smallest/oldest pending receipt, or select a random PK within the filtered set).
| {% if receipt.receipt_file %} | ||
| <a href="{{ receipt.receipt_file.url }}" target="_blank" class="file-link" title="View receipt">View</a> | ||
| {% endif %} |
There was a problem hiding this comment.
This link opens a user-uploaded file in a new tab (target="_blank") without rel="noopener noreferrer", which enables reverse-tabnabbing. Add rel="noopener noreferrer" to external/new-tab links.
| wrapper.addEventListener('drop', function() { | ||
| wrapper.style.borderColor = ''; | ||
| wrapper.style.background = ''; | ||
| }); |
There was a problem hiding this comment.
The drag-and-drop drop handler doesn’t call preventDefault(). Dropping a file can trigger the browser’s default behavior (navigating away to open the file), which is a poor UX and can cause data loss. Add an event parameter and call e.preventDefault() (and typically e.stopPropagation()) in the drop handler.
| <h2 class="section-heading">Receipt File</h2> | ||
| <div style="margin-bottom: 2rem; background: var(--color-bg); border: 1px solid var(--color-border-light); border-radius: var(--radius-sm); padding: 1rem;"> | ||
| {% if receipt.receipt_file %} | ||
| <p style="margin-bottom: 0.5rem;"><a href="{{ receipt.receipt_file.url }}" target="_blank" class="btn btn-sm btn-secondary">Open File</a></p> |
There was a problem hiding this comment.
This link opens a user-uploaded file in a new tab (target="_blank") without rel="noopener noreferrer", which enables reverse-tabnabbing. Add rel="noopener noreferrer" to new-tab links.
| <p style="margin-bottom: 0.5rem;"><a href="{{ receipt.receipt_file.url }}" target="_blank" class="btn btn-sm btn-secondary">Open File</a></p> | |
| <p style="margin-bottom: 0.5rem;"><a href="{{ receipt.receipt_file.url }}" target="_blank" rel="noopener noreferrer" class="btn btn-sm btn-secondary">Open File</a></p> |
|
|
||
| @pytest.mark.django_db | ||
| def test_provide_info_get(client: Client, conference: Conference, user: User): | ||
| grant = TravelGrant.objects.create( |
There was a problem hiding this comment.
Variable grant is not used.
Summary
programsDjango app withActivity,ActivitySignup, andTravelGrantmodelsWhat's Included
Models
spots_remainingpropertyPublic Views (
/programs/)Manage Dashboard (
/manage/)Admin
ActivityAdminwithActivitySignupInlineTravelGrantAdminwith list-editable statusTest plan
make ci)django_program.programsmodule🤖 Generated with Claude Code