Skip to content

Commit de3fab8

Browse files
JacobCoffeeclaude
andcommitted
feat: sponsor directory and asset browser guide documentation
Adds a Sponsors page (More > Sponsors) to browse and search all sponsor companies with contact count, sponsorship count, location, website, and quick links to edit or create a sponsorship via the Composer. Also adds the Assets section to the guide documenting the grouped asset browser, filters, and Send Reminder feature. Updates sponsor edit to redirect back to the sponsor directory. Includes 4 tests. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 6409ab7 commit de3fab8

File tree

6 files changed

+206
-3
lines changed

6 files changed

+206
-3
lines changed

apps/sponsors/manage/tests.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2533,3 +2533,43 @@ def test_shows_active_sponsorship_assets(self):
25332533
def test_nav_has_assets_link(self):
25342534
response = self.client.get(reverse("manage_dashboard"))
25352535
self.assertContains(response, "Assets")
2536+
2537+
2538+
class SponsorListViewTests(SponsorManageTestBase):
2539+
"""Test sponsor directory view."""
2540+
2541+
@classmethod
2542+
def setUpTestData(cls):
2543+
super().setUpTestData()
2544+
cls.sponsor = Sponsor.objects.create(name="Acme Corp", city="Portland", country="US")
2545+
cls.sponsor2 = Sponsor.objects.create(name="Beta Inc", city="London", country="GB")
2546+
2547+
def setUp(self):
2548+
super().setUp()
2549+
self.client.login(username="staff", password="pass")
2550+
2551+
def test_list_loads(self):
2552+
response = self.client.get(reverse("manage_sponsors"))
2553+
self.assertEqual(response.status_code, 200)
2554+
self.assertContains(response, "Acme Corp")
2555+
self.assertContains(response, "Beta Inc")
2556+
2557+
def test_search(self):
2558+
response = self.client.get(reverse("manage_sponsors") + "?search=Acme")
2559+
self.assertContains(response, "Acme Corp")
2560+
self.assertNotContains(response, "Beta Inc")
2561+
2562+
def test_shows_sponsorship_count(self):
2563+
Sponsorship.objects.create(
2564+
sponsor=self.sponsor,
2565+
submited_by=self.staff_user,
2566+
package=self.package,
2567+
year=self.year,
2568+
status=Sponsorship.APPLIED,
2569+
)
2570+
response = self.client.get(reverse("manage_sponsors"))
2571+
self.assertEqual(response.status_code, 200)
2572+
2573+
def test_nav_has_sponsors_link(self):
2574+
response = self.client.get(reverse("manage_dashboard"))
2575+
self.assertContains(response, "Sponsors")

apps/sponsors/manage/urls.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,8 @@
9696
views.AssetExportView.as_view(),
9797
name="manage_sponsorship_export_assets",
9898
),
99-
# Sponsor (company) create/edit
99+
# Sponsor directory + create/edit
100+
path("sponsors/", views.SponsorListView.as_view(), name="manage_sponsors"),
100101
path("sponsors/new/", views.SponsorCreateView.as_view(), name="manage_sponsor_create"),
101102
path("sponsors/<int:pk>/edit/", views.SponsorEditView.as_view(), name="manage_sponsor_edit"),
102103
# Sponsor contacts

apps/sponsors/manage/views.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -555,6 +555,36 @@ def get_context_data(self, **kwargs):
555555
return context
556556

557557

