Coverage for portality / view / doaj.py: 65%

402 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-05-04 09:41 +0100

1import json 

2import os.path 

3import re 

4import urllib.error 

5import urllib.parse 

6import urllib.request 

7 

8from flask import Blueprint, request, make_response 

9from flask import render_template, abort, redirect, url_for, send_file, jsonify 

10from flask_login import current_user, login_required 

11 

12from portality.ui import exceptions as ui_exceptions 

13from portality.bll import exceptions, exceptions as bll_exceptions 

14from portality import constants 

15from portality import dao 

16from portality import models 

17from portality import store 

18from portality.bll import DOAJ 

19from portality.core import app 

20from portality.decorators import ssl_required, api_key_required 

21from portality.lcc import lcc_jstree 

22from portality.lib import plausible 

23from portality.ui.messages import Messages 

24from portality.ui import templates 

25from portality.bll import DOAJ 

26 

27# ~~DOAJ:Blueprint~~ 

28blueprint = Blueprint('doaj', __name__) 

29 

30 

31@blueprint.route("/") 

32def home(): 

33 news = models.News.latest(app.config.get("FRONT_PAGE_NEWS_ITEMS", 5)) 

34 recent_journals = models.Journal.recent(max=16) 

35 stats = DOAJ.siteService().site_statistics() 

36 return render_template(templates.PUBLIC_INDEX, news=news, recent_journals=recent_journals, statistics=stats) 

37 

38 

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

40def login(): 

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

42 

43 

44@blueprint.route("/dismiss_site_note") 

45def dismiss_site_note(): 

46 cont = request.values.get("continue") 

47 if cont is not None: 

48 resp = redirect(cont) 

49 else: 

50 resp = make_response() 

51 # set a cookie that lasts for one year 

52 resp.set_cookie(app.config.get("SITE_NOTE_KEY"), app.config.get("SITE_NOTE_COOKIE_VALUE"), 

53 max_age=app.config.get("SITE_NOTE_SLEEP"), samesite=None, secure=True) 

54 return resp 

55 

56 

57@blueprint.route("/news/") 

58def news(): 

59 # NOTE: On live this is also handled by the nginx redirect map, but this will strip those with parameters supplied 

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

61 

62 

63@blueprint.route("/fqw_hit", methods=['POST']) 

64def fqw_hit(): 

65 page = request.form.get('embedding_page') 

66 if page is not None: 

67 plausible.send_event(app.config.get('ANALYTICS_CATEGORY_FQW', 'FQW'), 

68 action=app.config.get('ANALYTICS_ACTION_FQW', 'hit'), 

69 label=request.form.get('embedding_page')) 

70 

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

72 return '', 204 

73 

74 

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

76def journals_search(): 

77 return render_template(templates.PUBLIC_JOURNAL_SEARCH, lcc_tree=lcc_jstree) 

78 

79 

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

81def articles_search(): 

82 return render_template(templates.PUBLIC_ARTICLE_SEARCH, lcc_tree=lcc_jstree) 

83 

84 

85@blueprint.route("/search", methods=['GET']) 

86def search(): 

87 # If there are URL params, check if we need to redirect to articles rather than journals 

88 if request.values: 

89 # Flat search the query params as string so we don't have to traverse all the way down the decoded json. 

90 if re.search(r'\"_type\"\s*:\s*\"article\"', request.values.get('source', '')): 

91 return redirect(url_for("doaj.articles_search"), 301) 

92 return redirect(url_for("doaj.journals_search"), 301) 

93 

94 

95@blueprint.route("/search", methods=['POST']) 

96def search_post(): 

97 """ Redirect a query from the box on the index page to the search page. """ 

98 if request.form.get('origin') != 'ui': 

99 abort(400) # bad request - we must receive searches from our own UI 

100 

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

102 if ref is None: 

103 abort(400) # Referrer is required 

104 

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

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

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

108 

109 if kw is None: 

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

111 

112 # lhs for journals, rhs for articles 

