Coverage for cc_modules/cc_html.py : 33%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1#!/usr/bin/env python
3"""
4camcops_server/cc_modules/cc_html.py
6===============================================================================
8 Copyright (C) 2012-2020 Rudolf Cardinal (rudolf@pobox.com).
10 This file is part of CamCOPS.
12 CamCOPS is free software: you can redistribute it and/or modify
13 it under the terms of the GNU General Public License as published by
14 the Free Software Foundation, either version 3 of the License, or
15 (at your option) any later version.
17 CamCOPS is distributed in the hope that it will be useful,
18 but WITHOUT ANY WARRANTY; without even the implied warranty of
19 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 GNU General Public License for more details.
22 You should have received a copy of the GNU General Public License
23 along with CamCOPS. If not, see <https://www.gnu.org/licenses/>.
25===============================================================================
27**Basic HTML creation functions.**
29"""
31import base64
32from typing import Any, Callable, List, Optional, TYPE_CHECKING, Union
34import cardinal_pythonlib.rnc_web as ws
36from camcops_server.cc_modules.cc_constants import CssClass
37from camcops_server.cc_modules.cc_text import SS
39if TYPE_CHECKING:
40 from camcops_server.cc_modules.cc_request import CamcopsRequest
43# =============================================================================
44# HTML elements
45# =============================================================================
47def table_row(columns: List[str],
48 classes: List[str] = None,
49 colspans: List[Union[str, int]] = None,
50 colwidths: List[str] = None,
51 default: str = "",
52 heading: bool = False) -> str:
53 """
54 Make HTML table row.
56 Args:
57 columns: contents of HTML table columns
58 classes: optional CSS classes, one for each column
59 colspans: ``colspan`` values for each column
60 colwidths: ``width`` values for each column
61 default: content to use if a ``column`` value is None
62 heading: use ``<th>`` rather than ``<td>`` for contents?
64 Returns:
65 the ``<tr>...</tr>`` string
66 """
67 n = len(columns)
69 if not classes or len(classes) != n:
70 # blank, or duff (in which case ignore)
71 classes = [""] * n
72 else:
73 classes = [(f' class="{x}"' if x else '') for x in classes]
75 if not colspans or len(colspans) != n:
76 # blank, or duff (in which case ignore)
77 colspans = [""] * n
78 else:
79 colspans = [(f' colspan="{x}"' if x else '') for x in colspans]
81 if not colwidths or len(colwidths) != n:
82 # blank, or duff (in which case ignore)
83 colwidths = [""] * n
84 else:
85 colwidths = [
86 (f' width="{x}"' if x else '')
87 for x in colwidths
88 ]
90 celltype = "th" if heading else "td"
91 rows = "".join([
92 (
93 f"<{celltype}{classes[i]}{colspans[i]}{colwidths[i]}>"
94 f"{default if columns[i] is None else columns[i]}"
95 f"</{celltype}>"
96 )
97 for i in range(n)
98 ])
99 return f"<tr>{rows}</tr>\n"
102def div(content: str, div_class: str = "") -> str:
103 """
104 Make simple HTML div.
105 """
106 class_str = f' class="{div_class}"' if div_class else ''
107 return f"""
108 <div{class_str}>
109 {content}
110 </div>
111 """
114def table(content: str, table_class: str = "") -> str:
115 """
116 Make simple HTML table.
117 """
118 class_str = f' class="{table_class}"' if table_class else ''
119 return f"""
120 <table{class_str}>
121 {content}
122 </table>
123 """
126def tr(*args, tr_class: str = "", literal: bool = False) -> str:
127 """
128 Make simple HTML table data row.
130 Args:
131 *args: Set of columns data.
132 literal: Treat elements as literals with their own ``<td> ... </td>``,
133 rather than things to be encapsulated.
134 tr_class: table row class
135 """
136 if literal:
137 elements = args
138 else:
139 elements = [td(x) for x in args]
140 tr_class = f' class="{tr_class}"' if tr_class else ''
141 contents = "".join(elements)
142 return f"<tr{tr_class}>{contents}</tr>\n"
145def td(contents: Any, td_class: str = "", td_width: str = "") -> str:
146 """
147 Make simple HTML table data ``<td>...</td>`` cell.
148 """
149 td_class = f' class="{td_class}"' if td_class else ''
150 td_width = f' width="{td_width}"' if td_width else ''
151 return f"<td{td_class}{td_width}>{contents}</td>\n"
154def th(contents: Any, th_class: str = "", th_width: str = "") -> str:
155 """
156 Make simple HTML table header ``<th>...</th>`` cell.
157 """
158 th_class = f' class="{th_class}"' if th_class else ''
159 th_width = f' width="{th_width}"' if th_width else ''
160 return f"<th{th_class}{th_width}>{contents}</th>\n"
163def tr_qa(q: str,
164 a: Any,
165 default: str = "?",
166 default_for_blank_strings: bool = False) -> str:
167 """
168 Make HTML two-column data row (``<tr>...</tr>``), with the right-hand
169 column formatted as an answer.
170 """
171 return tr(q, answer(a, default=default,
172 default_for_blank_strings=default_for_blank_strings))
175def heading_spanning_two_columns(s: str) -> str:
176 """
177 HTML table heading row spanning 2 columns.
178 """
179 return tr_span_col(s, cols=2, tr_class=CssClass.HEADING)
182def subheading_spanning_two_columns(s: str, th_not_td: bool = False) -> str:
183 """
184 HTML table subheading row spanning 2 columns.
185 """
186 return tr_span_col(s, cols=2, tr_class=CssClass.SUBHEADING,
187 th_not_td=th_not_td)
190def subheading_spanning_three_columns(s: str, th_not_td: bool = False) -> str:
191 """
192 HTML table subheading row spanning 3 columns.
193 """
194 return tr_span_col(s, cols=3, tr_class=CssClass.SUBHEADING,
195 th_not_td=th_not_td)
198def subheading_spanning_four_columns(s: str, th_not_td: bool = False) -> str:
199 """
200 HTML table subheading row spanning 4 columns.
201 """
202 return tr_span_col(s, cols=4, tr_class=CssClass.SUBHEADING,
203 th_not_td=th_not_td)
206def bold(x: str) -> str:
207 """
208 Applies HTML bold.
209 """
210 return f"<b>{x}</b>"
213def italic(x: str) -> str:
214 """
215 Applies HTML italic.
216 """
217 return f"<i>{x}</i>"
220def identity(x: Any) -> Any:
221 """
222 Returns argument unchanged.
223 """
224 return x
227def bold_webify(x: str) -> str:
228 """
229 Webifies the string, then makes it bold.
230 """
231 return bold(ws.webify(x))
234def sub(x: str) -> str:
235 """
236 Applies HTML subscript.
237 """
238 return f"<sub>{x}</sub>"
241def sup(x: str) -> str:
242 """
243 Applies HTML superscript.
244 """
245 return f"<sup>{x}</sup>"
248def answer(x: Any,
249 default: str = "?",
250 default_for_blank_strings: bool = False,
251 formatter_answer: Callable[[str], str] = bold_webify,
252 formatter_blank: Callable[[str], str] = italic) -> str:
253 """
254 Formats answer in bold, or the default value if None.
256 Avoid the word "None" for the default, e.g.
257 "Score indicating likelihood of abuse: None"... may be misleading!
258 Prefer "?" instead.
259 """
260 if x is None:
261 return formatter_blank(default)
262 if default_for_blank_strings and not x and isinstance(x, str):
263 return formatter_blank(default)
264 return formatter_answer(x)
267def tr_span_col(x: str,
268 cols: int = 2,
269 tr_class: str = "",
270 td_class: str = "",
271 th_not_td: bool = False) -> str:
272 """
273 HTML table data row spanning several columns.
275 Args:
276 x: Data.
277 cols: Number of columns to span.
278 tr_class: CSS class to apply to tr.
279 td_class: CSS class to apply to td.
280 th_not_td: make it a th, not a td.
281 """
282 cell = "th" if th_not_td else "td"
283 tr_cl = f' class="{tr_class}"' if tr_class else ""
284 td_cl = f' class="{td_class}"' if td_class else ""
285 return f'<tr{tr_cl}><{cell} colspan="{cols}"{td_cl}>{x}</{cell}></tr>'
288def get_data_url(mimetype: str, data: Union[bytes, memoryview]) -> str:
289 """
290 Takes data (in binary format) and returns a data URL as per RFC 2397
291 (https://tools.ietf.org/html/rfc2397), such as:
293 .. code-block:: none
295 data:MIMETYPE;base64,B64_ENCODED_DATA
296 """
297 return f"data:{mimetype};base64,{base64.b64encode(data).decode('ascii')}"
300def get_embedded_img_tag(mimetype: str, data: Union[bytes, memoryview]) -> str:
301 """
302 Takes a binary image and its MIME type, and produces an HTML tag of the
303 form:
305 .. code-block:: none
307 <img src="DATA_URL">
308 """
309 return f'<img src={get_data_url(mimetype, data)}>'
312# =============================================================================
313# Field formatting
314# =============================================================================
316def get_yes_no(req: "CamcopsRequest", x: Any) -> str:
317 """
318 'Yes' if x else 'No'
319 """
320 return req.sstring(SS.YES) if x else req.sstring(SS.NO)
323def get_yes_no_none(req: "CamcopsRequest", x: Any) -> Optional[str]:
324 """
325 Returns 'Yes' for True, 'No' for False, or None for None.
326 """
327 if x is None:
328 return None
329 return get_yes_no(req, x)
332def get_yes_no_unknown(req: "CamcopsRequest", x: Any) -> str:
333 """
334 Returns 'Yes' for True, 'No' for False, or '?' for None.
335 """
336 if x is None:
337 return "?"
338 return get_yes_no(req, x)
341def get_true_false(req: "CamcopsRequest", x: Any) -> str:
342 """
343 'True' if x else 'False'
344 """
345 return req.sstring(SS.TRUE) if x else req.sstring(SS.FALSE)
348def get_true_false_none(req: "CamcopsRequest", x: Any) -> Optional[str]:
349 """
350 Returns 'True' for True, 'False' for False, or None for None.
351 """
352 if x is None:
353 return None
354 return get_true_false(req, x)
357def get_true_false_unknown(req: "CamcopsRequest", x: Any) -> str:
358 """
359 Returns 'True' for True, 'False' for False, or '?' for None.
360 """
361 if x is None:
362 return "?"
363 return get_true_false(req, x)
366def get_present_absent(req: "CamcopsRequest", x: Any) -> str:
367 """
368 'Present' if x else 'Absent'
369 """
370 return req.sstring(SS.PRESENT) if x else req.sstring(SS.ABSENT)
373def get_present_absent_none(req: "CamcopsRequest", x: Any) -> Optional[str]:
374 """
375 Returns 'Present' for True, 'Absent' for False, or None for None.
376 """
377 if x is None:
378 return None
379 return get_present_absent(req, x)
382def get_present_absent_unknown(req: "CamcopsRequest", x: str) -> str:
383 """
384 Returns 'Present' for True, 'Absent' for False, or '?' for None.
385 """
386 if x is None:
387 return "?"
388 return get_present_absent(req, x)
391def get_ternary(x: Any,
392 value_true: Any = True,
393 value_false: Any = False,
394 value_none: Any = None) -> Any:
395 """
396 Returns ``value_none`` if ``x`` is ``None``, ``value_true`` if it's truthy,
397 or ``value_false`` if it's falsy.
398 """
399 if x is None:
400 return value_none
401 if x:
402 return value_true
403 return value_false
406def get_correct_incorrect_none(x: Any) -> Optional[str]:
407 """
408 Returns None if ``x`` is None, "Correct" if it's truthy, or "Incorrect" if
409 it's falsy.
410 """
411 return get_ternary(x, "Correct", "Incorrect", None)