Make course ids and usage ids opaque to LMS and Studio [partial commit]
This commit adds custom django fields for CourseKeys and UsageKeys. These keys are now objects with a limited interface, and the particular internal representation is managed by the data storage layer (the modulestore). For the LMS, there should be no outward-facing changes to the system. The keys are, for now, a change to internal representation only. For Studio, the new serialized form of the keys is used in urls, to allow for further migration in the future. Co-Author: Andy Armstrong <andya@edx.org> Co-Author: Christina Roberts <christina@edx.org> Co-Author: David Baumgold <db@edx.org> Co-Author: Diana Huang <dkh@edx.org> Co-Author: Don Mitchell <dmitchell@edx.org> Co-Author: Julia Hansbrough <julia@edx.org> Co-Author: Nimisha Asthagiri <nasthagiri@edx.org> Co-Author: Sarina Canelake <sarina@edx.org> [LMS-2370]
This commit is contained in:
0
common/djangoapps/xmodule_django/__init__.py
Normal file
0
common/djangoapps/xmodule_django/__init__.py
Normal file
143
common/djangoapps/xmodule_django/models.py
Normal file
143
common/djangoapps/xmodule_django/models.py
Normal file
@@ -0,0 +1,143 @@
|
||||
from django.db import models
|
||||
from django.core.exceptions import ValidationError
|
||||
from xmodule.modulestore.locations import SlashSeparatedCourseKey, Location
|
||||
|
||||
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):
|
||||
"""
|
||||
A :class:`django.db.models.Manager` that has a :class:`NoneToEmptyQuerySet`
|
||||
as its `QuerySet`, initialized with a set of specified `field_names`.
|
||||
"""
|
||||
def __init__(self):
|
||||
"""
|
||||
Args:
|
||||
field_names: The list of field names to initialize the :class:`NoneToEmptyQuerySet` with.
|
||||
"""
|
||||
super(NoneToEmptyManager, self).__init__()
|
||||
|
||||
def get_query_set(self):
|
||||
return NoneToEmptyQuerySet(self.model, using=self._db)
|
||||
|
||||
|
||||
class NoneToEmptyQuerySet(models.query.QuerySet):
|
||||
"""
|
||||
A :class:`django.db.query.QuerySet` that replaces `None` values passed to `filter` and `exclude`
|
||||
with the corresponding `Empty` value for all fields with an `Empty` attribute.
|
||||
|
||||
This is to work around Django automatically converting `exact` queries for `None` into
|
||||
`isnull` queries before the field has a chance to convert them to queries for it's own
|
||||
empty value.
|
||||
"""
|
||||
def _filter_or_exclude(self, *args, **kwargs):
|
||||
for name in self.model._meta.get_all_field_names():
|
||||
field_object, _model, direct, _m2m = self.model._meta.get_field_by_name(name)
|
||||
if direct and hasattr(field_object, 'Empty'):
|
||||
for suffix in ('', '_exact'):
|
||||
key = '{}{}'.format(name, suffix)
|
||||
if key in kwargs and kwargs[key] is None:
|
||||
kwargs[key] = field_object.Empty
|
||||
return super(NoneToEmptyQuerySet, self)._filter_or_exclude(*args, **kwargs)
|
||||
|
||||
|
||||
class CourseKeyField(models.CharField):
|
||||
description = "A SlashSeparatedCourseKey object, saved to the DB in the form of a string"
|
||||
|
||||
__metaclass__ = models.SubfieldBase
|
||||
|
||||
Empty = object()
|
||||
|
||||
def to_python(self, value):
|
||||
if value is self.Empty or value is None:
|
||||
return None
|
||||
|
||||
assert isinstance(value, (basestring, SlashSeparatedCourseKey))
|
||||
if value == '':
|
||||
# handle empty string for models being created w/o fields populated
|
||||
return None
|
||||
|
||||
if isinstance(value, basestring):
|
||||
return SlashSeparatedCourseKey.from_deprecated_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')
|
||||
|
||||
return super(CourseKeyField, self).get_prep_lookup(lookup, value)
|
||||
|
||||
def get_prep_value(self, value):
|
||||
if value is self.Empty or value is None:
|
||||
return '' # CharFields should use '' as their empty value, rather than None
|
||||
|
||||
assert isinstance(value, SlashSeparatedCourseKey)
|
||||
return value.to_deprecated_string()
|
||||
|
||||
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(CourseKeyField, 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)
|
||||
|
||||
|
||||
class LocationKeyField(models.CharField):
|
||||
description = "A Location object, saved to the DB in the form of a string"
|
||||
|
||||
__metaclass__ = models.SubfieldBase
|
||||
|
||||
Empty = object()
|
||||
|
||||
def to_python(self, value):
|
||||
if value is self.Empty or value is None:
|
||||
return value
|
||||
|
||||
assert isinstance(value, (basestring, Location))
|
||||
|
||||
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')
|
||||
|
||||
return super(LocationKeyField, self).get_prep_lookup(lookup, value)
|
||||
|
||||
def get_prep_value(self, value):
|
||||
if value is self.Empty:
|
||||
return ''
|
||||
|
||||
assert isinstance(value, Location)
|
||||
return value.to_deprecated_string()
|
||||
|
||||
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)
|
||||
Reference in New Issue
Block a user