Coverage for portality/bll/services/journal.py: 85%
127 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 logging
3from portality.lib.argvalidate import argvalidate
4from portality.lib import dates
5from portality import models, constants
6from portality.bll import exceptions
7from portality.core import app
8from portality import lock
9from portality.bll.doaj import DOAJ
10from portality.store import StoreFactory, prune_container
11from portality.crosswalks.journal_questions import Journal2QuestionXwalk
13from datetime import datetime
14import re, csv, random, string
17class JournalService(object):
18 """
19 ~~Journal:Service~~
20 """
21 def journal_2_application(self, journal, account=None, keep_editors=False):
22 """
23 Function to convert a given journal into an application object.
25 Provide the journal, and it will be converted
26 in-memory to the application object (currently a Suggestion). The new application
27 WILL NOT be saved by this method.
29 If an account is provided, this will validate that the account holder is
30 allowed to make this conversion
32 :param journal: a journal to convert
33 :param account: an account doing the action - optional, if specified the application will only be created if the account is allowed to
34 :return: Suggestion object
35 """
37 # first validate the incoming arguments to ensure that we've got the right thing
38 argvalidate("journal_2_application", [
39 {"arg": journal, "instance" : models.Journal, "allow_none" : False, "arg_name" : "journal"},
40 {"arg" : account, "instance" : models.Account, "arg_name" : "account"}
41 ], exceptions.ArgumentException)
43 if app.logger.isEnabledFor(logging.DEBUG): app.logger.debug("Entering journal_2_application")
45 # ~~-> AuthNZ:Service~~
46 authService = DOAJ.authorisationService()
48 # if an account is specified, check that it is allowed to perform this action
49 if account is not None:
50 try:
51 authService.can_create_update_request(account, journal) # throws exception if not allowed
52 except exceptions.AuthoriseException as e:
53 msg = "Account {x} is not permitted to create an update request on journal {y}".format(x=account.id, y=journal.id)
54 app.logger.info(msg)
55 e.args += (msg,)
56 raise
58 # copy all the relevant information from the journal to the application
59 bj = journal.bibjson()
60 notes = journal.notes
62 application = models.Suggestion() # ~~-> Application:Model~~
63 application.set_application_status(constants.APPLICATION_STATUS_UPDATE_REQUEST)
64 application.set_current_journal(journal.id)
65 if keep_editors is True:
66 if journal.editor is not None:
67 application.set_editor(journal.editor)
68 if journal.editor_group is not None:
69 application.set_editor_group(journal.editor_group)
70 for n in notes:
71 # NOTE: we keep the same id for notes between journal and application, since ids only matter within
72 # the scope of a record there are no id clashes, and at the same time it may be useful in future to
73 # check the origin of some journal notes by comparing ids to application notes.
74 application.add_note(n.get("note"), n.get("date"), n.get("id"))
75 application.set_owner(journal.owner)
76 application.set_seal(journal.has_seal())
77 application.set_bibjson(bj)
78 application.date_applied = dates.now()
80 if app.logger.isEnabledFor(logging.DEBUG): app.logger.debug("Completed journal_2_application; return application object")
81 return application
83 def journal(self, journal_id, lock_journal=False, lock_account=None, lock_timeout=None):
84 """
85 Function to retrieve a journal by its id, and to optionally lock the resource
87 May raise a Locked exception, if a lock is requested but can't be obtained.
89 :param journal_id: the id of the journal
90 :param: lock_journal: should we lock the resource on retrieval
91 :param: lock_account: which account is doing the locking? Must be present if lock_journal=True
92 :param: lock_timeout: how long to lock the resource for. May be none, in which case it will default
93 :return: Tuple of (Journal Object, Lock Object)
94 """
95 # first validate the incoming arguments to ensure that we've got the right thing
96 argvalidate("journal", [
97 {"arg": journal_id, "allow_none" : False, "arg_name" : "journal_id"},
98 {"arg": lock_journal, "instance" : bool, "allow_none" : False, "arg_name" : "lock_journal"},
99 {"arg": lock_account, "instance" : models.Account, "allow_none" : True, "arg_name" : "lock_account"},
100 {"arg": lock_timeout, "instance" : int, "allow_none" : True, "arg_name" : "lock_timeout"}
101 ], exceptions.ArgumentException)
103 # retrieve the journal
104 journal = models.Journal.pull(journal_id)
106 # if we've retrieved the journal, and a lock is requested, request it
107 the_lock = None
108 if journal is not None and lock_journal:
109 if lock_account is not None:
110 # ~~->Lock:Feature~~
111 the_lock = lock.lock(constants.LOCK_JOURNAL, journal_id, lock_account.id, lock_timeout)
112 else:
113 raise exceptions.ArgumentException("If you specify lock_journal on journal retrieval, you must also provide lock_account")
115 return journal, the_lock
117 def csv(self, prune=True):
118 """
119 Generate the Journal CSV
121 ~~-> JournalCSV:Feature~~
123 :param set_cache: whether to update the cache
124 :param out_dir: the directory to output the file to. If set_cache is True, this argument will be overridden by the cache container
125 :return: Tuple of (attachment_name, URL)
126 """
127 # first validate the incoming arguments to ensure that we've got the right thing
128 argvalidate("csv", [
129 {"arg": prune, "allow_none" : False, "arg_name" : "prune"}
130 ], exceptions.ArgumentException)
132 # ~~->FileStoreTemp:Feature~~
133 filename = 'journalcsv__doaj_' + datetime.strftime(datetime.utcnow(), '%Y%m%d_%H%M') + '_utf8.csv'
134 container_id = app.config.get("STORE_CACHE_CONTAINER")
135 tmpStore = StoreFactory.tmp()
136 out = tmpStore.path(container_id, filename, create_container=True, must_exist=False)
138 with open(out, 'w', encoding='utf-8') as csvfile:
139 self._make_journals_csv(csvfile)
141 # ~~->FileStore:Feature~~
142 mainStore = StoreFactory.get("cache")
143 try:
144 mainStore.store(container_id, filename, source_path=out)
145 url = mainStore.url(container_id, filename)
146 finally:
147 tmpStore.delete_file(container_id, filename) # don't delete the container, just in case someone else is writing to it
149 action_register = []
150 if prune:
151 def sort(filelist):
152 rx = "journalcsv__doaj_(.+?)_utf8.csv"
153 return sorted(filelist, key=lambda x: datetime.strptime(re.match(rx, x).groups(1)[0], '%Y%m%d_%H%M'), reverse=True)
155 def _filter(f_name):
156 return f_name.startswith("journalcsv__")
157 action_register = prune_container(mainStore, container_id, sort, filter=_filter, keep=2)
159 # update the ES record to point to the new file
160 # ~~-> Cache:Model~~
161 models.Cache.cache_csv(url)
162 return url, action_register
164 def admin_csv(self, file_path, account_sub_length=8, obscure_accounts=True):
165 """
166 ~~AdminJournalCSV:Feature->JournalCSV:Feature~~
168 :param file_path:
169 :param account_sub_length:
170 :param obscure_accounts:
171 :return:
172 """
173 # create a closure for substituting owners for consistently used random strings
174 unmap = {}
176 def usernames(j):
177 o = j.owner
178 if obscure_accounts:
179 if o in unmap:
180 sub = unmap[o]
181 else:
182 sub = "".join(random.choice(string.ascii_lowercase + string.ascii_uppercase + string.digits) for i in range(account_sub_length))
183 unmap[o] = sub
184 return [("Owner", sub)]
185 else:
186 return [("Owner", o)]
188 with open(file_path, "w", encoding="utf-8") as f:
189 self._make_journals_csv(f, [usernames])
191 @staticmethod
192 def _make_journals_csv(file_object, additional_columns=None):
193 """
194 Make a CSV file of information for all journals.
195 :param file_object: a utf8 encoded file object.
196 """
197 YES_NO = {True: 'Yes', False: 'No', None: '', '': ''}
199 def _get_doaj_meta_kvs(journal):
200 """
201 Get key, value pairs for some meta information we want from the journal object
202 :param journal: a models.Journal
203 :return: a list of (key, value) tuples for our metadata
204 """
205 kvs = [
206 ("Subjects", ' | '.join(journal.bibjson().lcc_paths())),
207 ("DOAJ Seal", YES_NO.get(journal.has_seal(), "")),
208 # ("Tick: Accepted after March 2014", YES_NO.get(journal.is_ticked(), "")),
209 ("Added on Date", journal.created_date),
210 ("Last updated Date", journal.last_manual_update)
211 ]
212 return kvs
214 def _get_doaj_toc_kv(journal):
215 return "URL in DOAJ", app.config.get('JOURNAL_TOC_URL_FRAG', 'https://doaj.org/toc/') + journal.id
217 def _get_article_kvs(journal):
218 stats = journal.article_stats()
219 kvs = [
220 ("Number of Article Records", str(stats.get("total"))),
221 ("Most Recent Article Added", stats.get("latest"))
222 ]
223 return kvs
225 # ~~!JournalCSV:Feature->Journal:Model~~
226 cols = {}
227 for j in models.Journal.all_in_doaj(page_size=1000): #Fixme: limited by ES, this may not be sufficient
228 bj = j.bibjson()
229 issn = bj.get_one_identifier(idtype=bj.P_ISSN)
230 if issn is None:
231 issn = bj.get_one_identifier(idtype=bj.E_ISSN)
232 if issn is None:
233 continue
235 # ~~!JournalCSV:Feature->JournalQuestions:Crosswalk~~
236 kvs = Journal2QuestionXwalk.journal2question(j)
237 meta_kvs = _get_doaj_meta_kvs(j)
238 article_kvs = _get_article_kvs(j)
239 additionals = []
240 if additional_columns is not None:
241 for col in additional_columns:
242 additionals += col(j)
243 cols[issn] = kvs + meta_kvs + article_kvs + additionals
245 # Get the toc URL separately from the meta kvs because it needs to be inserted earlier in the CSV
246 # ~~-> ToC:WebRoute~~
247 toc_kv = _get_doaj_toc_kv(j)
248 cols[issn].insert(2, toc_kv)
250 issns = cols.keys()
252 csvwriter = csv.writer(file_object)
253 qs = None
254 for i in sorted(issns):
255 if qs is None:
256 qs = [q for q, _ in cols[i]]
257 csvwriter.writerow(qs)
258 vs = [v for _, v in cols[i]]
259 csvwriter.writerow(vs)