Commit 42e78463 by Peter Fogg

Revert "Merge pull request #8986 from jazkarta/remove-ccx-enrollment"

This reverts commit 53db053c, reversing
changes made to 386a8a27.
parent 440c1575
...@@ -1200,6 +1200,12 @@ class CourseEnrollment(models.Model): ...@@ -1200,6 +1200,12 @@ class CourseEnrollment(models.Model):
if not user.is_authenticated(): if not user.is_authenticated():
return False return False
# unwrap CCXLocators so that we use the course as the access control
# source
from ccx_keys.locator import CCXLocator
if isinstance(course_key, CCXLocator):
course_key = course_key.to_course_locator()
try: try:
record = CourseEnrollment.objects.get(user=user, course_id=course_key) record = CourseEnrollment.objects.get(user=user, course_id=course_key)
return record.is_active return record.is_active
......
...@@ -655,6 +655,13 @@ def dashboard(request): ...@@ -655,6 +655,13 @@ def dashboard(request):
) )
courses_requirements_not_met = get_pre_requisite_courses_not_completed(user, courses_having_prerequisites) courses_requirements_not_met = get_pre_requisite_courses_not_completed(user, courses_having_prerequisites)
ccx_membership_triplets = []
if settings.FEATURES.get('CUSTOM_COURSES_EDX', False):
from ccx.utils import get_ccx_membership_triplets
ccx_membership_triplets = get_ccx_membership_triplets(
user, course_org_filter, org_filter_out_set
)
if 'notlive' in request.GET: if 'notlive' in request.GET:
redirect_message = _("The course you are looking for does not start until {date}.").format( redirect_message = _("The course you are looking for does not start until {date}.").format(
date=request.GET['notlive'] date=request.GET['notlive']
...@@ -690,6 +697,7 @@ def dashboard(request): ...@@ -690,6 +697,7 @@ def dashboard(request):
'provider_states': [], 'provider_states': [],
'order_history_list': order_history_list, 'order_history_list': order_history_list,
'courses_requirements_not_met': courses_requirements_not_met, 'courses_requirements_not_met': courses_requirements_not_met,
'ccx_membership_triplets': ccx_membership_triplets,
'nav_hidden': True, 'nav_hidden': True,
} }
...@@ -1896,6 +1904,16 @@ def activate_account(request, key): ...@@ -1896,6 +1904,16 @@ def activate_account(request, key):
manual_enrollment_audit.reason, enrollment manual_enrollment_audit.reason, enrollment
) )
# enroll student in any pending CCXs he/she may have if auto_enroll flag is set
if settings.FEATURES.get('CUSTOM_COURSES_EDX'):
from ccx.models import CcxMembership, CcxFutureMembership
ccxfms = CcxFutureMembership.objects.filter(
email=student[0].email
)
for ccxfm in ccxfms:
if ccxfm.auto_enroll:
CcxMembership.auto_enroll(student[0], ccxfm)
resp = render_to_response( resp = render_to_response(
"registration/activation_complete.html", "registration/activation_complete.html",
{ {
......
# -*- coding: utf-8 -*-
from south.utils import datetime_utils as datetime
from south.db import db
from south.v2 import DataMigration
from django.db import models
class Migration(DataMigration):
def forwards(self, orm):
"Convert CCX Memberships to Course Enrollments."
from ccx_keys.locator import CCXLocator
memberships = orm['ccx.CcxMembership'].objects.select_related('ccx', 'student').all()
for membership in memberships:
ccx = membership.ccx
course_key = CCXLocator.from_course_locator(ccx.course_id, ccx.id)
enrollment, created = orm['student.CourseEnrollment'].objects.get_or_create(
user=membership.student,
course_id=course_key,
)
def backwards(self, orm):
"""In the future, here we will convert back CCX Course Enrollments to CCX
Memberships.
"""
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'})
},
'ccx.ccxfieldoverride': {
'Meta': {'unique_together': "(('ccx', 'location', 'field'),)", 'object_name': 'CcxFieldOverride'},
'ccx': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['ccx.CustomCourseForEdX']"}),
'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'}),
'value': ('django.db.models.fields.TextField', [], {'default': "'null'"})
},
'ccx.customcourseforedx': {
'Meta': {'object_name': 'CustomCourseForEdX'},
'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'})
},
'ccx.ccxmembership': {
'Meta': {'object_name': 'CcxMembership'},
'active': ('django.db.models.fields.BooleanField', [], {'default': False}),
'ccx': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['ccx.CustomCourseForEdX']"}),
'student': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
},
'student.courseenrollment': {
'Meta': {'object_name': 'CourseEnrollment'},
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': True}),
'mode': ('django.db.models.fields.CharField', [], {'max_length': '100', 'default': '"honor"'}),
'created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
},
'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'})
}
}
complete_apps = ['ccx', 'ccx']
symmetrical = True
...@@ -9,6 +9,7 @@ from django.db import models ...@@ -9,6 +9,7 @@ from django.db import models
from django.utils.timezone import UTC from django.utils.timezone import UTC
from lazy import lazy from lazy import lazy
from student.models import CourseEnrollment, AlreadyEnrolledError # pylint: disable=import-error
from xmodule_django.models import CourseKeyField, LocationKeyField # pylint: disable=import-error from xmodule_django.models import CourseKeyField, LocationKeyField # pylint: disable=import-error
from xmodule.error_module import ErrorDescriptor from xmodule.error_module import ErrorDescriptor
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
...@@ -95,6 +96,52 @@ class CustomCourseForEdX(models.Model): ...@@ -95,6 +96,52 @@ class CustomCourseForEdX(models.Model):
return value return value
class CcxMembership(models.Model):
"""
Which students are in a CCX?
"""
ccx = models.ForeignKey(CustomCourseForEdX, db_index=True)
student = models.ForeignKey(User, db_index=True)
active = models.BooleanField(default=False)
@classmethod
def auto_enroll(cls, student, future_membership):
"""convert future_membership to an active membership
"""
if not future_membership.auto_enroll:
msg = "auto enrollment not allowed for {}"
raise ValueError(msg.format(future_membership))
membership = cls(
ccx=future_membership.ccx, student=student, active=True
)
try:
CourseEnrollment.enroll(
student, future_membership.ccx.course_id, check_access=True
)
except AlreadyEnrolledError:
# if the user is already enrolled in the course, great!
pass
membership.save()
future_membership.delete()
@classmethod
def memberships_for_user(cls, user, active=True):
"""
active memberships for a user
"""
return cls.objects.filter(student=user, active__exact=active)
class CcxFutureMembership(models.Model):
"""
Which emails for non-users are waiting to be added to CCX on registration
"""
ccx = models.ForeignKey(CustomCourseForEdX, db_index=True)
email = models.CharField(max_length=255)
auto_enroll = models.BooleanField(default=0)
class CcxFieldOverride(models.Model): class CcxFieldOverride(models.Model):
""" """
Field overrides for custom courses. Field overrides for custom courses.
......
...@@ -5,6 +5,8 @@ from factory import SubFactory ...@@ -5,6 +5,8 @@ from factory import SubFactory
from factory.django import DjangoModelFactory from factory.django import DjangoModelFactory
from student.tests.factories import UserFactory from student.tests.factories import UserFactory
from ccx.models import CustomCourseForEdX # pylint: disable=import-error from ccx.models import CustomCourseForEdX # pylint: disable=import-error
from ccx.models import CcxMembership # pylint: disable=import-error
from ccx.models import CcxFutureMembership # pylint: disable=import-error
class CcxFactory(DjangoModelFactory): # pylint: disable=missing-docstring class CcxFactory(DjangoModelFactory): # pylint: disable=missing-docstring
...@@ -12,3 +14,12 @@ class CcxFactory(DjangoModelFactory): # pylint: disable=missing-docstring ...@@ -12,3 +14,12 @@ class CcxFactory(DjangoModelFactory): # pylint: disable=missing-docstring
display_name = "Test CCX" display_name = "Test CCX"
id = None # pylint: disable=redefined-builtin, invalid-name id = None # pylint: disable=redefined-builtin, invalid-name
coach = SubFactory(UserFactory) coach = SubFactory(UserFactory)
class CcxMembershipFactory(DjangoModelFactory): # pylint: disable=missing-docstring
FACTORY_FOR = CcxMembership
active = False
class CcxFutureMembershipFactory(DjangoModelFactory): # pylint: disable=missing-docstring
FACTORY_FOR = CcxFutureMembership
...@@ -27,7 +27,7 @@ from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase, \ ...@@ -27,7 +27,7 @@ from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase, \
from xmodule.modulestore.tests.factories import check_mongo_calls, CourseFactory, check_sum_of_calls from xmodule.modulestore.tests.factories import check_mongo_calls, CourseFactory, check_sum_of_calls
from xmodule.modulestore.tests.utils import ProceduralCourseTestMixin from xmodule.modulestore.tests.utils import ProceduralCourseTestMixin
from ccx_keys.locator import CCXLocator from ccx_keys.locator import CCXLocator
from ccx.tests.factories import CcxFactory from ccx.tests.factories import CcxFactory, CcxMembershipFactory
@attr('shard_1') @attr('shard_1')
...@@ -65,7 +65,7 @@ class FieldOverridePerformanceTestCase(ProceduralCourseTestMixin, ...@@ -65,7 +65,7 @@ class FieldOverridePerformanceTestCase(ProceduralCourseTestMixin,
MakoMiddleware().process_request(self.request) MakoMiddleware().process_request(self.request)
def setup_course(self, size, enable_ccx, view_as_ccx): def setup_course(self, size, enable_ccx):
""" """
Build a gradable course where each node has `size` children. Build a gradable course where each node has `size` children.
""" """
...@@ -112,17 +112,18 @@ class FieldOverridePerformanceTestCase(ProceduralCourseTestMixin, ...@@ -112,17 +112,18 @@ class FieldOverridePerformanceTestCase(ProceduralCourseTestMixin,
) )
self.populate_course(size) self.populate_course(size)
course_key = self.course.id
if enable_ccx:
self.ccx = CcxFactory.create(course_id=self.course.id)
if view_as_ccx:
course_key = CCXLocator.from_course_locator(self.course.id, self.ccx.id)
CourseEnrollment.enroll( CourseEnrollment.enroll(
self.student, self.student,
course_key self.course.id
) )
if enable_ccx:
self.ccx = CcxFactory.create()
CcxMembershipFactory.create(
student=self.student,
ccx=self.ccx
)
def grade_course(self, course, view_as_ccx): def grade_course(self, course, view_as_ccx):
""" """
Renders the progress page for the given course. Renders the progress page for the given course.
...@@ -155,7 +156,7 @@ class FieldOverridePerformanceTestCase(ProceduralCourseTestMixin, ...@@ -155,7 +156,7 @@ class FieldOverridePerformanceTestCase(ProceduralCourseTestMixin,
""" """
Renders the progress page, instrumenting Mongo reads and SQL queries. Renders the progress page, instrumenting Mongo reads and SQL queries.
""" """
self.setup_course(course_width, enable_ccx, view_as_ccx) self.setup_course(course_width, enable_ccx)
# Switch to published-only mode to simulate the LMS # Switch to published-only mode to simulate the LMS
with self.settings(MODULESTORE_BRANCH='published-only'): with self.settings(MODULESTORE_BRANCH='published-only'):
......
...@@ -5,9 +5,12 @@ from datetime import datetime, timedelta ...@@ -5,9 +5,12 @@ from datetime import datetime, timedelta
from django.utils.timezone import UTC from django.utils.timezone import UTC
from mock import patch from mock import patch
from nose.plugins.attrib import attr from nose.plugins.attrib import attr
from student.models import CourseEnrollment # pylint: disable=import-error
from student.roles import CourseCcxCoachRole # pylint: disable=import-error from student.roles import CourseCcxCoachRole # pylint: disable=import-error
from student.tests.factories import ( # pylint: disable=import-error from student.tests.factories import ( # pylint: disable=import-error
AdminFactory, AdminFactory,
CourseEnrollmentFactory,
UserFactory,
) )
from util.tests.test_date_utils import fake_ugettext 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
...@@ -18,11 +21,123 @@ from xmodule.modulestore.tests.factories import ( ...@@ -18,11 +21,123 @@ from xmodule.modulestore.tests.factories import (
from .factories import ( from .factories import (
CcxFactory, CcxFactory,
CcxFutureMembershipFactory,
)
from ..models import (
CcxMembership,
CcxFutureMembership,
) )
from ..overrides import override_field_for_ccx from ..overrides import override_field_for_ccx
@attr('shard_1') @attr('shard_1')
class TestCcxMembership(ModuleStoreTestCase):
"""Unit tests for the CcxMembership model
"""
def setUp(self):
"""common setup for all tests"""
super(TestCcxMembership, self).setUp()
self.course = course = CourseFactory.create()
coach = AdminFactory.create()
role = CourseCcxCoachRole(course.id)
role.add_users(coach)
self.ccx = CcxFactory(course_id=course.id, coach=coach)
enrollment = CourseEnrollmentFactory.create(course_id=course.id)
self.enrolled_user = enrollment.user
self.unenrolled_user = UserFactory.create()
def create_future_enrollment(self, user, auto_enroll=True):
"""
utility method to create future enrollment
"""
pfm = CcxFutureMembershipFactory.create(
ccx=self.ccx,
email=user.email,
auto_enroll=auto_enroll
)
return pfm
def has_course_enrollment(self, user):
"""
utility method to create future enrollment
"""
enrollment = CourseEnrollment.objects.filter(
user=user, course_id=self.course.id
)
return enrollment.exists()
def has_ccx_membership(self, user):
"""
verify ccx membership
"""
membership = CcxMembership.objects.filter(
student=user, ccx=self.ccx, active=True
)
return membership.exists()
def has_ccx_future_membership(self, user):
"""
verify future ccx membership
"""
future_membership = CcxFutureMembership.objects.filter(
email=user.email, ccx=self.ccx
)
return future_membership.exists()
def call_mut(self, student, future_membership):
"""
Call the method undser test
"""
CcxMembership.auto_enroll(student, future_membership)
def test_ccx_auto_enroll_unregistered_user(self):
"""verify auto_enroll works when user is not enrolled in the MOOC
n.b. After auto_enroll, user will have both a MOOC enrollment and a
CCX membership
"""
user = self.unenrolled_user
pfm = self.create_future_enrollment(user)
self.assertTrue(self.has_ccx_future_membership(user))
self.assertFalse(self.has_course_enrollment(user))
# auto_enroll user
self.call_mut(user, pfm)
self.assertTrue(self.has_course_enrollment(user))
self.assertTrue(self.has_ccx_membership(user))
self.assertFalse(self.has_ccx_future_membership(user))
def test_ccx_auto_enroll_registered_user(self):
"""verify auto_enroll works when user is enrolled in the MOOC
"""
user = self.enrolled_user
pfm = self.create_future_enrollment(user)
self.assertTrue(self.has_ccx_future_membership(user))
self.assertTrue(self.has_course_enrollment(user))
self.call_mut(user, pfm)
self.assertTrue(self.has_course_enrollment(user))
self.assertTrue(self.has_ccx_membership(user))
self.assertFalse(self.has_ccx_future_membership(user))
def test_future_membership_disallows_auto_enroll(self):
"""verify that the CcxFutureMembership can veto auto_enroll
"""
user = self.unenrolled_user
pfm = self.create_future_enrollment(user, auto_enroll=False)
self.assertTrue(self.has_ccx_future_membership(user))
self.assertFalse(self.has_course_enrollment(user))
self.assertRaises(ValueError, self.call_mut, user, pfm)
self.assertFalse(self.has_course_enrollment(user))
self.assertFalse(self.has_ccx_membership(user))
self.assertTrue(self.has_ccx_future_membership(user))
@attr('shard_1')
class TestCCX(ModuleStoreTestCase): class TestCCX(ModuleStoreTestCase):
"""Unit tests for the CustomCourseForEdX model """Unit tests for the CustomCourseForEdX model
""" """
......
...@@ -20,12 +20,9 @@ from django.utils.timezone import UTC ...@@ -20,12 +20,9 @@ from django.utils.timezone import UTC
from django.test.utils import override_settings from django.test.utils import override_settings
from django.test import RequestFactory from django.test import RequestFactory
from edxmako.shortcuts import render_to_response # pylint: disable=import-error from edxmako.shortcuts import render_to_response # pylint: disable=import-error
from student.models import CourseEnrollment
from request_cache.middleware import RequestCache from request_cache.middleware import RequestCache
from student.roles import CourseCcxCoachRole # pylint: disable=import-error from student.roles import CourseCcxCoachRole # pylint: disable=import-error
from student.models import (
CourseEnrollment,
CourseEnrollmentAllowed,
)
from student.tests.factories import ( # pylint: disable=import-error from student.tests.factories import ( # pylint: disable=import-error
AdminFactory, AdminFactory,
CourseEnrollmentFactory, CourseEnrollmentFactory,
...@@ -45,10 +42,14 @@ from ccx_keys.locator import CCXLocator ...@@ -45,10 +42,14 @@ from ccx_keys.locator import CCXLocator
from ..models import ( from ..models import (
CustomCourseForEdX, CustomCourseForEdX,
CcxMembership,
CcxFutureMembership,
) )
from ..overrides import get_override_for_ccx, override_field_for_ccx from ..overrides import get_override_for_ccx, override_field_for_ccx
from .factories import ( from .factories import (
CcxFactory, CcxFactory,
CcxMembershipFactory,
CcxFutureMembershipFactory,
) )
...@@ -279,7 +280,7 @@ class TestCoachDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase): ...@@ -279,7 +280,7 @@ class TestCoachDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase):
self.assertTrue(student.email in outbox[0].recipients()) # pylint: disable=no-member self.assertTrue(student.email in outbox[0].recipients()) # pylint: disable=no-member
# a CcxMembership exists for this student # a CcxMembership exists for this student
self.assertTrue( self.assertTrue(
CourseEnrollment.objects.filter(course_id=self.course.id, user=student).exists() CcxMembership.objects.filter(ccx=ccx, student=student).exists()
) )
def test_unenroll_member_student(self): def test_unenroll_member_student(self):
...@@ -287,15 +288,16 @@ class TestCoachDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase): ...@@ -287,15 +288,16 @@ class TestCoachDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase):
""" """
self.make_coach() self.make_coach()
ccx = self.make_ccx() ccx = self.make_ccx()
course_key = CCXLocator.from_course_locator(self.course.id, ccx.id) enrollment = CourseEnrollmentFactory(course_id=self.course.id)
enrollment = CourseEnrollmentFactory(course_id=course_key)
student = enrollment.user student = enrollment.user
outbox = self.get_outbox() outbox = self.get_outbox()
self.assertEqual(outbox, []) self.assertEqual(outbox, [])
# student is member of CCX:
CcxMembershipFactory(ccx=ccx, student=student)
url = reverse( url = reverse(
'ccx_invite', 'ccx_invite',
kwargs={'course_id': course_key} kwargs={'course_id': CCXLocator.from_course_locator(self.course.id, ccx.id)}
) )
data = { data = {
'enrollment-button': 'Unenroll', 'enrollment-button': 'Unenroll',
...@@ -309,6 +311,10 @@ class TestCoachDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase): ...@@ -309,6 +311,10 @@ class TestCoachDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase):
self.assertTrue(302 in response.redirect_chain[0]) self.assertTrue(302 in response.redirect_chain[0])
self.assertEqual(len(outbox), 1) self.assertEqual(len(outbox), 1)
self.assertTrue(student.email in outbox[0].recipients()) # pylint: disable=no-member self.assertTrue(student.email in outbox[0].recipients()) # pylint: disable=no-member
# the membership for this student is gone
self.assertFalse(
CcxMembership.objects.filter(ccx=ccx, student=student).exists()
)
def test_enroll_non_user_student(self): def test_enroll_non_user_student(self):
"""enroll a list of students who are not users yet """enroll a list of students who are not users yet
...@@ -316,13 +322,12 @@ class TestCoachDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase): ...@@ -316,13 +322,12 @@ class TestCoachDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase):
test_email = "nobody@nowhere.com" test_email = "nobody@nowhere.com"
self.make_coach() self.make_coach()
ccx = self.make_ccx() ccx = self.make_ccx()
course_key = CCXLocator.from_course_locator(self.course.id, ccx.id)
outbox = self.get_outbox() outbox = self.get_outbox()
self.assertEqual(outbox, []) self.assertEqual(outbox, [])
url = reverse( url = reverse(
'ccx_invite', 'ccx_invite',
kwargs={'course_id': course_key} kwargs={'course_id': CCXLocator.from_course_locator(self.course.id, ccx.id)}
) )
data = { data = {
'enrollment-button': 'Enroll', 'enrollment-button': 'Enroll',
...@@ -337,8 +342,8 @@ class TestCoachDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase): ...@@ -337,8 +342,8 @@ class TestCoachDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase):
self.assertEqual(len(outbox), 1) self.assertEqual(len(outbox), 1)
self.assertTrue(test_email in outbox[0].recipients()) self.assertTrue(test_email in outbox[0].recipients())
self.assertTrue( self.assertTrue(
CourseEnrollmentAllowed.objects.filter( CcxFutureMembership.objects.filter(
course_id=course_key, email=test_email ccx=ccx, email=test_email
).exists() ).exists()
) )
...@@ -347,16 +352,14 @@ class TestCoachDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase): ...@@ -347,16 +352,14 @@ class TestCoachDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase):
""" """
test_email = "nobody@nowhere.com" test_email = "nobody@nowhere.com"
self.make_coach() self.make_coach()
course = CourseFactory.create()
ccx = self.make_ccx() ccx = self.make_ccx()
course_key = CCXLocator.from_course_locator(course.id, ccx.id)
outbox = self.get_outbox() outbox = self.get_outbox()
CourseEnrollmentAllowed(course_id=course_key, email=test_email) CcxFutureMembershipFactory(ccx=ccx, email=test_email)
self.assertEqual(outbox, []) self.assertEqual(outbox, [])
url = reverse( url = reverse(
'ccx_invite', 'ccx_invite',
kwargs={'course_id': course_key} kwargs={'course_id': CCXLocator.from_course_locator(self.course.id, ccx.id)}
) )
data = { data = {
'enrollment-button': 'Unenroll', 'enrollment-button': 'Unenroll',
...@@ -368,9 +371,11 @@ class TestCoachDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase): ...@@ -368,9 +371,11 @@ class TestCoachDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase):
# we were redirected to our current location # we were redirected to our current location
self.assertEqual(len(response.redirect_chain), 1) self.assertEqual(len(response.redirect_chain), 1)
self.assertTrue(302 in response.redirect_chain[0]) self.assertTrue(302 in response.redirect_chain[0])
self.assertEqual(len(outbox), 1)
self.assertTrue(test_email in outbox[0].recipients())
self.assertFalse( self.assertFalse(
CourseEnrollmentAllowed.objects.filter( CcxFutureMembership.objects.filter(
course_id=course_key, email=test_email ccx=ccx, email=test_email
).exists() ).exists()
) )
...@@ -379,8 +384,7 @@ class TestCoachDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase): ...@@ -379,8 +384,7 @@ class TestCoachDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase):
""" """
self.make_coach() self.make_coach()
ccx = self.make_ccx() ccx = self.make_ccx()
course_key = CCXLocator.from_course_locator(self.course.id, ccx.id) enrollment = CourseEnrollmentFactory(course_id=self.course.id)
enrollment = CourseEnrollmentFactory(course_id=course_key)
student = enrollment.user student = enrollment.user
# no emails have been sent so far # no emails have been sent so far
outbox = self.get_outbox() outbox = self.get_outbox()
...@@ -388,7 +392,7 @@ class TestCoachDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase): ...@@ -388,7 +392,7 @@ class TestCoachDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase):
url = reverse( url = reverse(
'ccx_manage_student', 'ccx_manage_student',
kwargs={'course_id': course_key} kwargs={'course_id': CCXLocator.from_course_locator(self.course.id, ccx.id)}
) )
data = { data = {
'student-action': 'add', 'student-action': 'add',
...@@ -402,7 +406,7 @@ class TestCoachDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase): ...@@ -402,7 +406,7 @@ class TestCoachDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase):
self.assertEqual(outbox, []) self.assertEqual(outbox, [])
# a CcxMembership exists for this student # a CcxMembership exists for this student
self.assertTrue( self.assertTrue(
CourseEnrollment.objects.filter(course_id=course_key, user=student).exists() CcxMembership.objects.filter(ccx=ccx, student=student).exists()
) )
def test_manage_remove_single_student(self): def test_manage_remove_single_student(self):
...@@ -410,9 +414,9 @@ class TestCoachDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase): ...@@ -410,9 +414,9 @@ class TestCoachDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase):
""" """
self.make_coach() self.make_coach()
ccx = self.make_ccx() ccx = self.make_ccx()
course_key = CCXLocator.from_course_locator(self.course.id, ccx.id) enrollment = CourseEnrollmentFactory(course_id=self.course.id)
enrollment = CourseEnrollmentFactory(course_id=course_key)
student = enrollment.user student = enrollment.user
CcxMembershipFactory(ccx=ccx, student=student)
# no emails have been sent so far # no emails have been sent so far
outbox = self.get_outbox() outbox = self.get_outbox()
self.assertEqual(outbox, []) self.assertEqual(outbox, [])
...@@ -431,6 +435,10 @@ class TestCoachDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase): ...@@ -431,6 +435,10 @@ class TestCoachDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase):
self.assertEqual(len(response.redirect_chain), 1) self.assertEqual(len(response.redirect_chain), 1)
self.assertTrue(302 in response.redirect_chain[0]) self.assertTrue(302 in response.redirect_chain[0])
self.assertEqual(outbox, []) self.assertEqual(outbox, [])
# a CcxMembership exists for this student
self.assertFalse(
CcxMembership.objects.filter(ccx=ccx, student=student).exists()
)
GET_CHILDREN = XModuleMixin.get_children GET_CHILDREN = XModuleMixin.get_children
...@@ -517,6 +525,7 @@ class TestCCXGrades(ModuleStoreTestCase, LoginEnrollmentTestCase): ...@@ -517,6 +525,7 @@ class TestCCXGrades(ModuleStoreTestCase, LoginEnrollmentTestCase):
self.student = student = UserFactory.create() self.student = student = UserFactory.create()
CourseEnrollmentFactory.create(user=student, course_id=self.course.id) CourseEnrollmentFactory.create(user=student, course_id=self.course.id)
CcxMembershipFactory(ccx=ccx, student=student, active=True)
# create grades for self.student as if they'd submitted the ccx # create grades for self.student as if they'd submitted the ccx
for chapter in self.course.get_children(): for chapter in self.course.get_children():
...@@ -665,14 +674,12 @@ class TestStudentDashboardWithCCX(ModuleStoreTestCase): ...@@ -665,14 +674,12 @@ class TestStudentDashboardWithCCX(ModuleStoreTestCase):
self.ccx = CcxFactory(course_id=self.split_course.id, coach=self.coach) self.ccx = CcxFactory(course_id=self.split_course.id, coach=self.coach)
last_week = datetime.datetime.now(UTC()) - datetime.timedelta(days=7) last_week = datetime.datetime.now(UTC()) - datetime.timedelta(days=7)
override_field_for_ccx(self.ccx, self.split_course, 'start', last_week) # Required by self.ccx.has_started(). override_field_for_ccx(self.ccx, self.split_course, 'start', last_week) # Required by self.ccx.has_started().
course_key = CCXLocator.from_course_locator(self.split_course.id, self.ccx.id) CcxMembershipFactory(ccx=self.ccx, student=self.student, active=True)
CourseEnrollment.enroll(self.student, course_key)
def test_load_student_dashboard(self): def test_load_student_dashboard(self):
self.client.login(username=self.student.username, password=self.student_password) self.client.login(username=self.student.username, password=self.student_password)
response = self.client.get(reverse('dashboard')) response = self.client.get(reverse('dashboard'))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertTrue(re.search('Test CCX', response.content))
def flatten(seq): def flatten(seq):
......
...@@ -36,24 +36,22 @@ from courseware.module_render import get_module_for_descriptor ...@@ -36,24 +36,22 @@ from courseware.module_render import get_module_for_descriptor
from edxmako.shortcuts import render_to_response from edxmako.shortcuts import render_to_response
from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.keys import CourseKey
from ccx_keys.locator import CCXLocator from ccx_keys.locator import CCXLocator
from student.roles import CourseCcxCoachRole # pylint: disable=import-error from student.roles import CourseCcxCoachRole
from student.models import CourseEnrollment
from instructor.offline_gradecalc import student_grades # pylint: disable=import-error from instructor.offline_gradecalc import student_grades
from instructor.views.api import _split_input_list # pylint: disable=import-error from instructor.views.api import _split_input_list
from instructor.views.tools import get_student_from_identifier # pylint: disable=import-error from instructor.views.tools import get_student_from_identifier
from instructor.enrollment import (
enroll_email,
unenroll_email,
get_email_params,
)
from .models import CustomCourseForEdX from .models import CustomCourseForEdX, CcxMembership
from .overrides import ( from .overrides import (
clear_override_for_ccx, clear_override_for_ccx,
get_override_for_ccx, get_override_for_ccx,
override_field_for_ccx, override_field_for_ccx,
) )
from .utils import (
enroll_email,
unenroll_email,
)
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -129,7 +127,7 @@ def dashboard(request, course, ccx=None): ...@@ -129,7 +127,7 @@ def dashboard(request, course, ccx=None):
context['schedule'] = json.dumps(schedule, indent=4) context['schedule'] = json.dumps(schedule, indent=4)
context['save_url'] = reverse( context['save_url'] = reverse(
'save_ccx', kwargs={'course_id': ccx_locator}) 'save_ccx', kwargs={'course_id': ccx_locator})
context['ccx_members'] = CourseEnrollment.objects.filter(course_id=ccx_locator) context['ccx_members'] = CcxMembership.objects.filter(ccx=ccx)
context['gradebook_url'] = reverse( context['gradebook_url'] = reverse(
'ccx_gradebook', kwargs={'course_id': ccx_locator}) 'ccx_gradebook', kwargs={'course_id': ccx_locator})
context['grades_csv_url'] = reverse( context['grades_csv_url'] = reverse(
...@@ -158,7 +156,7 @@ def create_ccx(request, course, ccx=None): ...@@ -158,7 +156,7 @@ def create_ccx(request, course, ccx=None):
"You cannot create a CCX from a course using a deprecated id. " "You cannot create a CCX from a course using a deprecated id. "
"Please create a rerun of this course in the studio to allow " "Please create a rerun of this course in the studio to allow "
"this action.")) "this action."))
url = reverse('ccx_coach_dashboard', kwargs={'course_id': course.id}) url = reverse('ccx_coach_dashboard', kwargs={'course_id', course.id})
return redirect(url) return redirect(url)
ccx = CustomCourseForEdX( ccx = CustomCourseForEdX(
...@@ -409,18 +407,15 @@ def ccx_invite(request, course, ccx=None): ...@@ -409,18 +407,15 @@ def ccx_invite(request, course, ccx=None):
email = user.email email = user.email
try: try:
validate_email(email) validate_email(email)
course_key = CCXLocator.from_course_locator(course.id, ccx.id)
email_params = get_email_params(course, auto_enroll)
if action == 'Enroll': if action == 'Enroll':
enroll_email( enroll_email(
course_key, ccx,
email, email,
auto_enroll=auto_enroll, auto_enroll=auto_enroll,
email_students=email_students, email_students=email_students
email_params=email_params
) )
if action == "Unenroll": if action == "Unenroll":
unenroll_email(course_key, email, email_students=email_students, email_params=email_params) unenroll_email(ccx, email, email_students=email_students)
except ValidationError: except ValidationError:
log.info('Invalid user name or email when trying to invite students: %s', email) log.info('Invalid user name or email when trying to invite students: %s', email)
url = reverse( url = reverse(
...@@ -449,21 +444,20 @@ def ccx_student_management(request, course, ccx=None): ...@@ -449,21 +444,20 @@ def ccx_student_management(request, course, ccx=None):
else: else:
email = user.email email = user.email
course_key = CCXLocator.from_course_locator(course.id, ccx.id)
try: try:
validate_email(email) validate_email(email)
if action == 'add': if action == 'add':
# by decree, no emails sent to students added this way # by decree, no emails sent to students added this way
# by decree, any students added this way are auto_enrolled # by decree, any students added this way are auto_enrolled
enroll_email(course_key, email, auto_enroll=True, email_students=False) enroll_email(ccx, email, auto_enroll=True, email_students=False)
elif action == 'revoke': elif action == 'revoke':
unenroll_email(course_key, email, email_students=False) unenroll_email(ccx, email, email_students=False)
except ValidationError: except ValidationError:
log.info('Invalid user name or email when trying to enroll student: %s', email) log.info('Invalid user name or email when trying to enroll student: %s', email)
url = reverse( url = reverse(
'ccx_coach_dashboard', 'ccx_coach_dashboard',
kwargs={'course_id': course_key} kwargs={'course_id': CCXLocator.from_course_locator(course.id, ccx.id)}
) )
return redirect(url) return redirect(url)
...@@ -502,8 +496,8 @@ def ccx_gradebook(request, course, ccx=None): ...@@ -502,8 +496,8 @@ def ccx_gradebook(request, course, ccx=None):
prep_course_for_grading(course, request) prep_course_for_grading(course, request)
enrolled_students = User.objects.filter( enrolled_students = User.objects.filter(
courseenrollment__course_id=ccx_key, ccxmembership__ccx=ccx,
courseenrollment__is_active=1 ccxmembership__active=1
).order_by('username').select_related("profile") ).order_by('username').select_related("profile")
student_info = [ student_info = [
...@@ -541,8 +535,8 @@ def ccx_grades_csv(request, course, ccx=None): ...@@ -541,8 +535,8 @@ def ccx_grades_csv(request, course, ccx=None):
prep_course_for_grading(course, request) prep_course_for_grading(course, request)
enrolled_students = User.objects.filter( enrolled_students = User.objects.filter(
courseenrollment__course_id=ccx_key, ccxmembership__ccx=ccx,
courseenrollment__is_active=1 ccxmembership__active=1
).order_by('username').select_related("profile") ).order_by('username').select_related("profile")
grades = iterate_grades_for(course, enrolled_students) grades = iterate_grades_for(course, enrolled_students)
......
<%page args="ccx, membership, course_overview, show_courseware_link, is_course_blocked" />
<%!
from django.utils.translation import ugettext as _
from django.core.urlresolvers import reverse
from courseware.courses import course_image_url, get_course_about_section
from ccx_keys.locator import CCXLocator
%>
<%
ccx_target = reverse('info', args=[CCXLocator.from_course_locator(course_overview.id, ccx.id)])
%>
<li class="course-item">
<article class="course">
<section class="details">
<div class="wrapper-course-image" aria-hidden="true">
% if show_courseware_link:
% if not is_course_blocked:
<a href="${ccx_target}" class="cover">
<img src="${course_overview.course_image_url}" class="course-image" alt="${_('{course_number} {ccx_name} Cover Image').format(course_number=course_overview.number, ccx_name=ccx.display_name) |h}" />
</a>
% else:
<a class="fade-cover">
<img src="${course_overview.course_image_url}" class="course-image" alt="${_('{course_number} {ccx_name} Cover Image').format(course_number=course_overview.number, ccx_name=ccx.display_name) |h}" />
</a>
% endif
% else:
<a class="cover">
<img src="${course_overview.course_image_url}" class="course-image" alt="${_('{course_number} {ccx_name} Cover Image').format(course_number=course_overview.number, ccx_name=ccx.display_name) |h}" />
</a>
% endif
</div>
<div class="wrapper-course-details">
<h3 class="course-title">
% if show_courseware_link:
% if not is_course_blocked:
<a href="${ccx_target}">${ccx.display_name}</a>
% else:
<a class="disable-look">${ccx.display_name}</a>
% endif
% else:
<span>${ccx.display_name}</span>
% endif
</h3>
<div class="course-info">
<span class="info-university">${get_course_about_section(course_overview, 'university')} - </span>
<span class="info-course-id">${course_overview.display_number_with_default | h}</span>
<span class="info-date-block" data-tooltip="Hi">
% if ccx.has_ended():
${_("Ended - {end_date}").format(end_date=ccx.end_datetime_text("SHORT_DATE"))}
% elif ccx.has_started():
${_("Started - {start_date}").format(start_date=ccx.start_datetime_text("SHORT_DATE"))}
% else: # hasn't started yet
${_("Starts - {start_date}").format(start_date=ccx.start_datetime_text("SHORT_DATE"))}
% endif
</span>
</div>
% if show_courseware_link:
<div class="wrapper-course-actions">
<div class="course-actions">
% if ccx.has_ended():
% if not is_course_blocked:
<a href="${ccx_target}" class="enter-course archived">${_('View Archived Custom Course')}<span class="sr">&nbsp;${ccx.display_name}</span></a>
% else:
<a class="enter-course-blocked archived">${_('View Archived Custom Course')}<span class="sr">&nbsp;${ccx.display_name}</span></a>
% endif
% else:
% if not is_course_blocked:
<a href="${ccx_target}" class="enter-course">${_('View Custom Course')}<span class="sr">&nbsp;${ccx.display_name}</span></a>
% else:
<a class="enter-course-blocked">${_('View Custom Course')}<span class="sr">&nbsp;${ccx.display_name}</span></a>
% endif
% endif
</div>
</div>
% endif
</div>
</section>
</article>
</li>
...@@ -63,8 +63,8 @@ ...@@ -63,8 +63,8 @@
<tbody> <tbody>
%for member in ccx_members: %for member in ccx_members:
<tr> <tr>
<td>${member.user}</td> <td>${member.student}</td>
<td>${member.user.email}</td> <td>${member.student.email}</td>
<td><div class="revoke"><i class="fa fa-times-circle"></i> Revoke access</div></td> <td><div class="revoke"><i class="fa fa-times-circle"></i> Revoke access</div></td>
</tr> </tr>
%endfor %endfor
......
...@@ -100,6 +100,14 @@ import json ...@@ -100,6 +100,14 @@ import json
<%include file='dashboard/_dashboard_course_listing.html' args="course_overview=enrollment.course_overview, enrollment=enrollment, show_courseware_link=show_courseware_link, cert_status=cert_status, credit_status=credit_status, show_email_settings=show_email_settings, course_mode_info=course_mode_info, show_refund_option=show_refund_option, is_paid_course=is_paid_course, is_course_blocked=is_course_blocked, verification_status=course_verification_status, course_requirements=course_requirements, dashboard_index=dashboard_index, share_settings=share_settings, user=user" /> <%include file='dashboard/_dashboard_course_listing.html' args="course_overview=enrollment.course_overview, enrollment=enrollment, show_courseware_link=show_courseware_link, cert_status=cert_status, credit_status=credit_status, show_email_settings=show_email_settings, course_mode_info=course_mode_info, show_refund_option=show_refund_option, is_paid_course=is_paid_course, is_course_blocked=is_course_blocked, verification_status=course_verification_status, course_requirements=course_requirements, dashboard_index=dashboard_index, share_settings=share_settings, user=user" />
% endfor % endfor
% if settings.FEATURES.get('CUSTOM_COURSES_EDX', False):
% for ccx, membership, course_overview in ccx_membership_triplets:
<% show_courseware_link = ccx.has_started() %>
<% is_course_blocked = False %>
<%include file='ccx/_dashboard_ccx_listing.html' args="ccx=ccx, membership=membership, course_overview=course_overview, show_courseware_link=show_courseware_link, is_course_blocked=is_course_blocked" />
% endfor
% endif
</ul> </ul>
% else: % else:
<section class="empty-dashboard-message"> <section class="empty-dashboard-message">
......
...@@ -15,8 +15,6 @@ from xmodule.error_module import ErrorDescriptor ...@@ -15,8 +15,6 @@ from xmodule.error_module import ErrorDescriptor
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule_django.models import CourseKeyField, UsageKeyField from xmodule_django.models import CourseKeyField, UsageKeyField
from ccx_keys.locator import CCXLocator
class CourseOverview(TimeStampedModel): class CourseOverview(TimeStampedModel):
""" """
...@@ -102,26 +100,17 @@ class CourseOverview(TimeStampedModel): ...@@ -102,26 +100,17 @@ class CourseOverview(TimeStampedModel):
except ValueError: except ValueError:
lowest_passing_grade = None lowest_passing_grade = None
display_name = course.display_name
start = course.start
end = course.end
if isinstance(course.id, CCXLocator):
from ccx.utils import get_ccx_from_ccx_locator # pylint: disable=import-error
ccx = get_ccx_from_ccx_locator(course.id)
display_name = ccx.display_name
start = ccx.start
end = ccx.due
return cls( return cls(
version=cls.VERSION, version=cls.VERSION,
id=course.id, id=course.id,
_location=course.location, _location=course.location,
display_name=display_name, display_name=course.display_name,
display_number_with_default=course.display_number_with_default, display_number_with_default=course.display_number_with_default,
display_org_with_default=course.display_org_with_default, display_org_with_default=course.display_org_with_default,
start=start, start=course.start,
end=end, end=course.end,
advertised_start=course.advertised_start, advertised_start=course.advertised_start,
course_image_url=course_image_url(course), course_image_url=course_image_url(course),
......
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