Merge pull request #6636 from jazkarta/feature-pocs
MIT CCx (was Personal Online Courses)
This commit is contained in:
@@ -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):
|
||||
|
||||
@@ -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",
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = {}
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user