From 77c74c0d1fb3ef577967624f42731685f9bde3de Mon Sep 17 00:00:00 2001 From: CJ Green <44074998+okaycj@users.noreply.github.com> Date: Tue, 5 Apr 2022 15:15:18 -0400 Subject: [PATCH] New user wizard (#954) * Format base css file * Add button to study details page * CSS for active navigation * Store next value in session on sign up * Add buttons to my account page * Conditionally disable study links on account view * Update conditional copy in demographics view * Add child view conditional copy * Add 'has_study_child' to demo template * Remove study from session when study is attempted * Redirect to demo view when save button is clicked If there is no child, demo view will be redirect to child list view. * Fix existing tests * Added has_study_child to all my account views * Update account navigation --- accounts/models.py | 26 ++- .../accounts/_account-navigation.html | 4 + accounts/views.py | 3 +- web/static/base.css | 148 ++++++++++++------ web/templates/web/_navigation.html | 2 +- web/templates/web/children-list.html | 14 +- .../web/demographic-data-update.html | 26 ++- web/templates/web/study-detail.html | 3 +- web/templatetags/web_extras.py | 96 +++++++++--- web/tests/test_views.py | 4 +- web/views.py | 43 ++++- 11 files changed, 291 insertions(+), 78 deletions(-) diff --git a/accounts/models.py b/accounts/models.py index 588223442..b2e7c4c8b 100644 --- a/accounts/models.py +++ b/accounts/models.py @@ -13,6 +13,7 @@ from django.contrib.postgres.fields.array import ArrayField from django.core.mail.message import EmailMultiAlternatives from django.db import models +from django.http import HttpRequest from django.template.loader import get_template from django.utils.html import mark_safe from django.utils.text import slugify @@ -28,7 +29,8 @@ from qrcode import make as make_qrcode from qrcode.image.svg import SvgPathImage -from accounts.queries import BitfieldQuerySet +import studies +from accounts.queries import BitfieldQuerySet, get_child_eligibility_for_study from studies.fields import CONDITIONS, GESTATIONAL_AGE_CHOICES, LANGUAGES from studies.helpers import send_mail from studies.permissions import ( @@ -163,7 +165,7 @@ def __init__(self, *args, **kwargs): @property def identicon(self): if not self._identicon: - rbw = self._make_rainbow() + rbw = self._make_rainbow generator = pydenticon.Generator( 5, 5, digest=hashlib.sha512, foreground=rbw, background="rgba(0,0,0,0)" ) @@ -194,6 +196,26 @@ def slug(self): """Temporary workaround.""" return f"{slugify(self.nickname or 'anonymous')}-{str(self.uuid).split('-')[0]}" + @property + def has_any_child(self): + return self.children.filter(deleted=False).exists() + + def has_study_child(self, request: HttpRequest) -> bool: + study_uuid = request.session.get("study_uuid", None) + if study_uuid: + study = studies.models.Study.objects.get(uuid=study_uuid) + children = self.children.filter(deleted=False) + return any( + get_child_eligibility_for_study(child, study) for child in children + ) + else: + return False + + @property + def has_demographics(self): + return self.demographics.exists() + + @property def _make_rainbow(self): rbw = [] for i in range(0, 255, 10): diff --git a/accounts/templates/accounts/_account-navigation.html b/accounts/templates/accounts/_account-navigation.html index 591480849..b7c1f2633 100644 --- a/accounts/templates/accounts/_account-navigation.html +++ b/accounts/templates/accounts/_account-navigation.html @@ -2,4 +2,8 @@ {% trans "Account Information" %}
{% trans "Change your login credentials and/or nickname." %}
{% trans "Demographic Survey" %}
{% trans "Tell us more about yourself." %}
{% trans "Children Information" %}
{% trans "Add or edit participant information." %}
+{% if request.session.study_name %} + {% trans "Continue to Study" %}
{% trans "Go on to" %} "{{ request.session.study_name|truncatechars:40 }}".
+{% endif %} +{% if request.session.study_name %}{% trans "Find Another Study" %}{% else %}{% trans "Find a Study Now" %}{% endif %}
{% trans "See all available studies." %}
{% trans "Email Preferences" %}
{% trans "Edit when you can be contacted." %}
diff --git a/accounts/views.py b/accounts/views.py index 0e7675ce5..e5b5a0aa1 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -233,6 +233,7 @@ def get_context_data(self, **kwargs): "otp_check_form": otp_check_form, "user": user, "otp": otp, + "has_study_child": user.has_study_child(self.request), } ) @@ -250,7 +251,7 @@ def _get_user_and_otp(self) -> Tuple[User, Union[GoogleAuthenticatorTOTP, None]] def _get_forms( self, - ) -> (forms.AccountUpdateForm, forms.PasswordChangeForm, forms.TOTPCheckForm): + ) -> Tuple[forms.AccountUpdateForm, forms.PasswordChangeForm, forms.TOTPCheckForm]: """Bind forms appropriately for method.""" request = self.request # TODO: switch to normal attribute access after this is fixed diff --git a/web/static/base.css b/web/static/base.css index 28a38103f..6068aee4e 100644 --- a/web/static/base.css +++ b/web/static/base.css @@ -9,12 +9,12 @@ black: #1A1718 html { - height: 100%; + height: 100%; } body { font-family: 'Lato', sans-serif; - font-size : 15px; + font-size: 15px; min-height: 100%; display: grid; grid-template-rows: 1fr auto; @@ -31,21 +31,28 @@ body { background: #FFFFFF; } -.nav.navbar-nav li a, .nav.navbar-nav li button{ - color:#666666; +.nav.navbar-nav li a, +.nav.navbar-nav li button { + color: #666666; } -.nav.navbar-nav li button{ +.nav.navbar-nav li button { padding: 15px; line-height: 15px; margin: 0; } -ul.nav.navbar-nav li a:hover, ul.nav.navbar-nav li button:hover{ - color:#000; +ul.nav.navbar-nav li a:hover, +ul.nav.navbar-nav li button:hover { + color: #000; text-decoration: none; } +ul.nav.navbar-nav li button.active { + color: #555; + background-color: #e7e7e7; +} + .nav .divider-vertical { height: 40px; margin: 0 9px; @@ -56,28 +63,32 @@ ul.nav.navbar-nav li a:hover, ul.nav.navbar-nav li button:hover{ .navbar-toggle .icon-bar { background: #337AB7; } + .lookit-row { padding: 1em 0; } -button.btn.btn-success, a.btn.btn-success { - background-color:#398339; - border-color:#398339; -} +button.btn.btn-success, +a.btn.btn-success { + background-color: #398339; + border-color: #398339; +} -button.btn.btn-success:hover, a.btn.btn-success:hover { - background-color:#5cb85c; - border-color:#5cb85c; -} +button.btn.btn-success:hover, +a.btn.btn-success:hover { + background-color: #5cb85c; + border-color: #5cb85c; +} -ul.pagination li.last.disabled a, ul.pagination li.prev.disabled a { - color:black; +ul.pagination li.last.disabled a, +ul.pagination li.prev.disabled a { + color: black; } .policy p.last-update { - text-align: center; - font-style: italic; + text-align: center; + font-style: italic; } .footer { @@ -86,34 +97,34 @@ ul.pagination li.last.disabled a, ul.pagination li.prev.disabled a { } footer .social-link { - padding-left: 5px; + padding-left: 5px; } .footer-row .funding-statement { - font-size: 75%; + font-size: 75%; } .footer-row .footer-resources ul { - list-style: none; - text-align: right; + list-style: none; + text-align: right; } .footer-row .footer-resource ul li { - background-position: 100% .4em; - padding-right: .6em; + background-position: 100% .4em; + padding-right: .6em; } div.alert.alert-success.alert-dismissable button.close { - opacity: 1; + opacity: 1; } /* JUMBOTRON */ .home-jumbotron { - background:white url('images/chs_collage.png') center top no-repeat; - height:595px; + background: white url('images/chs_collage.png') center top no-repeat; + height: 595px; } -.home-jumbotron .content { +.home-jumbotron .content { margin: 0 auto; color: #231f20; text-align: center; @@ -131,6 +142,7 @@ div.alert.alert-success.alert-dismissable button.close { font-size: 60px; color: #ff5f5c; } + .information-row h3 { min-height: 60px; } @@ -140,11 +152,13 @@ div.alert.alert-success.alert-dismissable button.close { background: #f5f5f5; border-top: 1px solid #eee; } + .news-row h3 { font-weight: 300; text-align: center; margin: 0 0 20px 0; } + .news-row .row { padding: 5px 0; } @@ -156,7 +170,7 @@ div.alert.alert-success.alert-dismissable button.close { @media (max-width:500px) { - .home-jumbotron > .content > h1 { + .home-jumbotron>.content>h1 { font-size: 36px; } } @@ -166,14 +180,17 @@ div.alert.alert-success.alert-dismissable button.close { .faq-row .panel { border: none; } + .faq-row .panel-heading { background-color: rgba(202, 218, 227, 0.46); padding: 12px; } + .faq-row .panel-title { font-size: 17px; } -.panel-group .panel + .panel { + +.panel-group .panel+.panel { margin-top: 9px; } @@ -185,9 +202,11 @@ div.alert.alert-success.alert-dismissable button.close { min-height: 600px; margin-top: 20px; } + .profile-img { text-align: center; } + img.profile-img { border-radius: 120px; border: 6px solid #d9edf7; @@ -205,9 +224,11 @@ h3.resources-row { font-weight: 300; color: #337AB7; } + h4.resources-row { padding: 25px 0 10px 0; } + .resources-local { padding: 15px; background: #f5f5f5; @@ -220,14 +241,17 @@ h4.resources-row { padding: 25px; box-shadow: 0 0 3px #ccc; } + label.login-row { margin-top: 15px; font-size: 20px; font-weight: 300; } + .login-row.pull-right { - padding-top:30px; + padding-top: 30px; } + button.login-row { width: 180px; } @@ -241,17 +265,21 @@ button.login-row { padding: 25px; box-shadow: 0 0 3px #ccc; } + label.account-row { margin-top: 15px; font-size: 20px; font-weight: 300; } + .account-row.pull-right { - padding-top:30px; + padding-top: 30px; } + .account-row a { overflow: hidden !important; } + button.account-row { width: 180px; } @@ -273,8 +301,9 @@ button.account-row { /* STUDIES PAGE */ -.study-list a, .study-list a:hover { - color:#333; +.study-list a, +.study-list a:hover { + color: #333; text-decoration: none; } @@ -311,8 +340,8 @@ button.account-row { } a.no-link-formatting { - color: black; - text-decoration: none; + color: black; + text-decoration: none; } /* STUDY DETAIL */ @@ -334,18 +363,20 @@ a.no-link-formatting { font-size: 200px; text-align: center; } + .study-detail-caption { line-height: 25px; font-size: 18px; font-weight: 300; } + .study-detail-info { margin-top: 30px; } table.study-detail-infotable { - width:100%; + width: 100%; } table.study-detail-infotable td { @@ -354,7 +385,7 @@ table.study-detail-infotable td { table.study-detail-infotable td:nth-child(2) { padding-left: 5px; - padding-top:20px; + padding-top: 20px; } table.study-detail-infotable td:first-child { @@ -372,10 +403,10 @@ table.study-detail-infotable td:first-child { } button:disabled#participate-button, -button[disabled]#participate-button{ - border: 1px solid #999999; - background-color: #cccccc; - color: #666666; +button[disabled]#participate-button { + border: 1px solid #999999; + background-color: #cccccc; + color: #666666; } #participate-button { @@ -427,6 +458,7 @@ button[disabled]#participate-button{ .textarea-full { width: 100%; } + .mb-lg { margin-bottom: 20px; } @@ -434,81 +466,107 @@ button[disabled]#participate-button{ .pb-xs { padding-bottom: 5px; } + .pb-sm { padding-bottom: 10px; } + .pb-md { padding-bottom: 15px; } + .pb-lg { padding-bottom: 20px; } + .pr-xs { padding-right: 5px; } + .pr-sm { padding-right: 10px; } + .pr-md { padding-right: 15px; } + .pr-lg { padding-right: 20px; } + .pt-xs { padding-top: 5px; } + .pt-sm { padding-top: 10px; } + .pt-md { padding-top: 15px; } + .pt-lg { padding-top: 20px; } + .pt-xl { padding-top: 25px; } + .pt-xxl { padding-top: 30px; } + .pt-xxxl { padding-top: 35px; } + .pl-sm { padding-left: 5px; } + .pl-md { padding-left: 10px; } + .pl-lg { padding-left: 15px; } + .pl-xl { padding-left: 20px; } + .mb-xs { - margin-bottom:10px; + margin-bottom: 10px; } + .mb-sm { margin-bottom: 15px; } + .mb-md { margin-bottom: 20px; } + .mb-lg { margin-bottom: 25px; } + .mt-sm { margin-top: 10px; } + .mt-md { margin-top: 15px; } + .mt-lg { margin-top: 20px; } + .mt-xl { margin-top: 25px; } @@ -527,7 +585,7 @@ li.response-feedback { .btn-primary { - color:#1A1718; + color: #1A1718; background-color: #44BCE8; border-color: #44BCE8; } diff --git a/web/templates/web/_navigation.html b/web/templates/web/_navigation.html index 6794b5a82..c5de449e7 100644 --- a/web/templates/web/_navigation.html +++ b/web/templates/web/_navigation.html @@ -35,7 +35,7 @@ {% nav_item request 'web:studies-history' 'My Past Studies' %}
  • {% trans "Logout" %}
  • {% else %} - {% nav_item request 'web:participant-signup' 'Sign up' %} + {% nav_signup request %} {% nav_login request %} {% endif %} diff --git a/web/templates/web/children-list.html b/web/templates/web/children-list.html index 8fd4467a3..5caa1895f 100644 --- a/web/templates/web/children-list.html +++ b/web/templates/web/children-list.html @@ -11,9 +11,21 @@ {% include 'accounts/_account-navigation.html' with current_page="children-list" %}
    + + {% if not user.has_any_child %} +

    Click the "Add Child" button to add a child to your account.

    + {% elif not request.session.study_name and user.has_any_child %} +

    You can edit information about the children listed in your account, or add another by clicking the "Add Child" button. Click the "Find a Study" button to view the studies available for your children.

    + {% elif request.session.study_name and has_study_child %} +

    When you are ready, click the "Continue to Study" button to go on to your study, "{{ request.session.study_name }}".

    + {% elif request.session.study_name and user.has_any_child and not has_study_child %} +

    You can edit information about the children listed in your account, or add another by clicking the "Add Child" button.

    +

    If the "Continue to Study" button still isn't lighting up, the study may have become full or be recruiting a slightly different set of kids right now. You might also be missing a piece of information about your family, such as the languages you speak at home.

    +

    You can click the "Demographic Survey" button to add more information about your family, "Find Another Study" to explore more studies for your family, or click here to review the requirements for "{{ request.session.study_name }}".

    + {% endif %} +
    -

    {% if children %}
    diff --git a/web/templates/web/demographic-data-update.html b/web/templates/web/demographic-data-update.html index a4242c4dd..f8dfd7a6f 100644 --- a/web/templates/web/demographic-data-update.html +++ b/web/templates/web/demographic-data-update.html @@ -82,8 +82,30 @@ {% include 'accounts/_account-navigation.html' with current_page="demographic-update" %}
    -

    {% trans "One reason we are developing Internet-based experiments is to represent a more diverse group of families in our research. Your answers to these questions will help us understand what audience we reach, as well as how factors like speaking multiple languages or having older siblings affect children's learning." %}

    -

    {% trans "Even if you allow your study videos to be published for scientific or publicity purposes, your demographic information is never published in conjunction with your video." %}

    + + {% if request.session.study_name and user.has_demographics and not has_study_child %} +

    {% trans "If the \"Continue to Study\" button still isn't lighting up, make sure you have completed the form below and added the participating child to your account. The study may also have become full or be recruiting a slightly different set of kids right now." %}

    +

    + {% trans "You can click the \"Children Information\" button to add another child, \"Find Another Study\" to explore more studies for your family, or" %} + {% trans "click here" %} + {% trans "to review the requirements for" %} + "{{ request.session.study_name }}". +

    + {% else %} + + {% if not request.session.study_name and not user.has_demographics %} +

    {% trans "Welcome to Children Helping Science! Before you take your first study, we are asking you to share some information about your family." %}

    + {% elif request.session.study_name and not user.has_demographics %} +

    {% trans "Welcome to Children Helping Science! Before you continue to the main study" %} ("{{ request.session.study_name }}"), {% trans "we are asking you to share some information about your family." %}

    + {% elif request.session.study_name and user.has_demographics and has_study_child %} +

    {% trans "Use this form to share more information about your family. When you are ready to move on, you can click the \"Continue to Study\" button on the left!" %}

    + {% endif %} + +

    {% trans "One reason we are developing Internet-based experiments is to represent a more diverse group of families in our research. Your answers to these questions will help us understand what audience we reach, as well as how factors like speaking multiple languages or having older siblings affect children's learning." %}

    +

    {% trans "Even if you allow your study videos to be published for scientific or publicity purposes, your demographic information is never published in conjunction with your video." %}

    + + {% endif %} +
    {% csrf_token %} diff --git a/web/templates/web/study-detail.html b/web/templates/web/study-detail.html index bafb34b2b..dfa01c377 100644 --- a/web/templates/web/study-detail.html +++ b/web/templates/web/study-detail.html @@ -136,7 +136,8 @@

    {{study.name}}

    {% trans "Would you like to participate in this study?" %}

    {% if not request.user.is_authenticated %} - {% nav_login request text="Log in to participate" button=True %} +

    {% nav_login request text="Log in to participate" button=True %}

    + {% nav_signup request text="Create a new account" button=True %} {% elif not children %} {% trans "Add child profile to " %} {% if preview_mode %} {% trans "preview" %} {% else %} {% trans "participate" %} {% endif %} {% elif not has_demographic %} diff --git a/web/templatetags/web_extras.py b/web/templatetags/web_extras.py index 2549e99b9..adb14b73c 100644 --- a/web/templatetags/web_extras.py +++ b/web/templatetags/web_extras.py @@ -23,6 +23,54 @@ def format(text: Text) -> Text: return "" +def active_nav(request, url) -> Text: + """Determine is this button is the active button in the navigation bar. + + Args: + request (Request): Request object from template + url (Text): String url for the current view + + Returns: + Text: "active" if this is the active view, else empty string. + """ + if request.path == url: + return "active" + else: + return "" + + +def nav_next(request, url, text, button): + """Create form that will submit the current page as the "next" query arg. + + Args: + request (Request): Request object submitted from template + url (Text): Target URL + text (Text): String to be displayed is button + button (bool): Is this to be styled as a button or as a link + + Returns: + SafeText: Returns html form used to capture current view and submit it as the "next" query arg + """ + active = active_nav(request, url) + + if button: + css_class = "btn btn-lg btn-default" + elif active: + css_class = f"{active} btn-link" + else: + css_class = "btn-link" + + form = f""" + + + """ + + if not button: + form = f"
  • {form}
  • " + + return mark_safe(form) + + @register.simple_tag def child_is_valid_for_study_criteria_expression(child, study): return get_child_eligibility(child, study.criteria_expression) @@ -43,43 +91,53 @@ def google_tag_manager() -> Text: @register.simple_tag def nav_item(request, url_name, text): + """General navigation bar item + + Args: + request (Request): Reqeust submitted from template + url_name (Text): Name of url to be looked up by reverse + text (Text): Text to be displayed in item + + Returns: + SafeText: HTML of navigation item + """ li_class = "" url = reverse(url_name) - - if request.path == url: - li_class = "active" + li_class = active_nav(request, url) return mark_safe(f'
  • {_(text)}
  • ') @register.simple_tag def nav_login(request, text="Login", button=False): - """Login button suited for either navigation bar or as it's own button. + """Navigation login button Args: - request (Request): Request object passed by the template. - text (str, optional): Text to be displayed in button. Defaults to "Login". - button (bool, optional): Should this login button be a button else it'll be styled as a link. Defaults to False. + request (Request): Request object submitted by template + text (str, optional): Text to be shown in button. Defaults to "Login". + button (bool, optional): Is this to be styled as a button or as a link. Defaults to False. Returns: - SafeText: Returned HTML is marked as safe. + SafeText: HTML form """ url = reverse("login") + return nav_next(request, url, text, button) - if button: - css_class = "btn btn-lg btn-default" - else: - css_class = "btn-link" - form = f"""
    - - - """ +@register.simple_tag +def nav_signup(request, text="Sign up", button=False): + """Navigation sign up button - if not button: - form = f"
  • {form}
  • " + Args: + request (Request): Request object submitted by template + text (str, optional): Text to be shown in button. Defaults to "Login". + button (bool, optional): Is this to be styled as a button or as a link. Defaults to False. - return mark_safe(form) + Returns: + SafeText: HTML form + """ + url = reverse("web:participant-signup") + return nav_next(request, url, text, button) @register.simple_tag diff --git a/web/tests/test_views.py b/web/tests/test_views.py index 7b8e68405..f5ab6cf44 100644 --- a/web/tests/test_views.py +++ b/web/tests/test_views.py @@ -236,7 +236,9 @@ def test_demographic_data_update_authenticated(self): response = self.client.post( reverse("web:demographic-data-update"), cleaned_data, follow=True ) - self.assertEqual(response.redirect_chain, [(reverse("web:studies-list"), 302)]) + self.assertEqual( + response.redirect_chain, [(reverse("web:demographic-data-update"), 302)] + ) self.assertEqual(response.status_code, 200) # Make sure we can retrieve updated data diff --git a/web/views.py b/web/views.py index 5f24eefe5..4807d77a4 100644 --- a/web/views.py +++ b/web/views.py @@ -1,5 +1,6 @@ +import re from hashlib import sha256 -from typing import Text +from typing import Any, Dict, Text from urllib.parse import parse_qs, urlencode, urlparse from uuid import UUID @@ -138,7 +139,25 @@ def form_valid(self, form): messages.success(self.request, _("Participant created.")) return resp + def store_study_in_session(self) -> None: + study_url = self.request.GET.get("next", "") + if study_url: + p = re.compile("^/studies/([\w]{8}-[\w]{4}-[\w]{4}-[\w]{4}-[\w]{12})") + m = p.match(study_url) + if m: + study_uuid = m.group(1) + study = Study.objects.only("name").get(uuid=study_uuid) + self.request.session["study_name"] = study.name + self.request.session["study_uuid"] = study_uuid + def get_success_url(self): + """Get the url if the form is successful. Additionally, the previous url is stored on the + "next" value on GET. This url is stored in the user's session. + + Returns: + str: URL of next view of form submission. + """ + self.store_study_in_session() return reverse("web:demographic-data-update") @@ -178,7 +197,7 @@ def get_initial(self): def get_success_url(self): if self.request.user.children.filter(deleted=False).exists(): - return reverse("web:studies-list") + return reverse("web:demographic-data-update") else: return reverse("web:children-list") @@ -191,6 +210,7 @@ def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["countries"] = countries context["states"] = USPS_CHOICES + context["has_study_child"] = self.request.user.has_study_child(self.request) return context @@ -206,10 +226,11 @@ def get_context_data(self, **kwargs): Add children that have not been deleted that belong to the current user to the context_dict. Also add info to hide the Add Child form on page load. """ + user = self.request.user + context = super().get_context_data(**kwargs) - context["children"] = Child.objects.filter( - deleted=False, user=self.request.user - ) + context["children"] = Child.objects.filter(deleted=False, user=user) + context["has_study_child"] = user.has_study_child(self.request) return context @@ -288,6 +309,11 @@ def form_valid(self, form): messages.success(self.request, _("Email preferences saved.")) return super().form_valid(form) + def get_context_data(self, **kwargs: Any) -> Dict[str, Any]: + context = super().get_context_data(**kwargs) + context["has_study_child"] = self.request.user.has_study_child(self.request) + return context + class StudiesListView(generic.ListView, FormView): """ @@ -542,11 +568,18 @@ def get_context_data(self, **kwargs): return context + def clear_study(self): + session = self.request.session + "study_name" in session and session.pop("study_name") + "study_uuid" in session and session.pop("study_uuid") + session.modified = True + def dispatch(self, request, *args, **kwargs): study = self.get_object() if study.state == "active": if request.method == "POST": + self.clear_study() child_uuid = request.POST["child_id"] if study.study_type.is_external: response = create_external_response(study, child_uuid)