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 = mark_safe("Primary contacts") if not_primary: - html += "Other contacts" - return mark_safe(html) + html += mark_safe("Other contacts") + 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 = "" - 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 = "") + 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