diff --git a/apps/boxes/templatetags/boxes.py b/apps/boxes/templatetags/boxes.py
index 553aae8a8..69f2b318b 100644
--- a/apps/boxes/templatetags/boxes.py
+++ b/apps/boxes/templatetags/boxes.py
@@ -15,7 +15,7 @@
def box(label):
"""Render the content of a Box identified by its label slug."""
try:
- return mark_safe(Box.objects.only("content").get(label=label).content.rendered)
+ return mark_safe(Box.objects.only("content").get(label=label).content.rendered) # noqa: S308
except Box.DoesNotExist:
log.warning("WARNING: box not found: label=%s", label)
return ""
diff --git a/apps/companies/templatetags/companies.py b/apps/companies/templatetags/companies.py
index 15b340399..8009cfc0f 100644
--- a/apps/companies/templatetags/companies.py
+++ b/apps/companies/templatetags/companies.py
@@ -2,12 +2,12 @@
from django import template
from django.template.defaultfilters import stringfilter
-from django.utils.html import format_html
+from django.utils.html import format_html_join, mark_safe
register = template.Library()
-@register.filter(is_safe=True)
+@register.filter()
@stringfilter
def render_email(value):
"""Render an email address with obfuscated dots and at-sign using spans."""
@@ -16,8 +16,8 @@ def render_email(value):
mailbox_tokens = mailbox.split(".")
domain_tokens = domain.split(".")
- mailbox = ".".join(mailbox_tokens)
- domain = ".".join(domain_tokens)
+ mailbox = format_html_join(mark_safe("."), "{}", [(token,) for token in mailbox_tokens])
+ domain = format_html_join(mark_safe("."), "{}", [(token,) for token in domain_tokens])
- return format_html(f"{mailbox}@{domain}")
+ return mailbox + mark_safe("@") + domain
return None
diff --git a/apps/downloads/templatetags/download_tags.py b/apps/downloads/templatetags/download_tags.py
index 8a0830360..45d7c12f5 100644
--- a/apps/downloads/templatetags/download_tags.py
+++ b/apps/downloads/templatetags/download_tags.py
@@ -6,8 +6,7 @@
import requests
from django import template
from django.core.cache import cache
-from django.utils.html import format_html
-from django.utils.safestring import mark_safe
+from django.utils.html import format_html, format_html_join, mark_safe
from apps.downloads.models import Release
@@ -111,11 +110,11 @@ def wbr_wrap(value: str | None) -> str:
# Split into two halves, each half has internal breaks
midpoint = len(chunks) // 2
- first_half = "".join(chunks[:midpoint])
- second_half = "".join(chunks[midpoint:])
+ first_half = format_html_join(mark_safe(""), "{}", chunks[:midpoint])
+ second_half = format_html_join(mark_safe(""), "{}", chunks[midpoint:])
- return mark_safe(
- f'{first_half}{second_half}'
+ return format_html(
+ '{}{}', first_half, second_half
)
diff --git a/apps/sponsors/admin.py b/apps/sponsors/admin.py
index 376839ac8..28326415a 100644
--- a/apps/sponsors/admin.py
+++ b/apps/sponsors/admin.py
@@ -1,6 +1,7 @@
"""Django admin configuration for the sponsors app."""
import contextlib
+from textwrap import dedent
from django.contrib import admin
from django.contrib.contenttypes.admin import GenericTabularInline
@@ -12,7 +13,7 @@
from django.template import Context, Template
from django.urls import path, reverse
from django.utils.functional import cached_property
-from django.utils.html import mark_safe
+from django.utils.html import format_html, format_html_join, mark_safe
from import_export import resources
from import_export.admin import ImportExportActionModelAdmin
from import_export.fields import Field
@@ -296,12 +297,12 @@ def get_benefit_split(self, obj: SponsorshipPackage) -> str:
for i, (name, pct) in enumerate(split):
pct_str = f"{pct:.0f}%"
widths.append(pct_str)
- spans.append(f"{pct_str}")
+ spans.append((name, colors[i], pct_str))
# define a style that will show our span elements like a single horizontal stacked bar chart
style = f"color:#fff;text-align:center;cursor:pointer;display:grid;grid-template-columns:{' '.join(widths)}"
+ split_bar = format_html_join("", "{}", spans)
# wrap it all up and put a bow on it
- html = f"{''.join(spans)}
"
- return mark_safe(html)
+ return format_html("{}
", style, split_bar)
class SponsorContactInline(admin.TabularInline):
@@ -325,7 +326,7 @@ class SponsorshipsInline(admin.TabularInline):
def link(self, obj):
"""Return a link to the sponsorship change page."""
url = reverse("admin:sponsors_sponsorship_change", args=[obj.id])
- return mark_safe(f"{obj.id}")
+ return format_html("{}", url, obj.id)
@admin.register(Sponsor)
@@ -652,18 +653,17 @@ def get_readonly_fields(self, request, obj):
def sponsor_link(self, obj):
"""Return an HTML link to the sponsor's admin change page."""
url = reverse("admin:sponsors_sponsor_change", args=[obj.sponsor.id])
- return mark_safe(f"{obj.sponsor.name}")
+ return format_html("{}", url, obj.sponsor.name)
@admin.display(description="Estimated cost")
def get_estimated_cost(self, obj):
"""Return the estimated cost HTML for customized sponsorships."""
- cost = None
html = "This sponsorship has not customizations so there's no estimated cost"
if obj.for_modified_package:
msg = "This sponsorship has customizations and this cost is a sum of all benefit's internal values from when this sponsorship was created"
cost = intcomma(obj.estimated_cost)
- html = f"{cost} USD
Important: {msg}"
- return mark_safe(html)
+ html = format_html("{} USD
Important: {}", cost, msg)
+ return html
@admin.display(description="Contract")
def get_contract(self, obj):
@@ -671,8 +671,7 @@ def get_contract(self, obj):
if not obj.contract:
return "---"
url = reverse("admin:sponsors_contract_change", args=[obj.contract.pk])
- html = f"{obj.contract}"
- return mark_safe(html)
+ return format_html("{}", url, obj.contract)
def get_urls(self):
"""Register custom admin URLs for sponsorship workflow actions."""
@@ -741,19 +740,20 @@ def get_sponsor_web_logo(self, obj):
template = Template(html)
context = Context({"sponsor": obj.sponsor})
html = template.render(context)
- return mark_safe(html)
+ return mark_safe(html) # noqa: S308
@admin.display(description="Print Logo")
def get_sponsor_print_logo(self, obj):
"""Render and return the sponsor's print logo as a thumbnail image."""
img = obj.sponsor.print_logo
- html = ""
+ html = "---"
if img:
- html = "{% load thumbnail %}{% thumbnail img '150x150' format='PNG' quality=100 as im %}
{% endthumbnail %}"
- template = Template(html)
+ template = Template(
+ "{% load thumbnail %}{% thumbnail img '150x150' format='PNG' quality=100 as im %}
{% endthumbnail %}"
+ )
context = Context({"img": img})
- html = template.render(context)
- return mark_safe(html) if html else "---"
+ html = mark_safe(template.render(context)) # noqa: S308
+ return html
@admin.display(description="Primary Phone")
def get_sponsor_primary_phone(self, obj):
@@ -764,18 +764,24 @@ def get_sponsor_primary_phone(self, obj):
def get_sponsor_mailing_address(self, obj):
"""Return the sponsor's formatted mailing address as HTML."""
sponsor = obj.sponsor
- city_row = f"{sponsor.city} - {sponsor.get_country_display()} ({sponsor.country})"
if sponsor.state:
- city_row = f"{sponsor.city} - {sponsor.state} - {sponsor.get_country_display()} ({sponsor.country})"
+ city_row_html = format_html(
+ "{} - {} - {} ({})
", sponsor.city, sponsor.state, sponsor.get_country_display(), sponsor.country
+ )
+ else:
+ city_row_html = format_html(
+ "{} - {} ({})
", sponsor.city, sponsor.get_country_display(), sponsor.country
+ )
- mail_row = sponsor.mailing_address_line_1
if sponsor.mailing_address_line_2:
- mail_row += f" - {sponsor.mailing_address_line_2}"
+ mail_row_html = format_html(
+ "{} - {}
", sponsor.mailing_address_line_1, sponsor.mailing_address_line_2
+ )
+ else:
+ mail_row_html = format_html("{}
", sponsor.mailing_address_line_1)
- html = f"{city_row}
"
- html += f"{mail_row}
"
- html += f"{sponsor.postal_code}
"
- return mark_safe(html)
+ postal_code_row = format_html("{}
", sponsor.postal_code)
+ return city_row_html + mail_row_html + postal_code_row
@admin.display(description="Contacts")
def get_sponsor_contacts(self, obj):
@@ -785,14 +791,14 @@ def get_sponsor_contacts(self, obj):
primary = [c for c in contacts if c.primary]
not_primary = [c for c in contacts if not c.primary]
if primary:
- html = "Primary contacts"
- html += "".join([f"- {c.name}: {c.email} / {c.phone}
" for c in primary])
- html += "
"
+ html = mark_safe("Primary contacts")
+ html += format_html_join("", "- {}: {} / {}
", [(c.name, c.email, c.phone) for c in primary])
+ html += mark_safe("
")
if not_primary:
- html += "Other contacts"
- html += "".join([f"- {c.name}: {c.email} / {c.phone}
" for c in not_primary])
- html += "
"
- return mark_safe(html)
+ html += mark_safe("Other contacts")
+ html += format_html_join("", "- {}: {} / {}
", [(c.name, c.email, c.phone) for c in not_primary])
+ html += mark_safe("
")
+ return html
@admin.display(description="Added by User")
def get_custom_benefits_added_by_user(self, obj):
@@ -801,8 +807,7 @@ def get_custom_benefits_added_by_user(self, obj):
if not benefits:
return "---"
- html = "".join([f"{b}
" for b in benefits])
- return mark_safe(html)
+ return format_html_join("", "{}
", benefits)
@admin.display(description="Removed by User")
def get_custom_benefits_removed_by_user(self, obj):
@@ -811,8 +816,7 @@ def get_custom_benefits_removed_by_user(self, obj):
if not benefits:
return "---"
- html = "".join([f"{b}
" for b in benefits])
- return mark_safe(html)
+ return format_html_join("", "{}
", benefits)
def rollback_to_editing_view(self, request, pk):
"""Delegate to the rollback_to_editing admin view."""
@@ -878,18 +882,25 @@ def links(self, obj):
benefits_url = reverse("admin:sponsors_sponsorshipbenefit_changelist")
preview_label = "View sponsorship application"
year = obj.year
- html = ""
preview_querystring = f"config_year={year}"
preview_url = f"{application_url}?{preview_querystring}"
filter_querystring = f"year={year}"
year_benefits_url = f"{benefits_url}?{filter_querystring}"
year_packages_url = f"{benefits_url}?{filter_querystring}"
- html += f"- List packages"
- html += f"
- List benefits"
- html += f"
- {preview_label}"
- html += "
"
- return mark_safe(html)
+ return format_html(
+ dedent("""
+
+ """),
+ year_packages_url=year_packages_url,
+ year_benefits_url=year_benefits_url,
+ preview_url=preview_url,
+ preview_label=preview_label,
+ )
@admin.display(description="Other configured years")
def other_years(self, obj):
@@ -904,7 +915,7 @@ def other_years(self, obj):
application_url = reverse("select_sponsorship_application_benefits")
benefits_url = reverse("admin:sponsors_sponsorshipbenefit_changelist")
preview_label = "View sponsorship application form for this year"
- html = ""
+ html = mark_safe("")
for year in configured_years:
preview_querystring = f"config_year={year}"
preview_url = f"{application_url}?{preview_querystring}"
@@ -912,14 +923,24 @@ def other_years(self, obj):
year_benefits_url = f"{benefits_url}?{filter_querystring}"
year_packages_url = f"{benefits_url}?{filter_querystring}"
- html += f"- {year}:"
- html += "
"
- html += "
"
- return mark_safe(html)
+ html += format_html(
+ dedent("""
+ - {year}:"
+
+
+ """),
+ year=year,
+ year_packages_url=year_packages_url,
+ year_benefits_url=year_benefits_url,
+ preview_url=preview_url,
+ preview_label=preview_label,
+ )
+ html += mark_safe("
")
+ return html
def clone_application_config(self, request):
"""Delegate to the clone_application_config admin view."""
@@ -1042,8 +1063,8 @@ def document_link(self, obj):
msg = "Download Signed Contract"
if url and msg:
- html = f'{msg}'
- return mark_safe(html)
+ html = format_html('{}', url, msg)
+ return html
@admin.display(description="Sponsorship")
def get_sponsorship_url(self, obj):
@@ -1051,8 +1072,7 @@ def get_sponsorship_url(self, obj):
if not obj.sponsorship:
return "---"
url = reverse("admin:sponsors_sponsorship_change", args=[obj.sponsorship.pk])
- html = f"{obj.sponsorship}"
- return mark_safe(html)
+ return format_html("{}", url, obj.sponsorship)
def get_urls(self):
"""Register custom admin URLs for contract workflow actions."""
@@ -1257,8 +1277,8 @@ def get_value(self, obj):
"""Return the asset value, linking to the file URL if applicable."""
html = obj.value
if obj.value and getattr(obj.value, "url", None):
- html = f"{obj.value}"
- return mark_safe(html)
+ html = format_html("{}", (obj.value.url, obj.value))
+ return html
@admin.display(description="Associated with")
def get_related_object(self, obj):
@@ -1276,8 +1296,7 @@ def get_related_object(self, obj):
if not content_object: # safety belt
return obj.content_object
- html = f"{content_object}"
- return mark_safe(html)
+ return format_html("{}", content_object.admin_url, content_object)
@admin.action(description="Export selected")
def export_assets_as_zipfile(self, request, queryset):
diff --git a/apps/sponsors/forms.py b/apps/sponsors/forms.py
index 3829596e6..1613811d1 100644
--- a/apps/sponsors/forms.py
+++ b/apps/sponsors/forms.py
@@ -10,7 +10,7 @@
from django.db.models import Q
from django.utils import timezone
from django.utils.functional import cached_property
-from django.utils.safestring import mark_safe
+from django.utils.html import format_html
from django.utils.text import slugify
from django.utils.translation import gettext_lazy as _
from django_countries.fields import CountryField
@@ -751,11 +751,11 @@ def __init__(self, *args, **kwargs):
field = required_asset.as_form_field(required=required, initial=value)
if required_asset.due_date and not bool(value):
- field.label = mark_safe(
- f"{field.label}
(Required by {required_asset.due_date})"
+ field.label = format_html(
+ "{}
(Required by {})", field.label, required_asset.due_date
)
if bool(value):
- field.label = mark_safe(f"{field.label}
(Fulfilled, thank you!)")
+ field.label = format_html("{}
(Fulfilled, thank you!)", field.label)
fields[f_name] = field
diff --git a/apps/successstories/admin.py b/apps/successstories/admin.py
index df4b721e8..526a138aa 100644
--- a/apps/successstories/admin.py
+++ b/apps/successstories/admin.py
@@ -35,4 +35,4 @@ def get_list_display(self, request):
@admin.display(description="View on site")
def show_link(self, obj):
"""Return a clickable link icon to the story's public page."""
- return format_html(f'\U0001f517')
+ return format_html('\U0001f517', obj.get_absolute_url())
diff --git a/pyproject.toml b/pyproject.toml
index f778f98c0..5cbed48d5 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -141,8 +141,6 @@ ignore = [
# Boolean args are idiomatic in Django models, forms, and views
"FBT001", # boolean-positional-arg-in-function-definition
"FBT002", # boolean-default-value-positional-argument
- # mark_safe is required Django pattern for admin display
- "S308", # suspicious-mark-safe-usage
# Circular imports are resolved with local imports in Django
"PLC0415", # import-outside-top-level
# TODO comment formatting is not worth enforcing