Mask grades on progress page according to "Show Correctness" setting.
This commit is contained in:
committed by
Tim Krones
parent
e8a36957b1
commit
6ca8a702ae
@@ -25,8 +25,9 @@ from capa.responsetypes import StudentInputError, ResponseError, LoncapaProblemE
|
|||||||
from capa.util import convert_files_to_filenames, get_inner_html_from_xpath
|
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.fields import Boolean, Dict, Float, Integer, Scope, String, XMLString
|
||||||
from xblock.scorable import ScorableXBlockMixin, Score
|
from xblock.scorable import ScorableXBlockMixin, Score
|
||||||
from xmodule.capa_base_constants import RANDOMIZATION, SHOWANSWER, SHOW_CORRECTNESS
|
from xmodule.capa_base_constants import RANDOMIZATION, SHOWANSWER
|
||||||
from xmodule.exceptions import NotFoundError
|
from xmodule.exceptions import NotFoundError
|
||||||
|
from xmodule.graders import ShowCorrectness
|
||||||
from .fields import Date, Timedelta
|
from .fields import Date, Timedelta
|
||||||
from .progress import Progress
|
from .progress import Progress
|
||||||
|
|
||||||
@@ -120,11 +121,11 @@ class CapaFields(object):
|
|||||||
help=_("Defines when to show whether a learner's answer to the problem is correct. "
|
help=_("Defines when to show whether a learner's answer to the problem is correct. "
|
||||||
"Configured on the subsection."),
|
"Configured on the subsection."),
|
||||||
scope=Scope.settings,
|
scope=Scope.settings,
|
||||||
default=SHOW_CORRECTNESS.ALWAYS,
|
default=ShowCorrectness.ALWAYS,
|
||||||
values=[
|
values=[
|
||||||
{"display_name": _("Always"), "value": SHOW_CORRECTNESS.ALWAYS},
|
{"display_name": _("Always"), "value": ShowCorrectness.ALWAYS},
|
||||||
{"display_name": _("Never"), "value": SHOW_CORRECTNESS.NEVER},
|
{"display_name": _("Never"), "value": ShowCorrectness.NEVER},
|
||||||
{"display_name": _("Past Due"), "value": SHOW_CORRECTNESS.PAST_DUE},
|
{"display_name": _("Past Due"), "value": ShowCorrectness.PAST_DUE},
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
showanswer = String(
|
showanswer = String(
|
||||||
@@ -921,17 +922,11 @@ class CapaMixin(ScorableXBlockMixin, CapaFields):
|
|||||||
|
|
||||||
Limits access to the correct/incorrect flags, messages, and problem score.
|
Limits access to the correct/incorrect flags, messages, and problem score.
|
||||||
"""
|
"""
|
||||||
if self.show_correctness == SHOW_CORRECTNESS.NEVER:
|
return ShowCorrectness.correctness_available(
|
||||||
return False
|
show_correctness=self.show_correctness,
|
||||||
elif self.runtime.user_is_staff:
|
due_date=self.close_date,
|
||||||
# This is after the 'never' check because admins can see correctness
|
has_staff_access=self.runtime.user_is_staff,
|
||||||
# unless the problem explicitly prevents it
|
)
|
||||||
return True
|
|
||||||
elif self.show_correctness == SHOW_CORRECTNESS.PAST_DUE:
|
|
||||||
return self.is_past_due()
|
|
||||||
|
|
||||||
# else: self.show_correctness == SHOW_CORRECTNESS.ALWAYS
|
|
||||||
return True
|
|
||||||
|
|
||||||
def update_score(self, data):
|
def update_score(self, data):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -4,15 +4,6 @@ Constants for capa_base problems
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
class SHOW_CORRECTNESS(object): # pylint: disable=invalid-name
|
|
||||||
"""
|
|
||||||
Constants for when to show correctness
|
|
||||||
"""
|
|
||||||
ALWAYS = "always"
|
|
||||||
PAST_DUE = "past_due"
|
|
||||||
NEVER = "never"
|
|
||||||
|
|
||||||
|
|
||||||
class SHOWANSWER(object):
|
class SHOWANSWER(object):
|
||||||
"""
|
"""
|
||||||
Constants for when to show answer
|
Constants for when to show answer
|
||||||
|
|||||||
@@ -10,9 +10,10 @@ import logging
|
|||||||
import random
|
import random
|
||||||
import sys
|
import sys
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from datetime import datetime # Used by pycontracts. pylint: disable=unused-import
|
from datetime import datetime
|
||||||
|
|
||||||
from contracts import contract
|
from contracts import contract
|
||||||
|
from pytz import UTC
|
||||||
|
|
||||||
log = logging.getLogger("edx.courseware")
|
log = logging.getLogger("edx.courseware")
|
||||||
|
|
||||||
@@ -462,3 +463,38 @@ def _min_or_none(itr):
|
|||||||
return min(itr)
|
return min(itr)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class ShowCorrectness(object):
|
||||||
|
"""
|
||||||
|
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"
|
||||||
|
|
||||||
|
@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 == cls.NEVER:
|
||||||
|
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
|
||||||
|
|||||||
@@ -2,13 +2,15 @@
|
|||||||
Grading tests
|
Grading tests
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from datetime import datetime
|
import unittest
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
import ddt
|
import ddt
|
||||||
import unittest
|
from pytz import UTC
|
||||||
|
|
||||||
from xmodule import graders
|
from xmodule import graders
|
||||||
from xmodule.graders import ProblemScore, AggregatedScore, aggregate_scores
|
from xmodule.graders import (
|
||||||
|
AggregatedScore, ProblemScore, ShowCorrectness, aggregate_scores
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class GradesheetTest(unittest.TestCase):
|
class GradesheetTest(unittest.TestCase):
|
||||||
@@ -315,3 +317,86 @@ class GraderTest(unittest.TestCase):
|
|||||||
with self.assertRaises(ValueError) as error:
|
with self.assertRaises(ValueError) as error:
|
||||||
graders.grader_from_conf([invalid_conf])
|
graders.grader_from_conf([invalid_conf])
|
||||||
self.assertIn(expected_error_message, error.exception.message)
|
self.assertIn(expected_error_message, error.exception.message)
|
||||||
|
|
||||||
|
|
||||||
|
@ddt.ddt
|
||||||
|
class ShowCorrectnessTest(unittest.TestCase):
|
||||||
|
"""
|
||||||
|
Tests the correctness_available method
|
||||||
|
"""
|
||||||
|
def setUp(self):
|
||||||
|
super(ShowCorrectnessTest, self).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.
|
||||||
|
"""
|
||||||
|
self.assertTrue(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.
|
||||||
|
"""
|
||||||
|
self.assertTrue(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.
|
||||||
|
"""
|
||||||
|
self.assertFalse(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)
|
||||||
|
self.assertEquals(
|
||||||
|
ShowCorrectness.correctness_available(ShowCorrectness.PAST_DUE, due_date, has_staff_access),
|
||||||
|
expected_result
|
||||||
|
)
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ SUPPORTED_FIELDS = [
|
|||||||
SupportedFieldType('graded'),
|
SupportedFieldType('graded'),
|
||||||
SupportedFieldType('format'),
|
SupportedFieldType('format'),
|
||||||
SupportedFieldType('due'),
|
SupportedFieldType('due'),
|
||||||
|
SupportedFieldType('show_correctness'),
|
||||||
# 'student_view_data'
|
# 'student_view_data'
|
||||||
SupportedFieldType(StudentViewTransformer.STUDENT_VIEW_DATA, StudentViewTransformer),
|
SupportedFieldType(StudentViewTransformer.STUDENT_VIEW_DATA, StudentViewTransformer),
|
||||||
# 'student_view_multi_device'
|
# 'student_view_multi_device'
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ class BlocksAPITransformer(BlockStructureTransformer):
|
|||||||
transform method.
|
transform method.
|
||||||
"""
|
"""
|
||||||
# collect basic xblock fields
|
# collect basic xblock fields
|
||||||
block_structure.request_xblock_fields('graded', 'format', 'display_name', 'category', 'due')
|
block_structure.request_xblock_fields('graded', 'format', 'display_name', 'category', 'due', 'show_correctness')
|
||||||
|
|
||||||
# collect data from containing transformers
|
# collect data from containing transformers
|
||||||
StudentViewTransformer.collect(block_structure)
|
StudentViewTransformer.collect(block_structure)
|
||||||
|
|||||||
@@ -172,6 +172,9 @@ class BlocksView(DeveloperErrorViewMixin, ListAPIView):
|
|||||||
|
|
||||||
* due: The due date of the block. Returned only if "due" is included
|
* due: The due date of the block. Returned only if "due" is included
|
||||||
in the "requested_fields" parameter.
|
in the "requested_fields" parameter.
|
||||||
|
|
||||||
|
* show_correctness: Whether to show scores/correctness to learners for the current sequence or problem.
|
||||||
|
Returned only if "show_correctness" is included in the "requested_fields" parameter.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def list(self, request, usage_key_string): # pylint: disable=arguments-differ
|
def list(self, request, usage_key_string): # pylint: disable=arguments-differ
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ from xblock.core import XBlock
|
|||||||
from xblock.fields import String, Scope
|
from xblock.fields import String, Scope
|
||||||
from xblock.fragment import Fragment
|
from xblock.fragment import Fragment
|
||||||
|
|
||||||
|
from capa.tests.response_xml_factory import MultipleChoiceResponseXMLFactory
|
||||||
|
from courseware.model_data import FieldDataCache
|
||||||
|
from courseware.module_render import get_module
|
||||||
import courseware.views.views as views
|
import courseware.views.views as views
|
||||||
import shoppingcart
|
import shoppingcart
|
||||||
from certificates import api as certs_api
|
from certificates import api as certs_api
|
||||||
@@ -56,6 +59,7 @@ from openedx.core.djangoapps.crawlers.models import CrawlersConfig
|
|||||||
from openedx.core.djangoapps.credit.api import set_credit_requirements
|
from openedx.core.djangoapps.credit.api import set_credit_requirements
|
||||||
from openedx.core.djangoapps.credit.models import CreditCourse, CreditProvider
|
from openedx.core.djangoapps.credit.models import CreditCourse, CreditProvider
|
||||||
from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
|
from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
|
||||||
|
from openedx.core.djangolib.testing.utils import get_mock_request
|
||||||
from openedx.core.lib.gating import api as gating_api
|
from openedx.core.lib.gating import api as gating_api
|
||||||
from openedx.features.enterprise_support.tests.mixins.enterprise import EnterpriseTestConsentRequired
|
from openedx.features.enterprise_support.tests.mixins.enterprise import EnterpriseTestConsentRequired
|
||||||
from student.models import CourseEnrollment
|
from student.models import CourseEnrollment
|
||||||
@@ -63,6 +67,7 @@ from student.tests.factories import AdminFactory, UserFactory, CourseEnrollmentF
|
|||||||
from util.tests.test_date_utils import fake_ugettext, fake_pgettext
|
from util.tests.test_date_utils import fake_ugettext, fake_pgettext
|
||||||
from util.url import reload_django_url_config
|
from util.url import reload_django_url_config
|
||||||
from util.views import ensure_valid_course_key
|
from util.views import ensure_valid_course_key
|
||||||
|
from xmodule.graders import ShowCorrectness
|
||||||
from xmodule.modulestore import ModuleStoreEnum
|
from xmodule.modulestore import ModuleStoreEnum
|
||||||
from xmodule.modulestore.django import modulestore
|
from xmodule.modulestore.django import modulestore
|
||||||
from xmodule.modulestore.tests.django_utils import TEST_DATA_MIXED_MODULESTORE
|
from xmodule.modulestore.tests.django_utils import TEST_DATA_MIXED_MODULESTORE
|
||||||
@@ -1165,29 +1170,32 @@ class StartDateTests(ModuleStoreTestCase):
|
|||||||
# pylint: disable=protected-access, no-member
|
# pylint: disable=protected-access, no-member
|
||||||
@attr(shard=1)
|
@attr(shard=1)
|
||||||
@override_settings(ENABLE_ENTERPRISE_INTEGRATION=False)
|
@override_settings(ENABLE_ENTERPRISE_INTEGRATION=False)
|
||||||
@ddt.ddt
|
class ProgressPageBaseTests(ModuleStoreTestCase):
|
||||||
class ProgressPageTests(ModuleStoreTestCase):
|
|
||||||
"""
|
"""
|
||||||
Tests that verify that the progress page works correctly.
|
Base class for progress page tests.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
ENABLED_CACHES = ['default', 'mongo_modulestore_inheritance', 'loc_cache']
|
ENABLED_CACHES = ['default', 'mongo_modulestore_inheritance', 'loc_cache']
|
||||||
ENABLED_SIGNALS = ['course_published']
|
ENABLED_SIGNALS = ['course_published']
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(ProgressPageTests, self).setUp()
|
super(ProgressPageBaseTests, self).setUp()
|
||||||
self.user = UserFactory.create()
|
self.user = UserFactory.create()
|
||||||
self.assertTrue(self.client.login(username=self.user.username, password='test'))
|
self.assertTrue(self.client.login(username=self.user.username, password='test'))
|
||||||
|
|
||||||
self.setup_course()
|
self.setup_course()
|
||||||
|
|
||||||
def setup_course(self, **options):
|
def create_course(self, **options):
|
||||||
"""Create the test course."""
|
"""Create the test course."""
|
||||||
self.course = CourseFactory.create(
|
self.course = CourseFactory.create(
|
||||||
start=datetime(2013, 9, 16, 7, 17, 28),
|
start=datetime(2013, 9, 16, 7, 17, 28),
|
||||||
grade_cutoffs={u'çü†øƒƒ': 0.75, 'Pass': 0.5},
|
grade_cutoffs={u'çü†øƒƒ': 0.75, 'Pass': 0.5},
|
||||||
**options
|
**options
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def setup_course(self, **course_options):
|
||||||
|
"""Create the test course and content, and enroll the user."""
|
||||||
|
self.create_course(**course_options)
|
||||||
with self.store.bulk_operations(self.course.id):
|
with self.store.bulk_operations(self.course.id):
|
||||||
self.chapter = ItemFactory.create(category='chapter', parent_location=self.course.location)
|
self.chapter = ItemFactory.create(category='chapter', parent_location=self.course.location)
|
||||||
self.section = ItemFactory.create(category='sequential', parent_location=self.chapter.location)
|
self.section = ItemFactory.create(category='sequential', parent_location=self.chapter.location)
|
||||||
@@ -1197,7 +1205,7 @@ class ProgressPageTests(ModuleStoreTestCase):
|
|||||||
|
|
||||||
def _get_progress_page(self, expected_status_code=200):
|
def _get_progress_page(self, expected_status_code=200):
|
||||||
"""
|
"""
|
||||||
Gets the progress page for the user in the course.
|
Gets the progress page for the currently logged-in user.
|
||||||
"""
|
"""
|
||||||
resp = self.client.get(
|
resp = self.client.get(
|
||||||
reverse('progress', args=[unicode(self.course.id)])
|
reverse('progress', args=[unicode(self.course.id)])
|
||||||
@@ -1215,6 +1223,14 @@ class ProgressPageTests(ModuleStoreTestCase):
|
|||||||
self.assertEqual(resp.status_code, expected_status_code)
|
self.assertEqual(resp.status_code, expected_status_code)
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=protected-access, no-member
|
||||||
|
@ddt.ddt
|
||||||
|
class ProgressPageTests(ProgressPageBaseTests):
|
||||||
|
"""
|
||||||
|
Tests that verify that the progress page works correctly.
|
||||||
|
"""
|
||||||
|
|
||||||
@ddt.data('"><script>alert(1)</script>', '<script>alert(1)</script>', '</script><script>alert(1)</script>')
|
@ddt.data('"><script>alert(1)</script>', '<script>alert(1)</script>', '</script><script>alert(1)</script>')
|
||||||
def test_progress_page_xss_prevent(self, malicious_code):
|
def test_progress_page_xss_prevent(self, malicious_code):
|
||||||
"""
|
"""
|
||||||
@@ -1649,6 +1665,277 @@ class ProgressPageTests(ModuleStoreTestCase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=protected-access, no-member
|
||||||
|
@ddt.ddt
|
||||||
|
class ProgressPageShowCorrectnessTests(ProgressPageBaseTests):
|
||||||
|
"""
|
||||||
|
Tests that verify that the progress page works correctly when displaying subsections where correctness is hidden.
|
||||||
|
"""
|
||||||
|
# Constants used in the test data
|
||||||
|
NOW = datetime.now(UTC)
|
||||||
|
DAY_DELTA = timedelta(days=1)
|
||||||
|
YESTERDAY = NOW - DAY_DELTA
|
||||||
|
TODAY = NOW
|
||||||
|
TOMORROW = NOW + DAY_DELTA
|
||||||
|
GRADER_TYPE = 'Homework'
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(ProgressPageShowCorrectnessTests, self).setUp()
|
||||||
|
self.staff_user = UserFactory.create(is_staff=True)
|
||||||
|
|
||||||
|
def setup_course(self, show_correctness='', due_date=None, graded=False, **course_options):
|
||||||
|
"""
|
||||||
|
Set up course with a subsection with the given show_correctness, due_date, and graded settings.
|
||||||
|
"""
|
||||||
|
# Use a simple grading policy
|
||||||
|
course_options['grading_policy'] = {
|
||||||
|
"GRADER": [{
|
||||||
|
"type": self.GRADER_TYPE,
|
||||||
|
"min_count": 2,
|
||||||
|
"drop_count": 0,
|
||||||
|
"short_label": "HW",
|
||||||
|
"weight": 1.0
|
||||||
|
}],
|
||||||
|
"GRADE_CUTOFFS": {
|
||||||
|
'A': .9,
|
||||||
|
'B': .33
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.create_course(**course_options)
|
||||||
|
|
||||||
|
metadata = dict(
|
||||||
|
show_correctness=show_correctness,
|
||||||
|
)
|
||||||
|
if due_date is not None:
|
||||||
|
metadata['due'] = due_date
|
||||||
|
if graded:
|
||||||
|
metadata['graded'] = True
|
||||||
|
metadata['format'] = self.GRADER_TYPE
|
||||||
|
|
||||||
|
with self.store.bulk_operations(self.course.id):
|
||||||
|
self.chapter = ItemFactory.create(category='chapter', parent_location=self.course.location,
|
||||||
|
display_name="Section 1")
|
||||||
|
self.section = ItemFactory.create(category='sequential', parent_location=self.chapter.location,
|
||||||
|
display_name="Subsection 1", metadata=metadata)
|
||||||
|
self.vertical = ItemFactory.create(category='vertical', parent_location=self.section.location)
|
||||||
|
|
||||||
|
CourseEnrollmentFactory(user=self.user, course_id=self.course.id, mode=CourseMode.HONOR)
|
||||||
|
|
||||||
|
def add_problem(self):
|
||||||
|
"""
|
||||||
|
Add a problem to the subsection
|
||||||
|
"""
|
||||||
|
problem_xml = MultipleChoiceResponseXMLFactory().build_xml(
|
||||||
|
question_text='The correct answer is Choice 1',
|
||||||
|
choices=[True, False],
|
||||||
|
choice_names=['choice_0', 'choice_1']
|
||||||
|
)
|
||||||
|
self.problem = ItemFactory.create(category='problem', parent_location=self.vertical.location,
|
||||||
|
data=problem_xml, display_name='Problem 1')
|
||||||
|
# Re-fetch the course from the database
|
||||||
|
self.course = self.store.get_course(self.course.id)
|
||||||
|
|
||||||
|
def answer_problem(self, value=1, max_value=1):
|
||||||
|
"""
|
||||||
|
Submit the given score to the problem on behalf of the user
|
||||||
|
"""
|
||||||
|
# Get the module for the problem, as viewed by the user
|
||||||
|
field_data_cache = FieldDataCache.cache_for_descriptor_descendents(
|
||||||
|
self.course.id,
|
||||||
|
self.user,
|
||||||
|
self.course,
|
||||||
|
depth=2
|
||||||
|
)
|
||||||
|
# pylint: disable=protected-access
|
||||||
|
module = get_module(
|
||||||
|
self.user,
|
||||||
|
get_mock_request(self.user),
|
||||||
|
self.problem.scope_ids.usage_id,
|
||||||
|
field_data_cache,
|
||||||
|
)._xmodule
|
||||||
|
|
||||||
|
# Submit the given score/max_score to the problem xmodule
|
||||||
|
grade_dict = {'value': value, 'max_value': max_value, 'user_id': self.user.id}
|
||||||
|
module.system.publish(self.problem, 'grade', grade_dict)
|
||||||
|
|
||||||
|
def assert_progress_page_show_grades(self, response, show_correctness, due_date, graded,
|
||||||
|
show_grades, score, max_score, avg):
|
||||||
|
"""
|
||||||
|
Ensures that grades and scores are shown or not shown on the progress page as required.
|
||||||
|
"""
|
||||||
|
|
||||||
|
expected_score = "<dd>{score}/{max_score}</dd>".format(score=score, max_score=max_score)
|
||||||
|
percent = score / float(max_score)
|
||||||
|
|
||||||
|
if show_grades:
|
||||||
|
# If grades are shown, we should be able to see the current problem scores.
|
||||||
|
self.assertIn(expected_score, response.content)
|
||||||
|
|
||||||
|
if graded:
|
||||||
|
expected_summary_text = "Problem Scores:"
|
||||||
|
else:
|
||||||
|
expected_summary_text = "Practice Scores:"
|
||||||
|
|
||||||
|
else:
|
||||||
|
# If grades are hidden, we should not be able to see the current problem scores.
|
||||||
|
self.assertNotIn(expected_score, response.content)
|
||||||
|
|
||||||
|
if graded:
|
||||||
|
expected_summary_text = "Problem scores are hidden"
|
||||||
|
else:
|
||||||
|
expected_summary_text = "Practice scores are hidden"
|
||||||
|
|
||||||
|
if show_correctness == ShowCorrectness.PAST_DUE and due_date:
|
||||||
|
expected_summary_text += ' until the due date.'
|
||||||
|
else:
|
||||||
|
expected_summary_text += '.'
|
||||||
|
|
||||||
|
# Ensure that expected text is present
|
||||||
|
self.assertIn(expected_summary_text, response.content)
|
||||||
|
|
||||||
|
@ddt.data(
|
||||||
|
('', None, False),
|
||||||
|
('', None, True),
|
||||||
|
(ShowCorrectness.ALWAYS, None, False),
|
||||||
|
(ShowCorrectness.ALWAYS, None, True),
|
||||||
|
(ShowCorrectness.ALWAYS, YESTERDAY, False),
|
||||||
|
(ShowCorrectness.ALWAYS, YESTERDAY, True),
|
||||||
|
(ShowCorrectness.ALWAYS, TODAY, False),
|
||||||
|
(ShowCorrectness.ALWAYS, TODAY, True),
|
||||||
|
(ShowCorrectness.ALWAYS, TOMORROW, False),
|
||||||
|
(ShowCorrectness.ALWAYS, TOMORROW, True),
|
||||||
|
(ShowCorrectness.NEVER, None, False),
|
||||||
|
(ShowCorrectness.NEVER, None, True),
|
||||||
|
(ShowCorrectness.NEVER, YESTERDAY, False),
|
||||||
|
(ShowCorrectness.NEVER, YESTERDAY, True),
|
||||||
|
(ShowCorrectness.NEVER, TODAY, False),
|
||||||
|
(ShowCorrectness.NEVER, TODAY, True),
|
||||||
|
(ShowCorrectness.NEVER, TOMORROW, False),
|
||||||
|
(ShowCorrectness.NEVER, TOMORROW, True),
|
||||||
|
(ShowCorrectness.PAST_DUE, None, False),
|
||||||
|
(ShowCorrectness.PAST_DUE, None, True),
|
||||||
|
(ShowCorrectness.PAST_DUE, YESTERDAY, False),
|
||||||
|
(ShowCorrectness.PAST_DUE, YESTERDAY, True),
|
||||||
|
(ShowCorrectness.PAST_DUE, TODAY, False),
|
||||||
|
(ShowCorrectness.PAST_DUE, TODAY, True),
|
||||||
|
(ShowCorrectness.PAST_DUE, TOMORROW, False),
|
||||||
|
(ShowCorrectness.PAST_DUE, TOMORROW, True),
|
||||||
|
)
|
||||||
|
@ddt.unpack
|
||||||
|
def test_progress_page_no_problem_scores(self, show_correctness, due_date, graded):
|
||||||
|
"""
|
||||||
|
Test that "no problem scores are present" for a course with no problems,
|
||||||
|
regardless of the various show correctness settings.
|
||||||
|
"""
|
||||||
|
self.setup_course(show_correctness=show_correctness, due_date=due_date, graded=graded)
|
||||||
|
resp = self._get_progress_page()
|
||||||
|
|
||||||
|
# Test that no problem scores are present
|
||||||
|
self.assertIn('No problem scores in this section', resp.content)
|
||||||
|
|
||||||
|
@ddt.data(
|
||||||
|
('', None, False, True),
|
||||||
|
('', None, True, True),
|
||||||
|
(ShowCorrectness.ALWAYS, None, False, True),
|
||||||
|
(ShowCorrectness.ALWAYS, None, True, True),
|
||||||
|
(ShowCorrectness.ALWAYS, YESTERDAY, False, True),
|
||||||
|
(ShowCorrectness.ALWAYS, YESTERDAY, True, True),
|
||||||
|
(ShowCorrectness.ALWAYS, TODAY, False, True),
|
||||||
|
(ShowCorrectness.ALWAYS, TODAY, True, True),
|
||||||
|
(ShowCorrectness.ALWAYS, TOMORROW, False, True),
|
||||||
|
(ShowCorrectness.ALWAYS, TOMORROW, True, True),
|
||||||
|
(ShowCorrectness.NEVER, None, False, False),
|
||||||
|
(ShowCorrectness.NEVER, None, True, False),
|
||||||
|
(ShowCorrectness.NEVER, YESTERDAY, False, False),
|
||||||
|
(ShowCorrectness.NEVER, YESTERDAY, True, False),
|
||||||
|
(ShowCorrectness.NEVER, TODAY, False, False),
|
||||||
|
(ShowCorrectness.NEVER, TODAY, True, False),
|
||||||
|
(ShowCorrectness.NEVER, TOMORROW, False, False),
|
||||||
|
(ShowCorrectness.NEVER, TOMORROW, True, False),
|
||||||
|
(ShowCorrectness.PAST_DUE, None, False, True),
|
||||||
|
(ShowCorrectness.PAST_DUE, None, True, True),
|
||||||
|
(ShowCorrectness.PAST_DUE, YESTERDAY, False, True),
|
||||||
|
(ShowCorrectness.PAST_DUE, YESTERDAY, True, True),
|
||||||
|
(ShowCorrectness.PAST_DUE, TODAY, False, True),
|
||||||
|
(ShowCorrectness.PAST_DUE, TODAY, True, True),
|
||||||
|
(ShowCorrectness.PAST_DUE, TOMORROW, False, False),
|
||||||
|
(ShowCorrectness.PAST_DUE, TOMORROW, True, False),
|
||||||
|
)
|
||||||
|
@ddt.unpack
|
||||||
|
def test_progress_page_hide_scores_from_learner(self, show_correctness, due_date, graded, show_grades):
|
||||||
|
"""
|
||||||
|
Test that problem scores are hidden on progress page when correctness is not available to the learner, and that
|
||||||
|
they are visible when it is.
|
||||||
|
"""
|
||||||
|
self.setup_course(show_correctness=show_correctness, due_date=due_date, graded=graded)
|
||||||
|
self.add_problem()
|
||||||
|
|
||||||
|
self.client.login(username=self.user.username, password='test')
|
||||||
|
resp = self._get_progress_page()
|
||||||
|
|
||||||
|
# Ensure that expected text is present
|
||||||
|
self.assert_progress_page_show_grades(resp, show_correctness, due_date, graded, show_grades, 0, 1, 0)
|
||||||
|
|
||||||
|
# Submit answers to the problem, and re-fetch the progress page
|
||||||
|
self.answer_problem()
|
||||||
|
|
||||||
|
resp = self._get_progress_page()
|
||||||
|
|
||||||
|
# Test that the expected text is still present.
|
||||||
|
self.assert_progress_page_show_grades(resp, show_correctness, due_date, graded, show_grades, 1, 1, .5)
|
||||||
|
|
||||||
|
@ddt.data(
|
||||||
|
('', None, False, True),
|
||||||
|
('', None, True, True),
|
||||||
|
(ShowCorrectness.ALWAYS, None, False, True),
|
||||||
|
(ShowCorrectness.ALWAYS, None, True, True),
|
||||||
|
(ShowCorrectness.ALWAYS, YESTERDAY, False, True),
|
||||||
|
(ShowCorrectness.ALWAYS, YESTERDAY, True, True),
|
||||||
|
(ShowCorrectness.ALWAYS, TODAY, False, True),
|
||||||
|
(ShowCorrectness.ALWAYS, TODAY, True, True),
|
||||||
|
(ShowCorrectness.ALWAYS, TOMORROW, False, True),
|
||||||
|
(ShowCorrectness.ALWAYS, TOMORROW, True, True),
|
||||||
|
(ShowCorrectness.NEVER, None, False, False),
|
||||||
|
(ShowCorrectness.NEVER, None, True, False),
|
||||||
|
(ShowCorrectness.NEVER, YESTERDAY, False, False),
|
||||||
|
(ShowCorrectness.NEVER, YESTERDAY, True, False),
|
||||||
|
(ShowCorrectness.NEVER, TODAY, False, False),
|
||||||
|
(ShowCorrectness.NEVER, TODAY, True, False),
|
||||||
|
(ShowCorrectness.NEVER, TOMORROW, False, False),
|
||||||
|
(ShowCorrectness.NEVER, TOMORROW, True, False),
|
||||||
|
(ShowCorrectness.PAST_DUE, None, False, True),
|
||||||
|
(ShowCorrectness.PAST_DUE, None, True, True),
|
||||||
|
(ShowCorrectness.PAST_DUE, YESTERDAY, False, True),
|
||||||
|
(ShowCorrectness.PAST_DUE, YESTERDAY, True, True),
|
||||||
|
(ShowCorrectness.PAST_DUE, TODAY, False, True),
|
||||||
|
(ShowCorrectness.PAST_DUE, TODAY, True, True),
|
||||||
|
(ShowCorrectness.PAST_DUE, TOMORROW, False, True),
|
||||||
|
(ShowCorrectness.PAST_DUE, TOMORROW, True, True),
|
||||||
|
)
|
||||||
|
@ddt.unpack
|
||||||
|
def test_progress_page_hide_scores_from_staff(self, show_correctness, due_date, graded, show_grades):
|
||||||
|
"""
|
||||||
|
Test that problem scores are hidden from staff viewing a learner's progress page only if show_correctness=never.
|
||||||
|
"""
|
||||||
|
self.setup_course(show_correctness=show_correctness, due_date=due_date, graded=graded)
|
||||||
|
self.add_problem()
|
||||||
|
|
||||||
|
# Login as a course staff user to view the student progress page.
|
||||||
|
self.client.login(username=self.staff_user.username, password='test')
|
||||||
|
|
||||||
|
resp = self._get_student_progress_page()
|
||||||
|
|
||||||
|
# Ensure that expected text is present
|
||||||
|
self.assert_progress_page_show_grades(resp, show_correctness, due_date, graded, show_grades, 0, 1, 0)
|
||||||
|
|
||||||
|
# Submit answers to the problem, and re-fetch the progress page
|
||||||
|
self.answer_problem()
|
||||||
|
resp = self._get_student_progress_page()
|
||||||
|
|
||||||
|
# Test that the expected text is still present.
|
||||||
|
self.assert_progress_page_show_grades(resp, show_correctness, due_date, graded, show_grades, 1, 1, .5)
|
||||||
|
|
||||||
|
|
||||||
@attr(shard=1)
|
@attr(shard=1)
|
||||||
class VerifyCourseKeyDecoratorTests(TestCase):
|
class VerifyCourseKeyDecoratorTests(TestCase):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from logging import getLogger
|
|||||||
from lms.djangoapps.grades.scores import get_score, possibly_scored
|
from lms.djangoapps.grades.scores import get_score, possibly_scored
|
||||||
from lms.djangoapps.grades.models import BlockRecord, PersistentSubsectionGrade
|
from lms.djangoapps.grades.models import BlockRecord, PersistentSubsectionGrade
|
||||||
from xmodule import block_metadata_utils, graders
|
from xmodule import block_metadata_utils, graders
|
||||||
from xmodule.graders import AggregatedScore
|
from xmodule.graders import AggregatedScore, ShowCorrectness
|
||||||
|
|
||||||
from ..config.waffle import waffle, WRITE_ONLY_IF_ENGAGED
|
from ..config.waffle import waffle, WRITE_ONLY_IF_ENGAGED
|
||||||
|
|
||||||
@@ -27,6 +27,7 @@ class SubsectionGradeBase(object):
|
|||||||
self.format = getattr(subsection, 'format', '')
|
self.format = getattr(subsection, 'format', '')
|
||||||
self.due = getattr(subsection, 'due', None)
|
self.due = getattr(subsection, 'due', None)
|
||||||
self.graded = getattr(subsection, 'graded', False)
|
self.graded = getattr(subsection, 'graded', False)
|
||||||
|
self.show_correctness = getattr(subsection, 'show_correctness', '')
|
||||||
|
|
||||||
self.course_version = getattr(subsection, 'course_version', None)
|
self.course_version = getattr(subsection, 'course_version', None)
|
||||||
self.subtree_edited_timestamp = getattr(subsection, 'subtree_edited_on', None)
|
self.subtree_edited_timestamp = getattr(subsection, 'subtree_edited_on', None)
|
||||||
@@ -47,6 +48,12 @@ class SubsectionGradeBase(object):
|
|||||||
)
|
)
|
||||||
return self.all_total.attempted
|
return self.all_total.attempted
|
||||||
|
|
||||||
|
def show_grades(self, has_staff_access):
|
||||||
|
"""
|
||||||
|
Returns whether subsection scores are currently available to users with or without staff access.
|
||||||
|
"""
|
||||||
|
return ShowCorrectness.correctness_available(self.show_correctness, self.due, has_staff_access)
|
||||||
|
|
||||||
|
|
||||||
class ZeroSubsectionGrade(SubsectionGradeBase):
|
class ZeroSubsectionGrade(SubsectionGradeBase):
|
||||||
"""
|
"""
|
||||||
@@ -224,7 +231,7 @@ class SubsectionGrade(SubsectionGradeBase):
|
|||||||
log_func(
|
log_func(
|
||||||
u"Grades: SG.{}, subsection: {}, course: {}, "
|
u"Grades: SG.{}, subsection: {}, course: {}, "
|
||||||
u"version: {}, edit: {}, user: {},"
|
u"version: {}, edit: {}, user: {},"
|
||||||
u"total: {}/{}, graded: {}/{}".format(
|
u"total: {}/{}, graded: {}/{}, show_correctness: {}".format(
|
||||||
log_statement,
|
log_statement,
|
||||||
self.location,
|
self.location,
|
||||||
self.location.course_key,
|
self.location.course_key,
|
||||||
@@ -235,5 +242,6 @@ class SubsectionGrade(SubsectionGradeBase):
|
|||||||
self.all_total.possible,
|
self.all_total.possible,
|
||||||
self.graded_total.earned,
|
self.graded_total.earned,
|
||||||
self.graded_total.possible,
|
self.graded_total.possible,
|
||||||
|
self.show_correctness,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
"""
|
"""
|
||||||
Grades Transformer
|
Grades Transformer
|
||||||
"""
|
"""
|
||||||
|
import json
|
||||||
from base64 import b64encode
|
from base64 import b64encode
|
||||||
from functools import reduce as functools_reduce
|
from functools import reduce as functools_reduce
|
||||||
from hashlib import sha1
|
from hashlib import sha1
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
import json
|
|
||||||
|
|
||||||
from lms.djangoapps.course_blocks.transformers.utils import collect_unioned_set_field, get_field_on_block
|
from lms.djangoapps.course_blocks.transformers.utils import collect_unioned_set_field, get_field_on_block
|
||||||
from openedx.core.djangoapps.content.block_structure.transformer import BlockStructureTransformer
|
from openedx.core.djangoapps.content.block_structure.transformer import BlockStructureTransformer
|
||||||
@@ -29,6 +29,7 @@ class GradesTransformer(BlockStructureTransformer):
|
|||||||
graded: (boolean)
|
graded: (boolean)
|
||||||
has_score: (boolean)
|
has_score: (boolean)
|
||||||
weight: (numeric)
|
weight: (numeric)
|
||||||
|
show_correctness: (string) when to show grades (one of 'always', 'past_due', 'never')
|
||||||
|
|
||||||
Additionally, the following value is calculated and stored as a
|
Additionally, the following value is calculated and stored as a
|
||||||
transformer_block_field for each block:
|
transformer_block_field for each block:
|
||||||
@@ -37,7 +38,16 @@ class GradesTransformer(BlockStructureTransformer):
|
|||||||
"""
|
"""
|
||||||
WRITE_VERSION = 4
|
WRITE_VERSION = 4
|
||||||
READ_VERSION = 4
|
READ_VERSION = 4
|
||||||
FIELDS_TO_COLLECT = [u'due', u'format', u'graded', u'has_score', u'weight', u'course_version', u'subtree_edited_on']
|
FIELDS_TO_COLLECT = [
|
||||||
|
u'due',
|
||||||
|
u'format',
|
||||||
|
u'graded',
|
||||||
|
u'has_score',
|
||||||
|
u'weight',
|
||||||
|
u'course_version',
|
||||||
|
u'subtree_edited_on',
|
||||||
|
u'show_correctness',
|
||||||
|
]
|
||||||
|
|
||||||
EXPLICIT_GRADED_FIELD_NAME = 'explicit_graded'
|
EXPLICIT_GRADED_FIELD_NAME = 'explicit_graded'
|
||||||
|
|
||||||
|
|||||||
@@ -180,12 +180,30 @@ from django.utils.http import urlquote_plus
|
|||||||
%endif
|
%endif
|
||||||
</p>
|
</p>
|
||||||
%if len(section.problem_scores.values()) > 0:
|
%if len(section.problem_scores.values()) > 0:
|
||||||
<dl class="scores">
|
%if section.show_grades(staff_access):
|
||||||
<dt class="hd hd-6">${ _("Problem Scores: ") if section.graded else _("Practice Scores: ")}</dt>
|
<dl class="scores">
|
||||||
%for score in section.problem_scores.values():
|
<dt class="hd hd-6">${ _("Problem Scores: ") if section.graded else _("Practice Scores: ")}</dt>
|
||||||
<dd>${"{0:.3n}/{1:.3n}".format(float(score.earned),float(score.possible))}</dd>
|
%for score in section.problem_scores.values():
|
||||||
%endfor
|
<dd>${"{0:.3n}/{1:.3n}".format(float(score.earned),float(score.possible))}</dd>
|
||||||
</dl>
|
%endfor
|
||||||
|
</dl>
|
||||||
|
%else:
|
||||||
|
<p class="hide-scores">
|
||||||
|
%if section.show_correctness == 'past_due':
|
||||||
|
%if section.graded:
|
||||||
|
${_("Problem scores are hidden until the due date.")}
|
||||||
|
%else:
|
||||||
|
${_("Practice scores are hidden until the due date.")}
|
||||||
|
%endif
|
||||||
|
%else:
|
||||||
|
%if section.graded:
|
||||||
|
${_("Problem scores are hidden.")}
|
||||||
|
%else:
|
||||||
|
${_("Practice scores are hidden.")}
|
||||||
|
%endif
|
||||||
|
%endif
|
||||||
|
</p>
|
||||||
|
%endif
|
||||||
%else:
|
%else:
|
||||||
<p class="no-scores">${_("No problem scores in this section")}</p>
|
<p class="no-scores">${_("No problem scores in this section")}</p>
|
||||||
%endif
|
%endif
|
||||||
|
|||||||
Reference in New Issue
Block a user