Coverage for cc_modules/cc_filename.py: 34%
97 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_filename.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**Functions for handling filenames, and some associated constants.**
30"""
32import logging
33import os
34from typing import List, TYPE_CHECKING
36from cardinal_pythonlib.datetimefunc import (
37 format_datetime,
38 get_now_localtz_pendulum,
39)
40from cardinal_pythonlib.logs import BraceStyleAdapter
41from cardinal_pythonlib.stringfunc import mangle_unicode_to_ascii
42from pendulum import Date, DateTime as Pendulum
44from camcops_server.cc_modules.cc_constants import DateFormat
45from camcops_server.cc_modules.cc_exception import STR_FORMAT_EXCEPTIONS
47if TYPE_CHECKING:
48 from camcops_server.cc_modules.cc_patientidnum import (
49 PatientIdNum, # noqa: F401
50 )
51 from camcops_server.cc_modules.cc_request import (
52 CamcopsRequest, # noqa: F401
53 )
55log = BraceStyleAdapter(logging.getLogger(__name__))
58# =============================================================================
59# Ancillary functions for export filenames
60# =============================================================================
63class PatientSpecElementForFilename(object):
64 """
65 Parts of the patient information that can be used to autogenerate
66 the "patient" part of a filename specification.
67 """
69 SURNAME = "surname"
70 FORENAME = "forename"
71 DOB = "dob"
72 SEX = "sex"
73 ALLIDNUMS = "allidnums"
74 IDSHORTDESC_PREFIX = "idshortdesc" # special
75 IDNUM_PREFIX = "idnum" # special
78class FilenameSpecElement(object):
79 """
80 Types of informatino that can be used to autogenerate a filename.
81 """
83 PATIENT = "patient"
84 CREATED = "created"
85 NOW = "now"
86 TASKTYPE = "tasktype"
87 SERVERPK = "serverpk"
88 FILETYPE = "filetype"
89 ANONYMOUS = "anonymous"
90 # ... plus all those from PatientSpecElementForFilename
93def patient_spec_for_filename_is_valid(
94 patient_spec: str, valid_which_idnums: List[int]
95) -> bool:
96 """
97 Returns ``True`` if the ``patient_spec`` appears valid; otherwise
98 ``False``.
99 """
100 pse = PatientSpecElementForFilename
101 testdict = {
102 pse.SURNAME: "surname",
103 pse.FORENAME: "forename",
104 pse.DOB: "dob",
105 pse.SEX: "sex",
106 pse.ALLIDNUMS: "allidnums",
107 }
108 for n in valid_which_idnums:
109 nstr = str(n)
110 testdict[pse.IDSHORTDESC_PREFIX + nstr] = pse.IDSHORTDESC_PREFIX + nstr
111 testdict[pse.IDNUM_PREFIX + nstr] = pse.IDNUM_PREFIX + nstr
112 try:
113 # Legal substitutions only?
114 patient_spec.format(**testdict)
115 return True
116 except STR_FORMAT_EXCEPTIONS: # duff patient_spec; details unimportant
117 return False
120def filename_spec_is_valid(
121 filename_spec: str, valid_which_idnums: List[int]
122) -> bool:
123 """
124 Returns ``True`` if the ``filename_spec`` appears valid; otherwise
125 ``False``.
126 """
127 pse = PatientSpecElementForFilename
128 fse = FilenameSpecElement
129 testdict = {
130 # As above:
131 pse.SURNAME: "surname",
132 pse.FORENAME: "forename",
133 pse.DOB: "dob",
134 pse.SEX: "sex",
135 pse.ALLIDNUMS: "allidnums",
136 # Plus:
137 fse.PATIENT: "patient",
138 fse.CREATED: "created",
139 fse.NOW: "now",
140 fse.TASKTYPE: "tasktype",
141 fse.SERVERPK: "serverpk",
142 fse.FILETYPE: "filetype",
143 fse.ANONYMOUS: "anonymous",
144 }
145 for n in valid_which_idnums:
146 nstr = str(n)
147 testdict[pse.IDSHORTDESC_PREFIX + nstr] = pse.IDSHORTDESC_PREFIX + nstr
148 testdict[pse.IDNUM_PREFIX + nstr] = pse.IDNUM_PREFIX + nstr
149 try:
150 # Legal substitutions only?
151 filename_spec.format(**testdict)
152 return True
153 except STR_FORMAT_EXCEPTIONS: # duff filename_spec; details unimportant
154 return False
157def get_export_filename(
158 req: "CamcopsRequest",
159 patient_spec_if_anonymous: str,
160 patient_spec: str,
161 filename_spec: str,
162 filetype: str,
163 is_anonymous: bool = False,
164 surname: str = None,
165 forename: str = None,
166 dob: Date = None,
167 sex: str = None,
168 idnum_objects: List["PatientIdNum"] = None,
169 creation_datetime: Pendulum = None,
170 basetable: str = None,
171 serverpk: int = None,
172 skip_conversion_to_safe_filename: bool = False,
173) -> str:
174 """
175 Get filename, for file exports/transfers.
176 Also used for e-mail headers and bodies.
178 Args:
179 req: :class:`camcops_server.cc_modules.cc_request.CamcopsRequest`
180 patient_spec_if_anonymous:
181 patient specification to be used for anonymous tasks
182 patient_spec:
183 patient specification to be used for patient-identifiable tasks
184 filename_spec:
185 specification to use to create the filename (may include
186 patient information from the patient specification)
187 filetype:
188 task output format and therefore file type (e.g. HTML, PDF, XML)
189 is_anonymous: is it an anonymous task?
190 surname: patient's surname
191 forename: patient's forename
192 dob: patient's date of birth
193 sex: patient's sex
194 idnum_objects: list of :class:`PatientIdNum` objects for the patient
195 creation_datetime: date/time the task was created
196 basetable: name of the task's base table
197 serverpk: server PK of the task
198 skip_conversion_to_safe_filename: don't bother converting the result
199 to a safe filename (because it'll be used for something else, like
200 an e-mail subject)
202 Returns:
203 the generated filename
205 """
206 idnum_objects = idnum_objects or [] # type: List['PatientIdNum']
207 pse = PatientSpecElementForFilename
208 fse = FilenameSpecElement
209 d = {
210 pse.SURNAME: surname or "",
211 pse.FORENAME: forename or "",
212 pse.DOB: (
213 format_datetime(dob, DateFormat.FILENAME_DATE_ONLY, "")
214 if dob
215 else ""
216 ),
217 pse.SEX: sex or "",
218 }
219 all_id_components = []
220 for idobj in idnum_objects:
221 if idobj.which_idnum is not None:
222 nstr = str(idobj.which_idnum)
223 has_num = idobj.idnum_value is not None
224 d[pse.IDNUM_PREFIX + nstr] = (
225 str(idobj.idnum_value) if has_num else ""
226 )
227 d[pse.IDSHORTDESC_PREFIX + nstr] = (
228 idobj.short_description(req) or ""
229 )
230 if has_num and idobj.short_description(req):
231 all_id_components.append(idobj.get_filename_component(req))
232 d[pse.ALLIDNUMS] = "_".join(all_id_components)
233 if is_anonymous:
234 patient = patient_spec_if_anonymous
235 else:
236 try:
237 patient = str(patient_spec).format(**d)
238 except STR_FORMAT_EXCEPTIONS:
239 log.warning(
240 "Bad patient_spec: {!r}; dictionary was {!r}", patient_spec, d
241 )
242 patient = "invalid_patient_spec"
243 d.update(
244 {
245 fse.PATIENT: patient,
246 fse.CREATED: format_datetime(
247 creation_datetime, DateFormat.FILENAME, ""
248 ),
249 fse.NOW: format_datetime(
250 get_now_localtz_pendulum(), DateFormat.FILENAME
251 ),
252 fse.TASKTYPE: str(basetable or ""),
253 fse.SERVERPK: str(serverpk or ""),
254 fse.FILETYPE: filetype.lower(),
255 fse.ANONYMOUS: patient_spec_if_anonymous if is_anonymous else "",
256 }
257 )
258 try:
259 formatted = str(filename_spec).format(**d)
260 except STR_FORMAT_EXCEPTIONS:
261 log.warning("Bad filename_spec: {!r}", filename_spec)
262 formatted = "invalid_filename_spec"
263 if skip_conversion_to_safe_filename:
264 return formatted
265 return convert_string_for_filename(formatted, allow_paths=True)
268def convert_string_for_filename(s: str, allow_paths: bool = False) -> str:
269 """
270 Remove characters that don't play nicely in filenames across multiple
271 operating systems.
272 """
273 # http://stackoverflow.com/questions/7406102
274 # ... modified
275 s = mangle_unicode_to_ascii(s)
276 s = s.replace(" ", "_")
277 keepcharacters = [".", "_", "-"]
278 if allow_paths:
279 keepcharacters.extend([os.sep]) # '/' under UNIX; '\' under Windows
280 s = "".join(c for c in s if c.isalnum() or c in keepcharacters)
281 return s
284def change_filename_ext(filename: str, new_extension_with_dot: str) -> str:
285 """
286 Replaces the extension, i.e. the part of the filename after its last '.'.
287 """
288 (root, ext) = os.path.splitext(filename)
289 # ... converts "blah.blah.txt" to ("blah.blah", ".txt")
290 return root + new_extension_with_dot