From ccbff2dafc29d5ca8185e8d79cab99eea900e8a1 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Tue, 2 Dec 2014 13:54:51 -0500 Subject: [PATCH] Make a generic django field for storing OpaqueKey classes --- common/djangoapps/xmodule_django/models.py | 101 +++++++++------------ 1 file changed, 43 insertions(+), 58 deletions(-) diff --git a/common/djangoapps/xmodule_django/models.py b/common/djangoapps/xmodule_django/models.py index bcb287dc0b..b6449884e2 100644 --- a/common/djangoapps/xmodule_django/models.py +++ b/common/djangoapps/xmodule_django/models.py @@ -1,3 +1,5 @@ +import warnings + from django.db import models from django.core.exceptions import ValidationError from opaque_keys.edx.locations import SlashSeparatedCourseKey, Location @@ -5,8 +7,6 @@ from opaque_keys.edx.keys import CourseKey, UsageKey from opaque_keys.edx.locator import Locator from south.modelsinspector import add_introspection_rules -add_introspection_rules([], ["^xmodule_django\.models\.CourseKeyField"]) -add_introspection_rules([], ["^xmodule_django\.models\.LocationKeyField"]) class NoneToEmptyManager(models.Manager): @@ -67,32 +67,50 @@ def _strip_value(value, lookup='exact'): return stripped_value -class CourseKeyField(models.CharField): - description = "A CourseKey object, saved to the DB in the form of a string" +class OpaqueKeyField(models.CharField): + """ + A django field for storing OpaqueKeys. + + The baseclass will return the value from the database as a string, rather than an instance + of an OpaqueKey, leaving the application to determine which key subtype to parse the string + as. + + Subclasses must specify a KEY_CLASS attribute, in which case the field will use :meth:`from_string` + to parse the key string, and will return an instance of KEY_CLASS. + """ + description = "An OpaqueKey object, saved to the DB in the form of a string." __metaclass__ = models.SubfieldBase Empty = object() + KEY_CLASS = None + + def __init__(self, *args, **kwargs): + if self.KEY_CLASS is None: + raise ValueError('Must specify KEY_CLASS in OpaqueKeyField subclasses') + + super(OpaqueKeyField, self).__init__(*args, **kwargs) + def to_python(self, value): if value is self.Empty or value is None: return None - assert isinstance(value, (basestring, CourseKey)) + assert isinstance(value, (basestring, self.KEY_CLASS)) if value == '': # handle empty string for models being created w/o fields populated return None if isinstance(value, basestring): - return CourseKey.from_string(value) + return self.KEY_CLASS.from_string(value) else: return value def get_prep_lookup(self, lookup, value): if lookup == 'isnull': - raise TypeError('Use CourseKeyField.Empty rather than None to query for a missing CourseKeyField') + raise TypeError('Use {0}.Empty rather than None to query for a missing {0}'.format(self.__class__.__name__)) - return super(CourseKeyField, self).get_prep_lookup( + return super(OpaqueKeyField, self).get_prep_lookup( lookup, # strip key before comparing _strip_value(value, lookup) @@ -102,7 +120,7 @@ class CourseKeyField(models.CharField): if value is self.Empty or value is None: return '' # CharFields should use '' as their empty value, rather than None - assert isinstance(value, CourseKey) + assert isinstance(value, self.KEY_CLASS) return unicode(_strip_value(value)) def validate(self, value, model_instance): @@ -111,66 +129,33 @@ class CourseKeyField(models.CharField): if not self.blank and value is self.Empty: raise ValidationError(self.error_messages['blank']) else: - return super(CourseKeyField, self).validate(value, model_instance) + return super(OpaqueKeyField, self).validate(value, model_instance) def run_validators(self, value): """Validate Empty values, otherwise defer to the parent""" if value is self.Empty: return - return super(CourseKeyField, self).run_validators(value) + return super(OpaqueKeyField, self).run_validators(value) -class LocationKeyField(models.CharField): +class CourseKeyField(OpaqueKeyField): + description = "A CourseKey object, saved to the DB in the form of a string" + KEY_CLASS = CourseKey + + +class UsageKeyField(OpaqueKeyField): description = "A Location object, saved to the DB in the form of a string" + KEY_CLASS = UsageKey - __metaclass__ = models.SubfieldBase - Empty = object() +class LocationKeyField(UsageKeyField): + def __init__(self, *args, **kwargs): + warnings.warn("LocationKeyField is deprecated. Please use UsageKeyField instead.", stacklevel=2) + super(LocationKeyField, self).__init__(*args, **kwargs) - def to_python(self, value): - if value is self.Empty or value is None: - return value - assert isinstance(value, (basestring, UsageKey)) - if value == '': - return None - - if isinstance(value, basestring): - return Location.from_deprecated_string(value) - else: - return value - - def get_prep_lookup(self, lookup, value): - if lookup == 'isnull': - raise TypeError('Use LocationKeyField.Empty rather than None to query for a missing LocationKeyField') - - # remove version and branch info before comparing keys - return super(LocationKeyField, self).get_prep_lookup( - lookup, - # strip key before comparing - _strip_value(value, lookup) - ) - - def get_prep_value(self, value): - if value is self.Empty: - return '' - - assert isinstance(value, UsageKey) - return unicode(_strip_value(value)) - - def validate(self, value, model_instance): - """Validate Empty values, otherwise defer to the parent""" - # raise validation error if the use of this field says it can't be blank but it is - if not self.blank and value is self.Empty: - raise ValidationError(self.error_messages['blank']) - else: - return super(LocationKeyField, self).validate(value, model_instance) - - def run_validators(self, value): - """Validate Empty values, otherwise defer to the parent""" - if value is self.Empty: - return - - return super(LocationKeyField, self).run_validators(value) +add_introspection_rules([], ["^xmodule_django\.models\.CourseKeyField"]) +add_introspection_rules([], ["^xmodule_django\.models\.LocationKeyField"]) +add_introspection_rules([], ["^xmodule_django\.models\.UsageKeyField"])