Merge pull request #15170 from edx/jeskew/PLAT_1316_partitions_inheritance
Move fields.py, inheritance.py, and partitions to openedx/core.
This commit is contained in:
@@ -11,26 +11,27 @@ import struct
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
from capa.capa_problem import LoncapaProblem, LoncapaSystem
|
||||
from capa.inputtypes import Status
|
||||
from capa.responsetypes import LoncapaProblemError, ResponseError, StudentInputError
|
||||
from capa.util import convert_files_to_filenames, get_inner_html_from_xpath
|
||||
from django.conf import settings
|
||||
from openedx.core.djangolib.markup import HTML, Text
|
||||
from openedx.core.lib.xblock_fields.fields import Date, Timedelta
|
||||
from pytz import utc
|
||||
from xblock.fields import Boolean, Dict, Float, Integer, Scope, String, XMLString
|
||||
from xblock.scorable import ScorableXBlockMixin, Score
|
||||
from xmodule.capa_base_constants import RANDOMIZATION, SHOW_CORRECTNESS, SHOWANSWER
|
||||
from xmodule.exceptions import NotFoundError
|
||||
|
||||
from .progress import Progress
|
||||
|
||||
# We don't want to force a dependency on datadog, so make the import conditional
|
||||
try:
|
||||
import dogstats_wrapper as dog_stats_api
|
||||
except ImportError:
|
||||
dog_stats_api = None
|
||||
from pytz import utc
|
||||
|
||||
from capa.capa_problem import LoncapaProblem, LoncapaSystem
|
||||
from capa.inputtypes import Status
|
||||
from capa.responsetypes import StudentInputError, ResponseError, LoncapaProblemError
|
||||
from capa.util import convert_files_to_filenames, get_inner_html_from_xpath
|
||||
from xblock.fields import Boolean, Dict, Float, Integer, Scope, String, XMLString
|
||||
from xblock.scorable import ScorableXBlockMixin, Score
|
||||
from xmodule.capa_base_constants import RANDOMIZATION, SHOWANSWER, SHOW_CORRECTNESS
|
||||
from xmodule.exceptions import NotFoundError
|
||||
from .fields import Date, Timedelta
|
||||
from .progress import Progress
|
||||
|
||||
from openedx.core.djangolib.markup import HTML, Text
|
||||
|
||||
log = logging.getLogger("edx.courseware")
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ from math import exp
|
||||
|
||||
from pytz import utc
|
||||
|
||||
DEFAULT_START_DATE = datetime(2030, 1, 1, tzinfo=utc)
|
||||
from openedx.core.lib.xblock_fields.inherited_fields import DEFAULT_START_DATE
|
||||
|
||||
|
||||
def clean_course_key(course_key, padding_char):
|
||||
|
||||
@@ -9,17 +9,16 @@ from datetime import datetime
|
||||
import requests
|
||||
from lazy import lazy
|
||||
from lxml import etree
|
||||
from openedx.core.lib.xblock_fields.fields import Date
|
||||
from openedx.core.lib.xblock_fields.inherited_fields import DEFAULT_START_DATE
|
||||
from path import Path as path
|
||||
from pytz import utc
|
||||
from xblock.fields import Scope, List, String, Dict, Boolean, Integer, Float
|
||||
|
||||
from xblock.fields import Boolean, Dict, Float, Integer, List, Scope, String
|
||||
from xmodule import course_metadata_utils
|
||||
from xmodule.course_metadata_utils import DEFAULT_START_DATE
|
||||
from xmodule.graders import grader_from_conf
|
||||
from xmodule.mixin import LicenseMixin
|
||||
from xmodule.seq_module import SequenceDescriptor, SequenceModule
|
||||
from xmodule.tabs import CourseTabList, InvalidTabsException
|
||||
from .fields import Date
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -1,255 +0,0 @@
|
||||
import time
|
||||
import logging
|
||||
import re
|
||||
|
||||
from xblock.fields import JSONField
|
||||
import datetime
|
||||
import dateutil.parser
|
||||
|
||||
from pytz import UTC
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Date(JSONField):
|
||||
'''
|
||||
Date fields know how to parse and produce json (iso) compatible formats. Converts to tz aware datetimes.
|
||||
'''
|
||||
# See note below about not defaulting these
|
||||
CURRENT_YEAR = datetime.datetime.now(UTC).year
|
||||
PREVENT_DEFAULT_DAY_MON_SEED1 = datetime.datetime(CURRENT_YEAR, 1, 1, tzinfo=UTC)
|
||||
PREVENT_DEFAULT_DAY_MON_SEED2 = datetime.datetime(CURRENT_YEAR, 2, 2, tzinfo=UTC)
|
||||
|
||||
MUTABLE = False
|
||||
|
||||
def _parse_date_wo_default_month_day(self, field):
|
||||
"""
|
||||
Parse the field as an iso string but prevent dateutils from defaulting the day or month while
|
||||
allowing it to default the other fields.
|
||||
"""
|
||||
# It's not trivial to replace dateutil b/c parsing timezones as Z, +03:30, -400 is hard in python
|
||||
# however, we don't want dateutil to default the month or day (but some tests at least expect
|
||||
# us to default year); so, we'll see if dateutil uses the defaults for these the hard way
|
||||
result = dateutil.parser.parse(field, default=self.PREVENT_DEFAULT_DAY_MON_SEED1)
|
||||
result_other = dateutil.parser.parse(field, default=self.PREVENT_DEFAULT_DAY_MON_SEED2)
|
||||
if result != result_other:
|
||||
log.warning("Field {0} is missing month or day".format(self.name))
|
||||
return None
|
||||
if result.tzinfo is None:
|
||||
result = result.replace(tzinfo=UTC)
|
||||
return result
|
||||
|
||||
def from_json(self, field):
|
||||
"""
|
||||
Parse an optional metadata key containing a time: if present, complain
|
||||
if it doesn't parse.
|
||||
Return None if not present or invalid.
|
||||
"""
|
||||
if field is None:
|
||||
return field
|
||||
elif field is "":
|
||||
return None
|
||||
elif isinstance(field, basestring):
|
||||
return self._parse_date_wo_default_month_day(field)
|
||||
elif isinstance(field, (int, long, float)):
|
||||
return datetime.datetime.fromtimestamp(field / 1000, UTC)
|
||||
elif isinstance(field, time.struct_time):
|
||||
return datetime.datetime.fromtimestamp(time.mktime(field), UTC)
|
||||
elif isinstance(field, datetime.datetime):
|
||||
return field
|
||||
else:
|
||||
msg = "Field {0} has bad value '{1}'".format(
|
||||
self.name, field)
|
||||
raise TypeError(msg)
|
||||
|
||||
def to_json(self, value):
|
||||
"""
|
||||
Convert a time struct to a string
|
||||
"""
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, time.struct_time):
|
||||
# struct_times are always utc
|
||||
return time.strftime('%Y-%m-%dT%H:%M:%SZ', value)
|
||||
elif isinstance(value, datetime.datetime):
|
||||
if value.tzinfo is None or value.utcoffset().total_seconds() == 0:
|
||||
if value.year < 1900:
|
||||
# strftime doesn't work for pre-1900 dates, so use
|
||||
# isoformat instead
|
||||
return value.isoformat()
|
||||
# isoformat adds +00:00 rather than Z
|
||||
return value.strftime('%Y-%m-%dT%H:%M:%SZ')
|
||||
else:
|
||||
return value.isoformat()
|
||||
else:
|
||||
raise TypeError("Cannot convert {!r} to json".format(value))
|
||||
|
||||
enforce_type = from_json
|
||||
|
||||
TIMEDELTA_REGEX = re.compile(r'^((?P<days>\d+?) day(?:s?))?(\s)?((?P<hours>\d+?) hour(?:s?))?(\s)?((?P<minutes>\d+?) minute(?:s)?)?(\s)?((?P<seconds>\d+?) second(?:s)?)?$')
|
||||
|
||||
|
||||
class Timedelta(JSONField):
|
||||
# Timedeltas are immutable, see http://docs.python.org/2/library/datetime.html#available-types
|
||||
MUTABLE = False
|
||||
|
||||
def from_json(self, time_str):
|
||||
"""
|
||||
time_str: A string with the following components:
|
||||
<D> day[s] (optional)
|
||||
<H> hour[s] (optional)
|
||||
<M> minute[s] (optional)
|
||||
<S> second[s] (optional)
|
||||
|
||||
Returns a datetime.timedelta parsed from the string
|
||||
"""
|
||||
if time_str is None:
|
||||
return None
|
||||
|
||||
if isinstance(time_str, datetime.timedelta):
|
||||
return time_str
|
||||
|
||||
parts = TIMEDELTA_REGEX.match(time_str)
|
||||
if not parts:
|
||||
return
|
||||
parts = parts.groupdict()
|
||||
time_params = {}
|
||||
for (name, param) in parts.iteritems():
|
||||
if param:
|
||||
time_params[name] = int(param)
|
||||
return datetime.timedelta(**time_params)
|
||||
|
||||
def to_json(self, value):
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
values = []
|
||||
for attr in ('days', 'hours', 'minutes', 'seconds'):
|
||||
cur_value = getattr(value, attr, 0)
|
||||
if cur_value > 0:
|
||||
values.append("%d %s" % (cur_value, attr))
|
||||
return ' '.join(values)
|
||||
|
||||
def enforce_type(self, value):
|
||||
"""
|
||||
Ensure that when set explicitly the Field is set to a timedelta
|
||||
"""
|
||||
if isinstance(value, datetime.timedelta) or value is None:
|
||||
return value
|
||||
|
||||
return self.from_json(value)
|
||||
|
||||
|
||||
class RelativeTime(JSONField):
|
||||
"""
|
||||
Field for start_time and end_time video module properties.
|
||||
|
||||
It was decided, that python representation of start_time and end_time
|
||||
should be python datetime.timedelta object, to be consistent with
|
||||
common time representation.
|
||||
|
||||
At the same time, serialized representation should be "HH:MM:SS"
|
||||
This format is convenient to use in XML (and it is used now),
|
||||
and also it is used in frond-end studio editor of video module as format
|
||||
for start and end time fields.
|
||||
|
||||
In database we previously had float type for start_time and end_time fields,
|
||||
so we are checking it also.
|
||||
|
||||
Python object of RelativeTime is datetime.timedelta.
|
||||
JSONed representation of RelativeTime is "HH:MM:SS"
|
||||
"""
|
||||
# Timedeltas are immutable, see http://docs.python.org/2/library/datetime.html#available-types
|
||||
MUTABLE = False
|
||||
|
||||
@classmethod
|
||||
def isotime_to_timedelta(cls, value):
|
||||
"""
|
||||
Validate that value in "HH:MM:SS" format and convert to timedelta.
|
||||
|
||||
Validate that user, that edits XML, sets proper format, and
|
||||
that max value that can be used by user is "23:59:59".
|
||||
"""
|
||||
try:
|
||||
obj_time = time.strptime(value, '%H:%M:%S')
|
||||
except ValueError as e:
|
||||
raise ValueError(
|
||||
"Incorrect RelativeTime value {!r} was set in XML or serialized. "
|
||||
"Original parse message is {}".format(value, e.message)
|
||||
)
|
||||
return datetime.timedelta(
|
||||
hours=obj_time.tm_hour,
|
||||
minutes=obj_time.tm_min,
|
||||
seconds=obj_time.tm_sec
|
||||
)
|
||||
|
||||
def from_json(self, value):
|
||||
"""
|
||||
Convert value is in 'HH:MM:SS' format to datetime.timedelta.
|
||||
|
||||
If not value, returns 0.
|
||||
If value is float (backward compatibility issue), convert to timedelta.
|
||||
"""
|
||||
if not value:
|
||||
return datetime.timedelta(seconds=0)
|
||||
|
||||
if isinstance(value, datetime.timedelta):
|
||||
return value
|
||||
|
||||
# We've seen serialized versions of float in this field
|
||||
if isinstance(value, float):
|
||||
return datetime.timedelta(seconds=value)
|
||||
|
||||
if isinstance(value, basestring):
|
||||
return self.isotime_to_timedelta(value)
|
||||
|
||||
msg = "RelativeTime Field {0} has bad value '{1!r}'".format(self.name, value)
|
||||
raise TypeError(msg)
|
||||
|
||||
def to_json(self, value):
|
||||
"""
|
||||
Convert datetime.timedelta to "HH:MM:SS" format.
|
||||
|
||||
If not value, return "00:00:00"
|
||||
|
||||
Backward compatibility: check if value is float, and convert it. No exceptions here.
|
||||
|
||||
If value is not float, but is exceed 23:59:59, raise exception.
|
||||
"""
|
||||
if not value:
|
||||
return "00:00:00"
|
||||
|
||||
if isinstance(value, float): # backward compatibility
|
||||
value = min(value, 86400)
|
||||
return self.timedelta_to_string(datetime.timedelta(seconds=value))
|
||||
|
||||
if isinstance(value, datetime.timedelta):
|
||||
if value.total_seconds() > 86400: # sanity check
|
||||
raise ValueError(
|
||||
"RelativeTime max value is 23:59:59=86400.0 seconds, "
|
||||
"but {} seconds is passed".format(value.total_seconds())
|
||||
)
|
||||
return self.timedelta_to_string(value)
|
||||
|
||||
raise TypeError("RelativeTime: cannot convert {!r} to json".format(value))
|
||||
|
||||
def timedelta_to_string(self, value):
|
||||
"""
|
||||
Makes first 'H' in str representation non-optional.
|
||||
|
||||
str(timedelta) has [H]H:MM:SS format, which is not suitable
|
||||
for front-end (and ISO time standard), so we force HH:MM:SS format.
|
||||
"""
|
||||
stringified = str(value)
|
||||
if len(stringified) == 7:
|
||||
stringified = '0' + stringified
|
||||
return stringified
|
||||
|
||||
def enforce_type(self, value):
|
||||
"""
|
||||
Ensure that when set explicitly the Field is set to a timedelta
|
||||
"""
|
||||
if isinstance(value, datetime.timedelta) or value is None:
|
||||
return value
|
||||
|
||||
return self.from_json(value)
|
||||
@@ -4,236 +4,9 @@ Support for inheritance of fields down an XBlock hierarchy.
|
||||
from __future__ import absolute_import
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from xmodule.partitions.partitions import UserPartition
|
||||
from xblock.fields import Scope, Boolean, String, Float, XBlockMixin, Dict, Integer, List
|
||||
from openedx.core.lib.xblock_fields.inherited_fields import InheritanceMixin
|
||||
from xblock.fields import Scope
|
||||
from xblock.runtime import KeyValueStore, KvsFieldData
|
||||
from xmodule.fields import Date, Timedelta
|
||||
from ..course_metadata_utils import DEFAULT_START_DATE
|
||||
|
||||
|
||||
# Make '_' a no-op so we can scrape strings
|
||||
# Using lambda instead of `django.utils.translation.ugettext_noop` because Django cannot be imported in this file
|
||||
_ = lambda text: text
|
||||
|
||||
|
||||
class UserPartitionList(List):
|
||||
"""Special List class for listing UserPartitions"""
|
||||
def from_json(self, values):
|
||||
return [UserPartition.from_json(v) for v in values]
|
||||
|
||||
def to_json(self, values):
|
||||
return [user_partition.to_json()
|
||||
for user_partition in values]
|
||||
|
||||
|
||||
class InheritanceMixin(XBlockMixin):
|
||||
"""Field definitions for inheritable fields."""
|
||||
|
||||
graded = Boolean(
|
||||
help="Whether this module contributes to the final course grade",
|
||||
scope=Scope.settings,
|
||||
default=False,
|
||||
)
|
||||
start = Date(
|
||||
help="Start time when this module is visible",
|
||||
default=DEFAULT_START_DATE,
|
||||
scope=Scope.settings
|
||||
)
|
||||
due = Date(
|
||||
display_name=_("Due Date"),
|
||||
help=_("Enter the default date by which problems are due."),
|
||||
scope=Scope.settings,
|
||||
)
|
||||
visible_to_staff_only = Boolean(
|
||||
help=_("If true, can be seen only by course staff, regardless of start date."),
|
||||
default=False,
|
||||
scope=Scope.settings,
|
||||
)
|
||||
course_edit_method = String(
|
||||
display_name=_("Course Editor"),
|
||||
help=_("Enter the method by which this course is edited (\"XML\" or \"Studio\")."),
|
||||
default="Studio",
|
||||
scope=Scope.settings,
|
||||
deprecated=True # Deprecated because user would not change away from Studio within Studio.
|
||||
)
|
||||
giturl = String(
|
||||
display_name=_("GIT URL"),
|
||||
help=_("Enter the URL for the course data GIT repository."),
|
||||
scope=Scope.settings
|
||||
)
|
||||
xqa_key = String(
|
||||
display_name=_("XQA Key"),
|
||||
help=_("This setting is not currently supported."), scope=Scope.settings,
|
||||
deprecated=True
|
||||
)
|
||||
annotation_storage_url = String(
|
||||
help=_("Enter the location of the annotation storage server. The textannotation, videoannotation, and imageannotation advanced modules require this setting."),
|
||||
scope=Scope.settings,
|
||||
default="http://your_annotation_storage.com",
|
||||
display_name=_("URL for Annotation Storage")
|
||||
)
|
||||
annotation_token_secret = String(
|
||||
help=_("Enter the secret string for annotation storage. The textannotation, videoannotation, and imageannotation advanced modules require this string."),
|
||||
scope=Scope.settings,
|
||||
default="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
|
||||
display_name=_("Secret Token String for Annotation")
|
||||
)
|
||||
graceperiod = Timedelta(
|
||||
help="Amount of time after the due date that submissions will be accepted",
|
||||
scope=Scope.settings,
|
||||
)
|
||||
group_access = Dict(
|
||||
help=_("Enter the ids for the content groups this problem belongs to."),
|
||||
scope=Scope.settings,
|
||||
)
|
||||
|
||||
showanswer = String(
|
||||
display_name=_("Show Answer"),
|
||||
help=_(
|
||||
# Translators: DO NOT translate the words in quotes here, they are
|
||||
# specific words for the acceptable values.
|
||||
'Specify when the Show Answer button appears for each problem. '
|
||||
'Valid values are "always", "answered", "attempted", "closed", '
|
||||
'"finished", "past_due", "correct_or_past_due", and "never".'
|
||||
),
|
||||
scope=Scope.settings,
|
||||
default="finished",
|
||||
)
|
||||
|
||||
show_correctness = String(
|
||||
display_name=_("Show Results"),
|
||||
help=_(
|
||||
# Translators: DO NOT translate the words in quotes here, they are
|
||||
# specific words for the acceptable values.
|
||||
'Specify when to show answer correctness and score to learners. '
|
||||
'Valid values are "always", "never", and "past_due".'
|
||||
),
|
||||
scope=Scope.settings,
|
||||
default="always",
|
||||
)
|
||||
|
||||
rerandomize = String(
|
||||
display_name=_("Randomization"),
|
||||
help=_(
|
||||
# Translators: DO NOT translate the words in quotes here, they are
|
||||
# specific words for the acceptable values.
|
||||
'Specify the default for how often variable values in a problem are randomized. '
|
||||
'This setting should be set to "never" unless you plan to provide a Python '
|
||||
'script to identify and randomize values in most of the problems in your course. '
|
||||
'Valid values are "always", "onreset", "never", and "per_student".'
|
||||
),
|
||||
scope=Scope.settings,
|
||||
default="never",
|
||||
)
|
||||
days_early_for_beta = Float(
|
||||
display_name=_("Days Early for Beta Users"),
|
||||
help=_("Enter the number of days before the start date that beta users can access the course."),
|
||||
scope=Scope.settings,
|
||||
default=None,
|
||||
)
|
||||
static_asset_path = String(
|
||||
display_name=_("Static Asset Path"),
|
||||
help=_("Enter the path to use for files on the Files & Uploads page. This value overrides the Studio default, c4x://."),
|
||||
scope=Scope.settings,
|
||||
default='',
|
||||
)
|
||||
use_latex_compiler = Boolean(
|
||||
display_name=_("Enable LaTeX Compiler"),
|
||||
help=_("Enter true or false. If true, you can use the LaTeX templates for HTML components and advanced Problem components."),
|
||||
default=False,
|
||||
scope=Scope.settings
|
||||
)
|
||||
max_attempts = Integer(
|
||||
display_name=_("Maximum Attempts"),
|
||||
help=_("Enter the maximum number of times a student can try to answer problems. By default, Maximum Attempts is set to null, meaning that students have an unlimited number of attempts for problems. You can override this course-wide setting for individual problems. However, if the course-wide setting is a specific number, you cannot set the Maximum Attempts for individual problems to unlimited."),
|
||||
values={"min": 0}, scope=Scope.settings
|
||||
)
|
||||
matlab_api_key = String(
|
||||
display_name=_("Matlab API key"),
|
||||
help=_("Enter the API key provided by MathWorks for accessing the MATLAB Hosted Service. "
|
||||
"This key is granted for exclusive use in this course for the specified duration. "
|
||||
"Do not share the API key with other courses. Notify MathWorks immediately "
|
||||
"if you believe the key is exposed or compromised. To obtain a key for your course, "
|
||||
"or to report an issue, please contact moocsupport@mathworks.com"),
|
||||
scope=Scope.settings
|
||||
)
|
||||
# This is should be scoped to content, but since it's defined in the policy
|
||||
# file, it is currently scoped to settings.
|
||||
user_partitions = UserPartitionList(
|
||||
display_name=_("Group Configurations"),
|
||||
help=_("Enter the configurations that govern how students are grouped together."),
|
||||
default=[],
|
||||
scope=Scope.settings
|
||||
)
|
||||
video_speed_optimizations = Boolean(
|
||||
display_name=_("Enable video caching system"),
|
||||
help=_("Enter true or false. If true, video caching will be used for HTML5 videos."),
|
||||
default=True,
|
||||
scope=Scope.settings
|
||||
)
|
||||
video_bumper = Dict(
|
||||
display_name=_("Video Pre-Roll"),
|
||||
help=_(
|
||||
"Identify a video, 5-10 seconds in length, to play before course videos. Enter the video ID from "
|
||||
"the Video Uploads page and one or more transcript files in the following format: {format}. "
|
||||
"For example, an entry for a video with two transcripts looks like this: {example}"
|
||||
).format(
|
||||
format='{"video_id": "ID", "transcripts": {"language": "/static/filename.srt"}}',
|
||||
example=(
|
||||
'{'
|
||||
'"video_id": "77cef264-d6f5-4cf2-ad9d-0178ab8c77be", '
|
||||
'"transcripts": {"en": "/static/DemoX-D01_1.srt", "uk": "/static/DemoX-D01_1_uk.srt"}'
|
||||
'}'
|
||||
),
|
||||
),
|
||||
scope=Scope.settings
|
||||
)
|
||||
|
||||
reset_key = "DEFAULT_SHOW_RESET_BUTTON"
|
||||
default_reset_button = getattr(settings, reset_key) if hasattr(settings, reset_key) else False
|
||||
show_reset_button = Boolean(
|
||||
display_name=_("Show Reset Button for Problems"),
|
||||
help=_(
|
||||
"Enter true or false. If true, problems in the course default to always displaying a 'Reset' button. "
|
||||
"You can override this in each problem's settings. All existing problems are affected when "
|
||||
"this course-wide setting is changed."
|
||||
),
|
||||
scope=Scope.settings,
|
||||
default=default_reset_button
|
||||
)
|
||||
edxnotes = Boolean(
|
||||
display_name=_("Enable Student Notes"),
|
||||
help=_("Enter true or false. If true, students can use the Student Notes feature."),
|
||||
default=False,
|
||||
scope=Scope.settings
|
||||
)
|
||||
edxnotes_visibility = Boolean(
|
||||
display_name="Student Notes Visibility",
|
||||
help=_("Indicates whether Student Notes are visible in the course. "
|
||||
"Students can also show or hide their notes in the courseware."),
|
||||
default=True,
|
||||
scope=Scope.user_info
|
||||
)
|
||||
|
||||
in_entrance_exam = Boolean(
|
||||
display_name=_("Tag this module as part of an Entrance Exam section"),
|
||||
help=_("Enter true or false. If true, answer submissions for problem modules will be "
|
||||
"considered in the Entrance Exam scoring/gating algorithm."),
|
||||
scope=Scope.settings,
|
||||
default=False
|
||||
)
|
||||
|
||||
self_paced = Boolean(
|
||||
display_name=_('Self Paced'),
|
||||
help=_(
|
||||
'Set this to "true" to mark this course as self-paced. Self-paced courses do not have '
|
||||
'due dates for assignments, and students can progress through the course at any rate before '
|
||||
'the course ends.'
|
||||
),
|
||||
default=False,
|
||||
scope=Scope.settings
|
||||
)
|
||||
|
||||
|
||||
def compute_inherited_metadata(descriptor):
|
||||
|
||||
@@ -13,46 +13,45 @@ structure:
|
||||
"""
|
||||
|
||||
import copy
|
||||
from datetime import datetime
|
||||
from importlib import import_module
|
||||
import logging
|
||||
import pymongo
|
||||
import re
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from importlib import import_module
|
||||
from uuid import uuid4
|
||||
|
||||
import pymongo
|
||||
from bson.son import SON
|
||||
from contracts import contract, new_contract
|
||||
from fs.osfs import OSFS
|
||||
from mongodb_proxy import autoretry_read
|
||||
from opaque_keys.edx.keys import UsageKey, CourseKey, AssetKey
|
||||
from opaque_keys.edx.locations import Location, BlockUsageLocator, SlashSeparatedCourseKey
|
||||
from opaque_keys.edx.keys import AssetKey, CourseKey, UsageKey
|
||||
from opaque_keys.edx.locations import BlockUsageLocator, Location, SlashSeparatedCourseKey
|
||||
from opaque_keys.edx.locator import CourseLocator, LibraryLocator
|
||||
from openedx.core.lib.partitions.partitions_service import PartitionService
|
||||
from openedx.core.lib.xblock_fields.inherited_fields import InheritanceMixin
|
||||
from path import Path as path
|
||||
from pytz import UTC
|
||||
from xblock.core import XBlock
|
||||
from xblock.exceptions import InvalidScopeError
|
||||
from xblock.fields import Scope, ScopeIds, Reference, ReferenceList, ReferenceValueDict
|
||||
from xblock.fields import Reference, ReferenceList, ReferenceValueDict, Scope, ScopeIds
|
||||
from xblock.runtime import KvsFieldData
|
||||
|
||||
from xmodule.assetstore import AssetMetadata, CourseAssetsFromStorage
|
||||
from xmodule.course_module import CourseSummary
|
||||
from xmodule.error_module import ErrorDescriptor
|
||||
from xmodule.errortracker import null_error_tracker, exc_info_to_str
|
||||
from xmodule.errortracker import exc_info_to_str, null_error_tracker
|
||||
from xmodule.exceptions import HeartbeatFailure
|
||||
from xmodule.mako_module import MakoDescriptorSystem
|
||||
from xmodule.mongo_utils import connect_to_mongodb, create_collection_index
|
||||
from xmodule.modulestore import ModuleStoreWriteBase, ModuleStoreEnum, BulkOperationsMixin, BulkOpsRecord
|
||||
from xmodule.modulestore.draft_and_published import ModuleStoreDraftAndPublished, DIRECT_ONLY_CATEGORIES
|
||||
from xmodule.modulestore import BulkOperationsMixin, BulkOpsRecord, ModuleStoreEnum, ModuleStoreWriteBase
|
||||
from xmodule.modulestore.draft_and_published import DIRECT_ONLY_CATEGORIES, ModuleStoreDraftAndPublished
|
||||
from xmodule.modulestore.edit_info import EditInfoRuntimeMixin
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError, DuplicateCourseError, ReferentialIntegrityError
|
||||
from xmodule.modulestore.inheritance import InheritanceMixin, inherit_metadata, InheritanceKeyValueStore
|
||||
from xmodule.partitions.partitions_service import PartitionService
|
||||
from xmodule.modulestore.xml import CourseLocationManager
|
||||
from xmodule.modulestore.exceptions import DuplicateCourseError, ItemNotFoundError, ReferentialIntegrityError
|
||||
from xmodule.modulestore.inheritance import InheritanceKeyValueStore, inherit_metadata
|
||||
from xmodule.modulestore.store_utilities import DETACHED_XBLOCK_TYPES
|
||||
from xmodule.modulestore.xml import CourseLocationManager
|
||||
from xmodule.mongo_utils import connect_to_mongodb, create_collection_index
|
||||
from xmodule.services import SettingsService
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
new_contract('CourseKey', CourseKey)
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
import sys
|
||||
import logging
|
||||
import sys
|
||||
|
||||
from contracts import contract, new_contract
|
||||
from fs.osfs import OSFS
|
||||
from lazy import lazy
|
||||
from xblock.runtime import KvsFieldData, KeyValueStore
|
||||
from xblock.fields import ScopeIds
|
||||
from opaque_keys.edx.locator import BlockUsageLocator, CourseLocator, DefinitionLocator, LibraryLocator, LocalId
|
||||
from openedx.core.lib.xblock_fields.inherited_fields import InheritanceMixin
|
||||
from xblock.core import XBlock
|
||||
from opaque_keys.edx.locator import BlockUsageLocator, LocalId, CourseLocator, LibraryLocator, DefinitionLocator
|
||||
|
||||
from xmodule.library_tools import LibraryToolsService
|
||||
from xmodule.mako_module import MakoDescriptorSystem
|
||||
from xblock.fields import ScopeIds
|
||||
from xblock.runtime import KeyValueStore, KvsFieldData
|
||||
from xmodule.error_module import ErrorDescriptor
|
||||
from xmodule.errortracker import exc_info_to_str
|
||||
from xmodule.library_tools import LibraryToolsService
|
||||
from xmodule.mako_module import MakoDescriptorSystem
|
||||
from xmodule.modulestore import BlockData
|
||||
from xmodule.modulestore.edit_info import EditInfoRuntimeMixin
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
from xmodule.modulestore.inheritance import inheriting_field_data, InheritanceMixin
|
||||
from xmodule.modulestore.inheritance import inheriting_field_data
|
||||
from xmodule.modulestore.split_mongo import BlockKey, CourseEnvelope
|
||||
from xmodule.modulestore.split_mongo.id_manager import SplitMongoIdManager
|
||||
from xmodule.modulestore.split_mongo.definition_lazy_loader import DefinitionLazyLoader
|
||||
from xmodule.modulestore.split_mongo.id_manager import SplitMongoIdManager
|
||||
from xmodule.modulestore.split_mongo.split_mongo_kvs import SplitMongoKVS
|
||||
from xmodule.x_module import XModuleMixin
|
||||
|
||||
|
||||
@@ -57,41 +57,56 @@ import copy
|
||||
import datetime
|
||||
import hashlib
|
||||
import logging
|
||||
import six
|
||||
from contracts import contract, new_contract
|
||||
from collections import defaultdict
|
||||
from importlib import import_module
|
||||
from mongodb_proxy import autoretry_read
|
||||
from path import Path as path
|
||||
from pytz import UTC
|
||||
from bson.objectid import ObjectId
|
||||
from types import NoneType
|
||||
|
||||
from xblock.core import XBlock
|
||||
from xblock.fields import Scope, Reference, ReferenceList, ReferenceValueDict
|
||||
from xmodule.course_module import CourseSummary
|
||||
from xmodule.errortracker import null_error_tracker
|
||||
import six
|
||||
|
||||
from bson.objectid import ObjectId
|
||||
from ccx_keys.locator import CCXBlockUsageLocator, CCXLocator
|
||||
from contracts import contract, new_contract
|
||||
from mongodb_proxy import autoretry_read
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from opaque_keys.edx.locator import (
|
||||
BlockUsageLocator, DefinitionLocator, CourseLocator, LibraryLocator, VersionTree, LocalId,
|
||||
BlockUsageLocator,
|
||||
CourseLocator,
|
||||
DefinitionLocator,
|
||||
LibraryLocator,
|
||||
LocalId,
|
||||
VersionTree
|
||||
)
|
||||
from ccx_keys.locator import CCXLocator, CCXBlockUsageLocator
|
||||
from xmodule.modulestore.exceptions import InsufficientSpecificationError, VersionConflictError, DuplicateItemError, \
|
||||
DuplicateCourseError, MultipleCourseBlocksFound
|
||||
from openedx.core.lib.partitions.partitions_service import PartitionService
|
||||
from openedx.core.lib.xblock_fields.inherited_fields import InheritanceMixin
|
||||
from path import Path as path
|
||||
from pytz import UTC
|
||||
from xblock.core import XBlock
|
||||
from xblock.fields import Reference, ReferenceList, ReferenceValueDict, Scope
|
||||
from xmodule.assetstore import AssetMetadata
|
||||
from xmodule.course_module import CourseSummary
|
||||
from xmodule.error_module import ErrorDescriptor
|
||||
from xmodule.errortracker import null_error_tracker
|
||||
from xmodule.modulestore import (
|
||||
inheritance, ModuleStoreWriteBase, ModuleStoreEnum,
|
||||
BulkOpsRecord, BulkOperationsMixin, SortedAssetList, BlockData
|
||||
BlockData,
|
||||
BulkOperationsMixin,
|
||||
BulkOpsRecord,
|
||||
ModuleStoreEnum,
|
||||
ModuleStoreWriteBase,
|
||||
SortedAssetList
|
||||
)
|
||||
from xmodule.modulestore.exceptions import (
|
||||
DuplicateCourseError,
|
||||
DuplicateItemError,
|
||||
InsufficientSpecificationError,
|
||||
MultipleCourseBlocksFound,
|
||||
VersionConflictError
|
||||
)
|
||||
from xmodule.modulestore.split_mongo import BlockKey, CourseEnvelope
|
||||
from xmodule.modulestore.split_mongo.mongo_connection import DuplicateKeyError, MongoConnection
|
||||
from xmodule.modulestore.store_utilities import DETACHED_XBLOCK_TYPES
|
||||
|
||||
from ..exceptions import ItemNotFoundError
|
||||
from .caching_descriptor_system import CachingDescriptorSystem
|
||||
from xmodule.partitions.partitions_service import PartitionService
|
||||
from xmodule.modulestore.split_mongo.mongo_connection import MongoConnection, DuplicateKeyError
|
||||
from xmodule.modulestore.split_mongo import BlockKey, CourseEnvelope
|
||||
from xmodule.modulestore.store_utilities import DETACHED_XBLOCK_TYPES
|
||||
from xmodule.error_module import ErrorDescriptor
|
||||
from collections import defaultdict
|
||||
from types import NoneType
|
||||
from xmodule.assetstore import AssetMetadata
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -2149,7 +2164,7 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase):
|
||||
else:
|
||||
inherited_settings = parent_xblock.xblock_kvs.inherited_settings.copy()
|
||||
if fields is not None:
|
||||
for field_name in inheritance.InheritanceMixin.fields:
|
||||
for field_name in InheritanceMixin.fields:
|
||||
if field_name in fields:
|
||||
inherited_settings[field_name] = fields[field_name]
|
||||
|
||||
@@ -2698,7 +2713,7 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase):
|
||||
# update the inheriting w/ what should pass to children
|
||||
inheriting_settings = inherited_settings_map[block_key].copy()
|
||||
block_fields = block_data.fields
|
||||
for field_name in inheritance.InheritanceMixin.fields:
|
||||
for field_name in InheritanceMixin.fields:
|
||||
if field_name in block_fields:
|
||||
inheriting_settings[field_name] = block_fields[field_name]
|
||||
|
||||
|
||||
@@ -14,23 +14,25 @@ and then for each combination of modulestores, performing the sequence:
|
||||
|
||||
import itertools
|
||||
import os
|
||||
from path import Path as path
|
||||
from shutil import rmtree
|
||||
from tempfile import mkdtemp
|
||||
|
||||
import ddt
|
||||
from nose.plugins.attrib import attr
|
||||
from mock import patch
|
||||
|
||||
from xmodule.tests import CourseComparisonTest
|
||||
from xmodule.modulestore.xml_importer import import_course_from_xml
|
||||
from xmodule.modulestore.xml_exporter import export_course_to_xml
|
||||
from xmodule.modulestore.tests.utils import mock_tab_from_json
|
||||
from xmodule.partitions.tests.test_partitions import PartitionTestCase
|
||||
from nose.plugins.attrib import attr
|
||||
from openedx.core.lib.partitions.tests.test_partitions import PartitionTestCase
|
||||
from path import Path as path
|
||||
from xmodule.modulestore.tests.utils import (
|
||||
MongoContentstoreBuilder, MODULESTORE_SETUPS, SPLIT_MODULESTORE_SETUP,
|
||||
CONTENTSTORE_SETUPS, TEST_DATA_DIR
|
||||
CONTENTSTORE_SETUPS,
|
||||
MODULESTORE_SETUPS,
|
||||
SPLIT_MODULESTORE_SETUP,
|
||||
TEST_DATA_DIR,
|
||||
MongoContentstoreBuilder,
|
||||
mock_tab_from_json
|
||||
)
|
||||
from xmodule.modulestore.xml_exporter import export_course_to_xml
|
||||
from xmodule.modulestore.xml_importer import import_course_from_xml
|
||||
from xmodule.tests import CourseComparisonTest
|
||||
|
||||
COURSE_DATA_NAMES = (
|
||||
'toy',
|
||||
|
||||
@@ -1,59 +1,66 @@
|
||||
"""
|
||||
Unit tests for the Mixed Modulestore, with DDT for the various stores (Split, Draft, XML)
|
||||
"""
|
||||
from collections import namedtuple
|
||||
import datetime
|
||||
import logging
|
||||
import ddt
|
||||
import itertools
|
||||
import logging
|
||||
import mimetypes
|
||||
from uuid import uuid4
|
||||
from collections import namedtuple
|
||||
from contextlib import contextmanager
|
||||
from mock import patch, Mock, call
|
||||
from shutil import rmtree
|
||||
from tempfile import mkdtemp
|
||||
from uuid import uuid4
|
||||
|
||||
import ddt
|
||||
import pymongo
|
||||
# Mixed modulestore depends on django, so we'll manually configure some django settings
|
||||
# before importing the module
|
||||
# TODO remove this import and the configuration -- xmodule should not depend on django!
|
||||
from django.conf import settings
|
||||
from mock import Mock, call, patch
|
||||
from nose import SkipTest
|
||||
# This import breaks this test file when run separately. Needs to be fixed! (PLAT-449)
|
||||
from nose.plugins.attrib import attr
|
||||
from nose import SkipTest
|
||||
import pymongo
|
||||
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
from opaque_keys.edx.locator import BlockUsageLocator, CourseLocator, LibraryLocator
|
||||
from openedx.core.lib.xblock_fields.inherited_fields import InheritanceMixin
|
||||
from pytz import UTC
|
||||
from shutil import rmtree
|
||||
from tempfile import mkdtemp
|
||||
|
||||
from xmodule.x_module import XModuleMixin
|
||||
from xmodule.modulestore.edit_info import EditInfoMixin
|
||||
from xmodule.modulestore.inheritance import InheritanceMixin
|
||||
from xmodule.modulestore.tests.utils import MongoContentstoreBuilder
|
||||
from xmodule.contentstore.content import StaticContent
|
||||
from xmodule.modulestore.xml_importer import import_course_from_xml
|
||||
from xmodule.modulestore.xml_exporter import export_course_to_xml
|
||||
from xmodule.modulestore.tests.test_asides import AsideTestType
|
||||
from xblock.core import XBlockAside
|
||||
from xblock.fields import Scope, String, ScopeIds
|
||||
from xblock.fields import Scope, ScopeIds, String
|
||||
from xblock.fragment import Fragment
|
||||
from xblock.runtime import DictKeyValueStore, KvsFieldData
|
||||
from xblock.test.tools import TestRuntime
|
||||
from xmodule.contentstore.content import StaticContent
|
||||
from xmodule.exceptions import InvalidVersionError
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.draft_and_published import DIRECT_ONLY_CATEGORIES, UnsupportedRevisionError
|
||||
from xmodule.modulestore.edit_info import EditInfoMixin
|
||||
from xmodule.modulestore.exceptions import (
|
||||
DuplicateCourseError,
|
||||
ItemNotFoundError,
|
||||
NoPathToItem,
|
||||
ReferentialIntegrityError
|
||||
)
|
||||
from xmodule.modulestore.mixed import MixedModuleStore
|
||||
from xmodule.modulestore.search import navigation_index, path_to_location
|
||||
from xmodule.modulestore.store_utilities import DETACHED_XBLOCK_TYPES
|
||||
from xmodule.modulestore.tests.factories import check_exact_number_of_calls, check_mongo_calls, mongo_uses_error_check
|
||||
from xmodule.modulestore.tests.mongo_connection import MONGO_HOST, MONGO_PORT_NUM
|
||||
from xmodule.modulestore.tests.test_asides import AsideTestType
|
||||
from xmodule.modulestore.tests.utils import (
|
||||
LocationMixin,
|
||||
MongoContentstoreBuilder,
|
||||
create_modulestore_instance,
|
||||
mock_tab_from_json
|
||||
)
|
||||
from xmodule.modulestore.xml_exporter import export_course_to_xml
|
||||
from xmodule.modulestore.xml_importer import import_course_from_xml
|
||||
from xmodule.tests import DATA_DIR, CourseComparisonTest
|
||||
from xmodule.x_module import XModuleMixin
|
||||
|
||||
if not settings.configured:
|
||||
settings.configure()
|
||||
|
||||
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
from opaque_keys.edx.locator import BlockUsageLocator, CourseLocator, LibraryLocator
|
||||
from xmodule.exceptions import InvalidVersionError
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.draft_and_published import UnsupportedRevisionError, DIRECT_ONLY_CATEGORIES
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError, DuplicateCourseError, ReferentialIntegrityError, NoPathToItem
|
||||
from xmodule.modulestore.mixed import MixedModuleStore
|
||||
from xmodule.modulestore.search import path_to_location, navigation_index
|
||||
from xmodule.modulestore.store_utilities import DETACHED_XBLOCK_TYPES
|
||||
from xmodule.modulestore.tests.factories import check_mongo_calls, check_exact_number_of_calls, \
|
||||
mongo_uses_error_check
|
||||
from xmodule.modulestore.tests.utils import create_modulestore_instance, LocationMixin, mock_tab_from_json
|
||||
from xmodule.modulestore.tests.mongo_connection import MONGO_PORT_NUM, MONGO_HOST
|
||||
from xmodule.tests import DATA_DIR, CourseComparisonTest
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -1,52 +1,54 @@
|
||||
"""
|
||||
Unit tests for the Mongo modulestore
|
||||
"""
|
||||
# pylint: disable=protected-access
|
||||
# pylint: disable=no-name-in-module
|
||||
# pylint: disable=bad-continuation
|
||||
from nose.tools import assert_equals, assert_raises, \
|
||||
assert_not_equals, assert_false, assert_true, assert_greater, assert_is_instance, assert_is_none
|
||||
# pylint: enable=E0611
|
||||
from path import Path as path
|
||||
import pymongo
|
||||
|
||||
import logging
|
||||
import shutil
|
||||
import unittest
|
||||
from datetime import datetime
|
||||
from tempfile import mkdtemp
|
||||
from uuid import uuid4
|
||||
from datetime import datetime
|
||||
from pytz import UTC
|
||||
import unittest
|
||||
from mock import patch
|
||||
from xblock.core import XBlock
|
||||
|
||||
from xblock.fields import Scope, Reference, ReferenceList, ReferenceValueDict
|
||||
from xblock.runtime import KeyValueStore
|
||||
from xblock.exceptions import InvalidScopeError
|
||||
|
||||
from xmodule.tests import DATA_DIR
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from opaque_keys.edx.locations import Location
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.mongo import MongoKeyValueStore
|
||||
from xmodule.modulestore.draft import DraftModuleStore
|
||||
from opaque_keys.edx.locations import SlashSeparatedCourseKey, AssetLocation
|
||||
from opaque_keys.edx.locator import LibraryLocator, CourseLocator
|
||||
from opaque_keys.edx.keys import UsageKey
|
||||
from xmodule.modulestore.xml_exporter import export_course_to_xml
|
||||
from xmodule.modulestore.xml_importer import import_course_from_xml, perform_xlint
|
||||
from xmodule.contentstore.mongo import MongoContentStore
|
||||
|
||||
from nose.tools import assert_in
|
||||
from xmodule.exceptions import NotFoundError
|
||||
import pymongo
|
||||
from git.test.lib.asserts import assert_not_none
|
||||
from xmodule.x_module import XModuleMixin
|
||||
from xmodule.modulestore.mongo.base import as_draft
|
||||
from xmodule.modulestore.tests.mongo_connection import MONGO_PORT_NUM, MONGO_HOST
|
||||
from xmodule.modulestore.tests.utils import LocationMixin, mock_tab_from_json
|
||||
from mock import patch
|
||||
# pylint: disable=no-name-in-module
|
||||
# pylint: disable=bad-continuation
|
||||
from nose.tools import (
|
||||
assert_equals,
|
||||
assert_false,
|
||||
assert_greater,
|
||||
assert_in,
|
||||
assert_is_instance,
|
||||
assert_is_none,
|
||||
assert_not_equals,
|
||||
assert_raises,
|
||||
assert_true
|
||||
)
|
||||
from opaque_keys.edx.keys import CourseKey, UsageKey
|
||||
from opaque_keys.edx.locations import AssetLocation, Location, SlashSeparatedCourseKey
|
||||
from opaque_keys.edx.locator import CourseLocator, LibraryLocator
|
||||
from openedx.core.lib.xblock_fields.inherited_fields import InheritanceMixin
|
||||
from path import Path as path
|
||||
from pytz import UTC
|
||||
from xblock.core import XBlock
|
||||
from xblock.exceptions import InvalidScopeError
|
||||
from xblock.fields import Reference, ReferenceList, ReferenceValueDict, Scope
|
||||
from xblock.runtime import KeyValueStore
|
||||
from xmodule.contentstore.mongo import MongoContentStore
|
||||
from xmodule.exceptions import NotFoundError
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.draft import DraftModuleStore
|
||||
from xmodule.modulestore.edit_info import EditInfoMixin
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
from xmodule.modulestore.inheritance import InheritanceMixin
|
||||
|
||||
from xmodule.modulestore.mongo import MongoKeyValueStore
|
||||
from xmodule.modulestore.mongo.base import as_draft
|
||||
from xmodule.modulestore.tests.mongo_connection import MONGO_HOST, MONGO_PORT_NUM
|
||||
from xmodule.modulestore.tests.utils import LocationMixin, mock_tab_from_json
|
||||
from xmodule.modulestore.xml_exporter import export_course_to_xml
|
||||
from xmodule.modulestore.xml_importer import import_course_from_xml, perform_xlint
|
||||
from xmodule.tests import DATA_DIR
|
||||
from xmodule.x_module import XModuleMixin
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -1,42 +1,42 @@
|
||||
"""
|
||||
Test split modulestore w/o using any django stuff.
|
||||
"""
|
||||
from mock import patch
|
||||
import datetime
|
||||
from importlib import import_module
|
||||
from path import Path as path
|
||||
import random
|
||||
import re
|
||||
import unittest
|
||||
import uuid
|
||||
from importlib import import_module
|
||||
|
||||
import ddt
|
||||
from ccx_keys.locator import CCXBlockUsageLocator
|
||||
from contracts import contract
|
||||
from django.core.cache import InvalidCacheBackendError, caches
|
||||
from mock import patch
|
||||
from nose.plugins.attrib import attr
|
||||
from django.core.cache import caches, InvalidCacheBackendError
|
||||
|
||||
from opaque_keys.edx.locator import BlockUsageLocator, CourseKey, CourseLocator, LocalId, VersionTree
|
||||
from openedx.core.lib import tempdir
|
||||
from openedx.core.lib.xblock_fields.fields import Date, Timedelta
|
||||
from openedx.core.lib.xblock_fields.inherited_fields import InheritanceMixin
|
||||
from path import Path as path
|
||||
from xblock.fields import Reference, ReferenceList, ReferenceValueDict
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.exceptions import (
|
||||
ItemNotFoundError, VersionConflictError,
|
||||
DuplicateItemError, DuplicateCourseError,
|
||||
InsufficientSpecificationError
|
||||
)
|
||||
from opaque_keys.edx.locator import CourseKey, CourseLocator, BlockUsageLocator, VersionTree, LocalId
|
||||
from ccx_keys.locator import CCXBlockUsageLocator
|
||||
from xmodule.modulestore.inheritance import InheritanceMixin
|
||||
from xmodule.x_module import XModuleMixin
|
||||
from xmodule.fields import Date, Timedelta
|
||||
from xmodule.modulestore.split_mongo.split import SplitMongoModuleStore
|
||||
from xmodule.modulestore.tests.test_modulestore import check_has_course_method
|
||||
from xmodule.modulestore.split_mongo import BlockKey
|
||||
from xmodule.modulestore.tests.factories import check_mongo_calls
|
||||
from xmodule.modulestore.tests.mongo_connection import MONGO_PORT_NUM, MONGO_HOST
|
||||
from xmodule.modulestore.tests.utils import mock_tab_from_json
|
||||
from xmodule.modulestore.edit_info import EditInfoMixin
|
||||
|
||||
from xmodule.modulestore.exceptions import (
|
||||
DuplicateCourseError,
|
||||
DuplicateItemError,
|
||||
InsufficientSpecificationError,
|
||||
ItemNotFoundError,
|
||||
VersionConflictError
|
||||
)
|
||||
from xmodule.modulestore.split_mongo import BlockKey
|
||||
from xmodule.modulestore.split_mongo.split import SplitMongoModuleStore
|
||||
from xmodule.modulestore.tests.factories import check_mongo_calls
|
||||
from xmodule.modulestore.tests.mongo_connection import MONGO_HOST, MONGO_PORT_NUM
|
||||
from xmodule.modulestore.tests.test_modulestore import check_has_course_method
|
||||
from xmodule.modulestore.tests.utils import mock_tab_from_json
|
||||
from xmodule.x_module import XModuleMixin
|
||||
|
||||
BRANCH_NAME_DRAFT = ModuleStoreEnum.BranchName.draft
|
||||
BRANCH_NAME_PUBLISHED = ModuleStoreEnum.BranchName.published
|
||||
|
||||
@@ -3,17 +3,16 @@ import random
|
||||
import unittest
|
||||
import uuid
|
||||
|
||||
from nose.plugins.attrib import attr
|
||||
import mock
|
||||
|
||||
from opaque_keys.edx.locator import CourseLocator, BlockUsageLocator
|
||||
from nose.plugins.attrib import attr
|
||||
from opaque_keys.edx.locator import BlockUsageLocator, CourseLocator
|
||||
from openedx.core.lib.xblock_fields.inherited_fields import InheritanceMixin
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.x_module import XModuleMixin
|
||||
from xmodule.modulestore.inheritance import InheritanceMixin
|
||||
from xmodule.modulestore.mongo import DraftMongoModuleStore
|
||||
from xmodule.modulestore.split_mongo.split import SplitMongoModuleStore
|
||||
from xmodule.modulestore.tests.mongo_connection import MONGO_PORT_NUM, MONGO_HOST
|
||||
from xmodule.modulestore.tests.mongo_connection import MONGO_HOST, MONGO_PORT_NUM
|
||||
from xmodule.modulestore.tests.utils import MemoryCache
|
||||
from xmodule.x_module import XModuleMixin
|
||||
|
||||
|
||||
@attr('mongo')
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
"""
|
||||
Tests for XML importer.
|
||||
"""
|
||||
import mock
|
||||
from opaque_keys.edx.locator import BlockUsageLocator, CourseLocator
|
||||
from xblock.fields import String, Scope, ScopeIds, List
|
||||
from xblock.runtime import Runtime, KvsFieldData, DictKeyValueStore
|
||||
from xmodule.x_module import XModuleMixin
|
||||
from opaque_keys.edx.locations import Location
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.inheritance import InheritanceMixin
|
||||
from xmodule.modulestore.xml_importer import _update_and_import_module, _update_module_location
|
||||
from xmodule.modulestore.tests.mongo_connection import MONGO_PORT_NUM, MONGO_HOST
|
||||
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
from xmodule.tests import DATA_DIR
|
||||
from uuid import uuid4
|
||||
import unittest
|
||||
import importlib
|
||||
import unittest
|
||||
from uuid import uuid4
|
||||
|
||||
import mock
|
||||
from opaque_keys.edx.locations import Location, SlashSeparatedCourseKey
|
||||
from opaque_keys.edx.locator import BlockUsageLocator, CourseLocator
|
||||
from openedx.core.lib.xblock_fields.inherited_fields import InheritanceMixin
|
||||
from xblock.fields import List, Scope, ScopeIds, String
|
||||
from xblock.runtime import DictKeyValueStore, KvsFieldData, Runtime
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.tests.mongo_connection import MONGO_HOST, MONGO_PORT_NUM
|
||||
from xmodule.modulestore.xml_importer import _update_and_import_module, _update_module_location
|
||||
from xmodule.tests import DATA_DIR
|
||||
from xmodule.x_module import XModuleMixin
|
||||
|
||||
|
||||
class ModuleStoreNoSettings(unittest.TestCase):
|
||||
|
||||
@@ -2,30 +2,29 @@
|
||||
Helper classes and methods for running modulestore tests without Django.
|
||||
"""
|
||||
import random
|
||||
|
||||
from contextlib import contextmanager, nested
|
||||
from importlib import import_module
|
||||
from opaque_keys.edx.keys import UsageKey
|
||||
from path import Path as path
|
||||
from shutil import rmtree
|
||||
from tempfile import mkdtemp
|
||||
from unittest import TestCase
|
||||
|
||||
from opaque_keys.edx.keys import UsageKey
|
||||
from openedx.core.lib.xblock_fields.inherited_fields import InheritanceMixin
|
||||
from path import Path as path
|
||||
from xblock.fields import XBlockMixin
|
||||
from xmodule.x_module import XModuleMixin
|
||||
from xmodule.contentstore.mongo import MongoContentStore
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.draft_and_published import ModuleStoreDraftAndPublished
|
||||
from xmodule.modulestore.edit_info import EditInfoMixin
|
||||
from xmodule.modulestore.inheritance import InheritanceMixin
|
||||
from xmodule.modulestore.mixed import MixedModuleStore
|
||||
from xmodule.modulestore.mongo.base import ModuleStoreEnum
|
||||
from xmodule.modulestore.mongo.draft import DraftModuleStore
|
||||
from xmodule.modulestore.split_mongo.split_draft import DraftVersioningModuleStore
|
||||
from xmodule.modulestore.tests.factories import ItemFactory
|
||||
from xmodule.modulestore.tests.mongo_connection import MONGO_PORT_NUM, MONGO_HOST
|
||||
from xmodule.modulestore.tests.mongo_connection import MONGO_HOST, MONGO_PORT_NUM
|
||||
from xmodule.modulestore.xml import XMLModuleStore
|
||||
from xmodule.tests import DATA_DIR
|
||||
from xmodule.x_module import XModuleMixin
|
||||
|
||||
|
||||
def load_function(path):
|
||||
|
||||
@@ -1,240 +0,0 @@
|
||||
"""Defines ``Group`` and ``UserPartition`` models for partitioning"""
|
||||
|
||||
from collections import namedtuple
|
||||
from stevedore.extension import ExtensionManager
|
||||
|
||||
# We use ``id`` in this file as the IDs of our Groups and UserPartitions,
|
||||
# which Pylint disapproves of.
|
||||
# pylint: disable=redefined-builtin
|
||||
|
||||
|
||||
# UserPartition IDs must be unique. The Cohort and Random UserPartitions (when they are
|
||||
# created via Studio) choose an unused ID in the range of 100 (historical) to MAX_INT. Therefore the
|
||||
# dynamic UserPartitionIDs must be under 100, and they have to be hard-coded to ensure
|
||||
# they are always the same whenever the dynamic partition is added (since the UserPartition
|
||||
# ID is stored in the xblock group_access dict).
|
||||
ENROLLMENT_TRACK_PARTITION_ID = 50
|
||||
|
||||
MINIMUM_STATIC_PARTITION_ID = 100
|
||||
|
||||
|
||||
class UserPartitionError(Exception):
|
||||
"""
|
||||
Base Exception for when an error was found regarding user partitions.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class NoSuchUserPartitionError(UserPartitionError):
|
||||
"""
|
||||
Exception to be raised when looking up a UserPartition by its ID fails.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class NoSuchUserPartitionGroupError(UserPartitionError):
|
||||
"""
|
||||
Exception to be raised when looking up a UserPartition Group by its ID fails.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class Group(namedtuple("Group", "id name")):
|
||||
"""
|
||||
An id and name for a group of students. The id should be unique
|
||||
within the UserPartition this group appears in.
|
||||
"""
|
||||
# in case we want to add to this class, a version will be handy
|
||||
# for deserializing old versions. (This will be serialized in courses)
|
||||
VERSION = 1
|
||||
|
||||
def __new__(cls, id, name):
|
||||
return super(Group, cls).__new__(cls, int(id), name)
|
||||
|
||||
def to_json(self):
|
||||
"""
|
||||
'Serialize' to a json-serializable representation.
|
||||
|
||||
Returns:
|
||||
a dictionary with keys for the properties of the group.
|
||||
"""
|
||||
return {
|
||||
"id": self.id,
|
||||
"name": self.name,
|
||||
"version": Group.VERSION
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def from_json(value):
|
||||
"""
|
||||
Deserialize a Group from a json-like representation.
|
||||
|
||||
Args:
|
||||
value: a dictionary with keys for the properties of the group.
|
||||
|
||||
Raises TypeError if the value doesn't have the right keys.
|
||||
"""
|
||||
if isinstance(value, Group):
|
||||
return value
|
||||
|
||||
for key in ("id", "name", "version"):
|
||||
if key not in value:
|
||||
raise TypeError("Group dict {0} missing value key '{1}'".format(
|
||||
value, key))
|
||||
|
||||
if value["version"] != Group.VERSION:
|
||||
raise TypeError("Group dict {0} has unexpected version".format(
|
||||
value))
|
||||
|
||||
return Group(value["id"], value["name"])
|
||||
|
||||
|
||||
# The Stevedore extension point namespace for user partition scheme plugins.
|
||||
USER_PARTITION_SCHEME_NAMESPACE = 'openedx.user_partition_scheme'
|
||||
|
||||
|
||||
class UserPartition(namedtuple("UserPartition", "id name description groups scheme parameters active")):
|
||||
"""A named way to partition users into groups, primarily intended for
|
||||
running experiments. It is expected that each user will be in at most one
|
||||
group in a partition.
|
||||
|
||||
A Partition has an id, name, scheme, description, parameters, and a list
|
||||
of groups. The id is intended to be unique within the context where these
|
||||
are used. (e.g., for partitions of users within a course, the ids should
|
||||
be unique per-course). The scheme is used to assign users into groups.
|
||||
The parameters field is used to save extra parameters e.g., location of
|
||||
the block in case of VerificationPartitionScheme.
|
||||
|
||||
Partitions can be marked as inactive by setting the "active" flag to False.
|
||||
Any group access rule referencing inactive partitions will be ignored
|
||||
when performing access checks.
|
||||
"""
|
||||
VERSION = 3
|
||||
|
||||
# The collection of user partition scheme extensions.
|
||||
scheme_extensions = None
|
||||
|
||||
# The default scheme to be used when upgrading version 1 partitions.
|
||||
VERSION_1_SCHEME = "random"
|
||||
|
||||
def __new__(cls, id, name, description, groups, scheme=None, parameters=None, active=True, scheme_id=VERSION_1_SCHEME): # pylint: disable=line-too-long
|
||||
if not scheme:
|
||||
scheme = UserPartition.get_scheme(scheme_id)
|
||||
if parameters is None:
|
||||
parameters = {}
|
||||
|
||||
return super(UserPartition, cls).__new__(cls, int(id), name, description, groups, scheme, parameters, active)
|
||||
|
||||
@staticmethod
|
||||
def get_scheme(name):
|
||||
"""
|
||||
Returns the user partition scheme with the given name.
|
||||
"""
|
||||
# Note: we're creating the extension manager lazily to ensure that the Python path
|
||||
# has been correctly set up. Trying to create this statically will fail, unfortunately.
|
||||
if not UserPartition.scheme_extensions:
|
||||
UserPartition.scheme_extensions = ExtensionManager(namespace=USER_PARTITION_SCHEME_NAMESPACE)
|
||||
try:
|
||||
scheme = UserPartition.scheme_extensions[name].plugin
|
||||
except KeyError:
|
||||
raise UserPartitionError("Unrecognized scheme '{0}'".format(name))
|
||||
scheme.name = name
|
||||
return scheme
|
||||
|
||||
def to_json(self):
|
||||
"""
|
||||
'Serialize' to a json-serializable representation.
|
||||
|
||||
Returns:
|
||||
a dictionary with keys for the properties of the partition.
|
||||
"""
|
||||
return {
|
||||
"id": self.id,
|
||||
"name": self.name,
|
||||
"scheme": self.scheme.name,
|
||||
"description": self.description,
|
||||
"parameters": self.parameters,
|
||||
"groups": [g.to_json() for g in self.groups],
|
||||
"active": bool(self.active),
|
||||
"version": UserPartition.VERSION
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def from_json(value):
|
||||
"""
|
||||
Deserialize a Group from a json-like representation.
|
||||
|
||||
Args:
|
||||
value: a dictionary with keys for the properties of the group.
|
||||
|
||||
Raises TypeError if the value doesn't have the right keys.
|
||||
"""
|
||||
if isinstance(value, UserPartition):
|
||||
return value
|
||||
|
||||
for key in ("id", "name", "description", "version", "groups"):
|
||||
if key not in value:
|
||||
raise TypeError("UserPartition dict {0} missing value key '{1}'".format(value, key))
|
||||
|
||||
if value["version"] == 1:
|
||||
# If no scheme was provided, set it to the default ('random')
|
||||
scheme_id = UserPartition.VERSION_1_SCHEME
|
||||
|
||||
# Version changes should be backwards compatible in case the code
|
||||
# gets rolled back. If we see a version number greater than the current
|
||||
# version, we should try to read it rather than raising an exception.
|
||||
elif value["version"] >= 2:
|
||||
if "scheme" not in value:
|
||||
raise TypeError("UserPartition dict {0} missing value key 'scheme'".format(value))
|
||||
|
||||
scheme_id = value["scheme"]
|
||||
else:
|
||||
raise TypeError("UserPartition dict {0} has unexpected version".format(value))
|
||||
|
||||
parameters = value.get("parameters", {})
|
||||
active = value.get("active", True)
|
||||
groups = [Group.from_json(g) for g in value["groups"]]
|
||||
scheme = UserPartition.get_scheme(scheme_id)
|
||||
if not scheme:
|
||||
raise TypeError("UserPartition dict {0} has unrecognized scheme {1}".format(value, scheme_id))
|
||||
|
||||
if hasattr(scheme, "create_user_partition"):
|
||||
return scheme.create_user_partition(
|
||||
value["id"],
|
||||
value["name"],
|
||||
value["description"],
|
||||
groups,
|
||||
parameters,
|
||||
active,
|
||||
)
|
||||
else:
|
||||
return UserPartition(
|
||||
value["id"],
|
||||
value["name"],
|
||||
value["description"],
|
||||
groups,
|
||||
scheme,
|
||||
parameters,
|
||||
active,
|
||||
)
|
||||
|
||||
def get_group(self, group_id):
|
||||
"""
|
||||
Returns the group with the specified id.
|
||||
|
||||
Arguments:
|
||||
group_id (int): ID of the partition group.
|
||||
|
||||
Raises:
|
||||
NoSuchUserPartitionGroupError: The specified group could not be found.
|
||||
|
||||
"""
|
||||
for group in self.groups:
|
||||
if group.id == group_id:
|
||||
return group
|
||||
|
||||
raise NoSuchUserPartitionGroupError(
|
||||
"Could not find a Group with ID [{group_id}] in UserPartition [{partition_id}].".format(
|
||||
group_id=group_id, partition_id=self.id
|
||||
)
|
||||
)
|
||||
@@ -1,183 +0,0 @@
|
||||
"""
|
||||
This is a service-like API that assigns tracks which groups users are in for various
|
||||
user partitions. It uses the user_service key/value store provided by the LMS runtime to
|
||||
persist the assignments.
|
||||
"""
|
||||
from django.conf import settings
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
import logging
|
||||
|
||||
from xmodule.partitions.partitions import UserPartition, UserPartitionError, ENROLLMENT_TRACK_PARTITION_ID
|
||||
from xmodule.modulestore.django import modulestore
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# settings will not be available when running nosetests.
|
||||
FEATURES = getattr(settings, 'FEATURES', {})
|
||||
|
||||
|
||||
def get_all_partitions_for_course(course, active_only=False):
|
||||
"""
|
||||
A method that returns all `UserPartitions` associated with a course, as a List.
|
||||
This will include the ones defined in course.user_partitions, but it may also
|
||||
include dynamically included partitions (such as the `EnrollmentTrackUserPartition`).
|
||||
|
||||
Args:
|
||||
course: the course for which user partitions should be returned.
|
||||
active_only: if `True`, only partitions with `active` set to True will be returned.
|
||||
|
||||
Returns:
|
||||
A List of UserPartitions associated with the course.
|
||||
"""
|
||||
all_partitions = course.user_partitions + _get_dynamic_partitions(course)
|
||||
if active_only:
|
||||
all_partitions = [partition for partition in all_partitions if partition.active]
|
||||
return all_partitions
|
||||
|
||||
|
||||
def _get_dynamic_partitions(course):
|
||||
"""
|
||||
Return the dynamic user partitions for this course.
|
||||
If none exists, returns an empty array.
|
||||
"""
|
||||
enrollment_partition = _create_enrollment_track_partition(course)
|
||||
return [enrollment_partition] if enrollment_partition else []
|
||||
|
||||
|
||||
def _create_enrollment_track_partition(course):
|
||||
"""
|
||||
Create and return the dynamic enrollment track user partition.
|
||||
If it cannot be created, None is returned.
|
||||
"""
|
||||
if not FEATURES.get('ENABLE_ENROLLMENT_TRACK_USER_PARTITION'):
|
||||
return None
|
||||
|
||||
try:
|
||||
enrollment_track_scheme = UserPartition.get_scheme("enrollment_track")
|
||||
except UserPartitionError:
|
||||
log.warning("No 'enrollment_track' scheme registered, EnrollmentTrackUserPartition will not be created.")
|
||||
return None
|
||||
|
||||
used_ids = set(p.id for p in course.user_partitions)
|
||||
if ENROLLMENT_TRACK_PARTITION_ID in used_ids:
|
||||
# TODO: change to Exception after this has been in production for awhile, see TNL-6796.
|
||||
log.warning(
|
||||
"Can't add 'enrollment_track' partition, as ID {id} is assigned to {partition} in course {course}.".format(
|
||||
id=ENROLLMENT_TRACK_PARTITION_ID,
|
||||
partition=_get_partition_from_id(course.user_partitions, ENROLLMENT_TRACK_PARTITION_ID).name,
|
||||
course=unicode(course.id)
|
||||
)
|
||||
)
|
||||
return None
|
||||
|
||||
partition = enrollment_track_scheme.create_user_partition(
|
||||
id=ENROLLMENT_TRACK_PARTITION_ID,
|
||||
name=_(u"Enrollment Track Groups"),
|
||||
description=_(u"Partition for segmenting users by enrollment track"),
|
||||
parameters={"course_id": unicode(course.id)}
|
||||
)
|
||||
return partition
|
||||
|
||||
|
||||
class PartitionService(object):
|
||||
"""
|
||||
This is an XBlock service that returns information about the user partitions associated
|
||||
with a given course.
|
||||
"""
|
||||
|
||||
def __init__(self, course_id, track_function=None, cache=None):
|
||||
self._course_id = course_id
|
||||
self._track_function = track_function
|
||||
self._cache = cache
|
||||
|
||||
def get_course(self):
|
||||
"""
|
||||
Return the course instance associated with this PartitionService.
|
||||
This default implementation looks up the course from the modulestore.
|
||||
"""
|
||||
return modulestore().get_course(self._course_id)
|
||||
|
||||
@property
|
||||
def course_partitions(self):
|
||||
"""
|
||||
Return the set of partitions assigned to self._course_id (both those set directly on the course
|
||||
through course.user_partitions, and any dynamic partitions that exist). Note: this returns
|
||||
both active and inactive partitions.
|
||||
"""
|
||||
return get_all_partitions_for_course(self.get_course())
|
||||
|
||||
def get_user_group_id_for_partition(self, user, user_partition_id):
|
||||
"""
|
||||
If the user is already assigned to a group in user_partition_id, return the
|
||||
group_id.
|
||||
|
||||
If not, assign them to one of the groups, persist that decision, and
|
||||
return the group_id.
|
||||
|
||||
Args:
|
||||
user_partition_id -- an id of a partition that's hopefully in the
|
||||
runtime.user_partitions list.
|
||||
|
||||
Returns:
|
||||
The id of one of the groups in the specified user_partition_id (as a string).
|
||||
|
||||
Raises:
|
||||
ValueError if the user_partition_id isn't found.
|
||||
"""
|
||||
cache_key = "PartitionService.ugidfp.{}.{}.{}".format(
|
||||
user.id, self._course_id, user_partition_id
|
||||
)
|
||||
|
||||
if self._cache and (cache_key in self._cache):
|
||||
return self._cache[cache_key]
|
||||
|
||||
user_partition = self._get_user_partition(user_partition_id)
|
||||
if user_partition is None:
|
||||
raise ValueError(
|
||||
"Configuration problem! No user_partition with id {0} "
|
||||
"in course {1}".format(user_partition_id, self._course_id)
|
||||
)
|
||||
|
||||
group = self.get_group(user, user_partition)
|
||||
group_id = group.id if group else None
|
||||
|
||||
if self._cache is not None:
|
||||
self._cache[cache_key] = group_id
|
||||
|
||||
return group_id
|
||||
|
||||
def _get_user_partition(self, user_partition_id):
|
||||
"""
|
||||
Look for a user partition with a matching id in the course's partitions.
|
||||
Note that this method can return an inactive user partition.
|
||||
|
||||
Returns:
|
||||
A UserPartition, or None if not found.
|
||||
"""
|
||||
return _get_partition_from_id(self.course_partitions, user_partition_id)
|
||||
|
||||
def get_group(self, user, user_partition, assign=True):
|
||||
"""
|
||||
Returns the group from the specified user partition to which the user is assigned.
|
||||
If the user has not yet been assigned, a group will be chosen for them based upon
|
||||
the partition's scheme.
|
||||
"""
|
||||
return user_partition.scheme.get_group_for_user(
|
||||
self._course_id, user, user_partition, assign=assign, track_function=self._track_function
|
||||
)
|
||||
|
||||
|
||||
def _get_partition_from_id(partitions, user_partition_id):
|
||||
"""
|
||||
Look for a user partition with a matching id in the provided list of partitions.
|
||||
|
||||
Returns:
|
||||
A UserPartition, or None if not found.
|
||||
"""
|
||||
for partition in partitions:
|
||||
if partition.id == user_partition_id:
|
||||
return partition
|
||||
|
||||
return None
|
||||
@@ -1,620 +0,0 @@
|
||||
"""
|
||||
Test the partitions and partitions service
|
||||
|
||||
"""
|
||||
|
||||
from unittest import TestCase
|
||||
from mock import Mock
|
||||
|
||||
from opaque_keys.edx.locator import CourseLocator
|
||||
from stevedore.extension import Extension, ExtensionManager
|
||||
from xmodule.partitions.partitions import (
|
||||
Group, UserPartition, UserPartitionError, NoSuchUserPartitionGroupError,
|
||||
USER_PARTITION_SCHEME_NAMESPACE, ENROLLMENT_TRACK_PARTITION_ID
|
||||
)
|
||||
from xmodule.partitions.partitions_service import (
|
||||
PartitionService, get_all_partitions_for_course, FEATURES
|
||||
)
|
||||
|
||||
|
||||
class TestGroup(TestCase):
|
||||
"""Test constructing groups"""
|
||||
def test_construct(self):
|
||||
test_id = 10
|
||||
name = "Grendel"
|
||||
group = Group(test_id, name)
|
||||
self.assertEqual(group.id, test_id)
|
||||
self.assertEqual(group.name, name)
|
||||
|
||||
def test_string_id(self):
|
||||
test_id = "10"
|
||||
name = "Grendel"
|
||||
group = Group(test_id, name)
|
||||
self.assertEqual(group.id, 10)
|
||||
|
||||
def test_to_json(self):
|
||||
test_id = 10
|
||||
name = "Grendel"
|
||||
group = Group(test_id, name)
|
||||
jsonified = group.to_json()
|
||||
act_jsonified = {
|
||||
"id": test_id,
|
||||
"name": name,
|
||||
"version": group.VERSION
|
||||
}
|
||||
self.assertEqual(jsonified, act_jsonified)
|
||||
|
||||
def test_from_json(self):
|
||||
test_id = 5
|
||||
name = "Grendel"
|
||||
jsonified = {
|
||||
"id": test_id,
|
||||
"name": name,
|
||||
"version": Group.VERSION
|
||||
}
|
||||
group = Group.from_json(jsonified)
|
||||
self.assertEqual(group.id, test_id)
|
||||
self.assertEqual(group.name, name)
|
||||
|
||||
def test_from_json_broken(self):
|
||||
test_id = 5
|
||||
name = "Grendel"
|
||||
# Bad version
|
||||
jsonified = {
|
||||
"id": test_id,
|
||||
"name": name,
|
||||
"version": -1,
|
||||
}
|
||||
with self.assertRaisesRegexp(TypeError, "has unexpected version"):
|
||||
Group.from_json(jsonified)
|
||||
|
||||
# Missing key "id"
|
||||
jsonified = {
|
||||
"name": name,
|
||||
"version": Group.VERSION
|
||||
}
|
||||
with self.assertRaisesRegexp(TypeError, "missing value key 'id'"):
|
||||
Group.from_json(jsonified)
|
||||
|
||||
# Has extra key - should not be a problem
|
||||
jsonified = {
|
||||
"id": test_id,
|
||||
"name": name,
|
||||
"version": Group.VERSION,
|
||||
"programmer": "Cale"
|
||||
}
|
||||
group = Group.from_json(jsonified)
|
||||
self.assertNotIn("programmer", group.to_json())
|
||||
|
||||
|
||||
class MockUserPartitionScheme(object):
|
||||
"""
|
||||
Mock user partition scheme
|
||||
"""
|
||||
def __init__(self, name="mock", current_group=None, **kwargs):
|
||||
super(MockUserPartitionScheme, self).__init__(**kwargs)
|
||||
self.name = name
|
||||
self.current_group = current_group
|
||||
|
||||
def get_group_for_user(self, course_id, user, user_partition, assign=True, track_function=None): # pylint: disable=unused-argument
|
||||
"""
|
||||
Returns the current group if set, else the first group from the specified user partition.
|
||||
"""
|
||||
if self.current_group:
|
||||
return self.current_group
|
||||
groups = user_partition.groups
|
||||
if not groups or len(groups) == 0:
|
||||
return None
|
||||
return groups[0]
|
||||
|
||||
|
||||
class MockEnrollmentTrackUserPartitionScheme(MockUserPartitionScheme):
|
||||
|
||||
def create_user_partition(self, id, name, description, groups=None, parameters=None, active=True): # pylint: disable=redefined-builtin, invalid-name, unused-argument
|
||||
"""
|
||||
The EnrollmentTrackPartitionScheme provides this method to return a subclass of UserPartition.
|
||||
"""
|
||||
return UserPartition(id, name, description, groups, self, parameters, active)
|
||||
|
||||
|
||||
class PartitionTestCase(TestCase):
|
||||
"""Base class for test cases that require partitions"""
|
||||
TEST_ID = 0
|
||||
TEST_NAME = "Mock Partition"
|
||||
TEST_DESCRIPTION = "for testing purposes"
|
||||
TEST_PARAMETERS = {"location": "block-v1:edX+DemoX+Demo+type@block@uuid"}
|
||||
TEST_GROUPS = [Group(0, 'Group 1'), Group(1, 'Group 2')]
|
||||
TEST_SCHEME_NAME = "mock"
|
||||
ENROLLMENT_TRACK_SCHEME_NAME = "enrollment_track"
|
||||
|
||||
def setUp(self):
|
||||
super(PartitionTestCase, self).setUp()
|
||||
# Set up two user partition schemes: mock and random
|
||||
self.non_random_scheme = MockUserPartitionScheme(self.TEST_SCHEME_NAME)
|
||||
self.random_scheme = MockUserPartitionScheme("random")
|
||||
self.enrollment_track_scheme = MockEnrollmentTrackUserPartitionScheme(self.ENROLLMENT_TRACK_SCHEME_NAME)
|
||||
extensions = [
|
||||
Extension(
|
||||
self.non_random_scheme.name, USER_PARTITION_SCHEME_NAMESPACE, self.non_random_scheme, None
|
||||
),
|
||||
Extension(
|
||||
self.random_scheme.name, USER_PARTITION_SCHEME_NAMESPACE, self.random_scheme, None
|
||||
),
|
||||
Extension(
|
||||
self.enrollment_track_scheme.name, USER_PARTITION_SCHEME_NAMESPACE, self.enrollment_track_scheme, None
|
||||
),
|
||||
]
|
||||
UserPartition.scheme_extensions = ExtensionManager.make_test_instance(
|
||||
extensions, namespace=USER_PARTITION_SCHEME_NAMESPACE
|
||||
)
|
||||
|
||||
# Be sure to clean up the global scheme_extensions after the test.
|
||||
self.addCleanup(self.cleanup_scheme_extensions)
|
||||
|
||||
# Create a test partition
|
||||
self.user_partition = UserPartition(
|
||||
self.TEST_ID,
|
||||
self.TEST_NAME,
|
||||
self.TEST_DESCRIPTION,
|
||||
self.TEST_GROUPS,
|
||||
extensions[0].plugin,
|
||||
self.TEST_PARAMETERS,
|
||||
)
|
||||
|
||||
# Make sure the names are set on the schemes (which happens normally in code, but may not happen in tests).
|
||||
self.user_partition.get_scheme(self.non_random_scheme.name)
|
||||
self.user_partition.get_scheme(self.random_scheme.name)
|
||||
|
||||
def cleanup_scheme_extensions(self):
|
||||
"""
|
||||
Unset the UserPartition.scheme_extensions cache.
|
||||
"""
|
||||
UserPartition.scheme_extensions = None
|
||||
|
||||
|
||||
class TestUserPartition(PartitionTestCase):
|
||||
"""Test constructing UserPartitions"""
|
||||
|
||||
def test_construct(self):
|
||||
user_partition = UserPartition(
|
||||
self.TEST_ID,
|
||||
self.TEST_NAME,
|
||||
self.TEST_DESCRIPTION,
|
||||
self.TEST_GROUPS,
|
||||
MockUserPartitionScheme(),
|
||||
self.TEST_PARAMETERS,
|
||||
)
|
||||
self.assertEqual(user_partition.id, self.TEST_ID)
|
||||
self.assertEqual(user_partition.name, self.TEST_NAME)
|
||||
self.assertEqual(user_partition.description, self.TEST_DESCRIPTION)
|
||||
self.assertEqual(user_partition.groups, self.TEST_GROUPS)
|
||||
self.assertEquals(user_partition.scheme.name, self.TEST_SCHEME_NAME)
|
||||
self.assertEquals(user_partition.parameters, self.TEST_PARAMETERS)
|
||||
|
||||
def test_string_id(self):
|
||||
user_partition = UserPartition(
|
||||
"70",
|
||||
self.TEST_NAME,
|
||||
self.TEST_DESCRIPTION,
|
||||
self.TEST_GROUPS,
|
||||
MockUserPartitionScheme(),
|
||||
self.TEST_PARAMETERS,
|
||||
)
|
||||
self.assertEqual(user_partition.id, 70)
|
||||
|
||||
def test_to_json(self):
|
||||
jsonified = self.user_partition.to_json()
|
||||
act_jsonified = {
|
||||
"id": self.TEST_ID,
|
||||
"name": self.TEST_NAME,
|
||||
"description": self.TEST_DESCRIPTION,
|
||||
"parameters": self.TEST_PARAMETERS,
|
||||
"groups": [group.to_json() for group in self.TEST_GROUPS],
|
||||
"version": self.user_partition.VERSION,
|
||||
"scheme": self.TEST_SCHEME_NAME,
|
||||
"active": True,
|
||||
}
|
||||
self.assertEqual(jsonified, act_jsonified)
|
||||
|
||||
def test_from_json(self):
|
||||
jsonified = {
|
||||
"id": self.TEST_ID,
|
||||
"name": self.TEST_NAME,
|
||||
"description": self.TEST_DESCRIPTION,
|
||||
"parameters": self.TEST_PARAMETERS,
|
||||
"groups": [group.to_json() for group in self.TEST_GROUPS],
|
||||
"version": UserPartition.VERSION,
|
||||
"scheme": "mock",
|
||||
}
|
||||
user_partition = UserPartition.from_json(jsonified)
|
||||
self.assertEqual(user_partition.id, self.TEST_ID)
|
||||
self.assertEqual(user_partition.name, self.TEST_NAME)
|
||||
self.assertEqual(user_partition.description, self.TEST_DESCRIPTION)
|
||||
self.assertEqual(user_partition.parameters, self.TEST_PARAMETERS)
|
||||
|
||||
for act_group in user_partition.groups:
|
||||
self.assertIn(act_group.id, [0, 1])
|
||||
exp_group = self.TEST_GROUPS[act_group.id]
|
||||
self.assertEqual(exp_group.id, act_group.id)
|
||||
self.assertEqual(exp_group.name, act_group.name)
|
||||
|
||||
def test_version_upgrade(self):
|
||||
# Test that version 1 partitions did not have a scheme specified
|
||||
# and have empty parameters
|
||||
jsonified = {
|
||||
"id": self.TEST_ID,
|
||||
"name": self.TEST_NAME,
|
||||
"description": self.TEST_DESCRIPTION,
|
||||
"groups": [group.to_json() for group in self.TEST_GROUPS],
|
||||
"version": 1,
|
||||
}
|
||||
user_partition = UserPartition.from_json(jsonified)
|
||||
self.assertEqual(user_partition.scheme.name, "random")
|
||||
self.assertEqual(user_partition.parameters, {})
|
||||
self.assertTrue(user_partition.active)
|
||||
|
||||
def test_version_upgrade_2_to_3(self):
|
||||
# Test that version 3 user partition raises error if 'scheme' field is
|
||||
# not provided (same behavior as version 2)
|
||||
jsonified = {
|
||||
'id': self.TEST_ID,
|
||||
"name": self.TEST_NAME,
|
||||
"description": self.TEST_DESCRIPTION,
|
||||
"parameters": self.TEST_PARAMETERS,
|
||||
"groups": [group.to_json() for group in self.TEST_GROUPS],
|
||||
"version": 2,
|
||||
}
|
||||
with self.assertRaisesRegexp(TypeError, "missing value key 'scheme'"):
|
||||
UserPartition.from_json(jsonified)
|
||||
|
||||
# Test that version 3 partitions have a scheme specified
|
||||
# and a field 'parameters' (optional while setting user partition but
|
||||
# always present in response)
|
||||
jsonified = {
|
||||
"id": self.TEST_ID,
|
||||
"name": self.TEST_NAME,
|
||||
"description": self.TEST_DESCRIPTION,
|
||||
"groups": [group.to_json() for group in self.TEST_GROUPS],
|
||||
"version": 2,
|
||||
"scheme": self.TEST_SCHEME_NAME,
|
||||
}
|
||||
user_partition = UserPartition.from_json(jsonified)
|
||||
self.assertEqual(user_partition.scheme.name, self.TEST_SCHEME_NAME)
|
||||
self.assertEqual(user_partition.parameters, {})
|
||||
self.assertTrue(user_partition.active)
|
||||
|
||||
# now test that parameters dict is present in response with same value
|
||||
# as provided
|
||||
jsonified = {
|
||||
"id": self.TEST_ID,
|
||||
"name": self.TEST_NAME,
|
||||
"description": self.TEST_DESCRIPTION,
|
||||
"groups": [group.to_json() for group in self.TEST_GROUPS],
|
||||
"parameters": self.TEST_PARAMETERS,
|
||||
"version": 3,
|
||||
"scheme": self.TEST_SCHEME_NAME,
|
||||
}
|
||||
user_partition = UserPartition.from_json(jsonified)
|
||||
self.assertEqual(user_partition.parameters, self.TEST_PARAMETERS)
|
||||
self.assertTrue(user_partition.active)
|
||||
|
||||
def test_from_json_broken(self):
|
||||
# Missing field
|
||||
jsonified = {
|
||||
"name": self.TEST_NAME,
|
||||
"description": self.TEST_DESCRIPTION,
|
||||
"parameters": self.TEST_PARAMETERS,
|
||||
"groups": [group.to_json() for group in self.TEST_GROUPS],
|
||||
"version": UserPartition.VERSION,
|
||||
"scheme": self.TEST_SCHEME_NAME,
|
||||
}
|
||||
with self.assertRaisesRegexp(TypeError, "missing value key 'id'"):
|
||||
UserPartition.from_json(jsonified)
|
||||
|
||||
# Missing scheme
|
||||
jsonified = {
|
||||
'id': self.TEST_ID,
|
||||
"name": self.TEST_NAME,
|
||||
"description": self.TEST_DESCRIPTION,
|
||||
"parameters": self.TEST_PARAMETERS,
|
||||
"groups": [group.to_json() for group in self.TEST_GROUPS],
|
||||
"version": UserPartition.VERSION,
|
||||
}
|
||||
with self.assertRaisesRegexp(TypeError, "missing value key 'scheme'"):
|
||||
UserPartition.from_json(jsonified)
|
||||
|
||||
# Invalid scheme
|
||||
jsonified = {
|
||||
'id': self.TEST_ID,
|
||||
"name": self.TEST_NAME,
|
||||
"description": self.TEST_DESCRIPTION,
|
||||
"parameters": self.TEST_PARAMETERS,
|
||||
"groups": [group.to_json() for group in self.TEST_GROUPS],
|
||||
"version": UserPartition.VERSION,
|
||||
"scheme": "no_such_scheme",
|
||||
}
|
||||
with self.assertRaisesRegexp(UserPartitionError, "Unrecognized scheme"):
|
||||
UserPartition.from_json(jsonified)
|
||||
|
||||
# Wrong version
|
||||
jsonified = {
|
||||
'id': self.TEST_ID,
|
||||
"name": self.TEST_NAME,
|
||||
"description": self.TEST_DESCRIPTION,
|
||||
"parameters": self.TEST_PARAMETERS,
|
||||
"groups": [group.to_json() for group in self.TEST_GROUPS],
|
||||
"version": -1,
|
||||
"scheme": self.TEST_SCHEME_NAME,
|
||||
}
|
||||
with self.assertRaisesRegexp(TypeError, "has unexpected version"):
|
||||
UserPartition.from_json(jsonified)
|
||||
|
||||
# Has extra key - should not be a problem
|
||||
jsonified = {
|
||||
'id': self.TEST_ID,
|
||||
"name": self.TEST_NAME,
|
||||
"description": self.TEST_DESCRIPTION,
|
||||
"parameters": self.TEST_PARAMETERS,
|
||||
"groups": [group.to_json() for group in self.TEST_GROUPS],
|
||||
"version": UserPartition.VERSION,
|
||||
"scheme": "mock",
|
||||
"programmer": "Cale",
|
||||
}
|
||||
user_partition = UserPartition.from_json(jsonified)
|
||||
self.assertNotIn("programmer", user_partition.to_json())
|
||||
|
||||
# No error on missing parameters key (which is optional)
|
||||
jsonified = {
|
||||
'id': self.TEST_ID,
|
||||
"name": self.TEST_NAME,
|
||||
"description": self.TEST_DESCRIPTION,
|
||||
"groups": [group.to_json() for group in self.TEST_GROUPS],
|
||||
"version": UserPartition.VERSION,
|
||||
"scheme": "mock",
|
||||
}
|
||||
user_partition = UserPartition.from_json(jsonified)
|
||||
self.assertEqual(user_partition.parameters, {})
|
||||
|
||||
def test_get_group(self):
|
||||
"""
|
||||
UserPartition.get_group correctly returns the group referenced by the
|
||||
`group_id` parameter, or raises NoSuchUserPartitionGroupError when
|
||||
the lookup fails.
|
||||
"""
|
||||
self.assertEqual(
|
||||
self.user_partition.get_group(self.TEST_GROUPS[0].id),
|
||||
self.TEST_GROUPS[0]
|
||||
)
|
||||
self.assertEqual(
|
||||
self.user_partition.get_group(self.TEST_GROUPS[1].id),
|
||||
self.TEST_GROUPS[1]
|
||||
)
|
||||
with self.assertRaises(NoSuchUserPartitionGroupError):
|
||||
self.user_partition.get_group(3)
|
||||
|
||||
def test_forward_compatibility(self):
|
||||
# If the user partition version is updated in a release,
|
||||
# then the release is rolled back, courses might contain
|
||||
# version numbers greater than the currently deployed
|
||||
# version number.
|
||||
newer_version_json = {
|
||||
"id": self.TEST_ID,
|
||||
"name": self.TEST_NAME,
|
||||
"description": self.TEST_DESCRIPTION,
|
||||
"groups": [group.to_json() for group in self.TEST_GROUPS],
|
||||
"version": UserPartition.VERSION + 1,
|
||||
"scheme": "mock",
|
||||
"additional_new_field": "foo",
|
||||
}
|
||||
partition = UserPartition.from_json(newer_version_json)
|
||||
self.assertEqual(partition.id, self.TEST_ID)
|
||||
self.assertEqual(partition.name, self.TEST_NAME)
|
||||
|
||||
|
||||
class MockPartitionService(PartitionService):
|
||||
"""
|
||||
Mock PartitionService for testing.
|
||||
"""
|
||||
def __init__(self, course, **kwargs):
|
||||
super(MockPartitionService, self).__init__(**kwargs)
|
||||
self._course = course
|
||||
|
||||
def get_course(self):
|
||||
return self._course
|
||||
|
||||
|
||||
class PartitionServiceBaseClass(PartitionTestCase):
|
||||
"""
|
||||
Base test class for testing the PartitionService.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(PartitionServiceBaseClass, self).setUp()
|
||||
self.course = Mock(id=CourseLocator('org_0', 'course_0', 'run_0'))
|
||||
self.partition_service = self._create_service("ma")
|
||||
|
||||
def _create_service(self, username, cache=None):
|
||||
"""Convenience method to generate a MockPartitionService for a user."""
|
||||
# Derive a "user_id" from the username, just so we don't have to add an
|
||||
# extra param to this method. Just has to be unique per user.
|
||||
user_id = abs(hash(username))
|
||||
self.user = Mock(
|
||||
username=username, email='{}@edx.org'.format(username), is_staff=False, is_active=True, id=user_id
|
||||
)
|
||||
self.course.user_partitions = [self.user_partition]
|
||||
|
||||
return MockPartitionService(
|
||||
self.course,
|
||||
course_id=self.course.id,
|
||||
track_function=Mock(),
|
||||
cache=cache
|
||||
)
|
||||
|
||||
|
||||
class TestPartitionService(PartitionServiceBaseClass):
|
||||
"""
|
||||
Test getting a user's group out of a partition
|
||||
"""
|
||||
def test_get_user_group_id_for_partition(self):
|
||||
# assign the first group to be returned
|
||||
user_partition_id = self.user_partition.id
|
||||
groups = self.user_partition.groups
|
||||
self.user_partition.scheme.current_group = groups[0]
|
||||
|
||||
# get a group assigned to the user
|
||||
group1_id = self.partition_service.get_user_group_id_for_partition(self.user, user_partition_id)
|
||||
self.assertEqual(group1_id, groups[0].id)
|
||||
|
||||
# switch to the second group and verify that it is returned for the user
|
||||
self.user_partition.scheme.current_group = groups[1]
|
||||
group2_id = self.partition_service.get_user_group_id_for_partition(self.user, user_partition_id)
|
||||
self.assertEqual(group2_id, groups[1].id)
|
||||
|
||||
def test_caching(self):
|
||||
username = "psvc_cache_user"
|
||||
user_partition_id = self.user_partition.id
|
||||
shared_cache = {}
|
||||
|
||||
# Two MockPartitionService objects that share the same cache:
|
||||
ps_shared_cache_1 = self._create_service(username, shared_cache)
|
||||
ps_shared_cache_2 = self._create_service(username, shared_cache)
|
||||
|
||||
# A MockPartitionService with its own local cache
|
||||
ps_diff_cache = self._create_service(username, {})
|
||||
|
||||
# A MockPartitionService that never uses caching.
|
||||
ps_uncached = self._create_service(username)
|
||||
|
||||
# Set the group we expect users to be placed into
|
||||
first_group = self.user_partition.groups[0]
|
||||
self.user_partition.scheme.current_group = first_group
|
||||
|
||||
# Make sure our partition services all return the right thing, but skip
|
||||
# ps_shared_cache_2 so we can see if its cache got updated anyway.
|
||||
for part_svc in [ps_shared_cache_1, ps_diff_cache, ps_uncached]:
|
||||
self.assertEqual(
|
||||
first_group.id,
|
||||
part_svc.get_user_group_id_for_partition(self.user, user_partition_id)
|
||||
)
|
||||
|
||||
# Now select a new target group
|
||||
second_group = self.user_partition.groups[1]
|
||||
self.user_partition.scheme.current_group = second_group
|
||||
|
||||
# Both of the shared cache entries should return the old value, even
|
||||
# ps_shared_cache_2, which was never asked for the value the first time
|
||||
# Likewise, our separately cached piece should return the original answer
|
||||
for part_svc in [ps_shared_cache_1, ps_shared_cache_2, ps_diff_cache]:
|
||||
self.assertEqual(
|
||||
first_group.id,
|
||||
part_svc.get_user_group_id_for_partition(self.user, user_partition_id)
|
||||
)
|
||||
|
||||
# Our uncached service should be accurate.
|
||||
self.assertEqual(
|
||||
second_group.id,
|
||||
ps_uncached.get_user_group_id_for_partition(self.user, user_partition_id)
|
||||
)
|
||||
|
||||
# And a newly created service should see the right thing
|
||||
ps_new_cache = self._create_service(username, {})
|
||||
self.assertEqual(
|
||||
second_group.id,
|
||||
ps_new_cache.get_user_group_id_for_partition(self.user, user_partition_id)
|
||||
)
|
||||
|
||||
def test_get_group(self):
|
||||
"""
|
||||
Test that a partition group is assigned to a user.
|
||||
"""
|
||||
groups = self.user_partition.groups
|
||||
|
||||
# assign first group and verify that it is returned for the user
|
||||
self.user_partition.scheme.current_group = groups[0]
|
||||
group1 = self.partition_service.get_group(self.user, self.user_partition)
|
||||
self.assertEqual(group1, groups[0])
|
||||
|
||||
# switch to the second group and verify that it is returned for the user
|
||||
self.user_partition.scheme.current_group = groups[1]
|
||||
group2 = self.partition_service.get_group(self.user, self.user_partition)
|
||||
self.assertEqual(group2, groups[1])
|
||||
|
||||
|
||||
class TestGetCourseUserPartitions(PartitionServiceBaseClass):
|
||||
"""
|
||||
Test the helper method get_all_partitions_for_course.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(TestGetCourseUserPartitions, self).setUp()
|
||||
# django.conf.settings is not available when nosetests are run
|
||||
TestGetCourseUserPartitions._enable_enrollment_track_partition(True)
|
||||
|
||||
@staticmethod
|
||||
def _enable_enrollment_track_partition(enable):
|
||||
"""
|
||||
Enable or disable the feature flag for the enrollment track user partition.
|
||||
"""
|
||||
FEATURES['ENABLE_ENROLLMENT_TRACK_USER_PARTITION'] = enable
|
||||
|
||||
def test_enrollment_track_partition_added(self):
|
||||
"""
|
||||
Test that the dynamic enrollment track scheme is added if there is no conflict with the user partition ID.
|
||||
"""
|
||||
all_partitions = get_all_partitions_for_course(self.course)
|
||||
self.assertEqual(2, len(all_partitions))
|
||||
self.assertEqual(self.TEST_SCHEME_NAME, all_partitions[0].scheme.name)
|
||||
enrollment_track_partition = all_partitions[1]
|
||||
self.assertEqual(self.ENROLLMENT_TRACK_SCHEME_NAME, enrollment_track_partition.scheme.name)
|
||||
self.assertEqual(unicode(self.course.id), enrollment_track_partition.parameters['course_id'])
|
||||
self.assertEqual(ENROLLMENT_TRACK_PARTITION_ID, enrollment_track_partition.id)
|
||||
|
||||
def test_enrollment_track_partition_not_added_if_conflict(self):
|
||||
"""
|
||||
Test that the dynamic enrollment track scheme is NOT added if a UserPartition exists with that ID.
|
||||
"""
|
||||
self.user_partition = UserPartition(
|
||||
ENROLLMENT_TRACK_PARTITION_ID,
|
||||
self.TEST_NAME,
|
||||
self.TEST_DESCRIPTION,
|
||||
self.TEST_GROUPS,
|
||||
self.non_random_scheme,
|
||||
self.TEST_PARAMETERS,
|
||||
)
|
||||
self.course.user_partitions = [self.user_partition]
|
||||
all_partitions = get_all_partitions_for_course(self.course)
|
||||
self.assertEqual(1, len(all_partitions))
|
||||
self.assertEqual(self.TEST_SCHEME_NAME, all_partitions[0].scheme.name)
|
||||
|
||||
def test_enrollment_track_partition_not_added_if_disabled(self):
|
||||
"""
|
||||
Test that the dynamic enrollment track scheme is NOT added if the settings FEATURE flag is disabled.
|
||||
"""
|
||||
TestGetCourseUserPartitions._enable_enrollment_track_partition(False)
|
||||
all_partitions = get_all_partitions_for_course(self.course)
|
||||
self.assertEqual(1, len(all_partitions))
|
||||
self.assertEqual(self.TEST_SCHEME_NAME, all_partitions[0].scheme.name)
|
||||
|
||||
def test_filter_inactive_user_partitions(self):
|
||||
"""
|
||||
Tests supplying the `active_only` parameter.
|
||||
"""
|
||||
self.user_partition = UserPartition(
|
||||
self.TEST_ID,
|
||||
self.TEST_NAME,
|
||||
self.TEST_DESCRIPTION,
|
||||
self.TEST_GROUPS,
|
||||
self.non_random_scheme,
|
||||
self.TEST_PARAMETERS,
|
||||
active=False
|
||||
)
|
||||
self.course.user_partitions = [self.user_partition]
|
||||
|
||||
all_partitions = get_all_partitions_for_course(self.course, active_only=True)
|
||||
self.assertEqual(1, len(all_partitions))
|
||||
self.assertEqual(self.ENROLLMENT_TRACK_SCHEME_NAME, all_partitions[0].scheme.name)
|
||||
|
||||
all_partitions = get_all_partitions_for_course(self.course, active_only=False)
|
||||
self.assertEqual(2, len(all_partitions))
|
||||
self.assertEqual(self.TEST_SCHEME_NAME, all_partitions[0].scheme.name)
|
||||
self.assertEqual(self.ENROLLMENT_TRACK_SCHEME_NAME, all_partitions[1].scheme.name)
|
||||
@@ -4,22 +4,23 @@ xModule implementation of a learning sequence
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
import collections
|
||||
from datetime import datetime
|
||||
from django.utils.timezone import UTC
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
from pkg_resources import resource_string
|
||||
|
||||
from django.utils.timezone import UTC
|
||||
from lxml import etree
|
||||
from openedx.core.lib.xblock_fields.fields import Date
|
||||
from xblock.core import XBlock
|
||||
from xblock.fields import Integer, Scope, Boolean, String
|
||||
from xblock.fields import Boolean, Integer, Scope, String
|
||||
from xblock.fragment import Fragment
|
||||
|
||||
from .exceptions import NotFoundError
|
||||
from .fields import Date
|
||||
from .mako_module import MakoModuleDescriptor
|
||||
from .progress import Progress
|
||||
from .x_module import XModule, STUDENT_VIEW
|
||||
from .x_module import STUDENT_VIEW, XModule
|
||||
from .xml_module import XmlDescriptor
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -2,24 +2,22 @@
|
||||
Module for running content split tests
|
||||
"""
|
||||
|
||||
import logging
|
||||
import json
|
||||
from webob import Response
|
||||
from uuid import uuid4
|
||||
import logging
|
||||
from operator import itemgetter
|
||||
|
||||
from xmodule.progress import Progress
|
||||
from xmodule.seq_module import SequenceDescriptor
|
||||
from xmodule.studio_editable import StudioEditableModule, StudioEditableDescriptor
|
||||
from xmodule.x_module import XModule, module_attr, STUDENT_VIEW
|
||||
from xmodule.validation import StudioValidation, StudioValidationMessage
|
||||
from xmodule.modulestore.inheritance import UserPartitionList
|
||||
from uuid import uuid4
|
||||
|
||||
from lxml import etree
|
||||
|
||||
from openedx.core.lib.xblock_fields.inherited_fields import UserPartitionList
|
||||
from webob import Response
|
||||
from xblock.core import XBlock
|
||||
from xblock.fields import Scope, Integer, String, ReferenceValueDict
|
||||
from xblock.fields import Integer, ReferenceValueDict, Scope, String
|
||||
from xblock.fragment import Fragment
|
||||
from xmodule.progress import Progress
|
||||
from xmodule.seq_module import SequenceDescriptor
|
||||
from xmodule.studio_editable import StudioEditableDescriptor, StudioEditableModule
|
||||
from xmodule.validation import StudioValidation, StudioValidationMessage
|
||||
from xmodule.x_module import STUDENT_VIEW, XModule, module_attr
|
||||
|
||||
log = logging.getLogger('edx.' + __name__)
|
||||
|
||||
|
||||
@@ -25,12 +25,13 @@ from path import Path as path
|
||||
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
from xblock.field_data import DictFieldData
|
||||
from xblock.fields import ScopeIds, Scope, Reference, ReferenceList, ReferenceValueDict
|
||||
from openedx.core.lib.xblock_fields.inherited_fields import InheritanceMixin
|
||||
from xmodule.assetstore import AssetMetadata
|
||||
from xmodule.error_module import ErrorDescriptor
|
||||
from xmodule.mako_module import MakoDescriptorSystem
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.draft_and_published import DIRECT_ONLY_CATEGORIES, ModuleStoreDraftAndPublished
|
||||
from xmodule.modulestore.inheritance import InheritanceMixin, own_metadata
|
||||
from xmodule.modulestore.inheritance import own_metadata
|
||||
from xmodule.modulestore.mongo.draft import DraftModuleStore
|
||||
from xmodule.modulestore.xml import CourseLocationManager
|
||||
from xmodule.x_module import ModuleSystem, XModuleDescriptor, XModuleMixin
|
||||
|
||||
@@ -6,6 +6,7 @@ from datetime import timedelta, datetime
|
||||
from unittest import TestCase
|
||||
|
||||
from pytz import utc
|
||||
from openedx.core.lib.xblock_fields.inherited_fields import DEFAULT_START_DATE
|
||||
from xmodule.block_metadata_utils import (
|
||||
url_name_for_block,
|
||||
display_name_with_default,
|
||||
@@ -16,7 +17,6 @@ from xmodule.course_metadata_utils import (
|
||||
number_for_course_location,
|
||||
has_course_started,
|
||||
has_course_ended,
|
||||
DEFAULT_START_DATE,
|
||||
course_start_date_is_default,
|
||||
may_certify_for_course,
|
||||
)
|
||||
|
||||
@@ -4,9 +4,7 @@ import datetime
|
||||
import unittest
|
||||
|
||||
from django.utils.timezone import UTC
|
||||
|
||||
from xmodule.fields import Date, Timedelta, RelativeTime
|
||||
from xmodule.timeinfo import TimeInfo
|
||||
from openedx.core.lib.xblock_fields.fields import Date, RelativeTime, Timedelta, TimeInfo
|
||||
|
||||
|
||||
class DateTest(unittest.TestCase):
|
||||
|
||||
@@ -1,30 +1,25 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import datetime
|
||||
import ddt
|
||||
import unittest
|
||||
|
||||
import ddt
|
||||
from django.utils.timezone import UTC
|
||||
from fs.memoryfs import MemoryFS
|
||||
from lxml import etree
|
||||
from mock import Mock, patch
|
||||
|
||||
from django.utils.timezone import UTC
|
||||
|
||||
from xmodule.xml_module import is_pointer_tag
|
||||
from opaque_keys.edx.locations import Location
|
||||
from xmodule.modulestore import only_xmodules
|
||||
from xmodule.modulestore.xml import ImportSystem, XMLModuleStore, LibraryXMLModuleStore
|
||||
from xmodule.modulestore.inheritance import compute_inherited_metadata
|
||||
from xmodule.x_module import XModuleMixin
|
||||
from xmodule.fields import Date
|
||||
from xmodule.tests import DATA_DIR
|
||||
from xmodule.modulestore.inheritance import InheritanceMixin
|
||||
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
|
||||
from opaque_keys.edx.locations import Location, SlashSeparatedCourseKey
|
||||
from openedx.core.lib.xblock_fields.fields import Date
|
||||
from openedx.core.lib.xblock_fields.inherited_fields import InheritanceMixin
|
||||
from xblock.core import XBlock
|
||||
from xblock.fields import Scope, String, Integer
|
||||
from xblock.runtime import KvsFieldData, DictKeyValueStore
|
||||
|
||||
from xblock.fields import Integer, Scope, String
|
||||
from xblock.runtime import DictKeyValueStore, KvsFieldData
|
||||
from xmodule.modulestore import only_xmodules
|
||||
from xmodule.modulestore.inheritance import compute_inherited_metadata
|
||||
from xmodule.modulestore.xml import ImportSystem, LibraryXMLModuleStore, XMLModuleStore
|
||||
from xmodule.tests import DATA_DIR
|
||||
from xmodule.x_module import XModuleMixin
|
||||
from xmodule.xml_module import is_pointer_tag
|
||||
|
||||
ORG = 'test_org'
|
||||
COURSE = 'test_course'
|
||||
|
||||
@@ -2,17 +2,17 @@
|
||||
"""Test for LTI Xmodule functional logic."""
|
||||
|
||||
import datetime
|
||||
from django.utils.timezone import UTC
|
||||
from mock import Mock, patch, PropertyMock
|
||||
import textwrap
|
||||
from lxml import etree
|
||||
from webob.request import Request
|
||||
from copy import copy
|
||||
import urllib
|
||||
from copy import copy
|
||||
|
||||
from xmodule.fields import Timedelta
|
||||
from xmodule.lti_module import LTIDescriptor
|
||||
from django.utils.timezone import UTC
|
||||
from lxml import etree
|
||||
from mock import Mock, PropertyMock, patch
|
||||
from openedx.core.lib.xblock_fields.fields import Timedelta
|
||||
from webob.request import Request
|
||||
from xmodule.lti_2_util import LTIError
|
||||
from xmodule.lti_module import LTIDescriptor
|
||||
|
||||
from . import LogicTest
|
||||
|
||||
|
||||
@@ -3,17 +3,20 @@ Tests for the Split Testing Module
|
||||
"""
|
||||
import ddt
|
||||
import lxml
|
||||
from mock import Mock, patch
|
||||
from fs.memoryfs import MemoryFS
|
||||
|
||||
from xmodule.partitions.tests.test_partitions import MockPartitionService, PartitionTestCase, MockUserPartitionScheme
|
||||
from mock import Mock, patch
|
||||
from openedx.core.lib.partitions.partitions import MINIMUM_STATIC_PARTITION_ID, Group, UserPartition
|
||||
from openedx.core.lib.partitions.tests.test_partitions import (
|
||||
MockPartitionService,
|
||||
MockUserPartitionScheme,
|
||||
PartitionTestCase
|
||||
)
|
||||
from xmodule.split_test_module import SplitTestDescriptor, SplitTestFields, get_split_user_partitions
|
||||
from xmodule.tests import get_test_system
|
||||
from xmodule.tests.xml import factories as xml
|
||||
from xmodule.tests.xml import XModuleXmlImportTest
|
||||
from xmodule.tests import get_test_system
|
||||
from xmodule.x_module import AUTHOR_VIEW, STUDENT_VIEW
|
||||
from xmodule.validation import StudioValidationMessage
|
||||
from xmodule.split_test_module import SplitTestDescriptor, SplitTestFields, get_split_user_partitions
|
||||
from xmodule.partitions.partitions import Group, UserPartition, MINIMUM_STATIC_PARTITION_ID
|
||||
from xmodule.x_module import AUTHOR_VIEW, STUDENT_VIEW
|
||||
|
||||
|
||||
class SplitTestModuleFactory(xml.XmlImportFactory):
|
||||
|
||||
@@ -4,24 +4,29 @@
|
||||
import unittest
|
||||
|
||||
from mock import Mock
|
||||
from nose.tools import assert_equals, assert_not_equals, assert_true, assert_false, assert_in, assert_not_in # pylint: disable=no-name-in-module
|
||||
from nose.tools import ( # pylint: disable=no-name-in-module
|
||||
assert_equals,
|
||||
assert_false,
|
||||
assert_in,
|
||||
assert_not_equals,
|
||||
assert_not_in,
|
||||
assert_true
|
||||
)
|
||||
from opaque_keys.edx.locator import BlockUsageLocator, CourseLocator
|
||||
|
||||
from openedx.core.lib.xblock_fields.fields import Date, RelativeTime, Timedelta
|
||||
from openedx.core.lib.xblock_fields.inherited_fields import InheritanceMixin
|
||||
from xblock.field_data import DictFieldData
|
||||
from xblock.fields import Scope, String, Dict, Boolean, Integer, Float, Any, List
|
||||
from xblock.runtime import KvsFieldData, DictKeyValueStore
|
||||
|
||||
from xmodule.fields import Date, Timedelta, RelativeTime
|
||||
from xmodule.modulestore.inheritance import InheritanceKeyValueStore, InheritanceMixin, InheritingFieldData
|
||||
from xmodule.modulestore.split_mongo.split_mongo_kvs import SplitMongoKVS
|
||||
from xmodule.xml_module import XmlDescriptor, serialize_field, deserialize_field
|
||||
from xblock.fields import Any, Boolean, Dict, Float, Integer, List, Scope, String
|
||||
from xblock.runtime import DictKeyValueStore, KvsFieldData
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
from xmodule.modulestore.inheritance import InheritanceKeyValueStore, InheritingFieldData
|
||||
from xmodule.modulestore.split_mongo.split_mongo_kvs import SplitMongoKVS
|
||||
from xmodule.seq_module import SequenceDescriptor
|
||||
from xmodule.x_module import XModuleMixin
|
||||
|
||||
from xmodule.tests import get_test_descriptor_system
|
||||
from xmodule.tests.xml import XModuleXmlImportTest
|
||||
from xmodule.tests.xml.factories import CourseFactory, SequenceFactory, ProblemFactory
|
||||
from xmodule.tests.xml.factories import CourseFactory, ProblemFactory, SequenceFactory
|
||||
from xmodule.x_module import XModuleMixin
|
||||
from xmodule.xml_module import XmlDescriptor, deserialize_field, serialize_field
|
||||
|
||||
|
||||
class CrazyJsonString(String):
|
||||
|
||||
@@ -4,14 +4,13 @@ Factories for generating edXML for testing XModule import
|
||||
|
||||
import inspect
|
||||
|
||||
from factory import Factory, Sequence, lazy_attribute, post_generation
|
||||
from fs.memoryfs import MemoryFS
|
||||
from factory import Factory, lazy_attribute, post_generation, Sequence
|
||||
from lxml import etree
|
||||
|
||||
from openedx.core.lib.xblock_fields.inherited_fields import InheritanceMixin
|
||||
from xblock.mixins import HierarchyMixin
|
||||
from xmodule.modulestore.inheritance import InheritanceMixin
|
||||
from xmodule.x_module import XModuleMixin
|
||||
from xmodule.modulestore import only_xmodules
|
||||
from xmodule.x_module import XModuleMixin
|
||||
|
||||
|
||||
class XmlImportData(object):
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
import logging
|
||||
from xmodule.fields import Timedelta
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TimeInfo(object):
|
||||
"""
|
||||
This is a simple object that calculates and stores datetime information for an XModule
|
||||
based on the due date and the grace period string
|
||||
|
||||
So far it parses out three different pieces of time information:
|
||||
self.display_due_date - the 'official' due date that gets displayed to students
|
||||
self.grace_period - the length of the grace period
|
||||
self.close_date - the real due date
|
||||
|
||||
"""
|
||||
_delta_standin = Timedelta()
|
||||
|
||||
def __init__(self, due_date, grace_period_string_or_timedelta):
|
||||
if due_date is not None:
|
||||
self.display_due_date = due_date
|
||||
|
||||
else:
|
||||
self.display_due_date = None
|
||||
|
||||
if grace_period_string_or_timedelta is not None and self.display_due_date:
|
||||
if isinstance(grace_period_string_or_timedelta, basestring):
|
||||
try:
|
||||
self.grace_period = TimeInfo._delta_standin.from_json(grace_period_string_or_timedelta)
|
||||
except:
|
||||
log.error("Error parsing the grace period {0}".format(grace_period_string_or_timedelta))
|
||||
raise
|
||||
else:
|
||||
self.grace_period = grace_period_string_or_timedelta
|
||||
self.close_date = self.display_due_date + self.grace_period
|
||||
else:
|
||||
self.grace_period = None
|
||||
self.close_date = self.display_due_date
|
||||
@@ -8,26 +8,24 @@ StudioViewHandlers are handlers for video descriptor instance.
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from webob import Response
|
||||
|
||||
from xblock.core import XBlock
|
||||
|
||||
from xmodule.exceptions import NotFoundError
|
||||
from xmodule.fields import RelativeTime
|
||||
from opaque_keys.edx.locator import CourseLocator
|
||||
from openedx.core.lib.xblock_fields.fields import RelativeTime
|
||||
from webob import Response
|
||||
from xblock.core import XBlock
|
||||
from xmodule.exceptions import NotFoundError
|
||||
|
||||
from .transcripts_utils import (
|
||||
get_or_create_sjson,
|
||||
Transcript,
|
||||
TranscriptException,
|
||||
TranscriptsGenerationException,
|
||||
generate_sjson_for_all_speeds,
|
||||
youtube_speed_dict,
|
||||
Transcript,
|
||||
get_or_create_sjson,
|
||||
save_to_store,
|
||||
subs_filename
|
||||
subs_filename,
|
||||
youtube_speed_dict
|
||||
)
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@ XFields for video module.
|
||||
"""
|
||||
import datetime
|
||||
|
||||
from xblock.fields import Scope, String, Float, Boolean, List, Dict, DateTime
|
||||
from xmodule.fields import RelativeTime
|
||||
from openedx.core.lib.xblock_fields.fields import RelativeTime
|
||||
from xblock.fields import Boolean, DateTime, Dict, Float, List, Scope, String
|
||||
|
||||
# Make '_' a no-op so we can scrape strings. Using lambda instead of
|
||||
# `django.utils.translation.ugettext_noop` because Django cannot be imported in this file
|
||||
|
||||
@@ -2,40 +2,41 @@ import logging
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import yaml
|
||||
|
||||
from contracts import contract, new_contract
|
||||
from functools import partial
|
||||
from lxml import etree
|
||||
from collections import namedtuple
|
||||
from pkg_resources import (
|
||||
resource_exists,
|
||||
resource_listdir,
|
||||
resource_string,
|
||||
resource_isdir,
|
||||
)
|
||||
from functools import partial
|
||||
|
||||
from pkg_resources import resource_exists, resource_isdir, resource_listdir, resource_string
|
||||
|
||||
import dogstats_wrapper as dog_stats_api
|
||||
import yaml
|
||||
from contracts import contract, new_contract
|
||||
from lazy import lazy
|
||||
from lxml import etree
|
||||
from opaque_keys.edx.asides import AsideDefinitionKeyV2, AsideUsageKeyV2
|
||||
from opaque_keys.edx.keys import UsageKey
|
||||
from openedx.core.lib.xblock_fields.fields import RelativeTime
|
||||
from webob import Response
|
||||
from webob.multidict import MultiDict
|
||||
from lazy import lazy
|
||||
|
||||
from xblock.core import XBlock, XBlockAside
|
||||
from xblock.fields import (
|
||||
Scope, Integer, Float, List,
|
||||
String, Dict, ScopeIds, Reference, ReferenceList,
|
||||
ReferenceValueDict, UserScope
|
||||
Dict,
|
||||
Float,
|
||||
Integer,
|
||||
List,
|
||||
Reference,
|
||||
ReferenceList,
|
||||
ReferenceValueDict,
|
||||
Scope,
|
||||
ScopeIds,
|
||||
String,
|
||||
UserScope
|
||||
)
|
||||
|
||||
from xblock.fragment import Fragment
|
||||
from xblock.runtime import Runtime, IdReader, IdGenerator
|
||||
from xblock.runtime import IdGenerator, IdReader, Runtime
|
||||
from xmodule import block_metadata_utils
|
||||
from xmodule.fields import RelativeTime
|
||||
from xmodule.errortracker import exc_info_to_str
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
|
||||
from opaque_keys.edx.keys import UsageKey
|
||||
from opaque_keys.edx.asides import AsideUsageKeyV2, AsideDefinitionKeyV2
|
||||
from xmodule.exceptions import UndefinedContext
|
||||
import dogstats_wrapper as dog_stats_api
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user