Coverage for portality/app.py: 72%
227 statements
« prev ^ index » next coverage.py v6.4.2, created at 2022-09-20 23:33 +0100
« prev ^ index » next coverage.py v6.4.2, created at 2022-09-20 23:33 +0100
1# -*- coding: UTF-8 -*-
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.
10~~DOAJ:WebApp~~
11"""
13import os, sys
14import tzlocal
15import pytz
17from flask import request, abort, render_template, redirect, send_file, url_for, jsonify, send_from_directory
18from flask_login import login_user, current_user
20from datetime import datetime
22import portality.models as models
23from portality.core import app, es_connection, initialise_index
24from portality import settings
25from portality.lib import edges, dates
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
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~~
72app.register_blueprint(oaipmh) # ~~-> OAIPMH:Blueprint~~
73app.register_blueprint(openurl) # ~~-> OpenURL:Blueprint~~
74app.register_blueprint(atom) # ~~-> Atom:Blueprint~~
75app.register_blueprint(doaj) # ~~-> DOAJ:Blueprint~~
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)
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)
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)
101# Configure the Google Analytics tracker
102# ~~-> GoogleAnalytics:ExternalService~~
103from portality.lib import plausible
104plausible.create_logfile(app.config.get('PLAUSIBLE_LOG_DIR', None))
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)
128@app.route("/doaj2csv")
129def another_legacy_csv_route():
130 return redirect("/csv"), 301
132###################################################
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 )
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"
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 }
178# Jinja2 Template Filters
179# ~~-> Jinja2:Environment~~
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])
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)
206@app.template_filter("human_date")
207def human_date(stamp, string_format="%d %B %Y"):
208 return dates.reformat(stamp, out_format=string_format)
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 """
219 try:
220 return "https://doi.org/" + normalise_doi(doi)
221 except ValueError:
222 return ""
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.
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 ""
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
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
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]
267 from portality import lcc
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)
279 return ", ".join(results)
282#######################################################
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)
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)
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)
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})
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', '')
379@app.errorhandler(400)
380def page_not_found(e):
381 return render_template('400.html'), 400
384@app.errorhandler(401)
385def page_not_found(e):
386 return render_template('401.html'), 401
389@app.errorhandler(404)
390def page_not_found(e):
391 return render_template('404.html'), 404
394@app.errorhandler(500)
395def page_not_found(e):
396 return render_template('500.html'), 500
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
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)
410 app.run(host=app.config['HOST'], debug=app.config['DEBUG'], port=app.config['PORT'])