Coverage for portality / api / current / data_objects / application.py: 60%

187 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-05-05 00:09 +0100

1import uuid 

2from datetime import datetime 

3 

4from portality.api.current.data_objects.common import _check_for_script 

5from portality.lib import swagger, seamless, coerce, dates, dataobj 

6from portality import models 

7from copy import deepcopy 

8 

9from portality.api.current.data_objects.common_journal_application import OutgoingCommonJournalApplication, _SHARED_STRUCT 

10 

11# both incoming and outgoing applications share this struct 

12# "required" fields are only put on incoming applications 

13from portality.lib.coerce import COERCE_MAP 

14from portality.lib.seamless import SeamlessMixin 

15from portality.models import JournalLikeBibJSON 

16from portality.ui.messages import Messages 

17 

18OUTGOING_APPLICATION_STRUCT = { 

19 "fields": { 

20 "id": {"coerce": "unicode"}, # Note that we'll leave these in for ease of use by the 

21 "created_date": {"coerce": "utcdatetime"}, # caller, but we'll need to ignore them on the conversion 

22 "last_updated": {"coerce": "utcdatetime"}, # to the real object 

23 "last_manual_update": {"coerce": "utcdatetime"} 

24 }, 

25 "objects": ["admin", "bibjson"], 

26 "structs": { 

27 "admin" : { 

28 "fields" : { 

29 "application_status" : {"coerce" : "unicode"}, 

30 "current_journal" : {"coerce" : "unicode"}, 

31 "date_applied" : {"coerce" : "unicode"}, 

32 "owner" : {"coerce" : "unicode"} 

33 } 

34 } 

35 } 

36} 

37 

38INTERNAL_APPLICATION_STRUCT = { 

39 "fields": { 

40 "id": {"coerce": "unicode"}, # Note that we'll leave these in for ease of use by the 

41 "created_date": {"coerce": "utcdatetime"}, # caller, but we'll need to ignore them on the conversion 

42 "last_updated": {"coerce": "utcdatetime"}, # to the real object 

43 "last_manual_update": {"coerce": "utcdatetime"}, 

44 "es_type": {"coerce": "unicode"} 

45 }, 

46 "objects": ["admin", "bibjson"], 

47 "structs": { 

48 "admin" : { 

49 "fields" : { 

50 "related_journal" : {"coerce" : "unicode"}, 

51 "editor_group" : {"coerce" : "unicode"}, 

52 "editor" : {"coerce" : "unicode"}, 

53 "owner" : {"coerce" : "unicode"} 

54 }, 

55 "lists": { 

56 "notes" : {"contains" : "object"}, 

57 } 

58 } 

59 } 

60} 

61 

62 

63INCOMING_APPLICATION_REQUIREMENTS = { 

64 "required" : ["admin", "bibjson"], 

65 

66 "structs": { 

67 "bibjson": { 

68 "lists": { 

69 # override for lax language enforcement in the core, making it strict for incoming applications 

70 "language": {"contains": "field", "coerce": "isolang_2letter_strict"} 

71 }, 

72 "required": [ 

73 "copyright", 

74 "deposit_policy", 

75 "editorial", 

76 "eissn", 

77 "keywords", 

78 "language", 

79 "license", 

80 "ref", 

81 "pid_scheme", 

82 "pissn", 

83 "plagiarism", 

84 "preservation", 

85 "publication_time_weeks", 

86 "publisher", 

87 "oa_start", 

88 "other_charges", 

89 "waiver", 

90 "title" 

91 ], 

92 "structs": { 

93 "copyright": { 

94 "required" : ["url"] 

95 }, 

96 "editorial": { 

97 "required" : ["review_process", "review_url"] 

98 }, 

99 "plagiarism": { 

100 "required": ["detection","url"] 

101 }, 

102 "publisher": { 

103 "required": ["name"] 

104 }, 

105 "ref": { 

106 "required" : ["journal"] 

107 }, 

108 # override for lax currency code enforcement in the core, making it strict for incoming applications 

109 "apc" : { 

110 "lists" : { 

111 "max" : {"contains" : "object"} 

112 }, 

113 "structs" : { 

114 "max" : { 

115 "fields" : { 

116 "currency" : {"coerce" : "currency_code_strict"}, 

117 "price" : {"coerce" : "integer"} 

118 } 

119 } 

120 } 

121 } 

122 } 

123 } 

124 } 

125} 

126 

127 

128class IncomingApplication(SeamlessMixin, swagger.SwaggerSupport): 

129 """ 

130 ~~APIIncomingApplication:Model->Seamless:Library~~ 

131 """ 

132 __type__ = "application" 

133 __SEAMLESS_COERCE__ = dict(COERCE_MAP) 

134 __SEAMLESS_STRUCT__ = [ 

135 # FIXME: Struct merge isn't an OVERRIDE, so we apply the strict checks first since they'll persist 

136 # FIXME: can we live without specifying required fields, since the form validation will handle this? 

137 INCOMING_APPLICATION_REQUIREMENTS, 

138 OUTGOING_APPLICATION_STRUCT, 

139 # FIXME: should this be here? It looks like it allows users to send administrative data to the system 

140 # I have removed it as it was exposing incorrect data in the auto-generated documentation 

141 # INTERNAL_APPLICATION_STRUCT, 

142 _SHARED_STRUCT 

143 ] 

144 

145 def __init__(self, raw=None, **kwargs): 

146 if raw is None: 

147 super(IncomingApplication, self).__init__(silent_prune=False, check_required_on_init=False, **kwargs) 

148 else: 

149 super(IncomingApplication, self).__init__(raw=raw, silent_prune=False, **kwargs) 

150 

151 @property 

152 def data(self): 

153 return self.__seamless__.data 

154 

155 def custom_validate(self): 

156 # only attempt to validate if this is not a blank object 

157 if len(list(self.__seamless__.data.keys())) == 0: 

158 return 

159 

160 if _check_for_script(self.data): 

161 raise dataobj.ScriptTagFoundException(Messages.EXCEPTION_SCRIPT_TAG_FOUND) 

162 

163 # extract the p/e-issn identifier objects 

164 pissn = self.data["bibjson"]["pissn"] 

165 eissn = self.data["bibjson"]["eissn"] 

166 

167 # check that at least one of them appears and if they are different 

168 if pissn is None and eissn is None or pissn == eissn: 

169 raise seamless.SeamlessException("You must specify at least one of bibjson.pissn and bibjson.eissn, and they must be different") 

170 

171 # normalise the ids 

172 if pissn is not None: 

173 pissn = self._normalise_issn(pissn) 

174 if eissn is not None: 

175 eissn = self._normalise_issn(eissn) 

176 

177 # check they are not the same 

178 if pissn is not None and eissn is not None: 

179 if pissn == eissn: 

180 raise seamless.SeamlessException("Print ISSN and Online ISSN should be different") 

181 

182 # A link to the journal homepage is required 

183 # 

184 if self.data["bibjson"]["ref"]["journal"] is None or self.data["bibjson"]["ref"]["journal"] == "": 

185 raise seamless.SeamlessException("You must specify the journal homepage in bibjson.ref.journal") 

186 

187 # if plagiarism detection is done, then the url is a required field 

188 if self.data["bibjson"]["plagiarism"]["detection"] is True: 

189 url = self.data["bibjson"]["plagiarism"]["url"] 

190 if url is None: 

191 raise seamless.SeamlessException("In this context bibjson.plagiarism.url is required") 

192 

193 # if licence_display is "embed", then the url is a required field #TODO: what with "display" 

194 art = self.data["bibjson"]["article"] 

195 if "embed" in art["license_display"] or "display" in art["license_display"]: 

196 if art["license_display_example_url"] is None or art["license_display_example_url"] == "": 

197 raise seamless.SeamlessException("In this context bibjson.article.license_display_example_url is required") 

198 

199 # if the author does not hold the copyright the url is optional, otherwise it is required 

200 if self.data["bibjson"]["copyright"]["author_retains"] is not False: 

201 if self.data["bibjson"]["copyright"]["url"] is None or self.data["bibjson"]["copyright"]["url"] == "": 

202 raise seamless.SeamlessException("In this context bibjson.copyright.url is required") 

203 

204 # check the number of keywords is no more than 6 

205 if len(self.data["bibjson"]["keywords"]) > 6: 

206 raise seamless.SeamlessException("bibjson.keywords may only contain a maximum of 6 keywords") 

207 

208 def _normalise_issn(self, issn): 

209 issn = issn.upper() 

210 if len(issn) > 8: return issn 

211 if len(issn) == 8: 

212 if "-" in issn: return "0" + issn 

213 else: return issn[:4] + "-" + issn[4:] 

214 if len(issn) < 8: 

215 if "-" in issn: return ("0" * (9 - len(issn))) + issn 

216 else: 

217 issn = ("0" * (8 - len(issn))) + issn 

218 return issn[:4] + "-" + issn[4:] 

219 

220 def to_application_model(self, existing=None): 

221 nd = deepcopy(self.data) 

222 

223 if existing is None: 

224 return models.Suggestion(**nd) 

225 else: 

226 nnd = seamless.SeamlessMixin.extend_struct(self._struct, nd) 

227 return models.Suggestion(**nnd) 

228 

229 @property 

230 def id(self): 

231 return self.__seamless__.get_single("id") 

232 

233 def set_id(self, id=None): 

234 if id is None: 

235 id = self.makeid() 

236 self.__seamless__.set_with_struct("id", id) 

237 

238 def set_created(self, date=None): 

239 if date is None: 

240 date = dates.now_str() 

241 self.__seamless__.set_with_struct("created_date", date) 

242 

243 @property 

244 def created_date(self): 

245 return self.__seamless__.get_single("created_date") 

246 

247 @property 

248 def created_timestamp(self): 

249 return self.__seamless__.get_single("created_date", coerce=coerce.to_datestamp()) 

250 

251 def set_last_updated(self, date=None): 

252 if date is None: 

253 date = dates.now_str() 

254 self.__seamless__.set_with_struct("last_updated", date) 

255 

256 @property 

257 def last_updated(self): 

258 return self.__seamless__.get_single("last_updated") 

259 

260 @property 

261 def last_updated_timestamp(self): 

262 return self.__seamless__.get_single("last_updated", coerce=coerce.to_datestamp()) 

263 

264 def set_last_manual_update(self, date=None): 

265 if date is None: 

266 date = dates.now_str() 

267 self.__seamless__.set_with_struct("last_manual_update", date) 

268 

269 @property 

270 def last_manual_update(self): 

271 return self.__seamless__.get_single("last_manual_update") 

272 

273 @property 

274 def last_manual_update_timestamp(self): 

275 return self.__seamless__.get_single("last_manual_update", coerce=coerce.to_datestamp()) 

276 

277 def has_been_manually_updated(self): 

278 lmut = self.last_manual_update_timestamp 

279 if lmut is None: 

280 return False 

281 return lmut > datetime.utcfromtimestamp(0) 

282 

283 @property 

284 def owner(self): 

285 return self.__seamless__.get_single("admin.owner") 

286 

287 def set_owner(self, owner): 

288 self.__seamless__.set_with_struct("admin.owner", owner) 

289 

290 def remove_owner(self): 

291 self.__seamless__.delete("admin.owner") 

292 

293 @property 

294 def editor_group(self): 

295 return self.__seamless__.get_single("admin.editor_group") 

296 

297 def set_editor_group(self, eg): 

298 self.__seamless__.set_with_struct("admin.editor_group", eg) 

299 

300 def remove_editor_group(self): 

301 self.__seamless__.delete("admin.editor_group") 

302 

303 @property 

304 def editor(self): 

305 return self.__seamless__.get_single("admin.editor") 

306 

307 def set_editor(self, ed): 

308 self.__seamless__.set_with_struct("admin.editor", ed) 

309 

310 def remove_editor(self): 

311 self.__seamless__.delete('admin.editor') 

312 

313 def add_note(self, note, date=None, id=None, author_id=None,): 

314 if date is None: 

315 date = dates.now_str() 

316 obj = {"date": date, "note": note, "id": id, "author_id": author_id} 

317 self.__seamless__.delete_from_list("admin.notes", matchsub=obj) 

318 if id is None: 

319 obj["id"] = uuid.uuid4() 

320 self.__seamless__.add_to_list_with_struct("admin.notes", obj) 

321 

322 def remove_note(self, note): 

323 self.__seamless__.delete_from_list("admin.notes", matchsub=note) 

324 

325 def set_notes(self, notes): 

326 self.__seamless__.set_with_struct("admin.notes", notes) 

327 

328 def remove_notes(self): 

329 self.__seamless__.delete("admin.notes") 

330 

331 @property 

332 def notes(self): 

333 return self.__seamless__.get_list("admin.notes") 

334 

335 @property 

336 def ordered_notes(self): 

337 notes = self.notes 

338 clusters = {} 

339 for note in notes: 

340 if note["date"] not in clusters: 

341 clusters[note["date"]] = [note] 

342 else: 

343 clusters[note["date"]].append(note) 

344 ordered_keys = sorted(list(clusters.keys()), reverse=True) 

345 ordered = [] 

346 for key in ordered_keys: 

347 clusters[key].reverse() 

348 ordered += clusters[key] 

349 return ordered 

350 

351 def bibjson(self): 

352 bj = self.__seamless__.get_single("bibjson") 

353 if bj is None: 

354 self.__seamless__.set_single("bibjson", {}) 

355 bj = self.__seamless__.get_single("bibjson") 

356 return JournalLikeBibJSON(bj) 

357 

358 def set_bibjson(self, bibjson): 

359 bibjson = bibjson.data if isinstance(bibjson, JournalLikeBibJSON) else bibjson 

360 self.__seamless__.set_with_struct("bibjson", bibjson) 

361 

362 

363class OutgoingApplication(OutgoingCommonJournalApplication): 

364 """ 

365 ~~APIOutgoingApplication:Model->APIOutgoingCommonJournalApplication:Model~~ 

366 ~~->Seamless:Library~~ 

367 """ 

368 __SEAMLESS_COERCE__ = dict(COERCE_MAP) 

369 __SEAMLESS_STRUCT__ = [ 

370 OUTGOING_APPLICATION_STRUCT, 

371 _SHARED_STRUCT 

372 ] 

373 

374 def __init__(self, raw=None, **kwargs): 

375 super(OutgoingApplication, self).__init__(raw, silent_prune=True, **kwargs) 

376 

377 @classmethod 

378 def from_model(cls, application): 

379 assert isinstance(application, models.Suggestion) 

380 return super(OutgoingApplication, cls).from_model(application) 

381 

382 @property 

383 def data(self): 

384 return self.__seamless__.data