Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions examples/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,7 @@
"base_url": os.environ.get("PRETALX_BASE_URL", "https://pretalx.com"),
"token": os.environ.get("PRETALX_TOKEN", ""),
},
"psf_sponsors": {
"token": os.environ.get("PSF_SPONSOR_API_TOKEN", ""),
},
}
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ classifiers = [
dependencies = [
"django>=5.2",
"django-fernet-encrypted-fields>=0.3.1",
"pillow>=12.1.1",
"pretalx-client>=0.1.0",
"stripe>=12.0.0",
]
Expand Down
49 changes: 49 additions & 0 deletions src/django_program/manage/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

from django_program.conference.models import Conference, Section
from django_program.pretalx.models import Room, ScheduleSlot, Talk
from django_program.sponsors.models import Sponsor, SponsorLevel


class ImportFromPretalxForm(forms.Form):
Expand Down Expand Up @@ -187,3 +188,51 @@ def __init__(self, *args: object, **kwargs: object) -> None:
for field_name in self.SYNCED_FIELDS:
if field_name in self.fields:
self.fields[field_name].disabled = True


class SponsorLevelForm(forms.ModelForm):
"""Form for editing a sponsor level."""

class Meta:
model = SponsorLevel
fields = ["name", "cost", "description", "benefits_summary", "comp_ticket_count", "order"]


class SponsorForm(forms.ModelForm):
"""Form for editing a sponsor.

When the sponsor has an ``external_id`` (synced from the PSF API),
fields that come from the upstream API are disabled to prevent
overwriting synced data.
"""

SYNCED_FIELDS: list[str] = [
"name",
"level",
"website_url",
"logo_url",
"description",
]

class Meta:
model = Sponsor
fields = [
"name",
"level",
"website_url",
"logo",
"logo_url",
"description",
"contact_name",
"contact_email",
"is_active",
]

def __init__(self, *args: object, **kwargs: object) -> None:
"""Initialise the form and disable synced fields when locked by PSF sync."""
self.is_synced: bool = kwargs.pop("is_synced", False) # type: ignore[arg-type]
super().__init__(*args, **kwargs)
if self.is_synced:
for field_name in self.SYNCED_FIELDS:
if field_name in self.fields:
self.fields[field_name].disabled = True
Original file line number Diff line number Diff line change
Expand Up @@ -1041,6 +1041,21 @@
</li>
</ul>
</div>
<div class="sidebar-section">
<div class="sidebar-section-title">Sponsors</div>
<ul class="sidebar-nav">
<li>
<a href="{% url 'manage:sponsor-level-list' conference.slug %}" class="{% if active_nav == 'sponsor-levels' %}active{% endif %}">
<span class="sidebar-nav-icon">&#9733;</span> Levels
</a>
</li>
<li>
<a href="{% url 'manage:sponsor-manage-list' conference.slug %}" class="{% if active_nav == 'sponsors' %}active{% endif %}">
<span class="sidebar-nav-icon">&#9830;</span> Sponsors
</a>
</li>
</ul>
</div>
</nav>
{% endif %}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,12 @@ <h1>{{ conference.name }}</h1>
<div class="stat-card-label">Sections</div>
</a>
</div>
<div class="stat-card">
<a href="{% url 'manage:sponsor-manage-list' conference.slug %}">
<div class="stat-card-value">{{ stats.sponsors }}</div>
<div class="stat-card-label">Sponsors</div>
</a>
</div>
{% if stats.unscheduled_talks %}
<div class="stat-card" style="border-color: var(--color-warning-border, #fde68a); background: var(--color-warning-bg, #fffbeb);">
<a href="{% url 'manage:talk-list' conference.slug %}?scheduled=no">
Expand Down Expand Up @@ -186,6 +192,19 @@ <h2 class="section-heading">Pretalx Sync</h2>
</div>
{% endif %}

{% if has_psf_sponsor_sync %}
<h2 class="section-heading">PSF Sponsor Sync</h2>
<div class="sync-panel" style="background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--radius-md); padding: 1.5rem; box-shadow: var(--shadow-sm);">
<p style="margin-bottom: 1rem; color: var(--color-text-secondary); font-size: 0.9rem;">
Pull sponsor data from the Python Software Foundation sponsorship API.
</p>
<form method="post" action="{% url 'manage:sync-sponsors' conference.slug %}">
{% csrf_token %}
<button type="submit" class="btn btn-primary">Sync Sponsors from PSF</button>
</form>
</div>
{% endif %}

{% if conference.pretalx_event_slug %}
<script>
(function() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
{% extends "django_program/manage/base.html" %}

{% block title %}{% if is_create %}Add Sponsor{% else %}Edit Sponsor{% endif %}{% endblock %}

{% block breadcrumb %}
<nav class="breadcrumb" aria-label="Breadcrumb">
<a href="{% url 'manage:dashboard' conference.slug %}">Dashboard</a>
<span class="breadcrumb-separator"></span>
<a href="{% url 'manage:sponsor-manage-list' conference.slug %}">Sponsors</a>
<span class="breadcrumb-separator"></span>
<span>{% if is_create %}Add{% else %}Edit{% endif %}</span>
</nav>
{% endblock %}

{% block page_title %}
<h1>{% if is_create %}Add Sponsor{% else %}Edit Sponsor: {{ sponsor.name }}{% endif %}</h1>
{% endblock %}

{% block content %}
{% if is_synced %}
<div class="sync-banner">
<span>&#128274;</span>
This sponsor was synced from the PSF API (ID: {{ sponsor.external_id }}).
Synced fields are locked. Run a new sync to update PSF data.
</div>
{% endif %}

<div class="form-container">
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{% for field in form %}
<div class="form-group {% if is_synced and field.name in synced_fields %}form-group--locked{% endif %}">
<label for="{{ field.id_for_label }}">
{{ field.label }}
{% if is_synced and field.name in synced_fields %}<span class="lock-icon" title="Synced from PSF">&#128274;</span>{% endif %}
</label>
{{ field }}
{% if field.help_text %}
<span class="helptext">{{ field.help_text }}</span>
{% endif %}
{% if field.errors %}
<ul class="errorlist">
{% for error in field.errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
</div>
{% endfor %}
<div class="form-actions">
<button type="submit" class="btn btn-primary">{% if is_create %}Create Sponsor{% else %}Save Changes{% endif %}</button>
<a href="{% url 'manage:sponsor-manage-list' conference.slug %}" class="btn btn-secondary">Cancel</a>
</div>
</form>
</div>

{% if not is_create and benefits %}
<h2 class="section-heading" style="margin-top: 2rem;">Benefits</h2>
<table class="data-table">
<thead>
<tr>
<th>Benefit</th>
<th>Status</th>
<th>Notes</th>
</tr>
</thead>
<tbody>
{% for benefit in benefits %}
<tr>
<td>{{ benefit.name }}</td>
<td>
{% if benefit.is_complete %}
<span class="badge badge--active">Complete</span>
{% else %}
<span class="badge badge--warning">Pending</span>
{% endif %}
</td>
<td>{{ benefit.notes|default:"--" }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{% endblock %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{% extends "django_program/manage/base.html" %}

{% block title %}{% if is_create %}Add Sponsor Level{% else %}Edit Sponsor Level{% endif %}{% endblock %}

{% block breadcrumb %}
<nav class="breadcrumb" aria-label="Breadcrumb">
<a href="{% url 'manage:dashboard' conference.slug %}">Dashboard</a>
<span class="breadcrumb-separator"></span>
<a href="{% url 'manage:sponsor-level-list' conference.slug %}">Sponsor Levels</a>
<span class="breadcrumb-separator"></span>
<span>{% if is_create %}Add{% else %}Edit{% endif %}</span>
</nav>
{% endblock %}

{% block page_title %}
<h1>{% if is_create %}Add Sponsor Level{% else %}Edit Level: {{ level.name }}{% endif %}</h1>
{% endblock %}

{% block content %}
<div class="form-container">
<form method="post">
{% csrf_token %}
{% for field in form %}
<div class="form-group">
<label for="{{ field.id_for_label }}">{{ field.label }}</label>
{{ field }}
{% if field.help_text %}
<span class="helptext">{{ field.help_text }}</span>
{% endif %}
{% if field.errors %}
<ul class="errorlist">
{% for error in field.errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
</div>
{% endfor %}
<div class="form-actions">
<button type="submit" class="btn btn-primary">{% if is_create %}Create Level{% else %}Save Changes{% endif %}</button>
<a href="{% url 'manage:sponsor-level-list' conference.slug %}" class="btn btn-secondary">Cancel</a>
</div>
</form>
</div>
{% endblock %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{% extends "django_program/manage/base.html" %}

{% block title %}Sponsor Levels{% endblock %}

{% block page_title %}
<h1>Sponsor Levels</h1>
<p>Sponsorship tiers and pricing</p>
{% endblock %}

{% block page_actions %}
<a href="{% url 'manage:sponsor-level-add' conference.slug %}" class="btn btn-primary">Add Level</a>
{% endblock %}

{% block content %}
{% if levels %}
<table class="data-table">
<thead>
<tr>
<th>Name</th>
<th>Slug</th>
<th>Cost</th>
<th>Comp Tickets</th>
<th>Order</th>
<th>Sponsors</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for level in levels %}
<tr>
<td>{{ level.name }}</td>
<td><span class="mono">{{ level.slug }}</span></td>
<td>${{ level.cost }}</td>
<td>{{ level.comp_ticket_count }}</td>
<td>{{ level.order }}</td>
<td>{{ level.sponsor_count }}</td>
<td>
<a href="{% url 'manage:sponsor-level-edit' conference.slug level.pk %}" class="btn btn-sm btn-secondary">Edit</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="empty-state">
<p>No sponsor levels defined for this conference.</p>
<p><a href="{% url 'manage:sponsor-level-add' conference.slug %}" class="btn btn-primary">Add First Level</a></p>
</div>
{% endif %}
{% endblock %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
{% extends "django_program/manage/base.html" %}

{% block title %}Sponsors{% endblock %}

{% block page_title %}
<h1>Sponsors</h1>
<p>Conference sponsors and their benefits</p>
{% endblock %}

{% block page_actions %}
<a href="{% url 'manage:sponsor-add' conference.slug %}" class="btn btn-primary">Add Sponsor</a>
{% endblock %}

{% block content %}
{% if sponsors %}
<table class="data-table">
<thead>
<tr>
<th>Name</th>
<th>Level</th>
<th>Contact</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for sponsor in sponsors %}
<tr>
<td>{{ sponsor.name }}</td>
<td>{{ sponsor.level.name }}</td>
<td>{{ sponsor.contact_name|default:"--" }}</td>
<td>
{% if sponsor.is_active %}
<span class="badge badge--active">Active</span>
{% else %}
<span class="badge badge--inactive">Inactive</span>
{% endif %}
</td>
<td>
<a href="{% url 'manage:sponsor-edit' conference.slug sponsor.pk %}" class="btn btn-sm btn-secondary">Edit</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="empty-state">
<p>No sponsors for this conference yet.</p>
<p><a href="{% url 'manage:sponsor-add' conference.slug %}" class="btn btn-primary">Add First Sponsor</a></p>
</div>
{% endif %}
{% endblock %}
Loading
Loading