From abac77e10629f23d6f409e5a4348f14efee0604c Mon Sep 17 00:00:00 2001 From: ayesha waris <73840786+ayesha-waris@users.noreply.github.com> Date: Tue, 23 May 2023 15:08:06 +0500 Subject: [PATCH] feat: adds ability to disable posting in discussions indefinitely (#32171) * feat: adds ability to disable posting in discussions indefinitely * test: fixed ffailing test cases * test: added new model field in test cases * refactor: removes unnecessary migrations * refactor: removed previous migrations and adds new field in discussions api * refactor: added docstring and changed method name --------- Co-authored-by: ayesha waris <73840786+ayeshoali@users.noreply.github.com> --- lms/djangoapps/discussion/rest_api/api.py | 31 ++++++++++++++++++- .../discussion/rest_api/tests/test_api.py | 1 + .../discussion/rest_api/tests/test_views.py | 1 + .../migrations/0016_auto_20230518_0935.py | 23 ++++++++++++++ openedx/core/djangoapps/discussions/models.py | 17 ++++++++++ .../djangoapps/discussions/serializers.py | 1 + .../discussions/tests/test_models.py | 10 ++++++ 7 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 openedx/core/djangoapps/discussions/migrations/0016_auto_20230518_0935.py diff --git a/lms/djangoapps/discussion/rest_api/api.py b/lms/djangoapps/discussion/rest_api/api.py index 128d362658..7406676be7 100644 --- a/lms/djangoapps/discussion/rest_api/api.py +++ b/lms/djangoapps/discussion/rest_api/api.py @@ -40,7 +40,12 @@ from lms.djangoapps.courseware.exceptions import CourseAccessRedirect from lms.djangoapps.discussion.toggles import ENABLE_DISCUSSIONS_MFE, ENABLE_LEARNERS_TAB_IN_DISCUSSIONS_MFE from lms.djangoapps.discussion.toggles_utils import reported_content_email_notification_enabled from lms.djangoapps.discussion.views import is_privileged_user -from openedx.core.djangoapps.discussions.models import DiscussionsConfiguration, DiscussionTopicLink, Provider +from openedx.core.djangoapps.discussions.models import ( + DiscussionsConfiguration, + DiscussionTopicLink, + Provider, + PostingRestriction +) from openedx.core.djangoapps.discussions.utils import get_accessible_discussion_xblocks from openedx.core.djangoapps.django_comment_common import comment_client from openedx.core.djangoapps.django_comment_common.comment_client.comment import Comment @@ -319,14 +324,38 @@ def get_course(request, course_key): """ return dt.isoformat().replace('+00:00', 'Z') + def is_posting_allowed(posting_restrictions, blackout_schedules): + """ + Check if posting is allowed based on the given posting restrictions and blackout schedules. + + Args: + posting_restrictions (str): Values would be "disabled", "scheduled" or "enabled". + blackout_schedules (List[Dict[str, datetime]]): The list of blackout schedules + + Returns: + bool: True if posting is allowed, False otherwise. + """ + now = datetime.now(UTC) + if posting_restrictions == PostingRestriction.DISABLED: + return True + elif posting_restrictions == PostingRestriction.SCHEDULED: + return not any(schedule["start"] <= now <= schedule["end"] for schedule in blackout_schedules) + else: + return False + course = _get_course(course_key, request.user) user_roles = get_user_role_names(request.user, course_key) course_config = DiscussionsConfiguration.get(course_key) EDIT_REASON_CODES = getattr(settings, "DISCUSSION_MODERATION_EDIT_REASON_CODES", {}) CLOSE_REASON_CODES = getattr(settings, "DISCUSSION_MODERATION_CLOSE_REASON_CODES", {}) + is_posting_enabled = is_posting_allowed( + course_config.posting_restrictions, + course.get_discussion_blackout_datetimes() + ) return { "id": str(course_key), + "is_posting_enabled": is_posting_enabled, "blackouts": [ { "start": _format_datetime(blackout["start"]), diff --git a/lms/djangoapps/discussion/rest_api/tests/test_api.py b/lms/djangoapps/discussion/rest_api/tests/test_api.py index c8ab575df4..7aa702a2d2 100644 --- a/lms/djangoapps/discussion/rest_api/tests/test_api.py +++ b/lms/djangoapps/discussion/rest_api/tests/test_api.py @@ -190,6 +190,7 @@ class GetCourseTest(ForumsEnableMixin, UrlResetMixin, SharedModuleStoreTestCase) def test_basic(self): assert get_course(self.request, self.course.id) == { 'id': str(self.course.id), + 'is_posting_enabled': True, 'blackouts': [], 'thread_list_url': 'http://testserver/api/discussion/v1/threads/?course_id=course-v1%3Ax%2By%2Bz', 'following_thread_list_url': diff --git a/lms/djangoapps/discussion/rest_api/tests/test_views.py b/lms/djangoapps/discussion/rest_api/tests/test_views.py index 56e690761d..0e303cc240 100644 --- a/lms/djangoapps/discussion/rest_api/tests/test_views.py +++ b/lms/djangoapps/discussion/rest_api/tests/test_views.py @@ -517,6 +517,7 @@ class CourseViewTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase): 200, { "id": str(self.course.id), + "is_posting_enabled": True, "blackouts": [], "thread_list_url": "http://testserver/api/discussion/v1/threads/?course_id=course-v1%3Ax%2By%2Bz", "following_thread_list_url": ( diff --git a/openedx/core/djangoapps/discussions/migrations/0016_auto_20230518_0935.py b/openedx/core/djangoapps/discussions/migrations/0016_auto_20230518_0935.py new file mode 100644 index 0000000000..da9e791a88 --- /dev/null +++ b/openedx/core/djangoapps/discussions/migrations/0016_auto_20230518_0935.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.19 on 2023-05-18 09:35 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('discussions', '0015_discussiontopiclink_context'), + ] + + operations = [ + migrations.AddField( + model_name='discussionsconfiguration', + name='posting_restrictions', + field=models.CharField(choices=[('enabled', 'Enabled'), ('disabled', 'Disabled'), ('scheduled', 'Scheduled')], default='scheduled', help_text='The Posting availabilty in discussions whether it will be enabled, scheduled or indefinitely disabled.', max_length=15), + ), + migrations.AddField( + model_name='historicaldiscussionsconfiguration', + name='posting_restrictions', + field=models.CharField(choices=[('enabled', 'Enabled'), ('disabled', 'Disabled'), ('scheduled', 'Scheduled')], default='scheduled', help_text='The Posting availabilty in discussions whether it will be enabled, scheduled or indefinitely disabled.', max_length=15), + ), + ] diff --git a/openedx/core/djangoapps/discussions/models.py b/openedx/core/djangoapps/discussions/models.py index 319e92d476..c594ef52ae 100644 --- a/openedx/core/djangoapps/discussions/models.py +++ b/openedx/core/djangoapps/discussions/models.py @@ -46,6 +46,15 @@ class Provider: OPEN_EDX = 'openedx' +class PostingRestriction(models.TextChoices): + """ + Discussions Restrictions choices + """ + ENABLED = 'enabled' + DISABLED = 'disabled' + SCHEDULED = 'scheduled' + + DEFAULT_CONFIG_ENABLED = True @@ -413,6 +422,14 @@ class DiscussionsConfiguration(TimeStampedModel): default=True, help_text=_("If disabled, the discussions in the associated learning context/course will be disabled.") ) + posting_restrictions = models.CharField( + max_length=15, + default=PostingRestriction.SCHEDULED, + choices=PostingRestriction.choices, + help_text=_( + "The Posting availabilty in discussions whether it will be enabled, scheduled or indefinitely disabled." + ) + ) lti_configuration = models.ForeignKey( LtiConfiguration, on_delete=models.SET_NULL, diff --git a/openedx/core/djangoapps/discussions/serializers.py b/openedx/core/djangoapps/discussions/serializers.py index 5f0a35acdc..eff12d9202 100644 --- a/openedx/core/djangoapps/discussions/serializers.py +++ b/openedx/core/djangoapps/discussions/serializers.py @@ -176,6 +176,7 @@ class DiscussionsConfigurationSerializer(serializers.ModelSerializer): 'enable_in_context', 'enable_graded_units', 'unit_level_visibility', + 'posting_restrictions', ] fields = [ 'enabled', diff --git a/openedx/core/djangoapps/discussions/tests/test_models.py b/openedx/core/djangoapps/discussions/tests/test_models.py index fe85a19404..4078fe223d 100644 --- a/openedx/core/djangoapps/discussions/tests/test_models.py +++ b/openedx/core/djangoapps/discussions/tests/test_models.py @@ -16,6 +16,8 @@ from ..config.waffle import ENABLE_NEW_STRUCTURE_DISCUSSIONS from ..models import DEFAULT_CONFIG_ENABLED, Provider, get_default_provider_type from ..models import DiscussionsConfiguration from ..models import ProviderFilter +from ..models import PostingRestriction + SUPPORTED_PROVIDERS = [ 'legacy', @@ -141,6 +143,7 @@ class DiscussionsConfigurationModelTest(TestCase): self.configuration_with_defaults.save() self.configuration_with_values = DiscussionsConfiguration( context_key=self.course_key_with_values, + posting_restrictions=PostingRestriction.ENABLED, enabled=False, provider_type=Provider.LEGACY, plugin_configuration={ @@ -164,6 +167,7 @@ class DiscussionsConfigurationModelTest(TestCase): """ configuration = DiscussionsConfiguration.objects.get(context_key=self.course_key_with_defaults) assert configuration is not None + assert configuration.posting_restrictions == PostingRestriction.SCHEDULED assert configuration.enabled # by default assert configuration.lti_configuration is None assert len(configuration.plugin_configuration.keys()) == 0 @@ -175,6 +179,7 @@ class DiscussionsConfigurationModelTest(TestCase): """ configuration = DiscussionsConfiguration.objects.get(context_key=self.course_key_with_values) assert configuration is not None + assert configuration.posting_restrictions == PostingRestriction.ENABLED assert not configuration.enabled assert configuration.lti_configuration is None actual_url = configuration.plugin_configuration.get('url') @@ -187,6 +192,7 @@ class DiscussionsConfigurationModelTest(TestCase): Assert we can update an existing record """ configuration = DiscussionsConfiguration.objects.get(context_key=self.course_key_with_defaults) + configuration.posting_restrictions = PostingRestriction.SCHEDULED configuration.enabled = False configuration.plugin_configuration = { 'url': 'http://localhost', @@ -195,6 +201,7 @@ class DiscussionsConfigurationModelTest(TestCase): configuration.save() configuration = DiscussionsConfiguration.objects.get(context_key=self.course_key_with_defaults) assert configuration is not None + assert configuration.posting_restrictions == PostingRestriction.SCHEDULED assert not configuration.enabled assert configuration.lti_configuration is None assert configuration.plugin_configuration['url'] == 'http://localhost' @@ -233,6 +240,7 @@ class DiscussionsConfigurationModelTest(TestCase): with override_waffle_flag(ENABLE_NEW_STRUCTURE_DISCUSSIONS, active=new_structure_enabled): configuration = DiscussionsConfiguration.get(self.course_key_without_config) assert configuration is not None + assert configuration.posting_restrictions == PostingRestriction.SCHEDULED assert configuration.enabled == DEFAULT_CONFIG_ENABLED assert configuration.provider_type == default_provider_type assert not configuration.lti_configuration @@ -244,6 +252,7 @@ class DiscussionsConfigurationModelTest(TestCase): """ configuration = DiscussionsConfiguration.get(self.course_key_with_defaults) assert configuration is not None + assert configuration.posting_restrictions == PostingRestriction.SCHEDULED assert configuration.enabled assert not configuration.lti_configuration assert not configuration.plugin_configuration @@ -255,6 +264,7 @@ class DiscussionsConfigurationModelTest(TestCase): """ configuration = DiscussionsConfiguration.get(self.course_key_with_values) assert configuration is not None + assert configuration.posting_restrictions == PostingRestriction.ENABLED assert not configuration.enabled assert not configuration.lti_configuration assert configuration.plugin_configuration