From 65c4f1df89dacf34774d2e0d2c0b86985cba1bf2 Mon Sep 17 00:00:00 2001
From: Muhammad Shoaib <muammad.shoaib@arbisoft.com>
Date: Fri, 22 May 2015 17:41:23 +0500
Subject: [PATCH] SOL-236 Manual Enrollments

---
 .../0050_auto__add_manualenrollmentaudit.py   | 216 +++++++++++++++
 common/djangoapps/student/models.py           |  83 +++++-
 common/djangoapps/student/views.py            |  13 +-
 lms/djangoapps/instructor/enrollment.py       |   7 +-
 .../paidcourse_enrollment_report.py           |  10 +-
 lms/djangoapps/instructor/tests/test_api.py   | 257 ++++++++++++++++--
 .../tests/test_api_email_localization.py      |   2 +-
 lms/djangoapps/instructor/views/api.py        |  72 ++++-
 .../instructor/views/instructor_dashboard.py  |   9 +-
 .../tests/test_tasks_helper.py                |  24 +-
 .../instructor_dashboard/membership.coffee    |   9 +
 .../instructor_dashboard_2/membership.html    |  11 +-
 12 files changed, 666 insertions(+), 47 deletions(-)
 create mode 100644 common/djangoapps/student/migrations/0050_auto__add_manualenrollmentaudit.py

diff --git a/common/djangoapps/student/migrations/0050_auto__add_manualenrollmentaudit.py b/common/djangoapps/student/migrations/0050_auto__add_manualenrollmentaudit.py
new file mode 100644
index 00000000000..90c3025d5ea
--- /dev/null
+++ b/common/djangoapps/student/migrations/0050_auto__add_manualenrollmentaudit.py
@@ -0,0 +1,216 @@
+# -*- coding: utf-8 -*-
+from south.utils import datetime_utils as 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 'ManualEnrollmentAudit'
+        db.create_table('student_manualenrollmentaudit', (
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('enrollment', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['student.CourseEnrollment'], null=True)),
+            ('enrolled_by', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], null=True)),
+            ('enrolled_email', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)),
+            ('time_stamp', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, null=True, blank=True)),
+            ('state_transition', self.gf('django.db.models.fields.CharField')(max_length=255)),
+            ('reason', self.gf('django.db.models.fields.TextField')(null=True)),
+        ))
+        db.send_create_signal('student', ['ManualEnrollmentAudit'])
+
+    def backwards(self, orm):
+        # Deleting model 'ManualEnrollmentAudit'
+        db.delete_table('student_manualenrollmentaudit')
+
+    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.dashboardconfiguration': {
+            'Meta': {'object_name': 'DashboardConfiguration'},
+            'change_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+            'changed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.PROTECT'}),
+            'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'recent_enrollment_time_delta': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'student.entranceexamconfiguration': {
+            'Meta': {'unique_together': "(('user', 'course_id'),)", 'object_name': 'EntranceExamConfiguration'},
+            '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'}),
+            'skip_entrance_exam': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+        },
+        'student.languageproficiency': {
+            'Meta': {'unique_together': "(('code', 'user_profile'),)", 'object_name': 'LanguageProficiency'},
+            'code': ('django.db.models.fields.CharField', [], {'max_length': '16'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'user_profile': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'language_proficiencies'", 'to': "orm['student.UserProfile']"})
+        },
+        'student.linkedinaddtoprofileconfiguration': {
+            'Meta': {'object_name': 'LinkedInAddToProfileConfiguration'},
+            'change_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+            'changed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.PROTECT'}),
+            'company_identifier': ('django.db.models.fields.TextField', [], {}),
+            'dashboard_tracking_code': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
+            'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'trk_partner_name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '10', 'blank': '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.manualenrollmentaudit': {
+            'Meta': {'object_name': 'ManualEnrollmentAudit'},
+            'enrolled_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'}),
+            'enrolled_email': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+            'enrollment': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['student.CourseEnrollment']", 'null': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'reason': ('django.db.models.fields.TextField', [], {'null': 'True'}),
+            'state_transition': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'time_stamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'blank': 'True'})
+        },
+        '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'}),
+            'bio': ('django.db.models.fields.CharField', [], {'max_length': '3000', 'null': 'True', 'blank': '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'}),
+            'profile_image_uploaded_at': ('django.db.models.fields.DateTimeField', [], {'null': '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.usersignupsource': {
+            'Meta': {'object_name': 'UserSignupSource'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'site': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+        },
+        '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']
diff --git a/common/djangoapps/student/models.py b/common/djangoapps/student/models.py
index 647a636d5a1..944a074ace5 100644
--- a/common/djangoapps/student/models.py
+++ b/common/djangoapps/student/models.py
@@ -20,7 +20,7 @@ from collections import defaultdict, OrderedDict
 import dogstats_wrapper as dog_stats_api
 from urllib import urlencode
 
-from django.utils.translation import ugettext as _, ugettext_lazy
+from django.utils.translation import ugettext_lazy as _
 from django.conf import settings
 from django.utils import timezone
 from django.contrib.auth.models import User
@@ -59,6 +59,26 @@ log = logging.getLogger(__name__)
 AUDIT_LOG = logging.getLogger("audit")
 SessionStore = import_module(settings.SESSION_ENGINE).SessionStore  # pylint: disable=invalid-name
 
+UNENROLLED_TO_ALLOWEDTOENROLL = 'from unenrolled to allowed to enroll'
+ALLOWEDTOENROLL_TO_ENROLLED = 'from allowed to enroll to enrolled'
+ENROLLED_TO_ENROLLED = 'from enrolled to enrolled'
+ENROLLED_TO_UNENROLLED = 'from enrolled to unenrolled'
+UNENROLLED_TO_ENROLLED = 'from unenrolled to enrolled'
+ALLOWEDTOENROLL_TO_UNENROLLED = 'from allowed to enroll to enrolled'
+UNENROLLED_TO_UNENROLLED = 'from unenrolled to unenrolled'
+DEFAULT_TRANSITION_STATE = 'N/A'
+
+TRANSITION_STATES = (
+    (UNENROLLED_TO_ALLOWEDTOENROLL, UNENROLLED_TO_ALLOWEDTOENROLL),
+    (ALLOWEDTOENROLL_TO_ENROLLED, ALLOWEDTOENROLL_TO_ENROLLED),
+    (ENROLLED_TO_ENROLLED, ENROLLED_TO_ENROLLED),
+    (ENROLLED_TO_UNENROLLED, ENROLLED_TO_UNENROLLED),
+    (UNENROLLED_TO_ENROLLED, UNENROLLED_TO_ENROLLED),
+    (ALLOWEDTOENROLL_TO_UNENROLLED, ALLOWEDTOENROLL_TO_UNENROLLED),
+    (UNENROLLED_TO_UNENROLLED, UNENROLLED_TO_UNENROLLED),
+    (DEFAULT_TRANSITION_STATE, DEFAULT_TRANSITION_STATE)
+)
+
 
 class AnonymousUserId(models.Model):
     """
@@ -1291,6 +1311,53 @@ class CourseEnrollment(models.Model):
         return CourseMode.is_verified_slug(self.mode)
 
 
+class ManualEnrollmentAudit(models.Model):
+    """
+    Table for tracking which enrollments were performed through manual enrollment.
+    """
+    enrollment = models.ForeignKey(CourseEnrollment, null=True)
+    enrolled_by = models.ForeignKey(User, null=True)
+    enrolled_email = models.CharField(max_length=255, db_index=True)
+    time_stamp = models.DateTimeField(auto_now_add=True, null=True)
+    state_transition = models.CharField(max_length=255, choices=TRANSITION_STATES)
+    reason = models.TextField(null=True)
+
+    @classmethod
+    def create_manual_enrollment_audit(cls, user, email, state_transition, reason, enrollment=None):
+        """
+        saves the student manual enrollment information
+        """
+        cls.objects.create(
+            enrolled_by=user,
+            enrolled_email=email,
+            state_transition=state_transition,
+            reason=reason,
+            enrollment=enrollment
+        )
+
+    @classmethod
+    def get_manual_enrollment_by_email(cls, email):
+        """
+        if matches returns the most recent entry in the table filtered by email else returns None.
+        """
+        try:
+            manual_enrollment = cls.objects.filter(enrolled_email=email).latest('time_stamp')
+        except cls.DoesNotExist:
+            manual_enrollment = None
+        return manual_enrollment
+
+    @classmethod
+    def get_manual_enrollment(cls, enrollment):
+        """
+        if matches returns the most recent entry in the table filtered by enrollment else returns None,
+        """
+        try:
+            manual_enrollment = cls.objects.filter(enrollment=enrollment).latest('time_stamp')
+        except cls.DoesNotExist:
+            manual_enrollment = None
+        return manual_enrollment
+
+
 class CourseEnrollmentAllowed(models.Model):
     """
     Table of users (specified by email address strings) who are allowed to enroll in a specified course.
@@ -1536,16 +1603,16 @@ class LinkedInAddToProfileConfiguration(ConfigurationModel):
     """
 
     MODE_TO_CERT_NAME = {
-        "honor": ugettext_lazy(u"{platform_name} Honor Code Certificate for {course_name}"),
-        "verified": ugettext_lazy(u"{platform_name} Verified Certificate for {course_name}"),
-        "professional": ugettext_lazy(u"{platform_name} Professional Certificate for {course_name}"),
-        "no-id-professional": ugettext_lazy(
+        "honor": _(u"{platform_name} Honor Code Certificate for {course_name}"),
+        "verified": _(u"{platform_name} Verified Certificate for {course_name}"),
+        "professional": _(u"{platform_name} Professional Certificate for {course_name}"),
+        "no-id-professional": _(
             u"{platform_name} Professional Certificate for {course_name}"
         ),
     }
 
     company_identifier = models.TextField(
-        help_text=ugettext_lazy(
+        help_text=_(
             u"The company identifier for the LinkedIn Add-to-Profile button "
             u"e.g 0_0dPSPyS070e0HsE9HNz_13_d11_"
         )
@@ -1558,7 +1625,7 @@ class LinkedInAddToProfileConfiguration(ConfigurationModel):
         max_length=10,
         default="",
         blank=True,
-        help_text=ugettext_lazy(
+        help_text=_(
             u"Short identifier for the LinkedIn partner used in the tracking code.  "
             u"(Example: 'edx')  "
             u"If no value is provided, tracking codes will not be sent to LinkedIn."
@@ -1699,5 +1766,5 @@ class LanguageProficiency(models.Model):
         max_length=16,
         blank=False,
         choices=settings.ALL_LANGUAGES,
-        help_text=ugettext_lazy("The ISO 639-1 language code for this language.")
+        help_text=_("The ISO 639-1 language code for this language.")
     )
diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py
index 55329227cb3..cadfafefe15 100644
--- a/common/djangoapps/student/views.py
+++ b/common/djangoapps/student/views.py
@@ -55,7 +55,7 @@ from student.models import (
     PendingEmailChange, CourseEnrollment, unique_id_for_user,
     CourseEnrollmentAllowed, UserStanding, LoginFailures,
     create_comments_service_user, PasswordHistory, UserSignupSource,
-    DashboardConfiguration, LinkedInAddToProfileConfiguration)
+    DashboardConfiguration, LinkedInAddToProfileConfiguration, ManualEnrollmentAudit, ALLOWEDTOENROLL_TO_ENROLLED)
 from student.forms import AccountCreationForm, PasswordResetFormNoActive
 
 from verify_student.models import SoftwareSecurePhotoVerification, MidcourseReverificationWindow
@@ -1783,7 +1783,16 @@ def activate_account(request, key):
             ceas = CourseEnrollmentAllowed.objects.filter(email=student[0].email)
             for cea in ceas:
                 if cea.auto_enroll:
-                    CourseEnrollment.enroll(student[0], cea.course_id)
+                    enrollment = CourseEnrollment.enroll(student[0], cea.course_id)
+                    manual_enrollment_audit = ManualEnrollmentAudit.get_manual_enrollment_by_email(student[0].email)
+                    if manual_enrollment_audit is not None:
+                        # get the enrolled by user and reason from the ManualEnrollmentAudit table.
+                        # then create a new ManualEnrollmentAudit table entry for the same email
+                        # different transition state.
+                        ManualEnrollmentAudit.create_manual_enrollment_audit(
+                            manual_enrollment_audit.enrolled_by, student[0].email, ALLOWEDTOENROLL_TO_ENROLLED,
+                            manual_enrollment_audit.reason, enrollment
+                        )
 
             # enroll student in any pending CCXs he/she may have if auto_enroll flag is set
             if settings.FEATURES.get('CUSTOM_COURSES_EDX'):
diff --git a/lms/djangoapps/instructor/enrollment.py b/lms/djangoapps/instructor/enrollment.py
index f85d5bc1395..1cd3df73edc 100644
--- a/lms/djangoapps/instructor/enrollment.py
+++ b/lms/djangoapps/instructor/enrollment.py
@@ -100,7 +100,7 @@ def enroll_email(course_id, student_email, auto_enroll=False, email_students=Fal
         representing state before and after the action.
     """
     previous_state = EmailEnrollmentState(course_id, student_email)
-
+    enrollment_obj = None
     if previous_state.user:
         # if the student is currently unenrolled, don't enroll them in their
         # previous mode
@@ -108,7 +108,7 @@ def enroll_email(course_id, student_email, auto_enroll=False, email_students=Fal
         if previous_state.enrollment:
             course_mode = previous_state.mode
 
-        CourseEnrollment.enroll_by_email(student_email, course_id, course_mode)
+        enrollment_obj = CourseEnrollment.enroll_by_email(student_email, course_id, course_mode)
         if email_students:
             email_params['message'] = 'enrolled_enroll'
             email_params['email_address'] = student_email
@@ -125,7 +125,7 @@ def enroll_email(course_id, student_email, auto_enroll=False, email_students=Fal
 
     after_state = EmailEnrollmentState(course_id, student_email)
 
-    return previous_state, after_state
+    return previous_state, after_state, enrollment_obj
 
 
 def unenroll_email(course_id, student_email, email_students=False, email_params=None, language=None):
@@ -141,7 +141,6 @@ def unenroll_email(course_id, student_email, email_students=False, email_params=
         representing state before and after the action.
     """
     previous_state = EmailEnrollmentState(course_id, student_email)
-
     if previous_state.enrollment:
         CourseEnrollment.unenroll_by_email(student_email, course_id)
         if email_students:
diff --git a/lms/djangoapps/instructor/paidcourse_enrollment_report.py b/lms/djangoapps/instructor/paidcourse_enrollment_report.py
index da6f9b5f4b8..1d319312d7e 100644
--- a/lms/djangoapps/instructor/paidcourse_enrollment_report.py
+++ b/lms/djangoapps/instructor/paidcourse_enrollment_report.py
@@ -11,7 +11,7 @@ from instructor.enrollment_report import BaseAbstractEnrollmentReportProvider
 from microsite_configuration import microsite
 from shoppingcart.models import RegistrationCodeRedemption, PaidCourseRegistration, CouponRedemption, OrderItem, \
     InvoiceTransaction
-from student.models import CourseEnrollment
+from student.models import CourseEnrollment, ManualEnrollmentAudit
 
 
 class PaidCourseEnrollmentReportProvider(BaseAbstractEnrollmentReportProvider):
@@ -56,7 +56,13 @@ class PaidCourseEnrollmentReportProvider(BaseAbstractEnrollmentReportProvider):
             elif paid_course_reg_item is not None:
                 enrollment_source = _('Credit Card - Individual')
             else:
-                enrollment_source = _('Manually Enrolled')
+                manual_enrollment = ManualEnrollmentAudit.get_manual_enrollment(course_enrollment)
+                if manual_enrollment is not None:
+                    enrollment_source = _(
+                        'manually enrolled by user_id {user_id}, enrollment state transition: {transition}'
+                    ).format(user_id=manual_enrollment.enrolled_by_id, transition=manual_enrollment.state_transition)
+                else:
+                    enrollment_source = _('Manually Enrolled')
 
         enrollment_date = course_enrollment.created.strftime("%B %d, %Y")
         currently_enrolled = course_enrollment.is_active
diff --git a/lms/djangoapps/instructor/tests/test_api.py b/lms/djangoapps/instructor/tests/test_api.py
index 6fd54fa409d..8c79173692c 100644
--- a/lms/djangoapps/instructor/tests/test_api.py
+++ b/lms/djangoapps/instructor/tests/test_api.py
@@ -43,7 +43,10 @@ from shoppingcart.models import (
     InvoiceTransaction)
 from shoppingcart.pdf import PDFInvoice
 from student.models import (
-    CourseEnrollment, CourseEnrollmentAllowed, NonExistentCourseError
+    CourseEnrollment, CourseEnrollmentAllowed, NonExistentCourseError,
+    ManualEnrollmentAudit, UNENROLLED_TO_ENROLLED, ENROLLED_TO_UNENROLLED,
+    ALLOWEDTOENROLL_TO_UNENROLLED, ENROLLED_TO_ENROLLED, UNENROLLED_TO_ALLOWEDTOENROLL,
+    UNENROLLED_TO_UNENROLLED, ALLOWEDTOENROLL_TO_ENROLLED
 )
 from student.tests.factories import UserFactory, CourseModeFactory, AdminFactory
 from student.roles import CourseBetaTesterRole, CourseSalesAdminRole, CourseFinanceAdminRole, CourseInstructorRole
@@ -185,7 +188,8 @@ class TestInstructorAPIDenyLevels(ModuleStoreTestCase, LoginEnrollmentTestCase):
 
         # Endpoints that only Staff or Instructors can access
         self.staff_level_endpoints = [
-            ('students_update_enrollment', {'identifiers': 'foo@example.org', 'action': 'enroll'}),
+            ('students_update_enrollment',
+             {'identifiers': 'foo@example.org', 'action': 'enroll'}),
             ('get_grading_config', {}),
             ('get_students_features', {}),
             ('get_distribution', {}),
@@ -355,6 +359,10 @@ class TestInstructorAPIBulkAccountCreationAndEnrollment(ModuleStoreTestCase, Log
         self.assertEquals(len(data['warnings']), 0)
         self.assertEquals(len(data['general_errors']), 0)
 
+        manual_enrollments = ManualEnrollmentAudit.objects.all()
+        self.assertEqual(manual_enrollments.count(), 1)
+        self.assertEqual(manual_enrollments[0].state_transition, UNENROLLED_TO_ENROLLED)
+
         # test the log for email that's send to new created user.
         info_log.assert_called_with('email sent to new created user at %s', 'test_student@example.com')
 
@@ -372,6 +380,10 @@ class TestInstructorAPIBulkAccountCreationAndEnrollment(ModuleStoreTestCase, Log
         self.assertEquals(len(data['warnings']), 0)
         self.assertEquals(len(data['general_errors']), 0)
 
+        manual_enrollments = ManualEnrollmentAudit.objects.all()
+        self.assertEqual(manual_enrollments.count(), 1)
+        self.assertEqual(manual_enrollments[0].state_transition, UNENROLLED_TO_ENROLLED)
+
         # test the log for email that's send to new created user.
         info_log.assert_called_with('email sent to new created user at %s', 'test_student@example.com')
 
@@ -391,6 +403,10 @@ class TestInstructorAPIBulkAccountCreationAndEnrollment(ModuleStoreTestCase, Log
         self.assertEquals(len(data['warnings']), 0)
         self.assertEquals(len(data['general_errors']), 0)
 
+        manual_enrollments = ManualEnrollmentAudit.objects.all()
+        self.assertEqual(manual_enrollments.count(), 1)
+        self.assertEqual(manual_enrollments[0].state_transition, UNENROLLED_TO_ENROLLED)
+
         # test the log for email that's send to new created user.
         info_log.assert_called_with(
             u"user already exists with username '%s' and email '%s'",
@@ -409,6 +425,9 @@ class TestInstructorAPIBulkAccountCreationAndEnrollment(ModuleStoreTestCase, Log
         self.assertNotEquals(len(data['general_errors']), 0)
         self.assertEquals(data['general_errors'][0]['response'], 'Make sure that the file you upload is in CSV format with no extraneous characters or rows.')
 
+        manual_enrollments = ManualEnrollmentAudit.objects.all()
+        self.assertEqual(manual_enrollments.count(), 0)
+
     def test_bad_file_upload_type(self):
         """
         Try uploading some non-CSV file and verify that it is rejected
@@ -420,6 +439,9 @@ class TestInstructorAPIBulkAccountCreationAndEnrollment(ModuleStoreTestCase, Log
         self.assertNotEquals(len(data['general_errors']), 0)
         self.assertEquals(data['general_errors'][0]['response'], 'Could not read uploaded file.')
 
+        manual_enrollments = ManualEnrollmentAudit.objects.all()
+        self.assertEqual(manual_enrollments.count(), 0)
+
     def test_insufficient_data(self):
         """
         Try uploading a CSV file which does not have the exact four columns of data
@@ -434,6 +456,9 @@ class TestInstructorAPIBulkAccountCreationAndEnrollment(ModuleStoreTestCase, Log
         self.assertEquals(len(data['general_errors']), 1)
         self.assertEquals(data['general_errors'][0]['response'], 'Data in row #1 must have exactly four columns: email, username, full name, and country')
 
+        manual_enrollments = ManualEnrollmentAudit.objects.all()
+        self.assertEqual(manual_enrollments.count(), 0)
+
     def test_invalid_email_in_csv(self):
         """
         Test failure case of a poorly formatted email field
@@ -449,6 +474,9 @@ class TestInstructorAPIBulkAccountCreationAndEnrollment(ModuleStoreTestCase, Log
         self.assertEquals(len(data['general_errors']), 0)
         self.assertEquals(data['row_errors'][0]['response'], 'Invalid email {0}.'.format('test_student.example.com'))
 
+        manual_enrollments = ManualEnrollmentAudit.objects.all()
+        self.assertEqual(manual_enrollments.count(), 0)
+
     @patch('instructor.views.api.log.info')
     def test_csv_user_exist_and_not_enrolled(self, info_log):
         """
@@ -465,6 +493,9 @@ class TestInstructorAPIBulkAccountCreationAndEnrollment(ModuleStoreTestCase, Log
             u'NotEnrolledStudent',
             self.course.id
         )
+        manual_enrollments = ManualEnrollmentAudit.objects.all()
+        self.assertEqual(manual_enrollments.count(), 1)
+        self.assertTrue(manual_enrollments[0].state_transition, UNENROLLED_TO_ENROLLED)
 
     def test_user_with_already_existing_email_in_csv(self):
         """
@@ -485,6 +516,10 @@ class TestInstructorAPIBulkAccountCreationAndEnrollment(ModuleStoreTestCase, Log
         user = User.objects.get(email='test_student@example.com')
         self.assertTrue(CourseEnrollment.is_enrolled(user, self.course.id))
 
+        manual_enrollments = ManualEnrollmentAudit.objects.all()
+        self.assertEqual(manual_enrollments.count(), 1)
+        self.assertTrue(manual_enrollments[0].state_transition, UNENROLLED_TO_ENROLLED)
+
     def test_user_with_already_existing_username_in_csv(self):
         """
         If the username already exists (but not the email),
@@ -516,6 +551,9 @@ class TestInstructorAPIBulkAccountCreationAndEnrollment(ModuleStoreTestCase, Log
         self.assertNotEquals(len(data['general_errors']), 0)
         self.assertEquals(data['general_errors'][0]['response'], 'File is not attached.')
 
+        manual_enrollments = ManualEnrollmentAudit.objects.all()
+        self.assertEqual(manual_enrollments.count(), 0)
+
     def test_raising_exception_in_auto_registration_and_enrollment_case(self):
         """
         Test that exceptions are handled well
@@ -533,6 +571,9 @@ class TestInstructorAPIBulkAccountCreationAndEnrollment(ModuleStoreTestCase, Log
         self.assertNotEquals(len(data['row_errors']), 0)
         self.assertEquals(data['row_errors'][0]['response'], 'NonExistentCourseError')
 
+        manual_enrollments = ManualEnrollmentAudit.objects.all()
+        self.assertEqual(manual_enrollments.count(), 0)
+
     def test_generate_unique_password(self):
         """
         generate_unique_password should generate a unique password string that excludes certain characters.
@@ -558,6 +599,9 @@ class TestInstructorAPIBulkAccountCreationAndEnrollment(ModuleStoreTestCase, Log
         self.assertTrue(User.objects.filter(username='test_student_2', email='test_student2@example.com').exists())
         self.assertFalse(User.objects.filter(email='test_student3@example.com').exists())
 
+        manual_enrollments = ManualEnrollmentAudit.objects.all()
+        self.assertEqual(manual_enrollments.count(), 2)
+
     @patch.object(instructor.views.api, 'generate_random_string',
                   Mock(side_effect=['first', 'first', 'second']))
     def test_generate_unique_password_no_reuse(self):
@@ -575,6 +619,9 @@ class TestInstructorAPIBulkAccountCreationAndEnrollment(ModuleStoreTestCase, Log
         response = self.client.post(self.url, {'students_list': uploaded_file})
         self.assertEquals(response.status_code, 403)
 
+        manual_enrollments = ManualEnrollmentAudit.objects.all()
+        self.assertEqual(manual_enrollments.count(), 0)
+
 
 @attr('shard_1')
 @ddt.ddt
@@ -658,7 +705,8 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase):
 
     def test_invalid_username(self):
         url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id.to_deprecated_string()})
-        response = self.client.post(url, {'identifiers': 'percivaloctavius', 'action': 'enroll', 'email_students': False})
+        response = self.client.post(url,
+                                    {'identifiers': 'percivaloctavius', 'action': 'enroll', 'email_students': False})
         self.assertEqual(response.status_code, 200)
 
         # test the response data
@@ -678,7 +726,8 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase):
 
     def test_enroll_with_username(self):
         url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id.to_deprecated_string()})
-        response = self.client.post(url, {'identifiers': self.notenrolled_student.username, 'action': 'enroll', 'email_students': False})
+        response = self.client.post(url, {'identifiers': self.notenrolled_student.username, 'action': 'enroll',
+                                          'email_students': False})
         self.assertEqual(response.status_code, 200)
 
         # test the response data
@@ -703,13 +752,16 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase):
                 }
             ]
         }
-
+        manual_enrollments = ManualEnrollmentAudit.objects.all()
+        self.assertEqual(manual_enrollments.count(), 1)
+        self.assertEqual(manual_enrollments[0].state_transition, UNENROLLED_TO_ENROLLED)
         res_json = json.loads(response.content)
         self.assertEqual(res_json, expected)
 
     def test_enroll_without_email(self):
         url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id.to_deprecated_string()})
-        response = self.client.post(url, {'identifiers': self.notenrolled_student.email, 'action': 'enroll', 'email_students': False})
+        response = self.client.post(url, {'identifiers': self.notenrolled_student.email, 'action': 'enroll',
+                                          'email_students': False})
         print "type(self.notenrolled_student.email): {}".format(type(self.notenrolled_student.email))
         self.assertEqual(response.status_code, 200)
 
@@ -740,6 +792,9 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase):
             ]
         }
 
+        manual_enrollments = ManualEnrollmentAudit.objects.all()
+        self.assertEqual(manual_enrollments.count(), 1)
+        self.assertEqual(manual_enrollments[0].state_transition, UNENROLLED_TO_ENROLLED)
         res_json = json.loads(response.content)
         self.assertEqual(res_json, expected)
 
@@ -811,6 +866,9 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase):
         params = {'identifiers': self.notregistered_email, 'action': 'enroll', 'email_students': True}
         environ = {'wsgi.url_scheme': protocol}
         response = self.client.post(url, params, **environ)
+        manual_enrollments = ManualEnrollmentAudit.objects.all()
+        self.assertEqual(manual_enrollments.count(), 1)
+        self.assertEqual(manual_enrollments[0].state_transition, UNENROLLED_TO_ALLOWEDTOENROLL)
         self.assertEqual(response.status_code, 200)
 
         # Check the outbox
@@ -839,10 +897,14 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase):
         environ = {'wsgi.url_scheme': protocol}
         response = self.client.post(url, params, **environ)
 
+        manual_enrollments = ManualEnrollmentAudit.objects.all()
+        self.assertEqual(manual_enrollments.count(), 1)
+        self.assertEqual(manual_enrollments[0].state_transition, UNENROLLED_TO_ALLOWEDTOENROLL)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(
             mail.outbox[0].body,
-            "Dear student,\n\nYou have been invited to join {display_name} at edx.org by a member of the course staff.\n\n"
+            "Dear student,\n\nYou have been invited to join {display_name}"
+            " at edx.org by a member of the course staff.\n\n"
             "To finish your registration, please visit {proto}://{site}/register and fill out the registration form "
             "making sure to use robot-not-an-email-yet@robot.org in the E-mail field.\n"
             "You can then enroll in {display_name}.\n\n----\n"
@@ -867,12 +929,17 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase):
             mail.outbox[0].subject,
             u'You have been invited to register for {}'.format(self.course.display_name)
         )
+        manual_enrollments = ManualEnrollmentAudit.objects.all()
+        self.assertEqual(manual_enrollments.count(), 1)
+        self.assertEqual(manual_enrollments[0].state_transition, UNENROLLED_TO_ALLOWEDTOENROLL)
         self.assertEqual(
             mail.outbox[0].body,
-            "Dear student,\n\nYou have been invited to join {display_name} at edx.org by a member of the course staff.\n\n"
+            "Dear student,\n\nYou have been invited to join {display_name}"
+            " at edx.org by a member of the course staff.\n\n"
             "To finish your registration, please visit {proto}://{site}/register and fill out the registration form "
             "making sure to use robot-not-an-email-yet@robot.org in the E-mail field.\n"
-            "Once you have registered and activated your account, you will see {display_name} listed on your dashboard.\n\n----\n"
+            "Once you have registered and activated your account,"
+            " you will see {display_name} listed on your dashboard.\n\n----\n"
             "This email was automatically sent from edx.org to robot-not-an-email-yet@robot.org".format(
                 proto=protocol, site=self.site_name, display_name=self.course.display_name
             )
@@ -880,7 +947,8 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase):
 
     def test_unenroll_without_email(self):
         url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id.to_deprecated_string()})
-        response = self.client.post(url, {'identifiers': self.enrolled_student.email, 'action': 'unenroll', 'email_students': False})
+        response = self.client.post(url, {'identifiers': self.enrolled_student.email, 'action': 'unenroll',
+                                          'email_students': False})
         print "type(self.enrolled_student.email): {}".format(type(self.enrolled_student.email))
         self.assertEqual(response.status_code, 200)
 
@@ -911,6 +979,9 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase):
             ]
         }
 
+        manual_enrollments = ManualEnrollmentAudit.objects.all()
+        self.assertEqual(manual_enrollments.count(), 1)
+        self.assertEqual(manual_enrollments[0].state_transition, ENROLLED_TO_UNENROLLED)
         res_json = json.loads(response.content)
         self.assertEqual(res_json, expected)
 
@@ -919,7 +990,8 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase):
 
     def test_unenroll_with_email(self):
         url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id.to_deprecated_string()})
-        response = self.client.post(url, {'identifiers': self.enrolled_student.email, 'action': 'unenroll', 'email_students': True})
+        response = self.client.post(url, {'identifiers': self.enrolled_student.email, 'action': 'unenroll',
+                                          'email_students': True})
         print "type(self.enrolled_student.email): {}".format(type(self.enrolled_student.email))
         self.assertEqual(response.status_code, 200)
 
@@ -950,6 +1022,9 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase):
             ]
         }
 
+        manual_enrollments = ManualEnrollmentAudit.objects.all()
+        self.assertEqual(manual_enrollments.count(), 1)
+        self.assertEqual(manual_enrollments[0].state_transition, ENROLLED_TO_UNENROLLED)
         res_json = json.loads(response.content)
         self.assertEqual(res_json, expected)
 
@@ -972,7 +1047,8 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase):
 
     def test_unenroll_with_email_allowed_student(self):
         url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id.to_deprecated_string()})
-        response = self.client.post(url, {'identifiers': self.allowed_email, 'action': 'unenroll', 'email_students': True})
+        response = self.client.post(url,
+                                    {'identifiers': self.allowed_email, 'action': 'unenroll', 'email_students': True})
         print "type(self.allowed_email): {}".format(type(self.allowed_email))
         self.assertEqual(response.status_code, 200)
 
@@ -999,6 +1075,9 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase):
             ]
         }
 
+        manual_enrollments = ManualEnrollmentAudit.objects.all()
+        self.assertEqual(manual_enrollments.count(), 1)
+        self.assertEqual(manual_enrollments[0].state_transition, ALLOWEDTOENROLL_TO_UNENROLLED)
         res_json = json.loads(response.content)
         self.assertEqual(res_json, expected)
 
@@ -1054,7 +1133,8 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase):
         url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id.to_deprecated_string()})
         # Try with marketing site enabled
         with patch.dict('django.conf.settings.FEATURES', {'ENABLE_MKTG_SITE': True}):
-            response = self.client.post(url, {'identifiers': self.notregistered_email, 'action': 'enroll', 'email_students': True})
+            response = self.client.post(url, {'identifiers': self.notregistered_email, 'action': 'enroll',
+                                              'email_students': True})
 
         self.assertEqual(response.status_code, 200)
         self.assertEqual(
@@ -1087,7 +1167,8 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase):
 
         self.assertEqual(
             mail.outbox[0].body,
-            "Dear student,\n\nYou have been invited to join {display_name} at edx.org by a member of the course staff.\n\n"
+            "Dear student,\n\nYou have been invited to join {display_name}"
+            " at edx.org by a member of the course staff.\n\n"
             "To access the course visit {proto}://{site}{course_path} and login.\n\n----\n"
             "This email was automatically sent from edx.org to robot-not-an-email-yet@robot.org".format(
                 display_name=self.course.display_name,
@@ -1115,8 +1196,143 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase):
         course_enrollment = CourseEnrollment.objects.get(
             user=self.enrolled_student, course_id=self.course.id
         )
+        manual_enrollments = ManualEnrollmentAudit.objects.all()
+        self.assertEqual(manual_enrollments.count(), 1)
+        self.assertEqual(manual_enrollments[0].state_transition, ENROLLED_TO_ENROLLED)
         self.assertEqual(course_enrollment.mode, u"verified")
 
+    def create_paid_course(self):
+        """
+        create paid course mode.
+        """
+        paid_course = CourseFactory.create()
+        CourseModeFactory.create(course_id=paid_course.id, min_price=50)
+        CourseInstructorRole(paid_course.id).add_users(self.instructor)
+        return paid_course
+
+    def test_reason_field_should_not_be_empty(self):
+        """
+        test to check that reason field should not be empty when
+        manually enrolling the students for the paid courses.
+        """
+        paid_course = self.create_paid_course()
+        url = reverse('students_update_enrollment', kwargs={'course_id': paid_course.id.to_deprecated_string()})
+        params = {'identifiers': self.notregistered_email, 'action': 'enroll', 'email_students': False,
+                  'auto_enroll': False}
+        response = self.client.post(url, params)
+        manual_enrollments = ManualEnrollmentAudit.objects.all()
+        self.assertEqual(manual_enrollments.count(), 0)
+
+        # test the response data
+        expected = {
+            "action": "enroll",
+            "auto_enroll": False,
+            "results": [
+                {
+                    "error": True
+                }
+            ]
+        }
+        res_json = json.loads(response.content)
+        self.assertEqual(res_json, expected)
+
+    def test_unenrolled_allowed_to_enroll_user(self):
+        """
+        test to unenroll allow to enroll user.
+        """
+        paid_course = self.create_paid_course()
+        url = reverse('students_update_enrollment', kwargs={'course_id': paid_course.id.to_deprecated_string()})
+        params = {'identifiers': self.notregistered_email, 'action': 'enroll', 'email_students': False,
+                  'auto_enroll': False, 'reason': 'testing..'}
+        response = self.client.post(url, params)
+        manual_enrollments = ManualEnrollmentAudit.objects.all()
+        self.assertEqual(manual_enrollments.count(), 1)
+        self.assertEqual(manual_enrollments[0].state_transition, UNENROLLED_TO_ALLOWEDTOENROLL)
+        self.assertEqual(response.status_code, 200)
+
+        # now registered the user
+        UserFactory(email=self.notregistered_email)
+        url = reverse('students_update_enrollment', kwargs={'course_id': paid_course.id.to_deprecated_string()})
+        params = {'identifiers': self.notregistered_email, 'action': 'enroll', 'email_students': False,
+                  'auto_enroll': False, 'reason': 'testing'}
+        response = self.client.post(url, params)
+        manual_enrollments = ManualEnrollmentAudit.objects.all()
+        self.assertEqual(manual_enrollments.count(), 2)
+        self.assertEqual(manual_enrollments[1].state_transition, ALLOWEDTOENROLL_TO_ENROLLED)
+        self.assertEqual(response.status_code, 200)
+
+        # test the response data
+        expected = {
+            "action": "enroll",
+            "auto_enroll": False,
+            "results": [
+                {
+                    "identifier": self.notregistered_email,
+                    "before": {
+                        "enrollment": False,
+                        "auto_enroll": False,
+                        "user": True,
+                        "allowed": True,
+                    },
+                    "after": {
+                        "enrollment": True,
+                        "auto_enroll": False,
+                        "user": True,
+                        "allowed": True,
+                    }
+                }
+            ]
+        }
+        res_json = json.loads(response.content)
+        self.assertEqual(res_json, expected)
+
+    def test_unenrolled_already_not_enrolled_user(self):
+        """
+        test unenrolled user already not enrolled in a course.
+        """
+        paid_course = self.create_paid_course()
+        course_enrollment = CourseEnrollment.objects.filter(
+            user__email=self.notregistered_email, course_id=paid_course.id
+        )
+        self.assertEqual(course_enrollment.count(), 0)
+
+        url = reverse('students_update_enrollment', kwargs={'course_id': paid_course.id.to_deprecated_string()})
+        params = {'identifiers': self.notregistered_email, 'action': 'unenroll', 'email_students': False,
+                  'auto_enroll': False, 'reason': 'testing'}
+
+        response = self.client.post(url, params)
+        self.assertEqual(response.status_code, 200)
+
+        # test the response data
+        expected = {
+            "action": "unenroll",
+            "auto_enroll": False,
+            "results": [
+                {
+                    "identifier": self.notregistered_email,
+                    "before": {
+                        "enrollment": False,
+                        "auto_enroll": False,
+                        "user": False,
+                        "allowed": False,
+                    },
+                    "after": {
+                        "enrollment": False,
+                        "auto_enroll": False,
+                        "user": False,
+                        "allowed": False,
+                    }
+                }
+            ]
+        }
+
+        manual_enrollments = ManualEnrollmentAudit.objects.all()
+        self.assertEqual(manual_enrollments.count(), 1)
+        self.assertEqual(manual_enrollments[0].state_transition, UNENROLLED_TO_UNENROLLED)
+
+        res_json = json.loads(response.content)
+        self.assertEqual(res_json, expected)
+
     def test_unenroll_and_enroll_verified(self):
         """
         Test that unenrolling and enrolling a student from a verified track
@@ -1152,6 +1368,7 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase):
             'identifiers': user.email,
             'action': action,
             'email_students': True,
+            'reason': 'change user enrollment'
         }
         response = self.client.post(url, params)
         self.assertEqual(response.status_code, 200)
@@ -1382,7 +1599,9 @@ class TestInstructorAPIBulkBetaEnrollment(ModuleStoreTestCase, LoginEnrollmentTe
     def test_enroll_with_email_not_registered(self):
         # User doesn't exist
         url = reverse('bulk_beta_modify_access', kwargs={'course_id': self.course.id.to_deprecated_string()})
-        response = self.client.post(url, {'identifiers': self.notregistered_email, 'action': 'add', 'email_students': True})
+        response = self.client.post(url,
+                                    {'identifiers': self.notregistered_email, 'action': 'add', 'email_students': True,
+                                     'reason': 'testing'})
         self.assertEqual(response.status_code, 200)
         # test the response data
         expected = {
@@ -1403,7 +1622,9 @@ class TestInstructorAPIBulkBetaEnrollment(ModuleStoreTestCase, LoginEnrollmentTe
 
     def test_remove_without_email(self):
         url = reverse('bulk_beta_modify_access', kwargs={'course_id': self.course.id.to_deprecated_string()})
-        response = self.client.post(url, {'identifiers': self.beta_tester.email, 'action': 'remove', 'email_students': False})
+        response = self.client.post(url,
+                                    {'identifiers': self.beta_tester.email, 'action': 'remove', 'email_students': False,
+                                     'reason': 'testing'})
         self.assertEqual(response.status_code, 200)
 
         # Works around a caching bug which supposedly can't happen in prod. The instance here is not ==
@@ -1431,7 +1652,9 @@ class TestInstructorAPIBulkBetaEnrollment(ModuleStoreTestCase, LoginEnrollmentTe
 
     def test_remove_with_email(self):
         url = reverse('bulk_beta_modify_access', kwargs={'course_id': self.course.id.to_deprecated_string()})
-        response = self.client.post(url, {'identifiers': self.beta_tester.email, 'action': 'remove', 'email_students': True})
+        response = self.client.post(url,
+                                    {'identifiers': self.beta_tester.email, 'action': 'remove', 'email_students': True,
+                                     'reason': 'testing'})
         self.assertEqual(response.status_code, 200)
 
         # Works around a caching bug which supposedly can't happen in prod. The instance here is not ==
diff --git a/lms/djangoapps/instructor/tests/test_api_email_localization.py b/lms/djangoapps/instructor/tests/test_api_email_localization.py
index 6d39d5f0573..d9fd87df932 100644
--- a/lms/djangoapps/instructor/tests/test_api_email_localization.py
+++ b/lms/djangoapps/instructor/tests/test_api_email_localization.py
@@ -42,7 +42,7 @@ class TestInstructorAPIEnrollmentEmailLocalization(ModuleStoreTestCase):
         Update the current student enrollment status.
         """
         url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id.to_deprecated_string()})
-        args = {'identifiers': student_email, 'email_students': 'true', 'action': action}
+        args = {'identifiers': student_email, 'email_students': 'true', 'action': action, 'reason': 'testing'}
         response = self.client.post(url, args)
         return response
 
diff --git a/lms/djangoapps/instructor/views/api.py b/lms/djangoapps/instructor/views/api.py
index 27d8fc3b0ce..5c349dc8031 100644
--- a/lms/djangoapps/instructor/views/api.py
+++ b/lms/djangoapps/instructor/views/api.py
@@ -31,7 +31,10 @@ import urllib
 import decimal
 from student import auth
 from student.roles import GlobalStaff, CourseSalesAdminRole, CourseFinanceAdminRole
-from util.file import store_uploaded_file, course_and_time_based_filename_generator, FileValidationException, UniversalNewlineIterator
+from util.file import (
+    store_uploaded_file, course_and_time_based_filename_generator,
+    FileValidationException, UniversalNewlineIterator
+)
 from util.json_request import JsonResponse
 from instructor.views.instructor_task_helpers import extract_email_features, extract_task_features
 
@@ -59,7 +62,10 @@ from shoppingcart.models import (
 )
 from student.models import (
     CourseEnrollment, unique_id_for_user, anonymous_id_for_user,
-    UserProfile, Registration, EntranceExamConfiguration
+    UserProfile, Registration, EntranceExamConfiguration,
+    ManualEnrollmentAudit, UNENROLLED_TO_ALLOWEDTOENROLL, ALLOWEDTOENROLL_TO_ENROLLED,
+    ENROLLED_TO_ENROLLED, ENROLLED_TO_UNENROLLED, UNENROLLED_TO_ENROLLED,
+    UNENROLLED_TO_UNENROLLED, ALLOWEDTOENROLL_TO_UNENROLLED, DEFAULT_TRANSITION_STATE
 )
 import instructor_task.api
 from instructor_task.api_helper import AlreadyRunningError
@@ -413,7 +419,11 @@ def register_and_enroll_students(request, course_id):  # pylint: disable=too-man
 
                     # make sure user is enrolled in course
                     if not CourseEnrollment.is_enrolled(user, course_id):
-                        CourseEnrollment.enroll(user, course_id)
+                        enrollment_obj = CourseEnrollment.enroll(user, course_id)
+                        reason = 'Enrolling via csv upload'
+                        ManualEnrollmentAudit.create_manual_enrollment_audit(
+                            request.user, email, UNENROLLED_TO_ENROLLED, reason, enrollment_obj
+                        )
                         log.info(
                             u'user %s enrolled in the course %s',
                             username,
@@ -427,7 +437,11 @@ def register_and_enroll_students(request, course_id):  # pylint: disable=too-man
                     password = generate_unique_password(generated_passwords)
 
                     try:
-                        create_and_enroll_user(email, username, name, country, password, course_id)
+                        enrollment_obj = create_and_enroll_user(email, username, name, country, password, course_id)
+                        reason = 'Enrolling via csv upload'
+                        ManualEnrollmentAudit.create_manual_enrollment_audit(
+                            request.user, email, UNENROLLED_TO_ENROLLED, reason, enrollment_obj
+                        )
                     except IntegrityError:
                         row_errors.append({
                             'username': username, 'email': email, 'response': _('Username {user} already exists.').format(user=username)})
@@ -496,7 +510,7 @@ def create_and_enroll_user(email, username, name, country, password, course_id):
     profile.save()
 
     # try to enroll the user in this course
-    CourseEnrollment.enroll(user, course_id)
+    return CourseEnrollment.enroll(user, course_id)
 
 
 @ensure_csrf_cookie
@@ -546,6 +560,18 @@ def students_update_enrollment(request, course_id):
     identifiers = _split_input_list(identifiers_raw)
     auto_enroll = request.POST.get('auto_enroll') in ['true', 'True', True]
     email_students = request.POST.get('email_students') in ['true', 'True', True]
+    is_white_label = CourseMode.is_white_label(course_id)
+    reason = request.POST.get('reason')
+    if is_white_label:
+        if not reason:
+            return JsonResponse(
+                {
+                    'action': action,
+                    'results': [{'error': True}],
+                    'auto_enroll': auto_enroll,
+                }, status=400)
+    enrollment_obj = None
+    state_transition = DEFAULT_TRANSITION_STATE
 
     email_params = {}
     if email_students:
@@ -571,15 +597,44 @@ def students_update_enrollment(request, course_id):
             # validity (obviously, cannot check if email actually /exists/,
             # simply that it is plausibly valid)
             validate_email(email)  # Raises ValidationError if invalid
-
             if action == 'enroll':
-                before, after = enroll_email(
+                before, after, enrollment_obj = enroll_email(
                     course_id, email, auto_enroll, email_students, email_params, language=language
                 )
+                before_enrollment = before.to_dict()['enrollment']
+                before_user_registered = before.to_dict()['user']
+                before_allowed = before.to_dict()['allowed']
+                after_enrollment = after.to_dict()['enrollment']
+                after_allowed = after.to_dict()['allowed']
+
+                if before_user_registered:
+                    if after_enrollment:
+                        if before_enrollment:
+                            state_transition = ENROLLED_TO_ENROLLED
+                        else:
+                            if before_allowed:
+                                state_transition = ALLOWEDTOENROLL_TO_ENROLLED
+                            else:
+                                state_transition = UNENROLLED_TO_ENROLLED
+                else:
+                    if after_allowed:
+                        state_transition = UNENROLLED_TO_ALLOWEDTOENROLL
+
             elif action == 'unenroll':
                 before, after = unenroll_email(
                     course_id, email, email_students, email_params, language=language
                 )
+                before_enrollment = before.to_dict()['enrollment']
+                before_allowed = before.to_dict()['allowed']
+
+                if before_enrollment:
+                    state_transition = ENROLLED_TO_UNENROLLED
+                else:
+                    if before_allowed:
+                        state_transition = ALLOWEDTOENROLL_TO_UNENROLLED
+                    else:
+                        state_transition = UNENROLLED_TO_UNENROLLED
+
             else:
                 return HttpResponseBadRequest(strip_tags(
                     "Unrecognized action '{}'".format(action)
@@ -604,6 +659,9 @@ def students_update_enrollment(request, course_id):
             })
 
         else:
+            ManualEnrollmentAudit.create_manual_enrollment_audit(
+                request.user, email, state_transition, reason, enrollment_obj
+            )
             results.append({
                 'identifier': identifier,
                 'before': before.to_dict(),
diff --git a/lms/djangoapps/instructor/views/instructor_dashboard.py b/lms/djangoapps/instructor/views/instructor_dashboard.py
index ab93882d3eb..2c44060ca02 100644
--- a/lms/djangoapps/instructor/views/instructor_dashboard.py
+++ b/lms/djangoapps/instructor/views/instructor_dashboard.py
@@ -71,9 +71,11 @@ def instructor_dashboard_2(request, course_id):
     if not access['staff']:
         raise Http404()
 
+    is_white_label = CourseMode.is_white_label(course_key)
+
     sections = [
         _section_course_info(course, access),
-        _section_membership(course, access),
+        _section_membership(course, access, is_white_label),
         _section_cohort_management(course, access),
         _section_student_admin(course, access),
         _section_data_download(course, access),
@@ -92,8 +94,6 @@ def instructor_dashboard_2(request, course_id):
             unicode(course_key), len(paid_modes)
         )
 
-    is_white_label = CourseMode.is_white_label(course_key)
-
     if (settings.FEATURES.get('INDIVIDUAL_DUE_DATES') and access['instructor']):
         sections.insert(3, _section_extensions(course))
 
@@ -321,7 +321,7 @@ def _section_course_info(course, access):
     return section_data
 
 
-def _section_membership(course, access):
+def _section_membership(course, access, is_white_label):
     """ Provide data for the corresponding dashboard section """
     course_key = course.id
     ccx_enabled = settings.FEATURES.get('CUSTOM_COURSES_EDX', False) and course.enable_ccx
@@ -330,6 +330,7 @@ def _section_membership(course, access):
         'section_display_name': _('Membership'),
         'access': access,
         'ccx_is_enabled': ccx_enabled,
+        'is_white_label': is_white_label,
         'enroll_button_url': reverse('students_update_enrollment', kwargs={'course_id': unicode(course_key)}),
         'unenroll_button_url': reverse('students_update_enrollment', kwargs={'course_id': unicode(course_key)}),
         'upload_student_csv_button_url': reverse('register_and_enroll_students', kwargs={'course_id': unicode(course_key)}),
diff --git a/lms/djangoapps/instructor_task/tests/test_tasks_helper.py b/lms/djangoapps/instructor_task/tests/test_tasks_helper.py
index c93853ce51f..fa6beedf64c 100644
--- a/lms/djangoapps/instructor_task/tests/test_tasks_helper.py
+++ b/lms/djangoapps/instructor_task/tests/test_tasks_helper.py
@@ -27,7 +27,7 @@ from openedx.core.djangoapps.user_api.partition_schemes import RandomUserPartiti
 from shoppingcart.models import Order, PaidCourseRegistration, CourseRegistrationCode, Invoice, \
     CourseRegistrationCodeInvoiceItem, InvoiceTransaction
 from student.tests.factories import UserFactory
-from student.models import CourseEnrollment
+from student.models import CourseEnrollment, ManualEnrollmentAudit, ALLOWEDTOENROLL_TO_ENROLLED
 from verify_student.tests.factories import SoftwareSecurePhotoVerificationFactory
 from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
 from xmodule.partitions.partitions import Group, UserPartition
@@ -321,6 +321,28 @@ class TestInstructorDetailedEnrollmentReport(TestReportMixin, InstructorTaskCour
         self._verify_cell_data_in_csv(student.username, 'Enrollment Source', 'Credit Card - Individual')
         self._verify_cell_data_in_csv(student.username, 'Payment Status', 'purchased')
 
+    def test_student_manually_enrolled_in_detailed_enrollment_source(self):
+        """
+        test to check the manually enrolled user enrollment report status
+        and enrollment source.
+        """
+        student = UserFactory()
+        enrollment = CourseEnrollment.enroll(student, self.course.id)
+        ManualEnrollmentAudit.create_manual_enrollment_audit(
+            self.instructor, student.email, ALLOWEDTOENROLL_TO_ENROLLED,
+            'manually enrolling unenrolled user', enrollment
+        )
+
+        task_input = {'features': []}
+        with patch('instructor_task.tasks_helper._get_current_task'):
+            result = upload_enrollment_report(None, None, self.course.id, task_input, 'generating_enrollment_report')
+
+        enrollment_source = u'manually enrolled by user_id {user_id}, enrollment state transition: {transition}'.format(
+            user_id=self.instructor.id, transition=ALLOWEDTOENROLL_TO_ENROLLED)  # pylint: disable=no-member
+        self.assertDictContainsSubset({'attempted': 1, 'succeeded': 1, 'failed': 0}, result)
+        self._verify_cell_data_in_csv(student.username, 'Enrollment Source', enrollment_source)
+        self._verify_cell_data_in_csv(student.username, 'Payment Status', 'TBD')
+
     def test_student_used_enrollment_code_for_course_enrollment(self):
         """
         test to check the user enrollment source and payment status in the
diff --git a/lms/static/coffee/src/instructor_dashboard/membership.coffee b/lms/static/coffee/src/instructor_dashboard/membership.coffee
index 56bd752330e..4dc86bc8492 100644
--- a/lms/static/coffee/src/instructor_dashboard/membership.coffee
+++ b/lms/static/coffee/src/instructor_dashboard/membership.coffee
@@ -367,6 +367,8 @@ class BatchEnrollment
     # gather elements
     @$identifier_input       = @$container.find("textarea[name='student-ids']")
     @$enrollment_button      = @$container.find(".enrollment-button")
+    @$is_course_white_label  = @$container.find("#is_course_white_label").val()
+    @$reason_field           = @$container.find("textarea[name='reason-field']")
     @$checkbox_autoenroll    = @$container.find("input[name='auto-enroll']")
     @$checkbox_emailstudents = @$container.find("input[name='email-students']")
     @$task_response          = @$container.find(".request-response")
@@ -374,12 +376,18 @@ class BatchEnrollment
 
     # attach click handler for enrollment buttons
     @$enrollment_button.click (event) =>
+      if @$is_course_white_label == 'True'
+        if not @$reason_field.val()
+          @fail_with_error gettext "Reason field should not be left blank."
+          return false
+
       emailStudents = @$checkbox_emailstudents.is(':checked')
       send_data =
         action: $(event.target).data('action') # 'enroll' or 'unenroll'
         identifiers: @$identifier_input.val()
         auto_enroll: @$checkbox_autoenroll.is(':checked')
         email_students: emailStudents
+        reason: @$reason_field.val()
 
       $.ajax
         dataType: 'json'
@@ -393,6 +401,7 @@ class BatchEnrollment
   # clear the input text field
   clear_input: ->
     @$identifier_input.val ''
+    @$reason_field.val ''
     # default for the checkboxes should be checked
     @$checkbox_emailstudents.attr('checked', true)
     @$checkbox_autoenroll.attr('checked', true)
diff --git a/lms/templates/instructor/instructor_dashboard_2/membership.html b/lms/templates/instructor/instructor_dashboard_2/membership.html
index dfd983426af..aa4255036c1 100644
--- a/lms/templates/instructor/instructor_dashboard_2/membership.html
+++ b/lms/templates/instructor/instructor_dashboard_2/membership.html
@@ -37,7 +37,16 @@
       ${_("You will not get notification for emails that bounce, so please double-check spelling.")} </label>
     <textarea rows="6" name="student-ids" placeholder="${_("Email Addresses/Usernames")}" spellcheck="false"></textarea>
   </p>
-
+  <input type="hidden" id="is_course_white_label" value="${section_data['is_white_label']}">
+  % if section_data['is_white_label']:
+  <p>
+    <label for="reason-field-id">
+      ${_("Enter the reason why the students are to be manually enrolled or unenrolled.")}
+      ${_("This cannot be left blank and will be recorded and presented in Enrollment Reports.")}
+      ${_("Therefore, please given enough detail to account for this action.")} </label>
+    <textarea rows="2" id="reason-field-id" name="reason-field" placeholder="${_('Reason')}" spellcheck="false"></textarea>
+  </p>
+  %endif
   <div class="enroll-option">
     <input type="checkbox" name="auto-enroll" value="Auto-Enroll" checked="yes">
     <label for="auto-enroll">${_("Auto Enroll")}</label>
-- 
GitLab