113 field_map = { 

114 "all": (None, None), 

115 "title": ("bibjson.title", "bibjson.title"), 

116 "abstract": (None, "bibjson.abstract"), 

117 "subject": ("index.classification", "index.classification"), 

118 "author": (None, "bibjson.author.name"), 

119 "issn": ("index.issn.exact", None), 

120 "publisher": ("bibjson.publisher.name", None), 

121 "country": ("index.country", None) 

122 } 

123 default_field_opts = field_map.get(field, None) 

124 default_field = None 

125 

126 route = "" 

127 if not ct or ct == "journals": 

128 route = url_for("doaj.journals_search") 

129 if default_field_opts: 

130 default_field = default_field_opts[0] 

131 elif ct == "articles": 

132 route = url_for("doaj.articles_search") 

133 if default_field_opts: 

134 default_field = default_field_opts[1] 

135 else: 

136 abort(400) 

137 

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

139 

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

141 

142 

143############################################# 

144 

145@blueprint.route("/csv") 

146@plausible.pa_event(app.config.get('ANALYTICS_CATEGORY_JOURNALCSV', 'JournalCSV'), 

147 action=app.config.get('ANALYTICS_ACTION_JOURNALCSV', 'Download')) 

148def csv_data(): 

149 svc = DOAJ.journalService() 

150 if current_user and not current_user.is_anonymous and current_user.has_role(constants.ROLE_PREMIUM_CSV): 

151 jc = svc.get_premium_csv() 

152 else: 

153 jc = svc.get_free_csv() 

154 

155 if jc is None: 

156 abort(404) 

157 

158 store_url = svc.get_temporary_url(jc) 

159 

160 if store_url.startswith("/"): 

161 store_url = "/store" + store_url 

162 

163 return redirect(store_url, code=307) 

164 

165 

166@blueprint.route("/sitemap.xml") 

167def sitemap_legacy(): 

168 return redirect(url_for('doaj.sitemap', suffix="0"), 301) 

169 

170@blueprint.route("/sitemap_index.xml") 

171def sitemap_index_legacy(): 

172 return redirect(url_for('doaj.sitemap', suffix="_index_0"), 301) 

173 

174@blueprint.route("/sitemap<suffix>.xml") 

175def sitemap(suffix): 

176 """ 

177 This route handles both sitemaps, of the form /sitemapN.xml, and sitemap indexes, of the form /sitemap_index_N.xml 

178 :param suffix: 

179 :return: 

180 """ 

181 url = None 

182 if suffix.startswith("_index_"): 

183 n = suffix[len("_index_"):] 

184 url = models.Cache.get_sitemap_index(n) 

185 else: 

186 url = models.Cache.get_sitemap(suffix) 

187 if url is None: 

188 abort(404) 

189 if url.startswith("/"): 

190 url = "/store" + url 

191 return redirect(url, code=307) 

192 

193 

194@blueprint.route("/public-data-dump/<record_type>") 

195@login_required 

196@plausible.pa_event(app.config.get('ANALYTICS_CATEGORY_PUBLICDATADUMP', 'PublicDataDump'), 

197 action=app.config.get('ANALYTICS_ACTION_PUBLICDATADUMP', 'Download')) 

198def public_data_dump_redirect(record_type): 

199 if not current_user.has_role(constants.ROLE_PUBLIC_DATA_DUMP) and not current_user.has_role(constants.ROLE_PREMIUM_PDD): 

200 abort(404) 

201 

202 svc = DOAJ.publicDataDumpService() 

203 if current_user.has_role(constants.ROLE_PREMIUM_PDD): 

204 dd = svc.get_premium_dump() 

205 else: 

206 dd = svc.get_free_dump() 

207 

208 if dd is None: 

209 abort(404) 

210 

211 store_url = svc.get_temporary_url(dd, record_type) 

212 

213 if store_url.startswith("/"): 

214 store_url = "/store" + store_url 

215 

216 return redirect(store_url, code=307) 

217 

218@blueprint.route("/store/<container>/<dir>/<filename>") 

219def get_from_local_store_dir(container, dir, filename): 

220 file = os.path.join(dir, filename) 

221 return get_from_local_store(container, file) 

222 

223@blueprint.route("/store/<container>/<filename>") 

224def get_from_local_store(container, filename): 

