diff --git a/README.rst b/README.rst index 72e7c4654dc9f939864e8032c3e29bd495fab8af..f46c223004be068896fed52adf2211cfaaffd0c2 100644 --- a/README.rst +++ b/README.rst @@ -24,7 +24,6 @@ features: * Simple, text-based format for reusable course content * Based on standard `YAML `_, `Markdown `_ - * Instructors can implement custom question/page types in Python. See `example content `_. diff --git a/course/utils.py b/course/utils.py index b4ca3ea9b019ed6d289600ba47ef106fabeb194b..0fd5c34e8c7aca7136b5d9834b8b854f0b8128dd 100644 --- a/course/utils.py +++ b/course/utils.py @@ -1341,4 +1341,20 @@ class NBConvertExtension(markdown.Extension): # }}} + +def get_custom_page_types_stop_support_deadline(): + # type: () -> Optional[datetime.datetime] + from django.conf import settings + custom_page_types_removed_deadline = getattr( + settings, "RELATE_CUSTOM_PAGE_TYPES_REMOVED_DEADLINE", None) + + force_deadline = datetime.datetime(2019, 1, 1, 0, 0, 0, 0) + + if (custom_page_types_removed_deadline is None + or custom_page_types_removed_deadline > force_deadline): + custom_page_types_removed_deadline = force_deadline + + from relate.utils import localize_datetime + return localize_datetime(custom_page_types_removed_deadline) + # vim: foldmethod=marker diff --git a/course/validation.py b/course/validation.py index 4b184e24c5bd6cd310f9cd997ea7f0b0c6ba2199..7fb86716e141cc63d9aa53658afe8939c074f1d3 100644 --- a/course/validation.py +++ b/course/validation.py @@ -474,12 +474,34 @@ def validate_flow_page(vctx, location, page_desc): validate_identifier(vctx, location, page_desc.id) if page_desc.type.startswith("repo:"): - vctx.add_warning( - location, - _("Custom page type '%s' specified. " - "Custom page types will stop being supported in " - "Relate in 2019.") - % page_desc.type) + from django.conf import settings + from course.utils import get_custom_page_types_stop_support_deadline + from relate.utils import local_now, format_datetime_local + + deadline = get_custom_page_types_stop_support_deadline() + + assert deadline is not None + + if deadline < local_now(): + raise ValidationError( + location, + _("Custom page type '%(page_type)s' specified. " + "Custom page types were no longer supported in " + "%(relate_site_name)s since %(date_time)s.") + % {"page_type": page_desc.type, + "date_time": format_datetime_local(deadline), + "relate_site_name": settings.RELATE_SITE_NAME, + }) + else: + vctx.add_warning( + location, + _("Custom page type '%(page_type)s' specified. " + "Custom page types will stop being supported in " + "%(relate_site_name)s at %(date_time)s.") + % {"page_type": page_desc.type, + "date_time": format_datetime_local(deadline), + "relate_site_name": settings.RELATE_SITE_NAME + }) from course.content import get_flow_page_class try: diff --git a/doc/index.rst b/doc/index.rst index 5f19c8fa2507b4f5ea65794ca32478b340760178..e8fd262cf76e721a46247b0396a5dba3d42dde3d 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -16,7 +16,6 @@ features: * Simple, text-based format for reusable course content * Based on standard `YAML `_, `Markdown `_ - * Instructors can implement custom question/page types in Python. See `example content `_. diff --git a/relate/checks.py b/relate/checks.py index c5468932e379a305c7ac3c75411c9773d059e51d..0e7527d29fde826392e24ccea76e319b0efbb539 100644 --- a/relate/checks.py +++ b/relate/checks.py @@ -56,6 +56,8 @@ RELATE_STARTUP_CHECKS_TAG = "start_up_check" RELATE_STARTUP_CHECKS_EXTRA_TAG = "startup_checks_extra" RELATE_DISABLE_CODEHILITE_MARKDOWN_EXTENSION = ( "RELATE_DISABLE_CODEHILITE_MARKDOWN_EXTENSION") +RELATE_CUSTOM_PAGE_TYPES_REMOVED_DEADLINE = ( + "RELATE_CUSTOM_PAGE_TYPES_REMOVED_DEADLINE") class RelateCriticalCheckMessage(Critical): @@ -486,6 +488,20 @@ def check_relate_settings(app_configs, **kwargs): id="relate_override_templates_dirs.W001" )) + # }}} + + # {{{ check RELATE_CUSTOM_PAGE_TYPES_REMOVED_DEADLINE + relate_custom_page_types_removed_deadline = getattr( + settings, RELATE_CUSTOM_PAGE_TYPES_REMOVED_DEADLINE, None) + if relate_custom_page_types_removed_deadline is not None: + from datetime import datetime + if not isinstance(relate_custom_page_types_removed_deadline, datetime): + errors.append(RelateCriticalCheckMessage( + msg=(INSTANCE_ERROR_PATTERN + % {"location": RELATE_CUSTOM_PAGE_TYPES_REMOVED_DEADLINE, + "types": "datetime.datetime"}), + id="relate_custom_page_types_removed_deadline.E001")) + # }}} return errors diff --git a/tests/test_checks.py b/tests/test_checks.py index e81fe2d709319ac909c3bb16e36954c71156e2c0..76aeacfb2afbe1d1603c3de681fd231fef8f2c73 100644 --- a/tests/test_checks.py +++ b/tests/test_checks.py @@ -23,6 +23,8 @@ THE SOFTWARE. """ import os +from datetime import datetime + from django.test import SimpleTestCase from django.test.utils import override_settings from django.utils.translation import ugettext_lazy as _ @@ -712,3 +714,22 @@ class CheckRelateDisableCodehiliteMarkdownExtensions(CheckRelateSettingsBase): def test_warning_conf_false(self): self.assertCheckMessages( ["relate_disable_codehilite_markdown_extension.W002"]) + + +class CheckRelateCustomPageTypesRemovedDeadline(CheckRelateSettingsBase): + msg_id_prefix = "relate_custom_page_types_removed_deadline" + VALID_CONF = datetime(2017, 12, 31, 0, 0) + INVALID_CONF = "2017-12-31 00:00" + + @override_settings(RELATE_CUSTOM_PAGE_TYPES_REMOVED_DEADLINE=None) + def test_valid_conf_none(self): + self.assertCheckMessages([]) + + @override_settings(RELATE_CUSTOM_PAGE_TYPES_REMOVED_DEADLINE=VALID_CONF) + def test_valid_conf(self): + self.assertCheckMessages([]) + + @override_settings(RELATE_CUSTOM_PAGE_TYPES_REMOVED_DEADLINE=INVALID_CONF) + def test_invalid_conf(self): + self.assertCheckMessages( + ["relate_custom_page_types_removed_deadline.E001"]) diff --git a/tests/test_validation/test_flow_validation.py b/tests/test_validation/test_flow_validation.py new file mode 100644 index 0000000000000000000000000000000000000000..ed2bf16781fbae31f80b0915434b81cab409197f --- /dev/null +++ b/tests/test_validation/test_flow_validation.py @@ -0,0 +1,130 @@ +from __future__ import division + +__copyright__ = "Copyright (C) 2018 Dong Zhuang" + +__license__ = """ +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +""" + +from datetime import datetime + +from django.test import TestCase, override_settings + +from relate.utils import format_datetime_local + +from tests.base_test_mixins import ( + SingleCourseTestMixin, FallBackStorageMessageTestMixin) + + +class ValidateFlowPageTest(SingleCourseTestMixin, + FallBackStorageMessageTestMixin, TestCase): + + def setUp(self): + super(ValidateFlowPageTest, self).setUp() + self.current_commit_sha = self.get_course_commit_sha( + self.instructor_participation) + + force_deadline = datetime(2019, 1, 1, 0, 0, 0, 0) + + custom_page_type = "repo:simple_questions.MyTextQuestion" + + commit_sha_deprecated = b"593a1cdcecc6f4759fd5cadaacec0ba9dd0715a7" + + deprecate_warning_message_pattern = ( + "Custom page type '%(page_type)s' specified. " + "Custom page types will stop being supported in " + "RELATE at %(date_time)s.") + + expired_error_message_pattern = ( + "Custom page type '%(page_type)s' specified. " + "Custom page types were no longer supported in " + "RELATE since %(date_time)s.") + + def test_custom_page_types_deprecate(self): + deadline = datetime(2039, 1, 1, 0, 0, 0, 0) + + with override_settings( + RELATE_CUSTOM_PAGE_TYPES_REMOVED_DEADLINE=deadline): + resp = self.post_update_course_content( + commit_sha=self.commit_sha_deprecated) + self.assertEqual(resp.status_code, 200) + + if datetime.now() <= self.force_deadline: + expected_message = ( + self.deprecate_warning_message_pattern + % {"page_type": self.custom_page_type, + "date_time": format_datetime_local(self.force_deadline)} + ) + self.assertEqual( + self.get_course_commit_sha(self.instructor_participation), + self.commit_sha_deprecated) + else: + expected_message = ( + self.expired_error_message_pattern + % {"page_type": self.custom_page_type, + "date_time": format_datetime_local(self.force_deadline)} + ) + self.assertEqual( + self.get_course_commit_sha(self.instructor_participation), + self.current_commit_sha) + self.assertResponseMessagesContains(resp, expected_message, loose=True) + + def test_custom_page_types_not_supported(self): + deadline = datetime(2017, 1, 1, 0, 0, 0, 0) + with override_settings( + RELATE_CUSTOM_PAGE_TYPES_REMOVED_DEADLINE=deadline): + resp = self.post_update_course_content( + commit_sha=self.commit_sha_deprecated) + self.assertEqual(resp.status_code, 200) + expected_message = ( + self.expired_error_message_pattern + % {"page_type": self.custom_page_type, + "date_time": format_datetime_local(deadline)} + ) + self.assertResponseMessagesContains(resp, expected_message, loose=True) + self.assertEqual( + self.get_course_commit_sha(self.instructor_participation), + self.current_commit_sha) + + def test_custom_page_types_deadline_configured_none(self): + with override_settings( + RELATE_CUSTOM_PAGE_TYPES_REMOVED_DEADLINE=None): + resp = self.post_update_course_content( + commit_sha=self.commit_sha_deprecated) + self.assertEqual(resp.status_code, 200) + + if datetime.now() <= self.force_deadline: + expected_message = ( + self.deprecate_warning_message_pattern + % {"page_type": self.custom_page_type, + "date_time": format_datetime_local(self.force_deadline)} + ) + self.assertEqual( + self.get_course_commit_sha(self.instructor_participation), + self.commit_sha_deprecated) + else: + expected_message = ( + self.expired_error_message_pattern + % {"page_type": self.custom_page_type, + "date_time": format_datetime_local(self.force_deadline)} + ) + self.assertEqual( + self.get_course_commit_sha(self.instructor_participation), + self.current_commit_sha) + self.assertResponseMessagesContains(resp, expected_message, loose=True)