diff --git a/lms/djangoapps/course_home_api/admin.py b/lms/djangoapps/course_home_api/admin.py new file mode 100644 index 0000000000..6023b1444c --- /dev/null +++ b/lms/djangoapps/course_home_api/admin.py @@ -0,0 +1,41 @@ +""" +Django Admin for DisableProgressPageStackedConfig. +""" + +from django.contrib import admin +from django.utils.translation import ugettext_lazy as _ + +from openedx.core.djangoapps.config_model_utils.admin import StackedConfigModelAdmin + +from .models import DisableProgressPageStackedConfig + + +class DisableProgressPageStackedConfigAdmin(StackedConfigModelAdmin): + """ + Stacked Config Model Admin for disable the progress page + """ + fieldsets = ( + ('Context', { + 'fields': DisableProgressPageStackedConfig.KEY_FIELDS, + 'description': _( + 'These define the context to disable the frontend-app-learning progress page.' + 'If no values are set, then the configuration applies globally. ' + 'If a single value is set, then the configuration applies to all courses ' + 'within that context. At most one value can be set at a time.
' + 'If multiple contexts apply to a course (for example, if configuration ' + 'is specified for the course specifically, and for the org that the course ' + 'is in, then the more specific context overrides the more general context.' + ), + }), + ('Configuration', { + 'fields': ('disabled',), + 'description': _( + 'If any of these values is left empty or "Unknown", then their value ' + 'at runtime will be retrieved from the next most specific context that applies. ' + 'For example, if "Disabled" is left as "Unknown" in the course context, then that ' + 'course will be Disabled only if the org that it is in is Disabled.' + ), + }) + ) + +admin.site.register(DisableProgressPageStackedConfig, DisableProgressPageStackedConfigAdmin) diff --git a/lms/djangoapps/course_home_api/apps.py b/lms/djangoapps/course_home_api/apps.py new file mode 100644 index 0000000000..a3486432ca --- /dev/null +++ b/lms/djangoapps/course_home_api/apps.py @@ -0,0 +1,9 @@ +""" +Course home api application configuration +""" + +from django.apps import AppConfig + + +class CourseHomeApiConfig(AppConfig): + name = 'lms.djangoapps.course_home_api' diff --git a/lms/djangoapps/course_home_api/migrations/0001_initial.py b/lms/djangoapps/course_home_api/migrations/0001_initial.py new file mode 100644 index 0000000000..d7068e546a --- /dev/null +++ b/lms/djangoapps/course_home_api/migrations/0001_initial.py @@ -0,0 +1,45 @@ +# Generated by Django 2.2.20 on 2021-05-26 17:41 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import openedx.core.djangoapps.config_model_utils.models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('sites', '0002_alter_domain_unique'), + ('course_overviews', '0024_overview_adds_has_highlights'), + ] + + operations = [ + migrations.CreateModel( + name='DisableProgressPageStackedConfig', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('change_date', models.DateTimeField(auto_now_add=True, verbose_name='Change date')), + ('enabled', models.NullBooleanField(default=None, verbose_name='Enabled')), + ('org', models.CharField(blank=True, db_index=True, help_text='Configure values for all course runs associated with this Organization. This is the organization string (i.e. edX, MITx).', max_length=255, null=True)), + ('org_course', models.CharField(blank=True, db_index=True, help_text="Configure values for all course runs associated with this course. This is should be formatted as 'org+course' (i.e. MITx+6.002x, HarvardX+CS50).", max_length=255, null=True, validators=[openedx.core.djangoapps.config_model_utils.models.validate_course_in_org], verbose_name='Course in Org')), + ('disabled', models.NullBooleanField(default=None, verbose_name='Disabled')), + ('changed_by', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL, verbose_name='Changed by')), + ('course', models.ForeignKey(blank=True, help_text='Configure values for this course run. This should be formatted as the CourseKey (i.e. course-v1://MITx+6.002x+2019_Q1)', null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='course_overviews.CourseOverview', verbose_name='Course Run')), + ('site', models.ForeignKey(blank=True, help_text='Configure values for all course runs associated with this site.', null=True, on_delete=django.db.models.deletion.CASCADE, to='sites.Site')), + ], + options={ + 'abstract': False, + }, + ), + migrations.AddIndex( + model_name='disableprogresspagestackedconfig', + index=models.Index(fields=['site', 'org', 'course'], name='course_home_site_id_6988e4_idx'), + ), + migrations.AddIndex( + model_name='disableprogresspagestackedconfig', + index=models.Index(fields=['site', 'org', 'org_course', 'course'], name='course_home_site_id_23dec6_idx'), + ), + ] diff --git a/lms/djangoapps/course_home_api/migrations/__init__.py b/lms/djangoapps/course_home_api/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lms/djangoapps/course_home_api/models.py b/lms/djangoapps/course_home_api/models.py new file mode 100644 index 0000000000..7a5926aa38 --- /dev/null +++ b/lms/djangoapps/course_home_api/models.py @@ -0,0 +1,25 @@ +""" +Course home api models file +""" + +from django.db import models +from django.utils.translation import ugettext_lazy as _ + +from openedx.core.djangoapps.config_model_utils.models import StackedConfigurationModel + + +class DisableProgressPageStackedConfig(StackedConfigurationModel): + """ + Stacked Config Model for disabling the frontend-app-learning progress page + """ + + STACKABLE_FIELDS = ('disabled',) + # Since this config disables the progress page, + # it seemed it would be clearer to use a disabled flag instead of an enabled flag. + # The enabled field still exists but is not used or shown in the admin. + disabled = models.NullBooleanField(default=None, verbose_name=_("Disabled")) + + def __str__(self): + return "DisableProgressPageStackedConfig(disabled={!r})".format( + self.disabled + ) diff --git a/lms/djangoapps/course_home_api/progress/v1/tests/test_views.py b/lms/djangoapps/course_home_api/progress/v1/tests/test_views.py index 26d64ec081..9852f8bf9b 100644 --- a/lms/djangoapps/course_home_api/progress/v1/tests/test_views.py +++ b/lms/djangoapps/course_home_api/progress/v1/tests/test_views.py @@ -16,9 +16,11 @@ from common.djangoapps.course_modes.models import CourseMode from common.djangoapps.student.models import CourseEnrollment from common.djangoapps.student.tests.factories import UserFactory from lms.djangoapps.course_home_api.tests.utils import BaseCourseHomeTests +from lms.djangoapps.course_home_api.models import DisableProgressPageStackedConfig from lms.djangoapps.course_home_api.toggles import COURSE_HOME_MICROFRONTEND, COURSE_HOME_MICROFRONTEND_PROGRESS_TAB from lms.djangoapps.experiments.testutils import override_experiment_waffle_flag from lms.djangoapps.verify_student.models import ManualVerification +from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from openedx.core.djangoapps.course_date_signals.utils import MIN_DURATION from openedx.core.djangoapps.user_api.preferences.api import set_user_preference from openedx.features.course_duration_limits.models import CourseDurationLimitConfig @@ -155,3 +157,17 @@ class ProgressTabTestViews(BaseCourseHomeTests): assert response.data['verified_mode'] == {'access_expiration_date': (enrollment.created + MIN_DURATION), 'currency': 'USD', 'currency_symbol': '$', 'price': 149, 'sku': 'ABCD1234', 'upgrade_url': '/dashboard'} + + @override_experiment_waffle_flag(COURSE_HOME_MICROFRONTEND, active=True) + @override_waffle_flag(COURSE_HOME_MICROFRONTEND_PROGRESS_TAB, active=True) + def test_page_respects_stacked_config(self): + CourseEnrollment.enroll(self.user, self.course.id) + course_overview = CourseOverview.get_from_id(self.course.id) + + response = self.client.get(self.url) + assert response.status_code == 200 + + DisableProgressPageStackedConfig.objects.create(disabled=True, course=course_overview) + + response = self.client.get(self.url) + assert response.status_code == 404 diff --git a/lms/djangoapps/course_home_api/toggles.py b/lms/djangoapps/course_home_api/toggles.py index dcf4e79d2a..2522e3a10d 100644 --- a/lms/djangoapps/course_home_api/toggles.py +++ b/lms/djangoapps/course_home_api/toggles.py @@ -51,7 +51,10 @@ def course_home_mfe_outline_tab_is_active(course_key): def course_home_mfe_progress_tab_is_active(course_key): + # Avoiding a circular dependency + from .models import DisableProgressPageStackedConfig return ( course_home_mfe_is_active(course_key) and - COURSE_HOME_MICROFRONTEND_PROGRESS_TAB.is_enabled(course_key) + COURSE_HOME_MICROFRONTEND_PROGRESS_TAB.is_enabled(course_key) and + not DisableProgressPageStackedConfig.current(course_key=course_key).disabled ) diff --git a/lms/envs/common.py b/lms/envs/common.py index 610523ca49..88e5e25336 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -2866,6 +2866,9 @@ INSTALLED_APPS = [ 'lms.djangoapps.bulk_email', 'lms.djangoapps.branding', + # Course home api + 'lms.djangoapps.course_home_api', + # New (Blockstore-based) XBlock runtime 'openedx.core.djangoapps.xblock.apps.LmsXBlockAppConfig',