225 if not app.config.get("STORE_LOCAL_EXPOSE", False): 

226 abort(404) 

227 

228 from portality import store 

229 localStore = store.StoreFactory.get(None) 

230 file_handle = localStore.get(container, filename) 

231 return send_file(file_handle, mimetype="application/octet-stream", as_attachment=True, 

232 download_name=os.path.basename(filename)) 

233 

234 

235@blueprint.route('/autocomplete/<doc_type>/<field_name>', methods=["GET", "POST"]) 

236def autocomplete(doc_type, field_name): 

237 prefix = request.args.get('q', '') 

238 if not prefix: 

239 return jsonify({'suggestions': [{"id": "", 

240 "text": "No results found"}]}) # select2 does not understand 400, which is the correct code here... 

241 

242 m = models.lookup_model(doc_type) 

243 if not m: 

244 return jsonify({'suggestions': [{"id": "", 

245 "text": "No results found"}]}) # select2 does not understand 404, which is the correct code here... 

246 

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

248 

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

250 

251 suggs = [] 

252 if filter_field is None: 

253 suggs = m.autocomplete(field_name, prefix, size=size) 

254 else: 

255 suggs = m.advanced_autocomplete(filter_field, field_name, prefix, size=size, prefix_only=False) 

256 

257 return jsonify({'suggestions': suggs}) 

258 # you shouldn't return lists top-level in a JSON response: 

259 # http://flask.pocoo.org/docs/security/#json-security 

260 

261 

262def is_issn_by_identifier(identifier): 

263 return len(identifier) == 9 

264 

265 

266def find_correct_redirect_identifier(identifier, bibjson) -> str: 

267 """ 

268 return None if identifier is correct and no redirect is needed 

269 

270 :param identifier: 

271 :param bibjson: 

272 :return: 

273 """ 

274 if is_issn_by_identifier(identifier): # the journal is referred to by an ISSN 

275 # if there is an E-ISSN (and it's not the one in the request), redirect to it 

276 eissn = bibjson.get_one_identifier(bibjson.E_ISSN) 

277 if eissn and identifier != eissn: 

278 return eissn 

279 

280 # 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 

281 if not eissn: 

282 pissn = bibjson.get_one_identifier(bibjson.P_ISSN) 

283 if pissn and identifier != pissn: 

284 return pissn 

285 

286 # The journal has neither a PISSN or an EISSN. Yet somehow 

287 # issn_ref is True, the request was referring to the journal 

288 # by its ISSN. Not sure how this could ever happen, but just 

289 # continue loading the data and do nothing else in such a 

290 # case. 

291 

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

293 

294 # if there is an E-ISSN, redirect to it 

295 # if not, but there is a P-ISSN, redirect to it 

296 # if neither ISSN is present, continue loading the page 

297 issn = bibjson.get_one_identifier(bibjson.E_ISSN) 

298 if not issn: 

299 issn = bibjson.get_one_identifier(bibjson.P_ISSN) 

300 if issn: 

301 return issn 

302 

303 # let it continue loading if we only have the hex UUID for the journal (no ISSNs) 

304 # and the user is referring to the toc page via that ID 

305 

306 

307@blueprint.route("/toc/<identifier>") 

308def toc(identifier=None): 

309 """ Table of Contents page for a journal. identifier may be the journal id or an issn """ 

310 # If this route is changed, update JOURNAL_TOC_URL_FRAG in settings.py (partial ToC page link for journal CSV) 

311 if identifier is None: 

312 abort(400) 

313 

314 journalSvc = DOAJ.journalService() 

315 try: 

316 journal = journalSvc.find_best(identifier) 

317 except bll_exceptions.ArgumentException: 

318 abort(400) 

319 except bll_exceptions.TooManyJournals: 

320 abort(500) 

321 

322 if journal is None: 

323 abort(404) 

324 

325 if journal.is_in_doaj() is False: 

326 raise ui_exceptions.JournalWithdrawn() 

327 

328 # journal = find_toc_journal_by_identifier(identifier) 

329 bibjson = journal.bibjson() 

330 real_identifier = find_correct_redirect_identifier(identifier, bibjson) 

