Commit 0917535b by Michael Terry Committed by Michael Terry

Update seat when changing entitlement

We weren't propagating entitlement type/price changes to existing
seat products.

LEARNER-4347
parent f68edf9f
......@@ -3060,7 +3060,7 @@ class CourseEditViewTests(SiteMixin, TestCase):
self.course.save()
post_data = self._post_data(self.organization_extension)
post_data['team_admin'] = self.course_team_role.user.id
post_data['mode'] = Seat.VERIFIED
post_data['mode'] = CourseEntitlement.VERIFIED
post_data['price'] = 150
response = self.client.post(self.edit_page_url, data=post_data)
......@@ -3079,15 +3079,15 @@ class CourseEditViewTests(SiteMixin, TestCase):
self.assertEqual(response.status_code, 400)
# Assert that not setting a price for a verified course fails
post_data['mode'] = Seat.VERIFIED
post_data['mode'] = CourseEntitlement.VERIFIED
post_data['price'] = ''
response = self.client.post(self.edit_page_url, data=post_data)
self.assertEqual(response.status_code, 400)
# Assert that changing the price for a course with a Verified Entitlement is allowed
new_course = factories.CourseFactory()
factories.CourseEntitlementFactory(course=new_course, mode=Seat.VERIFIED)
post_data['mode'] = Seat.VERIFIED
factories.CourseEntitlementFactory(course=new_course, mode=CourseEntitlement.VERIFIED)
post_data['mode'] = CourseEntitlement.VERIFIED
post_data['price'] = 1
response = self.client.post(self.edit_page_url, data=post_data)
self.assertRedirects(
......@@ -3097,6 +3097,65 @@ class CourseEditViewTests(SiteMixin, TestCase):
target_status_code=200
)
def test_entitlement_changes(self):
"""
Verify that an entitlement course's type or price changes take effect correctly
"""
toggle_switch(PUBLISHER_CREATE_AUDIT_SEATS_FOR_VERIFIED_COURSE_RUNS, True)
self.user.groups.add(Group.objects.get(name=INTERNAL_USER_GROUP_NAME))
factories.CourseUserRoleFactory.create(course=self.course, role=PublisherUserRole.Publisher)
factories.CourseUserRoleFactory.create(course=self.course, role=PublisherUserRole.ProjectCoordinator)
factories.CourseUserRoleFactory.create(course=self.course, role=PublisherUserRole.MarketingReviewer)
# Initial course values via form
course_data = self._post_data(self.organization_extension)
course_data['team_admin'] = self.user.id
course_data['mode'] = CourseEntitlement.VERIFIED
course_data['price'] = 150
response = self.client.post(self.edit_page_url, data=course_data)
self.assertEqual(response.status_code, 302)
# New course run via form
new_run_url = reverse('publisher:publisher_course_runs_new', kwargs={'parent_course_id': self.course.id})
current_datetime = datetime.now(timezone('US/Central'))
run_data = {
'pacing_type': 'self_paced',
'start': (current_datetime + timedelta(days=1)).strftime('%Y-%m-%d %H:%M:%S'),
'end': (current_datetime + timedelta(days=3)).strftime('%Y-%m-%d %H:%M:%S'),
}
response = self.client.post(new_run_url, run_data)
self.assertEqual(response.status_code, 302)
# Sanity check that we have the expected entitlement & seats
self.assertEqual(CourseEntitlement.objects.count(), 1)
entitlement = CourseEntitlement.objects.get(mode=CourseEntitlement.VERIFIED, price=150)
self.assertEqual(Seat.objects.count(), 2)
paid_seat = Seat.objects.get(type=Seat.VERIFIED, price=150)
audit_seat = Seat.objects.get(type=Seat.AUDIT, price=0)
def refresh_from_db():
entitlement.refresh_from_db()
paid_seat.refresh_from_db()
audit_seat.refresh_from_db()
# Test price change
course_data['price'] = 99
response = self.client.post(self.edit_page_url, data=course_data)
self.assertEqual(response.status_code, 302)
refresh_from_db()
self.assertEqual(entitlement.price, 99)
self.assertEqual(paid_seat.price, 99)
self.assertEqual(audit_seat.price, 0)
# Test mode change
course_data['mode'] = CourseEntitlement.PROFESSIONAL
response = self.client.post(self.edit_page_url, data=course_data)
self.assertEqual(response.status_code, 302)
refresh_from_db()
self.assertEqual(entitlement.mode, CourseEntitlement.PROFESSIONAL)
self.assertEqual(paid_seat.type, Seat.PROFESSIONAL)
self.assertEqual(audit_seat.type, Seat.AUDIT)
def test_course_with_published_course_run(self):
"""
Verify that editing course with published course run does not changed state
......
......@@ -419,10 +419,13 @@ class CourseEditView(mixins.PublisherPermissionMixin, UpdateView):
return render(request, self.template_name, context)
def _get_active_course_runs(self, course):
return course.course_runs.filter(end__gt=datetime.now())
def _get_misconfigured_course_runs(self, course, price, mode):
misconfigured_seat_type_runs = set()
misconfigured_price_runs = set()
for course_run in course.publisher_course_runs.filter(end__gt=datetime.now()):
for course_run in self._get_active_course_runs(course):
seats = course_run.seats.all()
type_is_valid = True
price_is_valid = True
......@@ -459,6 +462,18 @@ class CourseEditView(mixins.PublisherPermissionMixin, UpdateView):
entitlement.save()
else:
entitlement_form.save(course=course)
return entitlement
@transaction.atomic
def _update_seats_from_entitlement(self, course, entitlement):
for run in self._get_active_course_runs(course):
for seat in run.seats.all():
# First make sure this seat is on the "entitlement side of things" (i.e. not an audit seat or similar)
if seat.type in CourseEntitlement.MODE_TO_SEAT_TYPE_MAPPING.values():
seat.type = CourseEntitlement.MODE_TO_SEAT_TYPE_MAPPING[entitlement.mode]
seat.price = entitlement.price
seat.currency = entitlement.currency
seat.save()
def _render_post_error(self, request, ctx_overrides=None, status=400):
context = self.get_context_data()
......@@ -466,11 +481,17 @@ class CourseEditView(mixins.PublisherPermissionMixin, UpdateView):
context.update(ctx_overrides)
return render(request, self.template_name, context, status=status)
def _update_course(self, course_form, user, course_version):
@transaction.atomic
def _update_course(self, course_form, entitlement_form, user, course_version):
course = course_form.save(commit=False)
course.changed_by = user
course.version = course_version
course.save()
if course.uses_entitlements:
entitlement = self._create_or_update_course_entitlement(course, entitlement_form)
self._update_seats_from_entitlement(course, entitlement)
return course
def _handle_entitlement_update(self, user, request, course_form):
......@@ -491,56 +512,47 @@ class CourseEditView(mixins.PublisherPermissionMixin, UpdateView):
# If the course is originally a SEAT_VERSION and it's now
# using entitlements check that there are no misconfigured runs
if self.object.version == Course.SEAT_VERSION:
if entitlement_mode:
type_misconfigurations, seat_misconfigurations = self._get_misconfigured_course_runs(
self.object, entitlement_price, entitlement_mode
)
if type_misconfigurations:
# pylint: disable=no-member
error_message = _(
'The entered price does not match the price for the following course run(s): '
'{course_runs}. The price that you enter must match the price of all active '
'and future course runs.'
).format(course_runs=', '.join(
str(course_run_start) for course_run_start in type_misconfigurations
))
messages.error(request, error_message)
if seat_misconfigurations:
# pylint: disable=no-member
error_message = _(
'The entered seat type does not match the seat type for the following course '
'run(s): {course_runs}. The seat type that you enter must match the seat '
'type of all active and future course runs.'
).format(course_runs=', '.join(
str(course_run_start) for course_run_start in seat_misconfigurations
))
messages.error(request, error_message)
if seat_misconfigurations or type_misconfigurations:
return self._render_post_error(request, ctx_overrides={
'course_form': course_form,
'entitlement_form': entitlement_form
})
else:
with transaction.atomic():
course = self._update_course(course_form, user, Course.ENTITLEMENT_VERSION)
self._create_or_update_course_entitlement(course, entitlement_form)
else:
self._update_course(course_form, user, Course.SEAT_VERSION)
elif self.object.version == Course.ENTITLEMENT_VERSION:
if entitlement_mode:
with transaction.atomic():
course = self._update_course(course_form, user, Course.ENTITLEMENT_VERSION)
self._create_or_update_course_entitlement(course, entitlement_form)
else:
messages.error(request, _(
"Courses with a previously set Type and Price cannot be "
"changed to an Audit/Credit Course"
if not self.object.uses_entitlements and entitlement_mode:
type_misconfigurations, seat_misconfigurations = self._get_misconfigured_course_runs(
self.object, entitlement_price, entitlement_mode
)
if type_misconfigurations:
# pylint: disable=no-member
error_message = _(
'The entered price does not match the price for the following course run(s): '
'{course_runs}. The price that you enter must match the price of all active '
'and future course runs.'
).format(course_runs=', '.join(
str(course_run_start) for course_run_start in type_misconfigurations
))
messages.error(request, error_message)
if seat_misconfigurations:
# pylint: disable=no-member
error_message = _(
'The entered seat type does not match the seat type for the following course '
'run(s): {course_runs}. The seat type that you enter must match the seat '
'type of all active and future course runs.'
).format(course_runs=', '.join(
str(course_run_start) for course_run_start in seat_misconfigurations
))
messages.error(request, error_message)
if seat_misconfigurations or type_misconfigurations:
return self._render_post_error(request, ctx_overrides={
'course_form': course_form,
'entitlement_form': entitlement_form
})
elif self.object.uses_entitlements and not entitlement_mode:
messages.error(request, _(
"Courses with a previously set Type and Price cannot be "
"changed to an Audit/Credit Course"
))
return self._render_post_error(request, ctx_overrides={
'course_form': course_form,
'entitlement_form': entitlement_form
})
version = Course.ENTITLEMENT_VERSION if entitlement_mode else Course.SEAT_VERSION
self._update_course(course_form, entitlement_form, user, version)
def post(self, request, *args, **kwargs):
user = self.request.user
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment