Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create a URL which initiates a library search #871

Merged
merged 9 commits into from Nov 10, 2021
59 changes: 59 additions & 0 deletions TWLight/forms.py
@@ -0,0 +1,59 @@
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Hidden, Submit, Layout

from django.conf import settings
from django.contrib.auth.models import User

from django import forms
from django.utils.translation import get_language, gettext as _


class EdsSearchForm(forms.Form):
""" """

lang = forms.ChoiceField(choices=settings.LANGUAGES)
schemaId = forms.CharField()
custid = forms.CharField()
groupid = forms.CharField()
profid = forms.CharField()
scope = forms.CharField()
site = forms.CharField()
direct = forms.CharField()
authtype = forms.CharField()
bquery = forms.CharField()

def __init__(self, *args, **kwargs):
language_code = get_language()
lang = language_code
bquery = kwargs.pop("bquery", None)

super().__init__(*args, **kwargs)
if language_code == "pt":
lang = "pt-pt"
elif language_code == "zh-hans":
lang = "zh-cn"
elif language_code == "zh-hant":
lang = "zh-tw"
self.helper = FormHelper()
self.helper.form_id = "search"
self.helper.form_action = "https://searchbox.ebsco.com/search/"
self.helper.form_method = "GET"
self.helper.label_class = "sr-only"
self.helper.layout = Layout(
Hidden("bquery", bquery),
Hidden("lang", lang),
Hidden("schemaId", "search"),
Hidden("custid", "ns253359"),
Hidden("groupid", "main"),
Hidden("profid", "eds"),
Hidden("scope", "site"),
Hidden("site", "eds-live"),
Hidden("direct", "true"),
Hidden("authtype", "url"),
Submit(
"submit",
# Translators: Shown in the search button.
_("Search"),
css_class="btn eds-search-button",
),
)
3 changes: 3 additions & 0 deletions TWLight/settings/base.py
Expand Up @@ -271,6 +271,9 @@ def show_toolbar(request):
# Overwrite django default SESSION_COOKIE_AGE
SESSION_COOKIE_AGE = 259200

# Add header to work with EDS
SECURE_REFERRER_POLICY = "origin"

# INTERNATIONALIZATION CONFIGURATION
# ------------------------------------------------------------------------------

Expand Down
9 changes: 9 additions & 0 deletions TWLight/static/css/new-local.css
Expand Up @@ -390,6 +390,15 @@ COMMON CONTENT BLOCK CSS
padding: 0.5rem 1rem;
}

/*
----------------------------------------------------------------------------
NOSCRIPT CSS
--------------------------------------------------------------------------
*/
#noscript {
color: red;
}

