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

1import json 

2 

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

4from flask_login import current_user 

5from flask_swagger import swagger 

6 

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 

15 

16blueprint = Blueprint('api_v3', __name__) 

17 

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

19 

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

23 

24 

25@blueprint.route('/') 

26def api_v3_root(): 

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

28 

29 

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) 

41 

42 

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

44def api_spec(): 

45 swag = swagger(app) 

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

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

48 

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

52 

53 

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

60 

61 

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

73 

74 # check the page is an integer 

75 try: 

76 page = int(page) 

77 except: 

78 raise Api400Error("Page number was not an integer") 

79 

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

85 

86 try: 

87 results = DiscoveryApi.search('application', current_user, search_query, page, psize, sort) 

88 except DiscoveryException as e: 

89 raise Api400Error(str(e)) 

90 

91 return jsonify_models(results) 

92 

93 

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

104 

105 # check the page is an integer 

106 try: 

107 page = int(page) 

108 except: 

109 raise Api400Error("Page number was not an integer") 

110 

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

116 

117 try: 

118 results = DiscoveryApi.search('journal', None, search_query, page, psize, sort) 

119 except DiscoveryException as e: 

120 raise Api400Error(str(e)) 

121 

122 return jsonify_models(results) 

123 

124 

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

135 

136 # check the page is an integer 

137 try: 

138 page = int(page) 

139 except: 

140 raise Api400Error("Page number was not an integer") 

141 

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

147 

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

153 

154 return jsonify_models(results) 

155 

156 

157######################################### 

158# Application CRUD API 

159 

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

172 

173 # delegate to the API implementation 

174 a = ApplicationsCrudApi.create(data, current_user._get_current_object()) 

175 

176 # respond with a suitable Created response 

177 return created(a, url_for("api_v2.retrieve_application", application_id=a.id)) 

178 

179 

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) 

189 

190 

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

204 

205 # delegate to the API implementation 

206 ApplicationsCrudApi.update(application_id, data, current_user._get_current_object()) 

207 

208 # respond with a suitable No Content successful response 

209 return no_content() 

210 

211 

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

222 

223 

224######################################### 

225# Article CRUD API 

226 

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

239 

240 # delegate to the API implementation 

241 a = ArticlesCrudApi.create(data, current_user._get_current_object()) 

242 

243 # respond with a suitable Created response 

244 return created(a, url_for("api_v2.retrieve_article", article_id=a.id)) 

245 

246 

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) 

256 

257 

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

271 

272 # delegate to the API implementation 

273 ArticlesCrudApi.update(article_id, data, current_user._get_current_object()) 

274 

275 # respond with a suitable No Content successful response 

276 return no_content() 

277 

278 

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

289 

290 

291######################################### 

292# Journal R API 

293 

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

302 

303 

304######################################### 

305# Application Bulk API 

306 

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

319 

320 # delegate to the API implementation 

321 ids = ApplicationsBulkApi.create(data, current_user._get_current_object()) 

322 

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

327 

328 # respond with a suitable Created response 

329 return bulk_created(inl) 

330 

331 

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

344 

345 ApplicationsBulkApi.delete(data, current_user._get_current_object()) 

346 

347 return no_content() 

348 

349 

350######################################### 

351# Article Bulk API 

352 

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

365 

366 # delegate to the API implementation 

367 ids = ArticlesBulkApi.create(data, current_user._get_current_object()) 

368 

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

373 

374 # respond with a suitable Created response 

375 return bulk_created(inl) 

376 

377 

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

390 

391 ArticlesBulkApi.delete(data, current_user._get_current_object()) 

392 

393 return no_content()