Commit 436d7739 by Ben McMorran Committed by cahrens

Displays release date of unit in sidebar

parent 9fe41e76
......@@ -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)
......@@ -31,11 +31,6 @@ define(["backbone", "underscore", "js/utils/module"], function(Backbone, _, Modu
*/
"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.
*/
"published": null,
......@@ -61,12 +56,18 @@ define(["backbone", "underscore", "js/utils/module"], function(Backbone, _, Modu
*/
"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(
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment