-
Notifications
You must be signed in to change notification settings - Fork 16
/
application.py
391 lines (329 loc) · 14 KB
/
application.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
import uuid
from datetime import datetime
from portality.api.current.data_objects.common import _check_for_script
from portality.lib import swagger, seamless, coerce, dates, dataobj
from portality import models
from copy import deepcopy
from portality.api.current.data_objects.common_journal_application import OutgoingCommonJournalApplication, _SHARED_STRUCT
# both incoming and outgoing applications share this struct
# "required" fields are only put on incoming applications
from portality.lib.coerce import COERCE_MAP
from portality.lib.seamless import SeamlessMixin
from portality.models import JournalLikeBibJSON
from portality.ui.messages import Messages
OUTGOING_APPLICATION_STRUCT = {
"fields": {
"id": {"coerce": "unicode"}, # Note that we'll leave these in for ease of use by the
"created_date": {"coerce": "utcdatetime"}, # caller, but we'll need to ignore them on the conversion
"last_updated": {"coerce": "utcdatetime"}, # to the real object
"last_manual_update": {"coerce": "utcdatetime"}
},
"objects": ["admin", "bibjson"],
"structs": {
"admin" : {
"fields" : {
"application_status" : {"coerce" : "unicode"},
"current_journal" : {"coerce" : "unicode"},
"date_applied" : {"coerce" : "unicode"},
"owner" : {"coerce" : "unicode"}
}
}
}
}
INTERNAL_APPLICATION_STRUCT = {
"fields": {
"id": {"coerce": "unicode"}, # Note that we'll leave these in for ease of use by the
"created_date": {"coerce": "utcdatetime"}, # caller, but we'll need to ignore them on the conversion
"last_updated": {"coerce": "utcdatetime"}, # to the real object
"last_manual_update": {"coerce": "utcdatetime"},
"es_type": {"coerce": "unicode"}
},
"objects": ["admin", "bibjson"],
"structs": {
"admin" : {
"fields" : {
"related_journal" : {"coerce" : "unicode"},
"editor_group" : {"coerce" : "unicode"},
"editor" : {"coerce" : "unicode"},
"owner" : {"coerce" : "unicode"},
"seal" : {"coerce" : "unicode"}
},
"lists": {
"notes" : {"contains" : "object"},
}
}
}
}
INCOMING_APPLICATION_REQUIREMENTS = {
"required" : ["admin", "bibjson"],
"structs": {
"bibjson": {
"lists": {
# override for lax language enforcement in the core, making it strict for incoming applications
"language": {"contains": "field", "coerce": "isolang_2letter_strict"}
},
"required": [
"copyright",
"deposit_policy",
"editorial",
"eissn",
"keywords",
"language",
"license",
"ref",
"pid_scheme",
"pissn",
"plagiarism",
"preservation",
"publication_time_weeks",
"publisher",
"oa_start",
"other_charges",
"waiver",
"title"
],
"structs": {
"copyright": {
"required" : ["url"]
},
"editorial": {
"required" : ["review_process", "review_url"]
},
"plagiarism": {
"required": ["detection","url"]
},
"publisher": {
"required": ["name"]
},
"ref": {
"required" : ["journal"]
},
# override for lax currency code enforcement in the core, making it strict for incoming applications
"apc" : {
"lists" : {
"max" : {"contains" : "object"}
},
"structs" : {
"max" : {
"fields" : {
"currency" : {"coerce" : "currency_code_strict"},
"price" : {"coerce" : "integer"}
}
}
}
}
}
}
}
}
class IncomingApplication(SeamlessMixin, swagger.SwaggerSupport):
"""
~~APIIncomingApplication:Model->Seamless:Library~~
"""
__type__ = "application"
__SEAMLESS_COERCE__ = dict(COERCE_MAP)
__SEAMLESS_STRUCT__ = [
# FIXME: Struct merge isn't an OVERRIDE, so we apply the strict checks first since they'll persist
# FIXME: can we live without specifying required fields, since the form validation will handle this?
INCOMING_APPLICATION_REQUIREMENTS,
OUTGOING_APPLICATION_STRUCT,
# FIXME: should this be here? It looks like it allows users to send administrative data to the system
# I have removed it as it was exposing incorrect data in the auto-generated documentation
# INTERNAL_APPLICATION_STRUCT,
_SHARED_STRUCT
]
def __init__(self, raw=None, **kwargs):
if raw is None:
super(IncomingApplication, self).__init__(silent_prune=False, check_required_on_init=False, **kwargs)
else:
super(IncomingApplication, self).__init__(raw=raw, silent_prune=False, **kwargs)
@property
def data(self):
return self.__seamless__.data
def custom_validate(self):
# only attempt to validate if this is not a blank object
if len(list(self.__seamless__.data.keys())) == 0:
return
if _check_for_script(self.data):
raise dataobj.ScriptTagFoundException(Messages.EXCEPTION_SCRIPT_TAG_FOUND)
# extract the p/e-issn identifier objects
pissn = self.data["bibjson"]["pissn"]
eissn = self.data["bibjson"]["eissn"]
# check that at least one of them appears and if they are different
if pissn is None and eissn is None or pissn == eissn:
raise seamless.SeamlessException("You must specify at least one of bibjson.pissn and bibjson.eissn, and they must be different")
# normalise the ids
if pissn is not None:
pissn = self._normalise_issn(pissn)
if eissn is not None:
eissn = self._normalise_issn(eissn)
# check they are not the same
if pissn is not None and eissn is not None:
if pissn == eissn:
raise seamless.SeamlessException("P-ISSN and E-ISSN should be different")
# A link to the journal homepage is required
#
if self.data["bibjson"]["ref"]["journal"] is None or self.data["bibjson"]["ref"]["journal"] == "":
raise seamless.SeamlessException("You must specify the journal homepage in bibjson.ref.journal")
# if plagiarism detection is done, then the url is a required field
if self.data["bibjson"]["plagiarism"]["detection"] is True:
url = self.data["bibjson"]["plagiarism"]["url"]
if url is None:
raise seamless.SeamlessException("In this context bibjson.plagiarism.url is required")
# if licence_display is "embed", then the url is a required field #TODO: what with "display"
art = self.data["bibjson"]["article"]
if "embed" in art["license_display"] or "display" in art["license_display"]:
if art["license_display_example_url"] is None or art["license_display_example_url"] == "":
raise seamless.SeamlessException("In this context bibjson.article.license_display_example_url is required")
# if the author does not hold the copyright the url is optional, otherwise it is required
if self.data["bibjson"]["copyright"]["author_retains"] is not False:
if self.data["bibjson"]["copyright"]["url"] is None or self.data["bibjson"]["copyright"]["url"] == "":
raise seamless.SeamlessException("In this context bibjson.copyright.url is required")
# check the number of keywords is no more than 6
if len(self.data["bibjson"]["keywords"]) > 6:
raise seamless.SeamlessException("bibjson.keywords may only contain a maximum of 6 keywords")
def _normalise_issn(self, issn):
issn = issn.upper()
if len(issn) > 8: return issn
if len(issn) == 8:
if "-" in issn: return "0" + issn
else: return issn[:4] + "-" + issn[4:]
if len(issn) < 8:
if "-" in issn: return ("0" * (9 - len(issn))) + issn
else:
issn = ("0" * (8 - len(issn))) + issn
return issn[:4] + "-" + issn[4:]
def to_application_model(self, existing=None):
nd = deepcopy(self.data)
if existing is None:
return models.Suggestion(**nd)
else:
nnd = seamless.SeamlessMixin.extend_struct(self._struct, nd)
return models.Suggestion(**nnd)
@property
def id(self):
return self.__seamless__.get_single("id")
def set_id(self, id=None):
if id is None:
id = self.makeid()
self.__seamless__.set_with_struct("id", id)
def set_created(self, date=None):
if date is None:
date = dates.now_str()
self.__seamless__.set_with_struct("created_date", date)
@property
def created_date(self):
return self.__seamless__.get_single("created_date")
@property
def created_timestamp(self):
return self.__seamless__.get_single("created_date", coerce=coerce.to_datestamp())
def set_last_updated(self, date=None):
if date is None:
date = dates.now_str()
self.__seamless__.set_with_struct("last_updated", date)
@property
def last_updated(self):
return self.__seamless__.get_single("last_updated")
@property
def last_updated_timestamp(self):
return self.__seamless__.get_single("last_updated", coerce=coerce.to_datestamp())
def set_last_manual_update(self, date=None):
if date is None:
date = dates.now_str()
self.__seamless__.set_with_struct("last_manual_update", date)
@property
def last_manual_update(self):
return self.__seamless__.get_single("last_manual_update")
@property
def last_manual_update_timestamp(self):
return self.__seamless__.get_single("last_manual_update", coerce=coerce.to_datestamp())
def has_been_manually_updated(self):
lmut = self.last_manual_update_timestamp
if lmut is None:
return False
return lmut > datetime.utcfromtimestamp(0)
def has_seal(self):
return self.__seamless__.get_single("admin.seal", default=False)
def set_seal(self, value):
self.__seamless__.set_with_struct("admin.seal", value)
@property
def owner(self):
return self.__seamless__.get_single("admin.owner")
def set_owner(self, owner):
self.__seamless__.set_with_struct("admin.owner", owner)
def remove_owner(self):
self.__seamless__.delete("admin.owner")
@property
def editor_group(self):
return self.__seamless__.get_single("admin.editor_group")
def set_editor_group(self, eg):
self.__seamless__.set_with_struct("admin.editor_group", eg)
def remove_editor_group(self):
self.__seamless__.delete("admin.editor_group")
@property
def editor(self):
return self.__seamless__.get_single("admin.editor")
def set_editor(self, ed):
self.__seamless__.set_with_struct("admin.editor", ed)
def remove_editor(self):
self.__seamless__.delete('admin.editor')
def add_note(self, note, date=None, id=None, author_id=None,):
if date is None:
date = dates.now_str()
obj = {"date": date, "note": note, "id": id, "author_id": author_id}
self.__seamless__.delete_from_list("admin.notes", matchsub=obj)
if id is None:
obj["id"] = uuid.uuid4()
self.__seamless__.add_to_list_with_struct("admin.notes", obj)
def remove_note(self, note):
self.__seamless__.delete_from_list("admin.notes", matchsub=note)
def set_notes(self, notes):
self.__seamless__.set_with_struct("admin.notes", notes)
def remove_notes(self):
self.__seamless__.delete("admin.notes")
@property
def notes(self):
return self.__seamless__.get_list("admin.notes")
@property
def ordered_notes(self):
notes = self.notes
clusters = {}
for note in notes:
if note["date"] not in clusters:
clusters[note["date"]] = [note]
else:
clusters[note["date"]].append(note)
ordered_keys = sorted(list(clusters.keys()), reverse=True)
ordered = []
for key in ordered_keys:
clusters[key].reverse()
ordered += clusters[key]
return ordered
def bibjson(self):
bj = self.__seamless__.get_single("bibjson")
if bj is None:
self.__seamless__.set_single("bibjson", {})
bj = self.__seamless__.get_single("bibjson")
return JournalLikeBibJSON(bj)
def set_bibjson(self, bibjson):
bibjson = bibjson.data if isinstance(bibjson, JournalLikeBibJSON) else bibjson
self.__seamless__.set_with_struct("bibjson", bibjson)
class OutgoingApplication(OutgoingCommonJournalApplication):
"""
~~APIOutgoingApplication:Model->APIOutgoingCommonJournalApplication:Model~~
~~->Seamless:Library~~
"""
__SEAMLESS_COERCE__ = dict(COERCE_MAP)
__SEAMLESS_STRUCT__ = [
OUTGOING_APPLICATION_STRUCT,
_SHARED_STRUCT
]
def __init__(self, raw=None, **kwargs):
super(OutgoingApplication, self).__init__(raw, silent_prune=True, **kwargs)
@classmethod
def from_model(cls, application):
assert isinstance(application, models.Suggestion)
return super(OutgoingApplication, cls).from_model(application)
@property
def data(self):
return self.__seamless__.data