Make course ids and usage ids opaque to LMS and Studio [partial commit]

This commit migrates roles from named groups to a relational table.

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:
Calen Pennington
2014-04-30 10:17:32 -04:00
parent 7852906ce0
commit 79cf4c7239
6 changed files with 797 additions and 187 deletions

View File

@@ -0,0 +1,189 @@
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding model 'CourseAccessRole'
db.create_table('student_courseaccessrole', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])),
('org', self.gf('django.db.models.fields.CharField')(db_index=True, max_length=64, blank=True)),
('course_id', self.gf('xmodule_django.models.CourseKeyField')(db_index=True, max_length=255, blank=True)),
('role', self.gf('django.db.models.fields.CharField')(max_length=64, db_index=True)),
))
db.send_create_signal('student', ['CourseAccessRole'])
# Adding unique constraint on 'CourseAccessRole', fields ['user', 'org', 'course_id', 'role']
db.create_unique('student_courseaccessrole', ['user_id', 'org', 'course_id', 'role'])
# Changing field 'AnonymousUserId.course_id'
db.alter_column('student_anonymoususerid', 'course_id', self.gf('xmodule_django.models.CourseKeyField')(max_length=255))
# Changing field 'CourseEnrollment.course_id'
db.alter_column('student_courseenrollment', 'course_id', self.gf('xmodule_django.models.CourseKeyField')(max_length=255))
# Changing field 'CourseEnrollmentAllowed.course_id'
db.alter_column('student_courseenrollmentallowed', 'course_id', self.gf('xmodule_django.models.CourseKeyField')(max_length=255))
def backwards(self, orm):
# Removing unique constraint on 'CourseAccessRole', fields ['user', 'org', 'course_id', 'role']
db.delete_unique('student_courseaccessrole', ['user_id', 'org', 'course_id', 'role'])
# Deleting model 'CourseAccessRole'
db.delete_table('student_courseaccessrole')
# Changing field 'AnonymousUserId.course_id'
db.alter_column('student_anonymoususerid', 'course_id', self.gf('django.db.models.fields.CharField')(max_length=255))
# Changing field 'CourseEnrollment.course_id'
db.alter_column('student_courseenrollment', 'course_id', self.gf('django.db.models.fields.CharField')(max_length=255))
# Changing field 'CourseEnrollmentAllowed.course_id'
db.alter_column('student_courseenrollmentallowed', 'course_id', self.gf('django.db.models.fields.CharField')(max_length=255))
models = {
'auth.group': {
'Meta': {'object_name': 'Group'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
'auth.permission': {
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
'student.anonymoususerid': {
'Meta': {'object_name': 'AnonymousUserId'},
'anonymous_user_id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32'}),
'course_id': ('xmodule_django.models.CourseKeyField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'student.courseaccessrole': {
'Meta': {'unique_together': "(('user', 'org', 'course_id', 'role'),)", 'object_name': 'CourseAccessRole'},
'course_id': ('xmodule_django.models.CourseKeyField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'org': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '64', 'blank': 'True'}),
'role': ('django.db.models.fields.CharField', [], {'max_length': '64', 'db_index': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'student.courseenrollment': {
'Meta': {'ordering': "('user', 'course_id')", 'unique_together': "(('user', 'course_id'),)", 'object_name': 'CourseEnrollment'},
'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}),
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'mode': ('django.db.models.fields.CharField', [], {'default': "'honor'", 'max_length': '100'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'student.courseenrollmentallowed': {
'Meta': {'unique_together': "(('email', 'course_id'),)", 'object_name': 'CourseEnrollmentAllowed'},
'auto_enroll': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}),
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}),
'email': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
},
'student.loginfailures': {
'Meta': {'object_name': 'LoginFailures'},
'failure_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'lockout_until': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'student.passwordhistory': {
'Meta': {'object_name': 'PasswordHistory'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'time_set': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'student.pendingemailchange': {
'Meta': {'object_name': 'PendingEmailChange'},
'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'new_email': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'})
},
'student.pendingnamechange': {
'Meta': {'object_name': 'PendingNameChange'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'new_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
'rationale': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}),
'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'})
},
'student.registration': {
'Meta': {'object_name': 'Registration', 'db_table': "'auth_registration'"},
'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'unique': 'True'})
},
'student.userprofile': {
'Meta': {'object_name': 'UserProfile', 'db_table': "'auth_userprofile'"},
'allow_certificate': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'city': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'country': ('django_countries.fields.CountryField', [], {'max_length': '2', 'null': 'True', 'blank': 'True'}),
'courseware': ('django.db.models.fields.CharField', [], {'default': "'course.xml'", 'max_length': '255', 'blank': 'True'}),
'gender': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '6', 'null': 'True', 'blank': 'True'}),
'goals': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'language': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
'level_of_education': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '6', 'null': 'True', 'blank': 'True'}),
'location': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
'mailing_address': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'meta': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'profile'", 'unique': 'True', 'to': "orm['auth.User']"}),
'year_of_birth': ('django.db.models.fields.IntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'})
},
'student.userstanding': {
'Meta': {'object_name': 'UserStanding'},
'account_status': ('django.db.models.fields.CharField', [], {'max_length': '31', 'blank': 'True'}),
'changed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'standing_last_changed_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'standing'", 'unique': 'True', 'to': "orm['auth.User']"})
},
'student.usertestgroup': {
'Meta': {'object_name': 'UserTestGroup'},
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}),
'users': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.User']", 'db_index': 'True', 'symmetrical': 'False'})
}
}
complete_apps = ['student']

View File

