Coverage for portality/api/current/data_objects/application.py: 60%
191 statements
« prev ^ index » next coverage.py v6.4.2, created at 2022-07-22 15:59 +0100
« prev ^ index » next coverage.py v6.4.2, created at 2022-07-22 15:59 +0100
1import uuid
2from datetime import datetime
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
9from portality.api.current.data_objects.common_journal_application import OutgoingCommonJournalApplication, _SHARED_STRUCT
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
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}
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}
64INCOMING_APPLICATION_REQUIREMENTS = {
65 "required" : ["admin", "bibjson"],
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}
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 ]
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)
134 @property
135 def data(self):
136 return self.__seamless__.data
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
143 if _check_for_script(self.data):
144 raise dataobj.ScriptTagFoundException(Messages.EXCEPTION_SCRIPT_TAG_FOUND)
146 # extract the p/e-issn identifier objects
147 pissn = self.data["bibjson"]["pissn"]
148 eissn = self.data["bibjson"]["eissn"]
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")
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)
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")
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")
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")
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")
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")
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")
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:]
203 def to_application_model(self, existing=None):
204 nd = deepcopy(self.data)
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)
212 @property
213 def id(self):
214 return self.__seamless__.get_single("id")
216 def set_id(self, id=None):
217 if id is None:
218 id = self.makeid()
219 self.__seamless__.set_with_struct("id", id)
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)
226 @property
227 def created_date(self):
228 return self.__seamless__.get_single("created_date")
230 @property
231 def created_timestamp(self):
232 return self.__seamless__.get_single("created_date", coerce=coerce.to_datestamp())
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)
239 @property
240 def last_updated(self):
241 return self.__seamless__.get_single("last_updated")
243 @property
244 def last_updated_timestamp(self):
245 return self.__seamless__.get_single("last_updated", coerce=coerce.to_datestamp())
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)
252 @property
253 def last_manual_update(self):
254 return self.__seamless__.get_single("last_manual_update")
256 @property
257 def last_manual_update_timestamp(self):
258 return self.__seamless__.get_single("last_manual_update", coerce=coerce.to_datestamp())
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)
266 def has_seal(self):
267 return self.__seamless__.get_single("admin.seal", default=False)
269 def set_seal(self, value):
270 self.__seamless__.set_with_struct("admin.seal", value)
272 @property
273 def owner(self):
274 return self.__seamless__.get_single("admin.owner")
276 def set_owner(self, owner):
277 self.__seamless__.set_with_struct("admin.owner", owner)
279 def remove_owner(self):
280 self.__seamless__.delete("admin.owner")
282 @property
283 def editor_group(self):
284 return self.__seamless__.get_single("admin.editor_group")
286 def set_editor_group(self, eg):
287 self.__seamless__.set_with_struct("admin.editor_group", eg)
289 def remove_editor_group(self):
290 self.__seamless__.delete("admin.editor_group")
292 @property
293 def editor(self):
294 return self.__seamless__.get_single("admin.editor")
296 def set_editor(self, ed):
297 self.__seamless__.set_with_struct("admin.editor", ed)
299 def remove_editor(self):
300 self.__seamless__.delete('admin.editor')
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)
311 def remove_note(self, note):
312 self.__seamless__.delete_from_list("admin.notes", matchsub=note)
314 def set_notes(self, notes):
315 self.__seamless__.set_with_struct("admin.notes", notes)
317 def remove_notes(self):
318 self.__seamless__.delete("admin.notes")
320 @property
321 def notes(self):
322 return self.__seamless__.get_list("admin.notes")
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
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)
347 def set_bibjson(self, bibjson):
348 bibjson = bibjson.data if isinstance(bibjson, JournalLikeBibJSON) else bibjson
349 self.__seamless__.set_with_struct("bibjson", bibjson)
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 ]
363 def __init__(self, raw=None, **kwargs):
364 super(OutgoingApplication, self).__init__(raw, silent_prune=True, **kwargs)
366 @classmethod
367 def from_model(cls, application):
368 assert isinstance(application, models.Suggestion)
369 return super(OutgoingApplication, cls).from_model(application)
371 @property
372 def data(self):
373 return self.__seamless__.data