diff --git a/course/utils.py b/course/utils.py index b4ca3ea9b019ed6d289600ba47ef106fabeb194b..b9dcd9265a2439ac29f64950fa2421ee176a7353 100644 --- a/course/utils.py +++ b/course/utils.py @@ -1341,4 +1341,18 @@ 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", + datetime.datetime(2019, 1, 1, 0, 0, 0, 0)) + + if custom_page_types_removed_deadline is None: + return None + + 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..bd7d63638a36f3ba3bd8a7536ec0217318272189 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() + + if 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/local_settings_example.py b/local_settings_example.py index db3188ac6eeae274bcaa67f4d46939be2bff81c8..6710efd47847429c84ee61d32867e82292b8e6b4 100644 --- a/local_settings_example.py +++ b/local_settings_example.py @@ -401,6 +401,18 @@ RELATE_TICKET_MINUTES_VALID_AFTER_USE = 12*60 # }}} +# {{{ deprecation deadline of custom page types + +# Uncomment the following to customize the deadline after which custom page types +# won't be supported for your RELATE instances. Format: a datetime.datetime +# instance. If not configured, the default deadline is 2019-01-01 00:00. If +# explicitly configured None, no deadline will be imposed. + +#from datetime import datetime +#RELATE_CUSTOM_PAGE_TYPES_REMOVED_DEADLINE = datetime(2019, 1, 1, 0, 0, 0, 0) + +# }}} + # {{{ saml2 (optional) if RELATE_SIGN_IN_BY_SAML2_ENABLED: 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..e5c2bc9280c787f6148687b6942a1b8c3dff6364 --- /dev/null +++ b/tests/test_validation/test_flow_validation.py @@ -0,0 +1,106 @@ +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) + + 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) + expected_message = ( + self.deprecate_warning_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.commit_sha_deprecated) + + 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) + not_expected_message = [ + "Custom page types were no longer supported", + "Custom page types will stop being supported" + ] + for m in not_expected_message: + self.assertNotContains(resp, not_expected_message) + + self.assertEqual( + self.get_course_commit_sha(self.instructor_participation), + self.commit_sha_deprecated)