Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ TIME_ZONE=America/Los_Angeles
CAT_ENGINE=browser

# Optional overrides (defaults live in settings.py):
# SECRET_KEY= # Django signing key; REQUIRED in production (falls
# # back to a locally generated secret_key.py file)
# GOOGLE_ANALYTICS_ID= # GA measurement ID; empty disables the snippet
# CAT_API_URL= # R-based CAT server; defaults to the production instance
# AWS_STORAGE_BUCKET_NAME= # required only when AWS_INSTANCE=True
# TRUSTED_ORIGINS= # CSRF trusted origins, same bracket format as ALLOWED_HOSTS
Expand Down
19 changes: 11 additions & 8 deletions webcdi/cdi_forms/templates/cdi_forms/administration_base.html
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,17 @@
</script>

{% block extra_css %}{% endblock %}
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=328947117"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '328947117');
</script>
{% if GOOGLE_ANALYTICS_ID %}
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id={{ GOOGLE_ANALYTICS_ID }}"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());

gtag('config', '{{ GOOGLE_ANALYTICS_ID }}');
</script>
{% endif %}
</head>

<body data-spy="scroll" data-target="#navbar">
Expand Down
8 changes: 5 additions & 3 deletions webcdi/cdi_forms/templates/cdi_forms/cdi_base.html
Original file line number Diff line number Diff line change
Expand Up @@ -57,15 +57,17 @@
</script>

{% block extra_css %}{% endblock %}
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=328947117"></script>
{% if GOOGLE_ANALYTICS_ID %}
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id={{ GOOGLE_ANALYTICS_ID }}"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());

gtag('config', '328947117');
gtag('config', '{{ GOOGLE_ANALYTICS_ID }}');
</script>
{% endif %}
</head>

<body data-spy="scroll" data-target="#navbar">
Expand Down
15 changes: 1 addition & 14 deletions webcdi/cdi_forms/views/administration_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,20 +70,7 @@ def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
ctx["gift_code"] = None
ctx["gift_amount"] = None
if self.object.study.allow_payment and self.object.bypass is None:
amazon_urls = {
"English": {
"redeem_url": "http://www.amazon.com/redeem",
"legal_url": "http://www.amazon.com/gc-legal",
},
"Spanish": {
"redeem_url": "http://www.amazon.com/gc/redeem/?language=es_US",
"legal_url": "http://www.amazon.com/gc-legal/?language=es_US",
},
"French Quebec": {
"redeem_url": "http://www.amazon.ca/gc/redeem/?language=fr_CA",
"legal_url": "http://www.amazon.ca/gc-legal/?language=fr_CA",
},
}
amazon_urls = settings.AMAZON_GIFT_CARD_URLS
url_obj = amazon_urls.get(self.object.study.instrument.language)
if url_obj is None:
url_obj = amazon_urls.get("English")
Expand Down
18 changes: 10 additions & 8 deletions webcdi/researcher_UI/templates/researcher_UI/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,17 @@
<script src="{% static 'cdi_forms/moment.js' %}"></script>
<style>
</style>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=328947117"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
{% if GOOGLE_ANALYTICS_ID %}
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id={{ GOOGLE_ANALYTICS_ID }}"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());

gtag('config', '328947117');
</script>
gtag('config', '{{ GOOGLE_ANALYTICS_ID }}');
</script>
{% endif %}
{% block extra_head %}{% endblock %}

</head>
Expand Down
6 changes: 6 additions & 0 deletions webcdi/researcher_UI/tests/test_views/ajax_views.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from django.contrib.auth.models import User
from django.test import TestCase, tag
from django.urls import reverse

Expand All @@ -6,6 +7,9 @@

class AjaxDemographicFormsTest(TestCase):
def setUp(self):
# these endpoints are researcher-only (LoginRequiredMixin)
self.user = User.objects.create_user(username="researcher", password="secret")
self.client.force_login(self.user)
self.url = reverse("researcher_ui:get_demographic_forms")
instrument_family = InstrumentFamily.objects.create(
name="BigCats", chargeable=True
Expand All @@ -32,6 +36,8 @@ def test_get_invalid_insrument(self):

class AjaxChargeStatusTest(TestCase):
def setUp(self):
self.user = User.objects.create_user(username="researcher", password="secret")
self.client.force_login(self.user)
instrument_family = InstrumentFamily.objects.create(
name="BigCats", chargeable=True
)
Expand Down
21 changes: 21 additions & 0 deletions webcdi/researcher_UI/tests/test_views/download_data_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ def setUp(self):
self.user = User.objects.create_user(
username="PaulMcCartney", password="PaulMcCartney"
)
# the clinical PDF exposes child data; the view now requires the
# owning researcher to be logged in
self.client.force_login(self.user)

instrument = Instrument.objects.filter(
language="English",
Expand Down Expand Up @@ -71,3 +74,21 @@ def test_get_no_template(self):
)
)
self.assertEqual(response.status_code, 302)

