Hide keyboard shortcuts

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 

2 

3""" 

4camcops_server/cc_modules/cc_filename.py 

5 

6=============================================================================== 

7 

8 Copyright (C) 2012-2020 Rudolf Cardinal (rudolf@pobox.com). 

9 

10 This file is part of CamCOPS. 

11 

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. 

16 

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. 

21 

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/>. 

24 

25=============================================================================== 

26 

27**Functions for handling filenames, and some associated constants.** 

28 

29""" 

30 

31import logging 

32import os 

33from typing import List, TYPE_CHECKING 

34 

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 

42 

43from camcops_server.cc_modules.cc_constants import DateFormat 

44from camcops_server.cc_modules.cc_exception import STR_FORMAT_EXCEPTIONS 

45 

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 

49 

50log = BraceStyleAdapter(logging.getLogger(__name__)) 

51 

52 

53# ============================================================================= 

54# Ancillary functions for export filenames 

55# ============================================================================= 

56 

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 

69 

70 

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 

83 

84 

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 

109 

110 

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 

145 

146 

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. 

165 

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) 

189 

190 Returns: 

191 the generated filename 

192 

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) 

244 

245 

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 

260 

261 

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