chore: upstream ProblemBlock exceptions and shared utilities to XBlock (#37806)
* fix: add support for xblock 5.3.0
This commit is contained in:
@@ -8,12 +8,12 @@ import logging
|
||||
|
||||
from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from xblock.fields import Date
|
||||
|
||||
from cms.djangoapps.contentstore.management.commands.utils import user_from_str
|
||||
from cms.djangoapps.contentstore.views.course import create_new_course_in_store
|
||||
from openedx.core.djangoapps.credit.models import CreditProvider
|
||||
from xmodule.course_block import CourseFields # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from xmodule.fields import Date # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from xmodule.modulestore.exceptions import DuplicateCourseError # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from xmodule.tabs import CourseTabList # lint-amnesty, pylint: disable=wrong-import-order
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ from rest_framework import serializers
|
||||
from rest_framework.fields import Field as SerializerField
|
||||
from xblock.fields import (
|
||||
Boolean,
|
||||
Date,
|
||||
DateTime,
|
||||
Dict,
|
||||
Field as XBlockField,
|
||||
@@ -15,7 +16,6 @@ from xblock.fields import (
|
||||
String,
|
||||
)
|
||||
from xmodule.course_block import CourseFields, EmailString
|
||||
from xmodule.fields import Date
|
||||
|
||||
from cms.djangoapps.models.settings.course_metadata import CourseMetadata
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ from edx_toggles.toggles.testutils import override_waffle_flag
|
||||
from milestones.models import MilestoneRelationshipType
|
||||
from milestones.tests.utils import MilestonesTestCaseMixin
|
||||
from pytz import UTC
|
||||
from xblock.fields import Date
|
||||
|
||||
from cms.djangoapps.contentstore import toggles
|
||||
from cms.djangoapps.contentstore.utils import reverse_course_url, reverse_usage_url
|
||||
@@ -49,7 +50,6 @@ from openedx.core.djangoapps.discussions.config.waffle import (
|
||||
)
|
||||
from openedx.core.djangoapps.models.course_details import CourseDetails
|
||||
from openedx.core.lib.teams_config import TeamsConfig
|
||||
from xmodule.fields import Date # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from xmodule.modulestore.tests.factories import CourseFactory # lint-amnesty, pylint: disable=wrong-import-order
|
||||
|
||||
|
||||
@@ -15,12 +15,12 @@ from opaque_keys.edx.locator import LibraryContainerLocator
|
||||
from rest_framework.request import Request
|
||||
from web_fragments.fragment import Fragment
|
||||
from xblock.django.request import django_to_webob_request, webob_to_django_response
|
||||
from xblock.exceptions import NoSuchHandlerError
|
||||
from xblock.exceptions import NoSuchHandlerError, NotFoundError, ProcessingError
|
||||
from xblock.runtime import KvsFieldData
|
||||
|
||||
from openedx.core.djangoapps.video_config.services import VideoConfigService
|
||||
from xmodule.contentstore.django import contentstore
|
||||
from xmodule.exceptions import NotFoundError, ProcessingError
|
||||
from xmodule.exceptions import NotFoundError as XModuleNotFoundError
|
||||
from xmodule.modulestore.django import XBlockI18nService, modulestore
|
||||
from xmodule.partitions.partitions_service import PartitionService
|
||||
from xmodule.services import SettingsService, TeamsConfigurationService
|
||||
@@ -81,7 +81,7 @@ def preview_handler(request, usage_key_string, handler, suffix=''):
|
||||
log.exception("XBlock %s attempted to access missing handler %r", instance, handler)
|
||||
raise Http404 # lint-amnesty, pylint: disable=raise-missing-from
|
||||
|
||||
except NotFoundError:
|
||||
except (XModuleNotFoundError, NotFoundError):
|
||||
log.exception("Module indicating to user that request doesn't exist")
|
||||
raise Http404 # lint-amnesty, pylint: disable=raise-missing-from
|
||||
|
||||
|
||||
@@ -8,9 +8,9 @@ import json
|
||||
from json.encoder import JSONEncoder
|
||||
|
||||
from opaque_keys.edx.locations import Location
|
||||
from xblock.fields import Date
|
||||
|
||||
from openedx.core.djangoapps.models.course_details import CourseDetails
|
||||
from xmodule.fields import Date # lint-amnesty, pylint: disable=wrong-import-order
|
||||
|
||||
from .course_grading import CourseGradingModel
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ from __future__ import annotations
|
||||
from django.contrib.auth import get_user_model
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from openedx.core.lib.grade_utils import round_away_from_zero
|
||||
from xmodule.graders import ShowCorrectness
|
||||
from xblock.scorable import ShowCorrectness
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from lms.djangoapps.courseware.courses import get_course_blocks_completion_summary
|
||||
|
||||
@@ -5,12 +5,12 @@ Tests for the Python APIs exposed by the Progress API of the Course Home API app
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.test import TestCase
|
||||
from xblock.scorable import ShowCorrectness
|
||||
|
||||
from lms.djangoapps.course_home_api.progress.api import (
|
||||
calculate_progress_for_learner_in_course,
|
||||
aggregate_assignment_type_grade_summary,
|
||||
)
|
||||
from xmodule.graders import ShowCorrectness
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from types import SimpleNamespace
|
||||
|
||||
|
||||
@@ -11,9 +11,9 @@ from opaque_keys.edx.keys import CourseKey
|
||||
from rest_framework.generics import RetrieveAPIView
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from xblock.scorable import ShowCorrectness
|
||||
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.graders import ShowCorrectness
|
||||
from common.djangoapps.student.models import CourseEnrollment
|
||||
from lms.djangoapps.course_home_api.progress.serializers import ProgressTabSerializer
|
||||
from lms.djangoapps.course_home_api.progress.api import aggregate_assignment_type_grade_summary
|
||||
|
||||
@@ -37,7 +37,7 @@ from rest_framework.exceptions import APIException
|
||||
from typing import Callable, TYPE_CHECKING
|
||||
from web_fragments.fragment import Fragment
|
||||
from xblock.django.request import django_to_webob_request, webob_to_django_response
|
||||
from xblock.exceptions import NoSuchHandlerError, NoSuchViewError
|
||||
from xblock.exceptions import NoSuchHandlerError, NoSuchViewError, NotFoundError, ProcessingError
|
||||
from xblock.reference.plugins import FSService
|
||||
from xblock.runtime import KvsFieldData
|
||||
|
||||
@@ -45,7 +45,7 @@ from lms.djangoapps.teams.services import TeamsService
|
||||
from openedx.core.djangoapps.video_config.services import VideoConfigService
|
||||
from openedx.core.lib.xblock_services.call_to_action import CallToActionService
|
||||
from xmodule.contentstore.django import contentstore
|
||||
from xmodule.exceptions import NotFoundError, ProcessingError
|
||||
from xmodule.exceptions import NotFoundError as XModuleNotFoundError
|
||||
from xmodule.library_tools import LegacyLibraryToolsService
|
||||
from xmodule.modulestore.django import XBlockI18nService, modulestore
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
@@ -966,7 +966,7 @@ def _invoke_xblock_handler(request, course_id, usage_id, handler, suffix, course
|
||||
raise Http404 # lint-amnesty, pylint: disable=raise-missing-from
|
||||
|
||||
# If we can't find the block, respond with a 404
|
||||
except NotFoundError:
|
||||
except (XModuleNotFoundError, NotFoundError):
|
||||
log.exception("Module indicating to user that request doesn't exist")
|
||||
raise Http404 # lint-amnesty, pylint: disable=raise-missing-from
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ from django.db import migrations
|
||||
|
||||
|
||||
def move_overrides_to_edx_when(apps, schema_editor):
|
||||
from xmodule.fields import Date
|
||||
from xblock.fields import Date
|
||||
from edx_when import api
|
||||
date_field = Date()
|
||||
StudentFieldOverride = apps.get_model('courseware', 'StudentFieldOverride')
|
||||
|
||||
@@ -33,9 +33,9 @@ from rest_framework.test import APIClient
|
||||
from web_fragments.fragment import Fragment
|
||||
from xblock.core import XBlock
|
||||
from xblock.fields import Scope, String
|
||||
from xblock.scorable import ShowCorrectness
|
||||
from xmodule.capa.tests.response_xml_factory import MultipleChoiceResponseXMLFactory
|
||||
from xmodule.data import CertificatesDisplayBehaviors
|
||||
from xmodule.graders import ShowCorrectness
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.tests.django_utils import CourseUserType, ModuleStoreTestCase, SharedModuleStoreTestCase
|
||||
|
||||
@@ -8,11 +8,12 @@ from collections import OrderedDict
|
||||
from datetime import datetime, timezone
|
||||
from logging import getLogger
|
||||
from lazy import lazy
|
||||
from xblock.scorable import ShowCorrectness
|
||||
|
||||
from lms.djangoapps.grades.models import BlockRecord, PersistentSubsectionGrade
|
||||
from lms.djangoapps.grades.scores import compute_percent, get_score, possibly_scored
|
||||
from xmodule import block_metadata_utils, graders # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from xmodule.graders import AggregatedScore, ShowCorrectness # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from xmodule.graders import AggregatedScore # lint-amnesty, pylint: disable=wrong-import-order
|
||||
|
||||
log = getLogger(__name__)
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ from opaque_keys.edx.keys import CourseKey
|
||||
from opaque_keys.edx.locator import UsageKey
|
||||
from pytz import UTC
|
||||
from testfixtures import LogCapture
|
||||
from xblock.fields import Date
|
||||
|
||||
from common.djangoapps.course_modes.models import CourseMode
|
||||
from common.djangoapps.course_modes.tests.factories import CourseModeFactory
|
||||
@@ -102,7 +103,6 @@ from openedx.core.djangoapps.user_api.preferences.api import delete_user_prefere
|
||||
from openedx.core.lib.teams_config import TeamsConfig
|
||||
from openedx.core.lib.xblock_utils import grade_histogram
|
||||
from openedx.features.course_experience import RELATIVE_DATES_FLAG
|
||||
from xmodule.fields import Date
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.tests.django_utils import (
|
||||
TEST_DATA_SPLIT_MODULESTORE,
|
||||
|
||||
@@ -17,7 +17,7 @@ from edx_when.api import get_dates_for_course, set_dates_for_course
|
||||
from edx_when.field_data import DateLookupFieldData
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from pytz import UTC
|
||||
from xmodule.fields import Date
|
||||
from xblock.fields import Date
|
||||
from xmodule.modulestore.tests.django_utils import (
|
||||
TEST_DATA_SPLIT_MODULESTORE, ModuleStoreTestCase, SharedModuleStoreTestCase,
|
||||
)
|
||||
|
||||
@@ -10,13 +10,13 @@ from django.urls import reverse
|
||||
from urllib.parse import quote_plus
|
||||
from django.utils.translation import gettext as _
|
||||
from pytz import UTC
|
||||
from xblock.scorable import ShowCorrectness
|
||||
|
||||
from common.djangoapps.course_modes.models import CourseMode
|
||||
from lms.djangoapps.certificates.data import CertificateStatuses
|
||||
from lms.djangoapps.grades.api import constants as grades_constants
|
||||
from openedx.core.djangolib.markup import HTML, Text
|
||||
from openedx.features.enterprise_support.utils import get_enterprise_learner_generic_name
|
||||
from xmodule.graders import ShowCorrectness
|
||||
%>
|
||||
|
||||
<%
|
||||
|
||||
@@ -7,11 +7,11 @@ import logging
|
||||
import re
|
||||
|
||||
from django.conf import settings
|
||||
from xblock.fields import Date
|
||||
|
||||
from openedx.core.djangolib.markup import HTML
|
||||
from openedx.core.lib.courses import course_image_url
|
||||
from xmodule.data import CertificatesDisplayBehaviors # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from xmodule.fields import Date # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError # lint-amnesty, pylint: disable=wrong-import-order
|
||||
|
||||
|
||||
@@ -25,8 +25,23 @@ from lxml import etree
|
||||
from pytz import utc
|
||||
from web_fragments.fragment import Fragment
|
||||
from xblock.core import XBlock
|
||||
from xblock.fields import Boolean, Dict, Float, Integer, List, Scope, String, XMLString
|
||||
from xblock.scorable import ScorableXBlockMixin, Score
|
||||
from xblock.exceptions import NotFoundError, ProcessingError
|
||||
from xblock.fields import (
|
||||
Boolean,
|
||||
Date,
|
||||
Dict,
|
||||
Float,
|
||||
Integer,
|
||||
List,
|
||||
ListScoreField,
|
||||
Scope,
|
||||
ScoreField,
|
||||
String,
|
||||
Timedelta,
|
||||
XMLString
|
||||
)
|
||||
from xblock.progress import Progress
|
||||
from xblock.scorable import ScorableXBlockMixin, Score, ShowCorrectness
|
||||
from xblocks_contrib.problem import ProblemBlock as _ExtractedProblemBlock
|
||||
|
||||
from common.djangoapps.xblock_django.constants import (
|
||||
@@ -42,8 +57,6 @@ from xmodule.capa.responsetypes import LoncapaProblemError, ResponseError, Stude
|
||||
from xmodule.capa.util import convert_files_to_filenames, get_inner_html_from_xpath
|
||||
from xmodule.contentstore.django import contentstore
|
||||
from xmodule.editing_block import EditingMixin
|
||||
from xmodule.exceptions import NotFoundError, ProcessingError
|
||||
from xmodule.graders import ShowCorrectness
|
||||
from xmodule.raw_block import RawMixin
|
||||
from xmodule.util.builtin_assets import add_css_to_fragment, add_webpack_js_to_fragment
|
||||
from xmodule.util.sandboxing import SandboxService
|
||||
@@ -51,8 +64,6 @@ from xmodule.x_module import ResourceTemplates, XModuleMixin, XModuleToXBlockMix
|
||||
from xmodule.xml_block import XmlMixin
|
||||
|
||||
from .capa.xqueue_interface import XQueueService
|
||||
from .fields import Date, ListScoreField, ScoreField, Timedelta
|
||||
from .progress import Progress
|
||||
|
||||
log = logging.getLogger("edx.courseware")
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ from lazy import lazy
|
||||
from lxml import etree
|
||||
from path import Path as path
|
||||
from pytz import utc
|
||||
from xblock.fields import Boolean, Dict, Float, Integer, List, Scope, String
|
||||
from xblock.fields import Boolean, Date, Dict, Float, Integer, List, Scope, String
|
||||
from openedx.core.djangoapps.video_pipeline.models import VideoUploadsEnabledByDefault
|
||||
from openedx.core.djangoapps.video_config.sharing import (
|
||||
COURSE_VIDEO_SHARING_ALL_VIDEOS,
|
||||
@@ -32,7 +32,6 @@ from xmodule.graders import grader_from_conf
|
||||
from xmodule.seq_block import SequenceBlock
|
||||
from xmodule.tabs import CourseTabList, InvalidTabsException
|
||||
|
||||
from .fields import Date
|
||||
from .modulestore.exceptions import InvalidProctoringProvider
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -7,14 +7,6 @@ class NotFoundError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ProcessingError(Exception):
|
||||
'''
|
||||
An error occurred while processing a request to the XModule.
|
||||
For example: if an exception occurs while checking a capa problem.
|
||||
'''
|
||||
pass # lint-amnesty, pylint: disable=unnecessary-pass
|
||||
|
||||
|
||||
class InvalidVersionError(Exception):
|
||||
"""
|
||||
Tried to save an item with a location that a store cannot support (e.g., draft version
|
||||
|
||||
@@ -1,324 +0,0 @@
|
||||
# lint-amnesty, pylint: disable=missing-module-docstring
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
import re
|
||||
import time
|
||||
|
||||
import dateutil.parser
|
||||
from pytz import UTC
|
||||
from xblock.fields import JSONField, List
|
||||
from xblock.scorable import Score
|
||||
|
||||
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(f"Field {self.name} is missing month or day")
|
||||
return None
|
||||
if result.tzinfo is None:
|
||||
result = result.replace(tzinfo=UTC)
|
||||
return result
|
||||
|
||||
def from_json(self, field): # lint-amnesty, pylint: disable=arguments-differ
|
||||
"""
|
||||
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 == "":
|
||||
return None
|
||||
elif isinstance(field, str):
|
||||
return self._parse_date_wo_default_month_day(field)
|
||||
elif isinstance(field, int) or isinstance( # lint-amnesty, pylint: disable=consider-merging-isinstance
|
||||
field, 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 {} has bad value '{}'".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(f"Cannot convert {value!r} to json")
|
||||
|
||||
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)?)?$" # lint-amnesty, pylint: disable=line-too-long
|
||||
)
|
||||
|
||||
|
||||
class Timedelta(JSONField): # lint-amnesty, pylint: disable=missing-class-docstring
|
||||
# Timedeltas are immutable, see http://docs.python.org/2/library/datetime.html#available-types
|
||||
MUTABLE = False
|
||||
|
||||
def from_json(self, time_str): # lint-amnesty, pylint: disable=arguments-differ
|
||||
"""
|
||||
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.items():
|
||||
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 block 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 block 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( # lint-amnesty, pylint: disable=raise-missing-from
|
||||
"Incorrect RelativeTime value {!r} was set in XML or serialized. "
|
||||
"Original parse message is {}".format(value, str(e))
|
||||
)
|
||||
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, str):
|
||||
return self.isotime_to_timedelta(value)
|
||||
|
||||
msg = f"RelativeTime Field {self.name} has bad value '{value!r}'"
|
||||
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(f"RelativeTime: cannot convert {value!r} to json")
|
||||
|
||||
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)
|
||||
|
||||
|
||||
class ScoreField(JSONField):
|
||||
"""
|
||||
Field for blocks that need to store a Score. XBlocks that implement
|
||||
the ScorableXBlockMixin may need to store their score separately
|
||||
from their problem state, specifically for use in staff override
|
||||
of problem scores.
|
||||
"""
|
||||
|
||||
MUTABLE = False
|
||||
|
||||
def from_json(self, value):
|
||||
if value is None:
|
||||
return value
|
||||
if isinstance(value, Score):
|
||||
return value
|
||||
|
||||
if set(value) != {"raw_earned", "raw_possible"}:
|
||||
raise TypeError("Scores must contain only a raw earned and raw possible value. Got {}".format(set(value)))
|
||||
|
||||
raw_earned = value["raw_earned"]
|
||||
raw_possible = value["raw_possible"]
|
||||
|
||||
if raw_possible < 0:
|
||||
raise ValueError(
|
||||
"Error deserializing field of type {}: Expected a positive number for raw_possible, got {}.".format(
|
||||
self.display_name,
|
||||
raw_possible,
|
||||
)
|
||||
)
|
||||
|
||||
if not (0 <= raw_earned <= raw_possible): # lint-amnesty, pylint: disable=superfluous-parens
|
||||
raise ValueError(
|
||||
"Error deserializing field of type {}: Expected raw_earned between 0 and {}, got {}.".format(
|
||||
self.display_name, raw_possible, raw_earned
|
||||
)
|
||||
)
|
||||
|
||||
return Score(raw_earned, raw_possible)
|
||||
|
||||
enforce_type = from_json
|
||||
|
||||
|
||||
class ListScoreField(ScoreField, List):
|
||||
"""
|
||||
Field for blocks that need to store a list of Scores.
|
||||
"""
|
||||
|
||||
MUTABLE = True
|
||||
_default = []
|
||||
|
||||
def from_json(self, value):
|
||||
if value is None:
|
||||
return value
|
||||
if isinstance(value, list):
|
||||
scores = []
|
||||
for score_json in value:
|
||||
score = super().from_json(score_json)
|
||||
scores.append(score)
|
||||
return scores
|
||||
|
||||
raise TypeError("Value must be a list of Scores. Got {}".format(type(value)))
|
||||
|
||||
enforce_type = from_json
|
||||
@@ -9,9 +9,7 @@ import logging
|
||||
import random
|
||||
import sys
|
||||
from collections import OrderedDict
|
||||
from datetime import datetime
|
||||
|
||||
from pytz import UTC
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from xmodule.util.misc import get_short_labeler
|
||||
@@ -471,37 +469,3 @@ def _min_or_none(itr):
|
||||
return min(itr)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
class ShowCorrectness:
|
||||
"""
|
||||
Helper class for determining whether correctness is currently hidden for a block.
|
||||
|
||||
When correctness is hidden, this limits the user's access to the correct/incorrect flags, messages, problem scores,
|
||||
and aggregate subsection and course grades.
|
||||
"""
|
||||
|
||||
# Constants used to indicate when to show correctness
|
||||
ALWAYS = "always"
|
||||
PAST_DUE = "past_due"
|
||||
NEVER = "never"
|
||||
NEVER_BUT_INCLUDE_GRADE = "never_but_include_grade"
|
||||
|
||||
@classmethod
|
||||
def correctness_available(cls, show_correctness='', due_date=None, has_staff_access=False):
|
||||
"""
|
||||
Returns whether correctness is available now, for the given attributes.
|
||||
"""
|
||||
if show_correctness in (cls.NEVER, cls.NEVER_BUT_INCLUDE_GRADE):
|
||||
return False
|
||||
elif has_staff_access:
|
||||
# This is after the 'never' check because course staff can see correctness
|
||||
# unless the sequence/problem explicitly prevents it
|
||||
return True
|
||||
elif show_correctness == cls.PAST_DUE:
|
||||
# Is it now past the due date?
|
||||
return (due_date is None or
|
||||
due_date < datetime.now(UTC))
|
||||
|
||||
# else: show_correctness == cls.ALWAYS
|
||||
return True
|
||||
|
||||
@@ -6,11 +6,10 @@ Support for inheritance of fields down an XBlock hierarchy.
|
||||
import warnings
|
||||
from django.utils import timezone
|
||||
from xblock.core import XBlockMixin
|
||||
from xblock.fields import Boolean, Dict, Float, Integer, List, Scope, String
|
||||
from xblock.fields import Boolean, Date, Dict, Float, Integer, List, Scope, String, Timedelta
|
||||
from xblock.runtime import KeyValueStore, KvsFieldData
|
||||
|
||||
from xmodule.error_block import ErrorBlock
|
||||
from xmodule.fields import Date, Timedelta
|
||||
from xmodule.partitions.partitions import UserPartition
|
||||
|
||||
from ..course_metadata_utils import DEFAULT_START_DATE
|
||||
|
||||
@@ -16,13 +16,12 @@ import ddt
|
||||
from ccx_keys.locator import CCXBlockUsageLocator
|
||||
from django.core.cache import InvalidCacheBackendError, caches
|
||||
from opaque_keys.edx.locator import BlockUsageLocator, CourseKey, CourseLocator, LocalId
|
||||
from xblock.fields import Reference, ReferenceList, ReferenceValueDict
|
||||
from xblock.fields import Date, Reference, ReferenceList, ReferenceValueDict, Timedelta
|
||||
|
||||
from openedx.core.djangolib.testing.utils import CacheIsolationMixin
|
||||
from openedx.core.lib import tempdir
|
||||
from openedx.core.lib.tests import attr
|
||||
from xmodule.course_block import CourseBlock
|
||||
from xmodule.fields import Date, Timedelta
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.edit_info import EditInfoMixin
|
||||
from xmodule.modulestore.exceptions import (
|
||||
|
||||
@@ -1,133 +0,0 @@
|
||||
"""
|
||||
Progress class for blocks. Represents where a student is in a block.
|
||||
|
||||
For most subclassing needs, you should only need to reimplement
|
||||
frac() and __str__().
|
||||
"""
|
||||
|
||||
import numbers
|
||||
|
||||
|
||||
class Progress:
|
||||
"""Represents a progress of a/b (a out of b done)
|
||||
|
||||
a and b must be numeric, but not necessarily integer, with
|
||||
0 <= a <= b and b > 0.
|
||||
|
||||
Progress can only represent Progress for blocks where that makes sense. Other
|
||||
blocks (e.g. html) should return None from get_progress().
|
||||
|
||||
TODO: add tag for module type? Would allow for smarter merging.
|
||||
"""
|
||||
|
||||
def __init__(self, a, b):
|
||||
"""Construct a Progress object. a and b must be numbers, and must have
|
||||
0 <= a <= b and b > 0
|
||||
"""
|
||||
|
||||
# Want to do all checking at construction time, so explicitly check types
|
||||
if not (isinstance(a, numbers.Number) and isinstance(b, numbers.Number)):
|
||||
raise TypeError(f"a and b must be numbers. Passed {a}/{b}")
|
||||
|
||||
if a > b: # lint-amnesty, pylint: disable=consider-using-min-builtin
|
||||
a = b
|
||||
|
||||
if a < 0: # lint-amnesty, pylint: disable=consider-using-max-builtin
|
||||
a = 0
|
||||
|
||||
if b <= 0:
|
||||
raise ValueError(f"fraction a/b = {a}/{b} must have b > 0")
|
||||
|
||||
self._a = a
|
||||
self._b = b
|
||||
|
||||
def frac(self):
|
||||
"""Return tuple (a,b) representing progress of a/b"""
|
||||
return (self._a, self._b)
|
||||
|
||||
def percent(self):
|
||||
"""Returns a percentage progress as a float between 0 and 100.
|
||||
|
||||
subclassing note: implemented in terms of frac(), assumes sanity
|
||||
checking is done at construction time.
|
||||
"""
|
||||
(a, b) = self.frac()
|
||||
return 100.0 * a / b
|
||||
|
||||
def started(self):
|
||||
"""Returns True if fractional progress is greater than 0.
|
||||
|
||||
subclassing note: implemented in terms of frac(), assumes sanity
|
||||
checking is done at construction time.
|
||||
"""
|
||||
return self.frac()[0] > 0
|
||||
|
||||
def inprogress(self):
|
||||
"""Returns True if fractional progress is strictly between 0 and 1.
|
||||
|
||||
subclassing note: implemented in terms of frac(), assumes sanity
|
||||
checking is done at construction time.
|
||||
"""
|
||||
(a, b) = self.frac()
|
||||
return a > 0 and a < b # lint-amnesty, pylint: disable=chained-comparison
|
||||
|
||||
def done(self):
|
||||
"""Return True if this represents done.
|
||||
|
||||
subclassing note: implemented in terms of frac(), assumes sanity
|
||||
checking is done at construction time.
|
||||
"""
|
||||
(a, b) = self.frac()
|
||||
return a == b
|
||||
|
||||
def ternary_str(self):
|
||||
"""Return a string version of this progress: either
|
||||
"none", "in_progress", or "done".
|
||||
|
||||
subclassing note: implemented in terms of frac()
|
||||
"""
|
||||
(a, b) = self.frac()
|
||||
if a == 0:
|
||||
return "none"
|
||||
if a < b:
|
||||
return "in_progress"
|
||||
return "done"
|
||||
|
||||
def __eq__(self, other):
|
||||
"""Two Progress objects are equal if they have identical values.
|
||||
Implemented in terms of frac()"""
|
||||
if not isinstance(other, Progress):
|
||||
return False
|
||||
(a, b) = self.frac()
|
||||
(a2, b2) = other.frac()
|
||||
return a == a2 and b == b2
|
||||
|
||||
def __ne__(self, other):
|
||||
"""The opposite of equal"""
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __str__(self):
|
||||
"""Return a string representation of this string. Rounds results to
|
||||
two decimal places, stripping out any trailing zeroes.
|
||||
|
||||
subclassing note: implemented in terms of frac().
|
||||
|
||||
"""
|
||||
(a, b) = self.frac()
|
||||
display = lambda n: f"{n:.2f}".rstrip("0").rstrip(".")
|
||||
return f"{display(a)}/{display(b)}"
|
||||
|
||||
@staticmethod
|
||||
def add_counts(a, b):
|
||||
"""Add two progress indicators, assuming that each represents items done:
|
||||
(a / b) + (c / d) = (a + c) / (b + d).
|
||||
If either is None, returns the other.
|
||||
"""
|
||||
if a is None:
|
||||
return b
|
||||
if b is None:
|
||||
return a
|
||||
# get numerators + denominators
|
||||
(n, d) = a.frac()
|
||||
(n2, d2) = b.frac()
|
||||
return Progress(n + n2, d + d2)
|
||||
@@ -20,7 +20,8 @@ from web_fragments.fragment import Fragment
|
||||
from xblock.completable import XBlockCompletionMode
|
||||
from xblock.core import XBlock
|
||||
from xblock.exceptions import NoSuchServiceError
|
||||
from xblock.fields import Boolean, Integer, List, Scope, String
|
||||
from xblock.fields import Boolean, Date, Integer, List, Scope, String
|
||||
from xblock.progress import Progress
|
||||
|
||||
from edx_toggles.toggles import WaffleFlag, SettingDictToggle
|
||||
from xmodule.util.builtin_assets import add_webpack_js_to_fragment, add_css_to_fragment
|
||||
@@ -35,13 +36,10 @@ from xmodule.x_module import (
|
||||
from common.djangoapps.xblock_django.constants import ATTR_KEY_USER_ID, ATTR_KEY_USER_IS_STAFF
|
||||
|
||||
from .exceptions import NotFoundError
|
||||
from .fields import Date
|
||||
from .mako_block import MakoTemplateBlockBase
|
||||
from .progress import Progress
|
||||
from .x_module import AUTHOR_VIEW, PUBLIC_VIEW
|
||||
from .xml_block import XmlMixin
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# HACK: This shouldn't be hard-coded to two types
|
||||
|
||||
@@ -17,9 +17,9 @@ from webob import Response
|
||||
from xblock.core import XBlock
|
||||
from xblock.exceptions import NoSuchServiceError
|
||||
from xblock.fields import Integer, ReferenceValueDict, Scope, String
|
||||
from xblock.progress import Progress
|
||||
from xmodule.mako_block import MakoTemplateBlockBase
|
||||
from xmodule.modulestore.inheritance import UserPartitionList
|
||||
from xmodule.progress import Progress
|
||||
from xmodule.seq_block import ProctoringFields, SequenceMixin
|
||||
from xmodule.studio_editable import StudioEditableBlock
|
||||
from xmodule.util.builtin_assets import add_webpack_js_to_fragment
|
||||
|
||||
@@ -25,11 +25,11 @@ from lxml import etree
|
||||
from opaque_keys.edx.locator import BlockUsageLocator, CourseLocator
|
||||
from pytz import UTC
|
||||
from webob.multidict import MultiDict
|
||||
from xblock.exceptions import NotFoundError
|
||||
from xblock.field_data import DictFieldData
|
||||
from xblock.fields import ScopeIds
|
||||
from xblock.scorable import Score
|
||||
|
||||
import xmodule
|
||||
from lms.djangoapps.courseware.user_state_client import XBlockUserState
|
||||
from openedx.core.djangolib.testing.utils import skip_unless_lms
|
||||
from xmodule.capa import responsetypes
|
||||
@@ -1198,7 +1198,7 @@ class ProblemBlockTest(unittest.TestCase): # lint-amnesty, pylint: disable=miss
|
||||
# Simulate that ProblemBlock.closed() always returns True
|
||||
with patch("xmodule.capa_block.ProblemBlock.closed") as mock_closed:
|
||||
mock_closed.return_value = True
|
||||
with pytest.raises(xmodule.exceptions.NotFoundError):
|
||||
with pytest.raises(NotFoundError):
|
||||
get_request_dict = {CapaFactory.input_key(): "3.14"}
|
||||
block.submit_problem(get_request_dict)
|
||||
|
||||
@@ -1214,7 +1214,7 @@ class ProblemBlockTest(unittest.TestCase): # lint-amnesty, pylint: disable=miss
|
||||
block.done = True
|
||||
|
||||
# Expect that we cannot submit
|
||||
with pytest.raises(xmodule.exceptions.NotFoundError):
|
||||
with pytest.raises(NotFoundError):
|
||||
get_request_dict = {CapaFactory.input_key(): "3.14"}
|
||||
block.submit_problem(get_request_dict)
|
||||
|
||||
@@ -1848,7 +1848,7 @@ class ProblemBlockTest(unittest.TestCase): # lint-amnesty, pylint: disable=miss
|
||||
block = CapaFactory.create(done=False)
|
||||
|
||||
# Try to rescore the problem, and get exception
|
||||
with pytest.raises(xmodule.exceptions.NotFoundError):
|
||||
with pytest.raises(NotFoundError):
|
||||
block.rescore(only_if_higher=False)
|
||||
|
||||
def test_rescore_problem_not_supported(self):
|
||||
|
||||
@@ -16,11 +16,11 @@ from unittest.mock import Mock
|
||||
import pytest
|
||||
from opaque_keys.edx.locator import BlockUsageLocator, CourseLocator
|
||||
from pytz import UTC
|
||||
from xblock.exceptions import NotFoundError
|
||||
from xblock.field_data import DictFieldData
|
||||
from xblock.fields import ScopeIds
|
||||
from xblock.scorable import Score
|
||||
|
||||
import xmodule
|
||||
from xmodule.capa_block import ProblemBlock
|
||||
|
||||
from . import get_test_system
|
||||
@@ -244,7 +244,7 @@ class XModuleQuizAttemptsDelayTest(unittest.TestCase):
|
||||
# Already attempted once (just now) and thus has a submitted time
|
||||
num_attempts = 99
|
||||
# Regular create_and_check should fail
|
||||
with pytest.raises(xmodule.exceptions.NotFoundError):
|
||||
with pytest.raises(NotFoundError):
|
||||
(block, unused_result) = self.create_and_check(
|
||||
num_attempts=num_attempts,
|
||||
last_submission_time=datetime.datetime(2013, 12, 6, 0, 17, 36, tzinfo=UTC),
|
||||
|
||||
@@ -1,164 +0,0 @@
|
||||
"""Tests for classes defined in fields.py."""
|
||||
|
||||
import datetime
|
||||
import unittest
|
||||
|
||||
import pytest
|
||||
from pytz import UTC
|
||||
|
||||
from xmodule.fields import Date, RelativeTime, Timedelta
|
||||
|
||||
|
||||
class DateTest(unittest.TestCase): # lint-amnesty, pylint: disable=missing-class-docstring
|
||||
date = Date()
|
||||
|
||||
def compare_dates(self, dt1, dt2, expected_delta):
|
||||
assert (dt1 - dt2) == expected_delta, (((str(dt1) + "-") + str(dt2)) + "!=") + str(expected_delta)
|
||||
|
||||
def test_from_json(self):
|
||||
"""Test conversion from iso compatible date strings to struct_time"""
|
||||
self.compare_dates(
|
||||
DateTest.date.from_json("2013-01-01"), DateTest.date.from_json("2012-12-31"), datetime.timedelta(days=1)
|
||||
)
|
||||
self.compare_dates(
|
||||
DateTest.date.from_json("2013-01-01T00"),
|
||||
DateTest.date.from_json("2012-12-31T23"),
|
||||
datetime.timedelta(hours=1),
|
||||
)
|
||||
self.compare_dates(
|
||||
DateTest.date.from_json("2013-01-01T00:00"),
|
||||
DateTest.date.from_json("2012-12-31T23:59"),
|
||||
datetime.timedelta(minutes=1),
|
||||
)
|
||||
self.compare_dates(
|
||||
DateTest.date.from_json("2013-01-01T00:00:00"),
|
||||
DateTest.date.from_json("2012-12-31T23:59:59"),
|
||||
datetime.timedelta(seconds=1),
|
||||
)
|
||||
self.compare_dates(
|
||||
DateTest.date.from_json("2013-01-01T00:00:00Z"),
|
||||
DateTest.date.from_json("2012-12-31T23:59:59Z"),
|
||||
datetime.timedelta(seconds=1),
|
||||
)
|
||||
self.compare_dates(
|
||||
DateTest.date.from_json("2012-12-31T23:00:01-01:00"),
|
||||
DateTest.date.from_json("2013-01-01T00:00:00+01:00"),
|
||||
datetime.timedelta(hours=1, seconds=1),
|
||||
)
|
||||
|
||||
def test_enforce_type(self):
|
||||
assert DateTest.date.enforce_type(None) is None
|
||||
assert DateTest.date.enforce_type("") is None
|
||||
assert DateTest.date.enforce_type("2012-12-31T23:00:01") == datetime.datetime(
|
||||
2012, 12, 31, 23, 0, 1, tzinfo=UTC
|
||||
)
|
||||
assert DateTest.date.enforce_type(1234567890000) == datetime.datetime(2009, 2, 13, 23, 31, 30, tzinfo=UTC)
|
||||
assert DateTest.date.enforce_type(datetime.datetime(2014, 5, 9, 21, 1, 27, tzinfo=UTC)) == datetime.datetime(
|
||||
2014, 5, 9, 21, 1, 27, tzinfo=UTC
|
||||
)
|
||||
with pytest.raises(TypeError):
|
||||
DateTest.date.enforce_type([1])
|
||||
|
||||
def test_return_None(self):
|
||||
assert DateTest.date.from_json("") is None
|
||||
assert DateTest.date.from_json(None) is None
|
||||
with pytest.raises(TypeError):
|
||||
DateTest.date.from_json(["unknown value"])
|
||||
|
||||
def test_old_due_date_format(self):
|
||||
current = datetime.datetime.today()
|
||||
assert datetime.datetime(current.year, 3, 12, 12, tzinfo=UTC) == DateTest.date.from_json("March 12 12:00")
|
||||
assert datetime.datetime(current.year, 12, 4, 16, 30, tzinfo=UTC) == DateTest.date.from_json("December 4 16:30")
|
||||
assert DateTest.date.from_json("12 12:00") is None
|
||||
|
||||
def test_non_std_from_json(self):
|
||||
"""
|
||||
Test the non-standard args being passed to from_json
|
||||
"""
|
||||
now = datetime.datetime.now(UTC)
|
||||
delta = now - datetime.datetime.fromtimestamp(0, UTC)
|
||||
assert DateTest.date.from_json(delta.total_seconds() * 1000) == now
|
||||
yesterday = datetime.datetime.now(UTC) - datetime.timedelta(days=-1)
|
||||
assert DateTest.date.from_json(yesterday) == yesterday
|
||||
|
||||
def test_to_json(self):
|
||||
"""
|
||||
Test converting time reprs to iso dates
|
||||
"""
|
||||
assert (
|
||||
DateTest.date.to_json(datetime.datetime.strptime("2012-12-31T23:59:59Z", "%Y-%m-%dT%H:%M:%SZ"))
|
||||
== "2012-12-31T23:59:59Z"
|
||||
)
|
||||
assert DateTest.date.to_json(DateTest.date.from_json("2012-12-31T23:59:59Z")) == "2012-12-31T23:59:59Z"
|
||||
assert (
|
||||
DateTest.date.to_json(DateTest.date.from_json("2012-12-31T23:00:01-01:00")) == "2012-12-31T23:00:01-01:00"
|
||||
)
|
||||
with pytest.raises(TypeError):
|
||||
DateTest.date.to_json("2012-12-31T23:00:01-01:00")
|
||||
|
||||
|
||||
class TimedeltaTest(unittest.TestCase): # lint-amnesty, pylint: disable=missing-class-docstring
|
||||
delta = Timedelta()
|
||||
|
||||
def test_from_json(self):
|
||||
assert TimedeltaTest.delta.from_json("1 day 12 hours 59 minutes 59 seconds") == datetime.timedelta(
|
||||
days=1, hours=12, minutes=59, seconds=59
|
||||
)
|
||||
|
||||
assert TimedeltaTest.delta.from_json("1 day 46799 seconds") == datetime.timedelta(days=1, seconds=46799)
|
||||
|
||||
def test_enforce_type(self):
|
||||
assert TimedeltaTest.delta.enforce_type(None) is None
|
||||
assert TimedeltaTest.delta.enforce_type(datetime.timedelta(days=1, seconds=46799)) == datetime.timedelta(
|
||||
days=1, seconds=46799
|
||||
)
|
||||
assert TimedeltaTest.delta.enforce_type("1 day 46799 seconds") == datetime.timedelta(days=1, seconds=46799)
|
||||
with pytest.raises(TypeError):
|
||||
TimedeltaTest.delta.enforce_type([1])
|
||||
|
||||
def test_to_json(self):
|
||||
assert "1 days 46799 seconds" == TimedeltaTest.delta.to_json(
|
||||
datetime.timedelta(days=1, hours=12, minutes=59, seconds=59)
|
||||
)
|
||||
|
||||
|
||||
class RelativeTimeTest(unittest.TestCase): # lint-amnesty, pylint: disable=missing-class-docstring
|
||||
|
||||
delta = RelativeTime()
|
||||
|
||||
def test_from_json(self):
|
||||
assert RelativeTimeTest.delta.from_json("0:05:07") == datetime.timedelta(seconds=307)
|
||||
|
||||
assert RelativeTimeTest.delta.from_json(100.0) == datetime.timedelta(seconds=100)
|
||||
assert RelativeTimeTest.delta.from_json(None) == datetime.timedelta(seconds=0)
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
RelativeTimeTest.delta.from_json(1234) # int
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
RelativeTimeTest.delta.from_json("77:77:77")
|
||||
|
||||
def test_enforce_type(self):
|
||||
assert RelativeTimeTest.delta.enforce_type(None) is None
|
||||
assert RelativeTimeTest.delta.enforce_type(datetime.timedelta(days=1, seconds=46799)) == datetime.timedelta(
|
||||
days=1, seconds=46799
|
||||
)
|
||||
assert RelativeTimeTest.delta.enforce_type("0:05:07") == datetime.timedelta(seconds=307)
|
||||
with pytest.raises(TypeError):
|
||||
RelativeTimeTest.delta.enforce_type([1])
|
||||
|
||||
def test_to_json(self):
|
||||
assert "01:02:03" == RelativeTimeTest.delta.to_json(datetime.timedelta(seconds=3723))
|
||||
assert "00:00:00" == RelativeTimeTest.delta.to_json(None)
|
||||
assert "00:01:40" == RelativeTimeTest.delta.to_json(100.0)
|
||||
|
||||
error_msg = "RelativeTime max value is 23:59:59=86400.0 seconds, but 90000.0 seconds is passed"
|
||||
with self.assertRaisesRegex(ValueError, error_msg):
|
||||
RelativeTimeTest.delta.to_json(datetime.timedelta(seconds=90000))
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
RelativeTimeTest.delta.to_json("123")
|
||||
|
||||
def test_str(self):
|
||||
assert "01:02:03" == RelativeTimeTest.delta.to_json(datetime.timedelta(seconds=3723))
|
||||
assert "11:02:03" == RelativeTimeTest.delta.to_json(datetime.timedelta(seconds=39723))
|
||||
@@ -4,14 +4,13 @@ Grading tests
|
||||
|
||||
|
||||
import unittest
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import datetime
|
||||
import pytest
|
||||
import ddt
|
||||
from pytz import UTC
|
||||
|
||||
from lms.djangoapps.grades.scores import compute_percent
|
||||
from xmodule import graders
|
||||
from xmodule.graders import AggregatedScore, ProblemScore, ShowCorrectness, aggregate_scores
|
||||
from xmodule.graders import AggregatedScore, ProblemScore, aggregate_scores
|
||||
|
||||
|
||||
class GradesheetTest(unittest.TestCase):
|
||||
@@ -415,89 +414,3 @@ class GraderTest(unittest.TestCase):
|
||||
|
||||
for i, section_breakdown in enumerate(graded['section_breakdown']):
|
||||
assert expected_sequential_ids[i] == section_breakdown.get('sequential_id')
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class ShowCorrectnessTest(unittest.TestCase):
|
||||
"""
|
||||
Tests the correctness_available method
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
now = datetime.now(UTC)
|
||||
day_delta = timedelta(days=1)
|
||||
self.yesterday = now - day_delta
|
||||
self.today = now
|
||||
self.tomorrow = now + day_delta
|
||||
|
||||
def test_show_correctness_default(self):
|
||||
"""
|
||||
Test that correctness is visible by default.
|
||||
"""
|
||||
assert ShowCorrectness.correctness_available()
|
||||
|
||||
@ddt.data(
|
||||
(ShowCorrectness.ALWAYS, True),
|
||||
(ShowCorrectness.ALWAYS, False),
|
||||
# Any non-constant values behave like "always"
|
||||
('', True),
|
||||
('', False),
|
||||
('other-value', True),
|
||||
('other-value', False),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_show_correctness_always(self, show_correctness, has_staff_access):
|
||||
"""
|
||||
Test that correctness is visible when show_correctness is turned on.
|
||||
"""
|
||||
assert ShowCorrectness.correctness_available(show_correctness=show_correctness,
|
||||
has_staff_access=has_staff_access)
|
||||
|
||||
@ddt.data(True, False)
|
||||
def test_show_correctness_never(self, has_staff_access):
|
||||
"""
|
||||
Test that show_correctness="never" hides correctness from learners and course staff.
|
||||
"""
|
||||
assert not ShowCorrectness.correctness_available(show_correctness=ShowCorrectness.NEVER,
|
||||
has_staff_access=has_staff_access)
|
||||
|
||||
@ddt.data(
|
||||
# Correctness not visible to learners if due date in the future
|
||||
('tomorrow', False, False),
|
||||
# Correctness is visible to learners if due date in the past
|
||||
('yesterday', False, True),
|
||||
# Correctness is visible to learners if due date in the past (just)
|
||||
('today', False, True),
|
||||
# Correctness is visible to learners if there is no due date
|
||||
(None, False, True),
|
||||
# Correctness is visible to staff if due date in the future
|
||||
('tomorrow', True, True),
|
||||
# Correctness is visible to staff if due date in the past
|
||||
('yesterday', True, True),
|
||||
# Correctness is visible to staff if there is no due date
|
||||
(None, True, True),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_show_correctness_past_due(self, due_date_str, has_staff_access, expected_result):
|
||||
"""
|
||||
Test show_correctness="past_due" to ensure:
|
||||
* correctness is always visible to course staff
|
||||
* correctness is always visible to everyone if there is no due date
|
||||
* correctness is visible to learners after the due date, when there is a due date.
|
||||
"""
|
||||
if due_date_str is None:
|
||||
due_date = None
|
||||
else:
|
||||
due_date = getattr(self, due_date_str)
|
||||
assert ShowCorrectness.correctness_available(ShowCorrectness.PAST_DUE, due_date, has_staff_access) ==\
|
||||
expected_result
|
||||
|
||||
@ddt.data(True, False)
|
||||
def test_show_correctness_never_but_include_grade(self, has_staff_access):
|
||||
"""
|
||||
Test that show_correctness="never_but_include_grade" hides correctness from learners and course staff.
|
||||
"""
|
||||
assert not ShowCorrectness.correctness_available(show_correctness=ShowCorrectness.NEVER_BUT_INCLUDE_GRADE,
|
||||
has_staff_access=has_staff_access)
|
||||
|
||||
@@ -13,10 +13,9 @@ from opaque_keys.edx.keys import CourseKey
|
||||
from opaque_keys.edx.locator import BlockUsageLocator, CourseLocator
|
||||
from pytz import UTC
|
||||
from xblock.core import XBlock
|
||||
from xblock.fields import Integer, Scope, String
|
||||
from xblock.fields import Date, Integer, Scope, String
|
||||
from xblock.runtime import DictKeyValueStore, KvsFieldData
|
||||
|
||||
from xmodule.fields import Date
|
||||
from xmodule.modulestore.inheritance import InheritanceMixin, compute_inherited_metadata
|
||||
from xmodule.modulestore.xml import XMLImportingModuleStoreRuntime, LibraryXMLModuleStore, XMLModuleStore
|
||||
from xmodule.tests import DATA_DIR
|
||||
|
||||
@@ -17,11 +17,10 @@ from opaque_keys.edx.locator import BlockUsageLocator
|
||||
from pytz import UTC
|
||||
from webob.request import Request
|
||||
from xblock.field_data import DictFieldData
|
||||
from xblock.fields import ScopeIds
|
||||
from xblock.fields import ScopeIds, Timedelta
|
||||
|
||||
|
||||
from common.djangoapps.xblock_django.constants import ATTR_KEY_ANONYMOUS_USER_ID
|
||||
from xmodule.fields import Timedelta
|
||||
from xmodule.lti_2_util import LTIError
|
||||
from xmodule.lti_block import LTIBlock
|
||||
from xmodule.tests.helpers import StubUserService
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
"""Module progress tests"""
|
||||
|
||||
import unittest
|
||||
|
||||
from xmodule.progress import Progress
|
||||
|
||||
|
||||
class ProgressTest(unittest.TestCase):
|
||||
"""Test that basic Progress objects work. A Progress represents a
|
||||
fraction between 0 and 1.
|
||||
"""
|
||||
|
||||
not_started = Progress(0, 17)
|
||||
part_done = Progress(2, 6)
|
||||
half_done = Progress(3, 6)
|
||||
also_half_done = Progress(1, 2)
|
||||
done = Progress(7, 7)
|
||||
|
||||
def test_create_object(self):
|
||||
# These should work:
|
||||
prg1 = Progress(0, 2) # pylint: disable=unused-variable
|
||||
prg2 = Progress(1, 2) # pylint: disable=unused-variable
|
||||
prg3 = Progress(2, 2) # pylint: disable=unused-variable
|
||||
|
||||
prg4 = Progress(2.5, 5.0) # pylint: disable=unused-variable
|
||||
prg5 = Progress(3.7, 12.3333) # pylint: disable=unused-variable
|
||||
|
||||
# These shouldn't
|
||||
self.assertRaises(ValueError, Progress, 0, 0)
|
||||
self.assertRaises(ValueError, Progress, 2, 0)
|
||||
self.assertRaises(ValueError, Progress, 1, -2)
|
||||
|
||||
self.assertRaises(TypeError, Progress, 0, "all")
|
||||
# check complex numbers just for the heck of it :)
|
||||
self.assertRaises(TypeError, Progress, 2j, 3)
|
||||
|
||||
def test_clamp(self):
|
||||
assert (2, 2) == Progress(3, 2).frac()
|
||||
assert (0, 2) == Progress((-2), 2).frac()
|
||||
|
||||
def test_frac(self):
|
||||
prg = Progress(1, 2)
|
||||
(a_mem, b_mem) = prg.frac()
|
||||
assert a_mem == 1
|
||||
assert b_mem == 2
|
||||
|
||||
def test_percent(self):
|
||||
assert self.not_started.percent() == 0
|
||||
assert round(self.part_done.percent() - 33.33333333333333, 7) >= 0
|
||||
assert self.half_done.percent() == 50
|
||||
assert self.done.percent() == 100
|
||||
|
||||
assert self.half_done.percent() == self.also_half_done.percent()
|
||||
|
||||
def test_started(self):
|
||||
assert not self.not_started.started()
|
||||
|
||||
assert self.part_done.started()
|
||||
assert self.half_done.started()
|
||||
assert self.done.started()
|
||||
|
||||
def test_inprogress(self):
|
||||
# only true if working on it
|
||||
assert not self.done.inprogress()
|
||||
assert not self.not_started.inprogress()
|
||||
|
||||
assert self.part_done.inprogress()
|
||||
assert self.half_done.inprogress()
|
||||
|
||||
def test_done(self):
|
||||
assert self.done.done()
|
||||
assert not self.half_done.done()
|
||||
assert not self.not_started.done()
|
||||
|
||||
def test_str(self):
|
||||
assert str(self.not_started) == "0/17"
|
||||
assert str(self.part_done) == "2/6"
|
||||
assert str(self.done) == "7/7"
|
||||
assert str(Progress(2.1234, 7)) == "2.12/7"
|
||||
assert str(Progress(2.0034, 7)) == "2/7"
|
||||
assert str(Progress(0.999, 7)) == "1/7"
|
||||
|
||||
def test_add(self):
|
||||
"""Test the Progress.add_counts() method"""
|
||||
prg1 = Progress(0, 2)
|
||||
prg2 = Progress(1, 3)
|
||||
prg3 = Progress(2, 5)
|
||||
prg_none = None
|
||||
add = lambda a, b: Progress.add_counts(a, b).frac()
|
||||
|
||||
assert add(prg1, prg1) == (0, 4)
|
||||
assert add(prg1, prg2) == (1, 5)
|
||||
assert add(prg2, prg3) == (3, 8)
|
||||
|
||||
assert add(prg2, prg_none) == prg2.frac()
|
||||
assert add(prg_none, prg2) == prg2.frac()
|
||||
|
||||
def test_equality(self):
|
||||
"""Test that comparing Progress objects for equality
|
||||
works correctly."""
|
||||
prg1 = Progress(1, 2)
|
||||
prg2 = Progress(2, 4)
|
||||
prg3 = Progress(1, 2)
|
||||
assert prg1 == prg3
|
||||
assert prg1 != prg2
|
||||
|
||||
# Check != while we're at it
|
||||
assert prg1 != prg2
|
||||
assert prg1 == prg3
|
||||
@@ -8,11 +8,10 @@ from unittest.mock import Mock
|
||||
import dateutil.parser
|
||||
from opaque_keys.edx.locator import BlockUsageLocator, CourseLocator
|
||||
from xblock.field_data import DictFieldData
|
||||
from xblock.fields import Any, Boolean, Dict, Float, Integer, List, Scope, String
|
||||
from xblock.fields import Any, Boolean, Date, Dict, Float, Integer, List, RelativeTime, Scope, String, Timedelta
|
||||
from xblock.runtime import DictKeyValueStore, KvsFieldData
|
||||
|
||||
from xmodule.course_block import CourseBlock
|
||||
from xmodule.fields import Date, RelativeTime, Timedelta
|
||||
from xmodule.modulestore.inheritance import InheritanceKeyValueStore, InheritanceMixin, InheritingFieldData
|
||||
from xmodule.modulestore.split_mongo.split_mongo_kvs import SplitMongoKVS
|
||||
from xmodule.seq_block import SequenceBlock
|
||||
|
||||
@@ -15,9 +15,9 @@ from openedx_filters.learning.filters import VerticalBlockChildRenderStarted, Ve
|
||||
from web_fragments.fragment import Fragment
|
||||
from xblock.core import XBlock # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from xblock.fields import Boolean, Scope
|
||||
from xblock.progress import Progress
|
||||
|
||||
from xmodule.mako_block import MakoTemplateBlockBase
|
||||
from xmodule.progress import Progress
|
||||
from xmodule.seq_block import SequenceFields
|
||||
from xmodule.studio_editable import StudioEditableBlock
|
||||
from xmodule.util.builtin_assets import add_webpack_js_to_fragment
|
||||
|
||||
@@ -15,9 +15,9 @@ from opaque_keys.edx.locator import CourseLocator
|
||||
from webob import Response
|
||||
from xblock.core import XBlock
|
||||
from xblock.exceptions import JsonHandlerError
|
||||
from xblock.fields import RelativeTime
|
||||
|
||||
from xmodule.exceptions import NotFoundError
|
||||
from xmodule.fields import RelativeTime
|
||||
|
||||
from openedx.core.djangoapps.video_config.transcripts_utils import (
|
||||
Transcript,
|
||||
|
||||
@@ -5,9 +5,7 @@ XFields for video block.
|
||||
|
||||
import datetime
|
||||
|
||||
from xblock.fields import Boolean, DateTime, Dict, Float, List, Scope, String
|
||||
|
||||
from xmodule.fields import RelativeTime
|
||||
from xblock.fields import Boolean, DateTime, Dict, Float, List, RelativeTime, 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
|
||||
|
||||
@@ -17,7 +17,7 @@ from web_fragments.fragment import Fragment
|
||||
from webob import Response
|
||||
from webob.multidict import MultiDict
|
||||
from xblock.core import XBlock
|
||||
from xblock.fields import Dict, Float, Integer, List, Scope, String, UserScope
|
||||
from xblock.fields import Dict, Float, Integer, List, RelativeTime, Scope, String, UserScope
|
||||
from xblock.runtime import IdGenerator, IdReader, Runtime
|
||||
|
||||
from common.djangoapps.xblock_django.constants import (
|
||||
@@ -31,7 +31,6 @@ from common.djangoapps.xblock_django.constants import (
|
||||
)
|
||||
from openedx.core.djangolib.markup import HTML
|
||||
from xmodule import block_metadata_utils
|
||||
from xmodule.fields import RelativeTime
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
from xmodule.util.builtin_assets import add_webpack_js_to_fragment
|
||||
|
||||
|
||||
Reference in New Issue
Block a user