feat: Pass segment properties (#30919)

- For new enrollment email pass extra segment event properties.
VAN-999
This commit is contained in:
Mubbshar Anwar
2022-09-05 16:55:46 +05:00
committed by GitHub
parent cb9e6d4841
commit 22b378e605
12 changed files with 368 additions and 27 deletions

View File

@@ -324,11 +324,11 @@ class ChooseModeView(View):
# been configured. However, alternative enrollment workflows have been introduced into the
# system, such as third-party discovery. These workflows result in learners arriving
# directly at this screen, and they will not necessarily be pre-enrolled in the audit mode.
CourseEnrollment.enroll(request.user, course_key, CourseMode.AUDIT)
CourseEnrollment.enroll(request.user, course_key, CourseMode.AUDIT, request=request)
return self._redirect_to_course_or_dashboard(course, course_key, user)
if requested_mode == 'honor':
CourseEnrollment.enroll(user, course_key, mode=requested_mode)
CourseEnrollment.enroll(user, course_key, mode=requested_mode, request=request)
return self._redirect_to_course_or_dashboard(course, course_key, user)
mode_info = allowed_modes[requested_mode]

View File

@@ -20,7 +20,7 @@ from django.core.validators import ValidationError
from django.db import IntegrityError, ProgrammingError, transaction
from django.urls import NoReverseMatch, reverse
from django.utils.translation import gettext as _
from pytz import UTC
from pytz import UTC, timezone
from common.djangoapps import third_party_auth
from common.djangoapps.course_modes.models import CourseMode
@@ -50,10 +50,15 @@ from lms.djangoapps.instructor import access
from lms.djangoapps.verify_student.models import VerificationDeadline
from lms.djangoapps.verify_student.services import IDVerificationService
from lms.djangoapps.verify_student.utils import is_verification_expiring_soon, verification_for_datetime
from lms.djangoapps.courseware.courses import get_course_date_blocks, get_course_with_access
from lms.djangoapps.courseware.date_summary import TodaysDate
from lms.djangoapps.courseware.context_processor import user_timezone_locale_prefs
from lms.djangoapps.course_home_api.dates.serializers import DateSummarySerializer
from openedx.core.djangoapps.content.block_structure.exceptions import UsageKeyNotInBlockStructure
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 openedx.core.lib.time_zone_utils import get_time_zone_offset
from xmodule.data import CertificatesDisplayBehaviors # lint-amnesty, pylint: disable=wrong-import-order
# Enumeration of per-course verification statuses
@@ -814,3 +819,89 @@ def user_has_passing_grade_in_course(enrollment):
except AttributeError:
pass
return False
def get_instructors(course_run, marketing_root_url):
"""
Get course instructors.
"""
instructors = []
staff = course_run.get('staff', [])
for instructor in staff:
instructor = {
'name': f"{instructor.get('given_name')} {instructor.get('family_name')}",
'profile_image_url': instructor.get('profile_image_url'),
'organization_name': (instructor.get('position').get('organization_name')
if instructor.get('position') else ''),
'bio_url': f"{marketing_root_url}/bio/{instructor.get('slug')}"
}
instructors.append(instructor)
return instructors
def _prepare_date_block(block, block_date, user_timezone):
"""
Prepare date block which include assignment related data for this date
"""
timezone_offset = get_time_zone_offset(user_timezone, block_date)
block = {
'title': block.get('title', ''),
'assignment_type': block.get('assignment_type', '') or '',
'assignment_count': 0,
'link': block.get('link', ''),
'date': block_date,
'due_date': block_date.strftime("%a, %b %d, %Y"),
'due_time': (f'{block_date.strftime("%H:%M %p")} GMT{timezone_offset}' if block.get('assignment_type') else '')
}
return block
def get_course_dates_for_email(user, course_id, request):
"""
Getting nearest dates from today one would be before today and one
would be after today.
"""
user_timezone_locale = user_timezone_locale_prefs(request)
user_timezone = timezone(user_timezone_locale['user_timezone'] or str(UTC))
course = get_course_with_access(user, 'load', course_id)
date_blocks = get_course_date_blocks(course, user, request, include_access=True, include_past_dates=True)
date_blocks = [block for block in date_blocks if not isinstance(block, TodaysDate)]
blocks = DateSummarySerializer(date_blocks, many=True).data
today = datetime.now(user_timezone)
course_date = {
'title': '',
'assignment_type': '',
'link': '',
'assignment_count': 0,
'date': '',
'due_date': today.strftime("%a, %b %d, %Y"),
'due_time': ''
}
course_date_list = [{**course_date, }, {**course_date, 'date': today}, {**course_date}]
for block in blocks:
block_date = datetime.strptime(block.get('date')[:19], '%Y-%m-%dT%H:%M:%S')
block_date = block_date.replace(tzinfo=UTC)
block_date = block_date.astimezone(user_timezone)
if block_date < today:
if block_date == course_date_list[0]['date'] and block.get('assignment_type'):
course_date_list[0]['assignment_count'] += 1
else:
course_date_list[0].update(_prepare_date_block(block, block_date, user_timezone))
if block_date == today:
if block.get('assignment_type') and course_date_list[1]['assignment_type'] != '':
course_date_list[1]['assignment_count'] += 1
else:
course_date_list[1].update(_prepare_date_block(block, block_date, user_timezone))
if block_date > today:
if block_date == course_date_list[2]['date'] and block.get('assignment_type'):
course_date_list[2]['assignment_count'] += 1
if course_date_list[2]['date'] == '':
course_date_list[2].update(_prepare_date_block(block, block_date, user_timezone))
return course_date_list

View File

@@ -20,6 +20,7 @@ from common.djangoapps.student.models import (
)
from common.djangoapps.student.signals import UNENROLL_DONE
from common.djangoapps.student.tests.factories import UserFactory
from openedx.core.djangoapps.catalog.tests.factories import CourseRunFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.modulestore.tests.factories import CourseFactory # lint-amnesty, pylint: disable=wrong-import-order
@@ -54,7 +55,8 @@ class TestTransferStudents(ModuleStoreTestCase):
assert skip_refund
self.signal_fired = True
def test_transfer_students(self):
@patch('openedx.core.djangoapps.catalog.api.get_course_run_details')
def test_transfer_students(self, mock_get_course_run_details):
"""
Verify the transfer student command works as intended.
"""
@@ -65,6 +67,12 @@ class TestTransferStudents(ModuleStoreTestCase):
# Original Course
original_course_location = locator.CourseLocator('Org0', 'Course0', 'Run0')
course = self._create_course(original_course_location)
course_run = CourseRunFactory.create(key=course.id)
course_run['min_effort'] = 1
course_run['enrollment_count'] = 12345
mock_get_course_run_details.return_value = course_run
# Enroll the student in 'verified'
CourseEnrollment.enroll(student, course.id, mode='verified')

View File

@@ -1426,7 +1426,7 @@ class CourseEnrollment(models.Model):
from openedx.core.djangoapps.enrollments.permissions import ENROLL_IN_COURSE
return not user.has_perm(ENROLL_IN_COURSE, course)
def update_enrollment(self, mode=None, is_active=None, skip_refund=False, enterprise_uuid=None):
def update_enrollment(self, mode=None, is_active=None, skip_refund=False, enterprise_uuid=None, request=None):
"""
Updates an enrollment for a user in a class. This includes options
like changing the mode, toggling is_active True/False, etc.
@@ -1491,7 +1491,7 @@ class CourseEnrollment(models.Model):
if activation_changed:
if self.is_active:
self.emit_event(EVENT_NAME_ENROLLMENT_ACTIVATED, enterprise_uuid=enterprise_uuid)
self.emit_event(EVENT_NAME_ENROLLMENT_ACTIVATED, enterprise_uuid=enterprise_uuid, request=request)
else:
UNENROLL_DONE.send(sender=None, course_enrollment=self, skip_refund=skip_refund)
self.emit_event(EVENT_NAME_ENROLLMENT_DEACTIVATED)
@@ -1554,11 +1554,16 @@ class CourseEnrollment(models.Model):
mode=mode, course_id=course_id,
cost=cost, currency=currency)
def emit_event(self, event_name, enterprise_uuid=None):
def emit_event(self, event_name, enterprise_uuid=None, request=None): # pylint: disable=too-many-statements
"""
Emits an event to explicitly track course enrollment and unenrollment.
"""
from common.djangoapps.student.helpers import get_course_dates_for_email, get_instructors
from common.djangoapps.student.toggles import should_send_redesign_email
from openedx.core.djangoapps.schedules.config import set_up_external_updates_for_enrollment
from openedx.core.djangoapps.catalog.api import get_course_run_details
from openedx.core.djangoapps.catalog.utils import get_owners_for_course, get_course_uuid_for_course
from openedx.features.course_experience import ENABLE_COURSE_GOALS
from openedx.core.djangoapps.user_api.preferences.api import get_user_preference
from openedx.features.enterprise_support.utils import is_enterprise_learner
@@ -1596,6 +1601,55 @@ class CourseEnrollment(models.Model):
segment_traits['email'] = self.user.email
if event_name == EVENT_NAME_ENROLLMENT_ACTIVATED:
studio_request = settings.ROOT_URLCONF == 'cms.urls'
extra_segment_properties = {
'studio_request': studio_request
}
if not studio_request and should_send_redesign_email():
if not request:
request = crum.get_current_request()
marketing_root_url = settings.MKTG_URLS.get('ROOT')
course_dates_list = get_course_dates_for_email(self.user, self.course.id, request)
course_run_fields = [
'key', 'title', 'short_description', 'marketing_url', 'pacing_type', 'min_effort',
'max_effort', 'weeks_to_complete', 'enrollment_count', 'image', 'staff',
]
owners, course_run = None, None
try:
course_uuid = get_course_uuid_for_course(str(self.course_id))
owners = get_owners_for_course(course_uuid=course_uuid)
course_run = get_course_run_details(str(self.course_id), course_run_fields)
except Exception: # pylint: disable=broad-except
pass
if course_run:
instructors = get_instructors(course_run, marketing_root_url)
extra_segment_properties.update({
'instructors': instructors,
'instructors_count': 'even' if len(instructors) % 2 == 0 else 'odd',
'pacing_type': course_run.get('pacing_type'),
'min_effort': course_run.get('min_effort'),
'max_effort': course_run.get('max_effort'),
'weeks_to_complete': course_run.get('weeks_to_complete'),
'learners_count': '{:,}'.format(course_run.get('enrollment_count')),
'course_title': course_run.get('title'),
'short_description': course_run.get('short_description'),
'marketing_url': course_run.get('marketing_url'),
'banner_image_url': course_run.get('image').get('src') if course_run.get('image') else ''
})
extra_segment_properties.update({
'goals_enabled': ENABLE_COURSE_GOALS.is_enabled(self.course_id),
'course_date_blocks': course_dates_list,
'partner_image_url': owners[0].get('logo_image_url') if owners else '',
'learner_name': self.user.profile.name,
'course_run_key': str(self.course_id),
'price': self.course_price,
'lms_base_url': configuration_helpers.get_value('LMS_ROOT_URL', settings.LMS_ROOT_URL),
'learning_base_url': configuration_helpers.get_value('LEARNING_MICROFRONTEND_URL',
settings.LEARNING_MICROFRONTEND_URL)
})
segment_properties.update(extra_segment_properties)
segment_properties['email'] = self.user.email
# This next property is for an experiment, see method's comments for more information
segment_properties['external_course_updates'] = set_up_external_updates_for_enrollment(self.user,
@@ -1640,7 +1694,8 @@ class CourseEnrollment(models.Model):
)
@classmethod
def enroll(cls, user, course_key, mode=None, check_access=False, can_upgrade=False, enterprise_uuid=None):
def enroll(cls, user, course_key, mode=None, check_access=False, can_upgrade=False,
enterprise_uuid=None, request=None):
"""
Enroll a user in a course. This saves immediately.
@@ -1735,7 +1790,7 @@ class CourseEnrollment(models.Model):
# User is allowed to enroll if they've reached this point.
enrollment = cls.get_or_create_enrollment(user, course_key)
enrollment.update_enrollment(is_active=True, mode=mode, enterprise_uuid=enterprise_uuid)
enrollment.update_enrollment(is_active=True, mode=mode, enterprise_uuid=enterprise_uuid, request=request)
enrollment.send_signal(EnrollStatusChange.enroll)
# .. event_implemented_name: COURSE_ENROLLMENT_CREATED

View File

@@ -26,6 +26,7 @@ from common.djangoapps.student.roles import CourseInstructorRole, CourseStaffRol
from common.djangoapps.student.tests.factories import CourseEnrollmentAllowedFactory, UserFactory
from common.djangoapps.util.testing import UrlResetMixin
from openedx.core.djangoapps.embargo.test_utils import restrict_course
from openedx.core.djangoapps.catalog.tests.factories import CourseRunFactory
@ddt.ddt
@@ -74,6 +75,17 @@ class EnrollmentTest(UrlResetMixin, ModuleStoreTestCase, OpenEdxEventsTestMixin)
# Set up proctored exam
self._create_proctored_exam(self.proctored_course)
course_run = CourseRunFactory.create(key=self.course.id)
course_run.update({
'min_effort': 1,
'enrollment_count': 12345
})
patch_course_data = patch('openedx.core.djangoapps.catalog.api.get_course_run_details')
course_data = patch_course_data.start()
course_data.return_value = course_run
self.addCleanup(patch_course_data.stop)
def _create_proctored_exam(self, course):
"""
Helper function to create a proctored exam for a given course

View File

@@ -21,6 +21,7 @@ from markupsafe import escape
from opaque_keys.edx.keys import CourseKey
from opaque_keys.edx.locations import CourseLocator
from pyquery import PyQuery as pq
from edx_toggles.toggles.testutils import override_waffle_flag
from common.djangoapps.course_modes.models import CourseMode
from common.djangoapps.course_modes.tests.factories import CourseModeFactory
@@ -38,6 +39,7 @@ from common.djangoapps.student.tests.factories import CourseEnrollmentFactory, U
from common.djangoapps.student.views import complete_course_mode_info
from common.djangoapps.util.model_utils import USER_SETTINGS_CHANGED_EVENT_NAME
from common.djangoapps.util.testing import EventTestMixin
from common.djangoapps.student.toggles import ENROLLMENT_CONFIRMATION_EMAIL_REDESIGN
from lms.djangoapps.certificates.data import CertificateStatuses
from lms.djangoapps.certificates.tests.factories import GeneratedCertificateFactory
from lms.djangoapps.verify_student.tests import TestVerificationBase
@@ -47,6 +49,7 @@ from openedx.core.djangoapps.content.course_overviews.tests.factories import Cou
from openedx.core.djangoapps.programs.tests.mixins import ProgramsApiConfigMixin
from openedx.core.djangoapps.site_configuration.tests.mixins import SiteMixin
from openedx.core.djangolib.testing.utils import CacheIsolationTestCase, skip_unless_lms
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from xmodule.modulestore.tests.django_utils import ModuleStoreEnum, ModuleStoreTestCase, SharedModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.modulestore.tests.factories import CourseFactory, check_mongo_calls # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.data import CertificatesDisplayBehaviors # lint-amnesty, pylint: disable=wrong-import-order
@@ -284,6 +287,11 @@ class DashboardTest(ModuleStoreTestCase, TestVerificationBase):
self.client = Client()
cache.clear()
patch_context = patch('common.djangoapps.student.helpers.get_course_dates_for_email')
get_course = patch_context.start()
get_course.return_value = []
self.addCleanup(patch_context.stop)
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
def _check_verification_status_on(self, mode, value):
"""
@@ -651,7 +659,7 @@ class EnrollmentEventTestMixin(EventTestMixin):
)
self.mock_segment_tracker.reset_mock()
def assert_enrollment_event_was_emitted(self, user, course_key, course, enrollment):
def assert_enrollment_event_was_emitted(self, user, course_key, course, enrollment, course_run=None):
"""Ensures an enrollment event was emitted since the last event related assertion"""
self.mock_tracker.emit.assert_called_once_with(
'edx.course.enrollment.activated',
@@ -662,7 +670,8 @@ class EnrollmentEventTestMixin(EventTestMixin):
}
)
self.mock_tracker.reset_mock()
properties, traits = self._build_segment_properties_and_traits(user, course_key, course, enrollment, True)
properties, traits = self._build_segment_properties_and_traits(user, course_key, course,
enrollment, True, course_run)
self.mock_segment_tracker.track.assert_called_once_with(
user.id, 'edx.course.enrollment.activated', properties, traits=traits
)
@@ -685,7 +694,8 @@ class EnrollmentEventTestMixin(EventTestMixin):
)
self.mock_segment_tracker.reset_mock()
def _build_segment_properties_and_traits(self, user, course_key, course, enrollment, activated=False):
def _build_segment_properties_and_traits(self, user, course_key, course, enrollment,
activated=False, course_run=None):
""" Builds the segment properties and traits that are sent during enrollment events """
properties = {
'category': 'conversion',
@@ -698,6 +708,10 @@ class EnrollmentEventTestMixin(EventTestMixin):
traits = properties.copy()
traits.update({'course_title': course.display_name, 'email': user.email})
lms_root_url = configuration_helpers.get_value('LMS_ROOT_URL', settings.LMS_ROOT_URL)
learning_base_url = configuration_helpers.get_value('LEARNING_MICROFRONTEND_URL',
settings.LEARNING_MICROFRONTEND_URL)
studio_request = settings.ROOT_URLCONF == 'cms.urls'
if activated:
properties.update({
'email': user.email,
@@ -707,19 +721,78 @@ class EnrollmentEventTestMixin(EventTestMixin):
'course_start': course.start,
'course_pacing': course.pacing,
'redesign_email': False,
'studio_request': studio_request,
})
if not studio_request:
properties.update({
'price': 'Free',
'goals_enabled': False,
'learner_name': user.profile.name,
'course_run_key': str(course_key),
'lms_base_url': lms_root_url,
'learning_base_url': learning_base_url,
'course_title': course_run.get('title'),
'short_description': course_run.get('short_description'),
'marketing_url': course_run.get('marketing_url'),
'pacing_type': course_run.get('pacing_type'),
'partner_image_url': '',
'banner_image_url': course_run.get('image').get('src'),
'instructors': [],
'instructors_count': 'even',
'min_effort': course_run.get('min_effort'),
'max_effort': course_run.get('max_effort'),
'weeks_to_complete': course_run.get('weeks_to_complete'),
'learners_count': '{:,}'.format(course_run.get('enrollment_count')),
'course_date_blocks': [],
})
return properties, traits
class EnrollInCourseTest(EnrollmentEventTestMixin, CacheIsolationTestCase):
@override_waffle_flag(ENROLLMENT_CONFIRMATION_EMAIL_REDESIGN, active=True)
@override_settings(PAID_COURSE_REGISTRATION_CURRENCY=["USD", "$"])
class EnrollInCourseTest(EnrollmentEventTestMixin, CacheIsolationTestCase, ModuleStoreTestCase):
"""Tests enrolling and unenrolling in courses."""
def setUp(self):
"""
Set up tests
"""
super().setUp()
patch_context = patch('common.djangoapps.student.helpers.get_course_dates_for_email')
get_course = patch_context.start()
get_course.return_value = []
self.addCleanup(patch_context.stop)
@staticmethod
def _create_course_run(course_id, course):
"""
Discovery course run
"""
course_run = CourseRunFactory.create(key=course_id)
course_run.update({
'title': course.display_name,
'short_description': course.short_description,
'marketing_url': course.marketing_url,
'pacing_type': 'self_paced' if course.self_paced else 'instructor_paced',
'banner_image_url': course.banner_image_url,
'min_effort': 1,
'enrollment_count': 12345
})
return course_run
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
def test_enrollment(self):
user = UserFactory.create(username="joe", email="joe@joe.com", password="password")
course_id = CourseKey.from_string("edX/Test101/2013")
course_id_partial = CourseKey.from_string("edX/Test101/")
course = CourseOverviewFactory.create(id=course_id)
course_run = self._create_course_run(course_id, course)
patch_course_data = patch('openedx.core.djangoapps.catalog.api.get_course_run_details')
course_data = patch_course_data.start()
course_data.return_value = course_run
# Test basic enrollment
assert not CourseEnrollment.is_enrolled(user, course_id)
@@ -727,7 +800,7 @@ class EnrollInCourseTest(EnrollmentEventTestMixin, CacheIsolationTestCase):
enrollment = CourseEnrollment.enroll(user, course_id)
assert CourseEnrollment.is_enrolled(user, course_id)
assert CourseEnrollment.is_enrolled_by_partial(user, course_id_partial)
self.assert_enrollment_event_was_emitted(user, course_id, course, enrollment)
self.assert_enrollment_event_was_emitted(user, course_id, course, enrollment, course_run)
# Enrolling them again should be harmless
enrollment = CourseEnrollment.enroll(user, course_id)
@@ -761,12 +834,19 @@ class EnrollInCourseTest(EnrollmentEventTestMixin, CacheIsolationTestCase):
enrollment = CourseEnrollment.enroll(user, course_id, "audit")
assert CourseEnrollment.is_enrolled(user, course_id)
assert enrollment.mode == 'audit'
self.addCleanup(patch_course_data.stop)
@override_settings(LEARNING_MICROFRONTEND_URL='https://learningmfe.openedx.org')
def test_enrollment_non_existent_user(self):
# Testing enrollment of newly unsaved user (i.e. no database entry)
user = UserFactory(username="rusty", email="rusty@fake.edx.org")
course_id = CourseLocator("edX", "Test101", "2013")
course = CourseOverviewFactory.create(id=course_id)
course_run = self._create_course_run(course_id, course)
patch_course_data = patch('openedx.core.djangoapps.catalog.api.get_course_run_details')
course_data = patch_course_data.start()
course_data.return_value = course_run
assert not CourseEnrollment.is_enrolled(user, course_id)
@@ -778,17 +858,23 @@ class EnrollInCourseTest(EnrollmentEventTestMixin, CacheIsolationTestCase):
# should still work
enrollment = CourseEnrollment.enroll(user, course_id)
assert CourseEnrollment.is_enrolled(user, course_id)
self.assert_enrollment_event_was_emitted(user, course_id, course, enrollment)
self.assert_enrollment_event_was_emitted(user, course_id, course, enrollment, course_run)
self.addCleanup(patch_course_data.stop)
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
def test_enrollment_by_email(self):
user = UserFactory.create(username="jack", email="jack@fake.edx.org")
course_id = CourseLocator("edX", "Test101", "2013")
course = CourseOverviewFactory.create(id=course_id)
course_run = self._create_course_run(course_id, course)
patch_course_data = patch('openedx.core.djangoapps.catalog.api.get_course_run_details')
course_data = patch_course_data.start()
course_data.return_value = course_run
enrollment = CourseEnrollment.enroll_by_email("jack@fake.edx.org", course_id)
assert CourseEnrollment.is_enrolled(user, course_id)
self.assert_enrollment_event_was_emitted(user, course_id, course, enrollment)
self.assert_enrollment_event_was_emitted(user, course_id, course, enrollment, course_run)
# This won't throw an exception, even though the user is not found
assert CourseEnrollment.enroll_by_email('not_jack@fake.edx.org', course_id) is None
@@ -816,6 +902,7 @@ class EnrollInCourseTest(EnrollmentEventTestMixin, CacheIsolationTestCase):
# Unenroll on non-existent user shouldn't throw an error
CourseEnrollment.unenroll_by_email("not_jack@fake.edx.org", course_id)
self.assert_no_events_were_emitted()
self.addCleanup(patch_course_data.stop)
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
def test_enrollment_multiple_classes(self):
@@ -824,11 +911,24 @@ class EnrollInCourseTest(EnrollmentEventTestMixin, CacheIsolationTestCase):
course_id2 = CourseLocator("MITx", "6.003z", "2012")
course1 = CourseOverviewFactory.create(id=course_id1)
course2 = CourseOverviewFactory.create(id=course_id2)
course_run1 = self._create_course_run(course_id1, course1)
course_run2 = self._create_course_run(course_id2, course2)
patch_course_data1 = patch('openedx.core.djangoapps.catalog.api.get_course_run_details')
course_data1 = patch_course_data1.start()
course_data1.return_value = course_run1
enrollment1 = CourseEnrollment.enroll(user, course_id1)
self.assert_enrollment_event_was_emitted(user, course_id1, course1, enrollment1)
self.assert_enrollment_event_was_emitted(user, course_id1, course1, enrollment1, course_run1)
self.addCleanup(course_data1.stop)
patch_course_data2 = patch('openedx.core.djangoapps.catalog.api.get_course_run_details')
course_data2 = patch_course_data2.start()
course_data2.return_value = course_run2
enrollment2 = CourseEnrollment.enroll(user, course_id2)
self.assert_enrollment_event_was_emitted(user, course_id2, course2, enrollment2)
self.assert_enrollment_event_was_emitted(user, course_id2, course2, enrollment2, course_run2)
self.addCleanup(course_data2.stop)
assert CourseEnrollment.is_enrolled(user, course_id1)
assert CourseEnrollment.is_enrolled(user, course_id2)
@@ -847,6 +947,11 @@ class EnrollInCourseTest(EnrollmentEventTestMixin, CacheIsolationTestCase):
user = UserFactory.create(username="jack", email="jack@fake.edx.org")
course_id = CourseLocator("edX", "Test101", "2013")
course = CourseOverviewFactory.create(id=course_id)
course_run = self._create_course_run(course_id, course)
patch_course_data = patch('openedx.core.djangoapps.catalog.api.get_course_run_details')
course_data = patch_course_data.start()
course_data.return_value = course_run
assert not CourseEnrollment.is_enrolled(user, course_id)
# Creating an enrollment doesn't actually enroll a student
@@ -858,7 +963,7 @@ class EnrollInCourseTest(EnrollmentEventTestMixin, CacheIsolationTestCase):
# Until you explicitly activate it
enrollment.activate()
assert CourseEnrollment.is_enrolled(user, course_id)
self.assert_enrollment_event_was_emitted(user, course_id, course, enrollment)
self.assert_enrollment_event_was_emitted(user, course_id, course, enrollment, course_run)
# Activating something that's already active does nothing
enrollment.activate()
@@ -879,15 +984,22 @@ class EnrollInCourseTest(EnrollmentEventTestMixin, CacheIsolationTestCase):
# for that user/course_id combination
CourseEnrollment.enroll(user, course_id)
assert CourseEnrollment.is_enrolled(user, course_id)
self.assert_enrollment_event_was_emitted(user, course_id, course, enrollment)
self.assert_enrollment_event_was_emitted(user, course_id, course, enrollment, course_run)
self.addCleanup(course_data.stop)
@override_settings(LEARNING_MICROFRONTEND_URL='https://learningmfe.openedx.org')
def test_change_enrollment_modes(self):
user = UserFactory.create(username="justin", email="jh@fake.edx.org")
course_id = CourseLocator("edX", "Test101", "2013")
course = CourseOverviewFactory.create(id=course_id)
course_run = self._create_course_run(course_id, course)
patch_course_data = patch('openedx.core.djangoapps.catalog.api.get_course_run_details')
course_data = patch_course_data.start()
course_data.return_value = course_run
enrollment = CourseEnrollment.enroll(user, course_id, "audit")
self.assert_enrollment_event_was_emitted(user, course_id, course, enrollment)
self.assert_enrollment_event_was_emitted(user, course_id, course, enrollment, course_run)
enrollment = CourseEnrollment.enroll(user, course_id, "honor")
self.assert_enrollment_mode_change_event_was_emitted(user, course_id, "honor", course, enrollment)
@@ -898,6 +1010,7 @@ class EnrollInCourseTest(EnrollmentEventTestMixin, CacheIsolationTestCase):
enrollment = CourseEnrollment.enroll(user, course_id, "audit")
self.assert_enrollment_mode_change_event_was_emitted(user, course_id, "audit", course, enrollment)
self.addCleanup(course_data.stop)
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')

View File

@@ -3,7 +3,6 @@ Toggles for Dashboard page.
"""
from edx_toggles.toggles import WaffleFlag
# Namespace for student waffle flags.
WAFFLE_FLAG_NAMESPACE = 'student'
@@ -22,3 +21,22 @@ ENABLE_AMPLITUDE_RECOMMENDATIONS = WaffleFlag(f'{WAFFLE_FLAG_NAMESPACE}.enable_a
def should_show_amplitude_recommendations():
return ENABLE_AMPLITUDE_RECOMMENDATIONS.is_enabled()
# Waffle flag to enable redesigned course enrollment confirmation email.
# .. toggle_name: student.enable_redesign_enrollment_confirmation_email
# .. toggle_implementation: WaffleFlag
# .. toggle_default: False
# .. toggle_description: Enable redesign email template only for staff users for testing.
# .. toggle_use_cases: temporary
# .. toggle_creation_date: 2022-08-05
# .. toggle_target_removal_date: None
# .. toggle_warning: None
# .. toggle_tickets: VAN-1064
ENROLLMENT_CONFIRMATION_EMAIL_REDESIGN = WaffleFlag(
f'{WAFFLE_FLAG_NAMESPACE}.enable_redesign_enrollment_confirmation_email', __name__
)
def should_send_redesign_email():
return ENROLLMENT_CONFIRMATION_EMAIL_REDESIGN.is_enabled()

View File

@@ -387,7 +387,8 @@ def change_enrollment(request, check_access=True):
try:
enroll_mode = CourseMode.auto_enroll_mode(course_id, available_modes)
if enroll_mode:
CourseEnrollment.enroll(user, course_id, check_access=check_access, mode=enroll_mode)
CourseEnrollment.enroll(user, course_id, check_access=check_access,
mode=enroll_mode, request=request)
except Exception: # pylint: disable=broad-except
return HttpResponseBadRequest(_("Could not enroll"))

View File

@@ -76,6 +76,11 @@ class FieldOverridePerformanceTestCase(FieldOverrideTestMixin, ProceduralCourseT
self.course = None
self.ccx = None
patch_context = mock.patch('common.djangoapps.student.helpers.get_course_dates_for_email')
get_course = patch_context.start()
get_course.return_value = []
self.addCleanup(patch_context.stop)
def setup_course(self, size, enable_ccx, view_as_ccx):
"""
Build a gradable course where each node has `size` children.

View File

@@ -36,8 +36,7 @@ class DateSummarySerializer(serializers.Serializer):
def get_link(self, block):
if block.link:
request = self.context.get('request')
return request.build_absolute_uri(block.link)
return block.link
return ''
def get_first_component_block_id(self, block):

View File

@@ -7,6 +7,7 @@ import logging
from collections import defaultdict, namedtuple
from datetime import datetime
import six
import pytz
from crum import get_current_request
from dateutil.parser import parse as parse_date
@@ -494,6 +495,28 @@ def date_block_key_fn(block):
return block.date or datetime.max.replace(tzinfo=pytz.UTC)
def _get_absolute_url(request, url_path):
"""Construct an absolute URL back to the site.
Arguments:
request (request): request object.
url_path (string): The path of the URL.
Returns:
URL
"""
if not url_path:
return ''
if request:
return request.build_absolute_uri(url_path)
site_name = configuration_helpers.get_value('SITE_NAME', settings.SITE_NAME)
parts = ("https" if settings.HTTPS == "on" else "http", site_name, url_path, '', '', '')
return six.moves.urllib.parse.urlunparse(parts)
def get_course_assignment_date_blocks(course, user, request, num_return=None,
include_past_dates=False, include_access=False):
"""
@@ -510,7 +533,7 @@ def get_course_assignment_date_blocks(course, user, request, num_return=None,
date_block.complete = assignment.complete
date_block.assignment_type = assignment.assignment_type
date_block.past_due = assignment.past_due
date_block.link = request.build_absolute_uri(assignment.url) if assignment.url else ''
date_block.link = _get_absolute_url(request, assignment.url)
date_block.set_title(assignment.title, link=assignment.url)
date_block._extra_info = assignment.extra_info # pylint: disable=protected-access
date_blocks.append(date_block)

View File

@@ -338,8 +338,10 @@ class IndexQueryTestCase(ModuleStoreTestCase):
"""
NUM_PROBLEMS = 20
def test_index_query_counts(self):
@patch('common.djangoapps.student.helpers.get_course_dates_for_email')
def test_index_query_counts(self, mock_course_dates_for_email):
# TODO: decrease query count as part of REVO-28
mock_course_dates_for_email.return_value = []
ContentTypeGatingConfig.objects.create(enabled=True, enabled_as_of=datetime(2018, 1, 1))
with self.store.default_store(ModuleStoreEnum.Type.split):
course = CourseFactory.create()
@@ -350,6 +352,20 @@ class IndexQueryTestCase(ModuleStoreTestCase):
for _ in range(self.NUM_PROBLEMS):
ItemFactory.create(category='problem', parent_location=vertical.location)
course_run = CourseRunFactory.create(key=course.id)
course_run['title'] = course.display_name
course_run['short_description'] = None
course_run['marketing_url'] = 'www.edx.org'
course_run['pacing_type'] = 'self_paced'
course_run['banner_image_url'] = ''
course_run['min_effort'] = 1
course_run['enrollment_count'] = 12345
patch_course_data = patch('openedx.core.djangoapps.catalog.api.get_course_run_details')
course_data = patch_course_data.start()
course_data.return_value = course_run
self.addCleanup(patch_course_data.stop)
self.client.login(username=self.user.username, password=self.user_password)
CourseEnrollment.enroll(self.user, course.id)