Fix error when saving CourseEnrollment in admin

This commit is contained in:
Daniel Clemente Laboreo
2018-10-15 16:57:01 +03:00
parent 081267cc73
commit e0b0c375be
3 changed files with 62 additions and 4 deletions

View File

@@ -56,7 +56,7 @@ class CourseModeForm(forms.ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
# If args is a QueryDict, then the ModelForm addition request came in as a POST with a course ID string. # If args is a QueryDict, then the ModelForm addition request came in as a POST with a course ID string.
# Change the course ID string to a CourseLocator object by copying the QueryDict to make it mutable. # Change the course ID string to a CourseLocator object by copying the QueryDict to make it mutable.
if len(args) > 0 and 'course' in args[0] and isinstance(args[0], QueryDict): if args and 'course' in args[0] and isinstance(args[0], QueryDict):
args_copy = args[0].copy() args_copy = args[0].copy()
args_copy['course'] = CourseKey.from_string(args_copy['course']) args_copy['course'] = CourseKey.from_string(args_copy['course'])
args = [args_copy] args = [args_copy]

View File

@@ -7,6 +7,7 @@ from django.contrib.auth import get_user_model
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.contrib.auth.forms import ReadOnlyPasswordHashField, UserChangeForm as BaseUserChangeForm from django.contrib.auth.forms import ReadOnlyPasswordHashField, UserChangeForm as BaseUserChangeForm
from django.db import models from django.db import models
from django.http.request import QueryDict
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from opaque_keys import InvalidKeyError from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.keys import CourseKey
@@ -149,13 +150,26 @@ class LinkedInAddToProfileConfigurationAdmin(admin.ModelAdmin):
class CourseEnrollmentForm(forms.ModelForm): class CourseEnrollmentForm(forms.ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
# If args is a QueryDict, then the ModelForm addition request came in as a POST with a course ID string.
# Change the course ID string to a CourseLocator object by copying the QueryDict to make it mutable.
if args and 'course' in args[0] and isinstance(args[0], QueryDict):
args_copy = args[0].copy()
try:
args_copy['course'] = CourseKey.from_string(args_copy['course'])
except InvalidKeyError:
raise forms.ValidationError("Cannot make a valid CourseKey from id {}!".format(args_copy['course']))
args = [args_copy]
super(CourseEnrollmentForm, self).__init__(*args, **kwargs) super(CourseEnrollmentForm, self).__init__(*args, **kwargs)
if self.data.get('course'): if self.data.get('course'):
try: try:
self.data['course'] = CourseKey.from_string(self.data['course']) self.data['course'] = CourseKey.from_string(self.data['course'])
except InvalidKeyError: except AttributeError:
raise forms.ValidationError("Cannot make a valid CourseKey from id {}!".format(self.data['course'])) # Change the course ID string to a CourseLocator.
# On a POST request, self.data is a QueryDict and is immutable - so this code will fail.
# However, the args copy above before the super() call handles this case.
pass
def clean_course_id(self): def clean_course_id(self):
course_id = self.cleaned_data['course'] course_id = self.cleaned_data['course']

View File

@@ -4,6 +4,7 @@ Tests student admin.py
import ddt import ddt
from django.contrib.admin.sites import AdminSite from django.contrib.admin.sites import AdminSite
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.forms import ValidationError
from django.urls import reverse from django.urls import reverse
from django.test import TestCase from django.test import TestCase
from mock import Mock from mock import Mock
@@ -209,7 +210,7 @@ class CourseEnrollmentAdminTest(SharedModuleStoreTestCase):
super(CourseEnrollmentAdminTest, self).setUp() super(CourseEnrollmentAdminTest, self).setUp()
self.user = UserFactory.create(is_staff=True, is_superuser=True) self.user = UserFactory.create(is_staff=True, is_superuser=True)
self.course = CourseFactory() self.course = CourseFactory()
CourseEnrollmentFactory( self.course_enrollment = CourseEnrollmentFactory(
user=self.user, user=self.user,
course_id=self.course.id, # pylint: disable=no-member course_id=self.course.id, # pylint: disable=no-member
) )
@@ -254,3 +255,46 @@ class CourseEnrollmentAdminTest(SharedModuleStoreTestCase):
# Locate the <td> column containing the username # Locate the <td> column containing the username
user_field = next(col for col in response.context['results'][idx] if "field-user" in col) user_field = next(col for col in response.context['results'][idx] if "field-user" in col)
self.assertIn(username, user_field) self.assertIn(username, user_field)
def test_save_toggle_active(self):
"""
Edit a CourseEnrollment to toggle its is_active checkbox, save it and verify that it was toggled.
When the form is saved, Django uses a QueryDict object which is immutable and needs special treatment.
This test implicitly verifies that the POST parameters are handled correctly.
"""
# is_active will change from True to False
self.assertTrue(self.course_enrollment.is_active)
data = {
'user': unicode(self.course_enrollment.user.id),
'course': unicode(self.course_enrollment.course.id),
'is_active': 'false',
'mode': self.course_enrollment.mode,
}
with COURSE_ENROLLMENT_ADMIN_SWITCH.override(active=True):
response = self.client.post(
reverse('admin:student_courseenrollment_change', args=(self.course_enrollment.id, )),
data=data,
)
self.assertEqual(response.status_code, 302)
self.course_enrollment.refresh_from_db()
self.assertFalse(self.course_enrollment.is_active)
def test_save_invalid_course_id(self):
"""
Send an invalid course ID instead of "org.0/course_0/Run_0" when saving, and verify that it fails.
"""
data = {
'user': unicode(self.course_enrollment.user.id),
'course': 'invalid-course-id',
'is_active': 'true',
'mode': self.course_enrollment.mode,
}
with COURSE_ENROLLMENT_ADMIN_SWITCH.override(active=True):
with self.assertRaises(ValidationError):
self.client.post(
reverse('admin:student_courseenrollment_change', args=(self.course_enrollment.id, )),
data=data,
)