Commit 34ceeb56 by John Eskew

Remove deprecated SubfieldBase.

Also fix:
  - double-serialization of keys
  - using course keys instead of ids
  - using ids instead of course keys
  - empty course keys returning empty string instead of None
Other consequences:
  - If a model object is created using a string for an OpaqueKeyField,
    the field will stay the string, even after a save(). When the object
    is pulled back from the DB, the field will then be an Opaque Key
    object.
  - If a model is created without providing an opaque key for the
    OpaqueKeyField, the model instance returns an empty string
    for the key - it used to return None.
parent ba7acff2
...@@ -1529,7 +1529,7 @@ class CourseEnrollment(models.Model): ...@@ -1529,7 +1529,7 @@ class CourseEnrollment(models.Model):
if not status_hash: if not status_hash:
enrollments = cls.enrollments_for_user(user).values_list('course_id', 'mode') enrollments = cls.enrollments_for_user(user).values_list('course_id', 'mode')
enrollments = [(e[0].lower(), e[1].lower()) for e in enrollments] enrollments = [(unicode(e[0]).lower(), e[1].lower()) for e in enrollments]
enrollments = sorted(enrollments, key=lambda e: e[0]) enrollments = sorted(enrollments, key=lambda e: e[0])
hash_elements = [user.username] hash_elements = [user.username]
hash_elements += ['{course_id}={mode}'.format(course_id=e[0], mode=e[1]) for e in enrollments] hash_elements += ['{course_id}={mode}'.format(course_id=e[0], mode=e[1]) for e in enrollments]
......
...@@ -44,8 +44,6 @@ class CompressedTextField(models.TextField): ...@@ -44,8 +44,6 @@ class CompressedTextField(models.TextField):
""" TextField that transparently compresses data when saving to the database, and decompresses the data """ TextField that transparently compresses data when saving to the database, and decompresses the data
when retrieving it from the database. """ when retrieving it from the database. """
__metaclass__ = models.SubfieldBase
def get_prep_value(self, value): def get_prep_value(self, value):
""" Compress the text data. """ """ Compress the text data. """
if value is not None: if value is not None:
...@@ -59,5 +57,7 @@ class CompressedTextField(models.TextField): ...@@ -59,5 +57,7 @@ class CompressedTextField(models.TextField):
""" Decompresses the value from the database. """ """ Decompresses the value from the database. """
if isinstance(value, unicode): if isinstance(value, unicode):
value = decompress_string(value) value = decompress_string(value)
return value return value
def from_db_value(self, value, expression, connection, context):
return self.to_python(value)
...@@ -82,7 +82,7 @@ class BadgeClass(models.Model): ...@@ -82,7 +82,7 @@ class BadgeClass(models.Model):
if course_id and not modulestore().get_course(course_id).issue_badges: if course_id and not modulestore().get_course(course_id).issue_badges:
raise CourseBadgesDisabledError("This course does not have badges enabled.") raise CourseBadgesDisabledError("This course does not have badges enabled.")
if not course_id: if not course_id:
course_id = CourseKeyField.Empty course_id = ''
try: try:
return cls.objects.get(slug=slug, issuing_component=issuing_component, course_id=course_id) return cls.objects.get(slug=slug, issuing_component=issuing_component, course_id=course_id)
except cls.DoesNotExist: except cls.DoesNotExist:
...@@ -97,6 +97,7 @@ class BadgeClass(models.Model): ...@@ -97,6 +97,7 @@ class BadgeClass(models.Model):
description=description, description=description,
criteria=criteria, criteria=criteria,
) )
badge_class.image.save(image_file_handle.name, image_file_handle) badge_class.image.save(image_file_handle.name, image_file_handle)
badge_class.full_clean() badge_class.full_clean()
badge_class.save() badge_class.save()
......
...@@ -45,7 +45,7 @@ def get_problem_grade_distribution(course_id): ...@@ -45,7 +45,7 @@ def get_problem_grade_distribution(course_id):
# Loop through resultset building data for each problem # Loop through resultset building data for each problem
for row in db_query: for row in db_query:
curr_problem = UsageKey.from_string(row['module_state_key']).map_into_course(course_id) curr_problem = row['module_state_key'].map_into_course(course_id)
# Build set of grade distributions for each problem that has student responses # Build set of grade distributions for each problem that has student responses
if curr_problem in prob_grade_distrib: if curr_problem in prob_grade_distrib:
...@@ -85,7 +85,7 @@ def get_sequential_open_distrib(course_id): ...@@ -85,7 +85,7 @@ def get_sequential_open_distrib(course_id):
# Build set of "opened" data for each subsection that has "opened" data # Build set of "opened" data for each subsection that has "opened" data
sequential_open_distrib = {} sequential_open_distrib = {}
for row in db_query: for row in db_query:
row_loc = UsageKey.from_string(row['module_state_key']).map_into_course(course_id) row_loc = row['module_state_key'].map_into_course(course_id)
sequential_open_distrib[row_loc] = row['count_sequential'] sequential_open_distrib[row_loc] = row['count_sequential']
return sequential_open_distrib return sequential_open_distrib
...@@ -122,7 +122,7 @@ def get_problem_set_grade_distrib(course_id, problem_set): ...@@ -122,7 +122,7 @@ def get_problem_set_grade_distrib(course_id, problem_set):
# Loop through resultset building data for each problem # Loop through resultset building data for each problem
for row in db_query: for row in db_query:
row_loc = UsageKey.from_string(row['module_state_key']).map_into_course(course_id) row_loc = row['module_state_key'].map_into_course(course_id)
if row_loc not in prob_grade_distrib: if row_loc not in prob_grade_distrib:
prob_grade_distrib[row_loc] = { prob_grade_distrib[row_loc] = {
'max_grade': 0, 'max_grade': 0,
......
...@@ -960,7 +960,7 @@ class ScoresClient(object): ...@@ -960,7 +960,7 @@ class ScoresClient(object):
# attached to them (since old mongo identifiers don't include runs). # attached to them (since old mongo identifiers don't include runs).
# So we have to add that info back in before we put it into our lookup. # So we have to add that info back in before we put it into our lookup.
self._locations_to_scores.update({ self._locations_to_scores.update({
UsageKey.from_string(location).map_into_course(self.course_key): self.Score(correct, total, created) location.map_into_course(self.course_key): self.Score(correct, total, created)
for location, correct, total, created for location, correct, total, created
in scores_qset.values_list('module_state_key', 'grade', 'max_grade', 'created') in scores_qset.values_list('module_state_key', 'grade', 'max_grade', 'created')
}) })
......
...@@ -308,7 +308,7 @@ class TestECommerceDashboardViews(SiteMixin, SharedModuleStoreTestCase): ...@@ -308,7 +308,7 @@ class TestECommerceDashboardViews(SiteMixin, SharedModuleStoreTestCase):
Test Update Coupon Info Scenarios. Handle all the HttpResponses return by update_coupon view Test Update Coupon Info Scenarios. Handle all the HttpResponses return by update_coupon view
""" """
coupon = Coupon( coupon = Coupon(
code='AS452', description='asdsadsa', course_id=self.course.id.to_deprecated_string(), code='AS452', description='asdsadsa', course_id=self.course.id,
percentage_discount=10, created_by=self.instructor percentage_discount=10, created_by=self.instructor
) )
coupon.save() coupon.save()
......
...@@ -198,6 +198,7 @@ def issued_certificates(course_key, features): ...@@ -198,6 +198,7 @@ def issued_certificates(course_key, features):
# Report run date # Report run date
for data in generated_certificates: for data in generated_certificates:
data['course_id'] = unicode(data['course_id'])
data['report_run_date'] = report_run_date data['report_run_date'] = report_run_date
return generated_certificates return generated_certificates
...@@ -488,7 +489,6 @@ def course_registration_features(features, registration_codes, csv_type): ...@@ -488,7 +489,6 @@ def course_registration_features(features, registration_codes, csv_type):
except ObjectDoesNotExist: except ObjectDoesNotExist:
pass pass
course_registration_dict['course_id'] = course_registration_dict['course_id'].to_deprecated_string()
return course_registration_dict return course_registration_dict
return [extract_course_registration(code, features, csv_type) for code in registration_codes] return [extract_course_registration(code, features, csv_type) for code in registration_codes]
......
...@@ -574,7 +574,7 @@ class TestCourseRegistrationCodeAnalyticsBasic(ModuleStoreTestCase): ...@@ -574,7 +574,7 @@ class TestCourseRegistrationCodeAnalyticsBasic(ModuleStoreTestCase):
self.assertIn(course_registration['code'], [registration_code.code for registration_code in registration_codes]) self.assertIn(course_registration['code'], [registration_code.code for registration_code in registration_codes])
self.assertIn( self.assertIn(
course_registration['course_id'], course_registration['course_id'],
[registration_code.course_id.to_deprecated_string() for registration_code in registration_codes] [registration_code.course_id for registration_code in registration_codes]
) )
self.assertIn( self.assertIn(
course_registration['company_name'], course_registration['company_name'],
......
...@@ -2159,7 +2159,7 @@ class Donation(OrderItem): ...@@ -2159,7 +2159,7 @@ class Donation(OrderItem):
"line_desc": description "line_desc": description
} }
if course_id is not None: if course_id is not None and course_id != '':
params["course_id"] = course_id params["course_id"] = course_id
params["donation_type"] = "course" params["donation_type"] = "course"
else: else:
......
...@@ -1144,7 +1144,7 @@ class DonationTest(ModuleStoreTestCase): ...@@ -1144,7 +1144,7 @@ class DonationTest(ModuleStoreTestCase):
with self.assertRaises(CourseDoesNotExistException): with self.assertRaises(CourseDoesNotExistException):
Donation.add_to_order(self.cart, self.COST, course_id=fake_course_id) Donation.add_to_order(self.cart, self.COST, course_id=fake_course_id)
def _assert_donation(self, donation, donation_type=None, course_id=None, unit_cost=None, line_desc=None): def _assert_donation(self, donation, donation_type=None, course_id='', unit_cost=None, line_desc=None):
"""Verify the donation fields and that the donation can be purchased. """ """Verify the donation fields and that the donation can be purchased. """
self.assertEqual(donation.order, self.cart) self.assertEqual(donation.order, self.cart)
self.assertEqual(donation.user, self.user) self.assertEqual(donation.user, self.user)
......
...@@ -1055,7 +1055,7 @@ class MembershipListView(ExpandableFieldViewMixin, GenericAPIView): ...@@ -1055,7 +1055,7 @@ class MembershipListView(ExpandableFieldViewMixin, GenericAPIView):
CourseAccessRole.objects.filter(user=request.user, role='staff').values_list('course_id', flat=True) CourseAccessRole.objects.filter(user=request.user, role='staff').values_list('course_id', flat=True)
) )
accessible_course_ids = [item for sublist in (enrolled_courses, staff_courses) for item in sublist] accessible_course_ids = [item for sublist in (enrolled_courses, staff_courses) for item in sublist]
if requested_course_id is not None and requested_course_id not in accessible_course_ids: if requested_course_key is not None and requested_course_key not in accessible_course_ids:
return Response(status=status.HTTP_400_BAD_REQUEST) return Response(status=status.HTTP_400_BAD_REQUEST)
if not specified_username_or_team: if not specified_username_or_team:
...@@ -1068,7 +1068,7 @@ class MembershipListView(ExpandableFieldViewMixin, GenericAPIView): ...@@ -1068,7 +1068,7 @@ class MembershipListView(ExpandableFieldViewMixin, GenericAPIView):
if requested_course_key is not None: if requested_course_key is not None:
course_keys = [requested_course_key] course_keys = [requested_course_key]
elif accessible_course_ids is not None: elif accessible_course_ids is not None:
course_keys = [CourseKey.from_string(course_string) for course_string in accessible_course_ids] course_keys = accessible_course_ids
queryset = CourseTeamMembership.get_memberships(username, course_keys, team_id) queryset = CourseTeamMembership.get_memberships(username, course_keys, team_id)
page = self.paginate_queryset(queryset) page = self.paginate_queryset(queryset)
......
...@@ -552,8 +552,7 @@ class CourseOverview(TimeStampedModel): ...@@ -552,8 +552,7 @@ class CourseOverview(TimeStampedModel):
Returns all course keys from course overviews. Returns all course keys from course overviews.
""" """
return [ return [
CourseKey.from_string(course_overview['id']) course_overview['id'] for course_overview in CourseOverview.objects.values('id')
for course_overview in CourseOverview.objects.values('id')
] ]
def is_discussion_tab_enabled(self): def is_discussion_tab_enabled(self):
......
...@@ -80,7 +80,9 @@ class CohortMembership(models.Model): ...@@ -80,7 +80,9 @@ class CohortMembership(models.Model):
unique_together = (('user', 'course_id'), ) unique_together = (('user', 'course_id'), )
def clean_fields(self, *args, **kwargs): def clean_fields(self, *args, **kwargs):
if self.course_id is None: if self.course_id is None and self.course_user_group.course_id is None:
raise ValidationError("CohortMembership course_id and course_user_group course_id are both None")
if self.course_id in ('', None):
self.course_id = self.course_user_group.course_id self.course_id = self.course_user_group.course_id
super(CohortMembership, self).clean_fields(*args, **kwargs) super(CohortMembership, self).clean_fields(*args, **kwargs)
...@@ -91,9 +93,8 @@ class CohortMembership(models.Model): ...@@ -91,9 +93,8 @@ class CohortMembership(models.Model):
raise ValidationError("Non-matching course_ids provided") raise ValidationError("Non-matching course_ids provided")
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
self.full_clean(validate_unique=False)
log.info("Saving CohortMembership for user '%s' in '%s'", self.user.id, self.course_id) log.info("Saving CohortMembership for user '%s' in '%s'", self.user.id, self.course_id)
self.full_clean(validate_unique=False)
# Avoid infinite recursion if creating from get_or_create() call below. # Avoid infinite recursion if creating from get_or_create() call below.
# This block also allows middleware to use CohortMembership.get_or_create without worrying about outer_atomic # This block also allows middleware to use CohortMembership.get_or_create without worrying about outer_atomic
......
...@@ -6,6 +6,7 @@ import uuid # pylint:disable=unused-import ...@@ -6,6 +6,7 @@ import uuid # pylint:disable=unused-import
from django.contrib.auth.models import User from django.contrib.auth.models import User
import factory import factory
from factory.fuzzy import FuzzyText from factory.fuzzy import FuzzyText
from opaque_keys.edx.keys import CourseKey
import pytz import pytz
from openedx.core.djangoapps.credit.models import CreditProvider, CreditEligibility, CreditCourse, CreditRequest from openedx.core.djangoapps.credit.models import CreditProvider, CreditEligibility, CreditCourse, CreditRequest
...@@ -50,7 +51,9 @@ class CreditRequestFactory(factory.DjangoModelFactory): ...@@ -50,7 +51,9 @@ class CreditRequestFactory(factory.DjangoModelFactory):
Sets up parameters field. Sets up parameters field.
""" """
if not obj.parameters: if not obj.parameters:
course_key = obj.course.course_key # The factory is creating these keys as strings, in Django 1.9+ they don't get automatically
# marshalled to CourseKey objects, so this ensures they are created as expected.
course_key = CourseKey.from_string(obj.course.course_key)
user = User.objects.get(username=obj.username) user = User.objects.get(username=obj.username)
user_profile = user.profile user_profile = user.profile
......
...@@ -346,7 +346,7 @@ class CreditProviderRequestCreateViewTests(ApiTestCaseMixin, UserMixin, TestCase ...@@ -346,7 +346,7 @@ class CreditProviderRequestCreateViewTests(ApiTestCaseMixin, UserMixin, TestCase
""" Verify the endpoint can create a new credit request. """ """ Verify the endpoint can create a new credit request. """
username = self.user.username username = self.user.username
course = self.eligibility.course course = self.eligibility.course
course_key = course.course_key course_key = CourseKey.from_string(course.course_key)
final_grade = 0.95 final_grade = 0.95
# Enable provider integration # Enable provider integration
...@@ -521,6 +521,7 @@ class CreditProviderCallbackViewTests(UserMixin, TestCase): ...@@ -521,6 +521,7 @@ class CreditProviderCallbackViewTests(UserMixin, TestCase):
def _create_credit_request_and_get_uuid(self, username=None, course_key=None): def _create_credit_request_and_get_uuid(self, username=None, course_key=None):
""" Initiate a request for credit and return the request UUID. """ """ Initiate a request for credit and return the request UUID. """
username = username or self.user.username username = username or self.user.username
course = CreditCourse.objects.get(course_key=course_key) if course_key else self.eligibility.course course = CreditCourse.objects.get(course_key=course_key) if course_key else self.eligibility.course
credit_request = CreditRequestFactory(username=username, course=course, provider=self.provider) credit_request = CreditRequestFactory(username=username, course=course, provider=self.provider)
return credit_request.uuid return credit_request.uuid
......
...@@ -81,8 +81,6 @@ class OpaqueKeyField(models.CharField): ...@@ -81,8 +81,6 @@ class OpaqueKeyField(models.CharField):
""" """
description = "An OpaqueKey object, saved to the DB in the form of a string." description = "An OpaqueKey object, saved to the DB in the form of a string."
__metaclass__ = models.SubfieldBase
Empty = object() Empty = object()
KEY_CLASS = None KEY_CLASS = None
...@@ -117,6 +115,9 @@ class OpaqueKeyField(models.CharField): ...@@ -117,6 +115,9 @@ class OpaqueKeyField(models.CharField):
else: else:
return value return value
def from_db_value(self, value, expression, connection, context):
return self.to_python(value)
def get_prep_lookup(self, lookup, value): def get_prep_lookup(self, lookup, value):
if lookup == 'isnull': if lookup == 'isnull':
raise TypeError('Use {0}.Empty rather than None to query for a missing {0}'.format(self.__class__.__name__)) raise TypeError('Use {0}.Empty rather than None to query for a missing {0}'.format(self.__class__.__name__))
...@@ -131,7 +132,8 @@ class OpaqueKeyField(models.CharField): ...@@ -131,7 +132,8 @@ class OpaqueKeyField(models.CharField):
if value is self.Empty or value is None: if value is self.Empty or value is None:
return '' # CharFields should use '' as their empty value, rather than None return '' # CharFields should use '' as their empty value, rather than None
assert isinstance(value, self.KEY_CLASS), "%s is not an instance of %s" % (value, self.KEY_CLASS) assert isinstance(value, (basestring, self.KEY_CLASS)), \
"%s is not an instance of basestring or %s" % (value, self.KEY_CLASS)
serialized_key = unicode(_strip_value(value)) serialized_key = unicode(_strip_value(value))
if serialized_key.endswith('\n'): if serialized_key.endswith('\n'):
# An opaque key object serialized to a string with a trailing newline. # An opaque key object serialized to a string with a trailing newline.
......
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