Coverage for portality / view / api_v3.py: 85%

161 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-05-05 00:09 +0100

1import json, re 

2 

3from flask import Blueprint, url_for, request, render_template, make_response, jsonify, redirect 

4from flask_login import current_user 

5 

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 

16 

17blueprint = Blueprint('api_v3', __name__) 

18 

19API_VERSION_NUMBER = '3.0.1' # OA start added 2022-03-21 

20 

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', {}) 

24 

25API_UNSUPPORTED_ERROR = "Version 3 is no longer supported." 

26 

27@blueprint.route('/') 

28def api_root(): 

29 return redirect(url_for('.api_spec')) 

30 

31 

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

38 

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

49 

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) 

55 

56 

57@blueprint.route('/swagger.json') 

58def api_spec(): 

59 swag = swagger(app) 

60 swag['info']['title'] = "" 

61 swag['info']['version'] = API_VERSION_NUMBER 

62 

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

67 

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': '*'})) 

77 

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) 

83 

84 

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) 

93 

94 

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) 

102 

103 

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) 

111 

112 

113######################################### 

114# Application CRUD API 

115 

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

124 

125 

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) 

134 

135 

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) 

145 

146 

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) 

156 

157 

158######################################### 

159# Article CRUD API 

160 

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

169 

170 

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) 

179 

180 

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) 

190 

191 

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) 

201 

202 

203######################################### 

204# Journal R API 

205 

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) 

214 

215 

216######################################### 

217# Application Bulk API 

218 

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

227 

228 

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

237 

238 

239######################################### 

240# Article Bulk API 

241 

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

248 

249 

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) 

256 

257 

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