331 if real_identifier: 

332 return redirect(url_for('doaj.toc', identifier=real_identifier), 301) 

333 else: 

334 # now render all that information 

335 return render_template(templates.PUBLIC_TOC_MAIN, journal=journal, bibjson=bibjson, tab="main") 

336 

337 

338@blueprint.route("/toc/articles/<identifier>") 

339def toc_articles_legacy(identifier=None): 

340 return redirect(url_for('doaj.toc_articles', identifier=identifier, volume=1, issue=1), 301) 

341 

342 

343@blueprint.route("/toc/<identifier>/articles") 

344def toc_articles(identifier=None): 

345 if identifier is None: 

346 abort(400) 

347 

348 journalSvc = DOAJ.journalService() 

349 try: 

350 journal = journalSvc.find_best(identifier) 

351 except bll_exceptions.ArgumentException: 

352 abort(400) 

353 except bll_exceptions.TooManyJournals: 

354 abort(500) 

355 

356 if journal is None: 

357 abort(404) 

358 

359 if journal.is_in_doaj() is False: 

360 raise ui_exceptions.JournalWithdrawn() 

361 

362 bibjson = journal.bibjson() 

363 articles_no = journal.article_stats()["total"] 

364 real_identifier = find_correct_redirect_identifier(identifier, bibjson) 

365 if real_identifier: 

366 return redirect(url_for('doaj.toc_articles', identifier=real_identifier), 301) 

367 else: 

368 return render_template(templates.PUBLIC_TOC_ARTICLES, journal=journal, bibjson=bibjson, articles_no=articles_no, tab="articles") 

369 

370 

371# ~~->Article:Page~~ 

372@blueprint.route("/article/<identifier>") 

373def article_page(identifier=None): 

374 # identifier must be the article id 

375 article = models.Article.pull(identifier) 

376 

377 if article is None: 

378 article = models.ArticleTombstone.pull(identifier) 

379 if article: 

380 raise ui_exceptions.TombstoneArticle() 

381 else: 

382 abort(404, description=Messages.ARTICLE_NOT_FOUND) 

383 

384 if not article.is_in_doaj(): 

385 raise ui_exceptions.ArticleFromWithdrawnJournal() 

386 

387 # find the related journal record 

388 journal = article.get_journal() 

389 if journal is None: 

390 app.logger.exception(Messages.ARTICLE_ABANDONED_LOG.format(article_id=article.id)) 

391 abort(500, description=Messages.ARTICLE_ABANDONED_PUBLIC) 

392 if journal.is_in_doaj() is False: 

393 raise ui_exceptions.ArticleFromWithdrawnJournal() 

394 

395 # issns = article.bibjson().issns() 

396 # more_issns = article.bibjson().journal_issns 

397 # for issn in issns + more_issns: 

398 # journals = models.Journal.find_by_issn(issn) 

399 # if len(journals) == 0: 

400 # app.logger.exception(Messages.ARTICLE_ABANDONED_LOG.format(article_id=article.id)) 

401 # abort(500, description=Messages.ARTICLE_ABANDONED_PUBLIC) 

402 # try: 

403 # journal = models.Journal.get_active_journal(journals) 

404 # except exceptions.TooManyJournals: 

405 # app.logger.exception(Messages.TOO_MANY_JOURNALS_LOG.format(identifier=identifier)) 

406 # abort(500, description=Messages.TOO_MANY_JOURNALS.format(identifier=identifier)) 

407 # except exceptions.JournalWithdrawn: 

408 # raise exceptions.ArticleFromWithdrawnJournal 

409 

410 return render_template(templates.PUBLIC_ARTICLE, article=article, journal=journal, page={"highlight" : True}) 

411 

412 

413############################################################### 

414# The various static endpoints 

415############################################################### 

416 

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

418def google_webmaster_tools(): 

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

420 

421 

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

423def accessibility(): 

424 return render_template(templates.STATIC_PAGE, page_frag="/legal/accessibility.html") 

425 

426 

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

428def privacy(): 

429 return render_template(templates.STATIC_PAGE, page_frag="/legal/privacy.html") 

