Revert "feat: Reimagine certificate_availability_date and certificates_display_behavior"

This commit is contained in:
Matt Tuchfarber
2021-07-07 16:53:05 -04:00
committed by GitHub
parent b4df37d48e
commit 63cb6a97ff
34 changed files with 572 additions and 933 deletions

View File

@@ -49,7 +49,6 @@ from lms.djangoapps.verify_student.utils import is_verification_expiring_soon, v
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from openedx.core.djangoapps.theming.helpers import get_themes
from openedx.core.djangoapps.user_authn.utils import is_safe_login_or_logout_redirect
from xmodule.data import CertificatesDisplayBehaviors
# Enumeration of per-course verification statuses
# we display on the student dashboard.
@@ -512,17 +511,14 @@ def _cert_info(user, course_overview, cert_status):
is_hidden_status = status in ('processing', 'generating', 'notpassing', 'auditing')
if (
not certificates_viewable_for_course(course_overview)
and CertificateStatuses.is_passing_status(status)
and course_overview.certificates_display_behavior in (
CertificatesDisplayBehaviors.END_WITH_DATE,
CertificatesDisplayBehaviors.END
)
not certificates_viewable_for_course(course_overview) and
CertificateStatuses.is_passing_status(status) and
course_overview.certificate_available_date
):
status = certificate_earned_but_not_available_status
if (
course_overview.certificates_display_behavior == CertificatesDisplayBehaviors.EARLY_NO_INFO and
course_overview.certificates_display_behavior == 'early_no_info' and
is_hidden_status
):
return default_info

View File

@@ -22,7 +22,6 @@ from lms.djangoapps.certificates.tests.factories import (
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
from xmodule.data import CertificatesDisplayBehaviors
# pylint: disable=no-member
@@ -41,7 +40,7 @@ class CertificateDisplayTestBase(SharedModuleStoreTestCase):
def setUpClass(cls):
super().setUpClass()
cls.course = CourseFactory()
cls.course.certificates_display_behavior = CertificatesDisplayBehaviors.EARLY_NO_INFO
cls.course.certificates_display_behavior = "early_with_info"
with cls.store.branch_setting(ModuleStoreEnum.Branch.draft_preferred, cls.course.id):
cls.store.update_item(cls.course, cls.USERNAME)
@@ -117,54 +116,40 @@ class CertificateDashboardMessageDisplayTest(CertificateDisplayTestBase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.course.certificates_display_behavior = CertificatesDisplayBehaviors.END_WITH_DATE
cls.course.certificates_display_behavior = "end"
cls.course.save()
cls.store.update_item(cls.course, cls.USERNAME)
def _check_message(self, visible_date): # lint-amnesty, pylint: disable=missing-function-docstring
def _check_message(self, certificate_available_date): # lint-amnesty, pylint: disable=missing-function-docstring
response = self.client.get(reverse('dashboard'))
test_message = 'Your grade and certificate will be ready after'
is_past = visible_date < datetime.datetime.now(UTC)
if is_past:
if certificate_available_date is None:
self.assertNotContains(response, test_message)
self.assertNotContains(response, "View Test_Certificate")
self._check_can_download_certificate()
else:
elif datetime.datetime.now(UTC) < certificate_available_date:
self.assertContains(response, test_message)
self.assertNotContains(response, "View Test_Certificate")
else:
self._check_can_download_certificate()
@ddt.data(
(CertificatesDisplayBehaviors.END, True),
(CertificatesDisplayBehaviors.END, False),
(CertificatesDisplayBehaviors.END_WITH_DATE, True),
(CertificatesDisplayBehaviors.END_WITH_DATE, False)
)
@ddt.unpack
def test_certificate_available_date(self, certificates_display_behavior, past_date):
@ddt.data(True, False, None)
def test_certificate_available_date(self, past_certificate_available_date):
cert = self._create_certificate('verified')
cert.status = CertificateStatuses.downloadable
cert.save()
self.course.certificates_display_behavior = certificates_display_behavior
if certificates_display_behavior == CertificatesDisplayBehaviors.END:
if past_date:
self.course.end = PAST_DATE
else:
self.course.end = FUTURE_DATE
if certificates_display_behavior == CertificatesDisplayBehaviors.END_WITH_DATE:
if past_date:
self.course.certificate_available_date = PAST_DATE
else:
self.course.certificate_available_date = FUTURE_DATE
if past_certificate_available_date is None:
certificate_available_date = None
elif past_certificate_available_date:
certificate_available_date = PAST_DATE
elif not past_certificate_available_date:
certificate_available_date = FUTURE_DATE
self.course.certificate_available_date = certificate_available_date
self.course.save()
self.store.update_item(self.course, self.USERNAME)
self._check_message(PAST_DATE if past_date else FUTURE_DATE)
self._check_message(certificate_available_date)
@ddt.ddt

View File

@@ -40,7 +40,6 @@ from openedx.core.djangoapps.content.course_overviews.tests.factories import Cou
from openedx.core.djangoapps.site_configuration.tests.test_util import with_site_configuration_context
from openedx.features.course_duration_limits.models import CourseDurationLimitConfig
from openedx.features.course_experience.tests.views.helpers import add_course_mode
from xmodule.data import CertificatesDisplayBehaviors
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
@@ -228,7 +227,6 @@ class StudentDashboardTests(SharedModuleStoreTestCase, MilestonesTestCaseMixin,
id=course_key,
end_date=THREE_YEARS_AGO,
certificate_available_date=TOMORROW,
certificates_display_behavior=CertificatesDisplayBehaviors.END_WITH_DATE,
lowest_passing_grade=0.3
)
CourseEnrollmentFactory(course_id=course.id, user=self.user)
@@ -244,7 +242,6 @@ class StudentDashboardTests(SharedModuleStoreTestCase, MilestonesTestCaseMixin,
id=course_key,
end_date=TOMORROW,
certificate_available_date=TOMORROW,
certificates_display_behavior=CertificatesDisplayBehaviors.END_WITH_DATE,
lowest_passing_grade=0.3
)
CourseEnrollmentFactory(course_id=course.id, user=self.user)
@@ -260,7 +257,6 @@ class StudentDashboardTests(SharedModuleStoreTestCase, MilestonesTestCaseMixin,
id=course_key,
end_date=ONE_WEEK_AGO,
certificate_available_date=now(),
certificates_display_behavior=CertificatesDisplayBehaviors.END_WITH_DATE,
lowest_passing_grade=0.3
)
CourseEnrollmentFactory(course_id=course.id, user=self.user)

View File

@@ -49,8 +49,6 @@ from openedx.core.djangoapps.site_configuration.tests.mixins import SiteMixin
from openedx.core.djangolib.testing.utils import CacheIsolationTestCase, skip_unless_lms
from xmodule.modulestore.tests.django_utils import ModuleStoreEnum, ModuleStoreTestCase, SharedModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, check_mongo_calls
from xmodule.data import CertificatesDisplayBehaviors
log = logging.getLogger(__name__)
@@ -77,8 +75,7 @@ class CourseEndingTest(ModuleStoreTestCase):
survey_url = "http://a_survey.com"
course = CourseOverviewFactory.create(
end_of_course_survey_url=survey_url,
certificates_display_behavior=CertificatesDisplayBehaviors.END,
end=datetime.now(pytz.UTC) - timedelta(days=2)
certificates_display_behavior='end',
)
cert = GeneratedCertificateFactory.create(
user=user,
@@ -141,7 +138,7 @@ class CourseEndingTest(ModuleStoreTestCase):
'can_unenroll': True}
# test when the display is unavailable or notpassing, we get the correct results out
course2.certificates_display_behavior = CertificatesDisplayBehaviors.EARLY_NO_INFO
course2.certificates_display_behavior = 'early_no_info'
cert_status = {'status': 'unavailable', 'mode': 'honor', 'uuid': None}
assert _cert_info(user, course2, cert_status) == {'status': 'processing', 'show_survey_button': False,
'can_unenroll': True}
@@ -176,8 +173,7 @@ class CourseEndingTest(ModuleStoreTestCase):
survey_url = "http://a_survey.com"
course = CourseOverviewFactory.create(
end_of_course_survey_url=survey_url,
certificates_display_behavior=CertificatesDisplayBehaviors.END,
end=datetime.now(pytz.UTC) - timedelta(days=2),
certificates_display_behavior='end',
)
if cert_grade is not None:
@@ -202,8 +198,7 @@ class CourseEndingTest(ModuleStoreTestCase):
survey_url = "http://a_survey.com"
course = CourseOverviewFactory.create(
end_of_course_survey_url=survey_url,
certificates_display_behavior=CertificatesDisplayBehaviors.END,
end=datetime.now(pytz.UTC) - timedelta(days=2),
certificates_display_behavior='end',
)
cert_status = {'status': 'generating', 'mode': 'honor', 'uuid': None}

View File

@@ -14,8 +14,6 @@ from math import exp
import dateutil.parser
from pytz import utc
from xmodule.data import CertificatesDisplayBehaviors
DEFAULT_START_DATE = datetime(2030, 1, 1, tzinfo=utc)
"""
@@ -158,18 +156,15 @@ def may_certify_for_course(
self_paced (bool): Whether the course is self-paced.
"""
show_early = (
certificates_display_behavior == CertificatesDisplayBehaviors.EARLY_NO_INFO
certificates_display_behavior in ('early_with_info', 'early_no_info')
or certificates_show_before_end
)
past_available_date = (
certificates_display_behavior == CertificatesDisplayBehaviors.END_WITH_DATE
and certificate_available_date
certificate_available_date
and certificate_available_date < datetime.now(utc)
)
ended_without_available_date = (
certificates_display_behavior == CertificatesDisplayBehaviors.END
and has_ended
)
ended_without_available_date = (certificate_available_date is None) and has_ended
return any((self_paced, show_early, past_available_date, ended_without_available_date))

View File

@@ -5,7 +5,7 @@ Django module container for classes and operations related to the "Course Module
import json
import logging
from datetime import datetime
from datetime import datetime, timedelta
from io import BytesIO
import dateutil.parser
@@ -22,7 +22,6 @@ from openedx.core.lib.license import LicenseMixin
from openedx.core.lib.teams_config import TeamsConfig # lint-amnesty, pylint: disable=unused-import
from xmodule import course_metadata_utils
from xmodule.course_metadata_utils import DEFAULT_GRADING_POLICY, DEFAULT_START_DATE
from xmodule.data import CertificatesDisplayBehaviors
from xmodule.graders import grader_from_conf
from xmodule.seq_module import SequenceBlock
from xmodule.tabs import CourseTabList, InvalidTabsException
@@ -556,15 +555,15 @@ class CourseFields: # lint-amnesty, pylint: disable=missing-class-docstring
certificates_display_behavior = String(
display_name=_("Certificates Display Behavior"),
help=_(
"Enter end, end_with_date, or early_no_info. After certificate generation, students who passed see a "
"Enter end, early_with_info, or early_no_info. After certificate generation, students who passed see a "
"link to their certificates on the dashboard and students who did not pass see information about the "
"grading configuration. The default is end, which displays this certificate information to all students "
"after the course end date. To display the certificate information to all students at a date after the "
"course end date, use end_with_date and add a certificate_available_date. To display only the links to "
"passing students as soon as certificates are generated, enter early_no_info."
"after the course end date. To display this certificate information to all students as soon as "
"certificates are generated, enter early_with_info. To display only the links to passing students as "
"soon as certificates are generated, enter early_no_info."
),
scope=Scope.settings,
default=CertificatesDisplayBehaviors.END,
default="end"
)
course_image = String(
display_name=_("Course About Page Image"),
@@ -1062,6 +1061,8 @@ class CourseBlock(
except InvalidTabsException as err:
raise type(err)(f'{str(err)} For course: {str(self.id)}') # lint-amnesty, pylint: disable=line-too-long
self.set_default_certificate_available_date()
def set_grading_policy(self, course_policy):
"""
The JSON object can have the keys GRADER and GRADE_CUTOFFS. If either is
@@ -1087,6 +1088,10 @@ class CourseBlock(
self.raw_grader = grading_policy['GRADER'] # used for cms access
self.grade_cutoffs = grading_policy['GRADE_CUTOFFS']
def set_default_certificate_available_date(self):
if (not self.certificate_available_date) and self.end:
self.certificate_available_date = self.end + timedelta(days=2)
@classmethod
def read_grading_policy(cls, paths, system):
"""Load a grading policy from the specified paths, in order, if it exists."""

View File

@@ -1,25 +0,0 @@
"""
Public data structures for this app.
See OEP-49 for details
"""
from enum import Enum
class CertificatesDisplayBehaviors(str, Enum):
"""
Options for the certificates_display_behavior field of a course
end: Certificates are available at the end of the course
end_with_date: Certificates are available after the certificate_available_date (post course end)
early_no_info: Certificates are available immediately after earning them.
Only in affect for instructor based courses.
"""
END = "end"
END_WITH_DATE = "end_with_date"
EARLY_NO_INFO = "early_no_info"
@classmethod
def includes_value(cls, value):
return value in set(item.value for item in cls)

View File

@@ -23,7 +23,6 @@ from xmodule.course_metadata_utils import (
may_certify_for_course,
number_for_course_location
)
from xmodule.data import CertificatesDisplayBehaviors
from xmodule.modulestore.tests.utils import (
MixedModulestoreBuilder,
MongoModulestoreBuilder,
@@ -164,28 +163,16 @@ class CourseMetadataUtilsTestCase(TestCase):
TestScenario((DEFAULT_START_DATE, None), True),
]),
FunctionTest(may_certify_for_course, [
# Test certificates_show_before_end
TestScenario((CertificatesDisplayBehaviors.EARLY_NO_INFO, True, False, test_datetime, False), True),
TestScenario((CertificatesDisplayBehaviors.END, True, False, test_datetime, False), True),
TestScenario((CertificatesDisplayBehaviors.END_WITH_DATE, True, False, _NEXT_WEEK, False), True),
# Test that EARLY_NO_INFO
TestScenario((CertificatesDisplayBehaviors.EARLY_NO_INFO, True, True, test_datetime, False), True),
TestScenario((CertificatesDisplayBehaviors.EARLY_NO_INFO, False, False, test_datetime, False), True),
# Test END_WITH_DATE
TestScenario((CertificatesDisplayBehaviors.END_WITH_DATE, False, False, test_datetime, False), True),
TestScenario((CertificatesDisplayBehaviors.END_WITH_DATE, False, False, _LAST_WEEK, False), True),
TestScenario((CertificatesDisplayBehaviors.END_WITH_DATE, False, False, _NEXT_WEEK, False), False),
TestScenario((CertificatesDisplayBehaviors.END_WITH_DATE, False, False, None, False), False),
# Test END
TestScenario((CertificatesDisplayBehaviors.END, False, False, test_datetime, False), False),
TestScenario((CertificatesDisplayBehaviors.END, False, True, test_datetime, False), True),
# Test self_paced
TestScenario((CertificatesDisplayBehaviors.END, False, False, test_datetime, False), False),
TestScenario((CertificatesDisplayBehaviors.END, False, False, test_datetime, True), True),
TestScenario(('early_with_info', True, True, test_datetime, False), True),
TestScenario(('early_no_info', False, False, test_datetime, False), True),
TestScenario(('end', True, False, test_datetime, False), True),
TestScenario(('end', False, True, test_datetime, False), True),
TestScenario(('end', False, False, _NEXT_WEEK, False), False),
TestScenario(('end', False, False, _LAST_WEEK, False), True),
TestScenario(('end', False, False, None, False), False),
TestScenario(('early_with_info', False, False, None, False), True),
TestScenario(('end', False, False, _NEXT_WEEK, False), False),
TestScenario(('end', False, False, _NEXT_WEEK, True), True),
]),
]

View File

@@ -18,7 +18,6 @@ from xblock.runtime import DictKeyValueStore, KvsFieldData
from openedx.core.lib.teams_config import TeamsConfig, DEFAULT_COURSE_RUN_MAX_TEAM_SIZE
import xmodule.course_module
from xmodule.data import CertificatesDisplayBehaviors
from xmodule.modulestore.xml import ImportSystem, XMLModuleStore
from xmodule.modulestore.exceptions import InvalidProctoringProvider
@@ -106,41 +105,36 @@ class HasEndedMayCertifyTestCase(unittest.TestCase):
super().setUp()
system = DummySystem(load_error_modules=True) # lint-amnesty, pylint: disable=unused-variable
#sample_xml = """
# <course org="{org}" course="{course}" display_organization="{org}_display" display_coursenumber="{course}_display" # lint-amnesty, pylint: disable=line-too-long
# graceperiod="1 day" url_name="test"
# start="2012-01-01T12:00"
# {end}
# certificates_show_before_end={cert}>
# <chapter url="hi" url_name="ch" display_name="CH">
# <html url_name="h" display_name="H">Two houses, ...</html>
# </chapter>
# </course>
#""".format(org=ORG, course=COURSE)
past_end = (datetime.now() - timedelta(days=12)).strftime("%Y-%m-%dT%H:%M:00")
future_end = (datetime.now() + timedelta(days=12)).strftime("%Y-%m-%dT%H:%M:00")
self.past_show_certs = get_dummy_course(
"2012-01-01T12:00",
end=past_end,
certs=CertificatesDisplayBehaviors.EARLY_NO_INFO
)
self.past_show_certs_no_info = get_dummy_course(
"2012-01-01T12:00",
end=past_end,
certs=CertificatesDisplayBehaviors.EARLY_NO_INFO
)
self.past_noshow_certs = get_dummy_course(
"2012-01-01T12:00",
end=past_end,
certs=CertificatesDisplayBehaviors.END
)
self.future_show_certs_no_info = get_dummy_course(
"2012-01-01T12:00",
end=future_end,
certs=CertificatesDisplayBehaviors.EARLY_NO_INFO
)
self.future_noshow_certs = get_dummy_course(
"2012-01-01T12:00",
end=future_end,
certs=CertificatesDisplayBehaviors.END
)
self.past_show_certs = get_dummy_course("2012-01-01T12:00", end=past_end, certs='early_with_info')
self.past_show_certs_no_info = get_dummy_course("2012-01-01T12:00", end=past_end, certs='early_no_info')
self.past_noshow_certs = get_dummy_course("2012-01-01T12:00", end=past_end, certs='end')
self.future_show_certs = get_dummy_course("2012-01-01T12:00", end=future_end, certs='early_with_info')
self.future_show_certs_no_info = get_dummy_course("2012-01-01T12:00", end=future_end, certs='early_no_info')
self.future_noshow_certs = get_dummy_course("2012-01-01T12:00", end=future_end, certs='end')
#self.past_show_certs = system.process_xml(sample_xml.format(end=past_end, cert=True))
#self.past_noshow_certs = system.process_xml(sample_xml.format(end=past_end, cert=False))
#self.future_show_certs = system.process_xml(sample_xml.format(end=future_end, cert=True))
#self.future_noshow_certs = system.process_xml(sample_xml.format(end=future_end, cert=False))
def test_has_ended(self):
"""Check that has_ended correctly tells us when a course is over."""
assert self.past_show_certs.has_ended()
assert self.past_show_certs_no_info.has_ended()
assert self.past_noshow_certs.has_ended()
assert not self.future_show_certs.has_ended()
assert not self.future_show_certs_no_info.has_ended()
assert not self.future_noshow_certs.has_ended()
@@ -149,6 +143,7 @@ class HasEndedMayCertifyTestCase(unittest.TestCase):
assert self.past_show_certs.may_certify()
assert self.past_noshow_certs.may_certify()
assert self.past_show_certs_no_info.may_certify()
assert self.future_show_certs.may_certify()
assert self.future_show_certs_no_info.may_certify()
assert not self.future_noshow_certs.may_certify()
@@ -416,6 +411,14 @@ class CourseBlockTestCase(unittest.TestCase):
"""
assert self.course.number == COURSE
def test_set_default_certificate_available_date(self):
"""
The certificate_available_date field should default to two days
after the course end date.
"""
expected_certificate_available_date = self.course.end + timedelta(days=2)
assert expected_certificate_available_date == self.course.certificate_available_date
class ProctoringProviderTestCase(unittest.TestCase):
"""