Coverage for cc_modules/cc_filename.py : 34%

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