Coverage for portality/app.py: 72%

227 statements  

« prev     ^ index     » next       coverage.py v6.4.2, created at 2022-09-20 23:33 +0100

1# -*- coding: UTF-8 -*- 

2 

3""" 

4This is the default app controller for portality. 

5For inclusion in your own project you should make your own version of this controller 

6and include the views you require, as well as writing new ones. Of course, views must 

7also be backed up by models, so have a look at the example models and use them / write 

8new ones as required too. 

9 

10~~DOAJ:WebApp~~ 

11""" 

12 

13import os, sys 

14import tzlocal 

15import pytz 

16 

17from flask import request, abort, render_template, redirect, send_file, url_for, jsonify, send_from_directory 

18from flask_login import login_user, current_user 

19 

20from datetime import datetime 

21 

22import portality.models as models 

23from portality.core import app, es_connection, initialise_index 

24from portality import settings 

25from portality.lib import edges, dates 

26 

27from portality.view.account import blueprint as account 

28from portality.view.admin import blueprint as admin 

29from portality.view.publisher import blueprint as publisher 

30from portality.view.query import blueprint as query 

31from portality.view.doaj import blueprint as doaj 

32from portality.view.oaipmh import blueprint as oaipmh 

33from portality.view.openurl import blueprint as openurl 

34from portality.view.atom import blueprint as atom 

35from portality.view.editor import blueprint as editor 

36from portality.view.doajservices import blueprint as services 

37from portality.view.jct import blueprint as jct 

38from portality.view.apply import blueprint as apply 

39if 'api1' in app.config['FEATURES']: 

40 from portality.view.api_v1 import blueprint as api_v1 

41if 'api2' in app.config['FEATURES']: 

42 from portality.view.api_v2 import blueprint as api_v2 

43if 'api3' in app.config['FEATURES']: 

44 from portality.view.api_v3 import blueprint as api_v3 

45from portality.view.status import blueprint as status 

46from portality.lib.normalise import normalise_doi 

47from portality.view.dashboard import blueprint as dashboard 

48 

49app.register_blueprint(account, url_prefix='/account') #~~->Account:Blueprint~~ 

50app.register_blueprint(admin, url_prefix='/admin') #~~-> Admin:Blueprint~~ 

51app.register_blueprint(publisher, url_prefix='/publisher') #~~-> Publisher:Blueprint~~ 

52app.register_blueprint(query, url_prefix='/query') # ~~-> Query:Blueprint~~ 

53app.register_blueprint(query, url_prefix="/admin_query") 

54app.register_blueprint(query, url_prefix="/publisher_query") 

55app.register_blueprint(query, url_prefix="/editor_query") 

56app.register_blueprint(query, url_prefix="/associate_query") 

57app.register_blueprint(editor, url_prefix='/editor') # ~~-> Editor:Blueprint~~ 

58app.register_blueprint(services, url_prefix='/service') # ~~-> Services:Blueprint~~ 

59if 'api1' in app.config['FEATURES']: 

60 app.register_blueprint(api_v1, url_prefix='/api/v1') # ~~-> APIv1:Blueprint~~ 

61if 'api2' in app.config['FEATURES']: 

62 app.register_blueprint(api_v2, url_prefix='/api/v2') # ~~-> APIv2:Blueprint~~ 

63if 'api3' in app.config['FEATURES']: 

64 app.register_blueprint(api_v3, url_prefix='/api') # ~~-> APIv3:Blueprint~~ 

65 app.register_blueprint(api_v3, url_prefix='/api/v3') # ~~-> APIv3:Blueprint~~ 

66app.register_blueprint(status, url_prefix='/status') # ~~-> Status:Blueprint~~ 

67app.register_blueprint(status, url_prefix='/_status') 

68app.register_blueprint(apply, url_prefix='/apply') # ~~-> Apply:Blueprint~~ 

69app.register_blueprint(jct, url_prefix="/jct") # ~~-> JCT:Blueprint~~ 

70app.register_blueprint(dashboard, url_prefix="/dashboard") #~~-> Dashboard:Blueprint~~ 

71 

72app.register_blueprint(oaipmh) # ~~-> OAIPMH:Blueprint~~ 

73app.register_blueprint(openurl) # ~~-> OpenURL:Blueprint~~ 

74app.register_blueprint(atom) # ~~-> Atom:Blueprint~~ 

75app.register_blueprint(doaj) # ~~-> DOAJ:Blueprint~~ 

76 

77# initialise the index - don't put into if __name__ == '__main__' block, 

78# because that does not run if gunicorn is loading the app, as opposed 

79# to the app being run directly by python portality/app.py 

80# putting it here ensures it will run under any web server 

81initialise_index(app, es_connection) 

82 

83# serve static files from multiple potential locations 

84# this allows us to override the standard static file handling with our own dynamic version 

85# ~~-> Assets:WebRoute~~ 

86# @app.route("/static_content/<path:filename>") 

87@app.route("/static/<path:filename>") 

88@app.route("/assets/<path:filename>") 

89def our_static(filename): 

90 return custom_static(filename) 

91 

92 

93def custom_static(path): 

94 for dir in app.config.get("STATIC_PATHS", []): 

95 target = os.path.join(app.root_path, dir, path) 

96 if os.path.isfile(target): 

97 return send_from_directory(os.path.dirname(target), os.path.basename(target)) 

98 abort(404) 

99 

100 

101# Configure the Google Analytics tracker 

102# ~~-> GoogleAnalytics:ExternalService~~ 

103from portality.lib import plausible 

104plausible.create_logfile(app.config.get('PLAUSIBLE_LOG_DIR', None)) 

105 

106# Redirects from previous DOAJ app. 

107# RJ: I have decided to put these here so that they can be managed 

108# alongside the DOAJ codebase. I know they could also go into the 

109# nginx config, but there is a chance that they will get lost or forgotten 

110# some day, whereas this approach doesn't have that risk. 

111# ~~-> Legacy:WebRoute~~ 

112@app.route("/doaj") 

113def legacy(): 

114 func = request.values.get("func") 

115 if func == "csv": 

116 return redirect(url_for('doaj.csv_data')), 301 

117 elif func == "rss": 

118 return redirect(url_for('atom.feed')), 301 

119 elif func == "browse" or func == 'byPublicationFee ': 

120 return redirect(url_for('doaj.search')), 301 

121 elif func == "openurl": 

122 vals = request.values.to_dict(flat=True) 

123 del vals["func"] 

124 return redirect(url_for('openurl.openurl', **vals), 301) 

125 abort(404) 

126 

127 

128@app.route("/doaj2csv") 

129def another_legacy_csv_route(): 

130 return redirect("/csv"), 301 

131 

132################################################### 

133 

134# ~~-> DOAJArticleXML:Schema~~ 

135@app.route("/schemas/doajArticles.xsd") 

136def legacy_doaj_XML_schema(): 

137 schema_fn = 'doajArticles.xsd' 

138 return send_file( 

139 os.path.join(app.config.get("STATIC_DIR"), "doaj", schema_fn), 

140 mimetype="application/xml", as_attachment=True, attachment_filename=schema_fn 

141 ) 

142 

143 

144# ~~-> CrossrefArticleXML:WebRoute~~ 

145@app.route("/isCrossrefLoaded") 

146def is_crossref_loaded(): 

147 if app.config.get("LOAD_CROSSREF_THREAD") is not None and app.config.get("LOAD_CROSSREF_THREAD").is_alive(): 

148 return "false" 

149 else: 

150 return "true" 

151 

152 

153# FIXME: this used to calculate the site stats on request, but for the time being 

154# this is an unnecessary overhead, so taking it out. Will need to put something 

155# equivalent back in when we do the admin area 

156# ~~-> SiteStats:Feature~~ 

157@app.context_processor 

158def set_current_context(): 

159 """ Set some template context globals. """ 

160 ''' 

161 Inserts variables into every template this blueprint renders. This 

162 one deals with the announcement in the header, which can't be built 

163 into the template directly, as various styles are applied only if a 

164 header is present on the page. It also makes the list of DOAJ 

165 sponsors available and may include similar minor pieces of 

166 information. 

167 ''' 

168 return { 

169 'settings': settings, 

170 'statistics': models.JournalArticle.site_statistics(), 

171 "current_user": current_user, 

172 "app": app, 

173 "current_year": datetime.now().strftime('%Y'), 

174 "base_url": app.config.get('BASE_URL'), 

175 } 

176 

177 

178# Jinja2 Template Filters 

