Skip to content

Commit 8123d8e

Browse files
Migrate svg_icon middleware to return web component
1 parent 61ccbbc commit 8123d8e

34 files changed

Lines changed: 127 additions & 396 deletions

File tree

cfgov/cfgov/settings/test.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,9 @@
4545
]
4646

4747
STATICFILES_DIRS += [
48-
PROJECT_ROOT.joinpath("core", "testutils", "staticfiles"),
48+
PROJECT_ROOT.joinpath("core", "testutils"),
4949
]
5050

51-
MOCK_STATICFILES_PATTERNS = {
52-
"icons/*.svg": "icons/placeholder.svg",
53-
}
54-
5551
FLAG_SOURCES = ("flags.sources.SettingsFlagsSource",)
5652

5753
# We use a custom MEDIA_ROOT for testing so that tests that create images and
Lines changed: 2 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,14 @@
11
import logging
2-
import re
32

43
from django import template
5-
from django.contrib.staticfiles import finders
64
from django.utils.safestring import SafeString, mark_safe
75

86

97
logger = logging.getLogger(__name__)
108
register = template.Library()
119

1210

13-
FALLBACK_ICON_NAME = "error"
14-
SVG_REGEX = re.compile(
15-
r"^" # start of string
16-
r"\s*" # any leading whitespace
17-
r"<svg[^>]*>" # opening <svg> tag with any attributes
18-
r"(?!.*</svg>.*</svg>)" # only allow one closing </svg> tag
19-
r".*</svg>" # match anything and then the closing tag
20-
r"\s*" # any trailing whitespace
21-
r"$", # end of string
22-
re.DOTALL | re.IGNORECASE | re.MULTILINE,
23-
)
24-
25-
26-
def load_svg_from_file(name: str) -> str:
27-
relative_path = f"icons/{name}.svg"
28-
if not (static_filename := finders.find(relative_path)):
29-
raise FileNotFoundError(f"{relative_path} not found in staticfiles.")
30-
31-
with open(static_filename) as f:
32-
content = f.read()
33-
if not SVG_REGEX.match(content):
34-
raise ValueError(f"{static_filename} not a valid SVG.")
35-
36-
return content
37-
38-
39-
_SVG_ICON_CACHE = {}
40-
41-
42-
def load_svg(name: str) -> str:
43-
try:
44-
return _SVG_ICON_CACHE[name]
45-
except KeyError:
46-
pass
47-
48-
svg = load_svg_from_file(name)
49-
_SVG_ICON_CACHE[name] = svg
50-
return svg
51-
52-
5311
@register.simple_tag()
5412
def svg_icon(name: str) -> SafeString:
55-
"""Return SVG content given an icon name."""
56-
try:
57-
content = load_svg(name)
58-
except (FileNotFoundError, ValueError) as e:
59-
logger.warning(f"{e} Substituting with {FALLBACK_ICON_NAME}.svg!")
60-
content = load_svg(FALLBACK_ICON_NAME)
61-
return mark_safe(content)
13+
"""Return cfpb-icon web component."""
14+
return mark_safe(f'<cfpb-icon name="{name}"></cfpb-icon>')

cfgov/core/tests/middleware/test_middleware.py

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -132,13 +132,13 @@ def test_non_gov_link(self):
132132
"""Non gov links get external link icon."""
133133
link = '<body><a href="https://google.com">external link</a></body>'
134134
output = parse_links(link)
135-
self.assertIn("cf-icon-svg", output)
135+
self.assertIn("cfpb-icon", output)
136136

137137
def test_gov_link(self):
138138
"""External gov links get external link icon."""
139139
link = '<body><a href="https://www.fdic.gov/bar">gov link</a></body>'
140140
output = parse_links(link)
141-
self.assertIn("cf-icon-svg", output)
141+
self.assertIn("cfpb-icon", output)
142142