@@ -0,0 +1,274 @@
# -*- coding: utf-8 -*-
from south.db import db
from south.v2 import DataMigration
from django.db import models
from xmodule.modulestore.django import loc_mapper
import re
from xmodule.modulestore.locations import SlashSeparatedCourseKey
from opaque_keys import InvalidKeyError
import bson.son
import logging
from django.db.models.query_utils import Q
log = logging.getLogger(__name__)
class Migration(DataMigration):
"""
Converts course_creator, instructor_, staff_, and betatestuser_ to new table
"""
GROUP_ENTRY_RE = re.compile(r'(?P<role_id>staff|instructor|beta_testers|course_creator_group)_?(?P<course_id_string>.*)')
def forwards(self, orm):
"""
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..."
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
# course only once. The below datastructures help ensure that.
hold = {}
done = set()
orgs = {}
query = Q(name__startswith='course_creator_group')
for role in ['staff', 'instructor', 'beta_testers', ]:
query = query | Q(name__startswith=role)
for group in orm['auth.Group'].objects.filter(query).all():
def _migrate_users(correct_course_key, lower_org):
"""
Get all the users from the old group and migrate to this course key in the new table
"""
for user in orm['auth.user'].objects.filter(groups=group).all():
entry = orm['student.courseaccessrole'](
role=parsed_entry.group('role_id'), user=user,
org=correct_course_key.org, course_id=correct_course_key.to_deprecated_string()
)
entry.save()
orgs[lower_org] = correct_course_key.org
done.add(correct_course_key)
# should this actually loop through all groups and log any which are not compliant? That is,
# remove the above filter
parsed_entry = self.GROUP_ENTRY_RE.match(group.name)
if parsed_entry.group('role_id') == 'course_creator_group':
for user in orm['auth.user'].objects.filter(groups=group).all():
entry = orm['student.courseaccessrole'](role=parsed_entry.group('role_id'), user=user)
entry.save()
else:
course_id_string = parsed_entry.group('course_id_string')
try:
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id_string)
if course_key not in done:
# is the downcased version, get the normal cased one. loc_mapper() has no
# methods taking downcased SSCK; so, need to do it manually here
correct_course_key = self._map_downcased_ssck(course_key, loc_map_collection, done)
_migrate_users(correct_course_key, course_key.org)
done.add(course_key)
except InvalidKeyError:
entry = loc_map_collection.find_one({
'course_id': re.compile(r'^{}$'.format(course_id_string), re.IGNORECASE)
})
if entry is None:
# not a course_id as far as we can tell
if course_id_string not in done:
hold[course_id_string] = group
else:
correct_course_key = self._cache_done_return_ssck(entry, done)
_migrate_users(correct_course_key, entry['lower_id']['org'])
# see if any in hold ere missed above
for not_ssck, group in hold.iteritems():
if not_ssck not in done:
if not_ssck in orgs:
# they have org permission
for user in orm['auth.user'].objects.filter(groups=group).all():
entry = orm['student.courseaccessrole'](
role=parsed_entry.group('role_id'), user=user,
org=orgs[not_ssck],
)
entry.save()
else:
# should this just log or really make an effort to do the conversion?
log.warn("Didn't convert role %s", group.name)
def backwards(self, orm):
"Write your backwards methods here."
# 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.
orm['student.courseaccessrole'].objects.all().delete()
def _map_downcased_ssck(self, downcased_ssck, loc_map_collection, done):
"""
Get the normal cased version of this downcased slash sep course key and add
the lowercased locator form to done map
"""
# given the regex, the son may be an overkill
course_son = bson.son.SON([
('_id.org', re.compile(r'^{}$'.format(downcased_ssck.org), re.IGNORECASE)),
('_id.course', re.compile(r'^{}$'.format(downcased_ssck.course), re.IGNORECASE)),
('_id.name', re.compile(r'^{}$'.format(downcased_ssck.run), re.IGNORECASE)),
])
entry = loc_map_collection.find_one(course_son)
if entry:
return self._cache_done_return_ssck(entry, done)
else:
return None
def _cache_done_return_ssck(self, entry, done):
"""
Add all the various formats which auth may use to the done set and return the ssck for the entry
"""
# cache that the dotted form is done too
if 'lower_course_id' in entry:
done.add(entry['lower_course_id'])
elif 'course_id' in entry:
done.add(entry['course_id'].lower())
elif 'lower_org' in entry:
done.add('{}.{}'.format(entry['lower_org'], entry['lower_offering']))
else:
done.add('{}.{}'.format(entry['org'].lower(), entry['offering'].lower()))
return SlashSeparatedCourseKey(*entry['_id'].values())
models = {
'auth.group': {
'Meta': {'object_name': 'Group'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
'auth.permission': {
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
'student.anonymoususerid': {
'Meta': {'object_name': 'AnonymousUserId'},
'anonymous_user_id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32'}),
'course_id': ('xmodule_django.models.CourseKeyField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'student.courseaccessrole': {
'Meta': {'unique_together': "(('user', 'org', 'course_id', 'role'),)", 'object_name': 'CourseAccessRole'},
'course_id': ('xmodule_django.models.CourseKeyField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'org': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '64', 'blank': 'True'}),
'role': ('django.db.models.fields.CharField', [], {'max_length': '64', 'db_index': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'student.courseenrollment': {
'Meta': {'ordering': "('user', 'course_id')", 'unique_together': "(('user', 'course_id'),)", 'object_name': 'CourseEnrollment'},
'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}),
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'mode': ('django.db.models.fields.CharField', [], {'default': "'honor'", 'max_length': '100'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'student.courseenrollmentallowed': {
'Meta': {'unique_together': "(('email', 'course_id'),)", 'object_name': 'CourseEnrollmentAllowed'},
'auto_enroll': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}),
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}),
'email': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
},
'student.loginfailures': {
'Meta': {'object_name': 'LoginFailures'},
'failure_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'lockout_until': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'student.passwordhistory': {
'Meta': {'object_name': 'PasswordHistory'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'time_set': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'student.pendingemailchange': {
'Meta': {'object_name': 'PendingEmailChange'},
'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'new_email': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'})
},
'student.pendingnamechange': {
'Meta': {'object_name': 'PendingNameChange'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'new_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
'rationale': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}),
'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'})
},
'student.registration': {
'Meta': {'object_name': 'Registration', 'db_table': "'auth_registration'"},
'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'unique': 'True'})
},
'student.userprofile': {
'Meta': {'object_name': 'UserProfile', 'db_table': "'auth_userprofile'"},
'allow_certificate': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'city': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'country': ('django_countries.fields.CountryField', [], {'max_length': '2', 'null': 'True', 'blank': 'True'}),
'courseware': ('django.db.models.fields.CharField', [], {'default': "'course.xml'", 'max_length': '255', 'blank': 'True'}),
'gender': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '6', 'null': 'True', 'blank': 'True'}),
'goals': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'language': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
'level_of_education': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '6', 'null': 'True', 'blank': 'True'}),
'location': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
'mailing_address': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'meta': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'profile'", 'unique': 'True', 'to': "orm['auth.User']"}),
'year_of_birth': ('django.db.models.fields.IntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'})
},
'student.userstanding': {
'Meta': {'object_name': 'UserStanding'},
'account_status': ('django.db.models.fields.CharField', [], {'max_length': '31', 'blank': 'True'}),
'changed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'standing_last_changed_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'standing'", 'unique': 'True', 'to': "orm['auth.User']"})
},
'student.usertestgroup': {
'Meta': {'object_name': 'UserTestGroup'},
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}),
'users': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.User']", 'db_index': 'True', 'symmetrical': 'False'})
}
}
complete_apps = ['student']
symmetrical = True

View File

@@ -27,9 +27,7 @@ from django.contrib.auth.hashers import make_password
from django.contrib.auth.signals import user_logged_in, user_logged_out
from django.db import models, IntegrityError
from django.db.models import Count
from django.db.models.signals import post_save
from django.dispatch import receiver, Signal
import django.dispatch
from django.core.exceptions import ObjectDoesNotExist
from django.utils.translation import ugettext_noop
from django_countries import CountryField
@@ -38,11 +36,14 @@ from track.views import server_track
from eventtracking import tracker
from importlib import import_module
from xmodule.modulestore import Location
from xmodule.modulestore.locations import SlashSeparatedCourseKey
from course_modes.models import CourseMode
import lms.lib.comment_client as cc
from util.query import use_read_replica_if_available
from xmodule_django.models import CourseKeyField, NoneToEmptyManager
from xmodule.modulestore.keys import CourseKey
from functools import total_ordering
unenroll_done = Signal(providing_args=["course_enrollment"])
log = logging.getLogger(__name__)
@@ -59,9 +60,12 @@ class AnonymousUserId(models.Model):
We generate anonymous_user_id using md5 algorithm,
and use result in hex form, so its length is equal to 32 bytes.
"""
objects = NoneToEmptyManager()
user = models.ForeignKey(User, db_index=True)
anonymous_user_id = models.CharField(unique=True, max_length=32)
course_id = models.CharField(db_index=True, max_length=255)
course_id = CourseKeyField(db_index=True, max_length=255, blank=True)
unique_together = (user, course_id)
@@ -84,11 +88,12 @@ def anonymous_id_for_user(user, course_id):
hasher = hashlib.md5()
hasher.update(settings.SECRET_KEY)
hasher.update(unicode(user.id))
hasher.update(course_id.encode('utf-8'))
if course_id:
hasher.update(course_id.to_deprecated_string())
digest = hasher.hexdigest()
try:
anonymous_user_id, created = AnonymousUserId.objects.get_or_create(
anonymous_user_id, __ = AnonymousUserId.objects.get_or_create(
defaults={'anonymous_user_id': digest},
user=user,
course_id=course_id
@@ -267,7 +272,7 @@ def unique_id_for_user(user):
"""
# Setting course_id to '' makes it not affect the generated hash,
# and thus produce the old per-student anonymous id
return anonymous_id_for_user(user, '')
return anonymous_id_for_user(user, None)
# TODO: Should be renamed to generic UserGroup, and possibly
@@ -572,7 +577,7 @@ class CourseEnrollment(models.Model):
MODEL_TAGS = ['course_id', 'is_active', 'mode']
user = models.ForeignKey(User)
course_id = models.CharField(max_length=255, db_index=True)
course_id = CourseKeyField(max_length=255, db_index=True)
created = models.DateTimeField(auto_now_add=True, null=True, db_index=True)
# If is_active is False, then the student is not considered to be enrolled
@@ -593,7 +598,7 @@ class CourseEnrollment(models.Model):
).format(self.user, self.course_id, self.created, self.is_active)
@classmethod
def get_or_create_enrollment(cls, user, course_id):
def get_or_create_enrollment(cls, user, course_key):
"""
Create an enrollment for a user in a class. By default *this enrollment
is not active*. This is useful for when an enrollment needs to go
@@ -615,12 +620,14 @@ class CourseEnrollment(models.Model):
# save it to the database so that it can have an ID that we can throw
# into our CourseEnrollment object. Otherwise, we'll get an
# IntegrityError for having a null user_id.
assert(isinstance(course_key, CourseKey))
if user.id is None:
user.save()
enrollment, created = CourseEnrollment.objects.get_or_create(
user=user,
course_id=course_id,
course_id=course_key,
)
# If we *did* just create a new enrollment, set some defaults
@@ -650,7 +657,7 @@ class CourseEnrollment(models.Model):
"""
is_course_full = False
if course.max_student_enrollments_allowed is not None:
is_course_full = cls.num_enrolled_in(course.location.course_id) >= course.max_student_enrollments_allowed
is_course_full = cls.num_enrolled_in(course.id) >= course.max_student_enrollments_allowed
return is_course_full
def update_enrollment(self, mode=None, is_active=None):
@@ -679,15 +686,13 @@ class CourseEnrollment(models.Model):
if activation_changed or mode_changed:
self.save()
if activation_changed:
course_id_dict = Location.parse_course_id(self.course_id)
if self.is_active:
self.emit_event(EVENT_NAME_ENROLLMENT_ACTIVATED)
dog_stats_api.increment(
"common.student.enrollment",
tags=[u"org:{org}".format(**course_id_dict),
u"course:{course}".format(**course_id_dict),
u"run:{name}".format(**course_id_dict),
tags=[u"org:{}".format(self.course_id.org),
u"offering:{}".format(self.course_id.offering),
u"mode:{}".format(self.mode)]
)
@@ -698,9 +703,8 @@ class CourseEnrollment(models.Model):
dog_stats_api.increment(
"common.student.unenrollment",
tags=[u"org:{org}".format(**course_id_dict),
u"course:{course}".format(**course_id_dict),
u"run:{name}".format(**course_id_dict),
tags=[u"org:{}".format(self.course_id.org),
u"offering:{}".format(self.course_id.offering),
u"mode:{}".format(self.mode)]
)
@@ -711,9 +715,10 @@ class CourseEnrollment(models.Model):
try:
context = contexts.course_context_from_course_id(self.course_id)
assert(isinstance(self.course_id, SlashSeparatedCourseKey))
data = {
'user_id': self.user.id,
'course_id': self.course_id,
'course_id': self.course_id.to_deprecated_string(),
'mode': self.mode,
}
@@ -724,7 +729,7 @@ class CourseEnrollment(models.Model):
log.exception('Unable to emit event %s for user %s and course %s', event_name, self.user.username, self.course_id)
@classmethod
def enroll(cls, user, course_id, mode="honor"):
def enroll(cls, user, course_key, mode="honor"):
"""
Enroll a user in a course. This saves immediately.
@@ -744,7 +749,7 @@ class CourseEnrollment(models.Model):
It is expected that this method is called from a method which has already
verified the user authentication and access.
"""
enrollment = cls.get_or_create_enrollment(user, course_id)
enrollment = cls.get_or_create_enrollment(user, course_key)
enrollment.update_enrollment(is_active=True, mode=mode)
return enrollment
@@ -824,7 +829,7 @@ class CourseEnrollment(models.Model):
log.error(err_msg.format(email, course_id))
@classmethod
def is_enrolled(cls, user, course_id):
def is_enrolled(cls, user, course_key):
"""
Returns True if the user is enrolled in the course (the entry must exist
and it must have `is_active=True`). Otherwise, returns False.
@@ -836,7 +841,7 @@ class CourseEnrollment(models.Model):
`course_id` is our usual course_id string (e.g. "edX/Test101/2013_Fall)
"""
try:
record = CourseEnrollment.objects.get(user=user, course_id=course_id)
record = CourseEnrollment.objects.get(user=user, course_id=course_key)
return record.is_active
except cls.DoesNotExist:
return False
@@ -854,13 +859,16 @@ class CourseEnrollment(models.Model):
attribute), this method will automatically save it before
adding an enrollment for it.
`course_id_partial` is a starting substring for a fully qualified
course_id (e.g. "edX/Test101/").
`course_id_partial` (CourseKey) is missing the run component
"""
assert isinstance(course_id_partial, SlashSeparatedCourseKey)
assert not course_id_partial.run # None or empty string
course_key = SlashSeparatedCourseKey(course_id_partial.org, course_id_partial.course, '')
querystring = unicode(course_key.to_deprecated_string())
try:
return CourseEnrollment.objects.filter(
user=user,
course_id__startswith=course_id_partial,
course_id__startswith=querystring,
is_active=1
).exists()
except cls.DoesNotExist:
@@ -944,7 +952,7 @@ class CourseEnrollmentAllowed(models.Model):
even if the enrollment time window is past.
"""
email = models.CharField(max_length=255, db_index=True)
course_id = models.CharField(max_length=255, db_index=True)
course_id = CourseKeyField(max_length=255, db_index=True)
auto_enroll = models.BooleanField(default=0)
created = models.DateTimeField(auto_now_add=True, null=True, db_index=True)
@@ -955,7 +963,51 @@ class CourseEnrollmentAllowed(models.Model):
def __unicode__(self):
return "[CourseEnrollmentAllowed] %s: %s (%s)" % (self.email, self.course_id, self.created)
# cache_relation(User.profile)
@total_ordering
class CourseAccessRole(models.Model):
"""
Maps users to org, courses, and roles. Used by student.roles.CourseRole and OrgRole.
To establish a user as having a specific role over all courses in the org, create an entry
without a course_id.
"""
objects = NoneToEmptyManager()
user = models.ForeignKey(User)
# blank org is for global group based roles such as course creator (may be deprecated)
org = models.CharField(max_length=64, db_index=True, blank=True)
# blank course_id implies org wide role
course_id = CourseKeyField(max_length=255, db_index=True, blank=True)
role = models.CharField(max_length=64, db_index=True)
class Meta:
unique_together = ('user', 'org', 'course_id', 'role')
@property
def _key(self):
"""
convenience function to make eq overrides easier and clearer. arbitrary decision
that role is primary, followed by org, course, and then user
"""
return (self.role, self.org, self.course_id, self.user)
def __eq__(self, other):
"""
Overriding eq b/c the django impl relies on the primary key which requires fetch. sometimes we
just want to compare roles w/o doing another fetch.
"""
return type(self) == type(other) and self._key == other._key
def __hash__(self):
return hash(self._key)
def __lt__(self, other):
"""
Lexigraphic sort
"""
return self._key < other._key
#### Helper methods for use from python manage.py shell and other classes.

View File

@@ -5,19 +5,9 @@ adding users, removing users, and listing members
from abc import ABCMeta, abstractmethod
from django.contrib.auth.models import User, Group
from xmodule.modulestore import Location
from xmodule.modulestore.exceptions import InvalidLocationError, ItemNotFoundError
from xmodule.modulestore.django import loc_mapper
from xmodule.modulestore.locator import CourseLocator, Locator
class CourseContextRequired(Exception):
"""
Raised when a course_context is required to determine permissions
"""
pass
from django.contrib.auth.models import User
from student.models import CourseAccessRole
from xmodule_django.models import CourseKeyField
class AccessRole(object):
@@ -78,21 +68,25 @@ class GlobalStaff(AccessRole):
raise Exception("This operation is un-indexed, and shouldn't be used")
class GroupBasedRole(AccessRole):
class RoleBase(AccessRole):
"""
A role based on membership to any of a set of groups.
Roles by type (e.g., instructor, beta_user) and optionally org, course_key
"""
def __init__(self, group_names):
def __init__(self, role_name, org='', course_key=CourseKeyField.Empty):
"""
Create a GroupBasedRole from a list of group names
The first element of `group_names` will be the preferred group
to use when adding a user to this Role.
If a user is a member of any of the groups in the list, then
they will be consider a member of the Role
Create role from required role_name w/ optional org and course_key. You may just provide a role
name if it's a global role (not constrained to an org or course). Provide org if constrained to
an org. Provide org and course if constrained to a course. Although, you should use the subclasses
for all of these.
"""
self._group_names = [name.lower() for name in group_names]
super(RoleBase, self).__init__()
if course_key is None:
raise TypeError('course_key must be CourseKeyField.Empty or a valid CourseKey')
self.org = org
self.course_key = course_key
self._role_name = role_name
def has_user(self, user):
"""
@@ -102,10 +96,13 @@ class GroupBasedRole(AccessRole):
return False
# pylint: disable=protected-access
if not hasattr(user, '_groups'):
user._groups = set(name.lower() for name in user.groups.values_list('name', flat=True))
if not hasattr(user, '_roles'):
user._roles = set(
CourseAccessRole.objects.filter(user=user).all()
)
return len(user._groups.intersection(self._group_names)) > 0
role = CourseAccessRole(user=user, role=self._role_name, course_id=self.course_key, org=self.org)
return role in user._roles
def add_users(self, *users):
"""
@@ -113,87 +110,59 @@ class GroupBasedRole(AccessRole):
"""
# silently ignores anonymous and inactive users so that any that are
# legit get updated.
users = [user for user in users if user.is_authenticated() and user.is_active]
group, _ = Group.objects.get_or_create(name=self._group_names[0])
group.user_set.add(*users)
# remove cache
for user in users:
if hasattr(user, '_groups'):
del user._groups
if user.is_authenticated and user.is_active and not self.has_user(user):
entry = CourseAccessRole(user=user, role=self._role_name, course_id=self.course_key, org=self.org)
entry.save()
if hasattr(user, '_roles'):
del user._roles
def remove_users(self, *users):
"""
Remove the supplied django users from this role.
"""
groups = Group.objects.filter(name__in=self._group_names)
for group in groups:
group.user_set.remove(*users)
# remove cache
entries = CourseAccessRole.objects.filter(
user__in=users, role=self._role_name, org=self.org, course_id=self.course_key
)
entries.delete()
for user in users:
if hasattr(user, '_groups'):
del user._groups
if hasattr(user, '_roles'):
del user._roles
def users_with_role(self):
"""
Return a django QuerySet for all of the users with this role
"""
return User.objects.filter(groups__name__in=self._group_names)
entries = User.objects.filter(
courseaccessrole__role=self._role_name,
courseaccessrole__org=self.org,
courseaccessrole__course_id=self.course_key
)
return entries
class CourseRole(GroupBasedRole):
class CourseRole(RoleBase):
"""
A named role in a particular course
"""
def __init__(self, role, location, course_context=None):
def __init__(self, role, course_key):
"""
Location may be either a Location, a string, dict, or tuple which Location will accept
in its constructor, or a CourseLocator. Handle all these giving some preference to
the preferred naming.
Args:
course_key (CourseKey)
"""
# TODO: figure out how to make the group name generation lazy so it doesn't force the
# loc mapping?
self.location = Locator.to_locator_or_location(location)
self.role = role
# direct copy from auth.authz.get_all_course_role_groupnames will refactor to one impl asap
groupnames = []
super(CourseRole, self).__init__(role, course_key.org, course_key)
if isinstance(self.location, Location):
try:
groupnames.append(u'{0}_{1}'.format(role, self.location.course_id))
course_context = self.location.course_id # course_id is valid for translation
except InvalidLocationError: # will occur on old locations where location is not of category course
if course_context is None:
raise CourseContextRequired()
else:
groupnames.append(u'{0}_{1}'.format(role, course_context))
try:
locator = loc_mapper().translate_location_to_course_locator(course_context, self.location)
groupnames.append(u'{0}_{1}'.format(role, locator.package_id))
except (InvalidLocationError, ItemNotFoundError):
# if it's never been mapped, the auth won't be via the Locator syntax
pass
# least preferred legacy role_course format
groupnames.append(u'{0}_{1}'.format(role, self.location.course)) # pylint: disable=E1101, E1103
elif isinstance(self.location, CourseLocator):
groupnames.append(u'{0}_{1}'.format(role, self.location.package_id))
# handle old Location syntax
old_location = loc_mapper().translate_locator_to_location(self.location, get_course=True)
if old_location:
# the slashified version of the course_id (myu/mycourse/myrun)
groupnames.append(u'{0}_{1}'.format(role, old_location.course_id))
# add the least desirable but sometimes occurring format.
groupnames.append(u'{0}_{1}'.format(role, old_location.course)) # pylint: disable=E1101, E1103
super(CourseRole, self).__init__(groupnames)
@classmethod
def course_group_already_exists(self, course_key):
return CourseAccessRole.objects.filter(org=course_key.org, course_id=course_key).exists()
class OrgRole(GroupBasedRole):
class OrgRole(RoleBase):
"""
A named role in a particular org
A named role in a particular org independent of course
"""
def __init__(self, role, location):
location = Location(location)
super(OrgRole, self).__init__([u'{}_{}'.format(role, location.org)])
def __init__(self, role, org):
super(OrgRole, self).__init__(role, org)
class CourseStaffRole(CourseRole):
@@ -207,6 +176,7 @@ class CourseStaffRole(CourseRole):
class CourseInstructorRole(CourseRole):
"""A course Instructor"""
ROLE = 'instructor'
def __init__(self, *args, **kwargs):
super(CourseInstructorRole, self).__init__(self.ROLE, *args, **kwargs)
@@ -214,6 +184,7 @@ class CourseInstructorRole(CourseRole):
class CourseBetaTesterRole(CourseRole):
"""A course Beta Tester"""
ROLE = 'beta_testers'
def __init__(self, *args, **kwargs):
super(CourseBetaTesterRole, self).__init__(self.ROLE, *args, **kwargs)
@@ -230,11 +201,73 @@ class OrgInstructorRole(OrgRole):
super(OrgInstructorRole, self).__init__('instructor', *args, **kwargs)
class CourseCreatorRole(GroupBasedRole):
class CourseCreatorRole(RoleBase):
"""
This is the group of people who have permission to create new courses (we may want to eventually
make this an org based role).
"""
ROLE = "course_creator_group"
def __init__(self, *args, **kwargs):
super(CourseCreatorRole, self).__init__([self.ROLE], *args, **kwargs)
super(CourseCreatorRole, self).__init__(self.ROLE, *args, **kwargs)
class UserBasedRole(object):
"""
Backward mapping: given a user, manipulate the courses and roles
"""
def __init__(self, user, role):
"""
Create a UserBasedRole accessor: for a given user and role (e.g., "instructor")
"""
self.user = user
self.role = role
def has_course(self, course_key):
"""
Return whether the role's user has the configured role access to the passed course
"""
if not (self.user.is_authenticated() and self.user.is_active):
return False
# pylint: disable=protected-access
if not hasattr(self.user, '_roles'):
self.user._roles = list(
CourseAccessRole.objects.filter(user=self.user).all()
)
role = CourseAccessRole(user=self.user, role=self.role, course_id=course_key, org=course_key.org)
return role in self.user._roles
def add_course(self, *course_keys):
"""
Grant this object's user the object's role for the supplied courses
"""
if self.user.is_authenticated and self.user.is_active:
for course_key in course_keys:
entry = CourseAccessRole(user=self.user, role=self.role, course_id=course_key, org=course_key.org)
entry.save()
if hasattr(self.user, '_roles'):
del self.user._roles
else:
raise ValueError("user is not active. Cannot grant access to courses")
def remove_courses(self, *course_keys):
"""
Remove the supplied courses from this user's configured role.
"""
entries = CourseAccessRole.objects.filter(user=self.user, role=self.role, course_id__in=course_keys)
entries.delete()
if hasattr(self.user, '_roles'):
del self.user._roles
def courses_with_role(self):
"""
Return a django QuerySet for all of the courses with this user x role. You can access
any of these properties on each result record:
* user (will be self.user--thus uninteresting)
* org
* course_id
* role (will be self.role--thus uninteresting)
"""
return CourseAccessRole.objects.filter(role=self.role, user=self.user)

View File

@@ -4,13 +4,12 @@ Tests of student.roles
from django.test import TestCase
from xmodule.modulestore import Location
from courseware.tests.factories import UserFactory, StaffFactory, InstructorFactory
from student.tests.factories import AnonymousUserFactory
from student.roles import GlobalStaff, CourseRole, CourseStaffRole
from xmodule.modulestore.django import loc_mapper
from xmodule.modulestore.locator import BlockUsageLocator
from student.roles import GlobalStaff, CourseRole, CourseStaffRole, OrgStaffRole, OrgInstructorRole, \
CourseInstructorRole
from xmodule.modulestore.locations import SlashSeparatedCourseKey
class RolesTestCase(TestCase):
@@ -19,12 +18,13 @@ class RolesTestCase(TestCase):
"""
def setUp(self):
self.course = Location('i4x://edX/toy/course/2012_Fall')
self.course_id = SlashSeparatedCourseKey('edX', 'toy', '2012_Fall')
self.course_loc = self.course_id.make_usage_key('course', '2012_Fall')
self.anonymous_user = AnonymousUserFactory()
self.student = UserFactory()
self.global_staff = UserFactory(is_staff=True)
self.course_staff = StaffFactory(course=self.course)
self.course_instructor = InstructorFactory(course=self.course)
self.course_staff = StaffFactory(course=self.course_id)
self.course_instructor = InstructorFactory(course=self.course_id)
def test_global_staff(self):
self.assertFalse(GlobalStaff().has_user(self.student))
@@ -32,55 +32,124 @@ class RolesTestCase(TestCase):
self.assertFalse(GlobalStaff().has_user(self.course_instructor))
self.assertTrue(GlobalStaff().has_user(self.global_staff))
def test_group_name_case_insensitive(self):
uppercase_loc = "i4x://ORG/COURSE/course/NAME"
lowercase_loc = uppercase_loc.lower()
def test_group_name_case_sensitive(self):
uppercase_course_id = "ORG/COURSE/NAME"
lowercase_course_id = uppercase_course_id.lower()
uppercase_course_key = SlashSeparatedCourseKey.from_deprecated_string(uppercase_course_id)
lowercase_course_key = SlashSeparatedCourseKey.from_deprecated_string(lowercase_course_id)
lowercase_group = "role_org/course/name"
uppercase_group = lowercase_group.upper()
role = "role"
lowercase_user = UserFactory(groups=lowercase_group)
uppercase_user = UserFactory(groups=uppercase_group)
lowercase_user = UserFactory()
CourseRole(role, lowercase_course_key).add_users(lowercase_user)
uppercase_user = UserFactory()
CourseRole(role, uppercase_course_key).add_users(uppercase_user)
self.assertTrue(CourseRole("role", lowercase_loc).has_user(lowercase_user))
self.assertTrue(CourseRole("role", uppercase_loc).has_user(lowercase_user))
self.assertTrue(CourseRole("role", lowercase_loc).has_user(uppercase_user))
self.assertTrue(CourseRole("role", uppercase_loc).has_user(uppercase_user))
self.assertTrue(CourseRole(role, lowercase_course_key).has_user(lowercase_user))
self.assertFalse(CourseRole(role, uppercase_course_key).has_user(lowercase_user))
self.assertFalse(CourseRole(role, lowercase_course_key).has_user(uppercase_user))
self.assertTrue(CourseRole(role, uppercase_course_key).has_user(uppercase_user))
def test_course_role(self):
"""
Test that giving a user a course role enables access appropriately
"""
course_locator = loc_mapper().translate_location(
self.course.course_id, self.course, add_entry_if_missing=True
self.assertFalse(
CourseStaffRole(self.course_id).has_user(self.student),
"Student has premature access to {}".format(self.course_id)
)
CourseStaffRole(self.course_id).add_users(self.student)
self.assertTrue(
CourseStaffRole(self.course_id).has_user(self.student),
"Student doesn't have access to {}".format(unicode(self.course_id))
)
# remove access and confirm
CourseStaffRole(self.course_id).remove_users(self.student)
self.assertFalse(
CourseStaffRole(self.course_id).has_user(self.student),
"Student still has access to {}".format(self.course_id)
)
def test_org_role(self):
"""
Test that giving a user an org role enables access appropriately
"""
self.assertFalse(
OrgStaffRole(self.course_id.org).has_user(self.student),
"Student has premature access to {}".format(self.course_id.org)
)
OrgStaffRole(self.course_id.org).add_users(self.student)
self.assertTrue(
OrgStaffRole(self.course_id.org).has_user(self.student),
"Student doesn't have access to {}".format(unicode(self.course_id.org))
)
# remove access and confirm
OrgStaffRole(self.course_id.org).remove_users(self.student)
if hasattr(self.student, '_roles'):
del self.student._roles
self.assertFalse(
OrgStaffRole(self.course_id.org).has_user(self.student),
"Student still has access to {}".format(self.course_id.org)
)
def test_org_and_course_roles(self):
"""
Test that Org and course roles don't interfere with course roles or vice versa
"""
OrgInstructorRole(self.course_id.org).add_users(self.student)
CourseInstructorRole(self.course_id).add_users(self.student)
self.assertTrue(
OrgInstructorRole(self.course_id.org).has_user(self.student),
"Student doesn't have access to {}".format(unicode(self.course_id.org))
)
self.assertTrue(
CourseInstructorRole(self.course_id).has_user(self.student),
"Student doesn't have access to {}".format(unicode(self.course_id))
)
# remove access and confirm
OrgInstructorRole(self.course_id.org).remove_users(self.student)
self.assertFalse(
OrgInstructorRole(self.course_id.org).has_user(self.student),
"Student still has access to {}".format(self.course_id.org)
)
self.assertTrue(
CourseInstructorRole(self.course_id).has_user(self.student),
"Student doesn't have access to {}".format(unicode(self.course_id))
)
# ok now keep org role and get rid of course one
OrgInstructorRole(self.course_id.org).add_users(self.student)
CourseInstructorRole(self.course_id).remove_users(self.student)
self.assertTrue(
OrgInstructorRole(self.course_id.org).has_user(self.student),
"Student lost has access to {}".format(self.course_id.org)
)
self.assertFalse(
CourseStaffRole(course_locator).has_user(self.student),
"Student has premature access to {}".format(unicode(course_locator))
)
self.assertFalse(
CourseStaffRole(self.course).has_user(self.student),
"Student has premature access to {}".format(self.course.url())
)
CourseStaffRole(course_locator).add_users(self.student)
self.assertTrue(
CourseStaffRole(course_locator).has_user(self.student),
"Student doesn't have access to {}".format(unicode(course_locator))
)
self.assertTrue(
CourseStaffRole(self.course).has_user(self.student),
"Student doesn't have access to {}".format(unicode(self.course.url()))
)
# now try accessing something internal to the course
vertical_locator = BlockUsageLocator(
package_id=course_locator.package_id, branch='published', block_id='madeup'
)
vertical_location = self.course.replace(category='vertical', name='madeuptoo')
self.assertTrue(
CourseStaffRole(vertical_locator).has_user(self.student),
"Student doesn't have access to {}".format(unicode(vertical_locator))
)
self.assertTrue(
CourseStaffRole(vertical_location, course_context=self.course.course_id).has_user(self.student),
"Student doesn't have access to {}".format(unicode(vertical_location.url()))
CourseInstructorRole(self.course_id).has_user(self.student),
"Student doesn't have access to {}".format(unicode(self.course_id))
)
def test_get_user_for_role(self):
"""
test users_for_role
"""
role = CourseStaffRole(self.course_id)
role.add_users(self.student)
self.assertGreater(len(role.users_with_role()), 0)
def test_add_users_doesnt_add_duplicate_entry(self):
"""
Tests that calling add_users multiple times before a single call
to remove_users does not result in the user remaining in the group.
"""
role = CourseStaffRole(self.course_id)
role.add_users(self.student)
self.assertTrue(role.has_user(self.student))
# Call add_users a second time, then remove just once.
role.add_users(self.student)
role.remove_users(self.student)
self.assertFalse(role.has_user(self.student))

View File

@@ -20,6 +20,7 @@ from django.http import HttpResponse
from xmodule.modulestore.tests.factories import CourseFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from courseware.tests.tests import TEST_DATA_MIXED_MODULESTORE
from xmodule.modulestore.locations import SlashSeparatedCourseKey
from mock import Mock, patch, sentinel
@@ -30,9 +31,6 @@ from student.tests.factories import UserFactory, CourseModeFactory
import shoppingcart
COURSE_1 = 'edX/toy/2012_Fall'
COURSE_2 = 'edx/full/6.002_Spring_2012'
log = logging.getLogger(__name__)
@@ -203,38 +201,33 @@ class EnrollInCourseTest(TestCase):
def test_enrollment(self):
user = User.objects.create_user("joe", "joe@joe.com", "password")
course_id = "edX/Test101/2013"
course_id_partial = "edX/Test101"
course_id = SlashSeparatedCourseKey("edX", "Test101", "2013")
course_id_partial = SlashSeparatedCourseKey("edX", "Test101", None)
# Test basic enrollment
self.assertFalse(CourseEnrollment.is_enrolled(user, course_id))
self.assertFalse(CourseEnrollment.is_enrolled_by_partial(user,
course_id_partial))
self.assertFalse(CourseEnrollment.is_enrolled_by_partial(user, course_id_partial))
CourseEnrollment.enroll(user, course_id)
self.assertTrue(CourseEnrollment.is_enrolled(user, course_id))
self.assertTrue(CourseEnrollment.is_enrolled_by_partial(user,
course_id_partial))
self.assertTrue(CourseEnrollment.is_enrolled_by_partial(user, course_id_partial))
self.assert_enrollment_event_was_emitted(user, course_id)
# Enrolling them again should be harmless
CourseEnrollment.enroll(user, course_id)
self.assertTrue(CourseEnrollment.is_enrolled(user, course_id))
self.assertTrue(CourseEnrollment.is_enrolled_by_partial(user,
course_id_partial))
self.assertTrue(CourseEnrollment.is_enrolled_by_partial(user, course_id_partial))
self.assert_no_events_were_emitted()
# Now unenroll the user
CourseEnrollment.unenroll(user, course_id)
self.assertFalse(CourseEnrollment.is_enrolled(user, course_id))
self.assertFalse(CourseEnrollment.is_enrolled_by_partial(user,
course_id_partial))
self.assertFalse(CourseEnrollment.is_enrolled_by_partial(user, course_id_partial))
self.assert_unenrollment_event_was_emitted(user, course_id)
# Unenrolling them again should also be harmless
CourseEnrollment.unenroll(user, course_id)
self.assertFalse(CourseEnrollment.is_enrolled(user, course_id))
self.assertFalse(CourseEnrollment.is_enrolled_by_partial(user,
course_id_partial))
self.assertFalse(CourseEnrollment.is_enrolled_by_partial(user, course_id_partial))
self.assert_no_events_were_emitted()
# The enrollment record should still exist, just be inactive
@@ -257,26 +250,26 @@ class EnrollInCourseTest(TestCase):
self.assertFalse(self.mock_server_track.called)
self.mock_server_track.reset_mock()
def assert_enrollment_event_was_emitted(self, user, course_id):
def assert_enrollment_event_was_emitted(self, user, course_key):
"""Ensures an enrollment event was emitted since the last event related assertion"""
self.mock_server_track.assert_called_once_with(
sentinel.request,
'edx.course.enrollment.activated',
{
'course_id': course_id,
'course_id': course_key.to_deprecated_string(),
'user_id': user.pk,
'mode': 'honor'
}
)
self.mock_server_track.reset_mock()
def assert_unenrollment_event_was_emitted(self, user, course_id):
def assert_unenrollment_event_was_emitted(self, user, course_key):
"""Ensures an unenrollment event was emitted since the last event related assertion"""
self.mock_server_track.assert_called_once_with(
sentinel.request,
'edx.course.enrollment.deactivated',
{
'course_id': course_id,
'course_id': course_key.to_deprecated_string(),
'user_id': user.pk,
'mode': 'honor'
}
@@ -286,7 +279,7 @@ class EnrollInCourseTest(TestCase):
def test_enrollment_non_existent_user(self):
# Testing enrollment of newly unsaved user (i.e. no database entry)
user = User(username="rusty", email="rusty@fake.edx.org")
course_id = "edX/Test101/2013"
course_id = SlashSeparatedCourseKey("edX", "Test101", "2013")
self.assertFalse(CourseEnrollment.is_enrolled(user, course_id))
@@ -302,7 +295,7 @@ class EnrollInCourseTest(TestCase):
def test_enrollment_by_email(self):
user = User.objects.create(username="jack", email="jack@fake.edx.org")
course_id = "edX/Test101/2013"
course_id = SlashSeparatedCourseKey("edX", "Test101", "2013")
CourseEnrollment.enroll_by_email("jack@fake.edx.org", course_id)
self.assertTrue(CourseEnrollment.is_enrolled(user, course_id))
@@ -339,8 +332,8 @@ class EnrollInCourseTest(TestCase):
def test_enrollment_multiple_classes(self):
user = User(username="rusty", email="rusty@fake.edx.org")
course_id1 = "edX/Test101/2013"
course_id2 = "MITx/6.003z/2012"
course_id1 = SlashSeparatedCourseKey("edX", "Test101", "2013")
course_id2 = SlashSeparatedCourseKey("MITx", "6.003z", "2012")
CourseEnrollment.enroll(user, course_id1)
self.assert_enrollment_event_was_emitted(user, course_id1)
@@ -361,7 +354,7 @@ class EnrollInCourseTest(TestCase):
def test_activation(self):
user = User.objects.create(username="jack", email="jack@fake.edx.org")
course_id = "edX/Test101/2013"
course_id = SlashSeparatedCourseKey("edX", "Test101", "2013")
self.assertFalse(CourseEnrollment.is_enrolled(user, course_id))
# Creating an enrollment doesn't actually enroll a student
@@ -416,7 +409,7 @@ class PaidRegistrationTest(ModuleStoreTestCase):
@unittest.skipUnless(settings.FEATURES.get('ENABLE_SHOPPING_CART'), "Shopping Cart not enabled in settings")
def test_change_enrollment_add_to_cart(self):
request = self.req_factory.post(reverse('change_enrollment'), {'course_id': self.course.id,
request = self.req_factory.post(reverse('change_enrollment'), {'course_id': self.course.id.to_deprecated_string(),
'enrollment_action': 'add_to_cart'})
request.user = self.user
response = change_enrollment(request)