diff --git a/common/djangoapps/student/roles.py b/common/djangoapps/student/roles.py
index c1b78748e4f8d2e6873af4e278d15cf3818ca0ec..c94789641ffbbaa2c4f22e576d055d39398d3639 100644
--- a/common/djangoapps/student/roles.py
+++ b/common/djangoapps/student/roles.py
@@ -267,6 +267,14 @@ class LibraryUserRole(CourseRole):
         super(LibraryUserRole, self).__init__(self.ROLE, *args, **kwargs)
 
 
+class CoursePocCoachRole(CourseRole):
+    """A POC Coach"""
+    ROLE = 'poc_coach'
+
+    def __init__(self, *args, **kwargs):
+        super(CoursePocCoachRole, self).__init__(self.ROLE, *args, **kwargs)
+
+
 class OrgStaffRole(OrgRole):
     """An organization staff member"""
     def __init__(self, *args, **kwargs):
diff --git a/common/lib/xmodule/xmodule/tabs.py b/common/lib/xmodule/xmodule/tabs.py
index d4b986d3b65131bdc4c522b9066e874426f33b78..66285df67d961b0a0275729577afe6241d31877c 100644
--- a/common/lib/xmodule/xmodule/tabs.py
+++ b/common/lib/xmodule/xmodule/tabs.py
@@ -193,6 +193,7 @@ class CourseTab(object):
             'edxnotes': EdxNotesTab,
             'syllabus': SyllabusTab,
             'instructor': InstructorTab,  # not persisted
+            'poc_coach': PocCoachTab,  # not persisted
         }
 
         tab_type = tab_dict.get('type')
@@ -733,6 +734,28 @@ class InstructorTab(StaffTab):
         )
 
 
+class PocCoachTab(CourseTab):
+    """
+    A tab for the personal online course coaches.
+    """
+    type = 'poc_coach'
+
+    def __init__(self, tab_dict=None):  # pylint: disable=unused-argument
+        super(PocCoachTab, self).__init__(
+            name=_('POC Coach'),
+            tab_id=self.type,
+            link_func=link_reverse_func('poc_coach_dashboard'),
+        )
+
+    def can_display(self, course, settings, *args, **kw):
+        # TODO Check that user actually has 'poc_coach' role on course
+        #      this is difficult to do because the user isn't passed in.
+        #      We need either a hack or an architectural realignment.
+        return (
+            settings.FEATURES.get('PERSONAL_ONLINE_COURSES', False) and
+            super(PocCoachTab, self).can_display(course, settings, *args, **kw))
+
+
 class CourseTabList(List):
     """
     An XBlock field class that encapsulates a collection of Tabs in a course.
@@ -833,6 +856,9 @@ class CourseTabList(List):
         instructor_tab = InstructorTab()
         if instructor_tab.can_display(course, settings, is_user_authenticated, is_user_staff, is_user_enrolled):
             yield instructor_tab
+        poc_coach_tab = PocCoachTab()
+        if poc_coach_tab.can_display(course, settings, is_user_authenticated, is_user_staff, is_user_enrolled):
+            yield poc_coach_tab
 
     @staticmethod
     def iterate_displayable_cms(
diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py
index 923827e84d58826f72d16f7a064eab9aeefbcefb..9e258988cb7ce899ec1c9e7b3530299203199f53 100644
--- a/lms/djangoapps/courseware/module_render.py
+++ b/lms/djangoapps/courseware/module_render.py
@@ -681,7 +681,7 @@ def get_module_for_descriptor_internal(user, descriptor, field_data_cache, cours
         if not has_access(user, 'load', descriptor, course_id):
             return None
 
-    (system, field_data) = get_module_system_for_user(
+    (system, student_data) = get_module_system_for_user(
         user=user,
         field_data_cache=field_data_cache,  # These have implicit user bindings, the rest of args are considered not to
         descriptor=descriptor,
@@ -699,6 +699,15 @@ def get_module_for_descriptor_internal(user, descriptor, field_data_cache, cours
     authored_data = OverrideFieldData.wrap(user, descriptor._field_data)  # pylint: disable=protected-access
     descriptor.bind_for_student(system, LmsFieldData(authored_data, field_data), user.id)
     descriptor.scope_ids = descriptor.scope_ids._replace(user_id=user.id)  # pylint: disable=protected-access
+
+    # Do not check access when it's a noauth request.
+    # Not that the access check needs to happen after the descriptor is bound
+    # for the student, since there may be field override data for the student
+    # that affects xblock visibility.
+    if getattr(user, 'known', True):
+        if not has_access(user, 'load', descriptor, course_id):
+            return None
+
     return descriptor
 
 
diff --git a/lms/djangoapps/instructor/access.py b/lms/djangoapps/instructor/access.py
index 168814256ea94bcec4547875113cab8ed6ad3ded..d39fca0e1a8f24ff59f9900e3174f9906ee39e67 100644
--- a/lms/djangoapps/instructor/access.py
+++ b/lms/djangoapps/instructor/access.py
@@ -12,7 +12,13 @@ TO DO sync instructor and staff flags
 import logging
 from django_comment_common.models import Role
 
-from student.roles import CourseBetaTesterRole, CourseInstructorRole, CourseStaffRole
+from student.roles import (
+    CourseBetaTesterRole,
+    CourseInstructorRole,
+    CoursePocCoachRole,
+    CourseStaffRole,
+)
+
 
 log = logging.getLogger(__name__)
 
@@ -20,6 +26,7 @@ ROLES = {
     'beta': CourseBetaTesterRole,
     'instructor': CourseInstructorRole,
     'staff': CourseStaffRole,
+    'poc_coach': CoursePocCoachRole,
 }
 
 
diff --git a/lms/djangoapps/instructor/views/api.py b/lms/djangoapps/instructor/views/api.py
index 1610a796c14d05da75a4299b96fa614a810a2cad..29d0a5a1d77e457e53269b6e8c757af5f6358d1e 100644
--- a/lms/djangoapps/instructor/views/api.py
+++ b/lms/djangoapps/instructor/views/api.py
@@ -73,7 +73,7 @@ from instructor.enrollment import (
     send_beta_role_email,
     unenroll_email,
 )
-from instructor.access import list_with_level, allow_access, revoke_access, update_forum_role
+from instructor.access import list_with_level, allow_access, revoke_access, ROLES, update_forum_role
 from instructor.offline_gradecalc import student_grades
 import instructor_analytics.basic
 import instructor_analytics.distributions
@@ -679,7 +679,7 @@ def bulk_beta_modify_access(request, course_id):
 @common_exceptions_400
 @require_query_params(
     unique_student_identifier="email or username of user to change access",
-    rolename="'instructor', 'staff', or 'beta'",
+    rolename="'instructor', 'staff', 'beta', or 'poc_coach'",
     action="'allow' or 'revoke'"
 )
 def modify_access(request, course_id):
@@ -691,7 +691,7 @@ def modify_access(request, course_id):
 
     Query parameters:
     unique_student_identifer is the target user's username or email
-    rolename is one of ['instructor', 'staff', 'beta']
+    rolename is one of ['instructor', 'staff', 'beta', 'poc_coach']
     action is one of ['allow', 'revoke']
     """
     course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
@@ -720,10 +720,10 @@ def modify_access(request, course_id):
     rolename = request.GET.get('rolename')
     action = request.GET.get('action')
 
-    if rolename not in ['instructor', 'staff', 'beta']:
-        return HttpResponseBadRequest(strip_tags(
-            "unknown rolename '{}'".format(rolename)
-        ))
+    if not rolename in ROLES:
+        error = strip_tags("unknown rolename '{}'".format(rolename))
+        log.error(error)
+        return HttpResponseBadRequest(error)
 
     # disallow instructors from removing their own instructor access.
     if rolename == 'instructor' and user == request.user and action != 'allow':
@@ -762,7 +762,7 @@ def list_course_role_members(request, course_id):
     List instructors and staff.
     Requires instructor access.
 
