Coverage for portality/view/api_v3.py: 82%
235 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 json
3from flask import Blueprint, jsonify, url_for, request, make_response, render_template, redirect
4from flask_login import current_user
5from flask_swagger import swagger
7from portality.api.current import ApplicationsCrudApi, ArticlesCrudApi, JournalsCrudApi, ApplicationsBulkApi, \
8 ArticlesBulkApi
9from portality.api.current import DiscoveryApi, DiscoveryException
10from portality.api.current import jsonify_models, jsonify_data_object, Api400Error, Api404Error, created, \
11 no_content, bulk_created
12from portality.core import app
13from portality.decorators import api_key_required, api_key_optional, swag, write_required
14from portality.lib import plausible
16blueprint = Blueprint('api_v3', __name__)
18API_VERSION_NUMBER = '3.0.1' # OA start added 2022-03-21
20# Google Analytics category for API events
21GA_CATEGORY = app.config.get('GA_CATEGORY_API', 'API Hit')
22GA_ACTIONS = app.config.get('GA_ACTIONS_API', {})
25@blueprint.route('/')
26def api_v3_root():
27 return redirect(url_for('.api_spec'))
30@blueprint.route('/docs')
31def docs():
32 account_url = None
33 if current_user.is_authenticated:
34 account_url = url_for('account.username', username=current_user.id, _external=True,
35 _scheme=app.config.get('PREFERRED_URL_SCHEME', 'https'))
36 return render_template('api/current/api_docs.html',
37 api_version=API_VERSION_NUMBER,
38 base_url=app.config.get("BASE_API_URL", url_for('.api_v3_root')),
39 contact_us_url=url_for('doaj.contact'),
40 account_url=account_url)
43@blueprint.route('/swagger.json')
44def api_spec():
45 swag = swagger(app)
46 swag['info']['title'] = ""
47 swag['info']['version'] = API_VERSION_NUMBER
49 # Strip out the duplicate versioned route from the swagger so we only show latest as /api/
50 [swag['paths'].pop(p) for p in list(swag['paths'].keys()) if p.startswith('/api/v3/')]
51 return make_response((jsonify(swag), 200, {'Access-Control-Allow-Origin': '*'}))
54# Handle wayward paths by raising an API404Error
55@blueprint.route("/<path:invalid_path>", methods=["POST", "GET", "PUT", "DELETE", "PATCH", "HEAD"]) # leaving out methods should mean all, but tests haven't shown that behaviour.
56def missing_resource(invalid_path):
57 docs_url = app.config.get("BASE_URL", "") + url_for('.docs')
58 spec_url = app.config.get("BASE_URL", "") + url_for('.api_spec')
59 raise Api404Error("No endpoint at {0}. See {1} for valid paths or read the documentation at {2}.".format(invalid_path, spec_url, docs_url))
62@swag(swag_summary='Search your applications <span class="red">[Authenticated, not public]</span>',
63 swag_spec=DiscoveryApi.get_application_swag()) # must be applied after @api_key_(optional|required) decorators. They don't preserve func attributes.
64@blueprint.route("/search/applications/<path:search_query>")
65@api_key_required
66@plausible.pa_event(GA_CATEGORY, action=GA_ACTIONS.get('search_applications', 'Search applications'),
67 record_value_of_which_arg='search_query')
68def search_applications(search_query):
69 # get the values for the 2 other bits of search info: the page number and the page size
70 page = request.values.get("page", 1)
71 psize = request.values.get("pageSize", 10)
72 sort = request.values.get("sort")
74 # check the page is an integer
75 try:
76 page = int(page)
77 except:
78 raise Api400Error("Page number was not an integer")
80 # check the page size is an integer
81 try:
82 psize = int(psize)
83 except:
84 raise Api400Error("Page size was not an integer")
86 try:
87 results = DiscoveryApi.search('application', current_user, search_query, page, psize, sort)
88 except DiscoveryException as e:
89 raise Api400Error(str(e))
91 return jsonify_models(results)
94@swag(swag_summary='Search journals',
95 swag_spec=DiscoveryApi.get_journal_swag()) # must be applied after @api_key_(optional|required) decorators. They don't preserve func attributes.
96@blueprint.route('/search/journals/<path:search_query>')
97@plausible.pa_event(GA_CATEGORY, action=GA_ACTIONS.get('search_journals', 'Search journals'),
98 record_value_of_which_arg='search_query')
99def search_journals(search_query):
100 # get the values for the 2 other bits of search info: the page number and the page size
101 page = request.values.get("page", 1)
102 psize = request.values.get("pageSize", 10)
103 sort = request.values.get("sort")
105 # check the page is an integer
106 try:
107 page = int(page)
108 except:
109 raise Api400Error("Page number was not an integer")
111 # check the page size is an integer
112 try:
113 psize = int(psize)
114 except:
115 raise Api400Error("Page size was not an integer")
117 try:
118 results = DiscoveryApi.search('journal', None, search_query, page, psize, sort)
119 except DiscoveryException as e:
120 raise Api400Error(str(e))
122 return jsonify_models(results)
125@swag(swag_summary='Search articles',
126 swag_spec=DiscoveryApi.get_article_swag()) # must be applied after @api_key_(optional|required) decorators. They don't preserve func attributes.
127@blueprint.route('/search/articles/<path:search_query>')
128@plausible.pa_event(GA_CATEGORY, action=GA_ACTIONS.get('search_articles', 'Search articles'),
129 record_value_of_which_arg='search_query')
130def search_articles(search_query):
131 # get the values for the 2 other bits of search info: the page number and the page size
132 page = request.values.get("page", 1)
133 psize = request.values.get("pageSize", 10)
134 sort = request.values.get("sort")
136 # check the page is an integer
137 try:
138 page = int(page)
139 except:
140 raise Api400Error("Page number was not an integer")
142 # check the page size is an integer
143 try:
144 psize = int(psize)
145 except:
146 raise Api400Error("Page size was not an integer")
148 results = None
149 try:
150 results = DiscoveryApi.search('article', None, search_query, page, psize, sort)
151 except DiscoveryException as e:
152 raise Api400Error(str(e))
154 return jsonify_models(results)
157#########################################
158# Application CRUD API
160@blueprint.route("/applications", methods=["POST"])
161@api_key_required
162@write_required(api=True)
163@swag(swag_summary='Create an application <span class="red">[Authenticated, not public]</span>',
164 swag_spec=ApplicationsCrudApi.create_swag()) # must be applied after @api_key_(optional|required) decorators. They don't preserve func attributes.
165@plausible.pa_event(GA_CATEGORY, action=GA_ACTIONS.get('create_application', 'Create application'))
166def create_application():
167 # get the data from the request
168 try:
169 data = json.loads(request.data.decode("utf-8"))
170 except:
171 raise Api400Error("Supplied data was not valid JSON")
173 # delegate to the API implementation
174 a = ApplicationsCrudApi.create(data, current_user._get_current_object())
176 # respond with a suitable Created response
177 return created(a, url_for("api_v2.retrieve_application", application_id=a.id))
180@blueprint.route("/applications/<application_id>", methods=["GET"])
181@api_key_required
182@swag(swag_summary='Retrieve an application <span class="red">[Authenticated, not public]</span>',
183 swag_spec=ApplicationsCrudApi.retrieve_swag()) # must be applied after @api_key_(optional|required) decorators. They don't preserve func attributes.
184@plausible.pa_event(GA_CATEGORY, action=GA_ACTIONS.get('retrieve_application', 'Retrieve application'),
185 record_value_of_which_arg='application_id')
186def retrieve_application(application_id):
187 a = ApplicationsCrudApi.retrieve(application_id, current_user)
188 return jsonify_models(a)
191@blueprint.route("/applications/<application_id>", methods=["PUT"])
192@api_key_required
193@write_required(api=True)
194@swag(swag_summary='Update an application <span class="red">[Authenticated, not public]</span>',
195 swag_spec=ApplicationsCrudApi.update_swag()) # must be applied after @api_key_(optional|required) decorators. They don't preserve func attributes.
196@plausible.pa_event(GA_CATEGORY, action=GA_ACTIONS.get('update_application', 'Update application'),
197 record_value_of_which_arg='application_id')
198def update_application(application_id):
199 # get the data from the request
200 try:
201 data = json.loads(request.data.decode("utf-8"))
202 except:
203 raise Api400Error("Supplied data was not valid JSON")
205 # delegate to the API implementation
206 ApplicationsCrudApi.update(application_id, data, current_user._get_current_object())
208 # respond with a suitable No Content successful response
209 return no_content()
212@blueprint.route("/applications/<application_id>", methods=["DELETE"])
213@api_key_required
214@write_required(api=True)
215@swag(swag_summary='Delete an application <span class="red">[Authenticated, not public]</span>',
216 swag_spec=ApplicationsCrudApi.delete_swag()) # must be applied after @api_key_(optional|required) decorators. They don't preserve func attributes.
217@plausible.pa_event(GA_CATEGORY, action=GA_ACTIONS.get('delete_application', 'Delete application'),
218 record_value_of_which_arg='application_id')
219def delete_application(application_id):
220 ApplicationsCrudApi.delete(application_id, current_user._get_current_object())
221 return no_content()
224#########################################
225# Article CRUD API
227@blueprint.route("/articles", methods=["POST"])
228@api_key_required
229@write_required(api=True)
230@swag(swag_summary='Create an article <span class="red">[Authenticated, not public]</span>',
231 swag_spec=ArticlesCrudApi.create_swag()) # must be applied after @api_key_(optional|required) decorators. They don't preserve func attributes.
232@plausible.pa_event(GA_CATEGORY, action=GA_ACTIONS.get('create_article', 'Create article'))
233def create_article():
234 # get the data from the request
235 try:
236 data = json.loads(request.data.decode("utf-8"))
237 except:
238 raise Api400Error("Supplied data was not valid JSON")
240 # delegate to the API implementation
241 a = ArticlesCrudApi.create(data, current_user._get_current_object())
243 # respond with a suitable Created response
244 return created(a, url_for("api_v2.retrieve_article", article_id=a.id))
247@blueprint.route("/articles/<article_id>", methods=["GET"])
248@api_key_optional
249@swag(swag_summary='Retrieve an article',
250 swag_spec=ArticlesCrudApi.retrieve_swag()) # must be applied after @api_key_(optional|required) decorators. They don't preserve func attributes.
251@plausible.pa_event(GA_CATEGORY, action=GA_ACTIONS.get('retrieve_article', 'Retrieve article'),
252 record_value_of_which_arg='article_id')
253def retrieve_article(article_id):
254 a = ArticlesCrudApi.retrieve(article_id, current_user)
255 return jsonify_models(a)
258@blueprint.route("/articles/<article_id>", methods=["PUT"])
259@api_key_required
260@write_required(api=True)
261@swag(swag_summary='Update an article <span class="red">[Authenticated, not public]</span>',
262 swag_spec=ArticlesCrudApi.update_swag()) # must be applied after @api_key_(optional|required) decorators. They don't preserve func attributes.
263@plausible.pa_event(GA_CATEGORY, action=GA_ACTIONS.get('update_article', 'Update article'),
264 record_value_of_which_arg='article_id')
265def update_article(article_id):
266 # get the data from the request
267 try:
268 data = json.loads(request.data.decode("utf-8"))
269 except:
270 raise Api400Error("Supplied data was not valid JSON")
272 # delegate to the API implementation
273 ArticlesCrudApi.update(article_id, data, current_user._get_current_object())
275 # respond with a suitable No Content successful response
276 return no_content()
279@blueprint.route("/articles/<article_id>", methods=["DELETE"])
280@api_key_required
281@write_required(api=True)
282@swag(swag_summary='Delete an article <span class="red">[Authenticated, not public]</span>',
283 swag_spec=ArticlesCrudApi.delete_swag()) # must be applied after @api_key_(optional|required) decorators. They don't preserve func attributes.
284@plausible.pa_event(GA_CATEGORY, action=GA_ACTIONS.get('delete_article', 'Delete article'),
285 record_value_of_which_arg='article_id')
286def delete_article(article_id):
287 ArticlesCrudApi.delete(article_id, current_user)
288 return no_content()
291#########################################
292# Journal R API
294@blueprint.route('/journals/<journal_id>', methods=['GET'])
295@api_key_optional
296@swag(swag_summary='Retrieve a journal by ID',
297 swag_spec=JournalsCrudApi.retrieve_swag()) # must be applied after @api_key_(optional|required) decorators. They don't preserve func attributes.
298@plausible.pa_event(GA_CATEGORY, action=GA_ACTIONS.get('retrieve_journal', 'Retrieve journal'),
299 record_value_of_which_arg='journal_id')
300def retrieve_journal(journal_id):
301 return jsonify_data_object(JournalsCrudApi.retrieve(journal_id, current_user))
304#########################################
305# Application Bulk API
307@blueprint.route("/bulk/applications", methods=["POST"])
308@api_key_required
309@write_required(api=True)
310@swag(swag_summary='Create applications in bulk <span class="red">[Authenticated, not public]</span>',
311 swag_spec=ApplicationsBulkApi.create_swag()) # must be applied after @api_key_(optional|required) decorators. They don't preserve func attributes.
312@plausible.pa_event(GA_CATEGORY, action=GA_ACTIONS.get('bulk_application_create', 'Bulk application create'))
313def bulk_application_create():
314 # get the data from the request
315 try:
316 data = json.loads(request.data.decode("utf-8"))
317 except:
318 raise Api400Error("Supplied data was not valid JSON")
320 # delegate to the API implementation
321 ids = ApplicationsBulkApi.create(data, current_user._get_current_object())
323 # get all the locations for the ids
324 inl = []
325 for id in ids:
326 inl.append((id, url_for("api_v2.retrieve_application", application_id=id)))
328 # respond with a suitable Created response
329 return bulk_created(inl)
332@blueprint.route("/bulk/applications", methods=["DELETE"])
333@api_key_required
334@write_required(api=True)
335@swag(swag_summary='Delete applications in bulk <span class="red">[Authenticated, not public]</span>',
336 swag_spec=ApplicationsBulkApi.delete_swag()) # must be applied after @api_key_(optional|required) decorators. They don't preserve func attributes.
337@plausible.pa_event(GA_CATEGORY, action=GA_ACTIONS.get('bulk_application_delete', 'Bulk application delete'))
338def bulk_application_delete():
339 # get the data from the request
340 try:
341 data = json.loads(request.data.decode("utf-8"))
342 except:
343 raise Api400Error("Supplied data was not valid JSON")
345 ApplicationsBulkApi.delete(data, current_user._get_current_object())
347 return no_content()
350#########################################
351# Article Bulk API
353@blueprint.route("/bulk/articles", methods=["POST"])
354@api_key_required
355@write_required(api=True)
356@swag(swag_summary='Bulk article creation <span class="red">[Authenticated, not public]</span>',
357 swag_spec=ArticlesBulkApi.create_swag()) # must be applied after @api_key_(optional|required) decorators. They don't preserve func attributes.
358@plausible.pa_event(GA_CATEGORY, action=GA_ACTIONS.get('bulk_article_create', 'Bulk article create'))
359def bulk_article_create():
360 # get the data from the request
361 try:
362 data = json.loads(request.data.decode("utf-8"))
363 except:
364 raise Api400Error("Supplied data was not valid JSON")
366 # delegate to the API implementation
367 ids = ArticlesBulkApi.create(data, current_user._get_current_object())
369 # get all the locations for the ids
370 inl = []
371 for id in ids:
372 inl.append((id, url_for("api_v2.retrieve_article", article_id=id)))
374 # respond with a suitable Created response
375 return bulk_created(inl)
378@blueprint.route("/bulk/articles", methods=["DELETE"])
379@api_key_required
380@write_required(api=True)
381@swag(swag_summary='Bulk article delete <span class="red">[Authenticated, not public]</span>',
382 swag_spec=ArticlesBulkApi.delete_swag()) # must be applied after @api_key_(optional|required) decorators. They don't preserve func attributes.
383@plausible.pa_event(GA_CATEGORY, action=GA_ACTIONS.get('bulk_article_delete', 'Bulk article delete'))
384def bulk_article_delete():
385 # get the data from the request
386 try:
387 data = json.loads(request.data.decode("utf-8"))
388 except:
389 raise Api400Error("Supplied data was not valid JSON")
391 ArticlesBulkApi.delete(data, current_user._get_current_object())
393 return no_content()