/*
----------------------------------------------------------------------------
HOMEPAGE CAROUSEL CSS
Expand Down
23 changes: 23 additions & 0 deletions TWLight/templates/eds_search_endpoint.html
@@ -0,0 +1,23 @@
{% extends "new_base.html" %}
{% load i18n %}
{% load crispy_forms_tags %}
{% load twlight_perms %}
{% block content %}
{% comment %}Translators: On a special 'link to search' page, this message is shown if JavaScript is disabled.{% endcomment %}
<p id="noscript">{% trans "JavaScript is disabled; use the button below to continue." %}</p>
{% crispy form %}
{% endblock %}

{% block javascript %}
<script>
// On page load
document.addEventListener("DOMContentLoaded", function(event) {
// Delete noscript element.
// <noscript> text was not showing up in testing with the noscript extension.
var noscript = document.getElementById("noscript");
noscript.parentNode.removeChild(noscript);
// Submit the form.
document.createElement('form').submit.call(document.getElementById('search'));
});
</script>
{% endblock %}
65 changes: 65 additions & 0 deletions TWLight/tests.py
Expand Up @@ -34,6 +34,7 @@
PartnerCoordinatorOrSelf,
CoordinatorsOnly,
EditorsOnly,
EligibleEditorsOnly,
SelfOnly,
ToURequired,
EmailRequired,
Expand Down Expand Up @@ -81,6 +82,10 @@ class TestEditorsOnly(EditorsOnly, DispatchProvider):
pass


class TestEligibleEditorsOnly(EligibleEditorsOnly, DispatchProvider):
pass


class TestSelfOnly(SelfOnly, ObjGet, DispatchProvider):
pass

Expand Down Expand Up @@ -267,6 +272,66 @@ def test_editors_only_3(self):
with self.assertRaises(PermissionDenied):
test.dispatch(req)

def test_eligible_editors_only_1(self):
"""
EligibleEditorsOnly allows eligible editors.
"""
user = UserFactory()
_ = EditorFactory(user=user)
_.wp_bundle_eligible = True

req = RequestFactory()
req.user = user

test = TestEligibleEditorsOnly()
test.dispatch(req)

def test_eligible_editors_only_2(self):
"""
EligibleEditorsOnly does *not* allow superusers who aren't editors.
"""
user = UserFactory(is_superuser=True)
self.assertFalse(hasattr(user, "editor"))

req = RequestFactory()
req.user = user

test = TestEligibleEditorsOnly()
with self.assertRaises(PermissionDenied):
test.dispatch(req)

def test_eligible_editors_only_3(self):
"""
EligibleEditorsOnly does not allow non-superusers who aren't editors.
"""
user = UserFactory(is_superuser=False)
self.assertFalse(hasattr(user, "editor"))

req = RequestFactory()
req.user = user

test = TestEligibleEditorsOnly()
with self.assertRaises(PermissionDenied):
test.dispatch(req)

def test_eligible_editors_only_4(self):
"""
EligibleEditorsOnly redirects ineligible editors.
"""
user = UserFactory(is_superuser=False)
self.assertFalse(hasattr(user, "editor"))
_ = EditorFactory(user=user)
_.wp_bundle_eligible = False

req = RequestFactory()
req.user = user

test = TestEligibleEditorsOnly()

resp = test.dispatch(req)
# This test doesn't deny permission; it sends people to my_library.
self.assertTrue(isinstance(resp, HttpResponseRedirect))

def test_self_only_1(self):
"""
SelfOnly allows users who are also the object returned by get_object.
Expand Down
7 changes: 6 additions & 1 deletion TWLight/urls.py
Expand Up @@ -28,7 +28,7 @@
from TWLight.users.views import TermsView
from TWLight.ezproxy.urls import urlpatterns as ezproxy_urls

from .views import NewHomePageView
from .views import NewHomePageView, SearchEndpointFormView

handler400 = "TWLight.views.bad_request"

Expand Down Expand Up @@ -88,6 +88,11 @@
url(r"^contact/$", ContactUsView.as_view(), name="contact"),
url(r"^$", NewHomePageView.as_view(), name="homepage"),
url(r"^about/$", TemplateView.as_view(template_name="about.html"), name="about"),
url(
r"^search/$",
login_required(SearchEndpointFormView.as_view()),
name="search",
),
]

# Enable debug_toolbar if configured
Expand Down
13 changes: 10 additions & 3 deletions TWLight/users/oauth.py
Expand Up @@ -293,12 +293,15 @@ def get(self, request, *args, **kwargs):
next = query_dict.pop("next")
# Set the return url to the value of 'next'. Basic.
return_url = next[0]
# Pop the 'from_homepage' parameter out of the QueryDict.
# We don't need it here.
query_dict.pop("from_homepage", None)
# If there is anything left in the QueryDict after popping
# 'next', append it to the return url. This preserves state
# for filtered lists and redirected form submissions like
# the partner suggestion form.
if query_dict:
return_url += "?" + urlencode(query_dict)
return_url += "&" + urlencode(query_dict)
logger.info(
"User is already authenticated. Sending them on "
'for post-login redirection per "next" parameter.'
Expand Down Expand Up @@ -332,7 +335,8 @@ def get(self, request, *args, **kwargs):
next = query_dict.pop("next")
# Set the return url to the value of 'next'. Basic.
return_url = next[0]
from_homepage = query_dict.get("from_homepage", None)
# Pop the 'from_homepage' parameter out of the QueryDict.
from_homepage = query_dict.pop("from_homepage", None)

if from_homepage:
logger.info("Logging in from homepage, redirecting to Meta login")
Expand Down Expand Up @@ -513,12 +517,15 @@ def get(self, request, *args, **kwargs):
next = query_dict.pop("next")
# Set the return url to the value of 'next'. Basic.
return_url = next[0]
# Pop the 'from_homepage' parameter out of the QueryDict.
# We don't need it here.
query_dict.pop("from_homepage", None)
# If there is anything left in the QueryDict after popping
# 'next', append it to the return url. This preserves state
# for filtered lists and redirected form submissions like
# the partner suggestion form.
if query_dict:
return_url += "?" + urlencode(query_dict)
return_url += "&" + urlencode(query_dict)
logger.info(
"User authenticated. Sending them on for "
'post-login redirection per "next" parameter.'
Expand Down
25 changes: 25 additions & 0 deletions TWLight/view_mixins.py
Expand Up @@ -21,6 +21,7 @@
from TWLight.applications.models import Application
from TWLight.resources.models import Partner
from TWLight.users.models import Editor
from TWLight.users.helpers.editor_data import editor_bundle_eligible
from TWLight.users.groups import COORDINATOR_GROUP_NAME, RESTRICTED_GROUP_NAME

import logging
Expand Down Expand Up @@ -205,6 +206,30 @@ def dispatch(self, request, *args, **kwargs):
return super(EditorsOnly, self).dispatch(request, *args, **kwargs)


class EligibleEditorsOnly(object):
"""
Restricts visibility to:
* Eligible Editors.

Raises Permission denied for non-editors.
Redirects to my_library with message for ineligible editors.
"""

def dispatch(self, request, *args, **kwargs):
user = request.user
if not test_func_editors_only(user):
messages.add_message(
request,
messages.WARNING,
"You must be a coordinator or an editor to do that.",
)
raise PermissionDenied
elif not editor_bundle_eligible(user.editor):
# Send ineligible editors to my_library for info on eligibility.
return HttpResponseRedirect(reverse_lazy("users:my_library"))
return super().dispatch(request, *args, **kwargs)


def test_func_tou_required(user):
try:
return user.is_superuser or user.userprofile.terms_of_use
Expand Down
20 changes: 19 additions & 1 deletion TWLight/views.py
@@ -1,7 +1,8 @@
# -*- coding: utf-8 -*-
import json

from django.views.generic import TemplateView
from django.views.generic import RedirectView, TemplateView
from django.views.generic.edit import FormView
from django.views import View
from django.conf import settings
from django.contrib.messages import get_messages
Expand All @@ -17,6 +18,9 @@
from TWLight.resources.models import Partner, PartnerLogo
from TWLight.resources.helpers import get_partner_description, get_tag_dict

from .forms import EdsSearchForm
from .view_mixins import EligibleEditorsOnly

import logging

from django.views.defaults import ERROR_400_TEMPLATE_NAME, ERROR_PAGE_TEMPLATE
Expand Down Expand Up @@ -125,6 +129,20 @@ def get(self, request):
return render(request, "homepage.html", context)


class SearchEndpointFormView(EligibleEditorsOnly, FormView):
"""
Allows persistent links to EDS searches with referring URL authentication.
"""

def get_form_kwargs(self, **kwargs):
kwargs = super().get_form_kwargs()
kwargs["bquery"] = self.request.GET.get("q")
return kwargs

template_name = "eds_search_endpoint.html"
form_class = EdsSearchForm


@sensitive_variables()
@requires_csrf_token
def bad_request(request, exception, template_name=ERROR_400_TEMPLATE_NAME):
Expand Down
2 changes: 0 additions & 2 deletions conf/local.nginx.conf
Expand Up @@ -53,8 +53,6 @@ server {
# redirects, we set the Host: header above already.
proxy_redirect off;
proxy_pass http://twlight;
# Add header to work with EDS
add_header Referrer-Policy 'origin';
}

error_page 500 502 503 504 /500.html;
Expand Down
2 changes: 0 additions & 2 deletions conf/production.nginx.conf
Expand Up @@ -94,8 +94,6 @@ server {
# redirects, we set the Host: header above already.
proxy_redirect off;
proxy_pass http://twlight;
# Add header to work with EDS
add_header Referrer-Policy 'origin';
}

proxy_intercept_errors on;
Expand Down
2 changes: 0 additions & 2 deletions conf/staging.nginx.conf
Expand Up @@ -95,8 +95,6 @@ server {
# redirects, we set the Host: header above already.
proxy_redirect off;
proxy_pass http://twlight;
# Add header to work with EDS
add_header Referrer-Policy 'origin';
}

proxy_intercept_errors on;
Expand Down