-    rolename is one of ['instructor', 'staff', 'beta']
+    rolename is one of ['instructor', 'staff', 'beta', 'poc_coach']
 
     Returns JSON of the form {
         "course_id": "some/course/id",
@@ -783,7 +783,7 @@ def list_course_role_members(request, course_id):
 
     rolename = request.GET.get('rolename')
 
-    if rolename not in ['instructor', 'staff', 'beta']:
+    if not rolename in ROLES:
         return HttpResponseBadRequest()
 
     def extract_user_info(user):
diff --git a/lms/djangoapps/pocs/__init__.py b/lms/djangoapps/pocs/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/lms/djangoapps/pocs/migrations/0001_initial.py b/lms/djangoapps/pocs/migrations/0001_initial.py
new file mode 100644
index 0000000000000000000000000000000000000000..4572bf899164a946e1ade6abf9fb63254150ba47
--- /dev/null
+++ b/lms/djangoapps/pocs/migrations/0001_initial.py
@@ -0,0 +1,116 @@
+# -*- coding: utf-8 -*-
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+
+class Migration(SchemaMigration):
+
+    def forwards(self, orm):
+        # Adding model 'PersonalOnlineCourse'
+        db.create_table('pocs_personalonlinecourse', (
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('course_id', self.gf('xmodule_django.models.CourseKeyField')(max_length=255, db_index=True)),
+            ('display_name', self.gf('django.db.models.fields.CharField')(max_length=255)),
+            ('coach', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])),
+        ))
+        db.send_create_signal('pocs', ['PersonalOnlineCourse'])
+
+        # Adding model 'PocMembership'
+        db.create_table('pocs_pocmembership', (
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('poc', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['pocs.PersonalOnlineCourse'])),
+            ('student', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])),
+        ))
+        db.send_create_signal('pocs', ['PocMembership'])
+
+        # Adding model 'PocFieldOverride'
+        db.create_table('pocs_pocfieldoverride', (
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('poc', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['pocs.PersonalOnlineCourse'])),
+            ('location', self.gf('xmodule_django.models.LocationKeyField')(max_length=255, db_index=True)),
+            ('field', self.gf('django.db.models.fields.CharField')(max_length=255)),
+            ('value', self.gf('django.db.models.fields.TextField')(default='null')),
+        ))
+        db.send_create_signal('pocs', ['PocFieldOverride'])
+
+        # Adding unique constraint on 'PocFieldOverride', fields ['poc', 'location', 'field']
+        db.create_unique('pocs_pocfieldoverride', ['poc_id', 'location', 'field'])
+
+
+    def backwards(self, orm):
+        # Removing unique constraint on 'PocFieldOverride', fields ['poc', 'location', 'field']
+        db.delete_unique('pocs_pocfieldoverride', ['poc_id', 'location', 'field'])
+
+        # Deleting model 'PersonalOnlineCourse'
+        db.delete_table('pocs_personalonlinecourse')
+
+        # Deleting model 'PocMembership'
+        db.delete_table('pocs_pocmembership')
+
+        # Deleting model 'PocFieldOverride'
+        db.delete_table('pocs_pocfieldoverride')
+
+
+    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.pocmembership': {
+            'Meta': {'object_name': 'PocMembership'},
+            '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/migrations/0002_auto__add_pocfuturemembership__add_field_pocmembership_active.py b/lms/djangoapps/pocs/migrations/0002_auto__add_pocfuturemembership__add_field_pocmembership_active.py
new file mode 100644
index 0000000000000000000000000000000000000000..9e650b7526a4a4defb452a6f48903f9f2e36ac00
--- /dev/null
+++ b/lms/djangoapps/pocs/migrations/0002_auto__add_pocfuturemembership__add_field_pocmembership_active.py
@@ -0,0 +1,100 @@
+# -*- coding: utf-8 -*-
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+
+class Migration(SchemaMigration):
+
+    def forwards(self, orm):
+        # Adding model 'PocFutureMembership'
+        db.create_table('pocs_pocfuturemembership', (
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('poc', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['pocs.PersonalOnlineCourse'])),
+            ('email', self.gf('django.db.models.fields.CharField')(max_length=255)),
+        ))
+        db.send_create_signal('pocs', ['PocFutureMembership'])
+
+        # Adding field 'PocMembership.active'
+        db.add_column('pocs_pocmembership', 'active',
+                      self.gf('django.db.models.fields.BooleanField')(default=False),
+                      keep_default=False)
+
+
+    def backwards(self, orm):
+        # Deleting model 'PocFutureMembership'
+        db.delete_table('pocs_pocfuturemembership')
+
+        # Deleting field 'PocMembership.active'
+        db.delete_column('pocs_pocmembership', 'active')
+
+
+    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'},
+            '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/migrations/__init__.py b/lms/djangoapps/pocs/migrations/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/lms/djangoapps/pocs/models.py b/lms/djangoapps/pocs/models.py
new file mode 100644
index 0000000000000000000000000000000000000000..da06a9fc7bcf88b2a2ab3f77886bbdd0db9c50a1
--- /dev/null
+++ b/lms/djangoapps/pocs/models.py
@@ -0,0 +1,44 @@
+from django.contrib.auth.models import User
+from django.db import models
+
+from xmodule_django.models import CourseKeyField, LocationKeyField
+
+
+class PersonalOnlineCourse(models.Model):
+    """
+    A Personal Online Course.
+    """
+    course_id = CourseKeyField(max_length=255, db_index=True)
+    display_name = models.CharField(max_length=255)
+    coach = models.ForeignKey(User, db_index=True)
+
+
+class PocMembership(models.Model):
+    """
+    Which students are in a POC?
+    """
+    poc = models.ForeignKey(PersonalOnlineCourse, db_index=True)
+    student = models.ForeignKey(User, db_index=True)
+    active = models.BooleanField(default=False)
+
+
+class PocFutureMembership(models.Model):
+    """
+    Which emails for non-users are waiting to be added to POC on registration
+    """
+    poc = models.ForeignKey(PersonalOnlineCourse, db_index=True)
+    email = models.CharField(max_length=255)
+
+
+class PocFieldOverride(models.Model):
+    """
+    Field overrides for personal online courses.
+    """
+    poc = models.ForeignKey(PersonalOnlineCourse, db_index=True)
+    location = LocationKeyField(max_length=255, db_index=True)
+    field = models.CharField(max_length=255)
+
+    class Meta:
+        unique_together = (('poc', 'location', 'field'),)
+
+    value = models.TextField(default='null')
diff --git a/lms/djangoapps/pocs/overrides.py b/lms/djangoapps/pocs/overrides.py
new file mode 100644
index 0000000000000000000000000000000000000000..5af4d799c961125a2da815a5fdc8b04424d49970
--- /dev/null
+++ b/lms/djangoapps/pocs/overrides.py
@@ -0,0 +1,86 @@
+"""
+API related to providing field overrides for individual students.  This is used
+by the individual due dates feature.
+"""
+import json
+
+from courseware.field_overrides import FieldOverrideProvider
+
+from .models import PocMembership, PocFieldOverride
+
+
+class PersonalOnlineCoursesOverrideProvider(FieldOverrideProvider):
+    """
+    A concrete implementation of
+    :class:`~courseware.field_overrides.FieldOverrideProvider` which allows for
+    overrides to be made on a per user basis.
+    """
+    def get(self, block, name, default):
+        poc = get_current_poc(self.user)
+        if poc:
+            return get_override_for_poc(poc, block, name, default)
+        return default
+
+
+def get_current_poc(user):
+    """
+    TODO Needs to look in user's session
+    """
+    # Temporary implementation.  Final implementation will need to look in
+    # user's session so user can switch between (potentially multiple) POC and
+    # MOOC views.  See courseware.courses.get_request_for_thread for idea to
+    # get at the request object.
+    try:
+        membership = PocMembership.objects.get(student=user, active=True)
+        return membership.poc
+    except PocMembership.DoesNotExist:
+        return None
+
+
+def get_override_for_poc(poc, block, name, default=None):
+    """
+    Gets the value of the overridden field for the `poc`.  `block` and `name`
+    specify the block and the name of the field.  If the field is not
+    overridden for the given poc, returns `default`.
+    """
+    try:
+        override = PocFieldOverride.objects.get(
+            poc=poc,
+            location=block.location,
+            field=name)
+        field = block.fields[name]
+        return field.from_json(json.loads(override.value))
+    except PocFieldOverride.DoesNotExist:
+        pass
+    return default
+
+
+def override_field_for_poc(poc, block, name, value):
+    """
+    Overrides a field for the `poc`.  `block` and `name` specify the block
+    and the name of the field on that block to override.  `value` is the
+    value to set for the given field.
+    """
+    override, created = PocFieldOverride.objects.get_or_create(
+        poc=poc,
+        location=block.location,
+        field=name)
+    field = block.fields[name]
+    override.value = json.dumps(field.to_json(value))
+    override.save()
+
+
+def clear_override_for_poc(poc, block, name):
+    """
+    Clears a previously set field override for the `poc`.  `block` and `name`
+    specify the block and the name of the field on that block to clear.
+    This function is idempotent--if no override is set, nothing action is
+    performed.
+    """
+    try:
+        PocFieldOverride.objects.get(
+            poc=poc,
+            location=block.location,
+            field=name).delete()
+    except PocFieldOverride.DoesNotExist:
+        pass
diff --git a/lms/djangoapps/pocs/tests/__init__.py b/lms/djangoapps/pocs/tests/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/lms/djangoapps/pocs/tests/test_overrides.py b/lms/djangoapps/pocs/tests/test_overrides.py
new file mode 100644
index 0000000000000000000000000000000000000000..4a8178768f33b01e9bdd544924c77af4a23f962a
--- /dev/null
+++ b/lms/djangoapps/pocs/tests/test_overrides.py
@@ -0,0 +1,112 @@
+import datetime
+import mock
+import pytz
+
+from courseware.field_overrides import OverrideFieldData
+from django.test.utils import override_settings
+from student.tests.factories import AdminFactory
+from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
+from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
+
+from ..models import PersonalOnlineCourse
+from ..overrides import override_field_for_poc
+
+
+@override_settings(FIELD_OVERRIDE_PROVIDERS=(
+    'pocs.overrides.PersonalOnlineCoursesOverrideProvider',))
+class TestFieldOverrides(ModuleStoreTestCase):
+    """
+    Make sure field overrides behave in the expected manner.
+    """
+    def setUp(self):
+        """
+        Set up tests
+        """
+        self.course = course = CourseFactory.create()
+
+        # Create a course outline
+        self.mooc_start = start = datetime.datetime(
+            2010, 5, 12, 2, 42, tzinfo=pytz.UTC)
+        self.mooc_due = due = datetime.datetime(
+            2010, 7, 7, 0, 0, tzinfo=pytz.UTC)
+        chapters = [ItemFactory.create(start=start, parent=course)
+                    for _ in xrange(2)]
+        sequentials = flatten([
+            [ItemFactory.create(parent=chapter) for _ in xrange(2)]
+            for chapter in chapters])
+        verticals = flatten([
+            [ItemFactory.create(due=due, parent=sequential) for _ in xrange(2)]
+            for sequential in sequentials])
+        blocks = flatten([
+            [ItemFactory.create(parent=vertical) for _ in xrange(2)]
+            for vertical in verticals])
+
+        self.poc = poc = PersonalOnlineCourse(
+            course_id=course.id,
+            display_name='Test POC',
+            coach=AdminFactory.create())
+        poc.save()
+
+        patch = mock.patch('pocs.overrides.get_current_poc')
+        self.get_poc = get_poc = patch.start()
+        get_poc.return_value = poc
+        self.addCleanup(patch.stop)
+
+        # Apparently the test harness doesn't use LmsFieldStorage, and I'm not
+        # sure if there's a way to poke the test harness to do so.  So, we'll
+        # just inject the override field storage in this brute force manner.
+        OverrideFieldData.provider_classes = None
+        for block in iter_blocks(course):
+            block._field_data = OverrideFieldData.wrap(  # pylint: disable=protected-access
+                AdminFactory.create(), block._field_data)  # pylint: disable=protected-access
+
+    def test_override_start(self):
+        """
+        Test that overriding start date on a chapter works.
+        """
+        poc_start = datetime.datetime(2014, 12, 25, 00, 00, tzinfo=pytz.UTC)
+        chapter = self.course.get_children()[0]
+        override_field_for_poc(self.poc, chapter, 'start', poc_start)
+        self.assertEquals(chapter.start, poc_start)
+
+    def test_override_is_inherited(self):
+        """
+        Test that sequentials inherit overridden start date from chapter.
+        """
+        poc_start = datetime.datetime(2014, 12, 25, 00, 00, tzinfo=pytz.UTC)
+        chapter = self.course.get_children()[0]
+        override_field_for_poc(self.poc, chapter, 'start', poc_start)
+        self.assertEquals(chapter.get_children()[0].start, poc_start)
+        self.assertEquals(chapter.get_children()[1].start, poc_start)
+
+    def test_override_is_inherited_even_if_set_in_mooc(self):
+        """
+        Test that a due date set on a chapter is inherited by grandchildren
+        (verticals) even if a due date is set explicitly on grandchildren in
+        the mooc.
+        """
+        poc_due = datetime.datetime(2015, 1, 1, 00, 00, tzinfo=pytz.UTC)
+        chapter = self.course.get_children()[0]
+        chapter.display_name = 'itsme!'
+        override_field_for_poc(self.poc, chapter, 'due', poc_due)
+        vertical = chapter.get_children()[0].get_children()[0]
+        self.assertEqual(vertical.due, poc_due)
+
+
+def flatten(seq):
+    """
+    For [[1, 2], [3, 4]] returns [1, 2, 3, 4].  Does not recurse.
+    """
+    return [x for sub in seq for x in sub]
+
+
+def iter_blocks(course):
+    """
+    Returns an iterator over all of the blocks in a course.
+    """
+    def visit(block):
+        yield block
+        for child in block.get_children():
+            for descendant in visit(child):  # wish they'd backport yield from
+                yield descendant
+    return visit(course)
diff --git a/lms/djangoapps/pocs/tests/test_views.py b/lms/djangoapps/pocs/tests/test_views.py
new file mode 100644
index 0000000000000000000000000000000000000000..feaa83107fd44ff73998677250070c228a0344a3
--- /dev/null
+++ b/lms/djangoapps/pocs/tests/test_views.py
@@ -0,0 +1,166 @@
+import datetime
+import json
+import re
+import pytz
+from mock import patch
+
+from courseware.tests.helpers import LoginEnrollmentTestCase
+from django.core.urlresolvers import reverse
+from edxmako.shortcuts import render_to_response
+from student.roles import CoursePocCoachRole
+from student.tests.factories import AdminFactory
+from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
+from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
+
+from ..models import PersonalOnlineCourse
+from ..overrides import get_override_for_poc
+
+
+def intercept_renderer(path, context):
+    """
+    Intercept calls to `render_to_response` and attach the context dict to the
+    response for examination in unit tests.
+    """
+    # I think Django already does this for you in their TestClient, except
+    # we're bypassing that by using edxmako.  Probably edxmako should be
+    # integrated better with Django's rendering and event system.
+    response = render_to_response(path, context)
+    response.mako_context = context
+    response.mako_template = path
+    return response
+
+
+class TestCoachDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase):
+    """
+    Tests for Personal Online Courses views.
+    """
+    def setUp(self):
+        """
+        Set up tests
+        """
+        self.course = course = CourseFactory.create()
+
+        # Create instructor account
+        self.coach = coach = AdminFactory.create()
+        self.client.login(username=coach.username, password="test")
+
+        # Create a course outline
+        self.mooc_start = start = datetime.datetime(
+            2010, 5, 12, 2, 42, tzinfo=pytz.UTC)
+        self.mooc_due = due = datetime.datetime(
+            2010, 7, 7, 0, 0, tzinfo=pytz.UTC)
+        chapters = [ItemFactory.create(start=start, parent=course)
+                    for _ in xrange(2)]
+        sequentials = flatten([
+            [ItemFactory.create(parent=chapter) for _ in xrange(2)]
+            for chapter in chapters])
+        verticals = flatten([
+            [ItemFactory.create(due=due, parent=sequential) for _ in xrange(2)]
+            for sequential in sequentials])
+        blocks = flatten([
+            [ItemFactory.create(parent=vertical) for _ in xrange(2)]
+            for vertical in verticals])
+
+    def make_coach(self):
+        role = CoursePocCoachRole(self.course.id)
+        role.add_users(self.coach)
+
+    def tearDown(self):
+        """
+        Undo patches.
+        """
+        patch.stopall()
+
+    def test_not_a_coach(self):
+        """
+        User is not a coach, should get Forbidden response.
+        """
+        url = reverse(
+            'poc_coach_dashboard',
+            kwargs={'course_id': self.course.id.to_deprecated_string()})
+        response = self.client.get(url)
+        self.assertEqual(response.status_code, 403)
+
+    def test_no_poc_created(self):
+        """
+        No POC is created, coach should see form to add a POC.
+        """
+        self.make_coach()
+        url = reverse(
+            'poc_coach_dashboard',
+            kwargs={'course_id': self.course.id.to_deprecated_string()})
+        response = self.client.get(url)
+        self.assertEqual(response.status_code, 200)
+        self.assertTrue(re.search(
+            '<form action=".+create_poc"',
+            response.content))
+
+    def test_create_poc(self):
+        """
+        Create POC. Follow redirect to coach dashboard, confirm we see
+        the coach dashboard for the new POC.
+        """
+        self.make_coach()
+        url = reverse(
+            'create_poc',
+            kwargs={'course_id': self.course.id.to_deprecated_string()})
+        response = self.client.post(url, {'name': 'New POC'})
+        self.assertEqual(response.status_code, 302)
+        url = response.get('location')
+        response = self.client.get(url)
+        self.assertEqual(response.status_code, 200)
+        self.assertTrue(re.search('id="poc-schedule"', response.content))
+
+    @patch('pocs.views.render_to_response', intercept_renderer)
+    @patch('pocs.views.today')
+    def test_edit_schedule(self, today):
+        """
+        Get POC schedule, modify it, save it.
+        """
+        today.return_value = datetime.datetime(2014, 11, 25, tzinfo=pytz.UTC)
+        self.test_create_poc()
+        url = reverse(
+            'poc_coach_dashboard',
+            kwargs={'course_id': self.course.id.to_deprecated_string()})
+        response = self.client.get(url)
+        schedule = json.loads(response.mako_context['schedule'])
+        self.assertEqual(len(schedule), 2)
+        self.assertEqual(schedule[0]['hidden'], True)
+        self.assertEqual(schedule[0]['start'], None)
+        self.assertEqual(schedule[0]['children'][0]['start'], None)
+        self.assertEqual(schedule[0]['due'], None)
+        self.assertEqual(schedule[0]['children'][0]['due'], None)
+        self.assertEqual(
+            schedule[0]['children'][0]['children'][0]['due'], None
+        )
+
+        url = reverse(
+            'save_poc',
+            kwargs={'course_id': self.course.id.to_deprecated_string()})
+
+        schedule[0]['hidden'] = False
+        schedule[0]['start'] = u'2014-11-20 00:00'
+        schedule[0]['children'][0]['due'] = u'2014-12-25 00:00'  # what a jerk!
+        response = self.client.post(
+            url, json.dumps(schedule), content_type='application/json'
+        )
+
+        schedule = json.loads(response.content)
+        self.assertEqual(schedule[0]['hidden'], False)
+        self.assertEqual(schedule[0]['start'], u'2014-11-20 00:00')
+        self.assertEqual(
+            schedule[0]['children'][0]['due'], u'2014-12-25 00:00'
+        )
+
+        # Make sure start date set on course, follows start date of earliest
+        # scheduled chapter
+        poc = PersonalOnlineCourse.objects.get()
+        course_start = get_override_for_poc(poc, self.course, 'start')
+        self.assertEqual(str(course_start)[:-9], u'2014-11-20 00:00')
+
+
+def flatten(seq):
+    """
+    For [[1, 2], [3, 4]] returns [1, 2, 3, 4].  Does not recurse.
+    """
+    return [x for sub in seq for x in sub]
diff --git a/lms/djangoapps/pocs/utils.py b/lms/djangoapps/pocs/utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..6890bc18c65f6cc4a8087eef2e7dd6d6b7d6284b
--- /dev/null
+++ b/lms/djangoapps/pocs/utils.py
@@ -0,0 +1,213 @@
+"""
+POC Enrollment operations for use by Coach APIs.
+
+Does not include any access control, be sure to check access before calling.
+"""
+
+import json
+from django.contrib.auth.models import User
+from django.conf import settings
+from django.core.urlresolvers import reverse
+from django.core.mail import send_mail
+
+from edxmako.shortcuts import render_to_string
+
+from microsite_configuration import microsite
+
+from pocs.models import (
+    PersonalOnlineCourse,
+    PocMembership,
+    PocFutureMembership,
+)
+
+
+class EmailEnrollmentState(object):
+    """ Store the complete enrollment state of an email in a class """
+    def __init__(self, poc, email):
+        exists_user = User.objects.filter(email=email).exists()
+        if exists_user:
+            user = User.objects.get(email=email)
+            poc_member = PocMembership.objects.filter(poc=poc, student=user)
+            in_poc = poc_member.exists()
+            full_name = user.profile.name
+        else:
+            in_poc = False
+            full_name = None
+        self.user = exists_user
+        self.member = user
+        self.full_name = full_name
+        self.in_poc = in_poc
+
+    def __repr__(self):
+        return "{}(user={}, member={}, in_poc={}".format(
+            self.__class__.__name__,
+            self.user,
+            self.member,
+            self.in_poc,
+        )
+
+    def to_dict(self):
+        return {
+            'user': self.user,
+            'member': self.member,
+            'in_poc': self.in_poc,
+        }
+
+
+def enroll_email(poc, student_email, auto_enroll=False, email_students=False, email_params=None):
+    if email_params is None:
+        email_params = get_email_params(poc, True)
+    previous_state = EmailEnrollmentState(poc, student_email)
+
+    if previous_state.user:
+        if not previous_state.in_poc:
+            user = User.objects.get(email=student_email)
+            membership = PocMembership(poc=poc, student=user)
+            membership.save()
+        if email_students:
+            email_params['message'] = 'enrolled_enroll'
+            email_params['email_address'] = student_email
+            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.save()
+        if email_students:
+            email_params['message'] = 'allowed_enroll'
+            email_params['email_address'] = student_email
+            send_mail_to_student(student_email, email_params)
+
+    after_state = EmailEnrollmentState(poc, student_email)
+
+    return previous_state, after_state
+
+
+def unenroll_email(poc, student_email, email_students=False, email_params=None):
+    if email_params is None:
+        email_params = get_email_params(poc, True)
+    previous_state = EmailEnrollmentState(poc, student_email)
+
+    if previous_state.in_poc:
+        PocMembership.objects.get(
+            poc=poc, student=previous_state.member
+        ).delete()
+        if email_students:
+            email_params['message'] = 'enrolled_unenroll'
+            email_params['email_address'] = student_email
+            email_params['full_name'] = previous_state.full_name
+            send_mail_to_student(student_email, email_params)
+    else:
+        if PocFutureMembership.objects.filter(
+            poc=poc, email=student_email
+        ).exists():
+            PocFutureMembership.get(poc=poc, email=student_email).delete()
+        if email_students:
+            email_params['message'] = 'allowed_unenroll'
+            email_params['email_address'] = student_email
+            send_mail_to_student(student_email, email_params)
+
+    after_state = EmailEnrollmentState(poc, student_email)
+
+    return previous_state, after_state
+
+
+def get_email_params(poc, auto_enroll, secure=True):
+    protocol = 'https' if secure else 'http'
+    course_id = poc.course_id
+
+    stripped_site_name = microsite.get_value(
+        'SITE_NAME',
+        settings.SITE_NAME
+    )
+    registration_url = u'{proto}://{site}{path}'.format(
+        proto=protocol,
+        site=stripped_site_name,
+        path=reverse('student.views.register_user')
+    )
+    course_url = u'{proto}://{site}{path}'.format(
+        proto=protocol,
+        site=stripped_site_name,
+        path=reverse(
+            'course_root',
+            kwargs={'course_id': course_id.to_deprecated_string()}
+        )
+    )
+
+    course_about_url = None
+    if not settings.FEATURES.get('ENABLE_MKTG_SITE', False):
+        course_about_url = u'{proto}://{site}{path}'.format(
+            proto=protocol,
+            site=stripped_site_name,
+            path=reverse(
+                'about_course',
+                kwargs={'course_id': course_id.to_deprecated_string()}
+            )
+        )
+
+    email_params = {
+        'site_name': stripped_site_name,
+        'registration_url': registration_url,
+        'course': poc,
+        'auto_enroll': auto_enroll,
+        'course_url': course_url,
+        'course_about_url': course_about_url,
+    }
+    return email_params
+
+
+def send_mail_to_student(student, param_dict):
+    if 'course' in param_dict:
+        param_dict['course_name'] = param_dict['course'].display_name
+
+    param_dict['site_name'] = microsite.get_value(
+        'SITE_NAME',
+        param_dict['site_name']
+    )
+
+    subject = None
+    message = None
+
+    message_type = param_dict['message']
+
+    email_template_dict = {
+        'allowed_enroll': (
+            'pocs/enroll_email_allowedsubject.txt',
+            'pocs/enroll_email_allowedmessage.txt'
+        ),
+        'enrolled_enroll': (
+            'pocs/enroll_email_enrolledsubject.txt',
+            'pocs/enroll_email_enrolledmessage.txt'
+        ),
+        'allowed_unenroll': (
+            'pocs/unenroll_email_subject.txt',
+            'pocs/unenroll_email_allowedmessage.txt'
+        ),
+        'enrolled_unenroll': (
+            'pocs/unenroll_email_subject.txt',
+            'pocs/unenroll_email_enrolledmessage.txt'
+        ),
+    }
+
+    subject_template, message_template = email_template_dict.get(
+        message_type, (None, None)
+    )
+    if subject_template is not None and message_template is not None:
+        subject = render_to_string(subject_template, param_dict)
+        message = render_to_string(message_template, param_dict)
+
+    if subject and message:
+        message = message.strip()
+
+        subject = ''.join(subject.splitlines())
+        from_address = microsite.get_value(
+            'email_from_address',
+            settings.DEFAULT_FROM_EMAIL
+        )
+
+        send_mail(
+            subject,
+            message,
+            from_address,
+            [student],
+            fail_silently=False
+        )
diff --git a/lms/djangoapps/pocs/views.py b/lms/djangoapps/pocs/views.py
new file mode 100644
index 0000000000000000000000000000000000000000..89c53f4e2f0c157195e555bed4e21380fe2a6c32
--- /dev/null
+++ b/lms/djangoapps/pocs/views.py
@@ -0,0 +1,244 @@
+import datetime
+import functools
+import json
+import logging
+import pytz
+
+from django.core.urlresolvers import reverse
+from django.http import HttpResponse, HttpResponseForbidden
+from django.core.exceptions import ValidationError
+from django.core.validators import validate_email
+from django.shortcuts import redirect
+from django.utils.translation import ugettext as _
+from django.views.decorators.cache import cache_control
+from django_future.csrf import ensure_csrf_cookie
+from django.contrib.auth.models import User
+
+from courseware.courses import get_course_by_id
+from courseware.field_overrides import disable_overrides
+from edxmako.shortcuts import render_to_response
+from opaque_keys.edx.locations import SlashSeparatedCourseKey
+from student.roles import CoursePocCoachRole
+
+from instructor.views.api import _split_input_list
+from instructor.views.tools import get_student_from_identifier
+
+from .models import PersonalOnlineCourse, PocMembership
+from .overrides import (
+    clear_override_for_poc,
+    get_override_for_poc,
+    override_field_for_poc,
+)
+from .utils import enroll_email, unenroll_email
+
+log = logging.getLogger(__name__)
+today = datetime.datetime.today  # for patching in tests
+
+
+def coach_dashboard(view):
+    """
+    View decorator which enforces that the user have the POC coach role on the
+    given course and goes ahead and translates the course_id from the Django
+    route into a course object.
+    """
+    @functools.wraps(view)
+    def wrapper(request, course_id):
+        course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
+        role = CoursePocCoachRole(course_key)
+        if not role.has_user(request.user):
+            return HttpResponseForbidden(
+                _('You must be a POC Coach to access this view.'))
+        course = get_course_by_id(course_key, depth=None)
+        return view(request, course)
+    return wrapper
+
+
+@ensure_csrf_cookie
+@cache_control(no_cache=True, no_store=True, must_revalidate=True)
+@coach_dashboard
+def dashboard(request, course):
+    """
+    Display the POC Coach Dashboard.
+    """
+    poc = get_poc_for_coach(course, request.user)
+    schedule = get_poc_schedule(course, poc)
+    context = {
+        'course': course,
+        'poc': poc,
+        'schedule': json.dumps(schedule, indent=4),
+        'save_url': reverse('save_poc', kwargs={'course_id': course.id}),
+        'poc_members': PocMembership.objects.filter(poc=poc),
+    }
+    if not poc:
+        context['create_poc_url'] = reverse(
+            'create_poc', kwargs={'course_id': course.id})
+    return render_to_response('pocs/coach_dashboard.html', context)
+
+
+@ensure_csrf_cookie
+@cache_control(no_cache=True, no_store=True, must_revalidate=True)
+@coach_dashboard
+def create_poc(request, course):
+    """
+    Create a new POC
+    """
+    name = request.POST.get('name')
+    poc = PersonalOnlineCourse(
+        course_id=course.id,
+        coach=request.user,
+        display_name=name)
+    poc.save()
+
+    # Make sure start/due are overridden for entire course
+    start = today().replace(tzinfo=pytz.UTC)
+    override_field_for_poc(poc, course, 'start', start)
+    override_field_for_poc(poc, course, 'due', None)
+
+    # Hide anything that can show up in the schedule
+    hidden = 'visible_to_staff_only'
+    for chapter in course.get_children():
+        override_field_for_poc(poc, chapter, hidden, True)
+        for sequential in chapter.get_children():
+            override_field_for_poc(poc, sequential, hidden, True)
+            for vertical in sequential.get_children():
+                override_field_for_poc(poc, vertical, hidden, True)
+
+    url = reverse('poc_coach_dashboard', kwargs={'course_id': course.id})
+    return redirect(url)
+
+
+@ensure_csrf_cookie
+@cache_control(no_cache=True, no_store=True, must_revalidate=True)
+@coach_dashboard
+def save_poc(request, course):
+    """
+    Save changes to POC
+    """
+    poc = get_poc_for_coach(course, request.user)
+
+    def override_fields(parent, data, earliest=None):
+        blocks = {
+            str(child.location): child
+            for child in parent.get_children()}
+        for unit in data:
+            block = blocks[unit['location']]
+            override_field_for_poc(
+                poc, block, 'visible_to_staff_only', unit['hidden'])
+            start = parse_date(unit['start'])
+            if start:
+                if not earliest or start < earliest:
+                    earliest = start
+                override_field_for_poc(poc, block, 'start', start)
+            else:
+                clear_override_for_poc(poc, block, 'start')
+            due = parse_date(unit['due'])
+            if due:
+                override_field_for_poc(poc, block, 'due', due)
+            else:
+                clear_override_for_poc(poc, block, 'due')
+
+            children = unit.get('children', None)
+            if children:
+                override_fields(block, children, earliest)
+        return earliest
+
+    earliest = override_fields(course, json.loads(request.body))
+    if earliest:
+        override_field_for_poc(poc, course, 'start', earliest)
+
+    return HttpResponse(
+        json.dumps(get_poc_schedule(course, poc)),
+        content_type='application/json')
+
+
+def parse_date(s):
+    if s:
+        try:
+            date, time = s.split(' ')
+            year, month, day = map(int, date.split('-'))
+            hour, minute = map(int, time.split(':'))
+            return datetime.datetime(
+                year, month, day, hour, minute, tzinfo=pytz.UTC)
+        except:
+            log.warn("Unable to parse date: " + s)
+
+    return None
+
+
+def get_poc_for_coach(course, coach):
+    """
+    Looks to see if user is coach of a POC for this course.  Returns the POC or
+    None.
+    """
+    try:
+        return PersonalOnlineCourse.objects.get(
+            course_id=course.id,
+            coach=coach)
+    except PersonalOnlineCourse.DoesNotExist:
+        return None
+
+
+def get_poc_schedule(course, poc):
+    """
+    """
+    def visit(node, depth=1):
+        for child in node.get_children():
+            start = get_override_for_poc(poc, child, 'start', None)
+            if start:
+                start = str(start)[:-9]
+            due = get_override_for_poc(poc, child, 'due', None)
+            if due:
+                due = str(due)[:-9]
+            hidden = get_override_for_poc(
+                poc, child, 'visible_to_staff_only',
+                child.visible_to_staff_only)
+            visited = {
+                'location': str(child.location),
+                'display_name': child.display_name,
+                'category': child.category,
+                'start': start,
+                'due': due,
+                'hidden': hidden,
+            }
+            if depth < 3:
+                children = tuple(visit(child, depth + 1))
+                if children:
+                    visited['children'] = children
+                    yield visited
+            else:
+                yield visited
+
+    with disable_overrides():
+        return tuple(visit(course))
+
+
+@ensure_csrf_cookie
+@cache_control(no_cache=True, no_store=True, must_revalidate=True)
+@coach_dashboard
+def poc_invite(request, course):
+    """
+    Invite users to new poc
+    """
+    poc = get_poc_for_coach(course, request.user)
+    action = request.POST.get('enrollment-button')
+    identifiers_raw = request.POST.get('student-ids')
+    identifiers = _split_input_list(identifiers_raw)
+    for identifier in identifiers:
+        user = None
+        email = None
+        try:
+            user = get_student_from_identifier(identifier)
+        except User.DoesNotExist:
+            email = identifier
+        else:
+            email = user.email
+        try:
+            validate_email(email)
+            if action == 'Enroll':
+                enroll_email(poc, email, email_students=True)
+            if action == "Unenroll":
+                unenroll_email(poc, email, email_students=True)
+        except ValidationError:
+            pass  # maybe log this?
+    url = reverse('poc_coach_dashboard', kwargs={'course_id': course.id})
+    return redirect(url)
diff --git a/lms/envs/common.py b/lms/envs/common.py
index 080e40baed2300a6a8670ad663945f0e1e12fde4..36271ee723d2ab019b3e9d8d5db30e01758b81d1 100644
--- a/lms/envs/common.py
+++ b/lms/envs/common.py
@@ -214,6 +214,9 @@ FEATURES = {
     # True.
     'INDIVIDUAL_DUE_DATES': False,
 
+    # Enable Personal Online Courses
+    'PERSONAL_ONLINE_COURSES': False,
+
     # Enable legacy instructor dashboard
     'ENABLE_INSTRUCTOR_LEGACY_DASHBOARD': True,
 
diff --git a/lms/envs/test.py b/lms/envs/test.py
index 42044969889352c878bca6a80557b631fa446b13..35d1c5285cac06b3acef17a087c668286f5bcc5a 100644
--- a/lms/envs/test.py
+++ b/lms/envs/test.py
@@ -445,7 +445,6 @@ MONGODB_LOG = {
     'db': 'xlog',
 }
 
-
 # Enable EdxNotes for tests.
 FEATURES['ENABLE_EDXNOTES'] = True
 
@@ -469,3 +468,6 @@ FACEBOOK_API_VERSION = "v2.2"
 
 # Certificates Views
 FEATURES['CERTIFICATES_HTML_VIEW'] = True
+
+######### personal online courses #########
+INSTALLED_APPS += ('pocs',)
diff --git a/lms/static/sass/course.scss.mako b/lms/static/sass/course.scss.mako
index 1151a55aa1ba6b9ad0b09b910a5f0fc4c1def399..d401e5786f8971a6324f7fb4b978224f4b1576cb 100644
--- a/lms/static/sass/course.scss.mako
+++ b/lms/static/sass/course.scss.mako
@@ -77,5 +77,8 @@
 @import "course/instructor/email";
 @import "xmodule/descriptors/css/module-styles.scss";
 
-// course - discussion
+// course - poc_coach
+@import "course/poc_coach/dashboard";
+
+// discussion
 @import "course/discussion/form-wmd-toolbar";
diff --git a/lms/static/sass/course/poc_coach/_dashboard.scss b/lms/static/sass/course/poc_coach/_dashboard.scss
new file mode 100644
index 0000000000000000000000000000000000000000..e6bebb5e7a78c2cc6cc1c4544e013ddf9d020f81
--- /dev/null
+++ b/lms/static/sass/course/poc_coach/_dashboard.scss
@@ -0,0 +1,57 @@
+.poc-schedule-container {
+    float: left;
+    width: 750px;
+}
+
+table.poc-schedule {
+    width: 100%;
+
+    thead {
+        border-bottom: 2px solid black;
+    }
+    th:first-child {
+        width: 40%;
+    }
+    th:last-child {
+        width: 18%;
+    }
+    th, td {
+        padding: 10px;
+    }
+    .sequential .unit {
+        padding-left: 25px;
+    }
+    .vertical .unit {
+        padding-left: 40px;
+    }
+    a.empty {
+        display: block;
+        width: 100%;
+        color: white;
+    }
+    a.empty:hover {
+        color: #cbcbcb;
+    }
+}
+
+.poc-schedule-sidebar {
+    float: left;
+    width: 295px;
+    margin-left: 20px;
+}
+
+.poc-sidebar-panel {
+    border: 1px solid #cbcbcb;
+    padding: 15px;
+    margin-bottom: 20px;
+}
+
+form.poc-form {
+    line-height: 1.5;
+    select {
+        width: 100%;
+    }
+    .field {
+        margin: 5px 0 5px 0;
+    }
+}
diff --git a/lms/templates/instructor/instructor_dashboard_2/membership.html b/lms/templates/instructor/instructor_dashboard_2/membership.html
index d7ef87af5d2e21bde8dc53961f43d9d9079bd67e..489211d9eeca0aea6d93f9041a69e13188446543 100644
--- a/lms/templates/instructor/instructor_dashboard_2/membership.html
+++ b/lms/templates/instructor/instructor_dashboard_2/membership.html
@@ -243,5 +243,18 @@
       data-add-button-label="${_("Add Community TA")}"
     ></div>
   %endif
-
+  
+  %if section_data['access']['instructor'] and settings.FEATURES.get('PERSONAL_ONLINE_COURSES', False):
+    <div class="auth-list-container"
+      data-rolename="poc_coach"
+      data-display-name="${_("POC Coaches")}"
+      data-info-text="
+        ${_("POC Coaches are able to create their own Personal Online Courses "
+            "based on this course, which they can use to provide personalized "
+            "instruction to their own students based in this course material.")}"
+      data-list-endpoint="${section_data['list_course_role_members_url']}"
+      data-modify-endpoint="${section_data['modify_access_url']}"
+      data-add-button-label="${_("Add POC Coach")}"
+    ></div>
+  %endif
 </div>
diff --git a/lms/templates/pocs/coach_dashboard.html b/lms/templates/pocs/coach_dashboard.html
new file mode 100644
index 0000000000000000000000000000000000000000..3ad742619d00fe532db290745fea5c249650a9ad
--- /dev/null
+++ b/lms/templates/pocs/coach_dashboard.html
@@ -0,0 +1,81 @@
+<%! from django.utils.translation import ugettext as _ %>
+<%! from django.core.urlresolvers import reverse %>
+
+<%inherit file="/main.html" />
+<%namespace name='static' file='/static_content.html'/>
+
+<%block name="pagetitle">${_("POC Coach Dashboard")}</%block>
+<%block name="nav_skip">#poc-coach-dashboard-content</%block>
+
+<%block name="headextra">
+  <%static:css group='style-course-vendor'/>
+  <%static:css group='style-vendor-tinymce-content'/>
+  <%static:css group='style-vendor-tinymce-skin'/>
+  <%static:css group='style-course'/>
+</%block>
+
+<%include file="/courseware/course_navigation.html" args="active_page='poc_coach'" />
+
+<section class="container">
+  <div class="instructor-dashboard-wrapper-2">
+    <section class="instructor-dashboard-content-2" id="poc-coach-dashboard-content">
+      <h1>${_("POC Coach Dashboard")}</h1>
+
+      %if not poc:
+      <section>
+        <form action="${create_poc_url}" method="POST">
+          <input type="hidden" name="csrfmiddlewaretoken" value="${csrf_token}"/>
+          <input name="name" placeholder="Name your POC"/><br/>
+          <button id="create-poc">Coach a new Personal Online Course</button>
+        </form>
+      </section>
+      %endif
+      
+      %if poc:
+      <ul class="instructor-nav">
+        <li class="nav-item">
+          <a href="#" data-section="membership">${_("Enrollment")}</a>
+        </li>
+        <li class="nav-item">
+          <a href="#" data-section="schedule">${_("Schedule")}</a>
+        </li>
+      </ul>
+      <section id="membership" class="idash-section">
+        <%include file="enrollment.html" args="" />
+      </section>
+      <section id="schedule" class="idash-section">
+        <%include file="schedule.html" args="" />
+      </section>
+      %endif
+
+    </section>
+  </div>
+</section>
+
+<script>
+  function setup_tabs() {
+    $(".instructor-nav a").on("click", function(event) {
+        event.preventDefault();
+        $(".instructor-nav a").removeClass("active-section");
+        var section_sel = "#" + $(this).attr("data-section");
+        $("section.idash-section").hide();
+        $(section_sel).show();
+        $(this).addClass("active-section");
+    });
+
+    var url = document.URL,
+        hashbang = url.indexOf('#!');
+    if (hashbang != -1) {
+      var selector = '.instructor-nav a[data-section=' + 
+        url.substr(hashbang + 2) + ']';
+      $(selector).click();
+    }
+    else {
+      $(".instructor-nav a").first().click();
+    }
+  }
+
+  $(setup_tabs);
+
+
+</script>
diff --git a/lms/templates/pocs/enroll_email_allowedmessage.txt b/lms/templates/pocs/enroll_email_allowedmessage.txt
new file mode 100644
index 0000000000000000000000000000000000000000..4315b038d0f964bfe1aafe65abbbf13892b69927
--- /dev/null
+++ b/lms/templates/pocs/enroll_email_allowedmessage.txt
@@ -0,0 +1,43 @@
+<%! from django.utils.translation import ugettext as _ %>
+
+${_("Dear student,")}
+
+${_("You have been invited to join {course_name} at {site_name} by a "
+	"member of the course staff.").format(
+		course_name=course.display_name,
+		site_name=site_name
+	)}
+% if is_shib_course:
+% if auto_enroll:
+
+${_("To access the course visit {course_url} and login.").format(course_url=course_url)}
+% elif course_about_url is not None:
+
+${_("To access the course visit {course_about_url} and register for the course.").format(
+		course_about_url=course_about_url)}
+% endif
+% else:
+
+${_("To finish your registration, please visit {registration_url} and fill "
+	"out the registration form making sure to use {email_address} in the E-mail field.").format(
+		registration_url=registration_url,
+		email_address=email_address
+	)}
+% if auto_enroll:
+${_("Once you have registered and activated your account, you will see "
+	"{course_name} listed on your dashboard.").format(
+		course_name=course.display_name
+	)}
+% elif course_about_url is not None:
+${_("Once you have registered and activated your account, visit {course_about_url} "
+	"to join the course.").format(course_about_url=course_about_url)}
+% else:
+${_("You can then enroll in {course_name}.").format(course_name=course.display_name)}
+% endif
+% endif
+
+----
+${_("This email was automatically sent from {site_name} to "
+	"{email_address}").format(
+		site_name=site_name, email_address=email_address
+	)}
diff --git a/lms/templates/pocs/enroll_email_allowedsubject.txt b/lms/templates/pocs/enroll_email_allowedsubject.txt
new file mode 100644
index 0000000000000000000000000000000000000000..0a390361404bba6df2a191cae0a2aed875ff4631
--- /dev/null
+++ b/lms/templates/pocs/enroll_email_allowedsubject.txt
@@ -0,0 +1,5 @@
+<%! from django.utils.translation import ugettext as _ %>
+
+${_("You have been invited to register for {course_name}").format(
+		course_name=course.display_name
+	)}
diff --git a/lms/templates/pocs/enroll_email_enrolledmessage.txt b/lms/templates/pocs/enroll_email_enrolledmessage.txt
new file mode 100644
index 0000000000000000000000000000000000000000..b24f5a4a36ec1d67750e15a1f2a3a48cd6e87e3f
--- /dev/null
+++ b/lms/templates/pocs/enroll_email_enrolledmessage.txt
@@ -0,0 +1,20 @@
+<%! from django.utils.translation import ugettext as _ %>
+
+${_("Dear {full_name}").format(full_name=full_name)}
+
+${_("You have been enrolled in {course_name} at {site_name} by a member "
+	"of the course staff. The course should now appear on your {site_name} "
+	"dashboard.").format(
+		course_name=course.display_name,
+		site_name=site_name
+	)}
+
+${_("To start accessing course materials, please visit {course_url}").format(
+		course_url=course_url
+	)}
+
+----
+${_("This email was automatically sent from {site_name} to "
+	"{full_name}").format(
+		site_name=site_name, full_name=full_name
+	)}
diff --git a/lms/templates/pocs/enroll_email_enrolledsubject.txt b/lms/templates/pocs/enroll_email_enrolledsubject.txt
new file mode 100644
index 0000000000000000000000000000000000000000..dc84c3f0a8b5d1b2fedae6709cf37178c2ca68d0
--- /dev/null
+++ b/lms/templates/pocs/enroll_email_enrolledsubject.txt
@@ -0,0 +1,5 @@
+<%! from django.utils.translation import ugettext as _ %>
+
+${_("You have been enrolled in {course_name}").format(
+		course_name=course.display_name
+	)}
diff --git a/lms/templates/pocs/enrollment.html b/lms/templates/pocs/enrollment.html
new file mode 100644
index 0000000000000000000000000000000000000000..01071f7116d44fb06f88f552584dcc11ab5bab8e
--- /dev/null
+++ b/lms/templates/pocs/enrollment.html
@@ -0,0 +1,78 @@
+<%! from django.utils.translation import ugettext as _ %>
+
+<div class="batch-enrollment" style="float:left;width:50%">
+  <form method="POST" action="poc_invite">
+  <input type="hidden" name="csrfmiddlewaretoken" value="${ csrf_token }">
+  <h2> ${_("Batch Enrollment")} </h2>
+  <p>
+    <label for="student-ids">
+      ${_("Enter email addresses and/or usernames separated by new lines or commas.")}
+      ${_("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>
+
+  <div class="enroll-option">
+    <input type="checkbox" name="auto-enroll" value="Auto-Enroll" checked="yes">
+    <label style="display:inline" for="auto-enroll">${_("Auto Enroll")}</label>
+    <div class="hint auto-enroll-hint">
+      <span class="hint-caret"></span>
+      <p>
+	${_("If this option is <em>checked</em>, users who have not yet registered for {platform_name} will be automatically enrolled.").format(platform_name=settings.PLATFORM_NAME)}
+	${_("If this option is left <em>unchecked</em>, users who have not yet registered for {platform_name} will not be enrolled, but will be allowed to enroll once they make an account.").format(platform_name=settings.PLATFORM_NAME)}
+	<br /><br />
+	${_("Checking this box has no effect if 'Unenroll' is selected.")}
+      </p>
+    </div>
+  </div>
+
+  <div class="enroll-option">
+    <input type="checkbox" name="email-students" value="Notify-students-by-email" checked="yes">
+    <label style="display:inline" for="email-students">${_("Notify users by email")}</label>
+    <div class="hint email-students-hint">
+      <span class="hint-caret"></span>
+      <p>
+	${_("If this option is <em>checked</em>, users will receive an email notification.")}
+      </p>
+    </div>
+  </div>
+
+  <div>
+    <input type="submit" name="enrollment-button" class="enrollment-button" value="${_("Enroll")}">
+    <input type="submit" name="enrollment-button" class="enrollment-button" value="${_("Unenroll")}">
+  </div>
+  <div class="request-response"></div>
+  <div class="request-response-error"></div>
+  </form>
+</div>
+
+<div class="member-lists-management" style="float:left;width:50%">
+  <div class="auth-list-container active">
+    <div class="member-list-widget">
+      <div class="member-list">
+        <h2> ${_("Student List Management")}</h2>
+        <table>
+          <thead>
+            <tr>
+              <td class="label">Username</td>
+              <td class="label">Email</td>
+              <td class="label">Revoke access</td>
+            </tr>
+          </thead>
+          <tbody>
+            %for member in poc_members:
+            <tr>
+              <td>${member.student}</td>
+              <td>${member.student.email}</td>
+              <td><div class="revoke"><i class="icon-remove-sign"></i> Revoke access</div></td>
+            </tr>
+            %endfor
+          </tbody>
+        </table>
+      </div>
+      <div class="bottom-bar">
+        <input name="add-field" class="add-field" placeholder="Enter username or email" type="text">
+        <input name="add" class="add" value="Add Student" type="button">
+      </div>
+    </div>
+  </div>
+</div> 
diff --git a/lms/templates/pocs/schedule.html b/lms/templates/pocs/schedule.html
new file mode 100644
index 0000000000000000000000000000000000000000..330070f085757d58c5e442af3fffdb3ed64e6959
--- /dev/null
+++ b/lms/templates/pocs/schedule.html
@@ -0,0 +1,504 @@
+<%! from django.utils.translation import ugettext as _ %>
+
+<script id="poc-schedule-template" type="text/x-handlerbars-template">
+  <table class="poc-schedule">
+    <thead>
+      <tr>
+        <th>${_('Unit')}</th>
+        <th>${_('Start Date')}</th>
+        <th>${_('Due Date')}</th>
+        <th><a href="#" id="remove-all">
+          <i class="icon-remove-sign icon"></i> ${_('remove all')}
+        </a></th>
+      </tr>
+    </thead>
+    <tbody>
+      {{#each chapters}}
+        <tr class="chapter collapsed" data-location="{{location}}" data-depth="1">
+          <td class="unit">
+            <a href="#"><i class="icon-caret-right icon toggle-collapse"></i></a> 
+            {{display_name}}
+          </td>
+          <td class="date start-date"><a>{{start}}</a></td>
+          <td class="date due-date"><a>{{due}}</a></td>
+          <td><a href="#" class="remove-unit">
+            <i class="icon-remove-sign icon"></i> ${_('remove')}
+          </a></td>
+        </tr>
+        {{#each children}}
+          <tr class="sequential collapsed" data-depth="2" 
+              data-location="{{../location}} {{location}}">
+            <td class="unit">
+              <a href="#"><i class="icon-caret-right icon toggle-collapse"></i></a> 
+              {{display_name}}
+            </td>
+            <td class="date start-date"><a>{{start}}</a></td>
+            <td class="date due-date"><a>{{due}}</a></td>
+            <td><a href="#" class="remove-unit">
+              <i class="icon-remove-sign icon"></i> ${_('remove')}
+            </a></td>
+          </tr>
+          {{#each children}}
+            <tr class="vertical" data-dapth="3"
+                data-location="{{../../location}} {{../location}} {{location}}">
+              <td class="unit">&nbsp;{{display_name}}</td>
+              <td class="date start-date"><a>{{start}}</a></td>
+              <td class="date due-date"><a>{{due}}</a></td>
+              <td><a href="#" class="remove-unit">
+                <i class="icon-remove-sign icon"></i> ${_('remove')}
+              </a></td>
+          {{/each}}
+        {{/each}}
+      {{/each}}
+    </tbody>
+  </table>
+</script>
+
+<div class="poc-schedule-container">
+  <div id="poc-schedule"></div>
+</div>
+
+<section id="enter-date-modal" class="modal" aria-hidden="true">
+  <div class="inner-wrapper" role="dialog">
+    <button class="close-modal">
+      <i class="icon-remove"></i>
+      <span class="sr">
+        ${_("Close")}
+      </span>
+    </button>
+    <header>
+      <h2></h2>
+    </header>
+    <form role="form">
+      <div class="field">
+        <label></label>
+        <input type="date" name="date"/>
+        <input type="time" name="time"/>
+      </div>
+      <div class="field">
+        <button type="submit" class="btn btn-primary">${_('Set date')}</button>
+      </div>
+    </form>
+  </div>
+</section>
+
+<div class="poc-schedule-sidebar">
+  <div class="poc-sidebar-panel" id="dirty-schedule">
+    <h2>${_('Save changes')}</h2>
+    <form role="form">
+      <p>${_("You have unsaved changes.")}</p>
+      <div class="field">
+        <br/>
+        <button id="save-changes">${_("Save changes")}</button>
+      </div>
+    </form>
+  </div>
+  <div class="poc-sidebar-panel" id="ajax-error">
+    <h2>${_('Error')}</h2>
+    <p>${_("There was an error saving changes.")}</p>
+  </div>
+  <div class="poc-sidebar-panel">
+    <h2>${_('Schedule a Unit')}</h2>
+    <form role="form" id="add-unit" name="add-unit" class="poc-form">
+      <div class="field">
+        <b>${_('Chapter')}</b><br/>
+        <select name="chapter"></select>
+      </div>
+      <div class="field">
+        <b>${_('Sequential')}</b><br/>
+        <select name="sequential"></select>
+      </div>
+      <div class="field">
+        <b>${_('Vertical')}</b><br/>
+        <select name="vertical"></select>
+      </div>
+      <div class="field">
+        <b>${_('Start Date')}</b><br/>
+        <input type="date" name="start_date"/>
+        <input type="time" name="start_time"/>
+      </div>
+      <div class="field">
+        <b>${_('Due Date')}</b> ${_('(Optional)')}<br/>
+        <input type="date" name="due_date"/>
+        <input type="time" name="due_time"/>
+      </div>
+      <div class="field">
+        <br/>
+        <button id="add-unit-button">${_('Add Unit')}</button>
+      </div>
+      <div class="field">
+        <br/>
+        <button id="add-all">${_('Add All Units')}</button>
+      </div>
+    </form>
+    <div id="all-units-added">
+      ${_("All units have been added.")}
+    </div>
+  </div>
+</div>
+
+<script>
+var poc_schedule = (function () {
+  var save_url = '${save_url}';
+  var schedule = ${schedule};
+  var template = Handlebars.compile($('#poc-schedule-template').html());
+  var chapter_select = $('form#add-unit select[name="chapter"]'),
+      sequential_select = $('form#add-unit select[name="sequential"]'),
+      vertical_select = $('form#add-unit select[name="vertical"]');
+
+  var self = {
+    schedule: schedule,
+    dirty: false
+  };
+
+  function hide(unit) {
+    unit.hidden = true;
+  }
+
+  function show(unit) {
+    unit.hidden = false;
+  }
+
+  function get_datetime(which) {
+    var date = $('form#add-unit input[name=' + which + '_date]').val();
+    var time = $('form#add-unit input[name=' + which + '_time]').val();
+    if (date && time)
+      return date + ' ' + time;
+    return null;
+  }
+
+  function set_datetime(which, value) {
+    var parts = value ? value.split(' ') : ['', ''],
+        date = parts[0],
+        time = parts[1];
+    $('form#add-unit input[name=' + which + '_date]').val(date);
+    $('form#add-unit input[name=' + which + '_time]').val(time);
+  }
+
+  /**
+   * Render the course tree view.
+   */
+  self.render = function() {
+    this.hidden = pruned(this.schedule, function(node) {
+      return node.hidden || node.category !== 'vertical'});
+    this.showing = pruned(this.schedule, function(node) {
+      return !node.hidden});
+
+    // Render template
+    $('#poc-schedule').html(template({chapters: this.showing}));
+
+    // Start collapsed
+    $('table.poc-schedule .sequential,.vertical').hide();
+
+    // Click handlers for collapsible tree
+    $('table.poc-schedule .toggle-collapse').on('click', toggle_collapse);
+
+    // Hidden hover fields for empty date fields
+    $('table.poc-schedule .date a').each(function() {
+      if (! $(this).text()) {
+        $(this).text('Set date').addClass('empty');
+      }
+    });
+    
+    // Handle date edit clicks
+    $('table.poc-schedule .date a').attr('href', '#enter-date-modal')
+      .leanModal({closeButton: '.close-modal'});
+    $('table.poc-schedule .due-date a').on('click', enterNewDate('due'));
+    $('table.poc-schedule .start-date a').on('click', enterNewDate('start'));
+
+    // Click handler for remove all
+    $('table.poc-schedule a#remove-all').on('click', function(event) {
+      event.preventDefault();
+      schedule_apply(self.schedule, hide);
+      self.dirty = true;
+      self.render();
+    });
+
+    // Show or hide form
+    if (this.hidden.length) {
+      // Populate chapters select, depopulate others
+      chapter_select.html('')
+        .append('<option value="none">${_("Select a chapter")}...</option>')
+        .append(schedule_options(this.hidden));
+      sequential_select.html('').prop('disabled', true);
+      vertical_select.html('').prop('disabled', true);
+      $('form#add-unit').show();
+      $('#all-units-added').hide();
+      $('#add-unit-button').prop('disabled', true);
+    }
+    else {
+      $('form#add-unit').hide();
+      $('#all-units-added').show();
+    }
+
+    // Add unit handlers
+    chapter_select.on('change', function(event) {
+      var chapter_location = chapter_select.val();
+      vertical_select.html('').prop('disabled', true);
+      if (chapter_location !== 'none') {
+        chapter = find_unit(self.hidden, chapter_location);
+        sequential_select.html('')
+          .append('<option value="all">${_("All sequentials")}</option>')
+          .append(schedule_options(chapter.children));
+        sequential_select.prop('disabled', false);
+        $('#add-unit-button').prop('disabled', false);
+        set_datetime('start', chapter.start);
+        set_datetime('due', chapter.due);
+      }
+      else {
+        sequential_select.html('').prop('disabled', true);
+      }
+    });
+
+    sequential_select.on('change', function(event) {
+      var sequential_location = sequential_select.val();
+      if (sequential_location !== 'all') {
+        var chapter = chapter_select.val();
+        sequential = find_unit(self.hidden, chapter, sequential_location);
+        vertical_select.html('')
+          .append('<option value="all">${_("All verticals")}</option>')
+          .append(schedule_options(sequential.children));
+        vertical_select.prop('disabled', false);
+        set_datetime('start', sequential.start);
+        set_datetime('due', sequential.due);
+      }
+      else {
+        vertical_select.html('').prop('disabled', true);
+      } 
+    });
+    
+    vertical_select.on('change', function(event) {
+      var vertical_location = vertical_select.val();
+      if (vertical_location !== 'all') {
+        var chapter = chapter_select.val(),
+            sequential = sequential_select.val();
+        vertical = find_unit(
+          self.hidden, chapter, sequential, vertical_location);
+        set_datetime('start', vertical.start);
+        set_datetime('due', vertical.due);
+      }
+    });
+
+    // Add unit handler
+    $('#add-unit-button').on('click', function(event) {
+      event.preventDefault();
+      var chapter = chapter_select.val(),
+          sequential = sequential_select.val(),
+          vertical = vertical_select.val(),
+          units = find_lineage(self.schedule,
+            chapter,
+            sequential == 'all' ? null : sequential,
+            vertical == 'all' ? null: vertical),
+          start = get_datetime('start'),
+          due = get_datetime('due');
+      units.map(show);
+      unit = units[units.length - 1]
+      schedule_apply([unit], show);      
+      if (start) unit.start = start;
+      if (due) unit.due = due;
+      self.dirty = true;
+      self.render();
+    });
+
+    // Remove unit handler
+    $('table.poc-schedule a.remove-unit').on('click', function(event) {
+      var row = $(this).closest('tr'),
+          path = row.data('location').split(' '),
+          unit = find_unit(self.schedule, path[0], path[1], path[2]);
+      schedule_apply([unit], hide);
+      self.dirty = true;
+      self.render(); 
+    });
+
+    // Show or hide save button
+    if (this.dirty) $('#dirty-schedule').show()
+    else $('#dirty-schedule').hide();
+
+    // Handle save button
+    $('#dirty-schedule #save-changes').on('click', function(event) {
+        event.preventDefault();
+        self.save();
+    });
+
+    $('#ajax-error').hide();
+  }
+
+  /**
+   * Handle date entry.
+   */
+  function enterNewDate(what) {
+    return function(event) {
+      var row = $(this).closest('tr');
+      var modal = $('#enter-date-modal')
+        .data('what', what)
+        .data('location', row.data('location'));
+      modal.find('h2').text(
+          what == 'due' ? '${_("Enter Due Date")}' : 
+              '${_("Enter Start Date")}');
+      modal.find('label').text(row.find('td:first').text());
+
+      var path = row.data('location').split(' '),
+          unit = find_unit(self.schedule, path[0], path[1], path[2]),
+          parts = unit[what] ? unit[what].split(' ') : ['', ''],
+          date = parts[0],
+          time = parts[1];
+
+      modal.find('input[name=date]').val(date);
+      modal.find('input[name=time]').val(time);
+
+      modal.find('form').off('submit').on('submit', function(event) {
+        event.preventDefault();
+        var date = $(this).find('input[name=date]').val(),
+            time = $(this).find('input[name=time]').val();
+        unit[what] = date + ' ' + time;
+        modal.find('.close-modal').click();
+        self.dirty = true;
+        self.render();
+      });
+    }
+  }
+
+  /**
+   * Returns a sequence of <option> DOM elements for a particular sequence
+   * of schedule nodes.
+   */
+  function schedule_options(nodes) {
+    return nodes.map(function(node) {
+      return $('<option>')
+        .attr('value', node.location)
+        .text(node.display_name)[0];
+    });
+  }
+
+  /**
+   * Return a schedule pruned by the given filter function.
+   */
+  function pruned(tree, filter) {
+    return tree.filter(filter)
+      .map(function(node) {
+        var copy = {};
+        $.extend(copy, node);
+        if (node.children) copy.children = pruned(node.children, filter);
+        return copy;
+      })
+      .filter(function(node) {
+        return node.children === undefined || node.children.length;
+      });
+  }
+
+  /**
+   * Get table rows that represent the children of given row in the schedule
+   * tree.
+   */
+  function get_children(row) {
+    var depth = $(row).data('depth');
+    return $(row).nextUntil(
+      $(row).siblings().filter(function() {
+        return $(this).data('depth') <= depth;
+      })
+    );
+  }
+
+  /**
+   * Handle click event for expanding/collapsing nodes in the schedule tree.
+   */
+  function toggle_collapse(event) {
+    event.preventDefault();
+    var row = $(this).closest('tr');
+    var children = get_children(row);
+
+    if (row.is('.expanded')) {
+      $(this).removeClass('icon-caret-down').addClass('icon-caret-right');
+      row.removeClass('expanded').addClass('collapsed');
+      children.hide(); 
+    }
+
+    else {
+      $(this).removeClass('icon-caret-right').addClass('icon-caret-down');
+      row.removeClass('collapsed').addClass('expanded');
+      children.filter('.collapsed').each(function() {
+        children = children.not(get_children(this));
+      });
+      children.show(); 
+    }
+  }
+
+  /**
+   * Save changes.
+   */
+  self.save = function() {
+    var button = $('#dirty-schedule #save-changes');
+    button.prop('disabled', true).text('${_("Saving")}...');
+    
+    $.ajax({
+        url: save_url,
+        type: 'POST',
+        contentType: 'application/json',
+        data: JSON.stringify(this.schedule),
+        success: function(data, textStatus, jqXHR) {
+          self.schedule = data;
+          self.dirty = false;
+          self.render();
+          button.prop('disabled', false).text('${_("Save changes")}');
+        },
+        error: function(jqXHR, textStatus, error) {
+          console.log(jqXHR.responseText);
+          $('#ajax-error').show();
+          $('#dirty-schedule').hide();
+          $('form#add-unit select,input,button').prop('disabled', true);
+        }
+      });
+  }
+
+  /**
+   * Add all nodes.
+   */
+  $('#add-all').on('click', function(event) {
+      event.preventDefault();
+      schedule_apply(self.schedule, show);
+      self.dirty = true;
+      self.render();
+  });
+
+  /**
+   * Visit every tree node, applying function.
+   */
+  function schedule_apply(nodes, f) {
+    nodes.map(function(node) {
+      f(node);
+      if (node.children !== undefined) schedule_apply(node.children, f);
+    });
+  }
+
+  /**
+   * Find a unit in the tree.
+   */
+  function find_unit(tree, chapter, sequential, vertical) {
+    var units = find_lineage(tree, chapter, sequential, vertical);
+    return units[units.length -1];
+  }
+
+  function find_lineage(tree, chapter, sequential, vertical) {
+    function find_in(seq, location) {
+      for (var i = 0; i < seq.length; i++)
+        if (seq[i].location === location)
+          return seq[i];
+    }
+
+    var units = [],
+        unit = find_in(tree, chapter);
+    units[units.length] = unit;
+    if (sequential) {
+      units[units.length] = unit = find_in(unit.children, sequential);
+      if (vertical) 
+        units[units.length] = unit = find_in(unit.children, vertical);
+    }
+
+    return units;
+  }
+
+  return self;
+})();
+
+$(function() { poc_schedule.render(); });
+</script>
diff --git a/lms/templates/pocs/unenroll_email_allowedmessage.txt b/lms/templates/pocs/unenroll_email_allowedmessage.txt
new file mode 100644
index 0000000000000000000000000000000000000000..6eca90b8d5d4998a2c2b2ddd1c65b73b2d11d8a2
--- /dev/null
+++ b/lms/templates/pocs/unenroll_email_allowedmessage.txt
@@ -0,0 +1,13 @@
+<%! from django.utils.translation import ugettext as _ %>
+
+${_("Dear Student,")}
+
+${_("You have been un-enrolled from course {course_name} by a member "
+    "of the course staff. Please disregard the invitation "
+    "previously sent.").format(course_name=course.display_name)}
+
+----
+${_("This email was automatically sent from {site_name} "
+	"to {email_address}").format(
+		site_name=site_name, email_address=email_address
+	)}
diff --git a/lms/templates/pocs/unenroll_email_enrolledmessage.txt b/lms/templates/pocs/unenroll_email_enrolledmessage.txt
new file mode 100644
index 0000000000000000000000000000000000000000..38770886c08b994a5e8fce5126b651e691ab5c9c
--- /dev/null
+++ b/lms/templates/pocs/unenroll_email_enrolledmessage.txt
@@ -0,0 +1,17 @@
+<%! from django.utils.translation import ugettext as _ %>
+
+${_("Dear {full_name}").format(full_name=full_name)}
+
+${_("You have been un-enrolled in {course_name} at {site_name} by a member "
+    "of the course staff. The course will no longer appear on your "
+    "{site_name} dashboard.").format(
+    	course_name=course.display_name, site_name=site_name
+    )}
+
+${_("Your other courses have not been affected.")}
+
+----
+${_("This email was automatically sent from {site_name} to "
+	"{full_name}").format(
+    	full_name=full_name, site_name=site_name
+    )}
diff --git a/lms/templates/pocs/unenroll_email_subject.txt b/lms/templates/pocs/unenroll_email_subject.txt
new file mode 100644
index 0000000000000000000000000000000000000000..4b971dcb35458bdecc5629d380bf8a27404c7ce0
--- /dev/null
+++ b/lms/templates/pocs/unenroll_email_subject.txt
@@ -0,0 +1,5 @@
+<%! from django.utils.translation import ugettext as _ %>
+
+${_("You have been un-enrolled from {course_name}").format(
+	course_name=course.display_name
+)}
diff --git a/lms/urls.py b/lms/urls.py
index 512cb159ba1fdc1a02fc6f232e32f2001731813b..cc824c638cbb3c9b0a14e430da764304f10a97da 100644
--- a/lms/urls.py
+++ b/lms/urls.py
@@ -343,6 +343,15 @@ if settings.COURSEWARE_ENABLED:
         # For the instructor
         url(r'^courses/{}/instructor$'.format(settings.COURSE_ID_PATTERN),
             'instructor.views.instructor_dashboard.instructor_dashboard_2', name="instructor_dashboard"),
+        url(r'^courses/{}/poc_coach$'.format(settings.COURSE_ID_PATTERN),
+            'pocs.views.dashboard', name='poc_coach_dashboard'),
+        url(r'^courses/{}/create_poc$'.format(settings.COURSE_ID_PATTERN),
+            'pocs.views.create_poc', name='create_poc'),
+        url(r'^courses/{}/save_poc$'.format(settings.COURSE_ID_PATTERN),
+            'pocs.views.save_poc', name='save_poc'),
+        url(r'^courses/{}/poc_invite$'.format(settings.COURSE_ID_PATTERN),
+            'pocs.views.poc_invite', name='poc_invite'),
+
         url(r'^courses/{}/set_course_mode_price$'.format(settings.COURSE_ID_PATTERN),
             'instructor.views.instructor_dashboard.set_course_mode_price', name="set_course_mode_price"),
         url(r'^courses/{}/instructor/api/'.format(settings.COURSE_ID_PATTERN),