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):
if not user.is_authenticated():
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:
record = CourseEnrollment.objects.get(user=user, course_id=course_key)
return record.is_active
......
......@@ -655,6 +655,13 @@ def dashboard(request):
)
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:
redirect_message = _("The course you are looking for does not start until {date}.").format(
date=request.GET['notlive']
......@@ -690,6 +697,7 @@ def dashboard(request):
'provider_states': [],
'order_history_list': order_history_list,
'courses_requirements_not_met': courses_requirements_not_met,
'ccx_membership_triplets': ccx_membership_triplets,
'nav_hidden': True,
}
......@@ -1896,6 +1904,16 @@ def activate_account(request, key):
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(
"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
from django.utils.timezone import UTC
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.error_module import ErrorDescriptor
from xmodule.modulestore.django import modulestore
......@@ -95,6 +96,52 @@ class CustomCourseForEdX(models.Model):
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):
"""
Field overrides for custom courses.
......
......@@ -5,6 +5,8 @@ from factory import SubFactory
from factory.django import DjangoModelFactory
from student.tests.factories import UserFactory
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
......@@ -12,3 +14,12 @@ class CcxFactory(DjangoModelFactory): # pylint: disable=missing-docstring
display_name = "Test CCX"
id = None # pylint: disable=redefined-builtin, invalid-name
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, \
from xmodule.modulestore.tests.factories import check_mongo_calls, CourseFactory, check_sum_of_calls
from xmodule.modulestore.tests.utils import ProceduralCourseTestMixin
from ccx_keys.locator import CCXLocator
from ccx.tests.factories import CcxFactory
from ccx.tests.factories import CcxFactory, CcxMembershipFactory
@attr('shard_1')
......@@ -65,7 +65,7 @@ class FieldOverridePerformanceTestCase(ProceduralCourseTestMixin,
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.
"""
......@@ -112,17 +112,18 @@ class FieldOverridePerformanceTestCase(ProceduralCourseTestMixin,
)
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(
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):
"""
Renders the progress page for the given course.
......@@ -155,7 +156,7 @@ class FieldOverridePerformanceTestCase(ProceduralCourseTestMixin,
"""
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
with self.settings(MODULESTORE_BRANCH='published-only'):
......
......@@ -5,9 +5,12 @@ from datetime import datetime, timedelta
from django.utils.timezone import UTC
from mock import patch
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.tests.factories import ( # pylint: disable=import-error
AdminFactory,
CourseEnrollmentFactory,
UserFactory,
)
from util.tests.test_date_utils import fake_ugettext
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
......@@ -18,11 +21,123 @@ from xmodule.modulestore.tests.factories import (
from .factories import (
CcxFactory,
CcxFutureMembershipFactory,
)
from ..models import (
CcxMembership,
CcxFutureMembership,
)
from ..overrides import override_field_for_ccx
@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):
"""Unit tests for the CustomCourseForEdX model
"""
......
......@@ -20,12 +20,9 @@ from django.utils.timezone import UTC
from django.test.utils import override_settings
from django.test import RequestFactory
from edxmako.shortcuts import render_to_response # pylint: disable=import-error
from student.models import CourseEnrollment
from request_cache.middleware import RequestCache
from student.roles import CourseCcxCoachRole # pylint: disable=import-error
from student.models import (
CourseEnrollment,
CourseEnrollmentAllowed,
)
from student.tests.factories import ( # pylint: disable=import-error
AdminFactory,
CourseEnrollmentFactory,
......@@ -45,10 +42,14 @@ from ccx_keys.locator import CCXLocator
from ..models import (
CustomCourseForEdX,
CcxMembership,
CcxFutureMembership,
)
from ..overrides import get_override_for_ccx, override_field_for_ccx
from .factories import (
CcxFactory,
CcxMembershipFactory,
CcxFutureMembershipFactory,
)
......@@ -279,7 +280,7 @@ class TestCoachDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase):
self.assertTrue(student.email in outbox[0].recipients()) # pylint: disable=no-member
# a CcxMembership exists for this student
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):
......@@ -287,15 +288,16 @@ class TestCoachDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase):
"""
self.make_coach()
ccx = self.make_ccx()
course_key = CCXLocator.from_course_locator(self.course.id, ccx.id)
enrollment = CourseEnrollmentFactory(course_id=course_key)
enrollment = CourseEnrollmentFactory(course_id=self.course.id)
student = enrollment.user
outbox = self.get_outbox()
self.assertEqual(outbox, [])
# student is member of CCX:
CcxMembershipFactory(ccx=ccx, student=student)
url = reverse(
'ccx_invite',
kwargs={'course_id': course_key}
kwargs={'course_id': CCXLocator.from_course_locator(self.course.id, ccx.id)}
)
data = {
'enrollment-button': 'Unenroll',
......@@ -309,6 +311,10 @@ class TestCoachDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase):
self.assertTrue(302 in response.redirect_chain[0])
self.assertEqual(len(outbox), 1)
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):
"""enroll a list of students who are not users yet
......@@ -316,13 +322,12 @@ class TestCoachDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase):
test_email = "nobody@nowhere.com"
self.make_coach()
ccx = self.make_ccx()
course_key = CCXLocator.from_course_locator(self.course.id, ccx.id)
outbox = self.get_outbox()
self.assertEqual(outbox, [])
url = reverse(
'ccx_invite',
kwargs={'course_id': course_key}
kwargs={'course_id': CCXLocator.from_course_locator(self.course.id, ccx.id)}
)
data = {
'enrollment-button': 'Enroll',
......@@ -337,8 +342,8 @@ class TestCoachDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase):
self.assertEqual(len(outbox), 1)
self.assertTrue(test_email in outbox[0].recipients())
self.assertTrue(
CourseEnrollmentAllowed.objects.filter(
course_id=course_key, email=test_email
CcxFutureMembership.objects.filter(
ccx=ccx, email=test_email
).exists()
)
......@@ -347,16 +352,14 @@ class TestCoachDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase):
"""
test_email = "nobody@nowhere.com"
self.make_coach()
course = CourseFactory.create()
ccx = self.make_ccx()
course_key = CCXLocator.from_course_locator(course.id, ccx.id)
outbox = self.get_outbox()
CourseEnrollmentAllowed(course_id=course_key, email=test_email)
CcxFutureMembershipFactory(ccx=ccx, email=test_email)
self.assertEqual(outbox, [])
url = reverse(
'ccx_invite',
kwargs={'course_id': course_key}
kwargs={'course_id': CCXLocator.from_course_locator(self.course.id, ccx.id)}
)
data = {
'enrollment-button': 'Unenroll',
......@@ -368,9 +371,11 @@ class TestCoachDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase):
# we were redirected to our current location
self.assertEqual(len(response.redirect_chain), 1)
self.assertTrue(302 in response.redirect_chain[0])
self.assertEqual(len(outbox), 1)
self.assertTrue(test_email in outbox[0].recipients())
self.assertFalse(
CourseEnrollmentAllowed.objects.filter(
course_id=course_key, email=test_email
CcxFutureMembership.objects.filter(
ccx=ccx, email=test_email
).exists()
)
......@@ -379,8 +384,7 @@ class TestCoachDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase):
"""
self.make_coach()
ccx = self.make_ccx()
course_key = CCXLocator.from_course_locator(self.course.id, ccx.id)
enrollment = CourseEnrollmentFactory(course_id=course_key)
enrollment = CourseEnrollmentFactory(course_id=self.course.id)
student = enrollment.user
# no emails have been sent so far
outbox = self.get_outbox()
......@@ -388,7 +392,7 @@ class TestCoachDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase):
url = reverse(
'ccx_manage_student',
kwargs={'course_id': course_key}
kwargs={'course_id': CCXLocator.from_course_locator(self.course.id, ccx.id)}
)
data = {
'student-action': 'add',
......@@ -402,7 +406,7 @@ class TestCoachDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase):
self.assertEqual(outbox, [])
# a CcxMembership exists for this student
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):
......@@ -410,9 +414,9 @@ class TestCoachDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase):
"""
self.make_coach()
ccx = self.make_ccx()
course_key = CCXLocator.from_course_locator(self.course.id, ccx.id)
enrollment = CourseEnrollmentFactory(course_id=course_key)
enrollment = CourseEnrollmentFactory(course_id=self.course.id)
student = enrollment.user
CcxMembershipFactory(ccx=ccx, student=student)
# no emails have been sent so far
outbox = self.get_outbox()
self.assertEqual(outbox, [])
......@@ -431,6 +435,10 @@ class TestCoachDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase):
self.assertEqual(len(response.redirect_chain), 1)
self.assertTrue(302 in response.redirect_chain[0])
self.assertEqual(outbox, [])
# a CcxMembership exists for this student
self.assertFalse(
CcxMembership.objects.filter(ccx=ccx, student=student).exists()
)
GET_CHILDREN = XModuleMixin.get_children
......@@ -517,6 +525,7 @@ class TestCCXGrades(ModuleStoreTestCase, LoginEnrollmentTestCase):
self.student = student = UserFactory.create()
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
for chapter in self.course.get_children():
......@@ -665,14 +674,12 @@ class TestStudentDashboardWithCCX(ModuleStoreTestCase):
self.ccx = CcxFactory(course_id=self.split_course.id, coach=self.coach)
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().
course_key = CCXLocator.from_course_locator(self.split_course.id, self.ccx.id)
CourseEnrollment.enroll(self.student, course_key)
CcxMembershipFactory(ccx=self.ccx, student=self.student, active=True)
def test_load_student_dashboard(self):
self.client.login(username=self.student.username, password=self.student_password)
response = self.client.get(reverse('dashboard'))
self.assertEqual(response.status_code, 200)
self.assertTrue(re.search('Test CCX', response.content))
def flatten(seq):
......
......@@ -36,24 +36,22 @@ from courseware.module_render import get_module_for_descriptor
from edxmako.shortcuts import render_to_response
from opaque_keys.edx.keys import CourseKey
from ccx_keys.locator import CCXLocator
from student.roles import CourseCcxCoachRole # pylint: disable=import-error
from student.models import CourseEnrollment
from student.roles import CourseCcxCoachRole
from instructor.offline_gradecalc import student_grades # pylint: disable=import-error
from instructor.views.api import _split_input_list # pylint: disable=import-error
from instructor.views.tools import get_student_from_identifier # pylint: disable=import-error
from instructor.enrollment import (
enroll_email,
unenroll_email,
get_email_params,
)
from instructor.offline_gradecalc import student_grades
from instructor.views.api import _split_input_list
from instructor.views.tools import get_student_from_identifier
from .models import CustomCourseForEdX
from .models import CustomCourseForEdX, CcxMembership
from .overrides import (
clear_override_for_ccx,
get_override_for_ccx,
override_field_for_ccx,
)
from .utils import (
enroll_email,
unenroll_email,
)
log = logging.getLogger(__name__)
......@@ -129,7 +127,7 @@ def dashboard(request, course, ccx=None):
context['schedule'] = json.dumps(schedule, indent=4)
context['save_url'] = reverse(
'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(
'ccx_gradebook', kwargs={'course_id': ccx_locator})
context['grades_csv_url'] = reverse(
......@@ -158,7 +156,7 @@ def create_ccx(request, course, ccx=None):
"You cannot create a CCX from a course using a deprecated id. "
"Please create a rerun of this course in the studio to allow "
"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)
ccx = CustomCourseForEdX(
......@@ -409,18 +407,15 @@ def ccx_invite(request, course, ccx=None):
email = user.email
try:
validate_email(email)
course_key = CCXLocator.from_course_locator(course.id, ccx.id)
email_params = get_email_params(course, auto_enroll)
if action == 'Enroll':
enroll_email(
course_key,
ccx,
email,
auto_enroll=auto_enroll,
email_students=email_students,
email_params=email_params
email_students=email_students
)
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:
log.info('Invalid user name or email when trying to invite students: %s', email)
url = reverse(
......@@ -449,21 +444,20 @@ def ccx_student_management(request, course, ccx=None):
else:
email = user.email
course_key = CCXLocator.from_course_locator(course.id, ccx.id)
try:
validate_email(email)
if action == 'add':
# by decree, no emails sent to students added this way
# 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':
unenroll_email(course_key, email, email_students=False)
unenroll_email(ccx, email, email_students=False)
except ValidationError:
log.info('Invalid user name or email when trying to enroll student: %s', email)
url = reverse(
'ccx_coach_dashboard',
kwargs={'course_id': course_key}
kwargs={'course_id': CCXLocator.from_course_locator(course.id, ccx.id)}
)
return redirect(url)
......@@ -502,8 +496,8 @@ def ccx_gradebook(request, course, ccx=None):
prep_course_for_grading(course, request)
enrolled_students = User.objects.filter(
courseenrollment__course_id=ccx_key,
courseenrollment__is_active=1
ccxmembership__ccx=ccx,
ccxmembership__active=1
).order_by('username').select_related("profile")
student_info = [
......@@ -541,8 +535,8 @@ def ccx_grades_csv(request, course, ccx=None):
prep_course_for_grading(course, request)
enrolled_students = User.objects.filter(
courseenrollment__course_id=ccx_key,
courseenrollment__is_active=1
ccxmembership__ccx=ccx,
ccxmembership__active=1
).order_by('username').select_related("profile")
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 @@
<tbody>
%for member in ccx_members:
<tr>
<td>${member.user}</td>
<td>${member.user.email}</td>
<td>${member.student}</td>
<td>${member.student.email}</td>
<td><div class="revoke"><i class="fa fa-times-circle"></i> Revoke access</div></td>
</tr>
%endfor
......
......@@ -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" />
% 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>
% else:
<section class="empty-dashboard-message">
......
......@@ -15,8 +15,6 @@ from xmodule.error_module import ErrorDescriptor
from xmodule.modulestore.django import modulestore
from xmodule_django.models import CourseKeyField, UsageKeyField
from ccx_keys.locator import CCXLocator
class CourseOverview(TimeStampedModel):
"""
......@@ -102,26 +100,17 @@ class CourseOverview(TimeStampedModel):
except ValueError:
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(
version=cls.VERSION,
id=course.id,
_location=course.location,
display_name=display_name,
display_name=course.display_name,
display_number_with_default=course.display_number_with_default,
display_org_with_default=course.display_org_with_default,
start=start,
end=end,
start=course.start,
end=course.end,
advertised_start=course.advertised_start,
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