Coverage for portality / view / api_v3.py: 85%
161 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-04 09:41 +0100
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-04 09:41 +0100
1import json, re
3from flask import Blueprint, url_for, request, render_template, make_response, jsonify, redirect
4from flask_login import current_user
6from portality.api.current import Api400Error, bulk_created
7from portality.api.current import ApplicationsCrudApi, ArticlesCrudApi, JournalsCrudApi, ApplicationsBulkApi, \
8 ArticlesBulkApi
9from portality.api.current import DiscoveryApi
10from portality.core import app
11from portality.decorators import api_key_required, api_key_optional, swag, write_required
12from portality.lib import plausible
13from portality.view import api_v4
14from flask_swagger import swagger
15from portality.ui import templates
17blueprint = Blueprint('api_v3', __name__)
19API_VERSION_NUMBER = '3.0.1' # OA start added 2022-03-21
21# Google Analytics category for API events
22ANALYTICS_CATEGORY = app.config.get('ANALYTICS_CATEGORY_API', 'API Hit')
23ANALYTICS_ACTIONS = app.config.get('ANALYTICS_ACTIONS_API', {})
25API_UNSUPPORTED_ERROR = "Version 3 is no longer supported."
27@blueprint.route('/')
28def api_root():
29 return redirect(url_for('.api_spec'))
32@blueprint.route('/docs')
33def docs():
34 account_url = None
35 if current_user.is_authenticated:
36 account_url = url_for('account.username', username=current_user.id, _external=True,
37 _scheme=app.config.get('PREFERRED_URL_SCHEME', 'https'))
39 major_version = app.config.get("CURRENT_API_MAJOR_VERSION")
40 is_current = False
41 if major_version is not None:
42 is_current = API_VERSION_NUMBER.startswith(major_version + ".")
43 base_url = app.config.get("BASE_API_URL")
44 if not base_url.endswith("/"):
45 base_url = base_url + "/"
46 if not is_current:
47 this_major_version = API_VERSION_NUMBER.split(".")[0]
48 base_url = base_url + "v" + this_major_version + "/"
50 return render_template(templates.API_V3_DOCS,
51 api_version=API_VERSION_NUMBER,
52 base_url=base_url,
53 contact_us_url=url_for('doaj.contact'),
54 account_url=account_url)
57@blueprint.route('/swagger.json')
58def api_spec():
59 swag = swagger(app)
60 swag['info']['title'] = ""
61 swag['info']['version'] = API_VERSION_NUMBER
63 major_version = app.config.get("CURRENT_API_MAJOR_VERSION")
64 is_current = False
65 if major_version is not None:
66 is_current = API_VERSION_NUMBER.startswith(major_version + ".")
68 if is_current:
69 # Strip out all the `vN` specific routes, leaving only the "current" /api route displayed
70 [swag['paths'].pop(p) for p in list(swag['paths'].keys()) if re.match(r'/api/v\d+/', p)]
71 else:
72 this_major_version = API_VERSION_NUMBER.split(".")[0]
73 # strip out all the routes that are not for this version
74 [swag['paths'].pop(p) for p in list(swag['paths'].keys()) if
75 not re.match('/api/v' + this_major_version + '/', p)]
76 return make_response((jsonify(swag), 200, {'Access-Control-Allow-Origin': '*'}))
78# Handle wayward paths by raising an API404Error
79@blueprint.route("/<path:invalid_path>", methods=["POST", "GET", "PUT", "DELETE", "PATCH",
80 "HEAD"]) # leaving out methods should mean all, but tests haven't shown that behaviour.
81def missing_resource(invalid_path):
82 return api_v4.missing_resource(invalid_path)
85@swag(swag_summary='Search your applications <span class="red">[Authenticated, not public]</span>',
86 swag_spec=DiscoveryApi.get_application_swag()) # must be applied after @api_key_(optional|required) decorators. They don't preserve func attributes.
87@blueprint.route("/search/applications/<path:search_query>")
88@api_key_required
89@plausible.pa_event(ANALYTICS_CATEGORY, action=ANALYTICS_ACTIONS.get('search_applications', 'Search applications'),
90 record_value_of_which_arg='search_query')
91def search_applications(search_query):
92 return api_v4.search_applications(search_query)
95@swag(swag_summary='Search journals',
96 swag_spec=DiscoveryApi.get_journal_swag()) # must be applied after @api_key_(optional|required) decorators. They don't preserve func attributes.
97@blueprint.route('/search/journals/<path:search_query>')
98@plausible.pa_event(ANALYTICS_CATEGORY, action=ANALYTICS_ACTIONS.get('search_journals', 'Search journals'),
99 record_value_of_which_arg='search_query')
100def search_journals(search_query):
101 return api_v4.search_journals(search_query)
104@swag(swag_summary='Search articles',
105 swag_spec=DiscoveryApi.get_article_swag()) # must be applied after @api_key_(optional|required) decorators. They don't preserve func attributes.
106@blueprint.route('/search/articles/<path:search_query>')
107@plausible.pa_event(ANALYTICS_CATEGORY, action=ANALYTICS_ACTIONS.get('search_articles', 'Search articles'),
108 record_value_of_which_arg='search_query')
109def search_articles(search_query):
110 return api_v4.search_articles(search_query)
113#########################################
114# Application CRUD API
116@blueprint.route("/applications", methods=["POST"])
117@api_key_required
118@write_required(api=True)
119@swag(swag_summary='Create an application <span class="red">[Authenticated, not public]</span>',
120 swag_spec=ApplicationsCrudApi.create_swag()) # must be applied after @api_key_(optional|required) decorators. They don't preserve func attributes.
121@plausible.pa_event(ANALYTICS_CATEGORY, action=ANALYTICS_ACTIONS.get('create_application', 'Create application'))
122def create_application():
123 return api_v4.create_application()
126@blueprint.route("/applications/<application_id>", methods=["GET"])
127@api_key_required
128@swag(swag_summary='Retrieve an application <span class="red">[Authenticated, not public]</span>',
129 swag_spec=ApplicationsCrudApi.retrieve_swag()) # must be applied after @api_key_(optional|required) decorators. They don't preserve func attributes.
130@plausible.pa_event(ANALYTICS_CATEGORY, action=ANALYTICS_ACTIONS.get('retrieve_application', 'Retrieve application'),
131 record_value_of_which_arg='application_id')
132def retrieve_application(application_id):
133 return api_v4.retrieve_application(application_id)
136@blueprint.route("/applications/<application_id>", methods=["PUT"])
137@api_key_required
138@write_required(api=True)
139@swag(swag_summary='Update an application <span class="red">[Authenticated, not public]</span>',
140 swag_spec=ApplicationsCrudApi.update_swag()) # must be applied after @api_key_(optional|required) decorators. They don't preserve func attributes.
141@plausible.pa_event(ANALYTICS_CATEGORY, action=ANALYTICS_ACTIONS.get('update_application', 'Update application'),
142 record_value_of_which_arg='application_id')
143def update_application(application_id):
144 return api_v4.update_application(application_id)
147@blueprint.route("/applications/<application_id>", methods=["DELETE"])
148@api_key_required
149@write_required(api=True)
150@swag(swag_summary='Delete an application <span class="red">[Authenticated, not public]</span>',
151 swag_spec=ApplicationsCrudApi.delete_swag()) # must be applied after @api_key_(optional|required) decorators. They don't preserve func attributes.
152@plausible.pa_event(ANALYTICS_CATEGORY, action=ANALYTICS_ACTIONS.get('delete_application', 'Delete application'),
153 record_value_of_which_arg='application_id')
154def delete_application(application_id):
155 return api_v4.delete_application(application_id)
158#########################################
159# Article CRUD API
161@blueprint.route("/articles", methods=["POST"])
162@api_key_required
163@write_required(api=True)
164@swag(swag_summary='Create an article <span class="red">[Authenticated, not public]</span>',
165 swag_spec=ArticlesCrudApi.create_swag()) # must be applied after @api_key_(optional|required) decorators. They don't preserve func attributes.
166@plausible.pa_event(ANALYTICS_CATEGORY, action=ANALYTICS_ACTIONS.get('create_article', 'Create article'))
167def create_article():
168 return api_v4.create_article()
171@blueprint.route("/articles/<article_id>", methods=["GET"])
172@api_key_optional
173@swag(swag_summary='Retrieve an article',
174 swag_spec=ArticlesCrudApi.retrieve_swag()) # must be applied after @api_key_(optional|required) decorators. They don't preserve func attributes.
175@plausible.pa_event(ANALYTICS_CATEGORY, action=ANALYTICS_ACTIONS.get('retrieve_article', 'Retrieve article'),
176 record_value_of_which_arg='article_id')
177def retrieve_article(article_id):
178 return api_v4.retrieve_article(article_id)
181@blueprint.route("/articles/<article_id>", methods=["PUT"])
182@api_key_required
183@write_required(api=True)
184@swag(swag_summary='Update an article <span class="red">[Authenticated, not public]</span>',
185 swag_spec=ArticlesCrudApi.update_swag()) # must be applied after @api_key_(optional|required) decorators. They don't preserve func attributes.
186@plausible.pa_event(ANALYTICS_CATEGORY, action=ANALYTICS_ACTIONS.get('update_article', 'Update article'),
187 record_value_of_which_arg='article_id')
188def update_article(article_id):
189 return api_v4.update_article(article_id)
192@blueprint.route("/articles/<article_id>", methods=["DELETE"])
193@api_key_required
194@write_required(api=True)
195@swag(swag_summary='Delete an article <span class="red">[Authenticated, not public]</span>',
196 swag_spec=ArticlesCrudApi.delete_swag()) # must be applied after @api_key_(optional|required) decorators. They don't preserve func attributes.
197@plausible.pa_event(ANALYTICS_CATEGORY, action=ANALYTICS_ACTIONS.get('delete_article', 'Delete article'),
198 record_value_of_which_arg='article_id')
199def delete_article(article_id):
200 return api_v4.delete_article(article_id)
203#########################################
204# Journal R API
206@blueprint.route('/journals/<journal_id>', methods=['GET'])
207@api_key_optional
208@swag(swag_summary='Retrieve a journal by ID',
209 swag_spec=JournalsCrudApi.retrieve_swag()) # must be applied after @api_key_(optional|required) decorators. They don't preserve func attributes.
210@plausible.pa_event(ANALYTICS_CATEGORY, action=ANALYTICS_ACTIONS.get('retrieve_journal', 'Retrieve journal'),
211 record_value_of_which_arg='journal_id')
212def retrieve_journal(journal_id):
213 return api_v4.retrieve_journal(journal_id)
216#########################################
217# Application Bulk API
219@blueprint.route("/bulk/applications", methods=["POST"])
220@api_key_required
221@write_required(api=True)
222@swag(swag_summary='Create applications in bulk <span class="red">[Authenticated, not public]</span>',
223 swag_spec=ApplicationsBulkApi.create_swag()) # must be applied after @api_key_(optional|required) decorators. They don't preserve func attributes.
224@plausible.pa_event(ANALYTICS_CATEGORY, action=ANALYTICS_ACTIONS.get('bulk_application_create', 'Bulk application create'))
225def bulk_application_create():
226 return api_v4.bulk_application_create()
229@blueprint.route("/bulk/applications", methods=["DELETE"])
230@api_key_required
231@write_required(api=True)
232@swag(swag_summary='Delete applications in bulk <span class="red">[Authenticated, not public]</span>',
233 swag_spec=ApplicationsBulkApi.delete_swag()) # must be applied after @api_key_(optional|required) decorators. They don't preserve func attributes.
234@plausible.pa_event(ANALYTICS_CATEGORY, action=ANALYTICS_ACTIONS.get('bulk_application_delete', 'Bulk application delete'))
235def bulk_application_delete():
236 return api_v4.bulk_application_delete()
239#########################################
240# Article Bulk API
242def _load_income_articles_json(request):
243 # get the data from the request
244 try:
245 return json.loads(request.data.decode("utf-8"))
246 except:
247 raise Api400Error("Supplied data was not valid JSON")
250@blueprint.route("/bulk/articles", methods=["POST"])
251@api_key_required
252@write_required(api=True)
253@plausible.pa_event(ANALYTICS_CATEGORY, action=ANALYTICS_ACTIONS.get('bulk_article_create', 'Bulk article create'))
254def bulk_article_create():
255 raise Api400Error(API_UNSUPPORTED_ERROR)
258@blueprint.route("/bulk/articles", methods=["DELETE"])
259@api_key_required
260@write_required(api=True)
261@swag(swag_summary='Bulk article delete <span class="red">[Authenticated, not public]</span>',
262 swag_spec=ArticlesBulkApi.delete_swag()) # must be applied after @api_key_(optional|required) decorators. They don't preserve func attributes.
263@plausible.pa_event(ANALYTICS_CATEGORY, action=ANALYTICS_ACTIONS.get('bulk_article_delete', 'Bulk article delete'))
264def bulk_article_delete():
265 return api_v4.bulk_article_delete()