From d7099bcfd2b56f488f39831a2949a1d82b0836d6 Mon Sep 17 00:00:00 2001 From: CJ Green <44074998+okaycj@users.noreply.github.com> Date: Wed, 26 Jul 2023 12:42:06 -0400 Subject: [PATCH 01/45] Add js2py package for js validation --- poetry.lock | 57 +++++++++++++++++++++++++++++++++++++++++++++++++- pyproject.toml | 3 ++- 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/poetry.lock b/poetry.lock index 6967b451a..703270614 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1279,6 +1279,22 @@ files = [ {file = "jmespath-0.10.0.tar.gz", hash = "sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9"}, ] +[[package]] +name = "js2py" +version = "0.74" +description = "JavaScript to Python Translator & JavaScript interpreter written in 100% pure Python." +optional = false +python-versions = "*" +files = [ + {file = "Js2Py-0.74-py3-none-any.whl", hash = "sha256:40a508a79e2f8d624e3f2e604f90a1e6f46ac75b416d7f4745939ff4a2e95e09"}, + {file = "Js2Py-0.74.tar.gz", hash = "sha256:39f3a6aa8469180efba3c8677271df27c31332fd1b471df1af2af58b87b8972f"}, +] + +[package.dependencies] +pyjsparser = ">=2.5.1" +six = ">=1.10" +tzlocal = ">=1.2" + [[package]] name = "jsbeautifier" version = "1.14.9" @@ -1829,6 +1845,17 @@ files = [ {file = "pyinstrument_cext-0.2.4.tar.gz", hash = "sha256:79b29797209eebd441a8596accfa8b617445d9252fbf7ce75d3a4a0eb46cb877"}, ] +[[package]] +name = "pyjsparser" +version = "2.7.1" +description = "Fast javascript parser (based on esprima.js)" +optional = false +python-versions = "*" +files = [ + {file = "pyjsparser-2.7.1-py2-none-any.whl", hash = "sha256:2b12842df98d83f65934e0772fa4a5d8b123b3bc79f1af1789172ac70265dd21"}, + {file = "pyjsparser-2.7.1.tar.gz", hash = "sha256:be60da6b778cc5a5296a69d8e7d614f1f870faf94e1b1b6ac591f2ad5d729579"}, +] + [[package]] name = "pyotp" version = "2.6.0" @@ -2381,6 +2408,34 @@ files = [ {file = "typing_extensions-4.1.1.tar.gz", hash = "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42"}, ] +[[package]] +name = "tzdata" +version = "2023.3" +description = "Provider of IANA time zone data" +optional = false +python-versions = ">=2" +files = [ + {file = "tzdata-2023.3-py2.py3-none-any.whl", hash = "sha256:7e65763eef3120314099b6939b5546db7adce1e7d6f2e179e3df563c70511eda"}, + {file = "tzdata-2023.3.tar.gz", hash = "sha256:11ef1e08e54acb0d4f95bdb1be05da659673de4acbd21bf9c69e94cc5e907a3a"}, +] + +[[package]] +name = "tzlocal" +version = "5.0.1" +description = "tzinfo object for the local timezone" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tzlocal-5.0.1-py3-none-any.whl", hash = "sha256:f3596e180296aaf2dbd97d124fe76ae3a0e3d32b258447de7b939b3fd4be992f"}, + {file = "tzlocal-5.0.1.tar.gz", hash = "sha256:46eb99ad4bdb71f3f72b7d24f4267753e240944ecfc16f25d2719ba89827a803"}, +] + +[package.dependencies] +tzdata = {version = "*", markers = "platform_system == \"Windows\""} + +[package.extras] +devenv = ["black", "check-manifest", "flake8", "pyroma", "pytest (>=4.3)", "pytest-cov", "pytest-mock (>=3.3)", "zest.releaser"] + [[package]] name = "urllib3" version = "1.26.15" @@ -2544,4 +2599,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] [metadata] lock-version = "2.0" python-versions = ">=3.9 <3.10" -content-hash = "34bb0d988941235be5529d96ff77ff553a9f66a6663436fbd513d1c857843782" +content-hash = "0dd763f1c9e62a1a717214b9ec5a4103730aa8bd18bca787662d4415f9061a9c" diff --git a/pyproject.toml b/pyproject.toml index 44e49c749..838405ccd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,6 +51,7 @@ transitions = "0.9.0" uWSGI = "2.0.19.1" pillow = "9.4.0" django-bootstrap-icons = "0.8.2" +js2py = "^0.74" [tool.poetry.group.dev.dependencies] coverage = "^7.2" @@ -63,7 +64,7 @@ beautifulsoup4 = "^4.10.0" black = "22.3.0" pre-commit = "2.18.1" libsass = "^0.22.0" -djlint = "1.32.1" +djlint = "^1.32.1" [build-system] requires = ["poetry-core>=1.0.0"] From 7f70360a354f2876768e1202f0b148d8d237d05d Mon Sep 17 00:00:00 2001 From: CJ Green <44074998+okaycj@users.noreply.github.com> Date: Wed, 26 Jul 2023 12:43:02 -0400 Subject: [PATCH 02/45] html format changes --- accounts/templates/accounts/participant_list.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/accounts/templates/accounts/participant_list.html b/accounts/templates/accounts/participant_list.html index a8b14ae22..afffbb466 100644 --- a/accounts/templates/accounts/participant_list.html +++ b/accounts/templates/accounts/participant_list.html @@ -17,8 +17,8 @@ size="50" type="text" value="{{ match }}"/> - - + + @@ -49,7 +49,7 @@ {% for user in object_list %} {% url 'exp:participant-detail' user.id as url_participant_detail %} - {% empty %} diff --git a/studies/templates/studies/study_detail.html b/studies/templates/studies/study_detail.html index 6ad45f9c5..3dd3880ba 100644 --- a/studies/templates/studies/study_detail.html +++ b/studies/templates/studies/study_detail.html @@ -141,7 +141,7 @@ {% bs_icon "pencil-square" %}Study Ad{% bs_icon "pencil-square" %}Study Details + href="{% url 'exp:study-detail' pk=study.id %}">{% bs_icon "pencil-square" %}Study Details {% endif %} {% if "read_study__responses" in study_perms or "read_study__responses(is_preview=True)" in study_perms %} action="{% url 'exp:change-study-status' study.id %}" method="post"> {% csrf_token %} - + {% empty %} @@ -205,7 +205,7 @@
Videos
{% csrf_token %}
- +
Download response

Include with response download:

