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.
This commit is contained in:
@@ -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):
|
||||
|
||||
0
lms/djangoapps/pocs/__init__.py
Normal file
0
lms/djangoapps/pocs/__init__.py
Normal file
116
lms/djangoapps/pocs/migrations/0001_initial.py
Normal file
116
lms/djangoapps/pocs/migrations/0001_initial.py
Normal file
@@ -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']
|
||||
@@ -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']
|
||||
0
lms/djangoapps/pocs/migrations/__init__.py
Normal file
0
lms/djangoapps/pocs/migrations/__init__.py
Normal file
44
lms/djangoapps/pocs/models.py
Normal file
44
lms/djangoapps/pocs/models.py
Normal file
@@ -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')
|
||||
86
lms/djangoapps/pocs/overrides.py
Normal file
86
lms/djangoapps/pocs/overrides.py
Normal file
@@ -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
|
||||
0
lms/djangoapps/pocs/tests/__init__.py
Normal file
0
lms/djangoapps/pocs/tests/__init__.py
Normal file
112
lms/djangoapps/pocs/tests/test_overrides.py
Normal file
112
lms/djangoapps/pocs/tests/test_overrides.py
Normal file
@@ -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)
|
||||
166
lms/djangoapps/pocs/tests/test_views.py
Normal file
166
lms/djangoapps/pocs/tests/test_views.py
Normal file
@@ -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]
|
||||
213
lms/djangoapps/pocs/utils.py
Normal file
213
lms/djangoapps/pocs/utils.py
Normal file
@@ -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
|
||||
)
|
||||
244
lms/djangoapps/pocs/views.py
Normal file
244
lms/djangoapps/pocs/views.py
Normal file
@@ -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)
|
||||
Reference in New Issue
Block a user