Coverage for portality/bll/services/journal.py: 85%

127 statements  

« prev     ^ index     » next       coverage.py v6.4.2, created at 2022-07-19 18:38 +0100

1import logging 

2 

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 

12 

13from datetime import datetime 

14import re, csv, random, string 

15 

16 

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. 

24 

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. 

28 

29 If an account is provided, this will validate that the account holder is 

30 allowed to make this conversion 

31 

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 """ 

36 

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) 

42 

43 if app.logger.isEnabledFor(logging.DEBUG): app.logger.debug("Entering journal_2_application") 

44 

45 # ~~-> AuthNZ:Service~~ 

46 authService = DOAJ.authorisationService() 

47 

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 

57 

58 # copy all the relevant information from the journal to the application 

59 bj = journal.bibjson() 

60 notes = journal.notes 

61 

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() 

79 

80 if app.logger.isEnabledFor(logging.DEBUG): app.logger.debug("Completed journal_2_application; return application object") 

81 return application 

82 

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 

86 

87 May raise a Locked exception, if a lock is requested but can't be obtained. 

88 

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) 

102 

103 # retrieve the journal 

104 journal = models.Journal.pull(journal_id) 

105 

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") 

114 

115 return journal, the_lock 

116 

117 def csv(self, prune=True): 

118 """ 

119 Generate the Journal CSV 

120 

121 ~~-> JournalCSV:Feature~~ 

122 

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) 

131 

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) 

137 

138 with open(out, 'w', encoding='utf-8') as csvfile: 

139 self._make_journals_csv(csvfile) 

140 

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 

148 

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) 

154 

155 def _filter(f_name): 

156 return f_name.startswith("journalcsv__") 

157 action_register = prune_container(mainStore, container_id, sort, filter=_filter, keep=2) 

158 

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 

163 

164 def admin_csv(self, file_path, account_sub_length=8, obscure_accounts=True): 

165 """ 

166 ~~AdminJournalCSV:Feature->JournalCSV:Feature~~ 

167 

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 = {} 

175 

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)] 

187 

188 with open(file_path, "w", encoding="utf-8") as f: 

189 self._make_journals_csv(f, [usernames]) 

190 

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: '', '': ''} 

198 

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 

213 

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 

216 

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 

224 

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 

234 

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 

244 

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) 

249 

250 issns = cols.keys() 

251 

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) 

260