Merge pull request #13671 from edx/jeskew/move_xmodule_django_to_openedx_core
Move xmodule_django app to openedx.core.djangoapps
This commit is contained in:
@@ -3,10 +3,10 @@ from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import model_utils.fields
|
||||
import xmodule_django.models
|
||||
import jsonfield.fields
|
||||
import django.utils.timezone
|
||||
from django.conf import settings
|
||||
from openedx.core.djangoapps.xmodule_django.models import CourseKeyField, LocationKeyField
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
@@ -22,8 +22,8 @@ class Migration(migrations.Migration):
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, verbose_name='created', editable=False)),
|
||||
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, verbose_name='modified', editable=False)),
|
||||
('course_key', xmodule_django.models.CourseKeyField(max_length=255, db_index=True)),
|
||||
('usage_key', xmodule_django.models.LocationKeyField(max_length=255, db_index=True)),
|
||||
('course_key', CourseKeyField(max_length=255, db_index=True)),
|
||||
('usage_key', LocationKeyField(max_length=255, db_index=True)),
|
||||
('_path', jsonfield.fields.JSONField(help_text=b'Path in course tree to the block', db_column=b'path')),
|
||||
('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
@@ -34,8 +34,8 @@ class Migration(migrations.Migration):
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, verbose_name='created', editable=False)),
|
||||
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, verbose_name='modified', editable=False)),
|
||||
('course_key', xmodule_django.models.CourseKeyField(max_length=255, db_index=True)),
|
||||
('usage_key', xmodule_django.models.LocationKeyField(unique=True, max_length=255, db_index=True)),
|
||||
('course_key', CourseKeyField(max_length=255, db_index=True)),
|
||||
('usage_key', LocationKeyField(unique=True, max_length=255, db_index=True)),
|
||||
('display_name', models.CharField(default=b'', max_length=255)),
|
||||
('_paths', jsonfield.fields.JSONField(default=[], help_text=b'All paths in course tree to the corresponding block.', db_column=b'paths')),
|
||||
],
|
||||
|
||||
@@ -13,7 +13,7 @@ from opaque_keys.edx.keys import UsageKey
|
||||
from xmodule.modulestore import search
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError, NoPathToItem
|
||||
from xmodule_django.models import CourseKeyField, LocationKeyField
|
||||
from openedx.core.djangoapps.xmodule_django.models import CourseKeyField, LocationKeyField
|
||||
|
||||
from . import PathItem
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ from __future__ import unicode_literals
|
||||
from django.db import migrations, models
|
||||
import django.utils.timezone
|
||||
import model_utils.fields
|
||||
import xmodule_django.models
|
||||
from openedx.core.djangoapps.xmodule_django.models import CourseKeyField, UsageKeyField
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
@@ -19,8 +19,8 @@ class Migration(migrations.Migration):
|
||||
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, verbose_name='created', editable=False)),
|
||||
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, verbose_name='modified', editable=False)),
|
||||
('version', models.IntegerField()),
|
||||
('id', xmodule_django.models.CourseKeyField(max_length=255, serialize=False, primary_key=True, db_index=True)),
|
||||
('_location', xmodule_django.models.UsageKeyField(max_length=255)),
|
||||
('id', CourseKeyField(max_length=255, serialize=False, primary_key=True, db_index=True)),
|
||||
('_location', UsageKeyField(max_length=255)),
|
||||
('display_name', models.TextField(null=True)),
|
||||
('display_number_with_default', models.TextField()),
|
||||
('display_org_with_default', models.TextField()),
|
||||
|
||||
@@ -25,7 +25,7 @@ from xmodule import course_metadata_utils, block_metadata_utils
|
||||
from xmodule.course_module import CourseDescriptor, DEFAULT_START_DATE
|
||||
from xmodule.error_module import ErrorDescriptor
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule_django.models import CourseKeyField, UsageKeyField
|
||||
from openedx.core.djangoapps.xmodule_django.models import CourseKeyField, UsageKeyField
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -398,7 +398,7 @@ class CourseOverviewTestCase(ModuleStoreTestCase):
|
||||
# default value present. So mock it to avoid returning the empty str as primary key
|
||||
# value. Due to empty str, model.save will do an update instead of insert which is
|
||||
# incorrect and get exception in
|
||||
# common.djangoapps.xmodule_django.models.OpaqueKeyField.get_prep_value
|
||||
# openedx.core.djangoapps.xmodule_django.models.OpaqueKeyField.get_prep_value
|
||||
with mock.patch('django.db.models.Field.get_pk_value_on_save') as mock_get_pk_value_on_save:
|
||||
|
||||
mock_get_pk_value_on_save.return_value = None
|
||||
|
||||
@@ -5,7 +5,7 @@ from django.db import migrations, models
|
||||
import django.utils.timezone
|
||||
import model_utils.fields
|
||||
import util.models
|
||||
import xmodule_django.models
|
||||
from openedx.core.djangoapps.xmodule_django.models import CourseKeyField
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
@@ -20,7 +20,7 @@ class Migration(migrations.Migration):
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, verbose_name='created', editable=False)),
|
||||
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, verbose_name='modified', editable=False)),
|
||||
('course_id', xmodule_django.models.CourseKeyField(unique=True, max_length=255, verbose_name=b'Course ID', db_index=True)),
|
||||
('course_id', CourseKeyField(unique=True, max_length=255, verbose_name=b'Course ID', db_index=True)),
|
||||
('structure_json', util.models.CompressedTextField(null=True, verbose_name=b'Structure JSON', blank=True)),
|
||||
('discussion_id_map_json', util.models.CompressedTextField(null=True, verbose_name=b'Discussion ID Map JSON', blank=True)),
|
||||
],
|
||||
|
||||
@@ -8,7 +8,7 @@ from collections import OrderedDict
|
||||
from model_utils.models import TimeStampedModel
|
||||
|
||||
from util.models import CompressedTextField
|
||||
from xmodule_django.models import CourseKeyField, UsageKey
|
||||
from openedx.core.djangoapps.xmodule_django.models import CourseKeyField, UsageKey
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__) # pylint: disable=invalid-name
|
||||
|
||||
@@ -4,13 +4,13 @@ Course Structure Content sub-application test cases
|
||||
import json
|
||||
from nose.plugins.attrib import attr
|
||||
|
||||
from xmodule_django.models import UsageKey
|
||||
from xmodule.modulestore.django import SignalHandler
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
|
||||
from openedx.core.djangoapps.content.course_structures.models import CourseStructure
|
||||
from openedx.core.djangoapps.content.course_structures.signals import listen_for_course_publish
|
||||
from openedx.core.djangoapps.content.course_structures.tasks import _generate_course_structure, update_course_structure
|
||||
from openedx.core.djangoapps.xmodule_django.models import UsageKey
|
||||
|
||||
|
||||
class SignalDisconnectTestMixin(object):
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
from django.conf import settings
|
||||
import xmodule_django.models
|
||||
from openedx.core.djangoapps.xmodule_django.models import CourseKeyField
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
@@ -17,7 +17,7 @@ class Migration(migrations.Migration):
|
||||
name='CohortMembership',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('course_id', xmodule_django.models.CourseKeyField(max_length=255)),
|
||||
('course_id', CourseKeyField(max_length=255)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
@@ -32,7 +32,7 @@ class Migration(migrations.Migration):
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('is_cohorted', models.BooleanField(default=False)),
|
||||
('course_id', xmodule_django.models.CourseKeyField(help_text=b'Which course are these settings associated with?', unique=True, max_length=255, db_index=True)),
|
||||
('course_id', CourseKeyField(help_text=b'Which course are these settings associated with?', unique=True, max_length=255, db_index=True)),
|
||||
('_cohorted_discussions', models.TextField(null=True, db_column=b'cohorted_discussions', blank=True)),
|
||||
('always_cohort_inline_discussions', models.BooleanField(default=True)),
|
||||
],
|
||||
@@ -42,7 +42,7 @@ class Migration(migrations.Migration):
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('name', models.CharField(help_text=b'What is the name of this group? Must be unique within a course.', max_length=255)),
|
||||
('course_id', xmodule_django.models.CourseKeyField(help_text=b'Which course is this group associated with?', max_length=255, db_index=True)),
|
||||
('course_id', CourseKeyField(help_text=b'Which course is this group associated with?', max_length=255, db_index=True)),
|
||||
('group_type', models.CharField(max_length=20, choices=[(b'cohort', b'Cohort')])),
|
||||
('users', models.ManyToManyField(help_text=b'Who is in this group?', related_name='course_groups', to=settings.AUTH_USER_MODEL, db_index=True)),
|
||||
],
|
||||
|
||||
@@ -11,7 +11,7 @@ from util.db import outer_atomic
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db.models.signals import pre_delete
|
||||
from django.dispatch import receiver
|
||||
from xmodule_django.models import CourseKeyField
|
||||
from openedx.core.djangoapps.xmodule_django.models import CourseKeyField
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -4,12 +4,12 @@ from __future__ import unicode_literals
|
||||
from django.db import migrations, models
|
||||
import openedx.core.djangoapps.credit.models
|
||||
import model_utils.fields
|
||||
import xmodule_django.models
|
||||
import jsonfield.fields
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
from django.conf import settings
|
||||
import django.core.validators
|
||||
from openedx.core.djangoapps.xmodule_django.models import CourseKeyField
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
@@ -23,7 +23,7 @@ class Migration(migrations.Migration):
|
||||
name='CreditCourse',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('course_key', xmodule_django.models.CourseKeyField(unique=True, max_length=255, db_index=True)),
|
||||
('course_key', CourseKeyField(unique=True, max_length=255, db_index=True)),
|
||||
('enabled', models.BooleanField(default=False)),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -21,7 +21,7 @@ from jsonfield.fields import JSONField
|
||||
from model_utils.models import TimeStampedModel
|
||||
import pytz
|
||||
from simple_history.models import HistoricalRecords
|
||||
from xmodule_django.models import CourseKeyField
|
||||
from openedx.core.djangoapps.xmodule_django.models import CourseKeyField
|
||||
|
||||
|
||||
CREDIT_PROVIDER_ID_REGEX = r"[a-z,A-Z,0-9,\-]+"
|
||||
|
||||
@@ -6,7 +6,7 @@ import django.utils.timezone
|
||||
from django.conf import settings
|
||||
import model_utils.fields
|
||||
import django.core.validators
|
||||
import xmodule_django.models
|
||||
from openedx.core.djangoapps.xmodule_django.models import CourseKeyField
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
@@ -21,7 +21,7 @@ class Migration(migrations.Migration):
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('key', models.CharField(max_length=255, db_index=True)),
|
||||
('course_id', xmodule_django.models.CourseKeyField(max_length=255, db_index=True)),
|
||||
('course_id', CourseKeyField(max_length=255, db_index=True)),
|
||||
('value', models.TextField()),
|
||||
('user', models.ForeignKey(related_name='+', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
|
||||
@@ -9,7 +9,7 @@ from django.dispatch import receiver
|
||||
from model_utils.models import TimeStampedModel
|
||||
|
||||
from util.model_utils import get_changed_fields_dict, emit_setting_changed_event
|
||||
from xmodule_django.models import CourseKeyField
|
||||
from openedx.core.djangoapps.xmodule_django.models import CourseKeyField
|
||||
|
||||
# Currently, the "student" app is responsible for
|
||||
# accounts, profiles, enrollments, and the student dashboard.
|
||||
|
||||
0
openedx/core/djangoapps/xmodule_django/__init__.py
Normal file
0
openedx/core/djangoapps/xmodule_django/__init__.py
Normal file
198
openedx/core/djangoapps/xmodule_django/models.py
Normal file
198
openedx/core/djangoapps/xmodule_django/models.py
Normal file
@@ -0,0 +1,198 @@
|
||||
"""
|
||||
Useful django models for implementing XBlock infrastructure in django.
|
||||
"""
|
||||
import warnings
|
||||
import logging
|
||||
|
||||
from django.db import models
|
||||
from django.core.exceptions import ValidationError
|
||||
from opaque_keys.edx.keys import CourseKey, UsageKey, BlockTypeKey
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
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_queryset(self):
|
||||
"""
|
||||
Returns the result of NoneToEmptyQuerySet instead of a regular QuerySet.
|
||||
"""
|
||||
return NoneToEmptyQuerySet(self.model, using=self._db) # pylint: disable=no-member
|
||||
|
||||
|
||||
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):
|
||||
# pylint: disable=protected-access
|
||||
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)
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
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, self.KEY_CLASS)), \
|
||||
"%s is not an instance of basestring or %s" % (value, self.KEY_CLASS)
|
||||
if value == '':
|
||||
# handle empty string for models being created w/o fields populated
|
||||
return None
|
||||
|
||||
if isinstance(value, basestring):
|
||||
if value.endswith('\n'):
|
||||
# An opaque key with a trailing newline has leaked into the DB.
|
||||
# Log and strip the value.
|
||||
log.warning(u'{}:{}:{}:to_python: Invalid key: {}. Removing trailing newline.'.format(
|
||||
self.model._meta.db_table, # pylint: disable=protected-access
|
||||
self.name,
|
||||
self.KEY_CLASS.__name__,
|
||||
repr(value)
|
||||
))
|
||||
value = value.rstrip()
|
||||
return self.KEY_CLASS.from_string(value)
|
||||
else:
|
||||
return value
|
||||
|
||||
def get_prep_lookup(self, lookup, value):
|
||||
if lookup == 'isnull':
|
||||
raise TypeError('Use {0}.Empty rather than None to query for a missing {0}'.format(self.__class__.__name__))
|
||||
|
||||
return super(OpaqueKeyField, self).get_prep_lookup(
|
||||
lookup,
|
||||
# strip key before comparing
|
||||
_strip_value(value, lookup)
|
||||
)
|
||||
|
||||
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, self.KEY_CLASS), "%s is not an instance of %s" % (value, self.KEY_CLASS)
|
||||
serialized_key = unicode(_strip_value(value))
|
||||
if serialized_key.endswith('\n'):
|
||||
# An opaque key object serialized to a string with a trailing newline.
|
||||
# Log the value - but do not modify it.
|
||||
log.warning(u'{}:{}:{}:get_prep_value: Invalid key: {}.'.format(
|
||||
self.model._meta.db_table, # pylint: disable=protected-access
|
||||
self.name,
|
||||
self.KEY_CLASS.__name__,
|
||||
repr(serialized_key)
|
||||
))
|
||||
return serialized_key
|
||||
|
||||
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(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(OpaqueKeyField, self).run_validators(value)
|
||||
|
||||
|
||||
class CourseKeyField(OpaqueKeyField):
|
||||
"""
|
||||
A django Field that stores a CourseKey object as a string.
|
||||
"""
|
||||
description = "A CourseKey object, saved to the DB in the form of a string"
|
||||
KEY_CLASS = CourseKey
|
||||
|
||||
|
||||
class UsageKeyField(OpaqueKeyField):
|
||||
"""
|
||||
A django Field that stores a UsageKey object as a string.
|
||||
"""
|
||||
description = "A Location object, saved to the DB in the form of a string"
|
||||
KEY_CLASS = UsageKey
|
||||
|
||||
|
||||
class LocationKeyField(UsageKeyField):
|
||||
"""
|
||||
A django Field that stores a UsageKey object as a string.
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
warnings.warn("LocationKeyField is deprecated. Please use UsageKeyField instead.", stacklevel=2)
|
||||
super(LocationKeyField, self).__init__(*args, **kwargs)
|
||||
|
||||
|
||||
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
|
||||
Reference in New Issue
Block a user