Coverage for cc_modules/cc_html.py: 33%
118 statements
« prev ^ index » next coverage.py v6.5.0, created at 2022-11-08 23:14 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2022-11-08 23:14 +0000
1#!/usr/bin/env python
3"""
4camcops_server/cc_modules/cc_html.py
6===============================================================================
8 Copyright (C) 2012, University of Cambridge, Department of Psychiatry.
9 Created by Rudolf Cardinal (rnc1001@cam.ac.uk).
11 This file is part of CamCOPS.
13 CamCOPS is free software: you can redistribute it and/or modify
14 it under the terms of the GNU General Public License as published by
15 the Free Software Foundation, either version 3 of the License, or
16 (at your option) any later version.
18 CamCOPS is distributed in the hope that it will be useful,
19 but WITHOUT ANY WARRANTY; without even the implied warranty of
20 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 GNU General Public License for more details.
23 You should have received a copy of the GNU General Public License
24 along with CamCOPS. If not, see <https://www.gnu.org/licenses/>.
26===============================================================================
28**Basic HTML creation functions.**
30"""
32import base64
33from typing import Any, Callable, List, Optional, TYPE_CHECKING, Union
35import cardinal_pythonlib.rnc_web as ws
37from camcops_server.cc_modules.cc_constants import CssClass
38from camcops_server.cc_modules.cc_text import SS
40if TYPE_CHECKING:
41 from camcops_server.cc_modules.cc_request import CamcopsRequest
44# =============================================================================
45# HTML elements
46# =============================================================================
49def table_row(
50 columns: List[str],
51 classes: List[str] = None,
52 colspans: List[Union[str, int]] = None,
53 colwidths: List[str] = None,
54 default: str = "",
55 heading: bool = False,
56) -> str:
57 """
58 Make HTML table row.
60 Args:
61 columns: contents of HTML table columns
62 classes: optional CSS classes, one for each column
63 colspans: ``colspan`` values for each column
64 colwidths: ``width`` values for each column
65 default: content to use if a ``column`` value is None
66 heading: use ``<th>`` rather than ``<td>`` for contents?
68 Returns:
69 the ``<tr>...</tr>`` string
70 """
71 n = len(columns)
73 if not classes or len(classes) != n:
74 # blank, or duff (in which case ignore)
75 classes = [""] * n
76 else:
77 classes = [(f' class="{x}"' if x else "") for x in classes]
79 if not colspans or len(colspans) != n:
80 # blank, or duff (in which case ignore)
81 colspans = [""] * n
82 else:
83 colspans = [(f' colspan="{x}"' if x else "") for x in colspans]
85 if not colwidths or len(colwidths) != n:
86 # blank, or duff (in which case ignore)
87 colwidths = [""] * n
88 else:
89 colwidths = [(f' width="{x}"' if x else "") for x in colwidths]
91 celltype = "th" if heading else "td"
92 rows = "".join(
93 [
94 (
95 f"<{celltype}{classes[i]}{colspans[i]}{colwidths[i]}>"
96 f"{default if columns[i] is None else columns[i]}"
97 f"</{celltype}>"
98 )
99 for i in range(n)
100 ]
101 )
102 return f"<tr>{rows}</tr>\n"
105def div(content: str, div_class: str = "") -> str:
106 """
107 Make simple HTML div.
108 """
109 class_str = f' class="{div_class}"' if div_class else ""
110 return f"""
111 <div{class_str}>
112 {content}
113 </div>
114 """
117def table(content: str, table_class: str = "") -> str:
118 """
119 Make simple HTML table.
120 """
121 class_str = f' class="{table_class}"' if table_class else ""
122 return f"""
123 <table{class_str}>
124 {content}
125 </table>
126 """
129def tr(*args, tr_class: str = "", literal: bool = False) -> str:
130 """
131 Make simple HTML table data row.
133 Args:
134 *args: Set of columns data.
135 literal: Treat elements as literals with their own ``<td> ... </td>``,
136 rather than things to be encapsulated.
137 tr_class: table row class
138 """
139 if literal:
140 elements = args
141 else:
142 elements = [td(x) for x in args]
143 tr_class = f' class="{tr_class}"' if tr_class else ""
144 contents = "".join(elements)
145 return f"<tr{tr_class}>{contents}</tr>\n"
148def td(contents: Any, td_class: str = "", td_width: str = "") -> str:
149 """
150 Make simple HTML table data ``<td>...</td>`` cell.
151 """
152 td_class = f' class="{td_class}"' if td_class else ""
153 td_width = f' width="{td_width}"' if td_width else ""
154 return f"<td{td_class}{td_width}>{contents}</td>\n"
157def th(contents: Any, th_class: str = "", th_width: str = "") -> str:
158 """
159 Make simple HTML table header ``<th>...</th>`` cell.
160 """
161 th_class = f' class="{th_class}"' if th_class else ""
162 th_width = f' width="{th_width}"' if th_width else ""
163 return f"<th{th_class}{th_width}>{contents}</th>\n"
166def tr_qa(
167 q: str, a: Any, default: str = "?", default_for_blank_strings: bool = False
168) -> str:
169 """
170 Make HTML two-column data row (``<tr>...</tr>``), with the right-hand
171 column formatted as an answer.
172 """
173 return tr(
174 q,
175 answer(
176 a,
177 default=default,
178 default_for_blank_strings=default_for_blank_strings,
179 ),
180 )
183def heading_spanning_two_columns(s: str) -> str:
184 """
185 HTML table heading row spanning 2 columns.
186 """
187 return tr_span_col(s, cols=2, tr_class=CssClass.HEADING)
190def subheading_spanning_two_columns(s: str, th_not_td: bool = False) -> str:
191 """
192 HTML table subheading row spanning 2 columns.
193 """
194 return tr_span_col(
195 s, cols=2, tr_class=CssClass.SUBHEADING, th_not_td=th_not_td
196 )
199def subheading_spanning_three_columns(s: str, th_not_td: bool = False) -> str:
200 """
201 HTML table subheading row spanning 3 columns.
202 """
203 return tr_span_col(
204 s, cols=3, tr_class=CssClass.SUBHEADING, th_not_td=th_not_td
205 )
208def subheading_spanning_four_columns(s: str, th_not_td: bool = False) -> str:
209 """
210 HTML table subheading row spanning 4 columns.
211 """
212 return tr_span_col(
213 s, cols=4, tr_class=CssClass.SUBHEADING, th_not_td=th_not_td
214 )
217def bold(x: str) -> str:
218 """
219 Applies HTML bold.
220 """
221 return f"<b>{x}</b>"
224def italic(x: str) -> str:
225 """
226 Applies HTML italic.
227 """
228 return f"<i>{x}</i>"
231def identity(x: Any) -> Any:
232 """
233 Returns argument unchanged.
234 """
235 return x
238def bold_webify(x: str) -> str:
239 """
240 Webifies the string, then makes it bold.
241 """
242 return bold(ws.webify(x))
245def sub(x: str) -> str:
246 """
247 Applies HTML subscript.
248 """
249 return f"<sub>{x}</sub>"
252def sup(x: str) -> str:
253 """
254 Applies HTML superscript.
255 """
256 return f"<sup>{x}</sup>"
259def answer(
260 x: Any,
261 default: str = "?",
262 default_for_blank_strings: bool = False,
263 formatter_answer: Callable[[str], str] = bold_webify,
264 formatter_blank: Callable[[str], str] = italic,
265) -> str:
266 """
267 Formats answer in bold, or the default value if None.
269 Avoid the word "None" for the default, e.g.
270 "Score indicating likelihood of abuse: None"... may be misleading!
271 Prefer "?" instead.
272 """
273 if x is None:
274 return formatter_blank(default)
275 if default_for_blank_strings and not x and isinstance(x, str):
276 return formatter_blank(default)
277 return formatter_answer(x)
280def tr_span_col(
281 x: str,
282 cols: int = 2,
283 tr_class: str = "",
284 td_class: str = "",
285 th_not_td: bool = False,
286) -> str:
287 """
288 HTML table data row spanning several columns.
290 Args:
291 x: Data.
292 cols: Number of columns to span.
293 tr_class: CSS class to apply to tr.
294 td_class: CSS class to apply to td.
295 th_not_td: make it a th, not a td.
296 """
297 cell = "th" if th_not_td else "td"
298 tr_cl = f' class="{tr_class}"' if tr_class else ""
299 td_cl = f' class="{td_class}"' if td_class else ""
300 return f'<tr{tr_cl}><{cell} colspan="{cols}"{td_cl}>{x}</{cell}></tr>'
303def get_data_url(mimetype: str, data: Union[bytes, memoryview]) -> str:
304 """
305 Takes data (in binary format) and returns a data URL as per RFC 2397
306 (https://tools.ietf.org/html/rfc2397), such as:
308 .. code-block:: none
310 data:MIMETYPE;base64,B64_ENCODED_DATA
311 """
312 return f"data:{mimetype};base64,{base64.b64encode(data).decode('ascii')}"
315def get_embedded_img_tag(mimetype: str, data: Union[bytes, memoryview]) -> str:
316 """
317 Takes a binary image and its MIME type, and produces an HTML tag of the
318 form:
320 .. code-block:: none
322 <img src="DATA_URL">
323 """
324 return f"<img src={get_data_url(mimetype, data)}>"
327# =============================================================================
328# Field formatting
329# =============================================================================
332def get_yes_no(req: "CamcopsRequest", x: Any) -> str:
333 """
334 'Yes' if x else 'No'
335 """
336 return req.sstring(SS.YES) if x else req.sstring(SS.NO)
339def get_yes_no_none(req: "CamcopsRequest", x: Any) -> Optional[str]:
340 """
341 Returns 'Yes' for True, 'No' for False, or None for None.
342 """
343 if x is None:
344 return None
345 return get_yes_no(req, x)
348def get_yes_no_unknown(req: "CamcopsRequest", x: Any) -> str:
349 """
350 Returns 'Yes' for True, 'No' for False, or '?' for None.
351 """
352 if x is None:
353 return "?"
354 return get_yes_no(req, x)
357def get_true_false(req: "CamcopsRequest", x: Any) -> str:
358 """
359 'True' if x else 'False'
360 """
361 return req.sstring(SS.TRUE) if x else req.sstring(SS.FALSE)
364def get_true_false_none(req: "CamcopsRequest", x: Any) -> Optional[str]:
365 """
366 Returns 'True' for True, 'False' for False, or None for None.
367 """
368 if x is None:
369 return None
370 return get_true_false(req, x)
373def get_true_false_unknown(req: "CamcopsRequest", x: Any) -> str:
374 """
375 Returns 'True' for True, 'False' for False, or '?' for None.
376 """
377 if x is None:
378 return "?"
379 return get_true_false(req, x)
382def get_present_absent(req: "CamcopsRequest", x: Any) -> str:
383 """
384 'Present' if x else 'Absent'
385 """
386 return req.sstring(SS.PRESENT) if x else req.sstring(SS.ABSENT)
389def get_present_absent_none(req: "CamcopsRequest", x: Any) -> Optional[str]:
390 """
391 Returns 'Present' for True, 'Absent' for False, or None for None.
392 """
393 if x is None:
394 return None
395 return get_present_absent(req, x)
398def get_present_absent_unknown(req: "CamcopsRequest", x: str) -> str:
399 """
400 Returns 'Present' for True, 'Absent' for False, or '?' for None.
401 """
402 if x is None:
403 return "?"
404 return get_present_absent(req, x)
407def get_ternary(
408 x: Any,
409 value_true: Any = True,
410 value_false: Any = False,
411 value_none: Any = None,
412) -> Any:
413 """
414 Returns ``value_none`` if ``x`` is ``None``, ``value_true`` if it's truthy,
415 or ``value_false`` if it's falsy.
416 """
417 if x is None:
418 return value_none
419 if x:
420 return value_true
421 return value_false
424def get_correct_incorrect_none(x: Any) -> Optional[str]:
425 """
426 Returns None if ``x`` is None, "Correct" if it's truthy, or "Incorrect" if
427 it's falsy.
428 """
429 return get_ternary(x, "Correct", "Incorrect", None)