Commit a2cb7fd2 by Chris Rossi Committed by cewing

MIT: CCX. Implement Custom Courses for Edx.

This feature provides the ability to designate a "coach" who can create customized runs of an existing course, invite students to participate, and manage students through the run of the course.

In this squashed commit we implement the initial scifi, add the 'POC Coach' course role, refine the scifi, add migrations for models, create POCs, enforce POC Coach role, provide panels for Coach Dashboard, set up rudimentary display of course outline, add and remove units, show/hide all units, and save schedule changes, set dates when adding units, edit dates on units already added and provide some tests.

We also provide mechanisms for invitation and enrollment in a POC (to become CCX) and control the display of blocks to students in a POC.
parent 3256eb1f
......@@ -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):
......
......@@ -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(
......
......@@ -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
......
......@@ -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,
}
......
......@@ -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):
......
# -*- 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
# -*- 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
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')
"""
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
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)
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]
"""
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
)
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)
......@@ -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,
......
......@@ -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',)
......@@ -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";
.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;
}
}
......@@ -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>
<%! 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>
<%! 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
)}
<%! from django.utils.translation import ugettext as _ %>
${_("You have been invited to register for {course_name}").format(
course_name=course.display_name
)}
<%! 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
)}
<%! from django.utils.translation import ugettext as _ %>
${_("You have been enrolled in {course_name}").format(
course_name=course.display_name
)}
<%! 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>
<%! 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
)}
<%! 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
)}
<%! from django.utils.translation import ugettext as _ %>
${_("You have been un-enrolled from {course_name}").format(
course_name=course.display_name
)}
......@@ -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),
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment