From 004bfad202ca50f9cf8199d82b4ba1db6e6ab900 Mon Sep 17 00:00:00 2001 From: fujimeh Date: Thu, 25 May 2023 12:44:51 -0400 Subject: [PATCH 01/15] feat: Feature support for handling terms of service for users This supports platform or subsites terms of service --- common/djangoapps/student/helpers.py | 9 ++ common/djangoapps/student/views/dashboard.py | 3 + lms/djangoapps/static_template_view/views.py | 7 +- lms/static/sass/_build-lms-v1.scss | 1 + lms/static/sass/features/_termsofservice.scss | 105 +++++++++++++++ lms/templates/dashboard.html | 7 + lms/templates/static_templates/tos.html | 116 +++++++++++++++- .../termsofservice/modal_termsofservice.html | 10 ++ lms/templates/termsofservice/tos_content.html | 3 + lms/urls.py | 3 + openedx/features/termsofservice/__init__.py | 0 openedx/features/termsofservice/admin.py | 19 +++ .../features/termsofservice/api/__init__.py | 0 .../termsofservice/api/v1/__init__.py | 0 .../features/termsofservice/api/v1/urls.py | 12 ++ .../features/termsofservice/api/v1/views.py | 86 ++++++++++++ openedx/features/termsofservice/apps.py | 32 +++++ .../termsofservice/migrations/0001_initial.py | 54 ++++++++ .../termsofservice/migrations/__init__.py | 0 openedx/features/termsofservice/models.py | 51 +++++++ .../termsofservice/settings/__init__.py | 0 .../termsofservice/settings/common.py | 28 ++++ .../static/termsofservice/jsx/Modal.jsx | 124 ++++++++++++++++++ openedx/features/termsofservice/tests.py | 3 + openedx/features/termsofservice/urls.py | 7 + openedx/features/termsofservice/views.py | 23 ++++ setup.py | 1 + webpack.common.config.js | 1 + 28 files changed, 702 insertions(+), 3 deletions(-) create mode 100644 lms/static/sass/features/_termsofservice.scss create mode 100644 lms/templates/termsofservice/modal_termsofservice.html create mode 100644 lms/templates/termsofservice/tos_content.html create mode 100644 openedx/features/termsofservice/__init__.py create mode 100644 openedx/features/termsofservice/admin.py create mode 100644 openedx/features/termsofservice/api/__init__.py create mode 100644 openedx/features/termsofservice/api/v1/__init__.py create mode 100644 openedx/features/termsofservice/api/v1/urls.py create mode 100644 openedx/features/termsofservice/api/v1/views.py create mode 100644 openedx/features/termsofservice/apps.py create mode 100644 openedx/features/termsofservice/migrations/0001_initial.py create mode 100644 openedx/features/termsofservice/migrations/__init__.py create mode 100644 openedx/features/termsofservice/models.py create mode 100644 openedx/features/termsofservice/settings/__init__.py create mode 100644 openedx/features/termsofservice/settings/common.py create mode 100644 openedx/features/termsofservice/static/termsofservice/jsx/Modal.jsx create mode 100644 openedx/features/termsofservice/tests.py create mode 100644 openedx/features/termsofservice/urls.py create mode 100644 openedx/features/termsofservice/views.py diff --git a/common/djangoapps/student/helpers.py b/common/djangoapps/student/helpers.py index 71b46d5af5ec..abf37a13af89 100644 --- a/common/djangoapps/student/helpers.py +++ b/common/djangoapps/student/helpers.py @@ -56,6 +56,8 @@ from openedx.core.djangoapps.user_authn.utils import is_safe_login_or_logout_redirect from xmodule.data import CertificatesDisplayBehaviors # lint-amnesty, pylint: disable=wrong-import-order +from openedx.features.termsofservice.models import TermsOfServiceAcknowledgement, TermsOfService + # Enumeration of per-course verification statuses # we display on the student dashboard. VERIFY_STATUS_NEED_TO_VERIFY = "verify_need_to_verify" @@ -749,6 +751,13 @@ def do_create_account(form, custom_form=None): except Exception: log.exception(f"UserProfile creation failed for user {user.id}.") raise + + try: + tos_acknowledgement = TermsOfServiceAcknowledgement(user_id = user.id, terms_of_service_id = TermsOfService.objects.latest('date_modified').id) + tos_acknowledgement.save() + except Exception: + log.exception(f"Error creating User Terms of Service Agreement {user.id}.") + raise return user, profile, registration diff --git a/common/djangoapps/student/views/dashboard.py b/common/djangoapps/student/views/dashboard.py index 342fefe24e57..7d35ddf42c83 100644 --- a/common/djangoapps/student/views/dashboard.py +++ b/common/djangoapps/student/views/dashboard.py @@ -61,6 +61,7 @@ ) from common.djangoapps.util.milestones_helpers import get_pre_requisite_courses_not_completed from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order +from openedx.features.termsofservice.api.v1 import views as tos_views log = logging.getLogger("edx.student") @@ -762,6 +763,7 @@ def student_dashboard(request): # lint-amnesty, pylint: disable=too-many-statem show_account_activation_popup = request.COOKIES.get(settings.SHOW_ACTIVATE_CTA_POPUP_COOKIE_NAME, None) + tos_modal = tos_views.terms_of_service_api(request) context = { 'urls': urls, 'programs_data': programs_data, @@ -814,6 +816,7 @@ def student_dashboard(request): # lint-amnesty, pylint: disable=too-many-statem # TODO START: clean up as part of REVEM-199 (START) 'course_info': get_dashboard_course_info(user, course_enrollments), # TODO START: clean up as part of REVEM-199 (END) + 'tos_modal':tos_modal } # Include enterprise learner portal metadata and messaging diff --git a/lms/djangoapps/static_template_view/views.py b/lms/djangoapps/static_template_view/views.py index f63cb5817108..9bf99affa86d 100644 --- a/lms/djangoapps/static_template_view/views.py +++ b/lms/djangoapps/static_template_view/views.py @@ -7,6 +7,7 @@ import mimetypes +import html from django.conf import settings from django.http import Http404, HttpResponse, HttpResponseNotFound, HttpResponseServerError @@ -22,7 +23,7 @@ from common.djangoapps.util.cache import cache_if_anonymous from common.djangoapps.util.views import fix_crum_request from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers - +from openedx.features.termsofservice import views as tos_views valid_templates = [] if settings.STATIC_GRAB: @@ -59,6 +60,10 @@ def render(request, template): # This is necessary for the dialog presented with the TOS in /register if template == 'honor.html': context['allow_iframing'] = True + + latest_tos_html = tos_views.latest_terms_of_service() + context['tos_html'] = latest_tos_html + # Format Examples: static_template_about_header configuration_base = 'static_template_' + template.replace('.html', '').replace('-', '_') page_header = configuration_helpers.get_value(configuration_base + '_header') diff --git a/lms/static/sass/_build-lms-v1.scss b/lms/static/sass/_build-lms-v1.scss index 1171e3d14cdf..41733bc1d8b8 100644 --- a/lms/static/sass/_build-lms-v1.scss +++ b/lms/static/sass/_build-lms-v1.scss @@ -73,6 +73,7 @@ @import 'features/_unsupported-browser-alert'; @import 'features/content-type-gating'; @import 'features/course-duration-limits'; +@import 'features/termsofservice'; // search @import 'search/search'; diff --git a/lms/static/sass/features/_termsofservice.scss b/lms/static/sass/features/_termsofservice.scss new file mode 100644 index 000000000000..477150a8a7fe --- /dev/null +++ b/lms/static/sass/features/_termsofservice.scss @@ -0,0 +1,105 @@ +.modal-tos { + position: fixed; + top: 0; + left: 0; + width:100%; + height: 100%; + background: rgba(0, 0, 0, 0.6); + display: inline-block; + } + +.modal-main-tos { + position:fixed; + background: white; + width: 80%; + height: 90%; + top:50%; + left:50%; + transform: translate(-50%,-50%); + border-radius: 10px; + } + +.scrollable_tos_style { + height: 58vh; + width: auto; + overflow: scroll; + padding: 6px; + } + +.display-block { + display: block; + } + +.display-none { + display: none; + } + + .tos-part { + margin-top: 15px !important; +} + +.tos-section { + list-style: none !important; + padding: 0px !important; +} + +.tos-section-item { + display:block !important; + margin-bottom: 15px !important; +} + +.tos-section-item > span { + display:block !important; + font-weight: bold !important; + // position: absolute !important; +} + +.tos-section-item > p { + margin-top: -18px !important; + margin-left: 40px !important; + display:block !important; + // display: inline-block !important; + // position: relative !important; + /*top: -21px !important;*/ +} + +.tos-section-alpha { + list-style: none !important; + counter-reset: tos-section-alpha-counter; +} + +.tos-section-alpha > li { + margin: 10px 0; +} + +.tos-section-alpha > li::after { + min-width: 30px !important; +} + +.tos-section-alpha > li::before { + content: '(' counter(tos-section-alpha-counter, lower-alpha) ')'; + counter-increment: tos-section-alpha-counter; + // display:inline-block !important; + display: table-cell; + text-align: right; + + + // position: absolute !important; + /*min-width: 20px !important;*/ +} + +.tos-section-alpha > li p { + margin-left: 40px !important; + // display: inline-block !important; + margin-top: -18px !important; + + // position: relative !important; + /*top: -21px !important;*/ +} + + + + + + + diff --git a/lms/templates/dashboard.html b/lms/templates/dashboard.html index a09845b0163a..d9fbac9ca3c5 100644 --- a/lms/templates/dashboard.html +++ b/lms/templates/dashboard.html @@ -400,3 +400,10 @@

<%include file="dashboard/_dashboard_entitlement_unenrollment_modal.html"/> + +<%include file="dashboard/_dashboard_entitlement_unenrollment_modal.html"/> + +<%include file='termsofservice/modal_termsofservice.html' /> + + + diff --git a/lms/templates/static_templates/tos.html b/lms/templates/static_templates/tos.html index 107b03594354..630ee65fc089 100644 --- a/lms/templates/static_templates/tos.html +++ b/lms/templates/static_templates/tos.html @@ -1,9 +1,121 @@ <%page expression_filter="h"/> -<%! from django.utils.translation import ugettext as _ %> +<%! +from django.utils.translation import ugettext as _ +from openedx.core.djangolib.markup import HTML, Text +%> + <%inherit file="../main.html" /> <%block name="pagetitle">${_("Terms of Service")} + + +
+
+
+
+ +
+
+
+ +
+
+
+

+ ${_("Terms and Conditions")} +

+ +
+ ${tos_html} +
+ +
+
+
+
+

+ By clicking "Agree" I am representing that I have read the + above Terms, and expressly acknowledge and agree to the + above Terms. +

+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+ + - - - \ No newline at end of file diff --git a/openedx/features/termsofservice/api/v1/views.py b/openedx/features/termsofservice/api/v1/views.py index db4eb4cc4a95..d5a8bb325b57 100644 --- a/openedx/features/termsofservice/api/v1/views.py +++ b/openedx/features/termsofservice/api/v1/views.py @@ -1,10 +1,10 @@ # lint-amnesty, pylint: disable=missing-module-docstring -import json from django.views.decorators.csrf import ensure_csrf_cookie -from django.http import HttpResponseRedirect, JsonResponse +from django.http import JsonResponse from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers from django.conf import settings +from django.shortcuts import redirect from openedx.features.termsofservice.models import TermsOfServiceSites, TermsOfService from openedx.features.termsofservice.models import TermsOfServiceAcknowledgement, TermsOfServiceAllSites @@ -16,64 +16,56 @@ def terms_of_service_api(request): # lint-amnesty, pylint: disable=missing-func latest_tos_html = '' cur_site_name = configuration_helpers.get_value("SITE_NAME", settings.SITE_NAME) - cur_site_id = Site.objects.get(domain = cur_site_name) + cur_site_id = Site.objects.get(domain=cur_site_name) if request.method == 'GET': # Return Terms of Service as JSON try: latest_tos_html = '' has_user_agreed_to_latest_tos = False - # Get the curf_id associated with the Site + if settings.FEATURES.get('ENABLE_TERMSOFSERVICE_PER_SUBSITE'): - cur_site_curf_id = TermsOfServiceSites.objects.get(site_id = cur_site_id.id).curf_id + # Get the curf_id associated with the Site + cur_site_curf_id = TermsOfServiceSites.objects.get(site_id=cur_site_id.id).curf_id + cur_user_tos_ack = TermsOfServiceAcknowledgement.objects.filter( + user_id=request.user.id, curf_id=cur_site_curf_id).first() + + # Check if the user agreed curf id matches the latest curf id + + if cur_user_tos_ack is not None and cur_site_curf_id == cur_user_tos_ack.curf_id: + has_user_agreed_to_latest_tos = True + else: + latest_tos_html = TermsOfService.objects.get(curf_id=cur_site_curf_id).terms_of_service_text + else: default_tos = TermsOfServiceAllSites.objects.all().first() + #if there is no default TOS assigned, return a JSON response with an error if default_tos is None: result = { "tos_html": latest_tos_html, "tos_exists_for_site": False, "has_user_agreed_to_latest_tos": has_user_agreed_to_latest_tos, - "error": f"Need to setup a Terms of Service Acknowledgment for {cur_site_name}" + "Error": f"Need to setup a Terms of Service Acknowledgment for {cur_site_name}" } return JsonResponse(result) - else: - cur_site_curf_id = default_tos.curf_id - - current_valid_TOS = TermsOfService.objects.get(curf_id=cur_site_curf_id) - cur_user_tos_ack = None - cur_user_curf_id = None - try: - cur_user_tos_ack = TermsOfServiceAcknowledgement.objects.get( - user_id=request.user.id, curf_id=cur_site_curf_id) - if cur_user_tos_ack is not None: - cur_user_curf_id = cur_user_tos_ack.curf_id - except TermsOfServiceAcknowledgement.DoesNotExist: - cur_user_curf_id = None + cur_site_curf_id = default_tos.curf_id + cur_user_tos_ack = TermsOfServiceAcknowledgement.objects.filter( + user_id=request.user.id, curf_id=cur_site_curf_id).first() - if not settings.FEATURES.get('ENABLE_TERMSOFSERVICE_PER_SUBSITE'): - # Check if the user agreed curf id matches the latest curf id - has_user_agreed_to_latest_tos = cur_site_curf_id == cur_user_curf_id - else: - # check if user's agreed curf id is present in the TOS Site table + # Check if user's agreed curf id is present in the TOS Site table # if the object is present, that implies that the user has agreed to the latest TOS + if cur_user_tos_ack is not None and cur_site_curf_id == cur_user_tos_ack.curf_id: + has_user_agreed_to_latest_tos = True + else: + latest_tos_html = TermsOfService.objects.get(curf_id=cur_site_curf_id).terms_of_service_text - try: - TermsOfServiceSites_curf_id = TermsOfServiceSites.objects.get(curf_id=cur_user_curf_id) - if TermsOfServiceSites_curf_id is not None: - has_user_agreed_to_latest_tos = True - except TermsOfServiceSites.DoesNotExist: - has_user_agreed_to_latest_tos = False - - if not has_user_agreed_to_latest_tos: - # get the html text using the curf id - latest_tos_html = current_valid_TOS.terms_of_service_text except TermsOfServiceAcknowledgement.DoesNotExist: latest_tos_html = '' result = { - "tos_exists_for_site": True if latest_tos_html else False, + "tos_exists_for_site": bool(latest_tos_html), "tos_html": latest_tos_html, "has_user_agreed_to_latest_tos": has_user_agreed_to_latest_tos } @@ -81,9 +73,9 @@ def terms_of_service_api(request): # lint-amnesty, pylint: disable=missing-func return JsonResponse(result) if request.method == 'POST': - + if settings.FEATURES.get('ENABLE_TERMSOFSERVICE_PER_SUBSITE'): - current_valid_curf_id = TermsOfServiceSites.objects.get(site_id = cur_site_id.id).curf_id + current_valid_curf_id = TermsOfServiceSites.objects.get(site_id=cur_site_id.id).curf_id else: current_valid_curf_id = TermsOfServiceAllSites.objects.all().first().curf_id @@ -94,4 +86,4 @@ def terms_of_service_api(request): # lint-amnesty, pylint: disable=missing-func user_TOS_ack = TermsOfServiceAcknowledgement(user_id=request.user.id, curf_id=current_valid_curf_id) user_TOS_ack.save() - return HttpResponseRedirect(request.path_info) + return redirect(request.path_info) diff --git a/openedx/features/termsofservice/migrations/0002_initial_data.py b/openedx/features/termsofservice/migrations/0002_initial_data.py new file mode 100644 index 000000000000..8650563f955e --- /dev/null +++ b/openedx/features/termsofservice/migrations/0002_initial_data.py @@ -0,0 +1,37 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('termsofservice', '0001_initial'), + ] + + def forwards(apps, schema_editor): + + TermsOfService = apps.get_model('termsofservice', 'TermsOfService') + # create a sample terms of service object + tos = TermsOfService(date_modified='2023-01-01 00:00:00', terms_of_service_text='

Sample terms of service

', curf_id='test-2023-01') + tos.save() + + TermsOfServiceAllSites = apps.get_model('termsofservice', 'TermsOfServiceAllSites') + # get the previously created terms of service object + tos = apps.get_model('termsofservice', 'TermsOfService').objects.get(curf_id='test-2023-01') + # create a sample terms of service all sites object + tos_all_sites = TermsOfServiceAllSites(curf=tos) + tos_all_sites.save() + + def backwards(apps, schema_editor): + TermsOfService = apps.get_model('termsofservice', 'TermsOfService') + TermsOfServiceAllSites = apps.get_model('termsofservice', 'TermsOfServiceAllSites') + try: + tos = TermsOfService.objects.get(curf_id='test-2023-01') + tos_all_sites = TermsOfServiceAllSites.objects.get(curf=tos) + tos_all_sites.delete() + tos.delete() + except TermsOfService.DoesNotExist as error: + print(f"Error: {error}") + + operations = [ + migrations.RunPython(forwards, backwards), + ] \ No newline at end of file diff --git a/openedx/features/termsofservice/static/termsofservice/jsx/Modal.jsx b/openedx/features/termsofservice/static/termsofservice/jsx/Modal.jsx index 5a581ac366e6..075e09ec8ead 100644 --- a/openedx/features/termsofservice/static/termsofservice/jsx/Modal.jsx +++ b/openedx/features/termsofservice/static/termsofservice/jsx/Modal.jsx @@ -71,6 +71,7 @@ class ModalView extends Component { } } + fetch('/termsofservice/v1/current_tos/', { method: 'POST', mode: 'same-origin', diff --git a/openedx/features/termsofservice/tests/test_termsofservice.py b/openedx/features/termsofservice/tests/test_termsofservice.py index c93b88d93315..19d064f9ed1a 100644 --- a/openedx/features/termsofservice/tests/test_termsofservice.py +++ b/openedx/features/termsofservice/tests/test_termsofservice.py @@ -35,10 +35,7 @@ def setUpTestData(cls): curf_id="cf_id1" ) # Fill in sample data for Site - site1 = Site.objects.create( - domain="example-tos.com", - name="Example TOS Site" - ) + site1 = Site.objects.create(domain='edx.org', name='edx.org') # Fill in sample data for TermsOfServiceSites toss1 = TermsOfServiceSites.objects.create( diff --git a/openedx/features/termsofservice/views.py b/openedx/features/termsofservice/views.py index f59858ebdead..b945a192ab49 100644 --- a/openedx/features/termsofservice/views.py +++ b/openedx/features/termsofservice/views.py @@ -22,8 +22,8 @@ def latest_terms_of_service(): # lint-amnesty, pylint: disable=missing-function tos_html = TermsOfService.objects.get(curf_id=cur_site_curf_id).terms_of_service_text except AttributeError as error: msg = ( - f"Need to setup a Terms of Service Acknowledgment for {site_name}" - ) + f"Need to setup a Terms of Service Acknowledgment for {site_name}" + ) tos_html = msg logger.info(f"{msg}\n{str(error)}") From 77cb9241cf522176aee295a1e4fd02c14aa2c3e6 Mon Sep 17 00:00:00 2001 From: Divyanshu Date: Thu, 16 Feb 2023 11:09:57 -0500 Subject: [PATCH 07/15] feat: Code Review Changes In regards to CSRF Token --- openedx/features/termsofservice/api/v1/views.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openedx/features/termsofservice/api/v1/views.py b/openedx/features/termsofservice/api/v1/views.py index d5a8bb325b57..9d4e9eebd4bc 100644 --- a/openedx/features/termsofservice/api/v1/views.py +++ b/openedx/features/termsofservice/api/v1/views.py @@ -73,6 +73,11 @@ def terms_of_service_api(request): # lint-amnesty, pylint: disable=missing-func return JsonResponse(result) if request.method == 'POST': + + if settings.FEATURES.get('ENABLE_TERMSOFSERVICE_PER_SUBSITE'): + current_valid_curf_id = TermsOfServiceSites.objects.get(site_id = cur_site_id.id).curf_id + else: + current_valid_curf_id = TermsOfServiceAllSites.objects.all().first().curf_id if settings.FEATURES.get('ENABLE_TERMSOFSERVICE_PER_SUBSITE'): current_valid_curf_id = TermsOfServiceSites.objects.get(site_id=cur_site_id.id).curf_id From cb9c37d18a4dec81ec3ebff1848fa193d30f0702 Mon Sep 17 00:00:00 2001 From: Divyanshu Date: Thu, 16 Feb 2023 11:37:53 -0500 Subject: [PATCH 08/15] feat: Code Review changes To improve error handling --- openedx/features/termsofservice/views.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openedx/features/termsofservice/views.py b/openedx/features/termsofservice/views.py index b945a192ab49..58bf686405ff 100644 --- a/openedx/features/termsofservice/views.py +++ b/openedx/features/termsofservice/views.py @@ -18,6 +18,9 @@ def latest_terms_of_service(): # lint-amnesty, pylint: disable=missing-function else: try: cur_site_curf_id = TermsOfServiceAllSites.objects.all().first().curf_id + except TermsOfServiceAllSites.DoesNotExist: + tos_html = "Need to setup a Terms of Service Acknowledgment for " + site_name + logger.info('Need to setup a Terms of Service Acknowledgment for {site_name}') tos_html = TermsOfService.objects.get(curf_id=cur_site_curf_id).terms_of_service_text except AttributeError as error: From 0c7a93833af5beffb833301664a226d89b602b44 Mon Sep 17 00:00:00 2001 From: Zachary Trabookis Date: Thu, 16 Feb 2023 13:06:32 -0500 Subject: [PATCH 09/15] feat(terms-of-service): Added `tos_exists_for_site` to TOS api Added `tos_exists_for_site` to API to not show the Modal when TOS All Sites is empty --- openedx/features/termsofservice/views.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/openedx/features/termsofservice/views.py b/openedx/features/termsofservice/views.py index 58bf686405ff..b945a192ab49 100644 --- a/openedx/features/termsofservice/views.py +++ b/openedx/features/termsofservice/views.py @@ -18,9 +18,6 @@ def latest_terms_of_service(): # lint-amnesty, pylint: disable=missing-function else: try: cur_site_curf_id = TermsOfServiceAllSites.objects.all().first().curf_id - except TermsOfServiceAllSites.DoesNotExist: - tos_html = "Need to setup a Terms of Service Acknowledgment for " + site_name - logger.info('Need to setup a Terms of Service Acknowledgment for {site_name}') tos_html = TermsOfService.objects.get(curf_id=cur_site_curf_id).terms_of_service_text except AttributeError as error: From 6889dba6801eb61355450404dc9207c4405ca011 Mon Sep 17 00:00:00 2001 From: Divyanshu Date: Wed, 22 Feb 2023 10:31:01 -0500 Subject: [PATCH 10/15] feat: Code Review changes for Terms of Service Added Django migration to add initial data, Fixed CSS issues with TOS, Fixed double redirect after user agrees to TOS, and refactored some code for readability --- openedx/features/termsofservice/api/v1/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openedx/features/termsofservice/api/v1/views.py b/openedx/features/termsofservice/api/v1/views.py index 9d4e9eebd4bc..67db0807350f 100644 --- a/openedx/features/termsofservice/api/v1/views.py +++ b/openedx/features/termsofservice/api/v1/views.py @@ -73,9 +73,9 @@ def terms_of_service_api(request): # lint-amnesty, pylint: disable=missing-func return JsonResponse(result) if request.method == 'POST': - + if settings.FEATURES.get('ENABLE_TERMSOFSERVICE_PER_SUBSITE'): - current_valid_curf_id = TermsOfServiceSites.objects.get(site_id = cur_site_id.id).curf_id + current_valid_curf_id = TermsOfServiceSites.objects.get(site_id=cur_site_id.id).curf_id else: current_valid_curf_id = TermsOfServiceAllSites.objects.all().first().curf_id From 039ab62649b81590cbc557735cd1fdde4a291b86 Mon Sep 17 00:00:00 2001 From: Zachary Trabookis Date: Wed, 22 Feb 2023 17:36:30 -0500 Subject: [PATCH 11/15] feat: Allow `TermsOfServiceAllSites` to edit existing records. --- openedx/features/termsofservice/admin.py | 5 ++++- openedx/features/termsofservice/api/v1/views.py | 4 ++-- .../termsofservice/migrations/0001_initial.py | 17 +++++++++-------- openedx/features/termsofservice/models.py | 6 +++++- 4 files changed, 20 insertions(+), 12 deletions(-) diff --git a/openedx/features/termsofservice/admin.py b/openedx/features/termsofservice/admin.py index 722b3f655f8c..29a2c0514bfb 100644 --- a/openedx/features/termsofservice/admin.py +++ b/openedx/features/termsofservice/admin.py @@ -16,8 +16,11 @@ class TermsOfServiceAcknowledgementAdmin(admin.ModelAdmin): class TermsOfServiceSitesAdmin(admin.ModelAdmin): list_display = ('site', 'curf') +class TermsOfServiceAllSitesAdmin(admin.ModelAdmin): + list_display = ('curf',) + admin.site.register(TermsOfService, TermsOfServiceAdmin) admin.site.register(TermsOfServiceAcknowledgement, TermsOfServiceAcknowledgementAdmin) admin.site.register(TermsOfServiceSites, TermsOfServiceSitesAdmin) -admin.site.register(TermsOfServiceAllSites) +admin.site.register(TermsOfServiceAllSites, TermsOfServiceAllSitesAdmin) diff --git a/openedx/features/termsofservice/api/v1/views.py b/openedx/features/termsofservice/api/v1/views.py index 67db0807350f..02fc91680586 100644 --- a/openedx/features/termsofservice/api/v1/views.py +++ b/openedx/features/termsofservice/api/v1/views.py @@ -1,7 +1,7 @@ # lint-amnesty, pylint: disable=missing-module-docstring from django.views.decorators.csrf import ensure_csrf_cookie -from django.http import JsonResponse +from django.http import HttpResponse, JsonResponse from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers from django.conf import settings from django.shortcuts import redirect @@ -91,4 +91,4 @@ def terms_of_service_api(request): # lint-amnesty, pylint: disable=missing-func user_TOS_ack = TermsOfServiceAcknowledgement(user_id=request.user.id, curf_id=current_valid_curf_id) user_TOS_ack.save() - return redirect(request.path_info) + return HttpResponse("Successfully Posted Terms of Service Update", status=200) diff --git a/openedx/features/termsofservice/migrations/0001_initial.py b/openedx/features/termsofservice/migrations/0001_initial.py index b5576fc3801d..b42d50f57393 100644 --- a/openedx/features/termsofservice/migrations/0001_initial.py +++ b/openedx/features/termsofservice/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 3.2.13 on 2023-01-10 17:52 +# Generated by Django 3.2.13 on 2023-02-22 22:05 from django.conf import settings from django.db import migrations, models @@ -25,22 +25,23 @@ class Migration(migrations.Migration): ], ), migrations.CreateModel( - name='TermsOfServiceAllSites', + name='TermsOfServiceSites', fields=[ - ('curf', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to='termsofservice.termsofservice', to_field='curf_id')), + ('site', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to='sites.site')), + ('curf', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='termsofservice.termsofservice', to_field='curf_id')), ], options={ - 'verbose_name': 'TermsOfServiceAllSite', + 'verbose_name': 'TOS Site', }, ), migrations.CreateModel( - name='TermsOfServiceSites', + name='TermsOfServiceAllSites', fields=[ - ('site', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to='sites.site')), - ('curf', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='termsofservice.termsofservice', to_field='curf_id')), + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('curf', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='termsofservice.termsofservice', to_field='curf_id')), ], options={ - 'verbose_name': 'TOS Site', + 'verbose_name': 'TermsOfServiceAllSite', }, ), migrations.CreateModel( diff --git a/openedx/features/termsofservice/models.py b/openedx/features/termsofservice/models.py index 826dd01a50aa..bdead5701aed 100644 --- a/openedx/features/termsofservice/models.py +++ b/openedx/features/termsofservice/models.py @@ -51,4 +51,8 @@ class TermsOfServiceAllSites(models.Model): class Meta: app_label = 'termsofservice' verbose_name = 'TermsOfServiceAllSite' - curf = models.ForeignKey(TermsOfService, primary_key=True, to_field="curf_id", on_delete=models.CASCADE) + + # id = models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True) + curf = models.OneToOneField(TermsOfService, to_field="curf_id", on_delete=models.CASCADE) + + # models.ForeignKey(TermsOfService, primary_key=True, to_field="curf_id", on_delete=models.CASCADE) From 107093b9a24ee67dc59db93cdf7788e28da0611c Mon Sep 17 00:00:00 2001 From: Zachary Trabookis Date: Wed, 22 Feb 2023 23:54:23 -0500 Subject: [PATCH 12/15] feat: Make sure that `TermsOfService` modal form doesn't refresh the page. --- lms/static/sass/features/_termsofservice.scss | 9 +++++++-- lms/templates/static_templates/tos.html | 1 + openedx/features/termsofservice/admin.py | 1 + openedx/features/termsofservice/models.py | 6 +++--- .../termsofservice/static/termsofservice/jsx/Modal.jsx | 9 ++++++++- 5 files changed, 20 insertions(+), 6 deletions(-) diff --git a/lms/static/sass/features/_termsofservice.scss b/lms/static/sass/features/_termsofservice.scss index 4302c7df0a4b..3cb46c499294 100644 --- a/lms/static/sass/features/_termsofservice.scss +++ b/lms/static/sass/features/_termsofservice.scss @@ -12,17 +12,22 @@ position:fixed; background: white; width: 80%; - height: 90%; + height: auto; top:50%; left:50%; transform: translate(-50%,-50%); border-radius: 10px; + + form { + margin-bottom: 20px; } +} .scrollable_tos_style { height: 58vh; width: auto; - overflow: scroll; + overflow-x: hidden; + overflow-y: auto; padding: 6px; } diff --git a/lms/templates/static_templates/tos.html b/lms/templates/static_templates/tos.html index 552d6641ad32..cd5918db5d72 100644 --- a/lms/templates/static_templates/tos.html +++ b/lms/templates/static_templates/tos.html @@ -72,6 +72,7 @@
+

Terms of Service

${tos_html}
diff --git a/openedx/features/termsofservice/admin.py b/openedx/features/termsofservice/admin.py index 29a2c0514bfb..ce48ce45e9cb 100644 --- a/openedx/features/termsofservice/admin.py +++ b/openedx/features/termsofservice/admin.py @@ -16,6 +16,7 @@ class TermsOfServiceAcknowledgementAdmin(admin.ModelAdmin): class TermsOfServiceSitesAdmin(admin.ModelAdmin): list_display = ('site', 'curf') + class TermsOfServiceAllSitesAdmin(admin.ModelAdmin): list_display = ('curf',) diff --git a/openedx/features/termsofservice/models.py b/openedx/features/termsofservice/models.py index bdead5701aed..3cf7044ae5e3 100644 --- a/openedx/features/termsofservice/models.py +++ b/openedx/features/termsofservice/models.py @@ -48,11 +48,11 @@ class Meta: # TermsOfServiceAllSites - holds the default site - This model holds only one default object class TermsOfServiceAllSites(models.Model): + """ + Model to assign all sites (platform) to a Terms of Service (Linked curf_id) + """ class Meta: app_label = 'termsofservice' verbose_name = 'TermsOfServiceAllSite' - # id = models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True) curf = models.OneToOneField(TermsOfService, to_field="curf_id", on_delete=models.CASCADE) - - # models.ForeignKey(TermsOfService, primary_key=True, to_field="curf_id", on_delete=models.CASCADE) diff --git a/openedx/features/termsofservice/static/termsofservice/jsx/Modal.jsx b/openedx/features/termsofservice/static/termsofservice/jsx/Modal.jsx index 075e09ec8ead..9213b4cd24bf 100644 --- a/openedx/features/termsofservice/static/termsofservice/jsx/Modal.jsx +++ b/openedx/features/termsofservice/static/termsofservice/jsx/Modal.jsx @@ -36,6 +36,13 @@ class ModalView extends Component { return !this.state.tos_isChecked; } + // Prevent the default form from submitting and refreshing the page that it's included on. + // (e.g. The dashboard page won't refresh after the learner submits the form) + // https://stackoverflow.com/questions/28479239/setting-onsubmit-in-react-js + submitTOS (e) { + e.preventDefault(); + } + retrievePage() { $.get('/termsofservice/v1/current_tos/') .then(data => { @@ -105,7 +112,7 @@ class ModalView extends Component {
-
+