Coverage for portality/api/current/crud/articles.py: 81%
170 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
1# ~~APICrudArticles:Feature->APICrud:Feature~~
2import json
4from portality.api.current.crud.common import CrudApi
5from portality.api.current import Api400Error, Api401Error, Api403Error, Api404Error, Api500Error
6from portality.api.current.data_objects.article import IncomingArticleDO, OutgoingArticleDO
7from portality.core import app
8from portality.lib import dataobj
9from portality import models, app_email
10from portality.bll.doaj import DOAJ
11from portality.bll.exceptions import ArticleMergeConflict, ArticleNotAcceptable, DuplicateArticleException, \
12 IngestException
13from copy import deepcopy
15class ArticlesCrudApi(CrudApi):
17 API_KEY_OPTIONAL = False
19 # ~~->Swagger:Feature~~
20 # ~~->API:Documentation~~
21 SWAG_TAG = 'CRUD Articles'
22 SWAG_ID_PARAM = {
23 "description": "<div class=\"search-query-docs\">DOAJ article ID. E.g. 4cf8b72139a749c88d043129f00e1b07 .</div>",
24 "required": True,
25 "type": "string",
26 "name": "article_id",
27 "in": "path"
28 }
29 SWAG_ARTICLE_BODY_PARAM = {
30 "description": """<div class=\"search-query-docs\">
31 Article JSON that you would like to create or update. The contents should comply with the schema displayed
32 in the <a href=\"/api/docs#CRUD_Articles_get_api_articles_article_id\"> GET (Retrieve) an article route</a>.
33 Explicit documentation for the structure of this data is also <a href="https://doaj.github.io/doaj-docs/master/data_models/IncomingAPIArticle">provided here</a>.
34 Partial updates are not allowed, you have to supply the full JSON.</div>""",
35 "required": True,
36 "schema": {"type" : "string"},
37 "name": "article_json",
38 "in": "body"
39 }
41 @classmethod
42 def __handle_journal_info(cls, am):
43 # handle journal info - first save fields users ARE allowed to update into temporary vars
44 number = am.bibjson().number
45 volume = am.bibjson().volume
46 start_page = am.bibjson().start_page
47 end_page = am.bibjson().end_page
48 am.bibjson().remove_journal_metadata() # then destroy all journal metadata
50 try:
51 am.add_journal_metadata() # overwrite journal part of metadata and in_doaj setting
52 except models.NoJournalException as e:
53 raise Api400Error("No journal found to attach article to. Each article in DOAJ must belong to a journal and the (E)ISSNs provided in the bibjson.identifiers section of this article record do not match any DOAJ journal.")
55 # restore the user's data
56 am.bibjson().number = number
57 am.bibjson().volume = volume
58 am.bibjson().start_page = start_page
59 am.bibjson().end_page = end_page
60 return am
62 @classmethod
63 def create_swag(cls):
64 template = deepcopy(cls.SWAG_TEMPLATE)
65 template['parameters'].append(cls.SWAG_ARTICLE_BODY_PARAM)
66 template['responses']['201'] = cls.R201
67 template['responses']['400'] = cls.R400
68 template['responses']['401'] = cls.R401
69 template['responses']['403'] = cls.R403
70 return cls._build_swag_response(template)
72 @classmethod
73 def create(cls, data, account):
74 # as long as authentication (in the layer above) has been successful, and the account exists, then
75 # we are good to proceed
76 if account is None:
77 raise Api401Error()
79 # convert the data into a suitable article model (raises Api400Error if doesn't conform to struct)
80 am = cls.prep_article(data, account)
82 # ~~-> Article:Service~~
83 articleService = DOAJ.articleService()
84 try:
85 result = articleService.create_article(am, account, add_journal_info=True)
86 except ArticleMergeConflict as e:
87 raise Api400Error(str(e))
88 except ArticleNotAcceptable as e:
89 raise Api400Error(str(e))
90 except DuplicateArticleException as e:
91 raise Api403Error(str(e))
92 except IngestException as e:
93 raise Api400Error(str(e))
96 # Check we are allowed to create an article for this journal
97 if result.get("fail", 0) == 1:
98 raise Api403Error("It is not possible to create an article for this journal. Have you included in the upload an ISSN which is not associated with any journal in your account? ISSNs must match exactly the ISSNs against the journal record.")
100 return am
103 @classmethod
104 def prep_article(cls, data, account):
105 # first thing to do is a structural validation, by instantiating the data object
106 try:
107 ia = IncomingArticleDO(data)
108 except dataobj.DataStructureException as e:
109 raise Api400Error(str(e))
110 except dataobj.ScriptTagFoundException as e:
111 # ~~->Email:ExternalService~~
112 email_data = {"article": data, "account": account.__dict__}
113 jdata = json.dumps(email_data, indent=4)
114 # send warning email about the service tag in article metadata detected
115 try:
116 to = app.config.get('SCRIPT_TAG_DETECTED_EMAIL_RECIPIENTS')
117 fro = app.config.get("SYSTEM_EMAIL_FROM", "helpdesk@doaj.org")
118 subject = app.config.get("SERVICE_NAME", "") + " - script tag detected in application metadata"
119 es_type="article"
120 app_email.send_mail(to=to,
121 fro=fro,
122 subject=subject,
123 template_name="email/script_tag_detected.jinja2",
124 es_type=es_type,
125 data=jdata)
126 except app_email.EmailException:
127 app.logger.exception('Error sending script tag detection email - ' + jdata)
128 raise Api400Error(str(e))
130 # if that works, convert it to an Article object
131 am = ia.to_article_model()
133 # the user may have supplied metadata in the model for id and created_date
134 # and we want to can that data. If this is a truly new article its fine for
135 # us to assign a new id here, and if it's a duplicate, it will get attached
136 # to its duplicate id anyway.
137 am.set_id()
138 am.set_created()
140 # not allowed to set subjects
141 am.bibjson().remove_subjects()
143 # get the journal information set straight
144 am = cls.__handle_journal_info(am)
146 return am
149 @classmethod
150 def retrieve_swag(cls):
151 template = deepcopy(cls.SWAG_TEMPLATE)
152 template['parameters'].append(cls.SWAG_ID_PARAM)
153 template['responses']['200'] = cls.R200
154 template['responses']['200']['schema'] = IncomingArticleDO().struct_to_swag(schema_title='Article schema')
155 template['responses']['401'] = cls.R401
156 template['responses']['404'] = cls.R404
157 template['responses']['500'] = cls.R500
158 return cls._build_swag_response(template, api_key_optional_override=True)
160 @classmethod
161 def retrieve(cls, id, account):
163 # is the article id valid?
164 ar = models.Article.pull(id)
165 if ar is None:
166 raise Api404Error()
168 # at this point we're happy to return the article if it's
169 # meant to be seen by the public
170 if ar.is_in_doaj():
171 try:
172 return OutgoingArticleDO.from_model(ar)
173 except:
174 raise Api500Error()
176 # as long as authentication (in the layer above) has been successful, and the account exists, then
177 # we are good to proceed
178 if account is None or account.is_anonymous:
179 raise Api401Error()
181 # Check we're allowed to retrieve this article
182 articleService = DOAJ.articleService()
183 if not articleService.is_legitimate_owner(ar, account.id):
184 raise Api404Error() # not found for this account
186 # Return the article
187 oa = OutgoingArticleDO.from_model(ar)
188 return oa
190 @classmethod
191 def update_swag(cls):
192 template = deepcopy(cls.SWAG_TEMPLATE)
193 template['parameters'].append(cls.SWAG_ID_PARAM)
194 template['parameters'].append(cls.SWAG_ARTICLE_BODY_PARAM)
195 template['responses']['204'] = cls.R204
196 template['responses']['400'] = cls.R400
197 template['responses']['401'] = cls.R401
198 template['responses']['404'] = cls.R404
199 return cls._build_swag_response(template)
201 @classmethod
202 def update(cls, id, data, account):
203 # as long as authentication (in the layer above) has been successful, and the account exists, then
204 # we are good to proceed
205 if account is None:
206 raise Api401Error()
208 # now see if there's something for us to delete
209 ar = models.Article.pull(id)
210 if ar is None:
211 raise Api404Error()
213 # Check we're allowed to edit this article
214 # ~~-> Article:Service~~
215 articleService = DOAJ.articleService()
216 if not articleService.is_legitimate_owner(ar, account.id):
217 raise Api404Error() # not found for this account
219 # next thing to do is a structural validation of the replacement data, by instantiating the object
220 try:
221 ia = IncomingArticleDO(data)
222 except dataobj.DataStructureException as e:
223 raise Api400Error(str(e))
225 # if that works, convert it to an Article object bringing over everything outside the
226 # incoming article from the original article
228 # we need to ensure that any properties of the existing article that aren't allowed to change
229 # are copied over
230 new_ar = ia.to_article_model(ar)
231 new_ar.set_id(id)
232 new_ar.set_created(ar.created_date)
233 new_ar.bibjson().set_subjects(ar.bibjson().subjects())
235 try:
236 # Article save occurs in the BLL create_article
237 result = articleService.create_article(new_ar, account, add_journal_info=True, update_article_id=id)
238 except ArticleMergeConflict as e:
239 raise Api400Error(str(e))
240 except ArticleNotAcceptable as e:
241 raise Api400Error("; ".join(e.errors))
242 except DuplicateArticleException as e:
243 raise Api403Error(str(e))
245 if result.get("success") == 0:
246 raise Api400Error("Article update failed for unanticipated reason")
248 return new_ar
250 @classmethod
251 def delete_swag(cls):
252 template = deepcopy(cls.SWAG_TEMPLATE)
253 template['parameters'].append(cls.SWAG_ID_PARAM)
254 template['responses']['204'] = cls.R204
255 template['responses']['401'] = cls.R401
256 template['responses']['403'] = cls.R403
257 template['responses']['404'] = cls.R404
258 return cls._build_swag_response(template)
260 @classmethod
261 def delete(cls, id, account, dry_run=False):
262 # as long as authentication (in the layer above) has been successful, and the account exists, then
263 # we are good to proceed
264 if account is None:
265 raise Api401Error()
267 # now see if there's something for us to delete
268 ar = models.Article.pull(id)
269 if ar is None:
270 raise Api404Error()
272 # Check we're allowed to retrieve this article
273 # ~~-> Article:Service~~
274 articleService = DOAJ.articleService()
275 if not articleService.is_legitimate_owner(ar, account.id):
276 raise Api404Error() # not found for this account
278 # issue the delete (no record of the delete required)
279 if not dry_run:
280 ar.delete()