Merge pull request #6636 from jazkarta/feature-pocs

MIT CCx (was Personal Online Courses)
This commit is contained in:
David Ormsbee
2015-04-11 11:15:43 -04:00
66 changed files with 5129 additions and 282 deletions

View File

@@ -267,6 +267,14 @@ class LibraryUserRole(CourseRole):
super(LibraryUserRole, self).__init__(self.ROLE, *args, **kwargs)
class CourseCcxCoachRole(CourseRole):
"""A CCX Coach"""
ROLE = 'ccx_coach'
def __init__(self, *args, **kwargs):
super(CourseCcxCoachRole, self).__init__(self.ROLE, *args, **kwargs)
class OrgStaffRole(OrgRole):
"""An organization staff member"""
def __init__(self, *args, **kwargs):

View File

@@ -662,6 +662,17 @@ def dashboard(request):
if course.pre_requisite_courses)
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 import ACTIVE_CCX_KEY
from ccx.utils import get_ccx_membership_triplets
ccx_membership_triplets = get_ccx_membership_triplets(
user, course_org_filter, org_filter_out_set
)
# should we deselect any active CCX at this time so that we don't have
# to change the URL for viewing a course? I think so.
request.session[ACTIVE_CCX_KEY] = None
context = {
'enrollment_message': enrollment_message,
'course_enrollment_pairs': course_enrollment_pairs,
@@ -693,6 +704,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,
}
if third_party_auth.is_enabled():
@@ -1810,6 +1822,16 @@ def activate_account(request, key):
if cea.auto_enroll:
CourseEnrollment.enroll(student[0], cea.course_id)
# 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",
{

View File

@@ -26,7 +26,6 @@ from xmodule.exceptions import NotFoundError
from xblock.fields import Scope, String, Boolean, Dict, Integer, Float
from .fields import Timedelta, Date
from django.utils.timezone import UTC
from .util.duedate import get_extended_due_date
from xmodule.capa_base_constants import RANDOMIZATION, SHOWANSWER
from django.conf import settings
@@ -107,14 +106,6 @@ class CapaFields(object):
values={"min": 0}, scope=Scope.settings
)
due = Date(help=_("Date that this problem is due by"), scope=Scope.settings)
extended_due = Date(
help=_("Date that this problem is due by for a particular student. This "
"can be set by an instructor, and will override the global due "
"date if it is set to a date that is later than the global due "
"date."),
default=None,
scope=Scope.user_state,
)
graceperiod = Timedelta(
help=_("Amount of time after the due date that submissions will be accepted"),
scope=Scope.settings
@@ -218,7 +209,7 @@ class CapaMixin(CapaFields):
def __init__(self, *args, **kwargs):
super(CapaMixin, self).__init__(*args, **kwargs)
due_date = get_extended_due_date(self)
due_date = self.due
if self.graceperiod is not None and due_date:
self.close_date = due_date + self.graceperiod

View File

@@ -23,7 +23,6 @@ V1_SETTINGS_ATTRIBUTES = [
"accept_file_upload",
"skip_spelling_checks",
"due",
"extended_due",
"graceperiod",
"weight",
"min_to_calibrate",
@@ -258,16 +257,6 @@ class CombinedOpenEndedFields(object):
help=_("Date that this problem is due by"),
scope=Scope.settings
)
extended_due = Date(
help=_(
"Date that this problem is due by for a particular student. This "
"can be set by an instructor, and will override the global due "
"date if it is set to a date that is later than the global due "
"date."
),
default=None,
scope=Scope.user_state,
)
graceperiod = Timedelta(
help=_("Amount of time after the due date that submissions will be accepted"),
scope=Scope.settings

View File

@@ -11,7 +11,7 @@ from datetime import datetime
import dateutil.parser
from lazy import lazy
from xmodule.exceptions import UndefinedContext
from xmodule.seq_module import SequenceDescriptor, SequenceModule
from xmodule.graders import grader_from_conf
from xmodule.tabs import CourseTabList
@@ -835,8 +835,16 @@ class CourseFields(object):
)
class CourseModule(CourseFields, SequenceModule): # pylint: disable=abstract-method
"""
The CourseDescriptor needs its module_class to be a SequenceModule, but some code that
expects a CourseDescriptor to have all its fields can fail if it gets a SequenceModule instead.
This class is to make sure that all the fields are present in all cases.
"""
class CourseDescriptor(CourseFields, SequenceDescriptor):
module_class = SequenceModule
module_class = CourseModule
def __init__(self, *args, **kwargs):
"""
@@ -1213,6 +1221,14 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
"""
# If this descriptor has been bound to a student, return the corresponding
# XModule. If not, just use the descriptor itself
try:
module = getattr(self, '_xmodule', None)
if not module:
module = self
except UndefinedContext:
module = self
all_descriptors = []
graded_sections = {}

View File

@@ -8,7 +8,6 @@ from xmodule.x_module import XModule
from xmodule.xml_module import XmlDescriptor
from xblock.fields import Scope, Integer, String
from .fields import Date
from .util.duedate import get_extended_due_date
log = logging.getLogger(__name__)
@@ -21,14 +20,6 @@ class FolditFields(object):
required_level = Integer(default=4, scope=Scope.settings)
required_sublevel = Integer(default=5, scope=Scope.settings)
due = Date(help="Date that this problem is due by", scope=Scope.settings)
extended_due = Date(
help="Date that this problem is due by for a particular student. This "
"can be set by an instructor, and will override the global due "
"date if it is set to a date that is later than the global due "
"date.",
default=None,
scope=Scope.user_state,
)
show_basic_score = String(scope=Scope.settings, default='false')
show_leaderboard = String(scope=Scope.settings, default='false')
@@ -49,7 +40,7 @@ class FolditModule(FolditFields, XModule):
show_leaderboard="false"/>
"""
super(FolditModule, self).__init__(*args, **kwargs)
self.due_time = get_extended_due_date(self)
self.due_time = self.due
def is_complete(self):
"""

View File

@@ -44,14 +44,6 @@ class InheritanceMixin(XBlockMixin):
help=_("Enter the default date by which problems are due."),
scope=Scope.settings,
)
extended_due = Date(
help="Date that this problem is due by for a particular student. This "
"can be set by an instructor, and will override the global due "
"date if it is set to a date that is later than the global due "
"date.",
default=None,
scope=Scope.user_state,
)
visible_to_staff_only = Boolean(
help=_("If true, can be seen only by course staff, regardless of start date."),
default=False,

View File

@@ -8,7 +8,6 @@ from xmodule.progress import Progress
from xmodule.stringify import stringify_children
from xmodule.open_ended_grading_classes import self_assessment_module
from xmodule.open_ended_grading_classes import open_ended_module
from xmodule.util.duedate import get_extended_due_date
from .combined_open_ended_rubric import CombinedOpenEndedRubric, GRADER_TYPE_IMAGE_DICT, HUMAN_GRADER_TYPE, LEGEND_LIST
from xmodule.open_ended_grading_classes.peer_grading_service import PeerGradingService, MockPeerGradingService
from xmodule.open_ended_grading_classes.openendedchild import OpenEndedChild
@@ -150,7 +149,7 @@ class CombinedOpenEndedV1Module(object):
'peer_grade_finished_submissions_when_none_pending', False
)
due_date = get_extended_due_date(instance_state)
due_date = instance_state.get('due', None)
grace_period_string = instance_state.get('graceperiod', None)
try:
self.timeinfo = TimeInfo(due_date, grace_period_string)