558+
# ── Sponsor Directory ─────────────────────────────────────────────────
559+
560+
561+
class SponsorListView(SponsorshipAdminRequiredMixin, ListView):
562+
"""Browse and search all sponsors."""
563+
564+
template_name = "sponsors/manage/sponsor_list.html"
565+
context_object_name = "sponsors"
566+
paginate_by = 50
567+
568+
def get_queryset(self):
569+
"""Return sponsors filtered by search, annotated with sponsorship count."""
570+
from django.db.models import Count
571+
572+
qs = Sponsor.objects.annotate(
573+
sponsorship_count=Count("sponsorship"),
574+
contact_count=Count("contacts"),
575+
).order_by("name")
576+
self.filter_search = self.request.GET.get("search", "")
577+
if self.filter_search:
578+
qs = qs.filter(name__icontains=self.filter_search)
579+
return qs
580+
581+
def get_context_data(self, **kwargs):
582+
"""Return context with search term."""
583+
context = super().get_context_data(**kwargs)
584+
context["filter_search"] = self.filter_search
585+
return context
586+
587+
558588
class PackageListView(SponsorshipAdminRequiredMixin, ListView):
559589
"""List sponsorship packages grouped by year."""
560590

@@ -1127,7 +1157,7 @@ def get_success_url(self):
11271157
sp_pk = self.request.POST.get("from_sponsorship") or self.request.GET.get("from_sponsorship")
11281158
if sp_pk:
11291159
return reverse("manage_sponsorship_detail", args=[sp_pk])
1130-
return reverse("manage_sponsorships")
1160+
return reverse("manage_sponsors")
11311161

11321162

11331163
# ── Benefit management on sponsorships ────────────────────────────────

apps/sponsors/templates/sponsors/manage/_base.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -679,6 +679,7 @@
679679
<div class="manage-nav-more" style="position:relative;">
680680
<a href="#" class="{% if active_tab == 'clone' or active_tab == 'guide' %}active{% endif %}" onclick="event.preventDefault();this.parentElement.classList.toggle('open');">More &#9662;</a>
681681
<div class="manage-nav-dropdown">
682+
<a href="{% url 'manage_sponsors' %}">Sponsors</a>
682683
<a href="{% url 'manage_legal_clauses' %}">Legal Clauses</a>
683684
<a href="{% url 'manage_assets' %}">Assets</a>
684685
<a href="{% url 'manage_clone_year' %}">Clone Year</a>

apps/sponsors/templates/sponsors/manage/guide.html

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ <h2 style="font-size:22px;font-weight:700;color:#1a1a2e;margin:0 0 8px;">Sponsor
2323
<a href="#notifications" style="color:#3776ab;text-decoration:none;">Notifications</a><br>
2424
<a href="#sponsors-contacts" style="color:#3776ab;text-decoration:none;">Sponsors &amp; Contacts</a><br>
2525
<a href="#renewals" style="color:#3776ab;text-decoration:none;">Renewals &amp; Expiry</a><br>
26+
<a href="#assets" style="color:#3776ab;text-decoration:none;">Assets</a><br>
2627
<a href="#bulk" style="color:#3776ab;text-decoration:none;">Bulk Actions &amp; Export</a><br>
2728
<a href="#admin-only" style="color:#3776ab;text-decoration:none;">What's Still in Django Admin</a><br>
2829
</div>
@@ -266,7 +267,7 @@ <h3 style="font-size:16px;font-weight:700;color:#3776ab;border-bottom:2px solid
266267
<h3 style="font-size:16px;font-weight:700;color:#3776ab;border-bottom:2px solid #3776ab;padding-bottom:6px;">Sponsors &amp; Contacts</h3>
267268

268269
<p style="font-size:13px;line-height:1.8;color:#444;">
269-
A sponsor is the company entity. Create new sponsors from the Composer (step 1) or from the <a href="{% url 'manage_sponsor_create' %}" style="color:#3776ab;">direct create page</a>. Edit a sponsor's company info (name, description, website, address, phone) from their sponsorship detail page by clicking "Edit Sponsor."
270+
A sponsor is the company entity. Browse all sponsors from <a href="{% url 'manage_sponsors' %}" style="color:#3776ab;">More &rarr; Sponsors</a> &mdash; search by name, see contact and sponsorship counts, and jump to edit or create a new sponsorship. You can also create new sponsors from the Composer (step 1) or from the <a href="{% url 'manage_sponsor_create' %}" style="color:#3776ab;">direct create page</a>. Edit a sponsor's company info (name, description, website, address, phone) from their sponsorship detail page by clicking "Edit Sponsor."
270271
</p>
271272

