From d4a1e99a835aaa211a817f6441b0acac7da77332 Mon Sep 17 00:00:00 2001 From: cewing Date: Tue, 30 Dec 2014 14:25:06 -0800 Subject: [PATCH] MIT: CCX. Implement auto-enroll for CCX students Based loosely on course enrollment model Ensure that registered users are 'active' when they are enrolled in a POC Respect the 'auto enroll' and 'email students' checkboxes in the UI Add an auto_enroll flag to the PocFutureMembership model so we can automatically enroll non-users when they have registered and activated their account. Build a future enrollment using the auto_enroll value from the request so we can ensure that non-existent users can be auto-enrolled Ensure that any user added by way of the one-at-a-time UI is automatically auto-enrolled Update tests with email sending to use the flag from the request Provide api on the PocMembership object to auto-enroll a newly active member in this poc. This method will delete the passed PocFutureMembership object and will automatically enroll the user in the POC named in that future membership as well as the MOOC from which it was created Conditionally activate poc memberships that are pending when a new registree first activates their account. --- common/djangoapps/student/views.py | 10 +++ ..._poc_membership_future_auto_enroll_flag.py | 90 +++++++++++++++++++ lms/djangoapps/pocs/models.py | 16 ++++ lms/djangoapps/pocs/tests/test_views.py | 4 + lms/djangoapps/pocs/utils.py | 13 ++- lms/djangoapps/pocs/views.py | 14 ++- 6 files changed, 142 insertions(+), 5 deletions(-) create mode 100644 lms/djangoapps/pocs/migrations/0003_add_poc_membership_future_auto_enroll_flag.py diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index f990ca6060..8ec485a03a 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -1806,6 +1806,16 @@ def activate_account(request, key): if cea.auto_enroll: CourseEnrollment.enroll(student[0], cea.course_id) + # enroll student in any pending POCs he/she may have if auto_enroll flag is set + if settings.FEATURES.get('PERSONAL_ONLINE_COURSES'): + from pocs.models import PocMembership, PocFutureMembership + pfms = PocFutureMembership.objects.filter( + email=student[0].email + ) + for pfm in pfms: + if pfm.auto_enroll: + PocMembership.auto_enroll(student[0], pfm) + resp = render_to_response( "registration/activation_complete.html", { diff --git a/lms/djangoapps/pocs/migrations/0003_add_poc_membership_future_auto_enroll_flag.py b/lms/djangoapps/pocs/migrations/0003_add_poc_membership_future_auto_enroll_flag.py new file mode 100644 index 0000000000..27e844371d --- /dev/null +++ b/lms/djangoapps/pocs/migrations/0003_add_poc_membership_future_auto_enroll_flag.py @@ -0,0 +1,90 @@ +# -*- 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 'PocFutureMembership.auto_enroll' + db.add_column('pocs_pocfuturemembership', 'auto_enroll', + self.gf('django.db.models.fields.BooleanField')(default=False), + keep_default=False) + + + def backwards(self, orm): + # Deleting field 'PocFutureMembership.auto_enroll' + db.delete_column('pocs_pocfuturemembership', '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'}) + }, + 'pocs.personalonlinecourse': { + 'Meta': {'object_name': 'PersonalOnlineCourse'}, + 'coach': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}), + 'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}), + 'display_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'pocs.pocfieldoverride': { + 'Meta': {'unique_together': "(('poc', 'location', 'field'),)", 'object_name': 'PocFieldOverride'}, + 'field': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'location': ('xmodule_django.models.LocationKeyField', [], {'max_length': '255', 'db_index': 'True'}), + 'poc': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['pocs.PersonalOnlineCourse']"}), + 'value': ('django.db.models.fields.TextField', [], {'default': "'null'"}) + }, + 'pocs.pocfuturemembership': { + 'Meta': {'object_name': 'PocFutureMembership'}, + 'auto_enroll': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'email': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'poc': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['pocs.PersonalOnlineCourse']"}) + }, + 'pocs.pocmembership': { + 'Meta': {'object_name': 'PocMembership'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'poc': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['pocs.PersonalOnlineCourse']"}), + 'student': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + } + } + + complete_apps = ['pocs'] \ No newline at end of file diff --git a/lms/djangoapps/pocs/models.py b/lms/djangoapps/pocs/models.py index da06a9fc7b..19ef31e4eb 100644 --- a/lms/djangoapps/pocs/models.py +++ b/lms/djangoapps/pocs/models.py @@ -1,6 +1,7 @@ from django.contrib.auth.models import User from django.db import models +from student.models import CourseEnrollment, AlreadyEnrolledError from xmodule_django.models import CourseKeyField, LocationKeyField @@ -21,6 +22,20 @@ class PocMembership(models.Model): student = models.ForeignKey(User, db_index=True) active = models.BooleanField(default=False) + @classmethod + def auto_enroll(cls, student=None, future_membership=None): + assert student is not None and future_membership is not None + membership = cls( + poc=future_membership.poc, student=student, active=True + ) + try: + CourseEnrollment.enroll(student, future_membership.poc.course_id) + except AlreadyEnrolledError: + pass + else: + membership.save() + future_membership.delete() + class PocFutureMembership(models.Model): """ @@ -28,6 +43,7 @@ class PocFutureMembership(models.Model): """ poc = models.ForeignKey(PersonalOnlineCourse, db_index=True) email = models.CharField(max_length=255) + auto_enroll = models.BooleanField(default=0) class PocFieldOverride(models.Model): diff --git a/lms/djangoapps/pocs/tests/test_views.py b/lms/djangoapps/pocs/tests/test_views.py index bd6693328f..4f2edccffb 100644 --- a/lms/djangoapps/pocs/tests/test_views.py +++ b/lms/djangoapps/pocs/tests/test_views.py @@ -231,6 +231,7 @@ class TestCoachDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase): data = { 'enrollment-button': 'Enroll', 'student-ids': u','.join([student.email, ]), + 'email-students': 'Notify-students-by-email', } response = self.client.post(url, data=data, follow=True) self.assertEqual(response.status_code, 200) @@ -263,6 +264,7 @@ class TestCoachDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase): data = { 'enrollment-button': 'Unenroll', 'student-ids': u','.join([student.email, ]), + 'email-students': 'Notify-students-by-email', } response = self.client.post(url, data=data, follow=True) self.assertEqual(response.status_code, 200) @@ -292,6 +294,7 @@ class TestCoachDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase): data = { 'enrollment-button': 'Enroll', 'student-ids': u','.join([test_email, ]), + 'email-students': 'Notify-students-by-email', } response = self.client.post(url, data=data, follow=True) self.assertEqual(response.status_code, 200) @@ -323,6 +326,7 @@ class TestCoachDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase): data = { 'enrollment-button': 'Unenroll', 'student-ids': u','.join([test_email, ]), + 'email-students': 'Notify-students-by-email', } response = self.client.post(url, data=data, follow=True) self.assertEqual(response.status_code, 200) diff --git a/lms/djangoapps/pocs/utils.py b/lms/djangoapps/pocs/utils.py index f39017fcd3..9131c85342 100644 --- a/lms/djangoapps/pocs/utils.py +++ b/lms/djangoapps/pocs/utils.py @@ -65,7 +65,14 @@ def enroll_email(poc, student_email, auto_enroll=False, email_students=False, em if previous_state.user: if not previous_state.in_poc: user = User.objects.get(email=student_email) - membership = PocMembership(poc=poc, student=user) + membership = PocMembership( + poc=poc, student=user, active=True + ) + membership.save() + elif auto_enroll: + # activate existing memberships + membership = PocMembership.objects.get(student=user, poc=poc) + membership.active = True membership.save() if email_students: email_params['message'] = 'enrolled_enroll' @@ -73,7 +80,9 @@ def enroll_email(poc, student_email, auto_enroll=False, email_students=False, em email_params['full_name'] = previous_state.full_name send_mail_to_student(student_email, email_params) else: - membership = PocFutureMembership(poc=poc, email=student_email) + membership = PocFutureMembership( + poc=poc, auto_enroll=auto_enroll, email=student_email + ) membership.save() if email_students: email_params['message'] = 'allowed_enroll' diff --git a/lms/djangoapps/pocs/views.py b/lms/djangoapps/pocs/views.py index 964a79bc1d..62711aeb79 100644 --- a/lms/djangoapps/pocs/views.py +++ b/lms/djangoapps/pocs/views.py @@ -308,6 +308,8 @@ def poc_invite(request, course): action = request.POST.get('enrollment-button') identifiers_raw = request.POST.get('student-ids') identifiers = _split_input_list(identifiers_raw) + auto_enroll = True if 'auto-enroll' in request.POST else False + email_students = True if 'email-students' in request.POST else False for identifier in identifiers: user = None email = None @@ -320,9 +322,14 @@ def poc_invite(request, course): try: validate_email(email) if action == 'Enroll': - enroll_email(poc, email, email_students=True) + enroll_email( + poc, + email, + auto_enroll=auto_enroll, + email_students=email_students + ) if action == "Unenroll": - unenroll_email(poc, email, email_students=True) + unenroll_email(poc, email, email_students=email_students) except ValidationError: pass # maybe log this? url = reverse('poc_coach_dashboard', kwargs={'course_id': course.id}) @@ -350,7 +357,8 @@ def poc_student_management(request, course): validate_email(email) if action == 'add': # by decree, no emails sent to students added this way - enroll_email(poc, email, email_students=False) + # by decree, any students added this way are auto_enrolled + enroll_email(poc, email, auto_enroll=True, email_students=False) elif action == 'revoke': unenroll_email(poc, email, email_students=False) except ValidationError: