models.py 6.36 KB
Newer Older
1 2 3 4
"""
Useful django models for implementing XBlock infrastructure in django.
"""

5 6
import warnings

7 8
from django.db import models
from django.core.exceptions import ValidationError
9
from opaque_keys.edx.keys import CourseKey, UsageKey, BlockTypeKey
10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49

from south.modelsinspector import add_introspection_rules


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)


50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71
def _strip_object(key):
    """
    Strips branch and version info if the given key supports those attributes.
    """
    if hasattr(key, 'version_agnostic') and hasattr(key, 'for_branch'):
        return key.for_branch(None).version_agnostic()
    else:
        return key


def _strip_value(value, lookup='exact'):
    """
    Helper function to remove the branch and version information from the given value,
    which could be a single object or a list.
    """
    if lookup == 'in':
        stripped_value = [_strip_object(el) for el in value]
    else:
        stripped_value = _strip_object(value)
    return stripped_value


72 73 74 75 76 77 78 79 80 81 82 83
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."
84 85 86 87

    __metaclass__ = models.SubfieldBase

    Empty = object()
88 89 90 91 92 93 94 95
    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)

96 97 98 99
    def to_python(self, value):
        if value is self.Empty or value is None:
            return None

100 101
        assert isinstance(value, (basestring, self.KEY_CLASS)), \
            "%s is not an instance of basestring or %s" % (value, self.KEY_CLASS)
102 103 104 105 106
        if value == '':
            # handle empty string for models being created w/o fields populated
            return None

        if isinstance(value, basestring):
107
            return self.KEY_CLASS.from_string(value)
108 109 110 111 112
        else:
            return value

    def get_prep_lookup(self, lookup, value):
        if lookup == 'isnull':
113
            raise TypeError('Use {0}.Empty rather than None to query for a missing {0}'.format(self.__class__.__name__))
114

115
        return super(OpaqueKeyField, self).get_prep_lookup(
116 117 118 119
            lookup,
            # strip key before comparing
            _strip_value(value, lookup)
        )
120 121 122 123 124

    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

125
        assert isinstance(value, self.KEY_CLASS), "%s is not an instance of %s" % (value, self.KEY_CLASS)
126
        return unicode(_strip_value(value))
127 128 129 130 131 132 133

    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:
134
            return super(OpaqueKeyField, self).validate(value, model_instance)
135 136 137 138 139 140

    def run_validators(self, value):
        """Validate Empty values, otherwise defer to the parent"""
        if value is self.Empty:
            return

141
        return super(OpaqueKeyField, self).run_validators(value)
142 143


144
class CourseKeyField(OpaqueKeyField):
145 146 147
    """
    A django Field that stores a CourseKey object as a string.
    """
148 149
    description = "A CourseKey object, saved to the DB in the form of a string"
    KEY_CLASS = CourseKey
150 151


152
class UsageKeyField(OpaqueKeyField):
153 154 155
    """
    A django Field that stores a UsageKey object as a string.
    """
156 157
    description = "A Location object, saved to the DB in the form of a string"
    KEY_CLASS = UsageKey
158 159


160
class LocationKeyField(UsageKeyField):
161 162 163
    """
    A django Field that stores a UsageKey object as a string.
    """
164 165 166
    def __init__(self, *args, **kwargs):
        warnings.warn("LocationKeyField is deprecated. Please use UsageKeyField instead.", stacklevel=2)
        super(LocationKeyField, self).__init__(*args, **kwargs)
167 168


169 170 171 172 173 174 175
class BlockTypeKeyField(OpaqueKeyField):
    """
    A django Field that stores a BlockTypeKey object as a string.
    """
    description = "A BlockTypeKey object, saved to the DB in the form of a string."
    KEY_CLASS = BlockTypeKey

176

177 178 179
add_introspection_rules([], [r"^xmodule_django\.models\.CourseKeyField"])
add_introspection_rules([], [r"^xmodule_django\.models\.LocationKeyField"])
add_introspection_rules([], [r"^xmodule_django\.models\.UsageKeyField"])
180
add_introspection_rules([], [r"^xmodule_django\.models\.BlockTypeKeyField"])