143143
def test_internal_link(self):
144144
"""Internal links do not get external link icon."""
@@ -148,19 +148,19 @@ def test_internal_link(self):
148148
</body>
149149
"""
150150
output = parse_links(link)
151-
self.assertNotIn("cf-icon-svg", output)
151+
self.assertNotIn("cfpb-icon", output)
152152

153153
def test_files_get_download_icon(self):
154154
file_types = ["pdf", "doc", "docx", "xls", "xlsx", "csv", "zip"]
155155
for file_type in file_types:
156156
link = f'<body><a href="/something.{file_type}">link</a></body>'
157157
output = parse_links(link)
158-
self.assertIn("cf-icon-svg", output)
158+
self.assertIn("cfpb-icon", output)
159159

160160
def test_different_case_pdf_link_gets_download_icon(self):
161161
link = '<body><a href="/something.PDF">link</a></body>'
162162
output = parse_links(link)
163-
self.assertIn("cf-icon-svg", output)
163+
self.assertIn("cfpb-icon", output)
164164

165165
def test_rich_text_links_get_expanded(self):
166166
page = CFGOVPage(title="foo bar", slug="foo-bar")
@@ -184,7 +184,7 @@ def test_external_link_outside_body(self):
184184
</body>
185185
"""
186186
output = parse_links(s)
187-
self.assertNotIn("cf-icon-svg", output)
187+
self.assertNotIn("cfpb-icon", output)
188188

189189
def test_external_link_outside_body_with_attributes(self):
190190
s = """
@@ -193,7 +193,7 @@ def test_external_link_outside_body_with_attributes(self):
193193
</body>
194194
"""
195195
output = parse_links(s)
196-
self.assertNotIn("cf-icon-svg", output)
196+
self.assertNotIn("cfpb-icon", output)
197197

198198
def test_external_link_with_attribute(self):
199199
s = """
@@ -202,12 +202,12 @@ def test_external_link_with_attribute(self):
202202
</body>
203203
"""
204204
output = parse_links(s)
205-
self.assertIn("cf-icon-svg", output)
205+
self.assertIn("cfpb-icon", output)
206206

207207
def test_external_link_with_img(self):
208208
s = '<body><a href="https://somewhere"><img src="some.png"></a></body>'
209209
output = parse_links(s)
210-
self.assertNotIn("cf-icon-svg", output)
210+
self.assertNotIn("cfpb-icon", output)
211211

212212
def test_external_link_with_background_img(self):
213213
s = """
@@ -218,12 +218,12 @@ def test_external_link_with_background_img(self):
218218
</body>
219219
"""
220220
output = parse_links(s)
221-
self.assertNotIn("cf-icon-svg", output)
221+
self.assertNotIn("cfpb-icon", output)
222222

223223
def test_external_link_with_header(self):
224224
s = '<body><a href="https://somewhere"><h3>Header</h3></a></body>'
225225
output = parse_links(s)
226-
self.assertNotIn("cf-icon-svg", output)
226+
self.assertNotIn("cfpb-icon", output)
227227

228228
def test_multiline_external_gov_link(self):
229229
s = """
@@ -236,7 +236,7 @@ def test_multiline_external_gov_link(self):
236236
</body>
237237
"""
238238
output = parse_links(s)
239-
self.assertIn("cf-icon-svg", output)
239+
self.assertIn("cfpb-icon", output)
240240

