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

1import json 

2import re 

3import urllib.error 

4import urllib.parse 

5import urllib.request 

6 

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 

10 

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 

19 

20blueprint = Blueprint('doaj', __name__) 

21 

22 

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) 

28 

29@blueprint.route('/login/') 

30def login(): 

31 return redirect(url_for('account.login')) 

32 

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 

43 

44 

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 

55 

56 

57@blueprint.route("/news") 

58def news(): 

59 return redirect("https://blog.doaj.org") 

60 

61 

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

69 

70 # No content response, whether data there or not. 

71 return '', 204 

72 

73 

74@blueprint.route("/search/journals", methods=["GET"]) 

75def journals_search(): 

76 return render_template("doaj/journals_search.html", lcc_tree=lcc_jstree) 

77 

78 

79@blueprint.route("/search/articles", methods=["GET"]) 

80def articles_search(): 

81 return render_template("doaj/articles_search.html", lcc_tree=lcc_jstree) 

82 

83 

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) 

92 

93 

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 

99 

100 ref = request.form.get("ref") 

101 if ref is None: 

102 abort(400) # Referrer is required 

103 

104 ct = request.form.get("content-type") 

105 kw = request.form.get("keywords") 

106 field = request.form.get("fields") 

107 

108 if kw is None: 

109 kw = request.form.get("q") # back-compat for the simple search widget 

110 

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 

123 

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) 

135 

136 query = dao.Facetview2.make_query(kw, default_field=default_field, default_operator="AND") 

137 

138 return redirect(route + '?source=' + urllib.parse.quote(json.dumps(query)) + "&ref=" + urllib.parse.quote(ref)) 

139 

140############################################# 

141 

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) 

153 

154 j = models.Journal.pull(journal_id) 

155 if j is None: 

156 abort(404) 

157 

158 fc = JournalFormFactory.context("readonly") 

159 fc.processor(source=j) 

160 return fc.render_template(obj=j, lcc_tree=lcc_jstree) 

161 

162 

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) 

176 

177 

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) 

186 

187 

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) 

200 

201 

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) 

210 

211 

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) 

216 

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) 

221 

222 

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

228 

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

232 

233 size = request.args.get('size', 5) 

234 

235 filter_field = app.config.get("AUTOCOMPLETE_ADVANCED_FIELD_MAPS", {}).get(field_name) 

236 

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) 

242 

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 

246 

247 

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) 

254 

255 journal = None 

256 issn_ref = False 

257 

258 if identifier is None: 

259 abort(404) 

260 

261 if len(identifier) == 9: 

262 js = models.Journal.find_by_issn(identifier, in_doaj=True) 

263 

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] 

269 

270 if journal is None: 

271 abort(400) 

272 

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 

276 

277 if js is None or not js.is_in_doaj(): 

278 abort(404) 

279 journal = js 

280 else: 

281 abort(400) 

282 

283 # get the bibjson record that we're going to render 

284 bibjson = journal.bibjson() 

285 

286 # The issn we are using to build the TOC 

287 issn = bibjson.get_preferred_issn() 

288 

289 # now redirect to the canonical E-ISSN if one is available 

290 

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) 

296 

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) 

302 

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) 

309 

310 return redirect(url_for('doaj.toc', identifier=issn) + '?source=' + dao.Facetview2.url_encode_query(q)) 

311 

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. 

317 

318 else: # the journal is NOT referred to by any ISSN 

319 

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) 

328 

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 

331 

332 # get the continuations for this journal, future and past 

333 future_journals = journal.get_future_continuations() 

334 past_journals = journal.get_past_continuations() 

335 

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] 

339 

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

343 

344 

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) 

350 

351 if article is None or not article.is_in_doaj(): 

352 abort(404) 

353 

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] 

362 

363 return render_template('doaj/article.html', article=article, journal=journal, page={"highlight" : True}) 

364 

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) 

395 

396 

397############################################################### 

398# The various static endpoints 

399############################################################### 

400 

401@blueprint.route("/googlebdb21861de30fe30.html") 

402def google_webmaster_tools(): 

403 return 'google-site-verification: googlebdb21861de30fe30.html' 

404 

405 

406@blueprint.route("/accessibility/") 

407def accessibility(): 

408 return render_template("layouts/static_page.html", page_frag="/legal/accessibility.html") 

409 

410 

411@blueprint.route("/privacy/") 

412def privacy(): 

413 return render_template("layouts/static_page.html", page_frag="/legal/privacy.html") 

414 

415 

416@blueprint.route("/contact/") 

417def contact(): 

418 return render_template("layouts/static_page.html", page_frag="/legal/contact.html") 

419 

420 

421@blueprint.route("/terms/") 

422def terms(): 

423 return render_template("layouts/static_page.html", page_frag="/legal/terms.html") 

424 

425 

426@blueprint.route("/media/") 

427def media(): 

428 """ 

429 ~~Media:WebRoute~~ 

430 """ 

431 return render_template("layouts/static_page.html", page_frag="/legal/media.html") 

432 

433 

434@blueprint.route("/support/") 

435def support(): 

436 return render_template("layouts/static_page.html", page_frag="/support/index.html") 

