-
Notifications
You must be signed in to change notification settings - Fork 16
/
journal.py
1170 lines (954 loc) · 37.9 KB
/
journal.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
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
from portality.dao import DomainObject
from portality.core import app
from portality.lib.dates import DEFAULT_TIMESTAMP_VAL
from portality.models.v2.bibjson import JournalLikeBibJSON
from portality.models.v2 import shared_structs
from portality.models.account import Account
from portality.lib import es_data_mapping, dates, coerce
from portality.lib.seamless import SeamlessMixin
from portality.lib.coerce import COERCE_MAP
from copy import deepcopy
from datetime import datetime, timedelta
import string, uuid
from unidecode import unidecode
JOURNAL_STRUCT = {
"objects": [
"admin", "index"
],
"structs": {
"admin": {
"fields": {
"in_doaj": {"coerce": "bool"},
"ticked": {"coerce": "bool"},
"current_application": {"coerce": "unicode"}
},
"lists": {
"related_applications": {"contains": "object"}
},
"structs": {
"related_applications": {
"fields": {
"application_id": {"coerce": "unicode"},
"date_accepted": {"coerce": "utcdatetime"},
"status": {"coerce": "unicode"}
}
},
"contact": {
"name": {"coerce": "unicode"},
"email": {"coerce": "unicode"}
}
}
},
"index": {
"fields": {
"publisher_ac": {"coerce": "unicode"},
"institution_ac": {"coerce": "unicode"}
}
}
}
}
class ContinuationException(Exception):
pass
class JournalLikeObject(SeamlessMixin, DomainObject):
@classmethod
def find_by_issn(cls, issns, in_doaj=None, max=10):
if not isinstance(issns, list):
issns = [issns]
q = JournalQuery()
q.find_by_issn(issns, in_doaj=in_doaj, max=max)
result = cls.query(q=q.query)
# create an array of objects, using cls rather than Journal, which means subclasses can use it too
records = [cls(**r.get("_source")) for r in result.get("hits", {}).get("hits", [])]
return records
@classmethod
def find_by_issn_exact(cls, issns, in_doaj=None, max=2):
"""
Finds journal that matches given issns exactly - if no data problems should always be only 1
"""
if not isinstance(issns, list):
issns = [issns]
if len(issns) > 2:
return []
q = JournalQuery()
q.find_by_issn_exact(issns, in_doaj=in_doaj, max=max)
result = cls.query(q=q.query)
# create an array of objects, using cls rather than Journal, which means subclasses can use it too
records = [cls(**r.get("_source")) for r in result.get("hits", {}).get("hits", [])]
return records
@classmethod
def issns_by_owner(cls, owner, in_doaj=None):
q = IssnQuery(owner, in_doaj=in_doaj)
res = cls.query(q=q.query())
issns = [term.get("key") for term in res.get("aggregations", {}).get("issns", {}).get("buckets", [])]
return issns
@classmethod
def get_by_owner(cls, owner):
q = OwnerQuery(owner)
res = cls.query(q=q.query())
# get_by_owner() in application.py predates this, but I've made it an override because it does application stuff
records = [cls(**r.get("_source")) for r in res.get("hits", {}).get("hits", [])]
return records
@classmethod
def issns_by_query(cls, query):
issns = []
for j in cls.iterate(query):
issns += j.known_issns()
return issns
@classmethod
def find_by_journal_url(cls, url, in_doaj=None, max=10):
q = JournalURLQuery(url, in_doaj, max)
result = cls.query(q=q.query())
# create an array of objects, using cls rather than Journal, which means subclasses can use it too
records = [cls(**r.get("_source")) for r in result.get("hits", {}).get("hits", [])]
return records
@classmethod
def recent(cls, max=10):
q = RecentJournalsQuery(max)
result = cls.query(q=q.query())
# create an array of objects, using cls rather than Journal, which means subclasses can use it too
records = [cls(**r.get("_source")) for r in result.get("hits", {}).get("hits", [])]
return records
############################################
## base property methods
@property
def data(self):
return self.__seamless__.data
@property
def has_apc(self):
return self.__seamless__.get_single("bibjson.apc.has_apc")
@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 last_updated_since(self, days=0):
return self.last_updated_timestamp > (dates.now() - timedelta(days=days))
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")
def last_manually_updated_since(self, days=0):
return self.last_manual_update_timestamp > (datetime.utcnow() - timedelta(days=days))
@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)
def has_oa_start_date(self):
return self.__seamless__.get_single("bibjson.oa_start", default=False)
@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 owner_account(self):
if self.owner:
return Account.pull(self.owner)
return None
@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')
@property
def contact(self):
return self.__seamless__.get_single("admin.contact")
@property
def contact_name(self):
return self.__seamless__.get_single("admin.contact.name")
@contact_name.setter
def contact_name(self, name):
self.__seamless__.set_with_struct("admin.contact.name", name)
@property
def contact_email(self):
return self.__seamless__.get_single("admin.contact.email")
@contact_email.setter
def contact_email(self, email):
self.__seamless__.set_with_struct("admin.contact.email", email)
def set_contact(self, name, email):
self.contact_name = name
self.contact_email = email
def remove_contact(self):
self.__seamless__.delete("admin.contact")
def add_note(self, note, date=None, id=None, author_id=None):
if not date:
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 not id:
obj["id"] = uuid.uuid4()
self.__seamless__.add_to_list_with_struct("admin.notes", obj)
def add_note_by_dict(self, note):
return self.add_note(note=note.get("note"), date=note.get("date"),
id=note.get("id"), author_id=note.get("author_id"))
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):
"""Orders notes by newest first"""
notes = self.notes
clusters = {}
for note in notes:
if "date" not in note:
note["date"] = DEFAULT_TIMESTAMP_VAL # this really means something is broken with note date setting, which needs to be fixed
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)
######################################################
## DEPRECATED METHODS
def known_issns(self):
"""
DEPRECATED
all issns this journal is known by
This used to mean "all issns the journal has ever been known by", but that definition has changed since
continuations have been separated from the single journal object model.
Now this is just a proxy for self.bibjson().issns()
"""
return self.bibjson().issns()
def get_latest_contact_name(self):
return self.contact_name
def get_latest_contact_email(self):
return self.contact_email
def add_contact(self, name, email):
self.set_contact(name, email)
def remove_contacts(self):
self.remove_contact()
######################################################
## internal utility methods
def _generate_index(self):
# the index fields we are going to generate
titles = []
subjects = []
schema_subjects = []
schema_codes = []
schema_codes_tree = []
classification = []
langs = []
country = None
license = []
publisher = []
has_seal = None
classification_paths = []
unpunctitle = None
asciiunpunctitle = None
continued = "No"
has_editor_group = "No"
has_editor = "No"
# the places we're going to get those fields from
cbib = self.bibjson()
# get the title out of the current bibjson
if cbib.title is not None:
titles.append(cbib.title)
if cbib.alternative_title:
titles.append(cbib.alternative_title)
# get the subjects and concatenate them with their schemes from the current bibjson
for subs in cbib.subject:
scheme = subs.get("scheme")
term = subs.get("term")
if term:
subjects.append(term)
schema_subjects.append(scheme + ":" + term)
classification.append(term)
if "code" in subs:
schema_codes.append(scheme + ":" + subs.get("code"))
# now expand the classification to hold all its parent terms too
additional = []
for c in classification:
tp = cbib.term_path(c)
if tp is not None:
additional += tp
classification += additional
# add the keywords to the non-schema subjects (but not the classification)
subjects += cbib.keywords
# get the bibjson object to convert the languages to the english form
langs = cbib.language_name()
# get the english name of the country
country = cbib.country_name()
# get the type of the licenses
for l in cbib.licences:
license.append(l.get("type"))
# deduplicate the lists
titles = list(set(titles))
subjects = list(set(subjects))
schema_subjects = list(set(schema_subjects))
classification = list(set(classification))
license = list(set(license))
schema_codes = list(set(schema_codes))
# determine if the seal is applied
has_seal = "Yes" if self.has_seal() else "No"
# get the full classification paths for the subjects
classification_paths = cbib.lcc_paths()
schema_codes_tree = cbib.lcc_codes_full_list()
# create an unpunctitle
if cbib.title is not None:
throwlist = string.punctuation + '\n\t'
unpunctitle = "".join(c for c in cbib.title if c not in throwlist).strip()
try:
asciiunpunctitle = unidecode(unpunctitle)
except:
asciiunpunctitle = unpunctitle
# record if this journal object is a continuation
if len(cbib.replaces) > 0 or len(cbib.is_replaced_by) > 0:
continued = "Yes"
if self.editor_group is not None:
has_editor_group = "Yes"
if self.editor is not None:
has_editor = "Yes"
# build the index part of the object
index = {}
if country is not None:
index["country"] = country
if has_seal:
index["has_seal"] = has_seal
if unpunctitle is not None:
index["unpunctitle"] = unpunctitle
if asciiunpunctitle is not None:
index["asciiunpunctitle"] = asciiunpunctitle
index["continued"] = continued
index["has_editor_group"] = has_editor_group
index["has_editor"] = has_editor
index["issn"] = cbib.issns()
if len(titles) > 0:
index["title"] = titles
if len(subjects) > 0:
index["subject"] = subjects
if len(schema_subjects) > 0:
index["schema_subject"] = schema_subjects
if len(classification) > 0:
index["classification"] = classification
if len(langs) > 0:
index["language"] = langs
if len(license) > 0:
index["license"] = license
if len(classification_paths) > 0:
index["classification_paths"] = classification_paths
if len(schema_codes) > 0:
index["schema_code"] = schema_codes
if len(schema_codes_tree) > 0:
index["schema_codes_tree"] = schema_codes_tree
self.__seamless__.set_with_struct("index", index)
class Journal(JournalLikeObject):
__type__ = "journal"
__SEAMLESS_STRUCT__ = [
shared_structs.JOURNAL_BIBJSON,
shared_structs.SHARED_JOURNAL_LIKE,
JOURNAL_STRUCT
]
__SEAMLESS_COERCE__ = COERCE_MAP
def __init__(self, **kwargs):
# FIXME: hack, to deal with ES integration layer being improperly abstracted
if "_source" in kwargs:
kwargs = kwargs["_source"]
# FIXME: I have taken this out for the moment, as I'm not sure it's what we should be doing
#if kwargs:
# self.add_autogenerated_fields(**kwargs)
super(Journal, self).__init__(raw=kwargs)
@classmethod
def add_autogenerated_fields(cls, **kwargs):
bib = kwargs["bibjson"]
if "apc" in bib and bib["apc"] != '':
bib["apc"]["has_apc"] = len(bib["apc"]["max"]) != 0
else:
bib["apc"] = {"has_apc": False}
if "deposit_policy" in bib and bib["deposit_policy"] != []:
bib["deposit_policy"]["has_policy"] = True
else:
##change made in https://github.com/DOAJ/doaj/commit/e507123f423fe16fd270744055da0129e2b32005
bib["deposit_policy"] = {"has_policy": False}
if "other_charges" in bib and bib["other_charges"] != '':
bib["other_charges"]["has_other_charges"] = bib["other_charges"]["url"] is not None
else:
bib["other_charges"] = {"has_other_charges": False}
if "copyright" in bib and bib["copyright"]["url"] != '':
bib["copyright"]["author_retains"] = bib["copyright"]["url"] is not None
else:
bib["copyright"] = {"author_retains": False}
if "pid_scheme" in bib and bib["pid_scheme"] != '':
bib["pid_scheme"]["has_pid_scheme"] = len(bib["pid_scheme"]["scheme"]) != 0
else:
bib["pid_scheme"] = {"has_pid_scheme": False}
if "preservation" in bib and bib["preservation"] != '':
bib["preservation"]["has_preservation"] = (len(bib["preservation"]) != 0 or
bib["national_library"] is not None)
else:
bib["preservation"] = {"has_preservation": True}
#####################################################
## Journal-specific data access methods
@classmethod
def all_in_doaj(cls, page_size=5000):
q = JournalQuery()
return cls.iterate(q.all_in_doaj(), page_size=page_size, wrap=True, keepalive='5m')
@classmethod
def find_by_publisher(cls, publisher, exact=True):
q = PublisherQuery(publisher, exact)
result = cls.query(q=q.query())
records = [Journal(**r.get("_source")) for r in result.get("hits", {}).get("hits", [])]
return records
@classmethod
def find_by_title(cls, title):
q = TitleQuery(title)
result = cls.query(q=q.query())
records = [Journal(**r.get("_source")) for r in result.get("hits", {}).get("hits", [])]
return records
@classmethod
def delete_selected(cls, query, articles=False, snapshot_journals=True, snapshot_articles=True):
if articles:
# list the issns of all the journals
issns = cls.issns_by_query(query)
# issue a delete request over all the articles by those issns
from portality.models import Article
Article.delete_by_issns(issns, snapshot=snapshot_articles)
# snapshot the journal record
if snapshot_journals:
js = cls.iterate(query, page_size=1000)
for j in js:
j.snapshot()
# finally issue a delete request against the journals
cls.delete_by_query(query)
@classmethod
def add_mapping_extensions(cls, default_mappings: dict):
default_mappings_copy = deepcopy(default_mappings)
mapping_extensions = app.config.get("DATAOBJ_TO_MAPPING_COPY_TO_EXTENSIONS")
for key, value in mapping_extensions.items():
if key in default_mappings_copy:
default_mappings_copy[key] = {**default_mappings_copy[key], **value}
return default_mappings_copy
def all_articles(self):
from portality.models import Article
return Article.find_by_issns(self.known_issns())
def article_stats(self):
from portality.models import Article
q = ArticleStatsQuery(self.known_issns())
data = Article.query(q=q.query())
hits = data.get("hits", {})
total = hits.get("total", {}).get('value', 0)
latest = None
if total > 0:
latest = hits.get("hits", [])[0].get("_source").get("created_date")
return {
"total": total,
"latest": latest
}
def mappings(self):
return es_data_mapping.create_mapping(self.__seamless_struct__.raw, MAPPING_OPTS)
############################################
## base property methods
@property
def toc_id(self):
id_ = self.bibjson().get_preferred_issn()
if not id_:
id_ = self.id
return id_
@property
def last_update_request(self):
related = self.related_applications
if len(related) == 0:
return None
sorted(related, key=lambda x: x.get("date_accepted", DEFAULT_TIMESTAMP_VAL))
return related[0].get("date_accepted", DEFAULT_TIMESTAMP_VAL)
############################################################
## revision history methods
def snapshot(self):
from portality.models import JournalHistory
snap = deepcopy(self.data)
if "id" in snap:
snap["about"] = snap["id"]
del snap["id"]
if "index" in snap:
del snap["index"]
if "last_updated" in snap:
del snap["last_updated"]
if "created_date" in snap:
del snap["created_date"]
hist = JournalHistory(**snap)
hist.save()
#######################################################################
## Conversion methods
def make_continuation(self, type, eissn=None, pissn=None, title=None):
# check that the type is one we know. Must be either 'replaces' or 'is_replaced_by'
if type not in ["replaces", "is_replaced_by"]:
raise ContinuationException("type must be one of 'replaces' or 'is_replaced_by'")
if eissn is None and pissn is None:
raise ContinuationException("You must create a continuation with at least one issn")
# take a copy of the raw data for this journal, and the issns for this journal
raw_cont = deepcopy(self.data)
bibjson = self.bibjson()
issns = bibjson.issns()
cissns = []
# make a new instance of the journal - this will be our continuation
del raw_cont["id"]
del raw_cont["created_date"]
del raw_cont["last_updated"]
j = Journal(**raw_cont)
# ensure that the journal is NOT in doaj. That will be for the admin to decide
j.set_in_doaj(False)
# get a copy of the continuation's bibjson, then remove the existing issns
cbj = j.bibjson()
del cbj.eissn
del cbj.pissn
# also remove any existing continuation information
del cbj.replaces
del cbj.is_replaced_by
del cbj.discontinued_date
# now write the new identifiers
if eissn is not None and eissn != "":
cissns.append(eissn)
cbj.eissn = eissn
if pissn is not None and pissn != "":
cissns.append(pissn)
cbj.pissn = pissn
# update the title
if title is not None:
cbj.title = title
# now add the issns of the original journal in the appropriate field
#
# This is a bit confusing - because we're asking this of a Journal object, the relationship type we're asking
# for relates to this journal, not to the continuation we are creating. This means that when setting the
# new continuations properties, we have to do the opposite to what we do to the journal's properties
#
# "replaces" means that the current journal replaces the new continuation
if type == "replaces":
bibjson.replaces = cissns
cbj.is_replaced_by = issns
# "is_replaced_by" means that the current journal is replaced by the new continuation
elif type == "is_replaced_by":
bibjson.is_replaced_by = cissns
cbj.replaces = issns
# save this journal
self.save()
# save the continuation, and return a copy to the caller
j.save()
return j
####################################################
## admin data methods
def is_in_doaj(self):
return self.__seamless__.get_single("admin.in_doaj", default=False)
def set_in_doaj(self, value):
self.__seamless__.set_with_struct("admin.in_doaj", value)
def is_ticked(self):
return self.__seamless__.get_single("admin.ticked", default=False)
def set_ticked(self, ticked):
self.__seamless__.set_with_struct("admin.ticked", ticked)
@property
def current_application(self):
return self.__seamless__.get_single("admin.current_application")
def set_current_application(self, application_id):
self.__seamless__.set_with_struct("admin.current_application", application_id)
def remove_current_application(self):
self.__seamless__.delete("admin.current_application")
@property
def related_applications(self):
return self.__seamless__.get_list("admin.related_applications")
def add_related_application(self, application_id, date_accepted=None, status=None):
obj = {"application_id": application_id}
self.__seamless__.delete_from_list("admin.related_applications", matchsub=obj)
if date_accepted is not None:
obj["date_accepted"] = date_accepted
if status is not None:
obj["status"] = status
self.__seamless__.add_to_list_with_struct("admin.related_applications", obj)
def set_related_applications(self, related_applications_records):
self.__seamless__.set_with_struct("admin.related_applications", related_applications_records)
def remove_related_applications(self):
self.__seamless__.delete("admin.related_applications")
def remove_related_application(self, application_id):
self.set_related_applications([r for r in self.related_applications if r.get("application_id") != application_id])
def related_application_record(self, application_id):
for record in self.related_applications:
if record.get("application_id") == application_id:
return record
return None
def latest_related_application_id(self):
related = self.related_applications
if len(related) == 0:
return None
if len(related) == 1:
return related[0].get("application_id")
sorted(related, key=lambda x: x.get("date_accepted", DEFAULT_TIMESTAMP_VAL))
return related[0].get("application_id")
########################################################################
## Functions for handling continuations
def get_future_continuations(self):
irb = self.bibjson().is_replaced_by
q = ContinuationQuery(irb)
journals = self.q2obj(q=q.query())
subjournals = []
for j in journals:
subjournals += j.get_future_continuations()
future = journals + subjournals
return future
def get_past_continuations(self):
replaces = self.bibjson().replaces
q = ContinuationQuery(replaces)
journals = self.q2obj(q=q.query())
subjournals = []
for j in journals:
subjournals += j.get_past_continuations()
past = journals + subjournals
return past
#######################################################################
#####################################################
## operations we can do to the journal
def calculate_tick(self):
created_date = self.created_date
last_update_request = self.last_update_request
tick_threshold = app.config.get("TICK_THRESHOLD", '2014-03-19T00:00:00Z')
threshold = dates.parse(tick_threshold)
if created_date is None: # don't worry about the last_update_request date - you can't update unless you've been created!
# we haven't even saved the record yet. All we need to do is check that the tick
# threshold is in the past (which I suppose theoretically it could not be), then
# set it
if dates.now() >= threshold:
self.set_ticked(True)
else:
self.set_ticked(False)
return
# otherwise, this is an existing record, and we just need to update it
# convert the strings to datetime objects
created = dates.parse(created_date)
lud = None
if last_update_request is not None:
lud = dates.parse(last_update_request)
if created >= threshold and self.is_in_doaj():
self.set_ticked(True)
return
if lud is not None and lud >= threshold and self.is_in_doaj():
self.set_ticked(True)
return
self.set_ticked(False)
def propagate_in_doaj_status_to_articles(self):
for article in self.all_articles():
article.set_in_doaj(self.is_in_doaj())
article.save()
def prep(self, is_update=True):
self._ensure_in_doaj()
self.calculate_tick()
self._generate_index()
self._calculate_has_apc()
self._generate_autocompletes()
if is_update:
self.set_last_updated()
def save(self, snapshot=True, sync_owner=True, **kwargs):
self.prep()
self.verify_against_struct()
if sync_owner:
self._sync_owner_to_application()
res = super(Journal, self).save(**kwargs)
if snapshot:
self.snapshot()
return res
######################################################
## internal utility methods
def _generate_autocompletes(self):
bj = self.bibjson()
publisher = bj.publisher
institution = bj.institution
if publisher is not None:
self.__seamless__.set_with_struct("index.publisher_ac", publisher.lower())
if institution is not None:
self.__seamless__.set_with_struct("index.institution_ac", institution.lower())
def _ensure_in_doaj(self):
if self.__seamless__.get_single("admin.in_doaj", default=None) is None:
self.set_in_doaj(False)
def _sync_owner_to_application(self):
if self.current_application is None:
return
from portality.models.v2.application import Application
ca = Application.pull(self.current_application)
if ca is not None and ca.owner != self.owner:
ca.set_owner(self.owner)
ca.save(sync_owner=False)
def _calculate_has_apc(self):
# work out of the journal has an apc
has_apc = "No Information"
apc_present = self.bibjson().has_apc
if apc_present:
has_apc = "Yes"
elif self.is_ticked(): # Because if an item is not ticked we want to say "No Information"
has_apc = "No"
self.__seamless__.set_with_struct("index.has_apc", has_apc)
MAPPING_OPTS = {
"dynamic": None,
"coerces": Journal.add_mapping_extensions(app.config["DATAOBJ_TO_MAPPING_DEFAULTS"]),
"exceptions": app.config["ADMIN_NOTES_SEARCH_MAPPING"],
"additional_mappings": app.config["ADMIN_NOTES_INDEX_ONLY_FIELDS"]
}
########################################################
## Data Access Queries
class JournalQuery(object):
"""
wrapper around the kinds of queries we want to do against the journal type
"""
issn_query = {
"track_total_hits": True,
"query": {
"bool": {
"must": [
{
"terms": {"index.issn.exact": "<issn>"}
}
]
}
}
}
must_query = {
"track_total_hits": True,
"query": {
"bool": {
"must": [
]
}
}
}
all_doaj = {
"track_total_hits": True,
"query": {
"bool": {
"must": [
{"term": {"admin.in_doaj": True}}
]
}
}
}
_minified_fields = ["id", "bibjson.title", "last_updated"]
def __init__(self, minified=False, sort_by_title=False):
self.query = None
self.minified = minified
self.sort_by_title = sort_by_title
def find_by_issn(self, issns, in_doaj=None, max=10):
self.query = deepcopy(self.issn_query)
self.query["query"]["bool"]["must"][0]["terms"]["index.issn.exact"] = issns
if in_doaj is not None:
self.query["query"]["bool"]["must"].append({"term": {"admin.in_doaj": in_doaj}})
self.query["size"] = max
def find_by_issn_exact(self, issns, in_doaj=None, max=10):
self.query = deepcopy(self.must_query)
for issn in issns:
self.query["query"]["bool"]["must"].append({"term": {"index.issn.exact": issn}})
if in_doaj is not None:
self.query["query"]["bool"]["must"].append({"term": {"admin.in_doaj": in_doaj}})
self.query["size"] = max
def all_in_doaj(self):
q = deepcopy(self.all_doaj)
if self.minified:
q["fields"] = self._minified_fields
if self.sort_by_title:
q["sort"] = [{"bibjson.title.exact": {"order": "asc"}}]
return q
class JournalURLQuery(object):
def __init__(self, url, in_doaj=None, max=10):
self.url = url
self.in_doaj = in_doaj
self.max = max