def test_anonymous_is_redirected(self):
# child data must not be reachable without logging in
self.client.logout()
response = self.client.get(
f'{reverse("researcher_ui:pdf_summary", kwargs={"pk": self.study.id})}{self.ids}'
)
self.assertEqual(response.status_code, 302)
self.assertIn("/accounts/login", response.url)

def test_other_researcher_cannot_access(self):
# a logged-in researcher cannot read another researcher's study
other = User.objects.create_user(username="RingoStarr", password="RingoStarr")
self.client.force_login(other)
response = self.client.get(
f'{reverse("researcher_ui:pdf_summary", kwargs={"pk": self.study.id})}{self.ids}'
)
self.assertEqual(response.status_code, 404)
5 changes: 3 additions & 2 deletions webcdi/researcher_UI/views/ajax_views.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core import serializers
from django.http import HttpResponse, JsonResponse
from django.views.generic import DetailView

from researcher_UI.models import Instrument


class AjaxDemographicForms(DetailView):
class AjaxDemographicForms(LoginRequiredMixin, DetailView):
def get(self, request):
pk = request.GET["id"]
try:
Expand All @@ -19,7 +20,7 @@ def get(self, request):
return HttpResponse(data, content_type="application/json")


class AjaxChargeStatus(DetailView):
class AjaxChargeStatus(LoginRequiredMixin, DetailView):
def get(self, request):
pk = request.GET["id"]

Expand Down
7 changes: 6 additions & 1 deletion webcdi/researcher_UI/views/download_data_views.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import logging

from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpResponse
from django.shortcuts import redirect
from django.template.loader import get_template
Expand All @@ -15,9 +16,13 @@
logger = logging.getLogger("debug")


class PDFAdministrationDetailView(WeasyTemplateResponseMixin, DetailView):
class PDFAdministrationDetailView(LoginRequiredMixin, WeasyTemplateResponseMixin, DetailView):
model = Study

def get_queryset(self):
# clinical reports contain child data: only the owning researcher
return Study.objects.filter(researcher=self.request.user)

def get_template_names(self):
name = slugify(f"{self.object.instrument.verbose_name}")
template_name = f"researcher_UI/individual/{name}.html"
Expand Down
3 changes: 2 additions & 1 deletion webcdi/researcher_UI/views/profile.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from typing import Any

from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.forms import PasswordChangeForm
from django.contrib.auth.models import User
from django.contrib.auth.views import PasswordChangeView
Expand All @@ -14,7 +15,7 @@
from researcher_UI.forms import ProfileForm, ResearcherForm


class ProfileView(UpdateView):
class ProfileView(LoginRequiredMixin, UpdateView):
model = User
form_class = ProfileForm
template_name = "researcher_UI/profile.html"
Expand Down
1 change: 1 addition & 0 deletions webcdi/webcdi/context_processors.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ def home_page(request):
return {
"CONTACT_EMAIL": settings.CONTACT_EMAIL,
"MORE_INFO_LINK": settings.MORE_INFO_ADDRESS,
"GOOGLE_ANALYTICS_ID": settings.GOOGLE_ANALYTICS_ID,
}
26 changes: 1 addition & 25 deletions webcdi/webcdi/middleware.py
Original file line number Diff line number Diff line change
@@ -1,33 +1,9 @@
import re

from django.conf import settings
from django.core.exceptions import MiddlewareNotUsed
from django.http import HttpResponsePermanentRedirect, HttpResponseRedirect
from django.http import HttpResponsePermanentRedirect
from django.urls import resolve
from django.utils import translation
from django.utils.deprecation import MiddlewareMixin
from django.utils.http import url_has_allowed_host_and_scheme

EXEMPT_URLS = [re.compile(settings.LOGIN_URL.lstrip("/"))]
if hasattr(settings, "LOGIN_EXEMPT_URLS"):
EXEMPT_URLS += [re.compile(url) for url in settings.LOGIN_EXEMPT_URLS]


