Commit 9f2fa7e1 by Don Mitchell

sql migrations cannot use loc mapper

parent a9695828
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from south.db import db
from south.v2 import DataMigration from south.v2 import DataMigration
from django.db import models
from xmodule.modulestore.django import loc_mapper
import re import re
from opaque_keys.edx.locations import SlashSeparatedCourseKey from opaque_keys.edx.locations import SlashSeparatedCourseKey
from opaque_keys import InvalidKeyError from opaque_keys import InvalidKeyError
...@@ -10,6 +7,10 @@ import bson.son ...@@ -10,6 +7,10 @@ import bson.son
import logging import logging
from django.db.models.query_utils import Q from django.db.models.query_utils import Q
from django.db.utils import IntegrityError from django.db.utils import IntegrityError
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.mixed import MixedModuleStore
import itertools
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -25,8 +26,20 @@ class Migration(DataMigration): ...@@ -25,8 +26,20 @@ class Migration(DataMigration):
""" """
Converts group table entries for write access and beta_test roles to course access roles table. Converts group table entries for write access and beta_test roles to course access roles table.
""" """
store = modulestore()
if isinstance(store, MixedModuleStore):
self.mongostore = modulestore()._get_modulestore_by_type(ModuleStoreEnum.Type.mongo)
self.xmlstore = modulestore()._get_modulestore_by_type(ModuleStoreEnum.Type.xml)
elif store.get_modulestore_type() == ModuleStoreEnum.Type.mongo:
self.mongostore = store
self.xmlstore = None
elif store.get_modulestore_type() == ModuleStoreEnum.Type.xml:
self.mongostore = None
self.xmlstore = store
else:
return
# Note: Remember to use orm['appname.ModelName'] rather than "from appname.models..." # Note: Remember to use orm['appname.ModelName'] rather than "from appname.models..."
loc_map_collection = loc_mapper().location_map
# b/c the Groups table had several entries for each course, we need to ensure we process each unique # b/c the Groups table had several entries for each course, we need to ensure we process each unique
# course only once. The below datastructures help ensure that. # course only once. The below datastructures help ensure that.
hold = {} # key of course_id_strings with array of group objects. Should only be org scoped entries hold = {} # key of course_id_strings with array of group objects. Should only be org scoped entries
...@@ -64,21 +77,27 @@ class Migration(DataMigration): ...@@ -64,21 +77,27 @@ class Migration(DataMigration):
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id_string) course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id_string)
# course_key is the downcased version, get the normal cased one. loc_mapper() has no # course_key is the downcased version, get the normal cased one. loc_mapper() has no
# methods taking downcased SSCK; so, need to do it manually here # methods taking downcased SSCK; so, need to do it manually here
correct_course_key = self._map_downcased_ssck(course_key, loc_map_collection) correct_course_key = self._map_downcased_ssck(course_key)
if correct_course_key is not None: if correct_course_key is not None:
_migrate_users(correct_course_key, role, course_key.org) _migrate_users(correct_course_key, role, course_key.org)
except InvalidKeyError: except InvalidKeyError:
entry = loc_map_collection.find_one({ # old dotted format, try permutations
'course_id': re.compile(r'^{}$'.format(course_id_string), re.IGNORECASE) parts = course_id_string.split('.')
}) if len(parts) < 3:
if entry is None: hold.setdefault(course_id_string, []).append(group)
elif len(parts) == 3:
course_key = SlashSeparatedCourseKey(*parts)
correct_course_key = self._map_downcased_ssck(course_key)
if correct_course_key is None:
hold.setdefault(course_id_string, []).append(group) hold.setdefault(course_id_string, []).append(group)
else: else:
correct_course_key = SlashSeparatedCourseKey(*entry['_id'].values()) _migrate_users(correct_course_key, role, course_key.org)
if 'lower_id' in entry:
_migrate_users(correct_course_key, role, entry['lower_id']['org'])
else: else:
_migrate_users(correct_course_key, role, entry['_id']['org'].lower()) correct_course_key = self.divide_parts_find_key(parts)
if correct_course_key is None:
hold.setdefault(course_id_string, []).append(group)
else:
_migrate_users(correct_course_key, role, course_key.org)
# see if any in hold were missed above # see if any in hold were missed above
for held_auth_scope, groups in hold.iteritems(): for held_auth_scope, groups in hold.iteritems():
...@@ -99,27 +118,49 @@ class Migration(DataMigration): ...@@ -99,27 +118,49 @@ class Migration(DataMigration):
# don't silently skip unexpected roles # don't silently skip unexpected roles
log.warn("Didn't convert roles %s", [group.name for group in groups]) log.warn("Didn't convert roles %s", [group.name for group in groups])
def divide_parts_find_key(self, parts):
"""
Look for all possible org/course/run patterns from a possibly dotted source
"""
for org_stop, course_stop in itertools.combinations(range(1, len(parts)), 2):
org = '.'.join(parts[:org_stop])
course = '.'.join(parts[org_stop:course_stop])
run = '.'.join(parts[course_stop:])
course_key = SlashSeparatedCourseKey(org, course, run)
correct_course_key = self._map_downcased_ssck(course_key)
if correct_course_key is not None:
return correct_course_key
return None
def backwards(self, orm): def backwards(self, orm):
"Write your backwards methods here." "Removes the new table."
# Since this migration is non-destructive (monotonically adds information), I'm not sure what # Since this migration is non-destructive (monotonically adds information), I'm not sure what
# the semantic of backwards should be other than perhaps clearing the table. # the semantic of backwards should be other than perhaps clearing the table.
orm['student.courseaccessrole'].objects.all().delete() orm['student.courseaccessrole'].objects.all().delete()
def _map_downcased_ssck(self, downcased_ssck, loc_map_collection): def _map_downcased_ssck(self, downcased_ssck):
""" """
Get the normal cased version of this downcased slash sep course key Get the normal cased version of this downcased slash sep course key
""" """
# given the regex, the son may be an overkill if self.mongostore is not None:
course_son = bson.son.SON([ course_son = bson.son.SON([
('_id.tag', 'i4x'),
('_id.org', re.compile(r'^{}$'.format(downcased_ssck.org), re.IGNORECASE)), ('_id.org', re.compile(r'^{}$'.format(downcased_ssck.org), re.IGNORECASE)),
('_id.course', re.compile(r'^{}$'.format(downcased_ssck.course), re.IGNORECASE)), ('_id.course', re.compile(r'^{}$'.format(downcased_ssck.course), re.IGNORECASE)),
('_id.category', 'course'),
('_id.name', re.compile(r'^{}$'.format(downcased_ssck.run), re.IGNORECASE)), ('_id.name', re.compile(r'^{}$'.format(downcased_ssck.run), re.IGNORECASE)),
]) ])
entry = loc_map_collection.find_one(course_son) entry = self.mongostore.collection.find_one(course_son)
if entry: if entry:
idpart = entry['_id'] idpart = entry['_id']
return SlashSeparatedCourseKey(idpart['org'], idpart['course'], idpart['name']) return SlashSeparatedCourseKey(idpart['org'], idpart['course'], idpart['name'])
else: if self.xmlstore is not None:
for course in self.xmlstore.get_courses():
if (
course.id.org.lower() == downcased_ssck.org and course.id.course.lower() == downcased_ssck.course
and course.id.run.lower() == downcased_ssck.run
):
return course.id
return None return None
......
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from south.v2 import DataMigration from south.v2 import DataMigration
from xmodule.modulestore.django import loc_mapper, modulestore from xmodule.modulestore.django import modulestore
import re import re
from opaque_keys.edx.locations import SlashSeparatedCourseKey from opaque_keys.edx.locations import SlashSeparatedCourseKey
from opaque_keys import InvalidKeyError
import logging import logging
from django.db.models.query_utils import Q from django.db.models.query_utils import Q
from django.db.utils import IntegrityError from django.db.utils import IntegrityError
from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore import ModuleStoreEnum
import bson.son
from xmodule.modulestore.mixed import MixedModuleStore from xmodule.modulestore.mixed import MixedModuleStore
import itertools
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -24,11 +25,18 @@ class Migration(DataMigration): ...@@ -24,11 +25,18 @@ class Migration(DataMigration):
""" """
Converts group table entries for write access and beta_test roles to course access roles table. Converts group table entries for write access and beta_test roles to course access roles table.
""" """
# Note: Remember to use orm['appname.ModelName'] rather than "from appname.models..." store = modulestore()
loc_map_collection = loc_mapper().location_map if isinstance(store, MixedModuleStore):
mixed_ms = modulestore() self.mongostore = modulestore()._get_modulestore_by_type(ModuleStoreEnum.Type.mongo)
xml_ms = mixed_ms._get_modulestore_by_type(ModuleStoreEnum.Type.xml) self.xmlstore = modulestore()._get_modulestore_by_type(ModuleStoreEnum.Type.xml)
mongo_ms = mixed_ms._get_modulestore_by_type(ModuleStoreEnum.Type.mongo) elif store.get_modulestore_type() == ModuleStoreEnum.Type.mongo:
self.mongostore = store
self.xmlstore = None
elif store.get_modulestore_type() == ModuleStoreEnum.Type.xml:
self.mongostore = None
self.xmlstore = store
else:
return
query = Q(name__startswith='staff') | Q(name__startswith='instructor') | Q(name__startswith='beta_testers') query = Q(name__startswith='staff') | Q(name__startswith='instructor') | Q(name__startswith='beta_testers')
for group in orm['auth.Group'].objects.filter(query).exclude(name__contains="/").all(): for group in orm['auth.Group'].objects.filter(query).exclude(name__contains="/").all():
...@@ -59,10 +67,7 @@ class Migration(DataMigration): ...@@ -59,10 +67,7 @@ class Migration(DataMigration):
role = parsed_entry.group('role_id') role = parsed_entry.group('role_id')
course_id_string = parsed_entry.group('course_id_string') course_id_string = parsed_entry.group('course_id_string')
# if it's a full course_id w/ dots, ignore it # if it's a full course_id w/ dots, ignore it
entry = loc_map_collection.find_one({ if u'/' not in course_id_string and not self.dotted_course(course_id_string):
'course_id': re.compile(r'^{}$'.format(course_id_string), re.IGNORECASE)
})
if entry is None:
# check new table to see if it's been added as org permission # check new table to see if it's been added as org permission
if not orm['student.courseaccessrole'].objects.filter( if not orm['student.courseaccessrole'].objects.filter(
role=role, role=role,
...@@ -70,14 +75,14 @@ class Migration(DataMigration): ...@@ -70,14 +75,14 @@ class Migration(DataMigration):
).exists(): ).exists():
# old auth was of form role_coursenum. Grant access to all such courses wildcarding org and run # old auth was of form role_coursenum. Grant access to all such courses wildcarding org and run
# look in xml for matching courses # look in xml for matching courses
if xml_ms is not None: if self.xmlstore is not None:
for course in xml_ms.get_courses(): for course in self.xmlstore.get_courses():
if course_id_string == course.id.course.lower(): if course_id_string == course.id.course.lower():
_migrate_users(course.id, role) _migrate_users(course.id, role)
if mongo_ms is not None: if self.mongostore is not None:
mongo_query = re.compile(ur'^{}$'.format(course_id_string), re.IGNORECASE) mongo_query = re.compile(ur'^{}$'.format(course_id_string), re.IGNORECASE)
for mongo_entry in mongo_ms.collection.find( for mongo_entry in self.mongostore.collection.find(
{"_id.category": "course", "_id.course": mongo_query}, fields=["_id"] {"_id.category": "course", "_id.course": mongo_query}, fields=["_id"]
): ):
mongo_id_dict = mongo_entry['_id'] mongo_id_dict = mongo_entry['_id']
...@@ -86,6 +91,44 @@ class Migration(DataMigration): ...@@ -86,6 +91,44 @@ class Migration(DataMigration):
) )
_migrate_users(course_key, role) _migrate_users(course_key, role)
def dotted_course(self, parts):
"""
Look for all possible org/course/run patterns from a possibly dotted source
"""
for org_stop, course_stop in itertools.combinations(range(1, len(parts)), 2):
org = '.'.join(parts[:org_stop])
course = '.'.join(parts[org_stop:course_stop])
run = '.'.join(parts[course_stop:])
course_key = SlashSeparatedCourseKey(org, course, run)
correct_course_key = self._map_downcased_ssck(course_key)
if correct_course_key is not None:
return correct_course_key
return False
def _map_downcased_ssck(self, downcased_ssck):
"""
Get the normal cased version of this downcased slash sep course key
"""
if self.mongostore is not None:
course_son = bson.son.SON([
('_id.tag', 'i4x'),
('_id.org', re.compile(r'^{}$'.format(downcased_ssck.org), re.IGNORECASE)),
('_id.course', re.compile(r'^{}$'.format(downcased_ssck.course), re.IGNORECASE)),
('_id.category', 'course'),
('_id.name', re.compile(r'^{}$'.format(downcased_ssck.run), re.IGNORECASE)),
])
entry = self.mongostore.collection.find_one(course_son)
if entry:
idpart = entry['_id']
return SlashSeparatedCourseKey(idpart['org'], idpart['course'], idpart['name'])
if self.xmlstore is not None:
for course in self.xmlstore.get_courses():
if (
course.id.org.lower() == downcased_ssck.org and course.id.course.lower() == downcased_ssck.course
and course.id.run.lower() == downcased_ssck.run
):
return course.id
return None
def backwards(self, orm): def backwards(self, orm):
"No obvious way to reverse just this migration, but reversing 0035 will reverse this." "No obvious way to reverse just this migration, but reversing 0035 will reverse this."
......
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