179# ~~-> Jinja2:Environment~~ 

180 

181@app.template_filter("bytesToFilesize") 

182def bytes_to_filesize(size): 

183 units = ["bytes", "Kb", "Mb", "Gb"] 

184 scale = 0 

185 while size > 1000 and scale < len(units): 

186 size = float(size) / 1000.0 # note that it is no longer 1024 

187 scale += 1 

188 return "{size:.1f}{unit}".format(size=size, unit=units[scale]) 

189 

190 

191@app.template_filter('utc_timestamp') 

192def utc_timestamp(stamp, string_format="%Y-%m-%dT%H:%M:%SZ"): 

193 """ 

194 Format a local time datetime object to UTC 

195 :param stamp: a datetime object 

196 :param string_format: defaults to "%Y-%m-%dT%H:%M:%SZ", which complies with ISO 8601 

197 :return: the string formatted datetime 

198 """ 

199 local = tzlocal.get_localzone() 

200 ld = local.localize(stamp) 

201 tt = ld.utctimetuple() 

202 utcdt = datetime(tt.tm_year, tt.tm_mon, tt.tm_mday, tt.tm_hour, tt.tm_min, tt.tm_sec, tzinfo=pytz.utc) 

203 return utcdt.strftime(string_format) 

204 

205 

206@app.template_filter("human_date") 

207def human_date(stamp, string_format="%d %B %Y"): 

208 return dates.reformat(stamp, out_format=string_format) 

209 

210 

211@app.template_filter('doi_url') 

212def doi_url(doi): 

213 """ 

214 Create a link from a DOI. 

215 :param doi: the string DOI 

216 :return: the HTML link 

217 """ 

218 

219 try: 

220 return "https://doi.org/" + normalise_doi(doi) 

221 except ValueError: 

222 return "" 

223 

224 

225@app.template_filter('form_diff_table_comparison_value') 

226def form_diff_table_comparison_value(val): 

227 """ 

228 Function for converting the given value to a suitable UI value for presentation in the diff table 

229 on the admin forms for update requests. 

230 

231 :param val: the raw value to be converted to a display value 

232 :return: 

233 """ 

234 if val is None: 

235 return "" 

236 if isinstance(val, list) and len(val) == 0: 

237 return "" 

238 

239 if isinstance(val, list): 

240 dvals = [] 

241 for v in val: 

242 dvals.append(form_diff_table_comparison_value(v)) 

243 return ", ".join(dvals) 

244 else: 

245 if val is True or (isinstance(val, str) and val.lower() == "true"): 

246 return "Yes" 

247 elif val is False or (isinstance(val, str) and val.lower() == "false"): 

248 return "No" 

249 return val 

250 

251 

252@app.template_filter('form_diff_table_subject_expand') 

253def form_diff_table_subject_expand(val): 

254 """ 

255 Function for expanding one or more subject classifications out to their full terms 

256 

257 :param val: 

258 :return: 

259 """ 

260 if val is None: 

261 return "" 

262 if isinstance(val, list) and len(val) == 0: 

263 return "" 

264 if not isinstance(val, list): 

265 val = [val] 

266 

267 from portality import lcc 

268 

269 results = [] 

270 for v in val: 

271 if v is None or v == "": 

272 continue 

273 expanded = lcc.lcc_index_by_code.get(v) 

274 if expanded is not None: 

275 results.append(expanded + " [code: " + v + "]") 

276 else: 

277 results.append(v) 

278 

279 return ", ".join(results) 

280 

281 

282####################################################### 

283 

284@app.context_processor 

285def search_query_source_wrapper(): 

286 def search_query_source(**params): 

287 return edges.make_url_query(**params) 

288 return dict(search_query_source=search_query_source) 

289 

290 

291@app.context_processor 

292def maned_of_wrapper(): 

293 def maned_of(): 

294 # ~~-> EditorGroup:Model ~~ 

295 egs = [] 

296 assignments = {} 

297 if current_user.has_role("admin"): 

298 egs = models.EditorGroup.groups_by_maned(current_user.id) 

299 if len(egs) > 0: 

300 assignments = models.Application.assignment_to_editor_groups(egs) 

301 return egs, assignments 

302 return dict(maned_of=maned_of) 

303 

304 

305# ~~-> Account:Model~~ 