430 

431 

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

433def contact(): 

434 return render_template(templates.STATIC_PAGE, page_frag="/legal/contact.html") 

435 

436 

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

438def terms(): 

439 return render_template(templates.STATIC_PAGE, page_frag="/legal/terms.html") 

440 

441 

442@blueprint.route("/code-of-conduct/") 

443def conduct(): 

444 """ 

445 ~~Conduct:WebRoute~~ 

446 """ 

447 return render_template(templates.STATIC_PAGE, page_frag="/legal/code-of-conduct.html") 

448 

449 

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

451def media(): 

452 """ 

453 ~~Media:WebRoute~~ 

454 """ 

455 return render_template(templates.STATIC_PAGE, page_frag="/legal/media.html") 

456 

457 

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

459def support(): 

460 return render_template(templates.STATIC_PAGE, page_frag="/support/index.html") 

461 

462 

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

464def sponsors(): 

465 return render_template(templates.STATIC_PAGE, page_frag="/support/sponsors.html") 

466 

467 

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

469def publisher_supporters(): 

470 return render_template(templates.STATIC_PAGE, page_frag="/support/publisher-supporters.html") 

471 

472 

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

474def supporters(): 

475 return render_template(templates.STATIC_PAGE, page_frag="/support/supporters.html") 

476 

477 

478@blueprint.route("/support/funders/") 

479def funders(): 

480 return render_template(templates.STATIC_PAGE, page_frag="/support/funders.html") 

481 

482 

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

484def application_thanks(): 

485 return render_template(templates.STATIC_PAGE, page_frag="/support/thank-you.html") 

486 

487 

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

489def guide(): 

490 return render_template(templates.STATIC_PAGE, page_frag="/apply/guide.html") 

491 

492 

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

494def transparency(): 

495 return render_template(templates.STATIC_PAGE, page_frag="/apply/transparency.html") 

496 

497 

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

499def why_index(): 

500 return render_template(templates.STATIC_PAGE, page_frag="/apply/why-index.html") 

501 

502 

503@blueprint.route("/apply/publisher-responsibilities/") 

504def publisher_responsibilities(): 

505 return render_template(templates.STATIC_PAGE, page_frag="/apply/publisher-responsibilities.html") 

506 

507 

508@blueprint.route("/apply/copyright-and-licensing/") 

509def copyright_and_licensing(): 

510 return render_template(templates.STATIC_PAGE, page_frag="/apply/copyright-and-licensing.html") 

511 

512 

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

514def oai_pmh(): 

515 return render_template(templates.STATIC_PAGE, page_frag="/docs/oai-pmh.html") 

516 

517 

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

519def docs(): 

520 major_version = app.config.get("CURRENT_API_MAJOR_VERSION") 

521 return redirect(url_for('api_v' + major_version + '.docs')) 

522 

523 

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

525def xml(): 

526 return render_template(templates.STATIC_PAGE, page_frag="/docs/xml.html") 

527 

528 

529@blueprint.route("/docs/widgets/") 

530def widgets(): 

531 return render_template(templates.STATIC_PAGE, page_frag="/docs/widgets.html", base_url=app.config.get('BASE_URL')) 

532 

533 

534@blueprint.route("/docs/public-data-dump/") 

535def public_data_dump(): 

536 if current_user and not current_user.is_anonymous and ( 

537 current_user.has_role(constants.ROLE_PUBLIC_DATA_DUMP) or current_user.has_role(constants.ROLE_PREMIUM_PDD)): 

538 dds = DOAJ.publicDataDumpService() 

539 if current_user.has_role(constants.ROLE_PREMIUM_PDD): 

540 dd = dds.get_premium_dump() 

541 else: 

542 dd = dds.get_free_dump() 

543 return render_template(templates.STATIC_PAGE, page_frag="/docs/pdd-access.html", pdd=dd) 

544 return render_template(templates.STATIC_PAGE, page_frag="/docs/pdd-contact.html") 

545 

546 

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

548def openurl(): 

549 return render_template(templates.STATIC_PAGE, page_frag="/docs/openurl.html") 

550 

551 

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

553def faq(): 