{% include "studies/_data_options.html" with data_options=data_options %}
diff --git a/studies/templates/studies/study_responses_consent_ruling.html b/studies/templates/studies/study_responses_consent_ruling.html index 707e3e8f2..3c76bc602 100644 --- a/studies/templates/studies/study_responses_consent_ruling.html +++ b/studies/templates/studies/study_responses_consent_ruling.html @@ -12,7 +12,7 @@ {% comment %} When we upgrade to django 2.x, please convert to json_script. Please see: https://docs.djangoproject.com/en/2.1/ref/templates/builtins/#json-script - {% endcomment %} +{% endcomment %} {{ response_key_value_store | safe }} @@ -20,7 +20,7 @@ {% block breadcrumb %} {% breadcrumb %} {% url 'exp:study-list' %} Manage Studies - {% url 'exp:study-detail' pk=study.id %} {{ study.name }} + {% url 'exp:study' pk=study.id %} {{ study.name }} Consent Manager {% endbreadcrumb %} {% endblock breadcrumb %} @@ -63,7 +63,7 @@ overflow-y: scroll"> {% for response in loaded_responses %}
  • @@ -128,7 +128,7 @@ 0
  • - +
    + {% if user.nickname %} {{ user.nickname }} From c444af7187565207670b7d2cdd76e88bd9a2a0f7 Mon Sep 17 00:00:00 2001 From: CJ Green <44074998+okaycj@users.noreply.github.com> Date: Wed, 26 Jul 2023 12:56:00 -0400 Subject: [PATCH 03/45] Experiment runner config url --- exp/urls.py | 6 ++++ exp/views/study.py | 83 ++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 87 insertions(+), 2 deletions(-) diff --git a/exp/urls.py b/exp/urls.py index 19be5effa..ed8bb6694 100644 --- a/exp/urls.py +++ b/exp/urls.py @@ -61,6 +61,7 @@ from exp.views.study import ( ChangeStudyStatusView, CloneStudyView, + ExperimentRunnerEdit, ManageResearcherPermissionsView, StudyListViewActive, StudyListViewApproved, @@ -251,4 +252,9 @@ name="preview-proxy", ), path("support/", SupportView.as_view(), name="support"), + path( + "studies//runner/edit/", + ExperimentRunnerEdit.as_view(), + name="runner-edit", + ), ] diff --git a/exp/views/study.py b/exp/views/study.py index f7dd3b10a..a8dee37db 100644 --- a/exp/views/study.py +++ b/exp/views/study.py @@ -8,7 +8,8 @@ from django.contrib.auth.mixins import UserPassesTestMixin from django.db.models import Q from django.db.models.functions import Lower -from django.http import HttpResponseForbidden, HttpResponseRedirect +from django.forms.models import BaseModelForm +from django.http import HttpResponse, HttpResponseForbidden, HttpResponseRedirect from django.http.response import HttpResponse from django.shortcuts import get_object_or_404, reverse from django.views import generic @@ -24,7 +25,7 @@ StudyTypeMixin, ) from project import settings -from studies.forms import StudyCreateForm, StudyEditForm +from studies.forms import ExperimentRunnerForm, StudyCreateForm, StudyEditForm from studies.helpers import send_mail from studies.models import Study, StudyType from studies.permissions import LabPermission, StudyPermission @@ -1077,3 +1078,81 @@ def get_permitted_triggers(view_instance, triggers): permitted_triggers.append(trigger) return permitted_triggers + + +class ExperimentRunnerEdit( + ResearcherLoginRequiredMixin, + UserPassesTestMixin, + StudyTypeMixin, + SingleObjectFetchProtocol[Study], + generic.UpdateView, + # generic.FormView +): + template_name = "studies/runner_edit.html" + model = Study + form_class = ExperimentRunnerForm + + def user_can_edit_study(self): + """Test predicate for the experiment runner edit view. Borrowed permissions from study edit view. + + Returns: + True if this user can edit this Study, False otherwise + + """ + user: User = self.request.user + study = self.get_object() + + return ( + user + and user.is_researcher + and user.has_study_perms(StudyPermission.WRITE_STUDY_DETAILS, study) + ) + + test_func = user_can_edit_study + + def get_initial(self): + """Populate Exp Runner with data from the study metadata field. Also, convert structure code to json.""" + initial = super().get_initial() + study = self.object + metadata = study.metadata + structure = study.structure + + if "exact_text" in structure: + structure = structure["exact_text"] + else: + structure = json.dumps(structure) + + initial.update( + player_repo_url=metadata["player_repo_url"], + last_known_player_sha=metadata["last_known_player_sha"], + structure=structure, + ) + + return initial + + def form_valid(self, form: BaseModelForm) -> HttpResponse: + """After form has been determined to be valid, place metadata into the appropriate field in the study table. If + There are changes to metadata, set to study to NOT BUILT. + + Args: + form (BaseModelForm): _description_ + + Returns: + HttpResponse: _description_ + """ + study = self.object + metadata = { + "player_repo_url": form.data["player_repo_url"], + "last_known_player_sha": form.data["last_known_player_sha"], + } + + if metadata != study.metadata: + study.built = False + study.is_building = False + study.metadata = metadata + + return super().form_valid(form) + + def get_success_url(self, **kwargs): + """Upon successful form submission, change the view to study detail.""" + return reverse("exp:study-detail", kwargs={"pk": self.object.pk}) From 3f435a392a24851888199f2b1a8077b7c9d72d97 Mon Sep 17 00:00:00 2001 From: CJ Green <44074998+okaycj@users.noreply.github.com> Date: Wed, 26 Jul 2023 13:17:29 -0400 Subject: [PATCH 04/45] Update study edit forms --- studies/forms.py | 125 +++++++++++++++++++++++++++++++++++++---------- 1 file changed, 100 insertions(+), 25 deletions(-) diff --git a/studies/forms.py b/studies/forms.py index 1b1aa12e9..606071e41 100644 --- a/studies/forms.py +++ b/studies/forms.py @@ -1,5 +1,7 @@ import json +import js2py +import requests from ace_overlay.widgets import AceOverlayWidget from django import forms from django.db.models import Q @@ -175,14 +177,17 @@ class StudyForm(ModelForm): required=False, ) - external = forms.BooleanField( - required=False, - help_text="Post an external link to a study, rather than Lookit's experiment builder.", - ) - scheduled = forms.BooleanField( - required=False, - help_text="Schedule participants for one-on-one appointments with a researcher.", - ) + ###### + # TODO: Remove these comments after the external config has been built + + # external = forms.BooleanField( + # required=False, + # help_text="Post an external link to a study, rather than Lookit's experiment builder.", + # ) + # scheduled = forms.BooleanField( + # required=False, + # help_text="Schedule participants for one-on-one appointments with a researcher.", + # ) # Define initial value here rather than providing actual default so that any updates don't # require migrations: this isn't a true "default" value that would ever be used, but rather @@ -300,6 +305,7 @@ class Meta: "criteria_expression", "must_have_participated", "must_not_have_participated", + "study_type", ] labels = { "name": "Study Name", @@ -365,13 +371,9 @@ class Meta: ), "public": "List this study on the 'Studies' page once you start it.", "shared_preview": "Allow other Lookit researchers to preview your study and give feedback.", - "study_type": f"""

    After selecting an experiment runner type above, you'll be asked - to provide some additional configuration information.

    -

    If you're not sure what to enter here, just leave the defaults (you can change this later). - For more information on experiment runner types, please - see the documentation.

    """, + "study_type": "Choose what type of study you are creating - this will change the fields that appear in the Experiment Details section.", "structure": PROTOCOL_HELP_TEXT_INITIAL, - "priority": f"This affects how studies are ordered at your lab's custom URL, not the main study page. If you leave all studies at the highest priority (99), then all of your lab's active/discoverable studies will be shown in a randomized order on your lab page. If you lower the priority of this study to 1, then it will appear last in the list on your lab page. You can find your lab's custom URL from the labs page. For more info, see the documentation on study prioritization.", + "priority": "This affects how studies are ordered at your lab's custom URL, not the main study page. If you leave all studies at the highest priority (99), then all of your lab's active/discoverable studies will be shown in a randomized order on your lab page. If you lower the priority of this study to 1, then it will appear last in the list on your lab page. You can find your lab's custom URL from the labs page. For more info, see the documentation on study prioritization.", } @@ -380,8 +382,6 @@ class StudyEditForm(StudyForm): def __init__(self, user=None, *args, **kwargs): super().__init__(*args, **kwargs) - self.fields["external"].disabled = True - self.fields["structure"].help_text = PROTOCOL_HELP_TEXT_EDIT # Restrict ability to edit study lab based on user permissions can_change_lab = user.has_study_perms( StudyPermission.CHANGE_STUDY_LAB, self.instance @@ -411,16 +411,19 @@ def __init__(self, user=None, *args, **kwargs): ) self.fields["lab"].disabled = True - def clean_external(self): - study = self.instance - external = self.cleaned_data["external"] + ##### + # TODO: Remove this commented code after external view has been built - if (not external and study.study_type.is_external) or ( - external and study.study_type.is_ember_frame_player - ): - raise forms.ValidationError("Attempt to change study type not allowed.") + # def clean_external(self): + # study = self.instance + # external = self.cleaned_data["external"] + + # if (not external and study.study_type.is_external) or ( + # external and study.study_type.is_ember_frame_player + # ): + # raise forms.ValidationError("Attempt to change study type not allowed.") - return external + # return external class StudyCreateForm(StudyForm): @@ -428,7 +431,6 @@ class StudyCreateForm(StudyForm): def __init__(self, user=None, *args, **kwargs): super().__init__(*args, **kwargs) - self.fields["structure"].help_text = PROTOCOL_HELP_TEXT_EDIT # Limit initial lab options to labs this user is a member of & can create studies in self.fields["lab"].queryset = Lab.objects.filter( id__in=get_objects_for_user( @@ -445,3 +447,76 @@ class EmailParticipantsForm(forms.Form): recipients = forms.ChoiceField(widget=EmailRecipientSelectMultiple()) subject = forms.CharField() body = forms.CharField(widget=forms.Textarea()) + + +class ExperimentRunnerForm(ModelForm): + player_repo_url = forms.URLField() + last_known_player_sha = forms.CharField() + structure = forms.CharField( + widget=AceOverlayWidget( + mode="json", + wordwrap=True, + theme="textmate", + width="100%", + height="100%", + showprintmargin=False, + ), + required=False, + ) + generator = forms.CharField( + widget=AceOverlayWidget( + mode="javascript", + wordwrap=True, + theme="textmate", + width="100%", + height="100%", + showprintmargin=False, + ), + required=False, + ) + + class Meta: + model = Study + fields = ("use_generator", "generator", "structure") + + def clean_structure(self): + try: + structure = json.loads(self.cleaned_data["structure"]) + structure["exact_text"] = self.cleaned_data["structure"] + return structure + except json.JSONDecodeError: + raise forms.ValidationError( + "Saving protocol configuration failed due to invalid JSON! Please use valid JSON and save again. If you reload this page, all changes will be lost." + ) + + def clean_generator(self): + try: + generator = self.cleaned_data["generator"] + js2py.eval_js(generator) + return generator + except js2py.internals.simplex.JsException: + raise forms.ValidationError( + "Generator javascript seems to be invalid. Please edit and save again. If you reload this page, all changes will be lost." + ) + + def clean_player_repo_url(self): + player_repo_url = self.cleaned_data["player_repo_url"] + + if not requests.get(player_repo_url).ok: + raise forms.ValidationError( + f"Frameplayer repo url {player_repo_url} does not work." + ) + + return player_repo_url + + def clean_last_known_player_sha(self): + last_known_player_sha = self.cleaned_data["last_known_player_sha"] + player_repo_url = self.cleaned_data["player_repo_url"] + + if last_known_player_sha and player_repo_url: + if not requests.get(f"{player_repo_url}/commit/{last_known_player_sha}").ok: + raise forms.ValidationError( + f"Frameplayer commit {last_known_player_sha} does not exist." + ) + + return last_known_player_sha From 12b5a2163cbe76bc8c4d000dfe4fb99bbb204a54 Mon Sep 17 00:00:00 2001 From: CJ Green <44074998+okaycj@users.noreply.github.com> Date: Wed, 26 Jul 2023 13:22:45 -0400 Subject: [PATCH 05/45] Add exp runner edit button to detail view --- studies/templates/studies/study_detail.html | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/studies/templates/studies/study_detail.html b/studies/templates/studies/study_detail.html index b18e225c4..df619e368 100644 --- a/studies/templates/studies/study_detail.html +++ b/studies/templates/studies/study_detail.html @@ -130,7 +130,7 @@ method="post" style="display:none"> {% csrf_token %} - +
    {% if "read_study__responses(is_preview=True)" in study_perms %} @@ -140,6 +140,8 @@ {% if can_edit_study_details %} {% bs_icon "pencil-square" %} Edit Study + {% bs_icon "pencil-square" %} Edit Runner {% endif %} {% if "read_study__responses" in study_perms or "read_study__responses(is_preview=True)" in study_perms %} {% bs_icon "clipboard-check-fill" %} Clone Study + onclick="cloneStudy()">{% bs_icon "clipboard-check-fill" %} Clone Study {% endif %}
    From c17ebb96ade2135fc1855d707d5af4994ce42661 Mon Sep 17 00:00:00 2001 From: CJ Green <44074998+okaycj@users.noreply.github.com> Date: Wed, 26 Jul 2023 13:27:37 -0400 Subject: [PATCH 06/45] Add runner config edit html --- studies/templates/studies/runner_edit.html | 83 +++++++++++ web/static/js/ace-editor.js | 8 + web/static/js/runner.js | 115 ++++++++++++++ web/static/js/study-edit.js | 25 +--- web/static/js/study-fields.js | 165 +-------------------- 5 files changed, 215 insertions(+), 181 deletions(-) create mode 100644 studies/templates/studies/runner_edit.html create mode 100644 web/static/js/ace-editor.js create mode 100644 web/static/js/runner.js diff --git a/studies/templates/studies/runner_edit.html b/studies/templates/studies/runner_edit.html new file mode 100644 index 000000000..aecb28783 --- /dev/null +++ b/studies/templates/studies/runner_edit.html @@ -0,0 +1,83 @@ +{% extends "exp/base.html" %} +{% load django_bootstrap5 %} +{% load web_extras %} +{% load static %} +{% block title %} + Edit | {{ study.name }} +{% endblock title %} +{% block head %} + {{ block.super }} + {% comment %} ace editor fix cannot be defered {% endcomment %} + + {% comment %} {% endcomment %} + + {{ form.media }} +{% endblock head %} +{% block breadcrumb %} + {% breadcrumb %} + {% url 'exp:study-list' %} Manage Studies + {% url 'exp:study-detail' pk=study.id %} {{ study.name }} + Experiment Runner Edit +{% endbreadcrumb %} +{% endblock breadcrumb %} +{% block content %} + {% button_secondary_classes as btn_secondary_classes %} + {% url 'exp:study-detail' pk=study.pk as url_study_detail %} + {% bootstrap_form_errors form %} +
    + {% csrf_token %} + {% bootstrap_form form %} +
    +
    +

    + About this version +
    + {% bootstrap_button "Check for updates" button_class=btn_secondary_classes id="update-button" %} +
    +

    +
    +
    +

    + Your study will use commit : +

    +
    +
    +
    Date
    +
    +
    +
    +
    Author
    +
    +
    +
    +
    Message
    +
    +
    +
    +
    Files changed
    +
    +
    +
    +
    +
    +
    +
    +

    Update info

    +
    +
    +

    + Since the version you are using, there have been updates to the master branch of https://github.com/lookit/ember-lookit-frameplayer. +

    +
    +
    +
    Date
    +
    Description
    +
    Commit Sha
    +
    +
    +
    +
    + {% bootstrap_button "Submit" type="submit" %} + {% bootstrap_button "Cancel" href=url_study_detail %} +
    +{% endblock content %} diff --git a/web/static/js/ace-editor.js b/web/static/js/ace-editor.js new file mode 100644 index 000000000..a66c1c78c --- /dev/null +++ b/web/static/js/ace-editor.js @@ -0,0 +1,8 @@ +/* Fix for ace-editor. It thinks it's in the admin only */ +(function () { + if (window.jQuery !== undefined) { + window.django = { + 'jQuery': window.jQuery + }; + } +})(); diff --git a/web/static/js/runner.js b/web/static/js/runner.js new file mode 100644 index 000000000..c175a8f46 --- /dev/null +++ b/web/static/js/runner.js @@ -0,0 +1,115 @@ +// Show the generator function field only if use_generator is checked. +function updateGeneratorDisplay() { + const generator = document.querySelector('[for=id_generator]').parentNode; + document.querySelector('#id_use_generator:checked') ? generator.classList.remove('d-none') : generator.classList.add('d-none'); +} + +function updateCommitDescription() { + const httpRequest = new XMLHttpRequest(); + const playerSha = document.querySelector('#id_last_known_player_sha').value.trim(); + const playerRepoUrl = document.querySelector('#id_player_repo_url').value.trim(); + const githubApiUrl = `${playerRepoUrl}/commits/${playerSha}`.replace('github.com', 'api.github.com/repos') + const commitUpdateInfo = document.querySelector('#commit-update-info'); + const commitDescription = document.querySelector('#commit-description'); + + // Hide commit info cards + commitUpdateInfo.classList.add('d-none'); + commitDescription.classList.add('d-none'); + + httpRequest.onreadystatechange = () => { + + if (httpRequest.readyState === XMLHttpRequest.DONE) { + if (httpRequest.status === 200) { + const response = JSON.parse(httpRequest.responseText); + const sha = commitDescription.querySelector('.sha'); + const date = commitDescription.querySelector('.date'); + const name = commitDescription.querySelector('.name'); + const message = commitDescription.querySelector('.message'); + const files = commitDescription.querySelector('.files'); + + sha.innerHTML = response.sha; + sha.href = response.url; + date.innerHTML = response.commit.author.date; + name.innerHTML = response.commit.author.name; + + // Wrap each line of the message in a paragraph element + message.innerHTML = response.commit.message + .split('\n').map(e => `

    ${e}

    `) + .join('') + + // Wrap each file name in a div element and add the file url + files.innerHTML = response.files + .map(e => `
    ${e.status}: ${e.filename}
    `) + .join('') + + // show commit description card + commitDescription.classList.remove('d-none'); + } + } + }; + httpRequest.open("GET", githubApiUrl, true); + httpRequest.send(); +} + +function updateCommitUpdateInfo() { + const httpRequest = new XMLHttpRequest(); + httpRequest.onreadystatechange = () => { + if (httpRequest.readyState === XMLHttpRequest.DONE) { + if (httpRequest.status === 200) { + const response = JSON.parse(httpRequest.responseText); + const commitUpdateInfo = document.querySelector('#commit-update-info'); + const infoRows = commitUpdateInfo.querySelector('.card-body .container'); + + // Clear elements from list + while (infoRows.childNodes.length > 2) { + infoRows.removeChild(infoRows.lastChild) + } + + response.map(e => { + const row = document.createElement('div'); + const date = document.createElement('div'); + const message = document.createElement('div'); + const sha = document.createElement('div'); + + row.classList.add('row', 'pb-3'); + date.classList.add('col-2'); + [message, sha].map(e => e.classList.add('col')); + + date.innerHTML = e.commit.author.date; + message.innerHTML = e.commit.message; + sha.innerHTML = e.sha; + + row.append(date); + row.append(message); + row.append(sha); + infoRows.append(row) + }) + commitUpdateInfo.classList.remove('d-none') + } + } + } + + + const currentCommitDate = document.querySelector('#commit-description .date').innerHTML; + const playerRepoUrl = document.querySelector('#id_player_repo_url').value; + const githubApiUrl = `${playerRepoUrl}/commits?since=${currentCommitDate}`.replace('github.com', 'api.github.com/repos') + httpRequest.open("GET", githubApiUrl, true); + httpRequest.send(); +} + + +/** + * Page load + */ +updateGeneratorDisplay(); +updateCommitDescription(); + +/** + * Event Listeners + */ +document.querySelector('#id_use_generator').addEventListener("click", updateGeneratorDisplay); +document.querySelector('#update-button').addEventListener("click", (event) => { + event.preventDefault(); + updateCommitUpdateInfo(); +}) +document.querySelector('#id_last_known_player_sha').addEventListener('keyup', updateCommitDescription); diff --git a/web/static/js/study-edit.js b/web/static/js/study-edit.js index 2acbcc951..c16a3c4b4 100644 --- a/web/static/js/study-edit.js +++ b/web/static/js/study-edit.js @@ -1,17 +1,8 @@ -/* Fix for ace-editor. It thinks it's in the admin only */ -(function (){ - if (window.jQuery !== undefined) { - window.django = { - 'jQuery': window.jQuery - }; - } -})(); - -$(document).ready(function() { +$(document).ready(function () { $("#save-button").click(function () { $("#invalidate-build-warning").hide(); $("#save-study-confirmation-body").show(); - var runner_type_changed = $("#id_study_type").data("previous") != $("#id_study_type").val(); + let runner_type_changed = $("#id_study_type").data("previous") != $("#id_study_type").val(); $("#study-type-metadata div.metadata-key").each(function (index, element) { runner_type_changed = runner_type_changed || $(element).data("previous") != $("input", element).val(); }); @@ -20,12 +11,12 @@ $(document).ready(function() { } else { let save_study_confirmation_empty = true; $("#save-study-confirmation-body") - .children() - .each(function () { - if (!($(this).css("visibility") == "hidden" || $(this).css("display") == "none")) { - save_study_confirmation_empty = false; - } - }); + .children() + .each(function () { + if (!($(this).css("visibility") == "hidden" || $(this).css("display") == "none")) { + save_study_confirmation_empty = false; + } + }); if (save_study_confirmation_empty) { $("#save-study-confirmation-body").hide(); } diff --git a/web/static/js/study-fields.js b/web/static/js/study-fields.js index 2d5953362..de1361e93 100644 --- a/web/static/js/study-fields.js +++ b/web/static/js/study-fields.js @@ -1,62 +1,3 @@ -// Show the generator function field only if use_generator is checked. -function updateGeneratorDisplay() { - if ($('#id_use_generator:checked').length) { - document.querySelector('#generator-container').classList.remove('d-none'); - } else { - document.querySelector('#generator-container').classList.add('d-none'); - } -} - -// Display an error if generator is not a valid JS function. Returns 0 if no errors, 1 if errors. -function validateGenerator(code) { - - const $errorToDisplay = $('
    '); - const $generatorField = $('#id_generator').closest('.mb-4'); - $generatorField.removeClass('has-error'); - $('#clientside-generator-validation-message').remove(); - - try { - Function(code)(); //NOSONAR - try { - const generatorFn = Function('return ' + code)(); //NOSONAR - if (typeof generatorFn !== 'function') { - throw new Error(); - } - } catch (error) { - $errorToDisplay - .text('Warning: Generator function does not evaluate to a single function. Generator will be disabled if this value is saved.') - .insertBefore($generatorField.children().last()); - $generatorField.addClass('has-error'); - return 1; - } - } catch (error) { - $errorToDisplay - .text('Warning: Invalid Javascript. Generator will be disabled if this value is saved.') - .insertBefore($generatorField.children().last()); - $generatorField.addClass('has-error'); - return 1; - } - return 0; -} - -// Display an error if structure is not a valid JSON string. Returns 0 if no errors, 1 if errors. -function validateStructure(code) { - const $errorToDisplay = $('
    '); - const $protocolField = $('#id_structure').closest('.mb-4'); - $protocolField.removeClass('has-error'); - $('#clientside-protocol-validation-message').remove(); - try { - JSON.parse(code); - } catch (error) { - $errorToDisplay - .text('Warning: Invalid JSON.') - .insertBefore($protocolField.children().last()); - $protocolField.addClass('has-error'); - return 1; - } - return 0; -} - // Update a read-only field to display the calculated max age in days function updateMaxAgeDaysDisplay() { $("#max_age_in_days_val").text(Number($("#id_max_age_years").val()) * 365 + Number($("#id_max_age_months").val()) * 30 + Number($("#id_max_age_days").val())) @@ -67,127 +8,23 @@ function updateMinAgeDaysDisplay() { $("#min_age_in_days_val").text(Number($("#id_min_age_years").val()) * 365 + Number($("#id_min_age_months").val()) * 30 + Number($("#id_min_age_days").val())) } -function setExternal() { - // hide frame player fields through css - document - .querySelectorAll('#type-metadata-1, #structure-container, #generator-container, .use-generator.form-text') - .forEach(e => e.classList.add('d-none')) - // disable frame player metadata fields - document - .querySelectorAll("#type-metadata-1 input") - .forEach(e => e.disabled = true) - // enable external fields - document - .querySelectorAll("#type-metadata-2 input") - .forEach(e => e.disabled = false) - document.querySelector('#id_use_generator').closest('.form-group').classList.add('d-none') - document.querySelector('#type-metadata-2').classList.remove('d-none') -} -function setFramePlayer() { - // hide external fields through css - document - .querySelectorAll('#type-metadata-1, #structure-container, #generator-container, .use-generator.form-text') - .forEach(e => e.classList.remove('d-none')) - // enable frame player metadata fields - document - .querySelectorAll("#type-metadata-1 input") - .forEach(e => e.disabled = false) - // disable external fields - document - .querySelectorAll("#type-metadata-2 input") - .forEach(e => e.disabled = true) - document.querySelector('#id_use_generator').closest('.form-group').classList.remove('d-none') - document.querySelector('#type-metadata-2').classList.add('d-none') - updateGeneratorDisplay(); -} - -function updateStudyType(externalCheckbox) { - externalCheckbox.checked ? setExternal() : setFramePlayer() - updateScheduled() -} - -function updateScheduled() { - const external = document.querySelector('#id_external') - const scheduled = document.querySelector('#id_scheduled') - scheduled.disabled = !external.checked -} - -function updateScheduling(scheduledCheckBox) { - const scheduling = document.querySelector("#id_scheduling").closest('.mb-4') - if (!scheduledCheckBox.disabled && scheduledCheckBox.checked) { - scheduling.classList.remove("d-none") - } else { - scheduling.classList.add("d-none") - } -} -$(document).ready(function () { - // Do initial validation of structure, generator. - validateStructure($('#id_structure').val()); - validateGenerator($('#id_generator').val()); - // Update study form based on study type - const externalCheckbox = document.querySelector('#id_external') - externalCheckbox.addEventListener('click', () => updateStudyType(externalCheckbox)) - updateStudyType(externalCheckbox) - // When use_generator field changes, update whether generator field is displayed - $('#id_use_generator').on('change', function () { - updateGeneratorDisplay(); - }); - // Validate generator function upon closing its editor - $("#generator-container .ace-overlay .save, #generator-container .ace-overlay .cancel").bind("click", function () { - // data field 'editor' bound to editor but only while editor is open - const code = $('#generator-container .ace-overlay').data('editor').getValue(); - validateGenerator(code); - }); - // Validate structure function upon closing its editor - $("#structure-container .ace-overlay .save, #generator-container .ace-overlay .cancel").bind("click", function () { - // data field 'editor' bound to editor but only while editor is open - const code = $('#structure-container .ace-overlay').data('editor').getValue(); - validateStructure(code); - }); - // Use ctrl-S to close editor from either editor. - $(".ace-overlay .edit").bind("click", function () { - const $aceOverlay = $(this).closest('.ace-overlay') - const editor = $aceOverlay.data('editor'); - editor.commands.addCommand({ - name: 'save', - bindKey: { win: 'Ctrl-S', mac: 'Command-S' }, - exec: function () { - $aceOverlay.find('.save').click(); - // Could also consider triggering save button on broader form - }, - readOnly: false // should not apply in readOnly mode - }); - }); - // Upon submit, unset use generator if generator function is invalid - $("#create-study-button, #save-button").bind("click", function () { - if (validateGenerator($('#id_generator').val())) { - const $useGeneratorCheckbox = $('#id_use_generator'); - $useGeneratorCheckbox.prop("checked", false); - $useGeneratorCheckbox[0].value = false; - } - }); +$(document).ready(function () { // Calculate the min/max age in day(s) upon page load & updates $("#id_min_age_years, #id_min_age_months, #id_min_age_days").change(updateMinAgeDaysDisplay); $("#id_max_age_years, #id_max_age_months, #id_max_age_days").change(updateMaxAgeDaysDisplay); updateMinAgeDaysDisplay(); updateMaxAgeDaysDisplay(); - const scheduledCheckBox = document.querySelector("#id_scheduled") - scheduledCheckBox.addEventListener('click', () => { - updateScheduling(scheduledCheckBox) - }) - updateScheduling(scheduledCheckBox) - /* Priority current value */ From c6a41633e150d83f6afbe62830a5db4447eee230 Mon Sep 17 00:00:00 2001 From: CJ Green <44074998+okaycj@users.noreply.github.com> Date: Wed, 26 Jul 2023 13:27:54 -0400 Subject: [PATCH 07/45] Update css --- scss/study-fields.scss | 18 +++++++----------- web/static/custom_bootstrap5.css | 6 ++---- 2 files changed, 9 insertions(+), 15 deletions(-) diff --git a/scss/study-fields.scss b/scss/study-fields.scss index 6de1b3f45..60f6047d4 100644 --- a/scss/study-fields.scss +++ b/scss/study-fields.scss @@ -1,18 +1,14 @@ -#study-details-form, -#study-create-form { - .ace-overlay { - width: 100%; - margin-bottom: 1.5rem; - } +.ace-overlay { + width: 100%; + margin-bottom: 1.5rem; } -#structure-container, -#generator-container { - .code-container { - max-height: 10em; - } + +.code-container { + max-height: 10em; } + span.priority-highest { float: right; } diff --git a/web/static/custom_bootstrap5.css b/web/static/custom_bootstrap5.css index 3c590f78a..096b287fa 100644 --- a/web/static/custom_bootstrap5.css +++ b/web/static/custom_bootstrap5.css @@ -6,13 +6,11 @@ -moz-column-count: 3; -webkit-column-count: 3; } -#study-details-form .ace-overlay, -#study-create-form .ace-overlay { +.ace-overlay { width: 100%; margin-bottom: 1.5rem; } -#structure-container .code-container, -#generator-container .code-container { +.code-container { max-height: 10em; } span.priority-highest { From 85180ac82e51452d544eee87701dc0d5d0533607 Mon Sep 17 00:00:00 2001 From: CJ Green <44074998+okaycj@users.noreply.github.com> Date: Wed, 26 Jul 2023 13:31:46 -0400 Subject: [PATCH 08/45] Move study type field to drop down --- studies/templates/studies/_study_fields.html | 38 ++++++++------------ studies/templates/studies/study_edit.html | 1 - 2 files changed, 14 insertions(+), 25 deletions(-) diff --git a/studies/templates/studies/_study_fields.html b/studies/templates/studies/_study_fields.html index 8b92a0119..0bf1054c1 100644 --- a/studies/templates/studies/_study_fields.html +++ b/studies/templates/studies/_study_fields.html @@ -6,16 +6,7 @@ {% bootstrap_field form.lab label_class="form-label fw-bold" wrapper_class="mb-4" %} {% bootstrap_field form.priority label_class="form-label fw-bold" wrapper_class="mb-4" addon_before="" %}
    -
    -
    - -

    - Choose what type of study you are creating - this will change the fields that appear in the Experiment Details section. -

    - {% bootstrap_field form.external %} - {% bootstrap_field form.scheduled wrapper_class="" %} -
    -
    +{% bootstrap_field form.study_type label_class="form-label fw-bold" wrapper_class="mb-4" %}
    @@ -59,7 +50,7 @@ title="" id="id_min_age_years"> {% for x, y in form.fields.min_age_years.choices %} - {% endfor %} @@ -93,7 +84,7 @@ name="min_age_days" title=""> {% for x, y in form.fields.min_age_days.choices %} - {% endfor %} @@ -123,7 +114,7 @@ title="" id="id_max_age_years"> {% for x, y in form.fields.max_age_years.choices %} - {% endfor %} @@ -140,8 +131,7 @@ name="max_age_months" title=""> {% for x, y in form.fields.max_age_months.choices %} - {% endfor %} @@ -158,7 +148,7 @@ name="max_age_days" title=""> {% for x, y in form.fields.max_age_days.choices %} - {% endfor %} @@ -180,18 +170,18 @@ {% bootstrap_field form.must_have_participated label_class="form-label fw-bold" wrapper_class="mb-4" %} {% bootstrap_field form.must_not_have_participated label_class="form-label fw-bold" wrapper_class="mb-4" %} {% bootstrap_field form.criteria_expression label_class="form-label fw-bold" wrapper_class="mb-4" %} -
    -
    +{% comment %}
    {% endcomment %} +{% comment %}

    Now it's time for the actual study! For internal studies, you will add a protocol configuration or generator. For external studies, you will paste in your study or scheduling link. If you don't see what you expect, check the "External" and "Scheduled" checkboxes at the top of this form!

    -
    -
    {% bootstrap_field form.structure label_class="form-label fw-bold" %}
    -{% bootstrap_field form.use_generator label_class="form-label" wrapper_class="form-group" %} -
    {{ form.generator.help_text|safe }}
    -
    +
    {% endcomment %} +{% comment %}
    {% bootstrap_field form.structure label_class="form-label fw-bold" %}
    {% endcomment %} +{% comment %} {% bootstrap_field form.use_generator label_class="form-label" wrapper_class="form-group" %} {% endcomment %} +{% comment %}
    {{ form.generator.help_text|safe }}
    {% endcomment %} +{% comment %}
    {% bootstrap_field form.generator show_help=False label_class="form-label fw-bold" %} -
    +
    {% endcomment %} diff --git a/studies/templates/studies/study_edit.html b/studies/templates/studies/study_edit.html index 4fc587dd0..14302e4a0 100644 --- a/studies/templates/studies/study_edit.html +++ b/studies/templates/studies/study_edit.html @@ -42,7 +42,6 @@

    {% csrf_token %} {% include "studies/_study_fields.html" with form=form study=study %} - {% include "studies/_study_type.html" with types=types create=0 currentType=study.study_type.id %} {% form_buttons %} {% bootstrap_button "Cancel" button_class=btn_secondary_classes href=url_cancel %}

    -
    -
    -

    Update info

    +
    +

    + Your study will use commit : +

    +
    +
    +
    Date
    +
    +
    +
    +
    Author
    +
    +
    +
    +
    Message
    +
    +
    +
    +
    Files changed
    +
    +
    -
    -

    - Since the version you are using, there have been updates to the master branch of https://github.com/lookit/ember-lookit-frameplayer. -

    -
    -
    -
    Date
    -
    Description
    -
    Commit Sha
    -
    +
    +
    +
    +
    +

    Update info

    +
    +
    +

    + Since the version you are using, there have been updates to the master branch of https://github.com/lookit/ember-lookit-frameplayer. +

    +
    +
    +
    Date
    +
    Description
    +
    Commit Sha
    - {% bootstrap_button "Submit" type="submit" %} - {% bootstrap_button "Cancel" href=url_study_detail %} - -{% endblock content %} +
    +{% endblock form %} diff --git a/studies/templates/studies/experiment_runner/external_edit.html b/studies/templates/studies/experiment_runner/external_edit.html index 55b7bf76a..0605940bb 100644 --- a/studies/templates/studies/experiment_runner/external_edit.html +++ b/studies/templates/studies/experiment_runner/external_edit.html @@ -1,29 +1,12 @@ -{% extends "exp/base.html" %} +{% extends "studies/experiment_runner/base.html" %} {% load django_bootstrap5 %} -{% load web_extras %} {% load static %} -{% block title %} - Edit | {{ study.name }} -{% endblock title %} +{% load web_extras %} {% block head %} {{ block.super }} {% comment %} {% endcomment %} {% endblock head %} -{% block breadcrumb %} - {% breadcrumb %} - {% url 'exp:study-list' %} Manage Studies - {% url 'exp:study-detail' pk=study.id %} {{ study.name }} - Experiment Runner Edit -{% endbreadcrumb %} -{% endblock breadcrumb %} -{% block content %} - {% url 'exp:study-detail' pk=study.pk as url_study_detail %} - {% bootstrap_form_errors form %} -
    - {% csrf_token %} - {% bootstrap_form form %} - {% bootstrap_button "Submit" type="submit" %} - {% bootstrap_button "Cancel" href=url_study_detail %} -
    -{% endblock content %} +{% block form %} + {% bootstrap_form form %} +{% endblock form %} From bd21df87671892968b416d1d61ee5f7388216370 Mon Sep 17 00:00:00 2001 From: CJ Green <44074998+okaycj@users.noreply.github.com> Date: Fri, 4 Aug 2023 15:21:50 -0400 Subject: [PATCH 14/45] Add preview link to study details, fix view names --- exp/views/study.py | 2 +- .../studies/experiment_runner/base.html | 18 +++++++++++++----- .../experiment_runner/external_edit.html | 1 - studies/templates/studies/study_detail.html | 4 ++-- studies/templates/studies/study_edit.html | 3 +-- 5 files changed, 17 insertions(+), 11 deletions(-) diff --git a/exp/views/study.py b/exp/views/study.py index dc044ea6a..245411807 100644 --- a/exp/views/study.py +++ b/exp/views/study.py @@ -306,7 +306,7 @@ def get_context_data(self, **kwargs): return context def get_success_url(self): - return reverse("exp:study-edit", kwargs={"pk": self.object.id}) + return reverse("exp:study-detail", kwargs={"pk": self.object.id}) class StudyListView( diff --git a/studies/templates/studies/experiment_runner/base.html b/studies/templates/studies/experiment_runner/base.html index 0d570bd47..2e7ddef31 100644 --- a/studies/templates/studies/experiment_runner/base.html +++ b/studies/templates/studies/experiment_runner/base.html @@ -1,5 +1,6 @@ {% extends "exp/base.html" %} {% load django_bootstrap5 %} +{% load bootstrap_icons %} {% load web_extras %} {% load static %} {% block title %} @@ -9,15 +10,24 @@ {% breadcrumb %} {% url 'exp:study-list' %} Manage Studies {% url 'exp:study-detail' pk=study.id %} {{ study.name }} - Experiment Runner Edit + Edit {% endbreadcrumb %} {% endblock breadcrumb %} {% block content %} + {% button_secondary_classes as btn_secondary_classes %} + {% button_primary_classes as btn_primary_classes %} + {% url 'exp:preview-detail' uuid=study.uuid as url_preview_detail %} + {% bs_icon "play-circle" as bs_icon_play_circle %} {% bootstrap_form_errors form %}
    -

    Experiment Runner Editor

    +

    + Study Details +
    + {% bootstrap_button bs_icon_play_circle|add:"Preview Study" href=url_preview_detail button_class=btn_secondary_classes %} +
    +

    {% block header %} @@ -27,10 +37,8 @@

    Experiment Runner Editor

    {% block form %} {% endblock form %} {% form_buttons %} - {% button_secondary_classes as btn_secondary_classes %} - {% button_primary_classes as btn_primary_classes %} {% bootstrap_button "Cancel" button_class=btn_secondary_classes href=url_study_detail %} - {% bootstrap_button "Submit" button_class=btn_primary_classes type="submit" %} + {% bootstrap_button "Save Changes" button_class=btn_primary_classes type="submit" %} {% endform_buttons %}
    diff --git a/studies/templates/studies/experiment_runner/external_edit.html b/studies/templates/studies/experiment_runner/external_edit.html index 0605940bb..113c1383b 100644 --- a/studies/templates/studies/experiment_runner/external_edit.html +++ b/studies/templates/studies/experiment_runner/external_edit.html @@ -4,7 +4,6 @@ {% load web_extras %} {% block head %} {{ block.super }} - {% comment %} {% endcomment %} {% endblock head %} {% block form %} diff --git a/studies/templates/studies/study_detail.html b/studies/templates/studies/study_detail.html index df619e368..6ad45f9c5 100644 --- a/studies/templates/studies/study_detail.html +++ b/studies/templates/studies/study_detail.html @@ -139,9 +139,9 @@ {% endif %} {% if can_edit_study_details %} {% bs_icon "pencil-square" %} Edit Study + href="{% url 'exp:study-edit' pk=study.id %}">{% bs_icon "pencil-square" %}Study Ad {% bs_icon "pencil-square" %} Edit Runner + href="{% url 'exp:runner-edit' pk=study.id %}">{% bs_icon "pencil-square" %}Study Details {% endif %} {% if "read_study__responses" in study_perms or "read_study__responses(is_preview=True)" in study_perms %}

    - Study Editor + Study Ad
    {% bootstrap_button bs_icon_play_circle|add:"Preview Study" href=url_preview_detail button_class=btn_secondary_classes %}
    From 64eff19d33280fcfa3387b645ac95c124eee698d Mon Sep 17 00:00:00 2001 From: CJ Green <44074998+okaycj@users.noreply.github.com> Date: Sat, 5 Aug 2023 10:31:15 -0400 Subject: [PATCH 15/45] Rename study views There are two view named "Study details". The original view is renamed to "Study" and all urls have been updated. Additionally, Clicking save on Study Ad moves you to the new Study Details view. --- .../accounts/participant_detail.html | 2 +- ...otify_researcher_of_study_permissions.html | 2 +- ...notify_researcher_of_study_permissions.txt | 2 +- accounts/tests/test_accounts.py | 2 +- exp/tests/test_study_views.py | 16 +++++++-------- exp/urls.py | 14 ++++++++----- exp/views/responses.py | 2 +- exp/views/study.py | 20 +++++++++++-------- .../emails/notify_admins_of_study_action.html | 10 +++++----- .../emails/notify_admins_of_study_action.txt | 10 +++++----- ...tify_researchers_of_approval_decision.html | 2 +- ...otify_researchers_of_approval_decision.txt | 2 +- .../notify_researchers_of_build_failure.html | 2 +- .../notify_researchers_of_build_failure.txt | 2 +- .../notify_researchers_of_deployment.html | 2 +- .../notify_researchers_of_deployment.txt | 2 +- .../studies/_all_json_and_csv_data.html | 2 +- .../studies/experiment_runner/base.html | 2 +- .../studies/experiment_runner/efp_edit.html | 2 +- .../templates/studies/study_attachments.html | 6 +++--- studies/templates/studies/study_detail.html | 2 +- .../studies/study_detail/_modal.html | 4 ++-- .../studies/study_detail/_study_status.html | 2 +- studies/templates/studies/study_edit.html | 2 +- studies/templates/studies/study_list.html | 8 ++++---- .../studies/study_participant_contact.html | 8 ++++---- .../templates/studies/study_responses.html | 12 +++++------ .../study_responses_consent_ruling.html | 10 +++++----- web/static/js/external-runner.js | 2 +- web/templates/web/study-detail.html | 2 +- 30 files changed, 82 insertions(+), 74 deletions(-) diff --git a/accounts/templates/accounts/participant_detail.html b/accounts/templates/accounts/participant_detail.html index e750a57bb..e65572d96 100644 --- a/accounts/templates/accounts/participant_detail.html +++ b/accounts/templates/accounts/participant_detail.html @@ -239,7 +239,7 @@ {% for study in studies %}

    - {{ study.study.name }} + {{ study.study.name }} {{ study.response.uuid }} diff --git a/accounts/templates/emails/notify_researcher_of_study_permissions.html b/accounts/templates/emails/notify_researcher_of_study_permissions.html index 1fd0bd31c..f3094b2ef 100644 --- a/accounts/templates/emails/notify_researcher_of_study_permissions.html +++ b/accounts/templates/emails/notify_researcher_of_study_permissions.html @@ -1,7 +1,7 @@

    Dear {{ researcher_name }},

    You have been given {{ permission }} permissions to collaborate on {{ study_name }}.

    - Here is a link to start collaborating. + Here is a link to start collaborating.

    Best, diff --git a/accounts/templates/emails/notify_researcher_of_study_permissions.txt b/accounts/templates/emails/notify_researcher_of_study_permissions.txt index 3e9638791..bb371b309 100644 --- a/accounts/templates/emails/notify_researcher_of_study_permissions.txt +++ b/accounts/templates/emails/notify_researcher_of_study_permissions.txt @@ -2,7 +2,7 @@ Dear {{ researcher_name }}, You have been given {{ permission }} permissions to collaborate on {{ study_name }}. -Here is a link to start collaborating: {{ base_url }}{% url 'exp:study-detail' study_id %}. +Here is a link to start collaborating: {{ base_url }}{% url 'exp:study' study_id %}. Best, {{ lab_name }} Admin diff --git a/accounts/tests/test_accounts.py b/accounts/tests/test_accounts.py index 58d06dffc..0dffe071d 100644 --- a/accounts/tests/test_accounts.py +++ b/accounts/tests/test_accounts.py @@ -106,7 +106,7 @@ def setUp(self): reverse("exp:participant-detail", kwargs={"pk": self.participant.pk}), reverse("exp:study-participant-analytics"), reverse("exp:study-create"), - reverse("exp:study-detail", kwargs={"pk": self.study.pk}), + reverse("exp:study", kwargs={"pk": self.study.pk}), reverse("exp:study-participant-contact", kwargs={"pk": self.study.pk}), reverse("exp:study-edit", kwargs={"pk": self.study.pk}), reverse("exp:study-responses-list", kwargs={"pk": self.study.pk}), diff --git a/exp/tests/test_study_views.py b/exp/tests/test_study_views.py index e5da63021..ea9aa0f90 100644 --- a/exp/tests/test_study_views.py +++ b/exp/tests/test_study_views.py @@ -151,7 +151,7 @@ def setUp(self): self.all_study_views_urls = [ reverse("exp:study-list"), reverse("exp:study-create"), - reverse("exp:study-detail", kwargs={"pk": self.study.pk}), + reverse("exp:study", kwargs={"pk": self.study.pk}), reverse("exp:study-participant-contact", kwargs={"pk": self.study.pk}), reverse("exp:preview-detail", kwargs={"uuid": self.study.uuid}), reverse( @@ -530,7 +530,7 @@ def test_create_study_buttons_shown_if_allowed(self): "Create Study button not displayed on study list view", ) detail_view_response = self.client.get( - reverse("exp:study-detail", kwargs={"pk": self.study.pk}) + reverse("exp:study", kwargs={"pk": self.study.pk}) ) self.assertIn( "Clone Study", @@ -556,7 +556,7 @@ def test_create_study_buttons_not_shown_if_not_allowed(self): "Create Study button displayed on study list view", ) detail_view_response = self.client.get( - reverse("exp:study-detail", kwargs={"pk": self.study.pk}) + reverse("exp:study", kwargs={"pk": self.study.pk}) ) self.assertNotIn( "Clone Study", @@ -727,7 +727,7 @@ def test_post( self.assertEqual(change_study_status_view.post(), mock_http_response_redirect()) mock_http_response_redirect.assert_called_with() mock_reverse.assert_called_with( - "exp:study-detail", kwargs={"pk": mock_get_object().pk} + "exp:study", kwargs={"pk": mock_get_object().pk} ) change_study_status_view.update_trigger.assert_called_with() @@ -753,7 +753,7 @@ def test_post_exception( mock_request, f"TRANSITION ERROR: {sentinel.error_message}" ) mock_reverse.assert_called_with( - "exp:study-detail", kwargs={"pk": mock_get_object().pk} + "exp:study", kwargs={"pk": mock_get_object().pk} ) @patch.object(ChangeStudyStatusView, "request", create=True) @@ -869,7 +869,7 @@ def test_post( ) mock_https_response_redirect.assert_called_with() mock_reverse.assert_called_once_with( - "exp:study-detail", kwargs={"pk": mock_get_object().pk} + "exp:study", kwargs={"pk": mock_get_object().pk} ) mock_manage_researcher_permissions.assert_called_once_with() @@ -1314,14 +1314,14 @@ def test_user_can_see_or_edit_study_details( def test_study_detail_review_consent(self): # check if review consent is viewable on a frame player study response = self.client.get( - reverse("exp:study-detail", kwargs={"pk": self.frame_player_study.pk}) + reverse("exp:study", kwargs={"pk": self.frame_player_study.pk}) ) self.assertEqual(200, response.status_code) self.assertIn(b"Review Consent", response.content) # check that review consent is not view on an external study response = self.client.get( - reverse("exp:study-detail", kwargs={"pk": self.external_study.pk}) + reverse("exp:study", kwargs={"pk": self.external_study.pk}) ) self.assertEqual(200, response.status_code) self.assertNotIn(b"Review Consent", response.content) diff --git a/exp/urls.py b/exp/urls.py index f2af2eba1..8469105cf 100644 --- a/exp/urls.py +++ b/exp/urls.py @@ -124,7 +124,7 @@ name="study-participant-analytics", ), path("studies/create/", StudyCreateView.as_view(), name="study-create"), - path("studies//", StudyDetailView.as_view(), name="study-detail"), + path("studies//", StudyDetailView.as_view(), name="study"), path("studies//clone-study", CloneStudyView.as_view(), name="clone-study"), path( "studies//change-study-status", @@ -255,12 +255,16 @@ ), path("support/", SupportView.as_view(), name="support"), path( - "studies//runner/edit/", + "studies//study-detail/", ExperimentRunnerEdit.as_view(), - name="runner-edit", + name="study-detail", ), - path("studies//efp/edit/", EFPEdit.as_view(), name="efp-edit"), path( - "studies//external/edit/", ExternalEdit.as_view(), name="external-edit" + "studies//study-detail/efp/", EFPEdit.as_view(), name="efp-study-detail" + ), + path( + "studies//study-detail/external/", + ExternalEdit.as_view(), + name="external-study-detail", ), ] diff --git a/exp/views/responses.py b/exp/views/responses.py index 02293feb8..f87c1558e 100644 --- a/exp/views/responses.py +++ b/exp/views/responses.py @@ -857,7 +857,7 @@ def post(self, request, *args, **kwargs): def get(self, request, *args, **kwargs): if self.get_object().study_type.is_external: messages.error(request, "There is no consent manager for external studies.") - return HttpResponseRedirect(reverse("exp:study-detail", kwargs=kwargs)) + return HttpResponseRedirect(reverse("exp:study", kwargs=kwargs)) else: return super().get(request, *args, **kwargs) diff --git a/exp/views/study.py b/exp/views/study.py index 245411807..876edca34 100644 --- a/exp/views/study.py +++ b/exp/views/study.py @@ -154,7 +154,7 @@ def form_valid(self, form): return HttpResponseRedirect(self.get_success_url()) def get_success_url(self): - return reverse("exp:study-detail", kwargs=dict(pk=self.object.id)) + return reverse("exp:study", kwargs=dict(pk=self.object.id)) def get_context_data(self, **kwargs): """ @@ -598,7 +598,7 @@ def post(self, *args, **kwargs): return HttpResponseForbidden() return HttpResponseRedirect( - reverse("exp:study-detail", kwargs=dict(pk=self.get_object().pk)) + reverse("exp:study", kwargs=dict(pk=self.get_object().pk)) ) def send_study_email(self, user, permission): @@ -737,7 +737,7 @@ def post(self, *args, **kwargs): messages.error(self.request, f"TRANSITION ERROR: {e}") return HttpResponseRedirect( - reverse("exp:study-detail", kwargs=dict(pk=self.get_object().pk)) + reverse("exp:study", kwargs=dict(pk=self.get_object().pk)) ) def update_declarations(self, trigger: Text, study: Study): @@ -862,7 +862,7 @@ class StudyBuildView( slug_field = "uuid" def get_redirect_url(self, *args, **kwargs): - return reverse("exp:study-detail", kwargs={"pk": str(self.get_object().pk)}) + return reverse("exp:study", kwargs={"pk": str(self.get_object().pk)}) def user_can_build_study(self): user = self.request.user @@ -1093,9 +1093,13 @@ def get(self, request: HttpRequest, *args: str, **kwargs: Any) -> HttpResponse: study_type = self.object.study_type if study_type.is_ember_frame_player: - return redirect(reverse("exp:efp-edit", kwargs={"pk": self.object.id})) + return redirect( + reverse("exp:efp-study-detail", kwargs={"pk": self.object.id}) + ) if study_type.is_external: - return redirect(reverse("exp:external-edit", kwargs={"pk": self.object.id})) + return redirect( + reverse("exp:external-study-detail", kwargs={"pk": self.object.id}) + ) class EFPEdit( @@ -1173,7 +1177,7 @@ def form_valid(self, form: BaseModelForm) -> HttpResponse: def get_success_url(self, **kwargs): """Upon successful form submission, change the view to study detail.""" - return reverse("exp:study-detail", kwargs={"pk": self.object.pk}) + return reverse("exp:study", kwargs={"pk": self.object.pk}) class ExternalEdit( @@ -1206,7 +1210,7 @@ def user_can_edit_study(self): def get_success_url(self, **kwargs): """Upon successful form submission, change the view to study detail.""" - return reverse("exp:study-detail", kwargs={"pk": self.object.pk}) + return reverse("exp:study", kwargs={"pk": self.object.pk}) def get_initial(self): initial = super().get_initial() diff --git a/studies/templates/emails/notify_admins_of_study_action.html b/studies/templates/emails/notify_admins_of_study_action.html index f314449bf..c16b4db7b 100644 --- a/studies/templates/emails/notify_admins_of_study_action.html +++ b/studies/templates/emails/notify_admins_of_study_action.html @@ -7,19 +7,19 @@
    {{ comments|linebreaks }}
    - You can approve or disapprove the study here. + You can approve or disapprove the study here. {% elif action == 'retracted' %} {{ researcher_name }} has retracted the submission of a study: {{ study_name }} {% elif action == 'active' %} {{ researcher_name }} has started the study - {{ study_name }}. + {{ study_name }}. {% elif action == 'paused' %} {{ researcher_name }} has paused the study - {{ study_name }}. + {{ study_name }}. {% elif action == 'deactivated' %} {{ researcher_name }} has deactivated the study - {{ study_name }}. + {{ study_name }}. {% elif action == 'deployed' %} - An experiment runner has been built for {{ study_name }}. This study can be previewed here. When this study is approved and activated, participants will be able to access it here. + An experiment runner has been built for {{ study_name }}. This study can be previewed here. When this study is approved and activated, participants will be able to access it here. {% endif %}

    diff --git a/studies/templates/emails/notify_admins_of_study_action.txt b/studies/templates/emails/notify_admins_of_study_action.txt index 37f08e709..9b48751fe 100644 --- a/studies/templates/emails/notify_admins_of_study_action.txt +++ b/studies/templates/emails/notify_admins_of_study_action.txt @@ -7,17 +7,17 @@ Dear Lookit Admin, {{ comments|linebreaks }} - You can approve or disapprove the study here: {{ base_url }}{% url 'exp:study-detail' pk=study_id %} + You can approve or disapprove the study here: {{ base_url }}{% url 'exp:study' pk=study_id %} {% elif action == 'retracted' %} {{ researcher_name }} has retracted the submission of a study: {{ study_name }} {% elif action == 'active' %} - {{ researcher_name }} has started the study {{study_name}}. {{ base_url }}{% url 'exp:study-detail' pk=study_id %} + {{ researcher_name }} has started the study {{study_name}}. {{ base_url }}{% url 'exp:study' pk=study_id %} {% elif action == 'paused' %} - {{ researcher_name }} has paused the study {{study_name}}. {{ base_url }}{% url 'exp:study-detail' pk=study_id %} + {{ researcher_name }} has paused the study {{study_name}}. {{ base_url }}{% url 'exp:study' pk=study_id %} {% elif action == 'deactivated' %} - {{ researcher_name }} has deactivated the study {{study_name}}. {{ base_url }}{% url 'exp:study-detail' pk=study_id %} + {{ researcher_name }} has deactivated the study {{study_name}}. {{ base_url }}{% url 'exp:study' pk=study_id %} {% elif action == 'deployed' %} - An experiment runner has been built for {{ study_name }} ({{ base_url }}{% url 'exp:study-detail' pk=study_id %}) + An experiment runner has been built for {{ study_name }} ({{ base_url }}{% url 'exp:study' pk=study_id %}) This study can be previewed here: {{base_url}}{% url 'exp:preview-detail' uuid=study_uuid %} diff --git a/studies/templates/emails/notify_researchers_of_approval_decision.html b/studies/templates/emails/notify_researchers_of_approval_decision.html index 309c17d54..00a97bca1 100644 --- a/studies/templates/emails/notify_researchers_of_approval_decision.html +++ b/studies/templates/emails/notify_researchers_of_approval_decision.html @@ -13,7 +13,7 @@ You can modify your study and resubmit for approval. {% endif %}
    - Your study can be found here. + Your study can be found here.

    {% if comments %}

    diff --git a/studies/templates/emails/notify_researchers_of_approval_decision.txt b/studies/templates/emails/notify_researchers_of_approval_decision.txt index 8b896391f..0d0b281d6 100644 --- a/studies/templates/emails/notify_researchers_of_approval_decision.txt +++ b/studies/templates/emails/notify_researchers_of_approval_decision.txt @@ -12,7 +12,7 @@ To start your study, log in to Lookit, navigate to the study, and select "Start" You can modify your study and resubmit for approval. {% endif %} -Your study can be found here: {{base_url}}{% url 'exp:study-detail' study_id %} +Your study can be found here: {{base_url}}{% url 'exp:study' study_id %} {% if comments %} Comments from the Lookit Admin: diff --git a/studies/templates/emails/notify_researchers_of_build_failure.html b/studies/templates/emails/notify_researchers_of_build_failure.html index aca8371be..52d12e84b 100644 --- a/studies/templates/emails/notify_researchers_of_build_failure.html +++ b/studies/templates/emails/notify_researchers_of_build_failure.html @@ -1,6 +1,6 @@

    Dear Study Researchers,

    - The experiment runner for your study, {{ study_name }}, + The experiment runner for your study, {{ study_name }}, has failed to build.

    It failed during the {{ failure_stage }} stage. Logs are provided below to help with troubleshooting.

    diff --git a/studies/templates/emails/notify_researchers_of_build_failure.txt b/studies/templates/emails/notify_researchers_of_build_failure.txt index e2dd59879..7744d82a6 100644 --- a/studies/templates/emails/notify_researchers_of_build_failure.txt +++ b/studies/templates/emails/notify_researchers_of_build_failure.txt @@ -1,6 +1,6 @@ Dear Study Researchers, - The experiment runner for your study, {{ study_name }} ({{base_url}}{% url 'exp:study-detail' pk=study_id %}), + The experiment runner for your study, {{ study_name }} ({{base_url}}{% url 'exp:study' pk=study_id %}), has failed to build. It failed during the {{ failure_stage }} stage. Logs are provided below to help with troubleshooting. diff --git a/studies/templates/emails/notify_researchers_of_deployment.html b/studies/templates/emails/notify_researchers_of_deployment.html index f881c4107..551b2b542 100644 --- a/studies/templates/emails/notify_researchers_of_deployment.html +++ b/studies/templates/emails/notify_researchers_of_deployment.html @@ -1,4 +1,4 @@

    Dear Study Researchers,

    - An experiment runner has been built for {{ study_name }}. This study can now be previewed here. When this study is approved and activated, participants will be able to access it here. + An experiment runner has been built for {{ study_name }}. This study can now be previewed here. When this study is approved and activated, participants will be able to access it here.

    diff --git a/studies/templates/emails/notify_researchers_of_deployment.txt b/studies/templates/emails/notify_researchers_of_deployment.txt index 32fc000a8..3abf2e442 100644 --- a/studies/templates/emails/notify_researchers_of_deployment.txt +++ b/studies/templates/emails/notify_researchers_of_deployment.txt @@ -1,6 +1,6 @@ Dear Study Researchers, - An experiment runner has been built for {{ study_name}} ({{ base_url }}{% url 'exp:study-detail' pk=study_id %}). + An experiment runner has been built for {{ study_name}} ({{ base_url }}{% url 'exp:study' pk=study_id %}). This study can now be previewed here: {{ base_url }}{% url 'exp:preview-detail' uuid=study_uuid %} diff --git a/studies/templates/studies/_all_json_and_csv_data.html b/studies/templates/studies/_all_json_and_csv_data.html index b5bd98b0f..a4a805d83 100644 --- a/studies/templates/studies/_all_json_and_csv_data.html +++ b/studies/templates/studies/_all_json_and_csv_data.html @@ -16,7 +16,7 @@ {% block breadcrumb %} {% breadcrumb %} {% url 'exp:study-list' %} Manage Studies - {% url 'exp:study-detail' pk=study.id %} {{ study.name }} + {% url 'exp:study' pk=study.id %} {{ study.name }} {{ active_tab }} {% endbreadcrumb %} {% endblock breadcrumb %} diff --git a/studies/templates/studies/experiment_runner/base.html b/studies/templates/studies/experiment_runner/base.html index 2e7ddef31..b616c1483 100644 --- a/studies/templates/studies/experiment_runner/base.html +++ b/studies/templates/studies/experiment_runner/base.html @@ -9,7 +9,7 @@ {% block breadcrumb %} {% breadcrumb %} {% url 'exp:study-list' %} Manage Studies - {% url 'exp:study-detail' pk=study.id %} {{ study.name }} + {% url 'exp:study' pk=study.id %} {{ study.name }} Edit {% endbreadcrumb %} {% endblock breadcrumb %} diff --git a/studies/templates/studies/experiment_runner/efp_edit.html b/studies/templates/studies/experiment_runner/efp_edit.html index 541104e8c..303229819 100644 --- a/studies/templates/studies/experiment_runner/efp_edit.html +++ b/studies/templates/studies/experiment_runner/efp_edit.html @@ -18,7 +18,7 @@

    {% endblock header %} {% block form %} - {% url 'exp:study-detail' pk=study.pk as url_study_detail %} + {% url 'exp:study' pk=study.pk as url_study_detail %} {% bootstrap_form form %}
    diff --git a/studies/templates/studies/study_attachments.html b/studies/templates/studies/study_attachments.html index 259b4bc0a..13d9491bd 100644 --- a/studies/templates/studies/study_attachments.html +++ b/studies/templates/studies/study_attachments.html @@ -11,7 +11,7 @@ {% block breadcrumb %} {% breadcrumb %} {% url 'exp:study-list' %} Manage Studies - {% url 'exp:study-detail' pk=study.id %} {{ study.name }} + {% url 'exp:study' pk=study.id %} {{ study.name }} Videos {% endbreadcrumb %} {% endblock breadcrumb %} @@ -46,7 +46,7 @@ placeholder="Filter video name" type="text" value="{{ match }}"/> - +
    @@ -85,7 +85,7 @@
    {{ video.created_at|date:"n/j/Y g:i A"|default:"N/A" }} Download + class="{% button_primary_classes %} btn-sm">Download