Displays release date of unit in sidebar
This commit is contained in:
@@ -9,8 +9,9 @@ from django.test import TestCase
|
||||
from django.test.utils import override_settings
|
||||
|
||||
from contentstore import utils
|
||||
from contentstore.tests.utils import CourseTestCase
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
|
||||
from opaque_keys.edx.locations import SlashSeparatedCourseKey, Location
|
||||
|
||||
from xmodule.modulestore.django import modulestore
|
||||
@@ -260,3 +261,57 @@ class XBlockVisibilityTestCase(TestCase):
|
||||
modulestore().publish(location, self.dummy_user)
|
||||
|
||||
return vertical
|
||||
|
||||
|
||||
class ReleaseDateSourceTest(CourseTestCase):
|
||||
"""Tests for finding the source of an xblock's release date."""
|
||||
|
||||
def setUp(self):
|
||||
super(ReleaseDateSourceTest, self).setUp()
|
||||
|
||||
self.chapter = ItemFactory.create(category='chapter', parent_location=self.course.location)
|
||||
self.sequential = ItemFactory.create(category='sequential', parent_location=self.chapter.location)
|
||||
self.vertical = ItemFactory.create(category='vertical', parent_location=self.sequential.location)
|
||||
|
||||
# Read again so that children lists are accurate
|
||||
self.chapter = self.store.get_item(self.chapter.location)
|
||||
self.sequential = self.store.get_item(self.sequential.location)
|
||||
self.vertical = self.store.get_item(self.vertical.location)
|
||||
|
||||
self.date_one = datetime(1980, 1, 1, tzinfo=UTC)
|
||||
self.date_two = datetime(2020, 1, 1, tzinfo=UTC)
|
||||
|
||||
def _update_release_dates(self, chapter_start, sequential_start, vertical_start):
|
||||
"""Sets the release dates of the chapter, sequential, and vertical"""
|
||||
self.chapter.start = chapter_start
|
||||
self.chapter = self.store.update_item(self.chapter, ModuleStoreEnum.UserID.test)
|
||||
self.sequential.start = sequential_start
|
||||
self.sequential = self.store.update_item(self.sequential, ModuleStoreEnum.UserID.test)
|
||||
self.vertical.start = vertical_start
|
||||
self.vertical = self.store.update_item(self.vertical, ModuleStoreEnum.UserID.test)
|
||||
|
||||
def _verify_release_date_source(self, item, expected_source):
|
||||
"""Helper to verify that the release date source of a given item matches the expected source"""
|
||||
source = utils.find_release_date_source(item)
|
||||
self.assertEqual(source.location, expected_source.location)
|
||||
self.assertEqual(source.start, expected_source.start)
|
||||
|
||||
def test_chapter_source_for_vertical(self):
|
||||
"""Tests a vertical's release date being set by its chapter"""
|
||||
self._update_release_dates(self.date_one, self.date_one, self.date_one)
|
||||
self._verify_release_date_source(self.vertical, self.chapter)
|
||||
|
||||
def test_sequential_source_for_vertical(self):
|
||||
"""Tests a vertical's release date being set by its sequential"""
|
||||
self._update_release_dates(self.date_one, self.date_two, self.date_two)
|
||||
self._verify_release_date_source(self.vertical, self.sequential)
|
||||
|
||||
def test_chapter_source_for_sequential(self):
|
||||
"""Tests a sequential's release date being set by its chapter"""
|
||||
self._update_release_dates(self.date_one, self.date_one, self.date_one)
|
||||
self._verify_release_date_source(self.sequential, self.chapter)
|
||||
|
||||
def test_sequential_source_for_sequential(self):
|
||||
"""Tests a sequential's release date being set by itself"""
|
||||
self._update_release_dates(self.date_one, self.date_two, self.date_two)
|
||||
self._verify_release_date_source(self.sequential, self.sequential)
|
||||
|
||||
@@ -187,6 +187,28 @@ def is_xblock_visible_to_students(xblock):
|
||||
return True
|
||||
|
||||
|
||||
def find_release_date_source(xblock):
|
||||
"""
|
||||
Finds the ancestor of xblock that set its release date.
|
||||
"""
|
||||
|
||||
# Stop searching at the section level
|
||||
if xblock.category == 'chapter':
|
||||
return xblock
|
||||
|
||||
parent_location = modulestore().get_parent_location(xblock.location,
|
||||
revision=ModuleStoreEnum.RevisionOption.draft_preferred)
|
||||
# Orphaned xblocks set their own release date
|
||||
if not parent_location:
|
||||
return xblock
|
||||
|
||||
parent = modulestore().get_item(parent_location)
|
||||
if parent.start != xblock.start:
|
||||
return xblock
|
||||
else:
|
||||
return find_release_date_source(parent)
|
||||
|
||||
|
||||
def add_extra_panel_tab(tab_type, course):
|
||||
"""
|
||||
Used to add the panel tab to a course if it does not exist.
|
||||
|
||||
@@ -4,6 +4,8 @@ from __future__ import absolute_import
|
||||
import hashlib
|
||||
import logging
|
||||
from uuid import uuid4
|
||||
from datetime import datetime
|
||||
from pytz import UTC
|
||||
|
||||
from collections import OrderedDict
|
||||
from functools import partial
|
||||
@@ -26,6 +28,9 @@ from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError
|
||||
from xmodule.modulestore.inheritance import own_metadata
|
||||
from xmodule.x_module import PREVIEW_VIEWS, STUDIO_VIEW, STUDENT_VIEW
|
||||
|
||||
from xmodule.course_module import DEFAULT_START_DATE
|
||||
from contentstore.utils import find_release_date_source
|
||||
from django.contrib.auth.models import User
|
||||
from util.date_utils import get_default_time_display
|
||||
|
||||
@@ -591,6 +596,9 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F
|
||||
"""
|
||||
published = modulestore().has_item(xblock.location, revision=ModuleStoreEnum.RevisionOption.published_only)
|
||||
|
||||
# Treat DEFAULT_START_DATE as a magic number that means the release date has not been set
|
||||
release_date = get_default_time_display(xblock.start) if xblock.start != DEFAULT_START_DATE else None
|
||||
|
||||
def safe_get_username(user_id):
|
||||
"""
|
||||
Guard against bad user_ids, like the infamous "**replace_user**".
|
||||
@@ -619,6 +627,9 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F
|
||||
"published_on": get_default_time_display(xblock.published_date) if xblock.published_date else None,
|
||||
"published_by": safe_get_username(xblock.published_by),
|
||||
'studio_url': xblock_studio_url(xblock),
|
||||
"released_to_students": datetime.now(UTC) > xblock.start,
|
||||
"release_date": release_date,
|
||||
"release_date_from": _get_release_date_from(xblock) if release_date else None,
|
||||
}
|
||||
if data is not None:
|
||||
xblock_info["data"] = data
|
||||
@@ -677,3 +688,15 @@ def _create_xblock_child_info(xblock, include_children_predicate=NEVER):
|
||||
) for child in xblock.get_children()
|
||||
]
|
||||
return child_info
|
||||
|
||||
|
||||
def _get_release_date_from(xblock):
|
||||
"""
|
||||
Returns a string representation of the section or subsection that sets the xblock's release date
|
||||
"""
|
||||
source = find_release_date_source(xblock)
|
||||
# Translators: this will be a part of the release date message.
|
||||
# For example, 'Released: Jul 02, 2014 at 4:00 UTC with Section "Week 1"'
|
||||
return _('{section_or_subsection} "{display_name}"').format(
|
||||
section_or_subsection=xblock_type_display_name(source),
|
||||
display_name=source.display_name_with_default)
|
||||
|
||||
@@ -30,11 +30,6 @@ define(["backbone", "underscore", "js/utils/module"], function(Backbone, _, Modu
|
||||
* 2) Edits have been made to the xblock since the last published version.
|
||||
*/
|
||||
"has_changes": null,
|
||||
/**
|
||||
* True iff a published version of the xblock exists with a release date in the past,
|
||||
* and the xblock is not locked.
|
||||
*/
|
||||
"released_to_students": null,
|
||||
/**
|
||||
* True iff a published version of the xblock exists.
|
||||
*/
|
||||
@@ -60,13 +55,19 @@ define(["backbone", "underscore", "js/utils/module"], function(Backbone, _, Modu
|
||||
* User who last published the xblock, or null if never published.
|
||||
*/
|
||||
"published_by": null,
|
||||
/**
|
||||
* True iff the release date of the xblock is in the past.
|
||||
*/
|
||||
"released_to_students": null,
|
||||
/**
|
||||
* If the xblock is published, the date on which it will be released to students.
|
||||
* This can be null if the release date is unscheduled.
|
||||
*/
|
||||
"release_date": null,
|
||||
/**
|
||||
* The xblock which is determining the release date. For instance, for a unit,
|
||||
* this will either be the parent subsection or the grandparent section.
|
||||
* This can be null if the release date is unscheduled.
|
||||
*/
|
||||
"release_date_from":null
|
||||
},
|
||||
|
||||
@@ -102,6 +102,8 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
|
||||
publishButtonCss = ".action-publish",
|
||||
discardChangesButtonCss = ".action-discard",
|
||||
lastDraftCss = ".wrapper-last-draft",
|
||||
releaseDateTitleCss = ".wrapper-release .title",
|
||||
releaseDateContentCss = ".wrapper-release .copy",
|
||||
lastRequest, promptSpies, sendDiscardChangesToServer;
|
||||
|
||||
lastRequest = function() { return requests[requests.length - 1]; };
|
||||
@@ -276,6 +278,43 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
|
||||
expect(containerPage.$(lastDraftCss).text()).
|
||||
toContain("Draft saved on Jul 02, 2014 at 14:20 UTC by joe");
|
||||
});
|
||||
|
||||
it('renders the release date correctly when unreleased', function () {
|
||||
renderContainerPage(mockContainerXBlockHtml, this);
|
||||
fetch({ "id": "locator-container", "published": true, "released_to_students": false,
|
||||
"release_date": "Jul 02, 2014 at 14:20 UTC", "release_date_from": 'Section "Week 1"'});
|
||||
expect(containerPage.$(releaseDateTitleCss).text()).toContain("Scheduled:");
|
||||
expect(containerPage.$(releaseDateContentCss).text()).
|
||||
toContain('Jul 02, 2014 at 14:20 UTC with Section "Week 1"');
|
||||
});
|
||||
|
||||
it('renders the release date correctly when released', function () {
|
||||
renderContainerPage(mockContainerXBlockHtml, this);
|
||||
fetch({ "id": "locator-container", "published": true, "released_to_students": true,
|
||||
"release_date": "Jul 02, 2014 at 14:20 UTC", "release_date_from": 'Section "Week 1"' });
|
||||
expect(containerPage.$(releaseDateTitleCss).text()).toContain("Released:");
|
||||
expect(containerPage.$(releaseDateContentCss).text()).
|
||||
toContain('Jul 02, 2014 at 14:20 UTC with Section "Week 1"');
|
||||
});
|
||||
|
||||
it('renders the release date correctly when the release date is not set', function () {
|
||||
renderContainerPage(mockContainerXBlockHtml, this);
|
||||
fetch({ "id": "locator-container", "published": true, "released_to_students": false,
|
||||
"release_date": null, "release_date_from": null });
|
||||
expect(containerPage.$(releaseDateTitleCss).text()).toContain("Release:");
|
||||
expect(containerPage.$(releaseDateContentCss).text()).toContain("Unscheduled");
|
||||
});
|
||||
|
||||
it('renders the release date correctly when the unit is not published', function () {
|
||||
renderContainerPage(mockContainerXBlockHtml, this);
|
||||
fetch({ "id": "locator-container", "published": false, "released_to_students": true,
|
||||
"release_date": "Jul 02, 2014 at 14:20 UTC", "release_date_from": 'Section "Week 1"' });
|
||||
// Force a render because none of the fetched fields will trigger a render
|
||||
containerPage.xblockPublisher.render();
|
||||
expect(containerPage.$(releaseDateTitleCss).text()).toContain("Release:");
|
||||
expect(containerPage.$(releaseDateContentCss).text()).
|
||||
toContain('Jul 02, 2014 at 14:20 UTC with Section "Week 1"');
|
||||
});
|
||||
});
|
||||
|
||||
describe("PublishHistory", function () {
|
||||
|
||||
@@ -84,7 +84,10 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/
|
||||
edited_on: this.model.get('edited_on'),
|
||||
edited_by: this.model.get('edited_by'),
|
||||
published_on: this.model.get('published_on'),
|
||||
published_by: this.model.get('published_by')
|
||||
published_by: this.model.get('published_by'),
|
||||
released_to_students: this.model.get('released_to_students'),
|
||||
release_date: this.model.get('release_date'),
|
||||
release_date_from: this.model.get('release_date_from')
|
||||
}));
|
||||
|
||||
return this;
|
||||
|
||||
@@ -25,14 +25,30 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!--To be added in STUD-1829-->
|
||||
<!--<div class="wrapper-release bar-mod-content">-->
|
||||
<!--<h5 class="title">Will Release:</h5>-->
|
||||
<!--<p class="copy">-->
|
||||
<!--<span class="release-date">July 25, 2014</span> with-->
|
||||
<!--<span class="release-with">Section "Week 1"</span>-->
|
||||
<!--</p>-->
|
||||
<!--</div>-->
|
||||
<!--TODO this needs strikeout styles once staff lock exists-->
|
||||
<div class="wrapper-release bar-mod-content">
|
||||
<h5 class="title">
|
||||
<% if (published && release_date) {
|
||||
if (released_to_students) { %>
|
||||
<%= gettext("Released:") %>
|
||||
<% } else { %>
|
||||
<%= gettext("Scheduled:") %>
|
||||
<% }
|
||||
} else { %>
|
||||
<%= gettext("Release:") %>
|
||||
<% } %>
|
||||
</h5>
|
||||
<p class="copy">
|
||||
<% if (release_date) { %>
|
||||
<% var message = gettext("%(release_date)s with %(section_or_subsection)s") %>
|
||||
<%= interpolate(message, {
|
||||
release_date: '<span class="release-date">' + release_date + '</span>',
|
||||
section_or_subsection: '<span class="release-with">' + release_date_from + '</span>' }, true) %>
|
||||
<% } else { %>
|
||||
<%= gettext("Unscheduled") %>
|
||||
<% } %>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!--To be added in STUD-1830-->
|
||||
<!--<div class="wrapper-visibility bar-mod-content">-->
|
||||
|
||||
@@ -22,6 +22,7 @@ log = logging.getLogger(__name__)
|
||||
# Make '_' a no-op so we can scrape strings
|
||||
_ = lambda text: text
|
||||
|
||||
DEFAULT_START_DATE = datetime(2030, 1, 1, tzinfo=UTC())
|
||||
|
||||
class StringOrDate(Date):
|
||||
def from_json(self, value):
|
||||
@@ -170,7 +171,7 @@ class CourseFields(object):
|
||||
enrollment_start = Date(help="Date that enrollment for this class is opened", scope=Scope.settings)
|
||||
enrollment_end = Date(help="Date that enrollment for this class is closed", scope=Scope.settings)
|
||||
start = Date(help="Start time when this module is visible",
|
||||
default=datetime(2030, 1, 1, tzinfo=UTC()),
|
||||
default=DEFAULT_START_DATE,
|
||||
scope=Scope.settings)
|
||||
end = Date(help="Date that this class ends", scope=Scope.settings)
|
||||
advertised_start = String(
|
||||
|
||||
Reference in New Issue
Block a user