306# ~~-> AuthNZ:Feature~~ 

307@app.before_request 

308def standard_authentication(): 

309 """Check remote_user on a per-request basis.""" 

310 remote_user = request.headers.get('REMOTE_USER', '') 

311 if remote_user: 

312 user = models.Account.pull(remote_user) 

313 if user: 

314 login_user(user, remember=False) 

315 elif 'api_key' in request.values: 

316 q = models.Account.query(q='api_key:"' + request.values['api_key'] + '"') 

317 if 'hits' in q: 

318 res = q['hits']['hits'] 

319 if len(res) == 1: 

320 user = models.Account.pull(res[0]['_source']['id']) 

321 if user: 

322 login_user(user, remember=False) 

323 

324 

325# Register configured API versions 

326# ~~-> APIv1:Blueprint~~ 

327# ~~-> APIv2:Blueprint~~ 

328# ~~-> APIv3:Blueprint~~ 

329features = app.config.get('FEATURES', []) 

330if 'api1' in features or 'api2' in features or 'api3' in features: 

331 @app.route('/api/') 

332 def api_directory(): 

333 vers = [] 

334 # NOTE: we never could run API v1 and v2 at the same time. 

335 # This code is here for future reference to add additional API versions 

336 if 'api1' in features: 

337 vers.append( 

338 { 

339 'version': '1.0.0', 

340 'base_url': url_for('api_v1.api_spec', _external=True, 

341 _scheme=app.config.get('PREFERRED_URL_SCHEME', 'https')), 

342 'note': 'First version of the DOAJ API', 

343 'docs_url': url_for('api_v1.docs', _external=True, 

344 _scheme=app.config.get('PREFERRED_URL_SCHEME', 'https')) 

345 } 

346 ) 

347 if 'api2' in features: 

348 vers.append( 

349 { 

350 'version': '2.0.0', 

351 'base_url': url_for('api_v2.api_spec', _external=True, 

352 _scheme=app.config.get('PREFERRED_URL_SCHEME', 'https')), 

353 'note': 'Second version of the DOAJ API', 

354 'docs_url': url_for('api_v2.docs', _external=True, 

355 _scheme=app.config.get('PREFERRED_URL_SCHEME', 'https')) 

356 } 

357 ) 

358 if 'api3' in features: 

359 vers.append( 

360 { 

361 'version': '3.0.0', 

362 'base_url': url_for('api_v3.api_spec', _external=True, 

363 _scheme=app.config.get('PREFERRED_URL_SCHEME', 'https')), 

364 'note': 'Third version of the DOAJ API', 

365 'docs_url': url_for('api_v3.docs', _external=True, 

366 _scheme=app.config.get('PREFERRED_URL_SCHEME', 'https')) 

367 } 

368 ) 

369 return jsonify({'api_versions': vers}) 

370 

371 

372# Make the reCAPTCHA key available to the js 

373# ~~-> ReCAPTCHA:ExternalService~~ 

374@app.route('/get_recaptcha_site_key') 

375def get_site_key(): 

376 return app.config.get('RECAPTCHA_SITE_KEY', '') 

377 

378 

379@app.errorhandler(400) 

380def page_not_found(e): 

381 return render_template('400.html'), 400 

382 

383 

384@app.errorhandler(401) 

385def page_not_found(e): 

386 return render_template('401.html'), 401 

387 

388 

389@app.errorhandler(404) 

390def page_not_found(e): 

391 return render_template('404.html'), 404 

392 

393 

394@app.errorhandler(500) 

395def page_not_found(e): 

396 return render_template('500.html'), 500 

397 

398 

399if __name__ == "__main__": 

400 pycharm_debug = app.config.get('DEBUG_PYCHARM', False) 

401 if len(sys.argv) > 1: 

402 if sys.argv[1] == '-d': 

403 pycharm_debug = True 

404 

405 if pycharm_debug: 

406 app.config['DEBUG'] = False 

407 import pydevd 

408 pydevd.settrace(app.config.get('DEBUG_PYCHARM_SERVER', 'localhost'), port=app.config.get('DEBUG_PYCHARM_PORT', 6000), stdoutToServer=True, stderrToServer=True) 

409 

410 app.run(host=app.config['HOST'], debug=app.config['DEBUG'], port=app.config['PORT'])