272273
<p style="font-size:13px;line-height:1.8;color:#444;">
@@ -311,6 +312,25 @@ <h3 style="font-size:16px;font-weight:700;color:#3776ab;border-bottom:2px solid
311312
</div>
312313
</div>
313314

315+
<!-- Assets -->
316+
<div class="manage-section" id="assets">
317+
<h3 style="font-size:16px;font-weight:700;color:#3776ab;border-bottom:2px solid #3776ab;padding-bottom:6px;">Assets</h3>
318+
319+
<p style="font-size:13px;line-height:1.8;color:#444;">
320+
The <a href="{% url 'manage_assets' %}" style="color:#3776ab;">Asset Browser</a> (under More) shows all sponsor-submitted assets grouped by company. Each company section has a submitted/total badge:
321+
</p>
322+
323+
<ul style="font-size:13px;line-height:2;color:#444;padding-left:20px;">
324+
<li><span class="tag tag-green" style="font-size:10px;">green</span> &mdash; all assets submitted</li>
325+
<li><span class="tag tag-gold" style="font-size:10px;">orange</span> &mdash; partially submitted</li>
326+
<li><span class="tag tag-red" style="font-size:10px;">red</span> &mdash; nothing submitted</li>
327+
</ul>
328+
329+
<p style="font-size:13px;line-height:1.8;color:#444;">
330+
Filter by asset type (text, image, file, response), related object (sponsor vs sponsorship), submission status, or search by internal name. Assets from expired or rejected sponsorships are automatically hidden. Companies with missing assets show a <strong>"Send Reminder"</strong> button that links to the notification page for that sponsorship.
331+
</p>
332+
</div>
333+
314334
<!-- Bulk Actions & Export -->
315335
<div class="manage-section" id="bulk">
316336
<h3 style="font-size:16px;font-weight:700;color:#3776ab;border-bottom:2px solid #3776ab;padding-bottom:6px;">Bulk Actions &amp; Export</h3>
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
{% extends "sponsors/manage/_base.html" %}
2+
3+
{% block page_title %}Sponsors | Sponsor Management{% endblock %}
4+
5+
{% with active_tab="more" %}
6+
7+
{% block manage_breadcrumbs %}
8+
<div class="manage-crumbs">
9+
<a href="{% url 'manage_dashboard' %}">Dashboard</a>
10+
<span class="sep">/</span>
11+
<span class="current">Sponsors</span>
12+
</div>
13+
{% endblock %}
14+
15+
{% block topbar_actions %}
16+
<a href="{% url 'manage_sponsor_create' %}" class="btn btn-sm btn-primary">+ New Sponsor</a>
17+
{% endblock %}
18+
19+
{% block manage_content %}
20+
<!-- Search bar -->
21+
<div class="manage-filter-bar" style="padding:12px 20px;">
22+
<form method="get" style="display:flex;gap:10px;align-items:flex-end;flex-wrap:wrap;">
23+
<div class="filter-field">
24+
<input type="text" name="search" placeholder="Search by name..." value="{{ filter_search }}" style="width:240px;" autofocus>
25+
</div>
26+
<div class="filter-actions">
27+
<button type="submit" class="btn btn-sm btn-secondary">Search</button>
28+
{% if filter_search %}
29+
<a href="{% url 'manage_sponsors' %}" class="btn btn-sm btn-secondary">Clear</a>
30+
{% endif %}
31+
</div>
32+
<div style="margin-left:auto;font-size:12px;color:#777;">
33+
{{ page_obj.paginator.count }} sponsor{{ page_obj.paginator.count|pluralize }}
34+
</div>
35+
</form>
36+
</div>
37+
38+
{% if sponsors %}
39+
<table class="manage-table">
40+
<thead>
41+
<tr>
42+
<th>Company</th>
43+
<th>Location</th>
44+
<th style="text-align:center;">Contacts</th>
45+
<th style="text-align:center;">Sponsorships</th>
46+
<th>Website</th>
47+
<th style="width:140px;text-align:right;">Actions</th>
48+
</tr>
49+
</thead>
50+
<tbody>
51+
{% for sponsor in sponsors %}
52+
<tr>
53+
<td>
54+
<a href="{% url 'manage_sponsor_edit' sponsor.pk %}" style="font-weight:600;color:#1a1a2e;text-decoration:none;">
55+
{{ sponsor.name }}
56+
</a>
57+
</td>
58+
<td style="font-size:12px;color:#777;">
59+
{% if sponsor.city %}{{ sponsor.city }}{% endif %}{% if sponsor.city and sponsor.country %}, {% endif %}{% if sponsor.country %}{{ sponsor.country }}{% endif %}
60+
</td>
61+
<td style="text-align:center;">
62+
{% if sponsor.contact_count %}
63+
<span class="tag tag-blue" style="font-size:10px;">{{ sponsor.contact_count }}</span>
64+
{% else %}
65+
<span style="color:#ccc;">&mdash;</span>
66+
{% endif %}
67+
</td>
68+
<td style="text-align:center;">
69+
{% if sponsor.sponsorship_count %}
70+
<a href="{% url 'manage_sponsorships' %}?search={{ sponsor.name|urlencode }}" class="tag tag-green" style="font-size:10px;text-decoration:none;">{{ sponsor.sponsorship_count }}</a>
71+
{% else %}
72+
<span style="color:#ccc;">&mdash;</span>
73+
{% endif %}
74+
</td>
75+
<td style="font-size:12px;">
76+
{% if sponsor.landing_page_url %}
77+
<a href="{{ sponsor.landing_page_url }}" target="_blank" style="color:#3776ab;text-decoration:none;">{{ sponsor.landing_page_url|truncatechars:30 }}</a>
78+
{% else %}
79+
<span style="color:#ccc;">&mdash;</span>
80+
{% endif %}
81+
</td>
82+
<td style="text-align:right;">
83+
<a href="{% url 'manage_sponsor_edit' sponsor.pk %}" style="font-size:12px;color:#3776ab;text-decoration:none;margin-right:8px;">Edit</a>
84+
<a href="{% url 'manage_composer' %}?new=1&sponsor_id={{ sponsor.pk }}" style="font-size:12px;color:#3776ab;text-decoration:none;">+ Sponsorship</a>
85+
</td>
86+
</tr>
87+
{% endfor %}
88+
</tbody>
89+
</table>
90+
91+
{% if is_paginated %}
92+
<div style="display:flex;justify-content:center;gap:4px;margin-top:20px;">
93+
{% if page_obj.has_previous %}
94+
<a href="?page={{ page_obj.previous_page_number }}{% if filter_search %}&search={{ filter_search }}{% endif %}" class="btn btn-sm btn-secondary">&larr; Prev</a>
95+
{% endif %}
96+
<span style="padding:6px 12px;font-size:13px;color:#555;">{{ page_obj.number }} / {{ page_obj.paginator.num_pages }}</span>
97+
{% if page_obj.has_next %}
98+
<a href="?page={{ page_obj.next_page_number }}{% if filter_search %}&search={{ filter_search }}{% endif %}" class="btn btn-sm btn-secondary">Next &rarr;</a>
99+
{% endif %}
100+
</div>
101+
{% endif %}
102+
103+
{% else %}
104+
<div class="empty-state">
105+
<div class="empty-icon">&#127970;</div>
106+
<p>{% if filter_search %}No sponsors match "{{ filter_search }}"{% else %}No sponsors found{% endif %}</p>
107+
</div>
108+
{% endif %}
109+
{% endblock %}
110+
111+
{% endwith %}

0 commit comments

Comments
 (0)