feat: add sponsors app with models, admin, signals, views, and manage UI#21
feat: add sponsors app with models, admin, signals, views, and manage UI#21JacobCoffee merged 4 commits intomainfrom
Conversation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 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>
- 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>
There was a problem hiding this comment.
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.
| 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, |
There was a problem hiding this comment.
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).
| 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 |
There was a problem hiding this comment.
_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.
| 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 |
Summary
sponsorsDjango app withSponsorLevel,Sponsor, andSponsorBenefitmodelspost_savesignal for automatic comp voucher generation when sponsors are created (based onlevel.comp_ticket_count)/manage/dashboard (sponsor levels + sponsors) with sidebar navigation and dashboard stat cardSponsorBenefitInlineonSponsorAdmin