241241
def test_multiple_links(self):
242242
s = """
@@ -252,26 +252,26 @@ def test_multiple_links(self):
252252
def check_after_parse_links_has_this_many_svgs(self, count, s):
253253
output = parse_links(s)
254254
soup = BeautifulSoup(output, "html.parser")
255-
self.assertEqual(len(soup.find_all("svg")), count)
255+
self.assertEqual(len(soup.find_all("cfpb-icon")), count)
256256

257257
def test_link_ending_with_svg_doesnt_get_another_svg(self):
258258
self.check_after_parse_links_has_this_many_svgs(
259259
1,
260260
"<body>"
261261
'<a href="https://external.gov">'
262262
"<span>Text before icon</span>"
263-
"<svg>something</svg>"
263+
'<cfpb-icon name="external-link"></cfpb-icon>'
264264
"</a>"
265265
"</body>",
266266
)
267267

268268
def test_link_ending_with_svg_and_whitespace_doesnt_get_another_svg(self):
269269
self.check_after_parse_links_has_this_many_svgs(
270-
1,
270+
2,
271271
"<body>"
272272
'<a href="https://external.gov">'
273273
"<span>Text before icon</span> "
274-
"<svg>something</svg> \n\t"
274+
'<cfpb-icon name="external-link"></cfpb-icon> \n\t'
275275
"</a>"
276276
"</body>",
277277
)
@@ -281,7 +281,8 @@ def test_with_svg_not_at_the_end_still_gets_svg(self):
281281
2,
282282
"<body>"
283283
'<a href="https://external.gov">'
284-
"<span><svg>something</svg> Text after icon</span>"
284+
'<span><cfpb-icon name="external-link"></cfpb-icon>'
285+
" Text after icon</span>"
285286
"</a>"
286287
"</body>",
287288
)
@@ -291,7 +292,7 @@ def test_with_svg_then_span_still_gets_svg(self):
291292
2,
292293
"<body>"
293294
'<a href="https://external.gov">'
294-
"<svg>something</svg>"
295+
'<cfpb-icon name="external-link"></cfpb-icon>'
295296
"<span>Text after icon</span>"
296297
"</a>"
297298
"</body>",

cfgov/core/tests/staticfiles/icons/invalid.svg

Lines changed: 0 additions & 1 deletion
This file was deleted.

cfgov/core/tests/staticfiles/icons/test.svg

Lines changed: 0 additions & 3 deletions
This file was deleted.
Lines changed: 8 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -1,96 +1,13 @@
1-
import os
2-
from unittest.mock import patch
3-
41
from django.template import Context, Template
5-
from django.test import TestCase, override_settings
6-
from django.utils.safestring import SafeData
7-
8-
from core.templatetags.svg_icon import SVG_REGEX, load_svg, svg_icon
9-
10-
11-
VALID_SVG = (
12-
'<svg width="100" height="100">\n'
13-
' <circle cx="50" cy="50" r="40" stroke="green" fill="yellow" />\n'
14-
"</svg>\n"
15-
)
16-
17-
18-
class SvgRegexTests(TestCase):
19-
def test_empty_svg_matches(self):
20-
self.assertTrue(SVG_REGEX.match("<svg></svg>"))
21-
22-
def test_case_insensitive_matching(self):
23-
self.assertTrue(SVG_REGEX.match("<sVg></SvG>"))
24-
25-
def test_valid_svg_matches(self):
26-
self.assertTrue(SVG_REGEX.match(VALID_SVG))
27-
28-
def test_valid_svg_with_extra_whitespace_matches(self):
29-
self.assertTrue(SVG_REGEX.match(" " + VALID_SVG + "\n\n"))
30-
31-
def test_invalid_svg_does_not_match(self):
32-
self.assertFalse(SVG_REGEX.match('<script type="malicious"></script>'))
33-
34-
def test_nested_invalid_svg_does_not_match(self):
35-
self.assertFalse(
36-
SVG_REGEX.match(
37-
"<svg></svg>"
38-
'<script type="this looks valid but is malicious"></script>'
39-
"<svg></svg>"
40-
)
41-
)
2+
from django.test import TestCase
423

434

44-
@override_settings(
45-
MOCK_STATICFILES_PATTERNS={},
46-
STATICFILES_DIRS=[
47-
os.path.join(
48-
os.path.dirname(os.path.dirname(__file__)), "staticfiles"
49-
),
50-
],
51-
)
525
class SvgIconTests(TestCase):
53-
def test_assert_renders_valid_svg_from_staticfiles_icons(self):
54-
self.assertEqual(svg_icon("test"), VALID_SVG)
55-
56-
def test_svg_icon_result_marked_safe_for_rendering(self):
57-
self.assertIsInstance(svg_icon("test"), SafeData)
58-
59-
def test_invalid_svg_raises_value_error(self):
60-
with self.assertRaises(ValueError):
61-
load_svg("invalid")
62-
63-
def test_missing_svg_raises_file_not_found_error(self):
64-
with self.assertRaises(FileNotFoundError):
65-
load_svg("missing")
66-
676
def test_template_tag(self):
68-
template = Template('{% load svg_icon %}{% svg_icon "test" %}')
69-
self.assertEqual(template.render(Context()), VALID_SVG)
70-
71-
@patch("core.templatetags.svg_icon._SVG_ICON_CACHE", {"test": "cached"})
72-
def test_caching(self):
73-
template = Template('{% load svg_icon %}{% svg_icon "test" %}')
74-
self.assertEqual(template.render(Context()), "cached")
75-
76-
@patch("core.templatetags.svg_icon.FALLBACK_ICON_NAME", "test")
77-
def test_template_tag_fallback(self):
78-
"""
79-
This test is for svg_icon.FALLBACK_ICON_NAME and related functionality
80-
Should return the icon associated with FALLBACK_ICON_NAME when
81-
an invalid icon name is used.
82-
"""
83-
template = Template('{% load svg_icon %}{% svg_icon "invalid" %}')
84-
self.assertEqual(template.render(Context()), VALID_SVG)
85-
86-
@patch("core.templatetags.svg_icon.FALLBACK_ICON_NAME", "missing")
87-
def test_missing_fallback_raises_file_not_found_error(self):
88-
template = Template('{% load svg_icon %}{% svg_icon "invalid" %}')
89-
with self.assertRaises(FileNotFoundError):
90-
template.render(Context())
91-
92-
@patch("core.templatetags.svg_icon.FALLBACK_ICON_NAME", "invalid")
93-
def test_invalid_fallback_raises_value_error(self):
94-
template = Template('{% load svg_icon %}{% svg_icon "invalid" %}')
95-
with self.assertRaises(ValueError):
96-
template.render(Context())
7+
template = Template(
8+
'{% load svg_icon %}{% svg_icon "external-link" %}'
9+
)
10+
self.assertEqual(
11+
template.render(Context()),
12+
'<cfpb-icon name="external-link"></cfpb-icon>',
13+
)

cfgov/core/tests/test_jinja2tags.py

Lines changed: 4 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,17 @@
1-
import os
2-
from unittest.mock import patch
3-
41
from django.http import HttpRequest
52
from django.template import engines
63
from django.test import SimpleTestCase, TestCase, override_settings
74

8-
from core.tests.templatetags.test_svg_icon import VALID_SVG
9-
105

11-
@override_settings(
12-
STATICFILES_DIRS=[
13-
os.path.join(os.path.dirname(__file__), "staticfiles"),
14-
]
15-
)
166
class SvgIconTests(TestCase):
177
def setUp(self):
188
self.jinja_engine = engines["wagtail-env"]
199

2010
def test_jinja_tag(self):
21-
template = self.jinja_engine.from_string('{{ svg_icon("test") }}')
22-
self.assertEqual(template.render(), VALID_SVG)
23-
24-
@patch("core.templatetags.svg_icon.FALLBACK_ICON_NAME", "test")
25-
def test_jinja_tag_fallback(self):
26-
template = self.jinja_engine.from_string('{{ svg_icon("invalid") }}')
27-
self.assertEqual(template.render(), VALID_SVG)
28-
29-
@patch("core.templatetags.svg_icon.FALLBACK_ICON_NAME", "missing")
30-
def test_jinja_tag_fallback_not_found_error(self):
31-
template = self.jinja_engine.from_string('{{ svg_icon("missing") }}')
32-
with self.assertRaises(FileNotFoundError):
33-
template.render()
34-
35-
@patch("core.templatetags.svg_icon.FALLBACK_ICON_NAME", "invalid")
36-
def test_jinja_tag_fallback_invalid_error(self):
37-
template = self.jinja_engine.from_string('{{ svg_icon("invalid") }}')
38-
with self.assertRaises(ValueError):
39-
template.render()
11+
template = self.jinja_engine.from_string('{{ svg_icon("bank") }}')
12+
self.assertEqual(
13+
template.render(), '<cfpb-icon name="bank"></cfpb-icon>'
14+
)
4015

4116

4217
@override_settings(FLAGS={"MY_FLAG": [("boolean", True)]})

0 commit comments

Comments
 (0)