Finish implementing CCX coach as staff

parent 8533fd97
......@@ -647,9 +647,14 @@ class CcxListTest(CcxRestApiTest):
list_staff_ccx_course = list_with_level(course_ccx, 'staff')
list_instructor_ccx_course = list_with_level(course_ccx, 'instructor')
self.assertEqual(len(list_staff_master_course), len(list_staff_ccx_course))
for course_user, ccx_user in izip(sorted(list_staff_master_course), sorted(list_staff_ccx_course)):
self.assertEqual(course_user, ccx_user)
# The "Coach" in the parent course becomes "Staff" on the CCX, so the CCX should have 1 "Staff"
# user more than the parent course
self.assertEqual(len(list_staff_master_course) + 1, len(list_staff_ccx_course))
# Make sure all of the existing course staff are passed to the CCX
for course_user in list_staff_master_course:
self.assertIn(course_user, list_staff_ccx_course)
# Make sure the "Coach" on the parent course is "Staff" on the CCX
self.assertIn(self.coach, list_staff_ccx_course)
self.assertEqual(len(list_instructor_master_course), len(list_instructor_ccx_course))
for course_user, ccx_user in izip(sorted(list_instructor_master_course), sorted(list_instructor_ccx_course)):
self.assertEqual(course_user, ccx_user)
......
......@@ -766,8 +766,8 @@ class CCXDetailView(GenericAPIView):
email_students=True,
email_params=email_params,
)
# enroll the coach to the newly created ccx
assign_coach_role_to_ccx(ccx_course_key, coach, master_course_object.id)
# make the new coach staff on the CCX
assign_staff_role_to_ccx(ccx_course_key, coach, master_course_object.id)
# using CCX object as sender here.
responses = SignalHandler.course_published.send(
......
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import logging
from django.db import migrations, models
# We're doing something awful here, but it's necessary for the greater good:
from django.contrib.auth.models import User
from instructor.access import allow_access, revoke_access
from django.db import migrations
from ccx_keys.locator import CCXLocator
from instructor.access import allow_access, revoke_access
from lms.djangoapps.ccx.utils import ccx_course
log = logging.getLogger("edx.ccx")
def change_existing_ccx_coaches_to_staff(apps, schema_editor):
"""
Modify all coaches of CCX courses so that they have the staff role on the
......@@ -23,6 +24,10 @@ def change_existing_ccx_coaches_to_staff(apps, schema_editor):
"""
CustomCourseForEdX = apps.get_model('ccx', 'CustomCourseForEdX')
db_alias = schema_editor.connection.alias
if not db_alias == 'default':
# This migration is not intended to run against the student_module_history database and
# will fail if it does. Ensure that it'll only run against the default database.
return
list_ccx = CustomCourseForEdX.objects.using(db_alias).all()
for ccx in list_ccx:
ccx_locator = CCXLocator.from_course_locator(ccx.course_id, unicode(ccx.id))
......@@ -30,6 +35,10 @@ def change_existing_ccx_coaches_to_staff(apps, schema_editor):
coach = User.objects.get(id=ccx.coach.id)
allow_access(course, coach, 'staff', send_email=False)
revoke_access(course, coach, 'ccx_coach', send_email=False)
log.info(
'The CCX coach of CCX %s has been switched from "CCX Coach" to "Staff".',
unicode(ccx_locator)
)
def revert_ccx_staff_to_coaches(apps, schema_editor):
"""
......@@ -43,6 +52,8 @@ def revert_ccx_staff_to_coaches(apps, schema_editor):
"""
CustomCourseForEdX = apps.get_model('ccx', 'CustomCourseForEdX')
db_alias = schema_editor.connection.alias
if not db_alias == 'default':
return
list_ccx = CustomCourseForEdX.objects.using(db_alias).all()
for ccx in list_ccx:
ccx_locator = CCXLocator.from_course_locator(ccx.course_id, unicode(ccx.id))
......@@ -50,6 +61,10 @@ def revert_ccx_staff_to_coaches(apps, schema_editor):
coach = User.objects.get(id=ccx.coach.id)
allow_access(course, coach, 'ccx_coach', send_email=False)
revoke_access(course, coach, 'staff', send_email=False)
log.info(
'The CCX coach of CCX %s has been switched from "Staff" to "CCX Coach".',
unicode(ccx_locator)
)
class Migration(migrations.Migration):
......@@ -57,6 +72,7 @@ class Migration(migrations.Migration):
('ccx', '0001_initial'),
('ccx', '0002_customcourseforedx_structure_json'),
('ccx', '0003_add_master_course_staff_in_ccx'),
('ccx', '0004_seed_forum_roles_in_ccx_courses'),
]
operations = [
......
"""
Models for the custom course feature
"""
from __future__ import unicode_literals
import json
import logging
from datetime import datetime
......@@ -8,8 +9,9 @@ from datetime import datetime
from django.contrib.auth.models import User
from django.db import models
from pytz import utc
from lazy import lazy
from ccx_keys.locator import CCXLocator
from openedx.core.lib.time_zone_utils import get_time_zone_abbr
from xmodule_django.models import CourseKeyField, LocationKeyField
from xmodule.error_module import ErrorDescriptor
......@@ -121,6 +123,16 @@ class CustomCourseForEdX(models.Model):
return json.loads(self.structure_json)
return None
@property
def locator(self):
"""
Helper property that gets a corresponding CCXLocator for this CCX.
Returns:
The CCXLocator corresponding to this CCX.
"""
return CCXLocator.from_course_locator(self.course_id, unicode(self.id))
class CcxFieldOverride(models.Model):
"""
......
......@@ -12,7 +12,10 @@ from student.tests.factories import (
AdminFactory,
)
from util.tests.test_date_utils import fake_ugettext
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.django_utils import (
ModuleStoreTestCase,
TEST_DATA_SPLIT_MODULESTORE
)
from xmodule.modulestore.tests.factories import (
CourseFactory,
check_mongo_calls
......@@ -30,6 +33,8 @@ class TestCCX(ModuleStoreTestCase):
"""Unit tests for the CustomCourseForEdX model
"""
MODULESTORE = TEST_DATA_SPLIT_MODULESTORE
def setUp(self):
"""common setup for all tests"""
super(TestCCX, self).setUp()
......@@ -51,7 +56,7 @@ class TestCCX(ModuleStoreTestCase):
def test_ccx_course_caching(self):
"""verify that caching the propery works to limit queries"""
with check_mongo_calls(1):
with check_mongo_calls(3):
# these statements are used entirely to demonstrate the
# instance-level caching of these values on CCX objects. The
# check_mongo_calls context is the point here.
......@@ -77,7 +82,7 @@ class TestCCX(ModuleStoreTestCase):
"""verify that caching the start property works to limit queries"""
now = datetime.now(utc)
self.set_ccx_override('start', now)
with check_mongo_calls(1):
with check_mongo_calls(3):
# these statements are used entirely to demonstrate the
# instance-level caching of these values on CCX objects. The
# check_mongo_calls context is the point here.
......@@ -102,7 +107,7 @@ class TestCCX(ModuleStoreTestCase):
"""verify that caching the due property works to limit queries"""
expected = datetime.now(utc)
self.set_ccx_override('due', expected)
with check_mongo_calls(1):
with check_mongo_calls(3):
# these statements are used entirely to demonstrate the
# instance-level caching of these values on CCX objects. The
# check_mongo_calls context is the point here.
......@@ -269,3 +274,10 @@ class TestCCX(ModuleStoreTestCase):
)
self.assertEqual(ccx.structure_json, json_struct) # pylint: disable=no-member
self.assertEqual(ccx.structure, dummy_struct) # pylint: disable=no-member
def test_locator_property(self):
"""
Verify that the locator helper property returns a correct CCXLocator
"""
locator = self.ccx.locator # pylint: disable=no-member
self.assertEqual(self.ccx.id, long(locator.ccx))
......@@ -418,8 +418,8 @@ class TestCoachDashboard(CcxTestCase, LoginEnrollmentTestCase):
course_enrollments = get_override_for_ccx(ccx, self.course, 'max_student_enrollments_allowed')
self.assertEqual(course_enrollments, settings.CCX_MAX_STUDENTS_ALLOWED)
# assert ccx creator has role=ccx_coach
role = CourseCcxCoachRole(course_key)
# assert ccx creator has role=staff
role = CourseStaffRole(course_key)
self.assertTrue(role.has_user(self.coach))
# assert that staff and instructors of master course has staff and instructor roles on ccx
......@@ -432,8 +432,12 @@ class TestCoachDashboard(CcxTestCase, LoginEnrollmentTestCase):
with ccx_course(course_key) as course_ccx:
list_staff_ccx_course = list_with_level(course_ccx, 'staff')
self.assertEqual(len(list_staff_master_course), len(list_staff_ccx_course))
self.assertEqual(list_staff_master_course[0].email, list_staff_ccx_course[0].email)
# The "Coach" in the parent course becomes "Staff" on the CCX, so the CCX should have 1 "Staff"
# user more than the parent course
self.assertEqual(len(list_staff_master_course) + 1, len(list_staff_ccx_course))
self.assertIn(list_staff_master_course[0].email, [ccx_staff.email for ccx_staff in list_staff_ccx_course])
# Make sure the "Coach" on the parent course is "Staff" on the CCX
self.assertIn(self.coach, list_staff_ccx_course)
list_instructor_ccx_course = list_with_level(course_ccx, 'instructor')
self.assertEqual(len(list_instructor_ccx_course), len(list_instructor_master_course))
......
......@@ -199,6 +199,8 @@ class XQueueCertInterface(object):
Will change the certificate status to 'generating' or
`downloadable` in case of web view certificates.
The course must not be a CCX.
Certificate must be in the 'unavailable', 'error',
'deleted' or 'generating' state.
......@@ -214,6 +216,18 @@ class XQueueCertInterface(object):
Returns the newly created certificate instance
"""
if hasattr(course_id, 'ccx'):
LOGGER.warning(
(
u"Cannot create certificate generation task for user %s "
u"in the course '%s'; "
u"certificates are not allowed for CCX courses."
),
student.id,
unicode(course_id)
)
return None
valid_statuses = [
status.generating,
status.unavailable,
......
......@@ -77,6 +77,20 @@ class InstructorDashboardTab(CourseTab):
return bool(user and has_access(user, 'staff', course, course.id))
def show_analytics_dashboard_message(course_key):
"""
Defines whether or not the analytics dashboard URL should be displayed.
Arguments:
course_key (CourseLocator): The course locator to display the analytics dashboard message on.
"""
if hasattr(course_key, 'ccx'):
ccx_analytics_enabled = settings.FEATURES.get('ENABLE_CCX_ANALYTICS_DASHBOARD_URL', False)
return settings.ANALYTICS_DASHBOARD_URL and ccx_analytics_enabled
return settings.ANALYTICS_DASHBOARD_URL
@ensure_csrf_cookie
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
def instructor_dashboard_2(request, course_id):
......@@ -112,7 +126,7 @@ def instructor_dashboard_2(request, course_id):
]
analytics_dashboard_message = None
if settings.ANALYTICS_DASHBOARD_URL:
if show_analytics_dashboard_message(course_key):
# Construct a URL to the external analytics dashboard
analytics_dashboard_url = '{0}/courses/{1}'.format(settings.ANALYTICS_DASHBOARD_URL, unicode(course_key))
link_start = HTML("<a href=\"{}\" target=\"_blank\">").format(analytics_dashboard_url)
......@@ -169,7 +183,8 @@ def instructor_dashboard_2(request, course_id):
# Certificates panel
# This is used to generate example certificates
# and enable self-generated certificates for a course.
certs_enabled = CertificateGenerationConfiguration.current().enabled
# Note: This is hidden for all CCXs
certs_enabled = CertificateGenerationConfiguration.current().enabled and not hasattr(course_key, 'ccx')
if certs_enabled and access['admin']:
sections.append(_section_certificates(course))
......@@ -421,7 +436,7 @@ def _section_course_info(course, access):
if settings.FEATURES.get('DISPLAY_ANALYTICS_ENROLLMENTS'):
section_data['enrollment_count'] = CourseEnrollment.objects.enrollment_counts(course_key)
if settings.ANALYTICS_DASHBOARD_URL:
if show_analytics_dashboard_message(course_key):
# dashboard_link is already made safe in _get_dashboard_link
dashboard_link = _get_dashboard_link(course_key)
# so we can use Text() here so it's not double-escaped and rendering HTML on the front-end
......
......@@ -356,6 +356,11 @@ FEATURES = {
# lives in the Extended table, saving the frontend from
# making multiple queries.
'ENABLE_READING_FROM_MULTIPLE_HISTORY_TABLES': True,
# Display the 'Analytics' tab in the instructor dashboard for CCX courses.
# Note: This has no effect unless ANALYTICS_DASHBOARD_URL is already set,
# because without that setting, the tab does not show up for any courses.
'ENABLE_CCX_ANALYTICS_DASHBOARD_URL': False,
}
# Ignore static asset files on import which match this 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