Commit 0b4e03e8 by dcadams

Merge pull request #66 from edx/feature-dcadams-usermanagement

Feature user-management autoenroll
parents 1c348a94 7d961bae
# -*- 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 field 'CourseEnrollmentAllowed.auto_enroll'
db.add_column('student_courseenrollmentallowed', 'auto_enroll',
self.gf('django.db.models.fields.BooleanField')(default=False),
keep_default=False)
def backwards(self, orm):
# Deleting field 'CourseEnrollmentAllowed.auto_enroll'
db.delete_column('student_courseenrollmentallowed', 'auto_enroll')
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.courseenrollment': {
'Meta': {'unique_together': "(('user', 'course_id'),)", 'object_name': 'CourseEnrollment'},
'course_id': ('django.db.models.fields.CharField', [], {'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'}),
'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': ('django.db.models.fields.CharField', [], {'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.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.testcenterregistration': {
'Meta': {'object_name': 'TestCenterRegistration'},
'accommodation_code': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}),
'accommodation_request': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '1024', 'blank': 'True'}),
'authorization_id': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'db_index': 'True'}),
'client_authorization_id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '20', 'db_index': 'True'}),
'confirmed_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}),
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}),
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}),
'eligibility_appointment_date_first': ('django.db.models.fields.DateField', [], {'db_index': 'True'}),
'eligibility_appointment_date_last': ('django.db.models.fields.DateField', [], {'db_index': 'True'}),
'exam_series_code': ('django.db.models.fields.CharField', [], {'max_length': '15', 'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'processed_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}),
'testcenter_user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['student.TestCenterUser']"}),
'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
'upload_error_message': ('django.db.models.fields.CharField', [], {'max_length': '512', 'blank': 'True'}),
'upload_status': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '20', 'blank': 'True'}),
'uploaded_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}),
'user_updated_at': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'})
},
'student.testcenteruser': {
'Meta': {'object_name': 'TestCenterUser'},
'address_1': ('django.db.models.fields.CharField', [], {'max_length': '40'}),
'address_2': ('django.db.models.fields.CharField', [], {'max_length': '40', 'blank': 'True'}),
'address_3': ('django.db.models.fields.CharField', [], {'max_length': '40', 'blank': 'True'}),
'candidate_id': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'db_index': 'True'}),
'city': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}),
'client_candidate_id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '50', 'db_index': 'True'}),
'company_name': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '50', 'blank': 'True'}),
'confirmed_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}),
'country': ('django.db.models.fields.CharField', [], {'max_length': '3', 'db_index': 'True'}),
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}),
'extension': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '8', 'blank': 'True'}),
'fax': ('django.db.models.fields.CharField', [], {'max_length': '35', 'blank': 'True'}),
'fax_country_code': ('django.db.models.fields.CharField', [], {'max_length': '3', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}),
'middle_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'phone': ('django.db.models.fields.CharField', [], {'max_length': '35'}),
'phone_country_code': ('django.db.models.fields.CharField', [], {'max_length': '3', 'db_index': 'True'}),
'postal_code': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '16', 'blank': 'True'}),
'processed_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}),
'salutation': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}),
'state': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '20', 'blank': 'True'}),
'suffix': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
'upload_error_message': ('django.db.models.fields.CharField', [], {'max_length': '512', 'blank': 'True'}),
'upload_status': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '20', 'blank': 'True'}),
'uploaded_at': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['auth.User']", 'unique': 'True'}),
'user_updated_at': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'})
},
'student.userprofile': {
'Meta': {'object_name': 'UserProfile', 'db_table': "'auth_userprofile'"},
'allow_certificate': ('django.db.models.fields.BooleanField', [], {'default': '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.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']
\ No newline at end of file
...@@ -662,6 +662,7 @@ class CourseEnrollmentAllowed(models.Model): ...@@ -662,6 +662,7 @@ class CourseEnrollmentAllowed(models.Model):
""" """
email = models.CharField(max_length=255, db_index=True) email = models.CharField(max_length=255, db_index=True)
course_id = models.CharField(max_length=255, db_index=True) course_id = models.CharField(max_length=255, db_index=True)
auto_enroll = models.BooleanField(default=0)
created = models.DateTimeField(auto_now_add=True, null=True, db_index=True) created = models.DateTimeField(auto_now_add=True, null=True, db_index=True)
......
...@@ -32,7 +32,7 @@ from student.models import (Registration, UserProfile, TestCenterUser, TestCente ...@@ -32,7 +32,7 @@ from student.models import (Registration, UserProfile, TestCenterUser, TestCente
TestCenterRegistration, TestCenterRegistrationForm, TestCenterRegistration, TestCenterRegistrationForm,
PendingNameChange, PendingEmailChange, PendingNameChange, PendingEmailChange,
CourseEnrollment, unique_id_for_user, CourseEnrollment, unique_id_for_user,
get_testcenter_registration) get_testcenter_registration, CourseEnrollmentAllowed)
from certificates.models import CertificateStatuses, certificate_status_for_student from certificates.models import CertificateStatuses, certificate_status_for_student
...@@ -264,7 +264,6 @@ def dashboard(request): ...@@ -264,7 +264,6 @@ def dashboard(request):
if not user.is_active: if not user.is_active:
message = render_to_string('registration/activate_account_notice.html', {'email': user.email}) message = render_to_string('registration/activate_account_notice.html', {'email': user.email})
# Global staff can see what courses errored on their dashboard # Global staff can see what courses errored on their dashboard
staff_access = False staff_access = False
errored_courses = {} errored_courses = {}
...@@ -454,7 +453,6 @@ def login_user(request, error=""): ...@@ -454,7 +453,6 @@ def login_user(request, error=""):
expires_time = time.time() + max_age expires_time = time.time() + max_age
expires = cookie_date(expires_time) expires = cookie_date(expires_time)
response.set_cookie(settings.EDXMKTG_COOKIE_NAME, response.set_cookie(settings.EDXMKTG_COOKIE_NAME,
'true', max_age=max_age, 'true', max_age=max_age,
expires=expires, domain=settings.SESSION_COOKIE_DOMAIN, expires=expires, domain=settings.SESSION_COOKIE_DOMAIN,
...@@ -698,7 +696,6 @@ def create_account(request, post_override=None): ...@@ -698,7 +696,6 @@ def create_account(request, post_override=None):
expires_time = time.time() + max_age expires_time = time.time() + max_age
expires = cookie_date(expires_time) expires = cookie_date(expires_time)
response.set_cookie(settings.EDXMKTG_COOKIE_NAME, response.set_cookie(settings.EDXMKTG_COOKIE_NAME,
'true', max_age=max_age, 'true', max_age=max_age,
expires=expires, domain=settings.SESSION_COOKIE_DOMAIN, expires=expires, domain=settings.SESSION_COOKIE_DOMAIN,
...@@ -708,7 +705,6 @@ def create_account(request, post_override=None): ...@@ -708,7 +705,6 @@ def create_account(request, post_override=None):
return response return response
def exam_registration_info(user, course): def exam_registration_info(user, course):
""" Returns a Registration object if the user is currently registered for a current """ Returns a Registration object if the user is currently registered for a current
exam of the course. Returns None if the user is not registered, or if there is no exam of the course. Returns None if the user is not registered, or if there is no
...@@ -849,7 +845,6 @@ def create_exam_registration(request, post_override=None): ...@@ -849,7 +845,6 @@ def create_exam_registration(request, post_override=None):
response_data['non_field_errors'] = form.non_field_errors() response_data['non_field_errors'] = form.non_field_errors()
return HttpResponse(json.dumps(response_data), mimetype="application/json") return HttpResponse(json.dumps(response_data), mimetype="application/json")
# only do the following if there is accommodation text to send, # only do the following if there is accommodation text to send,
# and a destination to which to send it. # and a destination to which to send it.
# TODO: still need to create the accommodation email templates # TODO: still need to create the accommodation email templates
...@@ -872,7 +867,6 @@ def create_exam_registration(request, post_override=None): ...@@ -872,7 +867,6 @@ def create_exam_registration(request, post_override=None):
# response_data['non_field_errors'] = [ 'Could not send accommodation e-mail.', ] # response_data['non_field_errors'] = [ 'Could not send accommodation e-mail.', ]
# return HttpResponse(json.dumps(response_data), mimetype="application/json") # return HttpResponse(json.dumps(response_data), mimetype="application/json")
js = {'success': True} js = {'success': True}
return HttpResponse(json.dumps(js), mimetype="application/json") return HttpResponse(json.dumps(js), mimetype="application/json")
...@@ -916,6 +910,16 @@ def activate_account(request, key): ...@@ -916,6 +910,16 @@ def activate_account(request, key):
if not r[0].user.is_active: if not r[0].user.is_active:
r[0].activate() r[0].activate()
already_active = False already_active = False
#Enroll student in any pending courses he/she may have if auto_enroll flag is set
student = User.objects.filter(id=r[0].user_id)
if student:
ceas = CourseEnrollmentAllowed.objects.filter(email=student[0].email)
for cea in ceas:
if cea.auto_enroll:
course_id = cea.course_id
enrollment, created = CourseEnrollment.objects.get_or_create(user_id=student[0].id, course_id=course_id)
resp = render_to_response("registration/activation_complete.html", {'user_logged_in': user_logged_in, 'already_active': already_active}) resp = render_to_response("registration/activation_complete.html", {'user_logged_in': user_logged_in, 'already_active': already_active})
return resp return resp
if len(r) == 0: if len(r) == 0:
......
'''
Unit tests for enrollment methods in views.py
'''
from django.test.utils import override_settings
from django.contrib.auth.models import Group, User
from django.core.urlresolvers import reverse
from courseware.access import _course_staff_group_name
from courseware.tests.tests import LoginEnrollmentTestCase, TEST_DATA_XML_MODULESTORE, get_user
from xmodule.modulestore.django import modulestore
import xmodule.modulestore.django
from student.models import CourseEnrollment, CourseEnrollmentAllowed
from instructor.views import get_and_clean_student_list
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
class TestInstructorEnrollsStudent(LoginEnrollmentTestCase):
'''
Check Enrollment/Unenrollment with/without auto-enrollment on activation
'''
def setUp(self):
self.full = modulestore().get_course("edX/full/6.002_Spring_2012")
self.toy = modulestore().get_course("edX/toy/2012_Fall")
#Create instructor and student accounts
self.instructor = 'instructor1@test.com'
self.student1 = 'student1@test.com'
self.student2 = 'student2@test.com'
self.password = 'foo'
self.create_account('it1', self.instructor, self.password)
self.create_account('st1', self.student1, self.password)
self.create_account('st2', self.student2, self.password)
self.activate_user(self.instructor)
self.activate_user(self.student1)
self.activate_user(self.student2)
def make_instructor(course):
group_name = _course_staff_group_name(course.location)
g = Group.objects.create(name=group_name)
g.user_set.add(get_user(self.instructor))
make_instructor(self.toy)
#Enroll Students
self.logout()
self.login(self.student1, self.password)
self.enroll(self.toy)
self.logout()
self.login(self.student2, self.password)
self.enroll(self.toy)
#Enroll Instructor
self.logout()
self.login(self.instructor, self.password)
self.enroll(self.toy)
def test_unenrollment(self):
'''
Do un-enrollment test
'''
course = self.toy
url = reverse('instructor_dashboard', kwargs={'course_id': course.id})
response = self.client.post(url, {'action': 'Unenroll multiple students', 'multiple_students': 'student1@test.com, student2@test.com'})
#Check the page output
self.assertContains(response, '<td>student1@test.com</td>')
self.assertContains(response, '<td>student2@test.com</td>')
self.assertContains(response, '<td>un-enrolled</td>')
#Check the enrollment table
user = User.objects.get(email='student1@test.com')
ce = CourseEnrollment.objects.filter(course_id=course.id, user=user)
self.assertEqual(0, len(ce))
user = User.objects.get(email='student2@test.com')
ce = CourseEnrollment.objects.filter(course_id=course.id, user=user)
self.assertEqual(0, len(ce))
def test_enrollment_new_student_autoenroll_on(self):
'''
Do auto-enroll on test
'''
#Run the Enroll students command
course = self.toy
url = reverse('instructor_dashboard', kwargs={'course_id': course.id})
response = self.client.post(url, {'action': 'Enroll multiple students', 'multiple_students': 'test1_1@student.com, test1_2@student.com', 'auto_enroll': 'on'})
#Check the page output
self.assertContains(response, '<td>test1_1@student.com</td>')
self.assertContains(response, '<td>test1_2@student.com</td>')
self.assertContains(response, '<td>user does not exist, enrollment allowed, pending with auto enrollment on</td>')
#Check the enrollmentallowed db entries
cea = CourseEnrollmentAllowed.objects.filter(email='test1_1@student.com', course_id=course.id)
self.assertEqual(1, cea[0].auto_enroll)
cea = CourseEnrollmentAllowed.objects.filter(email='test1_2@student.com', course_id=course.id)
self.assertEqual(1, cea[0].auto_enroll)
#Check there is no enrollment db entry other than for the setup instructor and students
ce = CourseEnrollment.objects.filter(course_id=course.id)
self.assertEqual(3, len(ce))
#Create and activate student accounts with same email
self.student1 = 'test1_1@student.com'
self.password = 'bar'
self.create_account('s1_1', self.student1, self.password)
self.activate_user(self.student1)
self.student2 = 'test1_2@student.com'
self.create_account('s1_2', self.student2, self.password)
self.activate_user(self.student2)
#Check students are enrolled
user = User.objects.get(email='test1_1@student.com')
ce = CourseEnrollment.objects.filter(course_id=course.id, user=user)
self.assertEqual(1, len(ce))
user = User.objects.get(email='test1_2@student.com')
ce = CourseEnrollment.objects.filter(course_id=course.id, user=user)
self.assertEqual(1, len(ce))
def test_enrollmemt_new_student_autoenroll_off(self):
'''
Do auto-enroll off test
'''
#Run the Enroll students command
course = self.toy
url = reverse('instructor_dashboard', kwargs={'course_id': course.id})
response = self.client.post(url, {'action': 'Enroll multiple students', 'multiple_students': 'test2_1@student.com, test2_2@student.com'})
#Check the page output
self.assertContains(response, '<td>test2_1@student.com</td>')
self.assertContains(response, '<td>test2_2@student.com</td>')
self.assertContains(response, '<td>user does not exist, enrollment allowed, pending with auto enrollment off</td>')
#Check the enrollmentallowed db entries
cea = CourseEnrollmentAllowed.objects.filter(email='test2_1@student.com', course_id=course.id)
self.assertEqual(0, cea[0].auto_enroll)
cea = CourseEnrollmentAllowed.objects.filter(email='test2_2@student.com', course_id=course.id)
self.assertEqual(0, cea[0].auto_enroll)
#Check there is no enrollment db entry other than for the setup instructor and students
ce = CourseEnrollment.objects.filter(course_id=course.id)
self.assertEqual(3, len(ce))
#Create and activate student accounts with same email
self.student = 'test2_1@student.com'
self.password = 'bar'
self.create_account('s2_1', self.student, self.password)
self.activate_user(self.student)
self.student = 'test2_2@student.com'
self.create_account('s2_2', self.student, self.password)
self.activate_user(self.student)
#Check students are not enrolled
user = User.objects.get(email='test2_1@student.com')
ce = CourseEnrollment.objects.filter(course_id=course.id, user=user)
self.assertEqual(0, len(ce))
user = User.objects.get(email='test2_2@student.com')
ce = CourseEnrollment.objects.filter(course_id=course.id, user=user)
self.assertEqual(0, len(ce))
def test_get_and_clean_student_list(self):
'''
Clean user input test
'''
string = "abc@test.com, def@test.com ghi@test.com \n \n jkl@test.com "
cleaned_string, cleaned_string_lc = get_and_clean_student_list(string)
self.assertEqual(cleaned_string, ['abc@test.com', 'def@test.com', 'ghi@test.com', 'jkl@test.com'])
...@@ -269,7 +269,6 @@ def instructor_dashboard(request, course_id): ...@@ -269,7 +269,6 @@ def instructor_dashboard(request, course_id):
except: except:
msg += "<font color='red'>Couldn't reset module state. </font>" msg += "<font color='red'>Couldn't reset module state. </font>"
elif "Get link to student's progress page" in action: elif "Get link to student's progress page" in action:
unique_student_identifier = request.POST.get('unique_student_identifier', '') unique_student_identifier = request.POST.get('unique_student_identifier', '')
try: try:
...@@ -312,6 +311,7 @@ def instructor_dashboard(request, course_id): ...@@ -312,6 +311,7 @@ def instructor_dashboard(request, course_id):
msg2, rg_stud_data = _do_remote_gradebook(request.user, course, 'get-membership') msg2, rg_stud_data = _do_remote_gradebook(request.user, course, 'get-membership')
datatable = {'header': ['Student email', 'Match?']} datatable = {'header': ['Student email', 'Match?']}
rg_students = [x['email'] for x in rg_stud_data['retdata']] rg_students = [x['email'] for x in rg_stud_data['retdata']]
def domatch(x): def domatch(x):
return 'yes' if x.email in rg_students else 'No' return 'yes' if x.email in rg_students else 'No'
datatable['data'] = [[x.email, domatch(x)] for x in stud_data['students']] datatable['data'] = [[x.email, domatch(x)] for x in stud_data['students']]
...@@ -347,7 +347,6 @@ def instructor_dashboard(request, course_id): ...@@ -347,7 +347,6 @@ def instructor_dashboard(request, course_id):
msg2, _ = _do_remote_gradebook(request.user, course, 'post-grades', files=files) msg2, _ = _do_remote_gradebook(request.user, course, 'post-grades', files=files)
msg += msg2 msg += msg2
#---------------------------------------- #----------------------------------------
# Admin # Admin
...@@ -413,6 +412,7 @@ def instructor_dashboard(request, course_id): ...@@ -413,6 +412,7 @@ def instructor_dashboard(request, course_id):
profkeys = ['name', 'language', 'location', 'year_of_birth', 'gender', 'level_of_education', profkeys = ['name', 'language', 'location', 'year_of_birth', 'gender', 'level_of_education',
'mailing_address', 'goals'] 'mailing_address', 'goals']
datatable = {'header': ['username', 'email'] + profkeys} datatable = {'header': ['username', 'email'] + profkeys}
def getdat(u): def getdat(u):
p = u.profile p = u.profile
return [u.username, u.email] + [getattr(p, x, '') for x in profkeys] return [u.username, u.email] + [getattr(p, x, '') for x in profkeys]
...@@ -421,9 +421,8 @@ def instructor_dashboard(request, course_id): ...@@ -421,9 +421,8 @@ def instructor_dashboard(request, course_id):
datatable['title'] = 'Student profile data for course %s' % course_id datatable['title'] = 'Student profile data for course %s' % course_id
return return_csv('profiledata_%s.csv' % course_id, datatable) return return_csv('profiledata_%s.csv' % course_id, datatable)
elif 'Download CSV of all responses to problem' in action: elif 'Download CSV of all responses to problem' in action:
problem_to_dump = request.POST.get('problem_to_dump','') problem_to_dump = request.POST.get('problem_to_dump', '')
if problem_to_dump[-4:] == ".xml": if problem_to_dump[-4:] == ".xml":
problem_to_dump = problem_to_dump[:-4] problem_to_dump = problem_to_dump[:-4]
...@@ -441,7 +440,7 @@ def instructor_dashboard(request, course_id): ...@@ -441,7 +440,7 @@ def instructor_dashboard(request, course_id):
if smdat: if smdat:
datatable = {'header': ['username', 'state']} datatable = {'header': ['username', 'state']}
datatable['data'] = [ [x.student.username, x.state] for x in smdat ] datatable['data'] = [[x.student.username, x.state] for x in smdat]
datatable['title'] = 'Student state for problem %s' % problem_to_dump datatable['title'] = 'Student state for problem %s' % problem_to_dump
return return_csv('student_state_from_%s.csv' % problem_to_dump, datatable) return return_csv('student_state_from_%s.csv' % problem_to_dump, datatable)
...@@ -478,7 +477,6 @@ def instructor_dashboard(request, course_id): ...@@ -478,7 +477,6 @@ def instructor_dashboard(request, course_id):
msg += _list_course_forum_members(course_id, rolename, datatable) msg += _list_course_forum_members(course_id, rolename, datatable)
track.views.server_track(request, 'list-{0}'.format(rolename), {}, page='idashboard') track.views.server_track(request, 'list-{0}'.format(rolename), {}, page='idashboard')
elif action == 'Remove forum admin': elif action == 'Remove forum admin':
uname = request.POST['forumadmin'] uname = request.POST['forumadmin']
msg += _update_forum_role_membership(uname, course, FORUM_ROLE_ADMINISTRATOR, FORUM_ROLE_REMOVE) msg += _update_forum_role_membership(uname, course, FORUM_ROLE_ADMINISTRATOR, FORUM_ROLE_REMOVE)
...@@ -536,35 +534,17 @@ def instructor_dashboard(request, course_id): ...@@ -536,35 +534,17 @@ def instructor_dashboard(request, course_id):
datatable['data'] = [[x.email] for x in ceaset] datatable['data'] = [[x.email] for x in ceaset]
datatable['title'] = action datatable['title'] = action
elif action == 'Enroll student': elif action == 'Enroll multiple students':
student = request.POST.get('enstudent', '') students = request.POST.get('multiple_students', '')
ret = _do_enroll_students(course, course_id, student) auto_enroll = bool(request.POST.get('auto_enroll'))
ret = _do_enroll_students(course, course_id, students, auto_enroll=auto_enroll)
datatable = ret['datatable'] datatable = ret['datatable']
elif action == 'Un-enroll student': elif action == 'Unenroll multiple students':
student = request.POST.get('enstudent', '')
datatable = {}
isok = False
cea = CourseEnrollmentAllowed.objects.filter(course_id=course_id, email=student)
if cea:
cea.delete()
msg += "Un-enrolled student with email '%s'" % student
isok = True
try:
nce = CourseEnrollment.objects.get(user=User.objects.get(email=student), course_id=course_id)
nce.delete()
msg += "Un-enrolled student with email '%s'" % student
except Exception as err:
if not isok:
msg += "Error! Failed to un-enroll student with email '%s'\n" % student
msg += str(err) + '\n'
elif action == 'Enroll multiple students':
students = request.POST.get('enroll_multiple', '') students = request.POST.get('multiple_students', '')
ret = _do_enroll_students(course, course_id, students) ret = _do_unenroll_students(course_id, students)
datatable = ret['datatable'] datatable = ret['datatable']
elif action == 'List sections available in remote gradebook': elif action == 'List sections available in remote gradebook':
...@@ -586,7 +566,6 @@ def instructor_dashboard(request, course_id): ...@@ -586,7 +566,6 @@ def instructor_dashboard(request, course_id):
ret = _do_enroll_students(course, course_id, students, overload=overload) ret = _do_enroll_students(course, course_id, students, overload=overload)
datatable = ret['datatable'] datatable = ret['datatable']
#---------------------------------------- #----------------------------------------
# psychometrics # psychometrics
...@@ -982,17 +961,11 @@ def grade_summary(request, course_id): ...@@ -982,17 +961,11 @@ def grade_summary(request, course_id):
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
# enrollment # enrollment
def _do_enroll_students(course, course_id, students, overload=False): def _do_enroll_students(course, course_id, students, overload=False, auto_enroll=False):
"""Do the actual work of enrolling multiple students, presented as a string """Do the actual work of enrolling multiple students, presented as a string
of emails separated by commas or returns""" of emails separated by commas or returns"""
new_students = split_by_comma_and_whitespace(students) new_students, new_students_lc = get_and_clean_student_list(students)
new_students = [str(s.strip()) for s in new_students]
new_students_lc = [x.lower() for x in new_students]
if '' in new_students:
new_students.remove('')
status = dict([x, 'unprocessed'] for x in new_students) status = dict([x, 'unprocessed'] for x in new_students)
if overload: # delete all but staff if overload: # delete all but staff
...@@ -1012,27 +985,35 @@ def _do_enroll_students(course, course_id, students, overload=False): ...@@ -1012,27 +985,35 @@ def _do_enroll_students(course, course_id, students, overload=False):
try: try:
user = User.objects.get(email=student) user = User.objects.get(email=student)
except User.DoesNotExist: except User.DoesNotExist:
# user not signed up yet, put in pending enrollment allowed table
if CourseEnrollmentAllowed.objects.filter(email=student, course_id=course_id): #User not signed up yet, put in pending enrollment allowed table
status[student] = 'user does not exist, enrollment already allowed, pending' cea = CourseEnrollmentAllowed.objects.filter(email=student, course_id=course_id)
#If enrollmentallowed already exists, update auto_enroll flag to however it was set in UI
#Will be 0 or 1 records as there is a unique key on email + course_id
if cea:
cea[0].auto_enroll = auto_enroll
cea[0].save()
status[student] = 'user does not exist, enrollment already allowed, pending with auto enrollment ' \
+ ('on' if auto_enroll else 'off')
continue continue
cea = CourseEnrollmentAllowed(email=student, course_id=course_id) cea = CourseEnrollmentAllowed(email=student, course_id=course_id, auto_enroll=auto_enroll)
cea.save() cea.save()
status[student] = 'user does not exist, enrollment allowed, pending' status[student] = 'user does not exist, enrollment allowed, pending with auto enrollment ' + ('on' if auto_enroll else 'off')
continue continue
if CourseEnrollment.objects.filter(user=user, course_id=course_id): if CourseEnrollment.objects.filter(user=user, course_id=course_id):
status[student] = 'already enrolled' status[student] = 'already enrolled'
continue continue
try: try:
nce = CourseEnrollment(user=user, course_id=course_id) ce = CourseEnrollment(user=user, course_id=course_id)
nce.save() ce.save()
status[student] = 'added' status[student] = 'added'
except: except:
status[student] = 'rejected' status[student] = 'rejected'
datatable = {'header': ['StudentEmail', 'action']} datatable = {'header': ['StudentEmail', 'action']}
datatable['data'] = [[x, status[x]] for x in status] datatable['data'] = [[x, status[x]] for x in sorted(status)]
datatable['title'] = 'Enrollment of students' datatable['title'] = 'Enrollment of students'
def sf(stat): def sf(stat):
...@@ -1044,39 +1025,69 @@ def _do_enroll_students(course, course_id, students, overload=False): ...@@ -1044,39 +1025,69 @@ def _do_enroll_students(course, course_id, students, overload=False):
return data return data
@ensure_csrf_cookie #Unenrollment
@cache_control(no_cache=True, no_store=True, must_revalidate=True) def _do_unenroll_students(course_id, students):
def enroll_students(request, course_id): """Do the actual work of un-enrolling multiple students, presented as a string
"""Allows a staff member to enroll students in a course. of emails separated by commas or returns"""
This is a short-term hack for Berkeley courses launching fall old_students, old_students_lc = get_and_clean_student_list(students)
2012. In the long term, we would like functionality like this, but status = dict([x, 'unprocessed'] for x in old_students)
we would like both the instructor and the student to agree. Right
now, this allows any instructor to add students to their course,
which we do not want.
It is poorly written and poorly tested, but it's designed to be for student in old_students:
stripped out.
"""
course = get_course_with_access(request.user, course_id, 'staff') isok = False
existing_students = [ce.user.email for ce in CourseEnrollment.objects.filter(course_id=course_id)] cea = CourseEnrollmentAllowed.objects.filter(course_id=course_id, email=student)
#Will be 0 or 1 records as there is a unique key on email + course_id
if cea:
cea[0].delete()
status[student] = "un-enrolled"
isok = True
new_students = request.POST.get('new_students') try:
ret = _do_enroll_students(course, course_id, new_students) user = User.objects.get(email=student)
added_students = ret['added'] except User.DoesNotExist:
rejected_students = ret['rejected'] continue
ce = CourseEnrollment.objects.filter(user=user, course_id=course_id)
#Will be 0 or 1 records as there is a unique key on user + course_id
if ce:
try:
ce[0].delete()
status[student] = "un-enrolled"
except Exception as err:
if not isok:
status[student] = "Error! Failed to un-enroll"
datatable = {'header': ['StudentEmail', 'action']}
datatable['data'] = [[x, status[x]] for x in sorted(status)]
datatable['title'] = 'Un-enrollment of students'
data = dict(datatable=datatable)
return data
return render_to_response("enroll_students.html", {'course': course_id,
'existing_students': existing_students,
'added_students': added_students,
'rejected_students': rejected_students,
'debug': new_students})
def get_and_clean_student_list(students):
"""
Separate out individual student email from the comma, or space separated string.
In:
students: string coming from the input text area
Return:
students: list of cleaned student emails
students_lc: list of lower case cleaned student emails
"""
students = split_by_comma_and_whitespace(students)
students = [str(s.strip()) for s in students]
students = [s for s in students if s != '']
students_lc = [x.lower() for x in students]
return students, students_lc
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
# answer distribution # answer distribution
def get_answers_distribution(request, course_id): def get_answers_distribution(request, course_id):
""" """
Get the distribution of answers for all graded problems in the course. Get the distribution of answers for all graded problems in the course.
...@@ -1168,5 +1179,5 @@ def dump_grading_context(course): ...@@ -1168,5 +1179,5 @@ def dump_grading_context(course):
msg += " %s (format=%s, Assignment=%s%s)\n" % (s.display_name, format, aname, notes) msg += " %s (format=%s, Assignment=%s%s)\n" % (s.display_name, format, aname, notes)
msg += "all descriptors:\n" msg += "all descriptors:\n"
msg += "length=%d\n" % len(gc['all_descriptors']) msg += "length=%d\n" % len(gc['all_descriptors'])
msg = '<pre>%s</pre>' % msg.replace('<','&lt;') msg = '<pre>%s</pre>' % msg.replace('<', '&lt;')
return msg return msg
...@@ -296,9 +296,6 @@ function goto( mode) ...@@ -296,9 +296,6 @@ function goto( mode)
<p> <p>
<input type="submit" name="action" value="List enrolled students"> <input type="submit" name="action" value="List enrolled students">
<input type="submit" name="action" value="List students who may enroll but may not have yet signed up"> <input type="submit" name="action" value="List students who may enroll but may not have yet signed up">
<p>
Student Email: <input type="text" name="enstudent"> <input type="submit" name="action" value="Un-enroll student">
<input type="submit" name="action" value="Enroll student">
<hr width="40%" style="align:left"> <hr width="40%" style="align:left">
%if settings.MITX_FEATURES.get('REMOTE_GRADEBOOK_URL','') and instructor_access: %if settings.MITX_FEATURES.get('REMOTE_GRADEBOOK_URL','') and instructor_access:
...@@ -320,9 +317,13 @@ function goto( mode) ...@@ -320,9 +317,13 @@ function goto( mode)
%endif %endif
<p>Add students: enter emails, separated by new lines or commas;</p> <p>Enroll or un-enroll one or many students: enter emails, separated by new lines or commas;</p>
<textarea rows="6" cols="70" name="enroll_multiple"></textarea> <textarea rows="6" cols="70" name="multiple_students"></textarea>
<p>
<input type="checkbox" name="auto_enroll"> Auto-enroll students when they activate
<input type="submit" name="action" value="Enroll multiple students"> <input type="submit" name="action" value="Enroll multiple students">
<p>
<input type="submit" name="action" value="Unenroll multiple students">
%endif %endif
......
...@@ -267,8 +267,6 @@ if settings.COURSEWARE_ENABLED: ...@@ -267,8 +267,6 @@ if settings.COURSEWARE_ENABLED:
'instructor.views.gradebook', name='gradebook'), 'instructor.views.gradebook', name='gradebook'),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/grade_summary$', url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/grade_summary$',
'instructor.views.grade_summary', name='grade_summary'), 'instructor.views.grade_summary', name='grade_summary'),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/enroll_students$',
'instructor.views.enroll_students', name='enroll_students'),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/staff_grading$', url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/staff_grading$',
'open_ended_grading.views.staff_grading', name='staff_grading'), 'open_ended_grading.views.staff_grading', name='staff_grading'),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/staff_grading/get_next$', url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/staff_grading/get_next$',
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment