Conversation
apps/sponsors/manage/forms.py
Outdated
| MIN_YEAR = 2022 | ||
| MAX_YEAR = 2050 |
| count++; | ||
| }); | ||
| if (!count) html = '<div style="padding:10px 12px;font-size:13px;color:#999;">No matching benefits</div>'; | ||
| dropdown.innerHTML = html; |
Check failure
Code scanning / CodeQL
DOM text reinterpreted as HTML High
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 17 hours ago
In general, to fix this kind of issue you should stop building HTML by string concatenation with potentially tainted data and avoid assigning those strings to innerHTML. Instead, construct DOM elements with document.createElement, set attributes via setAttribute / dataset / property assignments, and set visible text with textContent. These APIs do not interpret the data as HTML, so meta‑characters are not treated as markup and cannot break out into scripts.
For this specific snippet in apps/sponsors/templates/sponsors/manage/sponsorship_detail.html, the safest approach is to:
- Remove the
htmlstring accumulation and thedropdown.innerHTML = html;assignment. - In
renderDropdown, clear the existing children ofdropdownand then:- If there are no matches, create a single
<div>node, set itstextContentto"No matching benefits", and append it. - Otherwise, for each matching
opt:- Create a
<div>element. - Add the
bs-optclass (andbs-selwhenhighlighted). - Set
data-valusingel.dataset.val = opt.value;. - Apply the inline styles via
styleproperties orsetAttribute. - Set
el.textContent = opt.text;so the label text is inserted without HTML interpretation. - Append the element to
dropdown.
- Create a
- If there are no matches, create a single
- Leave the event listeners as they are, but bind them to the newly created elements.
This preserves all existing behavior (styling, selection, event handling, filtering) while eliminating the dangerous re-interpretation of DOM text as HTML.
| @@ -459,20 +459,47 @@ | ||
| } | ||
|
|
||
| function renderDropdown(filter) { | ||
| var html = ''; | ||
| var q = (filter || '').toLowerCase(); | ||
| var count = 0; | ||
|
|
||
| // Clear existing dropdown content | ||
| while (dropdown.firstChild) { | ||
| dropdown.removeChild(dropdown.firstChild); | ||
| } | ||
|
|
||
| options.forEach(function(opt) { | ||
| if (q && opt.text.toLowerCase().indexOf(q) === -1) return; | ||
| var highlighted = opt.value === select.value; | ||
| html += '<div class="bs-opt' + (highlighted ? ' bs-sel' : '') + '" data-val="' + opt.value + '"' | ||
| + ' style="padding:7px 12px;font-size:13px;cursor:pointer;border-bottom:1px solid #f5f5f5;' | ||
| + (highlighted ? 'background:#e8f0fe;font-weight:600;' : '') | ||
| + '">' + opt.text + '</div>'; | ||
|
|
||
| var item = document.createElement('div'); | ||
| item.classList.add('bs-opt'); | ||
| if (highlighted) { | ||
| item.classList.add('bs-sel'); | ||
| } | ||
| item.dataset.val = opt.value; | ||
| item.style.padding = '7px 12px'; | ||
| item.style.fontSize = '13px'; | ||
| item.style.cursor = 'pointer'; | ||
| item.style.borderBottom = '1px solid #f5f5f5'; | ||
| if (highlighted) { | ||
| item.style.background = '#e8f0fe'; | ||
| item.style.fontWeight = '600'; | ||
| } | ||
| item.textContent = opt.text; | ||
|
|
||
| dropdown.appendChild(item); | ||
| count++; | ||
| }); | ||
| if (!count) html = '<div style="padding:10px 12px;font-size:13px;color:#999;">No matching benefits</div>'; | ||
| dropdown.innerHTML = html; | ||
|
|
||
| if (!count) { | ||
| var empty = document.createElement('div'); | ||
| empty.style.padding = '10px 12px'; | ||
| empty.style.fontSize = '13px'; | ||
| empty.style.color = '#999'; | ||
| empty.textContent = 'No matching benefits'; | ||
| dropdown.appendChild(empty); | ||
| } | ||
|
|
||
| dropdown.style.display = 'block'; | ||
|
|
||
| dropdown.querySelectorAll('.bs-opt').forEach(function(el) { |
There was a problem hiding this comment.
Pull request overview
This PR introduces a staff-facing Sponsor Management UI under the sponsors app, plus supporting infrastructure for email testing and outbound-notification auditing/logging.
Changes:
- Add a new
/sponsors/manage/management UI (templates, forms, URLs) for handling sponsorships, packages, benefits, contacts, contracts, and notifications. - Persist outbound sponsorship notification sends to a new
SponsorshipNotificationLogmodel and display history in the management UI. - Improve local dev ergonomics by adding MailDev + SMTP-backed local email configuration; update sponsor contract email templates; bump
django-storages.
Reviewed changes
Copilot reviewed 43 out of 44 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
uv.lock |
Bumps django-storages from 1.14.4 to 1.14.6 (lockfile update). |
pydotorg/settings/local.py |
Uses SMTP backend when EMAIL_HOST is set (for MailDev), otherwise console backend. |
docker-compose.yml |
Adds a maildev service and wires EMAIL_HOST/EMAIL_PORT into the web service. |
apps/sponsors/use_cases.py |
Logs each sent sponsorship notification to SponsorshipNotificationLog. |
apps/sponsors/urls.py |
Mounts the management UI under sponsors/manage/. |
apps/sponsors/templates/sponsors/manage/sponsorship_notify.html |
UI to preview/send a notification for a single sponsorship. |
apps/sponsors/templates/sponsors/manage/sponsorship_list.html |
Sponsorship list UI with filters, bulk actions, and live search. |
apps/sponsors/templates/sponsors/manage/sponsorship_edit.html |
Edit sponsorship details (package/fee/year). |
apps/sponsors/templates/sponsors/manage/sponsorship_detail.html |
Sponsorship detail/review UI incl. contracts, benefits, assets, communications. |
apps/sponsors/templates/sponsors/manage/sponsorship_approve_signed.html |
Approve sponsorship while uploading a signed contract document. |
apps/sponsors/templates/sponsors/manage/sponsorship_approve.html |
Approve sponsorship (creates draft contract) with quick date helpers. |
apps/sponsors/templates/sponsors/manage/sponsor_edit.html |
Create/edit sponsor company data. |
apps/sponsors/templates/sponsors/manage/package_list.html |
Package list UI grouped by year. |
apps/sponsors/templates/sponsors/manage/package_form.html |
Create/edit a sponsorship package. |
apps/sponsors/templates/sponsors/manage/package_confirm_delete.html |
Confirm-delete UI for packages. |
apps/sponsors/templates/sponsors/manage/notification_template_list.html |
List notification templates in the management UI. |
apps/sponsors/templates/sponsors/manage/notification_template_form.html |
Create/edit notification templates with variable copy helpers. |
apps/sponsors/templates/sponsors/manage/notification_template_confirm_delete.html |
Confirm-delete UI for notification templates. |
apps/sponsors/templates/sponsors/manage/notification_history.html |
Management UI listing sent notifications (log history). |
apps/sponsors/templates/sponsors/manage/guide.html |
End-user guide for PSF sponsorship team workflows. |
apps/sponsors/templates/sponsors/manage/dashboard.html |
Management dashboard summary for a selected year. |
apps/sponsors/templates/sponsors/manage/current_year_form.html |
UI to set the active sponsorship year. |
apps/sponsors/templates/sponsors/manage/contract_send.html |
UI for generating/sending contracts and internal review. |
apps/sponsors/templates/sponsors/manage/contract_execute.html |
UI to upload signed contract and execute/finalize sponsorship. |
apps/sponsors/templates/sponsors/manage/contact_form.html |
Create/edit sponsor contact roles and info. |
apps/sponsors/templates/sponsors/manage/clone_year.html |
Clone packages/benefits from one year to another. |
apps/sponsors/templates/sponsors/manage/bulk_notify.html |
UI to send notifications in bulk across sponsorships. |
apps/sponsors/templates/sponsors/manage/benefit_list.html |
Benefit list UI with filtering/pagination. |
apps/sponsors/templates/sponsors/manage/benefit_form.html |
Create/edit benefit + manage feature configurations and related data. |
apps/sponsors/templates/sponsors/manage/benefit_confirm_delete.html |
Confirm-delete UI for benefits. |
apps/sponsors/templates/sponsors/manage/benefit_config_form.html |
Create/edit benefit feature configuration objects. |
apps/sponsors/templates/sponsors/manage/_base.html |
Management UI shell + global JS table-sorting helper. |
apps/sponsors/templates/sponsors/email/sponsor_contract_subject.txt |
Replaces placeholder subject with a real contract subject template. |
apps/sponsors/templates/sponsors/email/sponsor_contract.txt |
Replaces placeholder contract email body with real copy. |
apps/sponsors/models/notifications.py |
Adds SponsorshipNotificationLog model to persist sent notifications. |
apps/sponsors/models/__init__.py |
Re-exports SponsorshipNotificationLog. |
apps/sponsors/migrations/0104_add_notification_log.py |
Migration creating the notification log table. |
apps/sponsors/management/commands/seed_sponsor_manage_data.py |
Dev-only command to seed realistic data for the management UI. |
apps/sponsors/manage/urls.py |
URL routing for the management UI. |
apps/sponsors/manage/forms.py |
Forms backing the management UI (benefits/packages/sponsorship ops/notifications/etc.). |
apps/sponsors/manage/__init__.py |
Documents that the manage UI is staff/group restricted. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| signed_document = forms.FileField( | ||
| label="Signed contract document", | ||
| help_text="Upload the signed PDF.", |
There was a problem hiding this comment.
The help text says "Upload the signed PDF" but the file input accepts both .pdf and .docx. Align the help text and accepted file types (either restrict to PDF or update the copy/validation to explicitly support DOCX).
| help_text="Upload the signed PDF.", | |
| help_text="Upload the signed contract as a PDF or DOCX file.", |
| var tbody = table.querySelector('tbody'); | ||
| var rows = Array.prototype.slice.call(tbody.querySelectorAll('tr')); | ||
| var ascending = th.dataset.sortDir !== 'asc'; |
There was a problem hiding this comment.
The global table-sorting script sorts every <tr> in each .manage-table tbody. Some tables in this UI use paired "summary row + hidden detail row" patterns (e.g., communications/history), and sorting will break the pairing / toggling behavior. Consider opting certain tables out (e.g., a data-no-sort flag), or adjust the sorter to keep row pairs together / ignore detail rows (like those with colspan).
| # Persist notification log | ||
| SponsorshipNotificationLog.objects.create( | ||
| sponsorship=sponsorship, | ||
| subject=email.subject, | ||
| content=email.body, | ||
| recipients=", ".join(email.to), | ||
| contact_types=", ".join(contact_types), | ||
| sent_by=request.user if request and hasattr(request, "user") else None, | ||
| ) |
There was a problem hiding this comment.
This use case now persists SponsorshipNotificationLog entries after sending. There are existing unit tests for SendSponsorshipNotificationUseCase, but none assert that a log row is created (or that recipients/contact_types/sent_by are stored correctly). Add coverage to prevent regressions (e.g., ensure one log per sent email, and verify sent_by behavior when no authenticated user is available).
| emptyRow.innerHTML = '<td colspan="9" style="text-align:center;padding:20px;color:#999;">No sponsorships match “' + q.replace(/</g, '<') + '”</td>'; | ||
| rows[0].parentNode.appendChild(emptyRow); | ||
| } else { | ||
| emptyRow.innerHTML = '<td colspan="9" style="text-align:center;padding:20px;color:#999;">No sponsorships match “' + q.replace(/</g, '<') + '”</td>'; |
There was a problem hiding this comment.
The empty-state row is built via innerHTML using raw user input q. Escaping only < is not sufficient (e.g., input like <img src=x onerror=...> contains no literal < but will be decoded by innerHTML and can execute). Build the message using DOM APIs/text nodes (e.g., set textContent) or perform full HTML escaping (&, <, >, quotes) before inserting.
| function renderDropdown(filter) { | ||
| var html = ''; | ||
| var q = (filter || '').toLowerCase(); | ||
| var count = 0; | ||
| options.forEach(function(opt) { | ||
| if (q && opt.text.toLowerCase().indexOf(q) === -1) return; | ||
| var highlighted = opt.value === select.value; | ||
| html += '<div class="bs-opt' + (highlighted ? ' bs-sel' : '') + '" data-val="' + opt.value + '"' | ||
| + ' style="padding:7px 12px;font-size:13px;cursor:pointer;border-bottom:1px solid #f5f5f5;' | ||
| + (highlighted ? 'background:#e8f0fe;font-weight:600;' : '') | ||
| + '">' + opt.text + '</div>'; | ||
| count++; | ||
| }); | ||
| if (!count) html = '<div style="padding:10px 12px;font-size:13px;color:#999;">No matching benefits</div>'; | ||
| dropdown.innerHTML = html; |
There was a problem hiding this comment.
renderDropdown() builds html using opt.text and assigns it via dropdown.innerHTML. Since select.options[i].text is already HTML-decoded, a benefit name containing entities like <img ...> will become real markup when inserted and can lead to XSS. Build the dropdown items with DOM APIs and set textContent, or robustly escape opt.text before concatenating into HTML.
| <span class="tag {% if contract.status == 'draft' %}tag-gray{% elif contract.status == 'awaiting_signature' %}tag-gold{% elif contract.status == 'executed' %}tag-green{% else %}tag-red{% endif %}"> | ||
| {{ contract.get_status_display }} |
There was a problem hiding this comment.
Contract.status values use strings like "awaiting signature" (with a space). This template checks for awaiting_signature (underscore), so awaiting-signature contracts will be styled as the fallback (red) status. Use the correct status value or (preferably) check a boolean property like contract.awaiting_signature / contract.is_draft to avoid string mismatches.
| recipients=", ".join(email.to), | ||
| contact_types=", ".join(contact_types), | ||
| sent_by=request.user if request and hasattr(request, "user") else None, | ||
| ) |
There was a problem hiding this comment.
sent_by is set to request.user when a request is present, but hasattr(request, 'user') isn't sufficient: request.user can be AnonymousUser (or other non-User objects), which will raise when assigned to the FK. Guard with request.user.is_authenticated (and possibly ensure it's an instance of the configured User model) before assigning; otherwise store None.
No description provided.