View File

@@ -11,7 +11,6 @@ from xmodule.fields import Date, Timedelta
from xmodule.modulestore.exceptions import ItemNotFoundError, NoPathToItem
from xmodule.raw_module import RawDescriptor
from xmodule.timeinfo import TimeInfo
from xmodule.util.duedate import get_extended_due_date
from xmodule.x_module import XModule, module_attr
from xmodule.open_ended_grading_classes.peer_grading_service import PeerGradingService, MockPeerGradingService
@@ -52,14 +51,6 @@ class PeerGradingFields(object):
due = Date(
help=_("Due date that should be displayed."),
scope=Scope.settings)
extended_due = Date(
help=_("Date that this problem is due by for a particular student. This "
"can be set by an instructor, and will override the global due "
"date if it is set to a date that is later than the global due "
"date."),
default=None,
scope=Scope.user_state,
)
graceperiod = Timedelta(
help=_("Amount of grace to give on the due date."),
scope=Scope.settings
@@ -141,8 +132,7 @@ class PeerGradingModule(PeerGradingFields, XModule):
self.linked_problem = self.system.get_module(linked_descriptors[0])
try:
self.timeinfo = TimeInfo(
get_extended_due_date(self), self.graceperiod)
self.timeinfo = TimeInfo(self.due, self.graceperiod)
except Exception:
log.error("Error parsing due date information in location {0}".format(self.location))
raise
@@ -570,7 +560,7 @@ class PeerGradingModule(PeerGradingFields, XModule):
except (NoPathToItem, ItemNotFoundError):
continue
if descriptor:
problem['due'] = get_extended_due_date(descriptor)
problem['due'] = descriptor.due
grace_period = descriptor.graceperiod
try:
problem_timeinfo = TimeInfo(problem['due'], grace_period)

View File

@@ -36,14 +36,6 @@ class SequenceFields(object):
help=_("Enter the date by which problems are due."),
scope=Scope.settings,
)
extended_due = Date(
help="Date that this problem is due by for a particular student. This "
"can be set by an instructor, and will override the global due "
"date if it is set to a date that is later than the global due "
"date.",
default=None,
scope=Scope.user_state,
)
# Entrance Exam flag -- see cms/contentstore/views/entrance_exam.py for usage
is_entrance_exam = Boolean(

View File

@@ -193,6 +193,7 @@ class CourseTab(object):
'edxnotes': EdxNotesTab,
'syllabus': SyllabusTab,
'instructor': InstructorTab, # not persisted
'ccx_coach': CcxCoachTab, # not persisted
}
tab_type = tab_dict.get('type')
@@ -375,6 +376,10 @@ class DiscussionTab(EnrolledOrStaffTab):
)
def can_display(self, course, settings, is_user_authenticated, is_user_staff, is_user_enrolled):
if settings.FEATURES.get('CUSTOM_COURSES_EDX', False):
from ccx.overrides import get_current_ccx # pylint: disable=import-error
if get_current_ccx():
return False
super_can_display = super(DiscussionTab, self).can_display(
course, settings, is_user_authenticated, is_user_staff, is_user_enrolled
)
@@ -733,6 +738,42 @@ class InstructorTab(StaffTab):
)
class CcxCoachTab(CourseTab):
"""
A tab for the custom course coaches.
"""
type = 'ccx_coach'
def __init__(self, tab_dict=None): # pylint: disable=unused-argument
super(CcxCoachTab, self).__init__(
name=_('CCX Coach'),
tab_id=self.type,
link_func=link_reverse_func('ccx_coach_dashboard'),
)
def can_display(self, course, settings, *args, **kw):
"""
Since we don't get the user here, we use a thread local defined in the ccx
overrides to get it, then use the course to get the coach role and find out if
the user is one.
"""
user_is_coach = False
if settings.FEATURES.get('CUSTOM_COURSES_EDX', False):
from opaque_keys.edx.locations import SlashSeparatedCourseKey
from student.roles import CourseCcxCoachRole # pylint: disable=import-error
from ccx.overrides import get_current_request # pylint: disable=import-error
course_id = course.id.to_deprecated_string()
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
role = CourseCcxCoachRole(course_key)
request = get_current_request()
if request is not None:
user_is_coach = role.has_user(request.user)
super_can_display = super(CcxCoachTab, self).can_display(
course, settings, *args, **kw
)
return user_is_coach and super_can_display
class CourseTabList(List):
"""
An XBlock field class that encapsulates a collection of Tabs in a course.
@@ -833,6 +874,9 @@ class CourseTabList(List):
instructor_tab = InstructorTab()
if instructor_tab.can_display(course, settings, is_user_authenticated, is_user_staff, is_user_enrolled):
yield instructor_tab
ccx_coach_tab = CcxCoachTab()
if ccx_coach_tab.can_display(course, settings, is_user_authenticated, is_user_staff, is_user_enrolled):
yield ccx_coach_tab
@staticmethod
def iterate_displayable_cms(

View File

@@ -430,13 +430,6 @@ class CapaModuleTest(unittest.TestCase):
due=self.yesterday_str)
self.assertTrue(module.closed())
def test_due_date_extension(self):
module = CapaFactory.create(
max_attempts="1", attempts="0", due=self.yesterday_str,
extended_due=self.tomorrow_str)
self.assertFalse(module.closed())
def test_parse_get_params(self):
# Valid GET param dict
@@ -1742,7 +1735,7 @@ class TestProblemCheckTracking(unittest.TestCase):
self.maxDiff = None
def test_choice_answer_text(self):
factory = self.capa_factory_for_problem_xml("""\
xml = """\
<problem display_name="Multiple Choice Questions">
<p>What color is the open ocean on a sunny day?</p>
<optionresponse>
@@ -1767,7 +1760,11 @@ class TestProblemCheckTracking(unittest.TestCase):
</checkboxgroup>
</choiceresponse>
</problem>
""")
"""
# Whitespace screws up comparisons
xml = ''.join(line.strip() for line in xml.split('\n'))
factory = self.capa_factory_for_problem_xml(xml)
module = factory.create()
answer_input_dict = {