Coverage for portality/view/doaj.py: 57%
341 statements
« prev ^ index » next coverage.py v6.4.2, created at 2022-08-30 11:09 +0100
« prev ^ index » next coverage.py v6.4.2, created at 2022-08-30 11:09 +0100
1import json
2import re
3import urllib.error
4import urllib.parse
5import urllib.request
7from flask import Blueprint, request, make_response
8from flask import render_template, abort, redirect, url_for, send_file, jsonify
9from flask_login import current_user, login_required
11from portality import dao
12from portality import models
13from portality.core import app
14from portality.decorators import ssl_required
15from portality.forms.application_forms import JournalFormFactory
16from portality.lcc import lcc_jstree
17from portality.lib import plausible
18from portality.ui.messages import Messages
20blueprint = Blueprint('doaj', __name__)
23@blueprint.route("/")
24def home():
25 news = models.News.latest(app.config.get("FRONT_PAGE_NEWS_ITEMS", 5))
26 recent_journals = models.Journal.recent(max=16)
27 return render_template('doaj/index.html', news=news, recent_journals=recent_journals)
29@blueprint.route('/login/')
30def login():
31 return redirect(url_for('account.login'))
33@blueprint.route("/cookie_consent")
34def cookie_consent():
35 cont = request.values.get("continue")
36 if cont is not None:
37 resp = redirect(cont)
38 else:
39 resp = make_response()
40 # set a cookie that lasts for one year
41 resp.set_cookie(app.config.get("CONSENT_COOKIE_KEY"), Messages.CONSENT_COOKIE_VALUE, max_age=31536000, samesite=None, secure=True)
42 return resp
45@blueprint.route("/dismiss_site_note")
46def dismiss_site_note():
47 cont = request.values.get("continue")
48 if cont is not None:
49 resp = redirect(cont)
50 else:
51 resp = make_response()
52 # set a cookie that lasts for one year
53 resp.set_cookie(app.config.get("SITE_NOTE_KEY"), app.config.get("SITE_NOTE_COOKIE_VALUE"), max_age=app.config.get("SITE_NOTE_SLEEP"), samesite=None, secure=True)
54 return resp
57@blueprint.route("/news")
58def news():
59 return redirect("https://blog.doaj.org")
62@blueprint.route("/fqw_hit", methods=['POST'])
63def fqw_hit():
64 page = request.form.get('embedding_page')
65 if page is not None:
66 plausible.send_event(app.config.get('GA_CATEGORY_FQW', 'FQW'),
67 action=app.config.get('GA_ACTION_FQW', 'hit'),
68 label=request.form.get('embedding_page'))
70 # No content response, whether data there or not.
71 return '', 204
74@blueprint.route("/search/journals", methods=["GET"])
75def journals_search():
76 return render_template("doaj/journals_search.html", lcc_tree=lcc_jstree)
79@blueprint.route("/search/articles", methods=["GET"])
80def articles_search():
81 return render_template("doaj/articles_search.html", lcc_tree=lcc_jstree)
84@blueprint.route("/search", methods=['GET'])
85def search():
86 # If there are URL params, check if we need to redirect to articles rather than journals
87 if request.values:
88 # Flat search the query params as string so we don't have to traverse all the way down the decoded json.
89 if re.search(r'\"_type\"\s*:\s*\"article\"', request.values.get('source', '')):
90 return redirect(url_for("doaj.articles_search"), 301)
91 return redirect(url_for("doaj.journals_search"), 301)
94@blueprint.route("/search", methods=['POST'])
95def search_post():
96 """ Redirect a query from the box on the index page to the search page. """
97 if request.form.get('origin') != 'ui':
98 abort(400) # bad request - we must receive searches from our own UI
100 ref = request.form.get("ref")
101 if ref is None:
102 abort(400) # Referrer is required
104 ct = request.form.get("content-type")
105 kw = request.form.get("keywords")
106 field = request.form.get("fields")
108 if kw is None:
109 kw = request.form.get("q") # back-compat for the simple search widget
111 # lhs for journals, rhs for articles
112 field_map = {
113 "all" : (None, None),
114 "title" : ("bibjson.title", "bibjson.title"),
115 "abstract" : (None, "bibjson.abstract"),
116 "subject" : ("index.classification", "index.classification"),
117 "author" : (None, "bibjson.author.name"),
118 "issn" : ("index.issn.exact", None),
119 "publisher" : ("bibjson.publisher.name", None)
120 }
121 default_field_opts = field_map.get(field, None)
122 default_field = None
124 route = ""
125 if not ct or ct == "journals":
126 route = url_for("doaj.journals_search")
127 if default_field_opts:
128 default_field = default_field_opts[0]
129 elif ct == "articles":
130 route = url_for("doaj.articles_search")
131 if default_field_opts:
132 default_field = default_field_opts[1]
133 else:
134 abort(400)
136 query = dao.Facetview2.make_query(kw, default_field=default_field, default_operator="AND")
138 return redirect(route + '?source=' + urllib.parse.quote(json.dumps(query)) + "&ref=" + urllib.parse.quote(ref))
140#############################################
142# FIXME: this should really live somewhere else more appropirate to who can access it
143@blueprint.route("/journal/readonly/<journal_id>", methods=["GET"])
144@login_required
145@ssl_required
146def journal_readonly(journal_id):
147 if (
148 not current_user.has_role("admin")
149 or not current_user.has_role("editor")
150 or not current_user.has_role("associate_editor")
151 ):
152 abort(401)
154 j = models.Journal.pull(journal_id)
155 if j is None:
156 abort(404)
158 fc = JournalFormFactory.context("readonly")
159 fc.processor(source=j)
160 return fc.render_template(obj=j, lcc_tree=lcc_jstree)
163@blueprint.route("/csv")
164@plausible.pa_event(app.config.get('GA_CATEGORY_JOURNALCSV', 'JournalCSV'),
165 action=app.config.get('GA_ACTION_JOURNALCSV', 'Download'))
166def csv_data():
167 csv_info = models.Cache.get_latest_csv()
168 if csv_info is None:
169 abort(404)
170 store_url = csv_info.get("url")
171 if store_url is None:
172 abort(404)
173 if store_url.startswith("/"):
174 store_url = "/store" + store_url
175 return redirect(store_url, code=307)
178@blueprint.route("/sitemap.xml")
179def sitemap():
180 sitemap_url = models.Cache.get_latest_sitemap()
181 if sitemap_url is None:
182 abort(404)
183 if sitemap_url.startswith("/"):
184 sitemap_url = "/store" + sitemap_url
185 return redirect(sitemap_url, code=307)
188# @blueprint.route("/public-data-dump")
189# def public_data_dump():
190# data_dump = models.Cache.get_public_data_dump()
191# show_article = data_dump.get("article", {}).get("url") is not None
192# article_size = data_dump.get("article", {}).get("size")
193# show_journal = data_dump.get("journal", {}).get("url") is not None
194# journal_size = data_dump.get("journal", {}).get("size")
195# return render_template("doaj/public_data_dump.html",
196# show_article=show_article,
197# article_size=article_size,
198# show_journal=show_journal,
199# journal_size=journal_size)
202@blueprint.route("/public-data-dump/<record_type>")
203def public_data_dump_redirect(record_type):
204 store_url = models.Cache.get_public_data_dump().get(record_type, {}).get("url")
205 if store_url is None:
206 abort(404)
207 if store_url.startswith("/"):
208 store_url = "/store" + store_url
209 return redirect(store_url, code=307)
212@blueprint.route("/store/<container>/<filename>")
213def get_from_local_store(container, filename):
214 if not app.config.get("STORE_LOCAL_EXPOSE", False):
215 abort(404)
217 from portality import store
218 localStore = store.StoreFactory.get(None)
219 file_handle = localStore.get(container, filename)
220 return send_file(file_handle, mimetype="application/octet-stream", as_attachment=True, attachment_filename=filename)
223@blueprint.route('/autocomplete/<doc_type>/<field_name>', methods=["GET", "POST"])
224def autocomplete(doc_type, field_name):
225 prefix = request.args.get('q', '')
226 if not prefix:
227 return jsonify({'suggestions': [{"id": "", "text": "No results found"}]}) # select2 does not understand 400, which is the correct code here...
229 m = models.lookup_model(doc_type)
230 if not m:
231 return jsonify({'suggestions': [{"id": "", "text": "No results found"}]}) # select2 does not understand 404, which is the correct code here...
233 size = request.args.get('size', 5)
235 filter_field = app.config.get("AUTOCOMPLETE_ADVANCED_FIELD_MAPS", {}).get(field_name)
237 suggs = []
238 if filter_field is None:
239 suggs = m.autocomplete(field_name, prefix, size=size)
240 else:
241 suggs = m.advanced_autocomplete(filter_field, field_name, prefix, size=size, prefix_only=False)
243 return jsonify({'suggestions': suggs})
244 # you shouldn't return lists top-level in a JSON response:
245 # http://flask.pocoo.org/docs/security/#json-security
248@blueprint.route("/toc/<identifier>")
249@blueprint.route("/toc/<identifier>/<volume>")
250@blueprint.route("/toc/<identifier>/<volume>/<issue>")
251def toc(identifier=None, volume=None, issue=None):
252 """ Table of Contents page for a journal. identifier may be the journal id or an issn """
253 # If this route is changed, update JOURNAL_TOC_URL_FRAG in settings.py (partial ToC page link for journal CSV)
255 journal = None
256 issn_ref = False
258 if identifier is None:
259 abort(404)
261 if len(identifier) == 9:
262 js = models.Journal.find_by_issn(identifier, in_doaj=True)
264 if len(js) > 1:
265 abort(400) # really this is a 500 - we have more than one journal with this issn
266 if len(js) == 0:
267 abort(404)
268 journal = js[0]
270 if journal is None:
271 abort(400)
273 issn_ref = True # just a flag so we can check if we were requested via issn
274 elif len(identifier) == 32:
275 js = models.Journal.pull(identifier) # Returns None on fail
277 if js is None or not js.is_in_doaj():
278 abort(404)
279 journal = js
280 else:
281 abort(400)
283 # get the bibjson record that we're going to render
284 bibjson = journal.bibjson()
286 # The issn we are using to build the TOC
287 issn = bibjson.get_preferred_issn()
289 # now redirect to the canonical E-ISSN if one is available
291 if issn_ref: # the journal is referred to by an ISSN
292 # if there is an E-ISSN (and it's not the one in the request), redirect to it
293 eissn = bibjson.get_one_identifier(bibjson.E_ISSN)
294 if eissn and identifier != eissn:
295 return redirect(url_for('doaj.toc', identifier=eissn, volume=volume, issue=issue), 301)
297 # if there's no E-ISSN, but there is a P-ISSN (and it's not the one in the request), redirect to the P-ISSN
298 if not eissn:
299 pissn = bibjson.get_one_identifier(bibjson.P_ISSN)
300 if pissn and identifier != pissn:
301 return redirect(url_for('doaj.toc', identifier=pissn, volume=volume, issue=issue), 301)
303 # Add the volume and issue to query if present in path
304 if volume:
305 filters = [dao.Facetview2.make_term_filter('bibjson.journal.volume.exact', volume)]
306 if issue:
307 filters += [dao.Facetview2.make_term_filter('bibjson.journal.number.exact', issue)]
308 q = dao.Facetview2.make_query(filters=filters)
310 return redirect(url_for('doaj.toc', identifier=issn) + '?source=' + dao.Facetview2.url_encode_query(q))
312 # The journal has neither a PISSN or an EISSN. Yet somehow
313 # issn_ref is True, the request was referring to the journal
314 # by its ISSN. Not sure how this could ever happen, but just
315 # continue loading the data and do nothing else in such a
316 # case.
318 else: # the journal is NOT referred to by any ISSN
320 # if there is an E-ISSN, redirect to it
321 # if not, but there is a P-ISSN, redirect to it
322 # if neither ISSN is present, continue loading the page
323 issn = bibjson.get_one_identifier(bibjson.E_ISSN)
324 if not issn:
325 issn = bibjson.get_one_identifier(bibjson.P_ISSN)
326 if issn:
327 return redirect(url_for('doaj.toc', identifier=issn, volume=volume, issue=issue), 301)
329 # let it continue loading if we only have the hex UUID for the journal (no ISSNs)
330 # and the user is referring to the toc page via that ID
332 # get the continuations for this journal, future and past
333 future_journals = journal.get_future_continuations()
334 past_journals = journal.get_past_continuations()
336 # extract the bibjson, which is what the template is after, and whether the record is in doaj
337 #future = [j.bibjson() j for j in future_journals]
338 #past = [j.bibjson() for j in past_journals]
340 # now render all that information
341 return render_template('doaj/toc.html', journal=journal, bibjson=bibjson, future=future_journals, past=past_journals,
342 toc_issns=journal.bibjson().issns())
345#~~->Article:Page~~
346@blueprint.route("/article/<identifier>")
347def article_page(identifier=None):
348 # identifier must be the article id
349 article = models.Article.pull(identifier)
351 if article is None or not article.is_in_doaj():
352 abort(404)
354 # find the related journal record
355 journal = None
356 issns = article.bibjson().issns()
357 more_issns = article.bibjson().journal_issns
358 for issn in issns + more_issns:
359 journals = models.Journal.find_by_issn(issn)
360 if len(journals) > 0:
361 journal = journals[0]
363 return render_template('doaj/article.html', article=article, journal=journal, page={"highlight" : True})
365# Not using this form for now but we might bring it back later
366# @blueprint.route("/contact/", methods=["GET", "POST"])
367# def contact():
368# if request.method == "GET":
369# form = ContactUs()
370# if current_user.is_authenticated:
371# form.email.data = current_user.email
372# return render_template("doaj/contact.html", form=form)
373# elif request.method == "POST":
374# prepop = request.values.get("ref")
375# form = ContactUs(request.form)
376#
377# if current_user.is_authenticated and (form.email.data is None or form.email.data == ""):
378# form.email.data = current_user.email
379#
380# if prepop is not None:
381# return render_template("doaj/contact.html", form=form)
382#
383# if not form.validate():
384# return render_template("doaj/contact.html", form=form)
385#
386# data = _verify_recaptcha(form.recaptcha_value.data)
387# if data["success"]:
388# send_contact_form(form)
389# flash("Thank you for your feedback which has been received by the DOAJ Team.", "success")
390# form = ContactUs()
391# return render_template("doaj/contact.html", form=form)
392# else:
393# flash("Your form could not be submitted,", "error")
394# return render_template("doaj/contact.html", form=form)
397###############################################################
398# The various static endpoints
399###############################################################
401@blueprint.route("/googlebdb21861de30fe30.html")
402def google_webmaster_tools():
403 return 'google-site-verification: googlebdb21861de30fe30.html'
406@blueprint.route("/accessibility/")
407def accessibility():
408 return render_template("layouts/static_page.html", page_frag="/legal/accessibility.html")
411@blueprint.route("/privacy/")
412def privacy():
413 return render_template("layouts/static_page.html", page_frag="/legal/privacy.html")
416@blueprint.route("/contact/")
417def contact():
418 return render_template("layouts/static_page.html", page_frag="/legal/contact.html")
421@blueprint.route("/terms/")
422def terms():
423 return render_template("layouts/static_page.html", page_frag="/legal/terms.html")
426@blueprint.route("/media/")
427def media():
428 """
429 ~~Media:WebRoute~~
430 """
431 return render_template("layouts/static_page.html", page_frag="/legal/media.html")
434@blueprint.route("/support/")
435def support():
436 return render_template("layouts/static_page.html", page_frag="/support/index.html")
439@blueprint.route("/support/sponsors/")
440def sponsors():
441 return render_template("layouts/static_page.html", page_frag="/support/sponsors.html")
444@blueprint.route("/support/publisher-supporters/")
445def publisher_supporters():
446 return render_template("layouts/static_page.html", page_frag="/support/publisher-supporters.html")
449@blueprint.route("/support/supporters/")
450def supporters():
451 return render_template("layouts/static_page.html", page_frag="/support/supporters.html")
454@blueprint.route("/support/thank-you/")
455def application_thanks():
456 return render_template("layouts/static_page.html", page_frag="/support/thank-you.html")
459@blueprint.route("/apply/guide/")
460def guide():
461 return render_template("layouts/static_page.html", page_frag="/apply/guide.html")
464@blueprint.route("/apply/seal/")
465def seal():
466 return render_template("layouts/static_page.html", page_frag="/apply/seal.html")
469@blueprint.route("/apply/transparency/")
470def transparency():
471 return render_template("layouts/static_page.html", page_frag="/apply/transparency.html")
474@blueprint.route("/apply/why-index/")
475def why_index():
476 return render_template("layouts/static_page.html", page_frag="/apply/why-index.html")
478# TODO: Uncomment when ready for public access - S.E. 2022-03-14
479# @blueprint.route("/apply/publisher-responsibilities/")
480# def publisher_responsibilities():
481# return render_template("layouts/static_page.html", page_frag="/apply/publisher-responsibilities.html")
484@blueprint.route("/apply/copyright-and-licensing/")
485def copyright_and_licensing():
486 return render_template("layouts/static_page.html", page_frag="/apply/copyright-and-licensing.html")
489@blueprint.route("/docs/oai-pmh/")
490def oai_pmh():
491 return render_template("layouts/static_page.html", page_frag="/docs/oai-pmh.html")
494@blueprint.route('/docs/api/')
495def docs():
496 return redirect(url_for('api_v3.docs'))
499@blueprint.route("/docs/xml/")
500def xml():
501 return render_template("layouts/static_page.html", page_frag="/docs/xml.html")
504@blueprint.route("/docs/widgets/")
505def widgets():
506 return render_template("layouts/static_page.html", page_frag="/docs/widgets.html", base_url=app.config.get('BASE_URL'))
509@blueprint.route("/docs/public-data-dump/")
510def public_data_dump():
511 return render_template("layouts/static_page.html", page_frag="/docs/public-data-dump.html")
514@blueprint.route("/docs/openurl/")
515def openurl():
516 return render_template("layouts/static_page.html", page_frag="/docs/openurl.html")
519@blueprint.route("/docs/faq/")
520def faq():
521 return render_template("layouts/static_page.html", page_frag="/docs/faq.html")
524@blueprint.route("/about/")
525def about():
526 return render_template("layouts/static_page.html", page_frag="/about/index.html")
529@blueprint.route("/about/ambassadors/")
530def ambassadors():
531 return render_template("layouts/static_page.html", page_frag="/about/ambassadors.html")
534@blueprint.route("/about/advisory-board-council/")
535def abc():
536 return render_template("layouts/static_page.html", page_frag="/about/advisory-board-council.html")
539@blueprint.route("/about/editorial-subcommittee/")
540def editorial_subcommittee():
541 return render_template("layouts/static_page.html", page_frag="/about/editorial-subcommittee.html")
544@blueprint.route("/about/volunteers/")
545def volunteers():
546 return render_template("layouts/static_page.html", page_frag="/about/volunteers.html")
549@blueprint.route("/about/team/")
550def team():
551 return render_template("layouts/static_page.html", page_frag="/about/team.html")
553@blueprint.route("/preservation/")
554def preservation():
555 return render_template("layouts/static_page.html", page_frag="/preservation/index.html")
557# LEGACY ROUTES
558@blueprint.route("/subjects")
559def subjects():
560 # return render_template("doaj/subjects.html", subject_page=True, lcc_jstree=json.dumps(lcc_jstree))
561 return redirect(url_for("doaj.journals_search"), 301)
564@blueprint.route("/application/new")
565def old_application():
566 return redirect(url_for("apply.public_application", **request.args), code=308)
569@blueprint.route("/<cc>/mejorespracticas")
570@blueprint.route("/<cc>/boaspraticas")
571@blueprint.route("/<cc>/bestpractice")
572@blueprint.route("/<cc>/editionsavante")
573@blueprint.route("/bestpractice")
574@blueprint.route("/oainfo")
575def bestpractice(cc=None):
576 return redirect(url_for("doaj.transparency", **request.args), code=308)
578@blueprint.route("/membership")
579def membership():
580 return redirect(url_for("doaj.support", **request.args), code=308)
583@blueprint.route("/publishermembers")
584def old_sponsors():
585 return redirect(url_for("doaj.sponsors", **request.args), code=308)
588@blueprint.route("/members")
589def members():
590 return redirect(url_for("doaj.supporters", **request.args), code=308)
593@blueprint.route('/features')
594def features():
595 return redirect(url_for("doaj.xml", **request.args), code=308)
598# @blueprint.route('/widgets')
599# def old_widgets():
600# return redirect(url_for("doaj.widgets", **request.args), code=308)
603# @blueprint.route("/public-data-dump/<record_type>")
604# def old_public_data_dump(record_type):
605# return redirect(url_for("doaj.public_data_dump", **request.args), code=308)
608@blueprint.route("/openurl/help")
609def old_openurl():
610 return redirect(url_for("doaj.openurl", **request.args), code=308)
613@blueprint.route("/faq")
614def old_faq():
615 return redirect(url_for("doaj.faq", **request.args), code=308)
618@blueprint.route("/publishers")
619def publishers():
620 return redirect(url_for("doaj.guide", **request.args), code=308)
623# Redirects necessitated by new templates
624@blueprint.route("/password-reset/")
625def new_password_reset():
626 return redirect(url_for('account.forgot'), code=301)