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:
@@ -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']
|
||||
274
common/djangoapps/student/migrations/0035_access_roles.py
Normal file
274
common/djangoapps/student/migrations/0035_access_roles.py
Normal 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
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user