Skip to content

feat: add sponsors app with models, admin, signals, views, and manage UI#21

Merged
JacobCoffee merged 4 commits intomainfrom
phase-5/sponsors-app
Feb 12, 2026
Merged

feat: add sponsors app with models, admin, signals, views, and manage UI#21
JacobCoffee merged 4 commits intomainfrom
phase-5/sponsors-app

Conversation

@JacobCoffee
Copy link
Owner

@JacobCoffee JacobCoffee commented Feb 12, 2026

Summary

  • Adds complete sponsors Django app with SponsorLevel, Sponsor, and SponsorBenefit models
  • Implements post_save signal for automatic comp voucher generation when sponsors are created (based on level.comp_ticket_count)
  • Adds public sponsor list/detail views and templates
  • Integrates full CRUD management views into the /manage/ dashboard (sponsor levels + sponsors) with sidebar navigation and dashboard stat card
  • Adds Django admin with SponsorBenefitInline on SponsorAdmin

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings February 12, 2026 01:49
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 61e0d1edac

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

This comment was marked as outdated.

JacobCoffee and others added 2 commits February 11, 2026 20:25
- Truncate voucher codes to fit 100-char max_length (slug can be 200)
- Add dispatch_uid to sponsor post_save signal connection
- Replace limit_choices_to F() with clean() for cross-model validation
- Fix N+1 query: annotate SponsorLevelListView with Count('sponsors')
- Public templates extend base.html and use {% regroup %} for O(n)
- Add PSF sponsor sync: config, profiles, sync service, mgmt command
- Lock synced fields in sponsor edit view
- Wire PSF_SPONSOR_API_TOKEN in examples/settings.py

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Files lost from staging after pre-commit hook failure in prior session.
Adds sponsor sync profiles (base/default/pyconus), SponsorSyncService,
sync_sponsors management command, migration for external_id/logo_url,
and comprehensive tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings February 12, 2026 02:29
@JacobCoffee JacobCoffee merged commit ce8bbab into main Feb 12, 2026
15 checks passed
@JacobCoffee JacobCoffee deleted the phase-5/sponsors-app branch February 12, 2026 02:31
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 40 out of 42 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +60 to +100
sponsor_id = str(placement.get("sponsor_id", ""))
sponsor_name = placement.get("sponsor", "")
sponsor_slug = placement.get("sponsor_slug", "")
level_name = placement.get("level_name", "")
level_order = int(placement.get("level_order", 0) or 0)
website_url = placement.get("sponsor_url", "")
logo_url = placement.get("logo", "")
description = placement.get("description", "")

if not sponsor_name:
continue

level, created = SponsorLevel.objects.get_or_create(
conference=self.conference,
name=level_name or "Sponsor",
defaults={"cost": 0, "order": level_order},
)
if not created and level.order != level_order:
level.order = level_order
level.save(update_fields=["order"])

sponsor = self._find_sponsor(sponsor_id, sponsor_name)
if sponsor is not None:
sponsor.name = sponsor_name
sponsor.slug = sponsor_slug or sponsor.slug
sponsor.level = level
sponsor.external_id = sponsor_id
sponsor.website_url = website_url or sponsor.website_url
sponsor.logo_url = logo_url or sponsor.logo_url
sponsor.description = description or sponsor.description
sponsor.save()
else:
Sponsor.objects.create(
conference=self.conference,
level=level,
name=sponsor_name,
slug=sponsor_slug,
external_id=sponsor_id,
website_url=website_url,
logo_url=logo_url,
description=description,
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In sync_sponsors(), several values pulled from the JSON payload use placement.get(key, ""). If the PSF API returns explicit nulls (JSON null -> Python None) for optional fields like sponsor_slug, sponsor_url, logo, or description, those variables become None and can later be assigned into SlugField/URLField/TextField during Sponsor.objects.create(...), which will raise validation/type errors. Normalize these fields to strings (e.g., placement.get(key) or "") and treat sponsor_id=None as empty rather than str(None).

Copilot uses AI. Check for mistakes.
Comment on lines +196 to +210
if external_id:
try:
return Sponsor.objects.get(
conference=self.conference,
external_id=external_id,
)
except Sponsor.DoesNotExist:
pass
try:
return Sponsor.objects.get(
conference=self.conference,
name=name,
)
except Sponsor.DoesNotExist:
return None
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_find_sponsor() uses Sponsor.objects.get(...) lookups by external_id and then by name. Since neither (conference, external_id) nor (conference, name) is constrained to be unique, existing data could contain duplicates and this will raise MultipleObjectsReturned, aborting the entire sync. Consider enforcing uniqueness at the model/db level for external_id (ideally scoped to conference) and/or switching these lookups to filter(...).order_by(...).first() with a deterministic tie-breaker.

Suggested change
if external_id:
try:
return Sponsor.objects.get(
conference=self.conference,
external_id=external_id,
)
except Sponsor.DoesNotExist:
pass
try:
return Sponsor.objects.get(
conference=self.conference,
name=name,
)
except Sponsor.DoesNotExist:
return None
base_qs = Sponsor.objects.filter(conference=self.conference)
if external_id:
sponsor = (
base_qs.filter(external_id=external_id)
.order_by("pk")
.first()
)
if sponsor is not None:
return sponsor
sponsor = base_qs.filter(name=name).order_by("pk").first()
return sponsor

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant