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

191 statements  

« prev     ^ index     » next       coverage.py v6.4.2, created at 2022-07-20 16:12 +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 "seal" : {"coerce" : "unicode"} 

55 }, 

56 "lists": { 

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

58 } 

59 } 

60 } 

61} 

62 

63 

64INCOMING_APPLICATION_REQUIREMENTS = { 

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

66 

67 "structs": { 

68 "bibjson": { 

69 "required": [ 

70 "copyright", 

71 "deposit_policy", 

72 "editorial", 

73 "eissn", 

74 "keywords", 

75 "language", 

76 "license", 

77 "ref", 

78 "pid_scheme", 

79 "pissn", 

80 "plagiarism", 

81 "preservation", 

82 "publication_time_weeks", 

83 "publisher", 

84 "ref", 

85 "oa_start", 

86 "other_charges", 

87 "waiver", 

88 "title" 

89 ], 

90 "structs": { 

91 "copyright": { 

92 "required" : ["url"] 

93 }, 

94 "editorial": { 

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

96 }, 

97 "plagiarism": { 

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

99 }, 

100 "publisher": { 

101 "required": ["name"] 

102 }, 

103 "ref": { 

104 "required" : ["journal"] 

105 } 

106 } 

107 } 

108 } 

109} 

110 

111 

112class IncomingApplication(SeamlessMixin, swagger.SwaggerSupport): 

113 """ 

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

115 """ 

116 __type__ = "application" 

117 __SEAMLESS_COERCE__ = COERCE_MAP 

118 __SEAMLESS_STRUCT__ = [ 

119 OUTGOING_APPLICATION_STRUCT, 

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

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

122 # INTERNAL_APPLICATION_STRUCT, 

123 _SHARED_STRUCT, 

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

125 INCOMING_APPLICATION_REQUIREMENTS 

126 ] 

127 

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

129 if raw is None: 

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

131 else: 

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

133 

134 @property 

135 def data(self): 

136 return self.__seamless__.data 

137 

138 def custom_validate(self): 

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

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

141 return 

142 

143 if _check_for_script(self.data): 

144 raise dataobj.ScriptTagFoundException(Messages.EXCEPTION_SCRIPT_TAG_FOUND) 

145 

146 # extract the p/e-issn identifier objects 

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

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

149 

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

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

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

153 

154 # normalise the ids 

155 if pissn is not None: 

156 pissn = self._normalise_issn(pissn) 

157 if eissn is not None: 

158 eissn = self._normalise_issn(eissn) 

159 

160 # check they are not the same 

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

162 if pissn == eissn: 

163 raise seamless.SeamlessException("P-ISSN and E-ISSN should be different") 

164 

165 # A link to the journal homepage is required 

166 # 

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

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

169 

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

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

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

173 if url is None: 

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

175 

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

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

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

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

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

181 

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

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

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

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

186 

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

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

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

190 

191 def _normalise_issn(self, issn): 

192 issn = issn.upper() 

193 if len(issn) > 8: return issn 

194 if len(issn) == 8: 

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

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

197 if len(issn) < 8: 

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

199 else: 

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

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

202 

203 def to_application_model(self, existing=None): 

204 nd = deepcopy(self.data) 

205 

206 if existing is None: 

207 return models.Suggestion(**nd) 

208 else: 

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

210 return models.Suggestion(**nnd) 

211 

212 @property 

213 def id(self): 

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

215 

216 def set_id(self, id=None): 

217 if id is None: 

218 id = self.makeid() 

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

220 

221 def set_created(self, date=None): 

222 if date is None: 

223 date = dates.now() 

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

225 

226 @property 

227 def created_date(self): 

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

229 

230 @property 

231 def created_timestamp(self): 

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

233 

234 def set_last_updated(self, date=None): 

235 if date is None: 

236 date = dates.now() 

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

238 

239 @property 

240 def last_updated(self): 

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

242 

243 @property 

244 def last_updated_timestamp(self): 

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

246 

247 def set_last_manual_update(self, date=None): 

248 if date is None: 

249 date = dates.now() 

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

251 

252 @property 

253 def last_manual_update(self): 

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

255 

256 @property 

257 def last_manual_update_timestamp(self): 

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

259 

260 def has_been_manually_updated(self): 

261 lmut = self.last_manual_update_timestamp 

262 if lmut is None: 

263 return False 

264 return lmut > datetime.utcfromtimestamp(0) 

265 

266 def has_seal(self): 

267 return self.__seamless__.get_single("admin.seal", default=False) 

268 

269 def set_seal(self, value): 

270 self.__seamless__.set_with_struct("admin.seal", value) 

271 

272 @property 

273 def owner(self): 

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

275 

276 def set_owner(self, owner): 

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

278 

279 def remove_owner(self): 

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

281 

282 @property 

283 def editor_group(self): 

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

285 

286 def set_editor_group(self, eg): 

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

288 

289 def remove_editor_group(self): 

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

291 

292 @property 

293 def editor(self): 

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

295 

296 def set_editor(self, ed): 

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

298 

299 def remove_editor(self): 

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

301 

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

303 if date is None: 

304 date = dates.now() 

305 obj = {"date": date, "note": note, "id": id} 

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

307 if id is None: 

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

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

310 

311 def remove_note(self, note): 

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

313 

314 def set_notes(self, notes): 

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

316 

317 def remove_notes(self): 

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

319 

320 @property 

321 def notes(self): 

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

323 

324 @property 

325 def ordered_notes(self): 

326 notes = self.notes 

327 clusters = {} 

328 for note in notes: 

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

330 clusters[note["date"]] = [note] 

331 else: 

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

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

334 ordered = [] 

335 for key in ordered_keys: 

336 clusters[key].reverse() 

337 ordered += clusters[key] 

338 return ordered 

339 

340 def bibjson(self): 

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

342 if bj is None: 

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

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

345 return JournalLikeBibJSON(bj) 

346 

347 def set_bibjson(self, bibjson): 

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

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

350 

351 

352class OutgoingApplication(OutgoingCommonJournalApplication): 

353 """ 

354 ~~APIOutgoingApplication:Model->APIOutgoingCommonJournalApplication:Model~~ 

355 ~~->Seamless:Library~~ 

356 """ 

357 __SEAMLESS_COERCE__ = COERCE_MAP 

358 __SEAMLESS_STRUCT__ = [ 

359 OUTGOING_APPLICATION_STRUCT, 

360 _SHARED_STRUCT 

361 ] 

362 

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

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

365 

366 @classmethod 

367 def from_model(cls, application): 

368 assert isinstance(application, models.Suggestion) 

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

370 

371 @property 

372 def data(self): 

373 return self.__seamless__.data