Coverage for app / helpers / utilities.py: 96%
39 statements
« prev ^ index » next coverage.py v7.12.0, created at 2025-12-06 04:49 +0000
« prev ^ index » next coverage.py v7.12.0, created at 2025-12-06 04:49 +0000
1"""
2This module provides utilities for building library search URLs, rendering icons, URL parsing,
3and computing navigation paths. It includes helper functions for escaping user input, mapping
4values to icons, and handling request referrer URLs.
5"""
6from urllib.parse import quote_plus, urlparse
8import bleach
9from flask import url_for
10from markupsafe import Markup
12PLACEHOLDER = '<span class="placeholder-icon"></span>'
15_SEARCH_TEMPLATES = {
16 "Los Gatos Library":
17 "https://losgatos.aspendiscovery.org/Search/Results?join=AND&bool0[]=AND&lookfor0[]=" +
18 "{title}&type0[]=Title&lookfor1[]={author}&type1[]=Author&submit=Find",
19 "Santa Clara Country Library":
20 "https://sccl.bibliocommons.com/v2/search?custom_edit=false&query=(title%3A({title})" +
21 "%20AND%20contributor%3A({author}))&searchType=bl&suppress=true",
22 "San Diego Library":
23 "https://sandiego.bibliocommons.com/v2/search?custom_edit=false&query=(title%3A({title})" +
24 "%20AND%20contributor%3A({author})%20)&searchType=bl&suppress=true",
25 "San Diego County Library":
26 "https://sdcl.bibliocommons.com/v2/search?custom_edit=false&query=(anywhere%3A({title})" +
27 "%20AND%20anywhere%3A({author})%20)&searchType=bl&suppress=true",
28 "Placer County Library":
29 "https://placer.polarislibrary.com/polaris/search/searchresults.aspx?type=Advanced" +
30 "&term={title}&relation=ALL&by=TI&term2={author}&relation2=ALL&by2=AU&bool1=AND" +
31 "&limit=TOM=*&sort=RELEVANCE&page=0",
32 "Nevada County Library":
33 "https://library.nevadacountyca.gov/polaris/search/searchresults.aspx?ctx=1.1033.0.0.1" +
34 "&type=Advanced&term={title}&relation=ALL&by=TI&term2={author}&relation2=ALL&by2=AU" +
35 "&bool1=AND&bool4=AND&limit=(TOM=*%20AND%20OWN=1)&sort=RELEVANCE&page=0&searchid=1"
36}
39def _strip_subtitles(title: str) -> str:
40 """
41 Removes subtitles or additional information from a given title string.
43 This function takes a title string and removes parts following a colon (:) or parentheses (),
44 returning the cleaned main title.
46 Args:
47 title (str): The title string to clean.
49 Returns:
50 str: The main title without subtitles or additional information.
51 """
52 returned_title = title
53 if ':' in returned_title:
54 returned_title = returned_title.split(':')[0].strip()
55 if '(' in returned_title: 55 ↛ 56line 55 didn't jump to line 56 because the condition on line 55 was never true
56 returned_title = returned_title.split('(')[0].strip()
57 return returned_title
60def build_library_search_urls(author, title) -> dict[str, str]:
61 """
62 Builds library search URLs using the given author and title.
64 This function takes the author's name and the title of a work, escapes
65 them to make them URL-safe, and substitutes them into predefined search
66 URL templates. The result is a dictionary of search URLs for external
67 library systems or online catalog platforms.
69 :param author: The name of the author to search for (must be a string).
70 :type author: str
71 :param title: The title of the work to search for (must be a string).
72 :type title: str
73 :return: A dictionary where the keys are search platform identifiers
74 and the values are URLs with the provided author and title
75 embedded within them.
76 :rtype: dict
77 """
78 escaped_title = quote_plus(_strip_subtitles(title), safe="")
79 escaped_author = quote_plus(author, safe="")
80 search_urls = {
81 key: value.replace("{title}", escaped_title).replace("{author}", escaped_author)
82 for key, value in _SEARCH_TEMPLATES.items()
83 }
84 return search_urls
87def render_icon(item, icon_mapping, span_id) -> Markup:
88 """
89 Render an HTML span element with an icon based on the given item and mapping.
91 This function generates a span element containing a font-awesome icon
92 corresponding to the provided item. The icon is determined by the
93 ``icon_mapping`` dictionary. If the item is not found in the ``icon_mapping``,
94 a default hidden placeholder HTML element is returned as a spacer.
96 :param item: The enumeration value representing the action (e.g., 'like', 'read')
97 to map to a corresponding icon.
98 :type item: str
99 :param icon_mapping: A dictionary mapping enumeration values to font-awesome icon
100 classes.
101 :type icon_mapping: dict
102 :param span_id: The ID attribute to assign to the span element in the resultant
103 HTML string.
104 :type span_id: str
105 :return: A `Markup` object containing the rendered span element string
106 with the appropriate icon or a placeholder if the item is not mapped.
107 :rtype: Markup
108 """
109 # item is the enumeration value (like 'like', 'read'),
110 # icon_mapping is a dictionary mapping enum values to font-awesome icons
111 if item in icon_mapping:
112 return Markup( # nosec B704
113 f'<span id="{span_id}">' +
114 f'<i class="fa {icon_mapping[item]}" aria-hidden="true"></i>' +
115 '</span>'
116 )
117 # not in mapping, just use hidden default as a spacer
118 return Markup(f'<span id="{span_id}">{PLACEHOLDER}</span>') # nosec B704
121def compute_next_url(request):
122 """
123 Compute the next URL based on the referrer of the provided request object.
125 This function examines the `referrer` attribute of the given `request` object
126 to compute the next URL to navigate to. If the `request` contains a valid `referrer`,
127 the function extracts the path and appends the query string (if present). If no
128 `referrer` is available, the function defaults to returning the URL for the
129 "index" route.
131 :param request: The request object containing information about the current
132 HTTP request. Should include a `referrer` attribute to extract navigation
133 information from.
134 :type request: flask.Request
135 :return: The computed next URL string based on the `referrer`, or the default
136 URL for the "index" route if no `referrer` is present.
137 :rtype: str
138 """
139 return urlparse(request.referrer).path + (
140 '?' + urlparse(request.referrer).query if urlparse(request.referrer).query else '') \
141 if request.referrer else url_for("index")
144# Define a custom function wrapping `urlparse` to parse URLs
145def parse_url(url):
146 """
147 Parses a given URL string and extracts its components.
149 This function takes a URL string as input, validates its presence, and
150 parses it using Python's `urlparse` module. It extracts the path and query
151 components and provides them along with the full URL in a dictionary format.
153 :param url: The URL string to be parsed.
154 :type url: str
155 :return: A dictionary containing the parsed components: "path", "query",
156 and the original "full" URL string. Returns None if the input URL is
157 not provided.
158 :rtype: dict or None
159 """
160 if not url:
161 return None
162 parsed = urlparse(url)
163 return {
164 "path": parsed.path,
165 "query": parsed.query,
166 "full": url
167 }
170def sanitize(content):
171 """
172 Strip all HTML content from the input string.
174 :param content: The content to sanitize
175 :return: Plain text with all HTML tags removed
176 """
177 if content is None or not isinstance(content, str):
178 return content
180 # Strip all HTML tags by setting allowed_tags to an empty list
181 return bleach.clean(
182 content,
183 tags=[], # No HTML tags allowed
184 attributes={}, # No attributes allowed
185 strip=True # Strip all disallowed tags (which is all tags)
186 )
189def sanitize_categories_flat(categories: str):
190 """
191 Clean the component pieces of a falt categories string.
192 :param categories:
193 :return:
194 """
195 cat_strings = categories.split(' > ')
196 sanitize_list = []
197 for cat in cat_strings:
198 sanitize_list.append(sanitize(cat))
199 return ' > '.join(sanitize_list)