class LoginRequiredMiddleware(MiddlewareMixin):
def process_request(self, request):
assert hasattr(request, "user"), "The Login Required Middleware"
if not request.user.is_authenticated:
path = request.path_info.lstrip("/")
if not any(m.match(path) for m in EXEMPT_URLS):
redirect_to = settings.LOGIN_URL
# 'next' variable to support redirection to attempted page after login
if len(path) > 0 and url_has_allowed_host_and_scheme(
url=request.path_info, allowed_hosts=request.get_host()
):
redirect_to = f"{settings.LOGIN_URL}?next={request.path_info}"

return HttpResponseRedirect(redirect_to)


class AdminLocaleMiddleware(MiddlewareMixin):
def process_request(self, request):
Expand Down
53 changes: 35 additions & 18 deletions webcdi/webcdi/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,25 @@ def get_secret(secret_name, region_name="us-west-2"):
CONTACT_EMAIL = os.environ.get("CONTACT_EMAIL", "webcdi-contact@stanford.edu")
MORE_INFO_ADDRESS = os.environ.get("MORE_INFO_ADDRESS", "http://mb-cdi.stanford.edu/")

# Google Analytics measurement ID (empty string disables the snippet)
GOOGLE_ANALYTICS_ID = os.environ.get("GOOGLE_ANALYTICS_ID", "328947117")

# Gift-card redemption links shown to compensated participants
AMAZON_GIFT_CARD_URLS = {
"English": {
"redeem_url": "http://www.amazon.com/redeem",
"legal_url": "http://www.amazon.com/gc-legal",
},
"Spanish": {
"redeem_url": "http://www.amazon.com/gc/redeem/?language=es_US",
"legal_url": "http://www.amazon.com/gc-legal/?language=es_US",
},
"French Quebec": {
"redeem_url": "http://www.amazon.ca/gc/redeem/?language=fr_CA",
"legal_url": "http://www.amazon.ca/gc-legal/?language=fr_CA",
},
}

# CAT Server
CAT_API_BASE_URL = os.environ.get(
"CAT_API_URL", "http://cdicatapi-env.eba-c2knb6uj.us-west-2.elasticbeanstalk.com/"
Expand Down Expand Up @@ -180,12 +199,18 @@ def generate_secret_key(fname):


# SECURITY WARNING: keep the secret key used in production secret!
try:
from .secret_key import * # noqa
except ImportError:
SETTINGS_DIR = os.path.abspath(os.path.dirname(__file__))
generate_secret_key(os.path.join(SETTINGS_DIR, "secret_key.py"))
from .secret_key import * # noqa
# Prefer the environment (set SECRET_KEY on the EB environment / in .env);
# the generated-file fallback keeps existing deployments working but means
# each fresh instance mints its own key (invalidating sessions), so
# production should always set the env var.
SECRET_KEY = os.environ.get("SECRET_KEY")
if not SECRET_KEY:
try:
from .secret_key import * # noqa
except ImportError:
SETTINGS_DIR = os.path.abspath(os.path.dirname(__file__))
generate_secret_key(os.path.join(SETTINGS_DIR, "secret_key.py"))
from .secret_key import * # noqa

DATA_UPLOAD_MAX_NUMBER_FIELDS = 10240

Expand Down Expand Up @@ -229,7 +254,6 @@ def generate_secret_key(fname):
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"django.middleware.security.SecurityMiddleware",
"webcdi.middleware.LoginRequiredMiddleware",
"webcdi.middleware.AdminLocaleMiddleware",
]

Expand Down Expand Up @@ -535,17 +559,10 @@ def generate_secret_key(fname):

CAT_FORMS = ["CAT", "CAT2"]

LOGIN_EXEMPT_URLS = (
r"^registration/logout/$",
r"^registration/register/$",
r"^registration/password-reset/$",
r"^registration/password-reset/done/$",
r"^registration/password-reset/confirm/",
r"^registration/password-reset/complete/$",
r"^registration/password/change/$",
r"^registration/password/change/",
r"^",
)
# NOTE: the old LoginRequiredMiddleware + LOGIN_EXEMPT_URLS pair was removed:
# the exempt list ended with r"^" (matches every URL), so the middleware had
# been a no-op. Access control lives on the views — researcher views use
# LoginRequiredMixin; participant views are anonymous by design (hash URLs).


DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
Expand Down