AA-99: Adding in new date pills for the dates tab
This commit is contained in:
@@ -29,6 +29,7 @@ def get_blocks(
|
||||
return_type='dict',
|
||||
block_types_filter=None,
|
||||
hide_access_denials=False,
|
||||
allow_start_dates_in_future=False,
|
||||
):
|
||||
"""
|
||||
Return a serialized representation of the course blocks.
|
||||
@@ -58,6 +59,9 @@ def get_blocks(
|
||||
hide_access_denials (bool): When True, filter out any blocks that were
|
||||
denied access to the user, even if they have access denial messages
|
||||
attached.
|
||||
allow_start_dates_in_future (bool): When True, will allow blocks to be
|
||||
returned that can bypass the StartDateTransformer's filter to show
|
||||
blocks with start dates in the future.
|
||||
"""
|
||||
|
||||
if HIDE_ACCESS_DENIALS_FLAG.is_enabled():
|
||||
@@ -101,7 +105,8 @@ def get_blocks(
|
||||
transformers += [BlockCompletionTransformer()]
|
||||
|
||||
# transform
|
||||
blocks = course_blocks_api.get_course_blocks(user, usage_key, transformers)
|
||||
blocks = course_blocks_api.get_course_blocks(
|
||||
user, usage_key, transformers, allow_start_dates_in_future=allow_start_dates_in_future)
|
||||
|
||||
# filter blocks by types
|
||||
if block_types_filter:
|
||||
|
||||
@@ -42,7 +42,9 @@ SUPPORTED_FIELDS = [
|
||||
SupportedFieldType('display_name', default_value=''),
|
||||
SupportedFieldType('graded'),
|
||||
SupportedFieldType('format'),
|
||||
SupportedFieldType('start'),
|
||||
SupportedFieldType('due'),
|
||||
SupportedFieldType('contains_gated_content'),
|
||||
SupportedFieldType('has_score'),
|
||||
SupportedFieldType('weight'),
|
||||
SupportedFieldType('show_correctness'),
|
||||
|
||||
@@ -57,6 +57,7 @@ def get_course_blocks(
|
||||
starting_block_usage_key,
|
||||
transformers=None,
|
||||
collected_block_structure=None,
|
||||
allow_start_dates_in_future=False,
|
||||
):
|
||||
"""
|
||||
A higher order function implemented on top of the
|
||||
@@ -90,7 +91,7 @@ def get_course_blocks(
|
||||
"""
|
||||
if not transformers:
|
||||
transformers = BlockStructureTransformers(get_course_block_access_transformers(user))
|
||||
transformers.usage_info = CourseUsageInfo(starting_block_usage_key.course_key, user)
|
||||
transformers.usage_info = CourseUsageInfo(starting_block_usage_key.course_key, user, allow_start_dates_in_future)
|
||||
|
||||
return get_block_structure_manager(starting_block_usage_key.course_key).get_transformed(
|
||||
transformers,
|
||||
|
||||
@@ -71,7 +71,7 @@ class StartDateTransformer(FilteringTransformerMixin, BlockStructureTransformer)
|
||||
|
||||
def transform_block_filters(self, usage_info, block_structure):
|
||||
# Users with staff access bypass the Start Date check.
|
||||
if usage_info.has_staff_access:
|
||||
if usage_info.has_staff_access or usage_info.allow_start_dates_in_future:
|
||||
return [block_structure.create_universal_filter()]
|
||||
|
||||
removal_condition = lambda block_key: not check_start_date(
|
||||
|
||||
@@ -14,13 +14,19 @@ class CourseUsageInfo(object):
|
||||
an instance of it in calls to BlockStructureTransformer.transform
|
||||
methods.
|
||||
'''
|
||||
def __init__(self, course_key, user):
|
||||
def __init__(self, course_key, user, allow_start_dates_in_future=False):
|
||||
# Course identifier (opaque_keys.edx.keys.CourseKey)
|
||||
self.course_key = course_key
|
||||
|
||||
# User object (django.contrib.auth.models.User)
|
||||
self.user = user
|
||||
|
||||
# Sometimes we want to allow blocks to be returned that can bypass the
|
||||
# StartDateTransformer's filter to show blocks with start dates in the future.
|
||||
# One use case of this is for the Dates page where we want to display
|
||||
# assignments that have not yet been released.
|
||||
self.allow_start_dates_in_future = allow_start_dates_in_future
|
||||
|
||||
# Cached value of whether the user has staff access (bool/None)
|
||||
self._has_staff_access = None
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ courseware.
|
||||
|
||||
import logging
|
||||
from collections import defaultdict, namedtuple
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import pytz
|
||||
import six
|
||||
@@ -17,7 +17,6 @@ from django.http import Http404, QueryDict
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import ugettext as _
|
||||
from edx_django_utils.monitoring import function_trace
|
||||
from edx_when.api import get_dates_for_course
|
||||
from fs.errors import ResourceNotFound
|
||||
from opaque_keys.edx.keys import UsageKey
|
||||
from path import Path as path
|
||||
@@ -58,7 +57,8 @@ from openedx.core.djangoapps.enrollments.api import get_course_enrollment_detail
|
||||
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
|
||||
from openedx.core.lib.api.view_utils import LazySequence
|
||||
from openedx.features.course_duration_limits.access import AuditExpiredError
|
||||
from openedx.features.course_experience import COURSE_ENABLE_UNENROLLED_ACCESS_FLAG, RELATIVE_DATES_FLAG
|
||||
from openedx.features.course_experience import RELATIVE_DATES_FLAG
|
||||
from openedx.features.course_experience.utils import get_course_outline_block_tree
|
||||
from static_replace import replace_static_urls
|
||||
from student.models import CourseEnrollment
|
||||
from survey.utils import SurveyRequiredAccessError, check_survey_required_and_unanswered
|
||||
@@ -66,14 +66,14 @@ from util.date_utils import strftime_localized
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
from xmodule.x_module import STUDENT_VIEW
|
||||
import lms.djangoapps.course_blocks.api as course_blocks_api
|
||||
from openedx.features.content_type_gating.helpers import CONTENT_GATING_PARTITION_ID
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Used by get_course_assignments below. You shouldn't need to use this type directly.
|
||||
_Assignment = namedtuple('Assignment', ['block_key', 'title', 'url', 'date', 'requires_full_access'])
|
||||
_Assignment = namedtuple(
|
||||
'Assignment', ['block_key', 'title', 'url', 'date', 'contains_gated_content', 'complete', 'past_due']
|
||||
)
|
||||
|
||||
|
||||
def get_course(course_id, depth=0):
|
||||
@@ -500,7 +500,9 @@ def get_course_assignment_date_blocks(course, user, request, num_return=None,
|
||||
for assignment in get_course_assignments(course.id, user, request, include_access=include_access):
|
||||
date_block = CourseAssignmentDate(course, user)
|
||||
date_block.date = assignment.date
|
||||
date_block.requires_full_access = assignment.requires_full_access
|
||||
date_block.contains_gated_content = assignment.contains_gated_content
|
||||
date_block.complete = assignment.complete
|
||||
date_block.past_due = assignment.past_due
|
||||
date_block.set_title(assignment.title, link=assignment.url)
|
||||
date_blocks.append(date_block)
|
||||
date_blocks = sorted((b for b in date_blocks if b.is_enabled or include_past_dates), key=date_block_key_fn)
|
||||
@@ -513,52 +515,38 @@ def get_course_assignments(course_key, user, request, include_access=False):
|
||||
"""
|
||||
Returns a list of assignment (at the subsection/sequential level) due dates for the given course.
|
||||
|
||||
Each returned object is a namedtuple with fields: block_key, title, url, date, requires_full_access
|
||||
Each returned object is a namedtuple with fields: title, url, date, contains_gated_content, complete, past_due
|
||||
"""
|
||||
store = modulestore()
|
||||
all_course_dates = get_dates_for_course(course_key, user)
|
||||
block_data = course_blocks_api.get_course_blocks(user, store.make_course_usage_key(course_key))
|
||||
assignments = []
|
||||
for (block_key, date_type), date in all_course_dates.items():
|
||||
if date_type != 'due' or block_key.block_type != 'sequential':
|
||||
continue
|
||||
# Ideally this function is always called with a request being passed in, but because it is also
|
||||
# a subfunction of `get_course_date_blocks` which does not require a request, we are being defensive here.
|
||||
if not request:
|
||||
return assignments
|
||||
|
||||
if block_key not in block_data:
|
||||
continue
|
||||
now = datetime.now(pytz.UTC)
|
||||
course_root_block = get_course_outline_block_tree(request, str(course_key), user, allow_start_dates_in_future=True)
|
||||
for section in course_root_block.get('children', []):
|
||||
for subsection in section.get('children', []):
|
||||
if not subsection.get('due') or not subsection.get('graded'):
|
||||
continue
|
||||
|
||||
block = block_data[block_key]
|
||||
if not block.graded:
|
||||
continue
|
||||
contains_gated_content = include_access and subsection.get('contains_gated_content', False)
|
||||
title = subsection.get('display_name', _('Assignment'))
|
||||
|
||||
requires_full_access = include_access and _requires_full_access(block_data, block, user)
|
||||
title = block.display_name or _('Assignment')
|
||||
url = None
|
||||
assignment_released = not subsection.get('start') or subsection.get('start') < now
|
||||
if assignment_released:
|
||||
url = subsection.get('lms_web_url')
|
||||
|
||||
url = None
|
||||
assignment_released = not block.start or block.start < datetime.now(pytz.UTC)
|
||||
if assignment_released:
|
||||
url = reverse('jump_to', args=[course_key, block_key])
|
||||
url = request and request.build_absolute_uri(url)
|
||||
|
||||
assignments.append(_Assignment(block_key, title, url, date, requires_full_access))
|
||||
complete = subsection.get('complete')
|
||||
past_due = not complete and subsection.get('due', now + timedelta(1)) < now
|
||||
assignments.append(_Assignment(
|
||||
subsection.get('id'), title, url, subsection.get('due'), contains_gated_content, complete, past_due
|
||||
))
|
||||
|
||||
return assignments
|
||||
|
||||
|
||||
def _requires_full_access(block_data, block, user):
|
||||
"""
|
||||
Returns a boolean if any child of the block_key specified has a group_access array consisting of just full_access
|
||||
"""
|
||||
for child_block_key in block_data.get_children(block.location):
|
||||
group_access = block_data.get_xblock_field(child_block_key, 'group_access')
|
||||
# If group_access is set on the block, and the content gating is
|
||||
# only full access, set the value on the CourseAssignmentDate object
|
||||
if(group_access and group_access.get(CONTENT_GATING_PARTITION_ID) == [
|
||||
settings.CONTENT_TYPE_GATE_GROUP_IDS['full_access']
|
||||
]):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
# TODO: Fix this such that these are pulled in as extra course-specific tabs.
|
||||
# arjun will address this by the end of October if no one does so prior to
|
||||
# then.
|
||||
|
||||
@@ -346,7 +346,9 @@ class CourseAssignmentDate(DateSummary):
|
||||
self.assignment_date = None
|
||||
self.assignment_title = None
|
||||
self.assignment_title_html = None
|
||||
self.requires_full_access = None
|
||||
self.contains_gated_content = False
|
||||
self.complete = None
|
||||
self.past_due = None
|
||||
|
||||
@property
|
||||
def date(self):
|
||||
|
||||
@@ -158,7 +158,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
|
||||
assignment_title_html = ['<a href=', '</a>']
|
||||
with self.store.bulk_operations(course.id):
|
||||
section = ItemFactory.create(category='chapter', parent_location=course.location)
|
||||
subsection_1 = ItemFactory.create(
|
||||
ItemFactory.create(
|
||||
category='sequential',
|
||||
display_name='Released',
|
||||
parent_location=section.location,
|
||||
@@ -166,7 +166,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
|
||||
due=now + timedelta(days=6),
|
||||
graded=True,
|
||||
)
|
||||
subsection_2 = ItemFactory.create(
|
||||
ItemFactory.create(
|
||||
category='sequential',
|
||||
display_name='Not released',
|
||||
parent_location=section.location,
|
||||
@@ -174,7 +174,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
|
||||
due=now + timedelta(days=7),
|
||||
graded=True,
|
||||
)
|
||||
subsection_3 = ItemFactory.create(
|
||||
ItemFactory.create(
|
||||
category='sequential',
|
||||
display_name='Third nearest assignment',
|
||||
parent_location=section.location,
|
||||
@@ -182,7 +182,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
|
||||
due=now + timedelta(days=8),
|
||||
graded=True,
|
||||
)
|
||||
subsection_4 = ItemFactory.create(
|
||||
ItemFactory.create(
|
||||
category='sequential',
|
||||
display_name='Past due date',
|
||||
parent_location=section.location,
|
||||
@@ -190,7 +190,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
|
||||
due=now - timedelta(days=7),
|
||||
graded=True,
|
||||
)
|
||||
subsection_5 = ItemFactory.create(
|
||||
ItemFactory.create(
|
||||
category='sequential',
|
||||
display_name='Not returned since we do not get non-graded subsections',
|
||||
parent_location=section.location,
|
||||
@@ -198,7 +198,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
|
||||
due=now - timedelta(days=7),
|
||||
graded=False,
|
||||
)
|
||||
subsection_6 = ItemFactory.create(
|
||||
ItemFactory.create(
|
||||
category='sequential',
|
||||
display_name='No start date',
|
||||
parent_location=section.location,
|
||||
@@ -206,7 +206,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
|
||||
due=now + timedelta(days=9),
|
||||
graded=True,
|
||||
)
|
||||
subsection_7 = ItemFactory.create(
|
||||
ItemFactory.create(
|
||||
category='sequential',
|
||||
# Setting display name to None should set the assignment title to 'Assignment'
|
||||
display_name=None,
|
||||
@@ -222,85 +222,66 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
|
||||
with self.store.branch_setting(ModuleStoreEnum.Branch.draft_preferred, course.id):
|
||||
self.store.delete_item(dummy_subsection.location, user.id)
|
||||
|
||||
with patch('lms.djangoapps.courseware.courses.get_dates_for_course') as mock_get_dates:
|
||||
mock_get_dates.return_value = {
|
||||
(subsection_1.location, 'due'): subsection_1.due,
|
||||
(subsection_1.location, 'start'): subsection_1.start,
|
||||
(subsection_2.location, 'due'): subsection_2.due,
|
||||
(subsection_2.location, 'start'): subsection_2.start,
|
||||
(subsection_3.location, 'due'): subsection_3.due,
|
||||
(subsection_3.location, 'start'): subsection_3.start,
|
||||
(subsection_4.location, 'due'): subsection_4.due,
|
||||
(subsection_4.location, 'start'): subsection_4.start,
|
||||
(subsection_5.location, 'due'): subsection_5.due,
|
||||
(subsection_5.location, 'start'): subsection_5.start,
|
||||
(subsection_6.location, 'due'): subsection_6.due,
|
||||
(subsection_7.location, 'due'): subsection_7.due,
|
||||
(subsection_7.location, 'start'): subsection_7.start,
|
||||
# Adding this in for the case where we return a block that
|
||||
# doesn't actually exist in the modulestore. Should just be ignored.
|
||||
(dummy_subsection.location, 'due'): dummy_subsection.due,
|
||||
}
|
||||
# Standard widget case where we restrict the number of assignments.
|
||||
expected_blocks = (
|
||||
TodaysDate, CourseAssignmentDate, CourseAssignmentDate, CourseEndDate, VerificationDeadlineDate
|
||||
)
|
||||
blocks = get_course_date_blocks(course, user, request, num_assignments=2)
|
||||
self.assertEqual(len(blocks), len(expected_blocks))
|
||||
self.assertEqual(set(type(b) for b in blocks), set(expected_blocks))
|
||||
assignment_blocks = filter(lambda b: isinstance(b, CourseAssignmentDate), blocks)
|
||||
for assignment in assignment_blocks:
|
||||
assignment_title = str(assignment.title_html) or str(assignment.title)
|
||||
self.assertNotEqual(assignment_title, 'Third nearest assignment')
|
||||
self.assertNotEqual(assignment_title, 'Past due date')
|
||||
self.assertNotEqual(assignment_title, 'Not returned since we do not get non-graded subsections')
|
||||
# checking if it is _in_ the title instead of being the title since released assignments
|
||||
# are actually links. Unreleased assignments are just the string of the title.
|
||||
if 'Released' in assignment_title:
|
||||
for html_tag in assignment_title_html:
|
||||
self.assertIn(html_tag, assignment_title)
|
||||
elif assignment_title == 'Not released':
|
||||
for html_tag in assignment_title_html:
|
||||
self.assertNotIn(html_tag, assignment_title)
|
||||
# Standard widget case where we restrict the number of assignments.
|
||||
expected_blocks = (
|
||||
TodaysDate, CourseAssignmentDate, CourseAssignmentDate, CourseEndDate, VerificationDeadlineDate
|
||||
)
|
||||
blocks = get_course_date_blocks(course, user, request, num_assignments=2)
|
||||
self.assertEqual(len(blocks), len(expected_blocks))
|
||||
self.assertEqual(set(type(b) for b in blocks), set(expected_blocks))
|
||||
assignment_blocks = filter(lambda b: isinstance(b, CourseAssignmentDate), blocks)
|
||||
for assignment in assignment_blocks:
|
||||
assignment_title = str(assignment.title_html) or str(assignment.title)
|
||||
self.assertNotEqual(assignment_title, 'Third nearest assignment')
|
||||
self.assertNotEqual(assignment_title, 'Past due date')
|
||||
self.assertNotEqual(assignment_title, 'Not returned since we do not get non-graded subsections')
|
||||
# checking if it is _in_ the title instead of being the title since released assignments
|
||||
# are actually links. Unreleased assignments are just the string of the title.
|
||||
if 'Released' in assignment_title:
|
||||
for html_tag in assignment_title_html:
|
||||
self.assertIn(html_tag, assignment_title)
|
||||
elif assignment_title == 'Not released':
|
||||
for html_tag in assignment_title_html:
|
||||
self.assertNotIn(html_tag, assignment_title)
|
||||
|
||||
# No restrictions on number of assignments to return
|
||||
expected_blocks = (
|
||||
CourseStartDate, TodaysDate, CourseAssignmentDate, CourseAssignmentDate, CourseAssignmentDate,
|
||||
CourseAssignmentDate, CourseAssignmentDate, CourseAssignmentDate, CourseEndDate,
|
||||
VerificationDeadlineDate
|
||||
)
|
||||
blocks = get_course_date_blocks(course, user, request, include_past_dates=True)
|
||||
self.assertEqual(len(blocks), len(expected_blocks))
|
||||
self.assertEqual(set(type(b) for b in blocks), set(expected_blocks))
|
||||
assignment_blocks = filter(lambda b: isinstance(b, CourseAssignmentDate), blocks)
|
||||
for assignment in assignment_blocks:
|
||||
assignment_title = str(assignment.title_html) or str(assignment.title)
|
||||
self.assertNotEqual(assignment_title, 'Not returned since we do not get non-graded subsections')
|
||||
# checking if it is _in_ the title instead of being the title since released assignments
|
||||
# are actually links. Unreleased assignments are just the string of the title.
|
||||
if 'Released' in assignment_title:
|
||||
for html_tag in assignment_title_html:
|
||||
self.assertIn(html_tag, assignment_title)
|
||||
elif assignment_title == 'Not released':
|
||||
for html_tag in assignment_title_html:
|
||||
self.assertNotIn(html_tag, assignment_title)
|
||||
elif assignment_title == 'Third nearest assignment':
|
||||
# It's still not released
|
||||
for html_tag in assignment_title_html:
|
||||
self.assertNotIn(html_tag, assignment_title)
|
||||
elif 'Past due date' in assignment_title:
|
||||
self.assertGreater(now, assignment.date)
|
||||
for html_tag in assignment_title_html:
|
||||
self.assertIn(html_tag, assignment_title)
|
||||
elif 'No start date' == assignment_title:
|
||||
# Can't determine if it is released so it does not get a link
|
||||
for html_tag in assignment_title_html:
|
||||
self.assertNotIn(html_tag, assignment_title)
|
||||
# This is the item with no display name where we set one ourselves.
|
||||
elif 'Assignment' in assignment_title:
|
||||
# Can't determine if it is released so it does not get a link
|
||||
for html_tag in assignment_title_html:
|
||||
self.assertIn(html_tag, assignment_title)
|
||||
# No restrictions on number of assignments to return
|
||||
expected_blocks = (
|
||||
CourseStartDate, TodaysDate, CourseAssignmentDate, CourseAssignmentDate, CourseAssignmentDate,
|
||||
CourseAssignmentDate, CourseAssignmentDate, CourseAssignmentDate, CourseEndDate,
|
||||
VerificationDeadlineDate
|
||||
)
|
||||
blocks = get_course_date_blocks(course, user, request, include_past_dates=True)
|
||||
self.assertEqual(len(blocks), len(expected_blocks))
|
||||
self.assertEqual(set(type(b) for b in blocks), set(expected_blocks))
|
||||
assignment_blocks = filter(lambda b: isinstance(b, CourseAssignmentDate), blocks)
|
||||
for assignment in assignment_blocks:
|
||||
assignment_title = str(assignment.title_html) or str(assignment.title)
|
||||
self.assertNotEqual(assignment_title, 'Not returned since we do not get non-graded subsections')
|
||||
# checking if it is _in_ the title instead of being the title since released assignments
|
||||
# are actually links. Unreleased assignments are just the string of the title.
|
||||
if 'Released' in assignment_title:
|
||||
for html_tag in assignment_title_html:
|
||||
self.assertIn(html_tag, assignment_title)
|
||||
elif assignment_title == 'Not released':
|
||||
for html_tag in assignment_title_html:
|
||||
self.assertNotIn(html_tag, assignment_title)
|
||||
elif assignment_title == 'Third nearest assignment':
|
||||
# It's still not released
|
||||
for html_tag in assignment_title_html:
|
||||
self.assertNotIn(html_tag, assignment_title)
|
||||
elif 'Past due date' in assignment_title:
|
||||
self.assertGreater(now, assignment.date)
|
||||
for html_tag in assignment_title_html:
|
||||
self.assertIn(html_tag, assignment_title)
|
||||
elif 'No start date' == assignment_title:
|
||||
# Can't determine if it is released so it does not get a link
|
||||
for html_tag in assignment_title_html:
|
||||
self.assertNotIn(html_tag, assignment_title)
|
||||
# This is the item with no display name where we set one ourselves.
|
||||
elif 'Assignment' in assignment_title:
|
||||
# Can't determine if it is released so it does not get a link
|
||||
for html_tag in assignment_title_html:
|
||||
self.assertIn(html_tag, assignment_title)
|
||||
|
||||
@RELATIVE_DATES_FLAG.override(active=True)
|
||||
def test_enabled_block_types_with_expired_course(self):
|
||||
|
||||
@@ -3187,55 +3187,47 @@ class DatesTabTestCase(ModuleStoreTestCase):
|
||||
display_name='Released',
|
||||
parent_location=section.location,
|
||||
start=now - timedelta(days=1),
|
||||
due=now, # Setting this today so it'll show the 'Due Today' pill
|
||||
due=now + timedelta(days=1), # Setting this to tomorrow so it'll show the 'Due Next' pill
|
||||
graded=True,
|
||||
)
|
||||
|
||||
with patch('lms.djangoapps.courseware.courses.get_dates_for_course') as mock_get_dates:
|
||||
with patch('lms.djangoapps.courseware.views.views.get_enrollment') as mock_get_enrollment:
|
||||
mock_get_dates.return_value = {
|
||||
(subsection.location, 'due'): subsection.due,
|
||||
(subsection.location, 'start'): subsection.start,
|
||||
}
|
||||
mock_get_enrollment.return_value = {
|
||||
'mode': enrollment.mode
|
||||
}
|
||||
response = self._get_response(self.course)
|
||||
self.assertContains(response, subsection.display_name)
|
||||
# Show the Verification Deadline for everyone
|
||||
self.assertContains(response, 'Verification Deadline')
|
||||
# Make sure pill exists for assignment due today
|
||||
self.assertContains(response, '<div class="pill due">')
|
||||
# No pills for verified enrollments
|
||||
self.assertNotContains(response, '<div class="pill verified">')
|
||||
with patch('lms.djangoapps.courseware.views.views.get_enrollment') as mock_get_enrollment:
|
||||
mock_get_enrollment.return_value = {
|
||||
'mode': enrollment.mode
|
||||
}
|
||||
response = self._get_response(self.course)
|
||||
self.assertContains(response, subsection.display_name)
|
||||
# Show the Verification Deadline for everyone
|
||||
self.assertContains(response, 'Verification Deadline')
|
||||
# Make sure pill exists for today's date
|
||||
self.assertContains(response, '<div class="pill today">')
|
||||
# Make sure pill exists for next due assignment
|
||||
self.assertContains(response, '<div class="pill due-next">')
|
||||
# No pills for verified enrollments
|
||||
self.assertNotContains(response, '<div class="pill verified">')
|
||||
|
||||
enrollment.delete()
|
||||
subsection.due = now + timedelta(days=1)
|
||||
enrollment = CourseEnrollmentFactory(course_id=self.course.id, user=self.user, mode=CourseMode.AUDIT)
|
||||
mock_get_dates.return_value = {
|
||||
(subsection.location, 'due'): subsection.due,
|
||||
(subsection.location, 'start'): subsection.start,
|
||||
}
|
||||
mock_get_enrollment.return_value = {
|
||||
'mode': enrollment.mode
|
||||
}
|
||||
enrollment.delete()
|
||||
enrollment = CourseEnrollmentFactory(course_id=self.course.id, user=self.user, mode=CourseMode.AUDIT)
|
||||
mock_get_enrollment.return_value = {
|
||||
'mode': enrollment.mode
|
||||
}
|
||||
|
||||
expected_calls = [
|
||||
call('course_id', text_type(self.course.id)),
|
||||
call('user_id', self.user.id),
|
||||
call('is_staff', self.user.is_staff),
|
||||
]
|
||||
expected_calls = [
|
||||
call('course_id', text_type(self.course.id)),
|
||||
call('user_id', self.user.id),
|
||||
call('is_staff', self.user.is_staff),
|
||||
]
|
||||
|
||||
response = self._get_response(self.course)
|
||||
response = self._get_response(self.course)
|
||||
|
||||
mock_set_custom_metric.assert_has_calls(expected_calls, any_order=True)
|
||||
self.assertContains(response, subsection.display_name)
|
||||
# Show the Verification Deadline for everyone
|
||||
self.assertContains(response, 'Verification Deadline')
|
||||
# Pill doesn't exist for assignment due tomorrow
|
||||
self.assertNotContains(response, '<div class="pill due">')
|
||||
# Should have verified pills for audit enrollments
|
||||
self.assertContains(response, '<div class="pill verified">')
|
||||
mock_set_custom_metric.assert_has_calls(expected_calls, any_order=True)
|
||||
self.assertContains(response, subsection.display_name)
|
||||
# Show the Verification Deadline for everyone
|
||||
self.assertContains(response, 'Verification Deadline')
|
||||
# Pill doesn't exist for assignment due tomorrow
|
||||
self.assertNotContains(response, '<div class="pill due-next">')
|
||||
# Should have verified pills for audit enrollments
|
||||
self.assertContains(response, '<div class="pill verified">')
|
||||
|
||||
|
||||
class TestShowCoursewareMFE(TestCase):
|
||||
|
||||
@@ -1071,20 +1071,20 @@ def dates(request, course_id):
|
||||
course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=False)
|
||||
course_date_blocks = get_course_date_blocks(course, request.user, request,
|
||||
include_access=True, include_past_dates=True)
|
||||
enrollment = get_enrollment(request.user.username, course_id)
|
||||
|
||||
learner_is_verified = False
|
||||
enrollment = get_enrollment(request.user.username, course_id)
|
||||
if enrollment:
|
||||
learner_is_verified = enrollment.get('mode') == 'verified'
|
||||
|
||||
# User locale settings
|
||||
user_timezone_locale = user_timezone_locale_prefs(request)
|
||||
user_timezone = user_timezone_locale['user_timezone']
|
||||
user_language = user_timezone_locale['user_language']
|
||||
|
||||
if enrollment:
|
||||
learner_is_verified = enrollment.get('mode') == 'verified'
|
||||
|
||||
context = {
|
||||
'course': course,
|
||||
'course_date_blocks': [block for block in course_date_blocks if block.title != 'current_datetime'],
|
||||
'course_date_blocks': course_date_blocks,
|
||||
'verified_upgrade_link': verified_upgrade_deadline_link(request.user, course=course),
|
||||
'learner_is_verified': learner_is_verified,
|
||||
'user_timezone': user_timezone,
|
||||
|
||||
@@ -59,15 +59,25 @@
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
position: absolute;
|
||||
left: -4px;
|
||||
left: -5px;
|
||||
background-color: #2d323e;
|
||||
border-radius: 50%;
|
||||
border-style: solid;
|
||||
border-width: thin;
|
||||
|
||||
&.active {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
left: -7px;
|
||||
background-color: #2d323e;
|
||||
&.past-date {
|
||||
background-color: #f5f5f5;
|
||||
|
||||
&.past-due {
|
||||
background-color: #d1d2d4;
|
||||
}
|
||||
}
|
||||
|
||||
&.todays-date {
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
left: -8px;
|
||||
background-color: #ffdb87;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,7 +89,7 @@
|
||||
flex: 100%;
|
||||
line-height: 1.25;
|
||||
|
||||
&.active {
|
||||
&.todays-date {
|
||||
top: -3px;
|
||||
}
|
||||
}
|
||||
@@ -99,6 +109,10 @@
|
||||
font-weight: bold;
|
||||
margin-bottom: 8px;
|
||||
align-items: center;
|
||||
|
||||
&.not-released {
|
||||
color: #767676;
|
||||
}
|
||||
}
|
||||
|
||||
.timeline-title {
|
||||
@@ -114,6 +128,10 @@
|
||||
color: #2d323e;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
&.not-released {
|
||||
color: #d1d2d4;
|
||||
}
|
||||
}
|
||||
|
||||
.timeline-description {
|
||||
@@ -139,7 +157,29 @@
|
||||
font-weight: bold;
|
||||
vertical-align: top;
|
||||
|
||||
&.due {
|
||||
&.completed {
|
||||
background-color: #f3f3f4;
|
||||
color: #2d323e;
|
||||
}
|
||||
|
||||
&.due-next {
|
||||
background-color: #686b73;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
&.not-released {
|
||||
background-color: $white;
|
||||
border-color: #d1d2d4;
|
||||
border-style: solid;
|
||||
border-width: thin;
|
||||
color: #767676;
|
||||
}
|
||||
|
||||
&.past-due {
|
||||
background-color: #d1d2d4;
|
||||
}
|
||||
|
||||
&.today {
|
||||
background-color: #ffdb87;
|
||||
color: #2d323e;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<%!
|
||||
|
||||
from django.utils.translation import ugettext as _
|
||||
from lms.djangoapps.courseware.date_summary import CourseAssignmentDate, VerificationDeadlineDate, VerifiedUpgradeDeadlineDate
|
||||
from lms.djangoapps.courseware.date_summary import CourseAssignmentDate, TodaysDate, VerificationDeadlineDate, VerifiedUpgradeDeadlineDate
|
||||
from openedx.core.djangolib.markup import HTML, Text
|
||||
%>
|
||||
|
||||
@@ -26,7 +26,7 @@ from openedx.core.djangolib.markup import HTML, Text
|
||||
<h2 class="hd hd-2 date-title">
|
||||
${_("Important Dates")}
|
||||
</h2>
|
||||
<% has_locked_assignments = any(hasattr(block, 'requires_full_access') and block.requires_full_access for block in course_date_blocks if isinstance(block, CourseAssignmentDate)) %>
|
||||
<% has_locked_assignments = any(hasattr(block, 'contains_gated_content') and block.contains_gated_content for block in course_date_blocks if isinstance(block, CourseAssignmentDate)) %>
|
||||
% if has_locked_assignments and verified_upgrade_link:
|
||||
<div class="upgrade-banner">
|
||||
<div class="upgrade-banner-text">
|
||||
@@ -44,39 +44,58 @@ from openedx.core.djangolib.markup import HTML, Text
|
||||
</div>
|
||||
% endif
|
||||
|
||||
<% due_next_set = False %>
|
||||
% for block in course_date_blocks:
|
||||
<% active = 'active' if block.date and (block.date.strftime(block.date_format) == block.current_time.strftime(block.date_format)) else '' %>
|
||||
<% block_is_verified = (hasattr(block, 'requires_full_access') and block.requires_full_access) or isinstance(block, VerificationDeadlineDate) %>
|
||||
<% block_is_verified = (hasattr(block, 'contains_gated_content') and block.contains_gated_content) or isinstance(block, VerificationDeadlineDate) %>
|
||||
<% learner_has_access = not block_is_verified or learner_is_verified %>
|
||||
<% access_class = '' if learner_has_access else 'no-access' %>
|
||||
<% is_assignment = isinstance(block, CourseAssignmentDate) %>
|
||||
<% todays_date = 'todays-date' if isinstance(block, TodaysDate) else '' %>
|
||||
<% past_date = 'past-date' if block.date and block.date < block.current_time else '' %>
|
||||
<% past_due = 'past-due' if learner_is_verified and is_assignment and block.past_due else '' %>
|
||||
<% due_in_future = True if learner_is_verified and is_assignment and block.date and block.date >= block.current_time else False %>
|
||||
<% not_released = 'not-released' if learner_is_verified and is_assignment and not block.title_html else '' %>
|
||||
% if not (learner_is_verified and isinstance(block, VerifiedUpgradeDeadlineDate)):
|
||||
<div class="timeline-item ${active}">
|
||||
<div class="date-circle ${active}"></div>
|
||||
<div class="date-content ${active}">
|
||||
<div class="timeline-date-content">
|
||||
% if block.date:
|
||||
<div class="timeline-date">
|
||||
<div class="course-date localized_datetime" aria-hidden="true" data-format="shortDate" data-datetime="${block.date}" data-language="${user_language}" data-timezone="${user_timezone}"></div>
|
||||
</div>
|
||||
% if active:
|
||||
<div class="pill due">${_('Due Today')}</div>
|
||||
% endif
|
||||
% if not learner_has_access:
|
||||
<div class="pill verified"><span class="fa fa-lock verified-icon" aria-hidden="true"></span>${_('Verified Only')}</div>
|
||||
% endif
|
||||
<div class="timeline-item">
|
||||
<div class="date-circle ${todays_date} ${past_date} ${past_due}"></div>
|
||||
<div class="date-content ${todays_date}">
|
||||
<div class="timeline-date-content ${not_released}">
|
||||
% if block.date:
|
||||
<div class="timeline-date">
|
||||
<div class="course-date localized_datetime" aria-hidden="true" data-format="shortDate" data-datetime="${block.date}" data-language="${user_language}" data-timezone="${user_timezone}"></div>
|
||||
</div>
|
||||
% if todays_date:
|
||||
<div class="pill today">${_('Today')}</div>
|
||||
% endif
|
||||
</div>
|
||||
<div class="timeline-title ${access_class}">
|
||||
% if block.title_html and is_assignment and learner_has_access:
|
||||
${block.title_html}
|
||||
% else:
|
||||
${block.title}
|
||||
% if not learner_has_access:
|
||||
<div class="pill verified"><span class="fa fa-lock verified-icon" aria-hidden="true"></span>${_('Verified Only')}</div>
|
||||
% else:
|
||||
% if is_assignment and block.complete:
|
||||
<div class="pill completed">${_('Completed')}</div>
|
||||
% elif is_assignment and block.past_due:
|
||||
<div class="pill past-due">${_('Past Due')}</div>
|
||||
% elif is_assignment and due_in_future and not due_next_set:
|
||||
<div class="pill due-next">${_('Due Next')}</div>
|
||||
<% due_next_set = True %>
|
||||
% endif
|
||||
% if not_released:
|
||||
<div class="pill not-released">${_('Not yet released')}</div>
|
||||
% endif
|
||||
%endif
|
||||
% endif
|
||||
</div>
|
||||
<div class="timeline-description ${access_class}">
|
||||
${block.description}
|
||||
</div>
|
||||
% if not todays_date:
|
||||
<div class="timeline-title ${access_class} ${not_released}">
|
||||
% if block.title_html and is_assignment and learner_has_access:
|
||||
${block.title_html}
|
||||
% else:
|
||||
${block.title}
|
||||
% endif
|
||||
</div>
|
||||
<div class="timeline-description ${access_class} ${not_released}">
|
||||
${block.description}
|
||||
</div>
|
||||
% endif
|
||||
</div>
|
||||
</div>
|
||||
% endif
|
||||
|
||||
@@ -31,9 +31,12 @@ class TestIcsGeneration(TestCase):
|
||||
self.request.site = SiteFactory()
|
||||
self.request.user = self.user
|
||||
|
||||
def make_assigment(self, block_key=None, title=None, url=None, date=None, requires_file_access=False):
|
||||
def make_assigment(
|
||||
self, block_key=None, title=None, url=None, date=None, contains_gated_content=False, complete=False,
|
||||
past_due=False
|
||||
):
|
||||
""" Bundles given info into a namedtupled like get_course_assignments returns """
|
||||
return _Assignment(block_key, title, url, date, requires_file_access)
|
||||
return _Assignment(block_key, title, url, date, contains_gated_content, complete, past_due)
|
||||
|
||||
def expected_ics(self, *assignments):
|
||||
""" Returns hardcoded expected ics strings for given assignments """
|
||||
|
||||
@@ -35,6 +35,18 @@ class ContentTypeGateTransformer(BlockStructureTransformer):
|
||||
"""
|
||||
block_structure.request_xblock_fields('group_access', 'graded', 'has_score', 'weight')
|
||||
|
||||
def _set_contains_gated_content_on_parents(self, block_structure, block_key):
|
||||
"""
|
||||
This will recursively set a field on all the parents of a block if one of the problems
|
||||
inside of it is content gated. `contains_gated_content` can then be used to indicate something
|
||||
in the blocks subtree is gated.
|
||||
"""
|
||||
for parent_block_key in block_structure.get_parents(block_key):
|
||||
if block_structure.get_xblock_field(parent_block_key, 'contains_gated_content'):
|
||||
continue
|
||||
block_structure.override_xblock_field(parent_block_key, 'contains_gated_content', True)
|
||||
self._set_contains_gated_content_on_parents(block_structure, parent_block_key)
|
||||
|
||||
def transform(self, usage_info, block_structure):
|
||||
if not ContentTypeGatingConfig.enabled_for_enrollment(
|
||||
user=usage_info.user,
|
||||
@@ -56,3 +68,4 @@ class ContentTypeGateTransformer(BlockStructureTransformer):
|
||||
[settings.CONTENT_TYPE_GATE_GROUP_IDS['full_access']]
|
||||
)
|
||||
block_structure.override_xblock_field(block_key, 'group_access', current_access)
|
||||
self._set_contains_gated_content_on_parents(block_structure, block_key)
|
||||
|
||||
@@ -28,9 +28,13 @@ log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@request_cached()
|
||||
def get_course_outline_block_tree(request, course_id, user=None):
|
||||
def get_course_outline_block_tree(request, course_id, user=None, allow_start_dates_in_future=False):
|
||||
"""
|
||||
Returns the root block of the course outline, with children as blocks.
|
||||
|
||||
allow_start_dates_in_future (bool): When True, will allow blocks to be
|
||||
returned that can bypass the StartDateTransformer's filter to show
|
||||
blocks with start dates in the future.
|
||||
"""
|
||||
|
||||
assert user is None or user.is_authenticated
|
||||
@@ -208,6 +212,8 @@ def get_course_outline_block_tree(request, course_id, user=None):
|
||||
'children',
|
||||
'display_name',
|
||||
'type',
|
||||
'start',
|
||||
'contains_gated_content',
|
||||
'due',
|
||||
'graded',
|
||||
'has_score',
|
||||
@@ -216,7 +222,8 @@ def get_course_outline_block_tree(request, course_id, user=None):
|
||||
'show_gated_sections',
|
||||
'format'
|
||||
],
|
||||
block_types_filter=block_types_filter
|
||||
block_types_filter=block_types_filter,
|
||||
allow_start_dates_in_future=allow_start_dates_in_future,
|
||||
)
|
||||
|
||||
course_outline_root_block = all_blocks['blocks'].get(all_blocks['root'], None)
|
||||
|
||||
@@ -74,7 +74,7 @@ class CourseHomeMessageFragmentView(EdxFragmentView):
|
||||
_register_course_home_messages(request, course, user_access, course_start_data)
|
||||
|
||||
# Register course date alerts
|
||||
for course_date_block in get_course_date_blocks(course, request.user):
|
||||
for course_date_block in get_course_date_blocks(course, request.user, request):
|
||||
course_date_block.register_alerts(request, course)
|
||||
|
||||
# Register a course goal message, if appropriate
|
||||
|
||||
@@ -2,6 +2,6 @@
|
||||
set -e
|
||||
|
||||
export LOWER_PYLINT_THRESHOLD=1000
|
||||
export UPPER_PYLINT_THRESHOLD=3310
|
||||
export UPPER_PYLINT_THRESHOLD=3300
|
||||
export ESLINT_THRESHOLD=5530
|
||||
export STYLELINT_THRESHOLD=880
|
||||
|
||||
Reference in New Issue
Block a user