554 return render_template(templates.STATIC_PAGE, page_frag="/docs/faq.html") 

555 

556@blueprint.route("/docs/journal-csv") 

557def journal_csv(): 

558 svc = DOAJ.journalService() 

559 if not current_user.is_anonymous and current_user.has_role(constants.ROLE_PREMIUM_CSV): 

560 jc = svc.get_premium_csv() 

561 else: 

562 jc = svc.get_free_csv() 

563 return render_template(templates.STATIC_PAGE, page_frag="/docs/journal-csv.html", jc=jc) 

564 

565@blueprint.route("/docs/premium") 

566def premium(): 

567 return render_template(templates.STATIC_PAGE, page_frag="/docs/premium.html") 

568 

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

570def about(): 

571 return render_template(templates.STATIC_PAGE, page_frag="/about/index.html") 

572 

573 

574 

575@blueprint.route("/at-20/") 

576def at_20(): 

577 return render_template(templates.STATIC_PAGE, page_frag="/about/at-20.html") 

578 

579 

580 

581 

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

583def ambassadors(): 

584 return render_template(templates.STATIC_PAGE, page_frag="/about/ambassadors.html") 

585 

586 

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

588def abc(): 

589 return render_template(templates.STATIC_PAGE, page_frag="/about/advisory-board-council.html") 

590 

591 

592@blueprint.route("/about/editorial-policy-advisory-group/") 

593def epag(): 

594 return render_template(templates.STATIC_PAGE, page_frag="/about/editorial-policy-advisory-group.html") 

595 

596 

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

598def volunteers(): 

599 return render_template(templates.STATIC_PAGE, page_frag="/about/volunteers.html") 

600 

601 

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

603def team(): 

604 return render_template(templates.STATIC_PAGE, page_frag="/about/team.html") 

605 

606 

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

608def preservation(): 

609 return render_template(templates.STATIC_PAGE, page_frag="/preservation/index.html") 

610 

611 

612# LEGACY ROUTES 

613@blueprint.route("/subjects") 

614def subjects(): 

615 # return render_template("doaj/subjects.html", subject_page=True, lcc_jstree=json.dumps(lcc_jstree)) 

616 return redirect(url_for("doaj.journals_search"), 301) 

617 

618 

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

620def old_application(): 

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

622 

623 

624@blueprint.route("/<cc>/mejorespracticas") 

625@blueprint.route("/<cc>/boaspraticas") 

626@blueprint.route("/<cc>/bestpractice") 

627@blueprint.route("/<cc>/editionsavante") 

628@blueprint.route("/bestpractice") 

629@blueprint.route("/oainfo") 

630def bestpractice(cc=None): 

631 return redirect(url_for("doaj.transparency", **request.args), code=308) 

632 

633 

634@blueprint.route("/membership") 

635def membership(): 

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

637 

638 

639@blueprint.route("/publishermembers") 

640def old_sponsors(): 

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

642 

643 

644@blueprint.route("/members") 

645def members(): 

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

647 

648 

649@blueprint.route('/features') 

650def features(): 

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

652 

653 

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

655# def old_widgets(): 

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

657 

658 

659# @blueprint.route("/public-data-dump/<record_type>") 

660# def old_public_data_dump(record_type): 

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

662 

663 

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

665def old_openurl(): 

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

667 

668 

669@blueprint.route("/faq") 

670def old_faq(): 

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

672 

673 

674@blueprint.route("/publishers") 

675def publishers(): 

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

677 

678 

679# Redirects necessitated by new templates 

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

681def new_password_reset(): 

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

683 

684 

685@blueprint.route("/u/<alias>") 

686@plausible.pa_event(app.config.get('ANALYTICS_CATEGORY_URLSHORT', 'Urlshort'), 

687 action=app.config.get('ANALYTICS_ACTION_URLSHORT_REDIRECT', 'Redirect')) 

688def shortened_url(alias): 

689 short = DOAJ.shortUrlService().find_url_by_alias(alias) 

690 if short: 

691 return redirect(short.url) 

692 

693 app.logger.debug(f"Shortened URL not found: [{alias}]") 

694 abort(404)