437 

438 

439@blueprint.route("/support/sponsors/") 

440def sponsors(): 

441 return render_template("layouts/static_page.html", page_frag="/support/sponsors.html") 

442 

443 

444@blueprint.route("/support/publisher-supporters/") 

445def publisher_supporters(): 

446 return render_template("layouts/static_page.html", page_frag="/support/publisher-supporters.html") 

447 

448 

449@blueprint.route("/support/supporters/") 

450def supporters(): 

451 return render_template("layouts/static_page.html", page_frag="/support/supporters.html") 

452 

453 

454@blueprint.route("/support/thank-you/") 

455def application_thanks(): 

456 return render_template("layouts/static_page.html", page_frag="/support/thank-you.html") 

457 

458 

459@blueprint.route("/apply/guide/") 

460def guide(): 

461 return render_template("layouts/static_page.html", page_frag="/apply/guide.html") 

462 

463 

464@blueprint.route("/apply/seal/") 

465def seal(): 

466 return render_template("layouts/static_page.html", page_frag="/apply/seal.html") 

467 

468 

469@blueprint.route("/apply/transparency/") 

470def transparency(): 

471 return render_template("layouts/static_page.html", page_frag="/apply/transparency.html") 

472 

473 

474@blueprint.route("/apply/why-index/") 

475def why_index(): 

476 return render_template("layouts/static_page.html", page_frag="/apply/why-index.html") 

477 

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

482 

483 

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

487 

488 

489@blueprint.route("/docs/oai-pmh/") 

490def oai_pmh(): 

491 return render_template("layouts/static_page.html", page_frag="/docs/oai-pmh.html") 

492 

493 

494@blueprint.route('/docs/api/') 

495def docs(): 

496 return redirect(url_for('api_v3.docs')) 

497 

498 

499@blueprint.route("/docs/xml/") 

500def xml(): 

501 return render_template("layouts/static_page.html", page_frag="/docs/xml.html") 

502 

503 

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

507 

508 

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

512 

513 

514@blueprint.route("/docs/openurl/") 

515def openurl(): 

516 return render_template("layouts/static_page.html", page_frag="/docs/openurl.html") 

517 

518 

519@blueprint.route("/docs/faq/") 

520def faq(): 

521 return render_template("layouts/static_page.html", page_frag="/docs/faq.html") 

522 

523 

524@blueprint.route("/about/") 

525def about(): 

526 return render_template("layouts/static_page.html", page_frag="/about/index.html") 

527 

528 

529@blueprint.route("/about/ambassadors/") 

530def ambassadors(): 

531 return render_template("layouts/static_page.html", page_frag="/about/ambassadors.html") 

532 

533 

534@blueprint.route("/about/advisory-board-council/") 

535def abc(): 

536 return render_template("layouts/static_page.html", page_frag="/about/advisory-board-council.html") 

537 

538 

539@blueprint.route("/about/editorial-subcommittee/") 

540def editorial_subcommittee(): 

541 return render_template("layouts/static_page.html", page_frag="/about/editorial-subcommittee.html") 

542 

543 

544@blueprint.route("/about/volunteers/") 

545def volunteers(): 

546 return render_template("layouts/static_page.html", page_frag="/about/volunteers.html") 

547 

548 

549@blueprint.route("/about/team/") 

550def team(): 

551 return render_template("layouts/static_page.html", page_frag="/about/team.html") 

552 

553@blueprint.route("/preservation/") 

554def preservation(): 

555 return render_template("layouts/static_page.html", page_frag="/preservation/index.html") 

556 

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) 

562 

563 

564@blueprint.route("/application/new") 

565def old_application(): 

566 return redirect(url_for("apply.public_application", **request.args), code=308) 

567 

568 

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) 

577 

578@blueprint.route("/membership") 

579def membership(): 

580 return redirect(url_for("doaj.support", **request.args), code=308) 

581 

582 

583@blueprint.route("/publishermembers") 

584def old_sponsors(): 

585 return redirect(url_for("doaj.sponsors", **request.args), code=308) 

586 

587 

588@blueprint.route("/members") 

589def members(): 

590 return redirect(url_for("doaj.supporters", **request.args), code=308) 

591 

592 

593@blueprint.route('/features') 

594def features(): 

595 return redirect(url_for("doaj.xml", **request.args), code=308) 

596 

597 

598# @blueprint.route('/widgets') 

599# def old_widgets(): 

600# return redirect(url_for("doaj.widgets", **request.args), code=308) 

601 

602 

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) 

606 

607 

608@blueprint.route("/openurl/help") 

609def old_openurl(): 

610 return redirect(url_for("doaj.openurl", **request.args), code=308) 

611 

612 

613@blueprint.route("/faq") 

614def old_faq(): 

615 return redirect(url_for("doaj.faq", **request.args), code=308) 

616 

617 

618@blueprint.route("/publishers") 

619def publishers(): 

620 return redirect(url_for("doaj.guide", **request.args), code=308) 

621 

622 

623# Redirects necessitated by new templates 

624@blueprint.route("/password-reset/") 

625def new_password_reset(): 

626 return redirect(url_for('account.forgot'), code=301)