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 ...@@ -9,8 +9,9 @@ from django.test import TestCase
from django.test.utils import override_settings from django.test.utils import override_settings
from contentstore import utils from contentstore import utils
from contentstore.tests.utils import CourseTestCase
from xmodule.modulestore import ModuleStoreEnum 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 opaque_keys.edx.locations import SlashSeparatedCourseKey, Location
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
...@@ -260,3 +261,57 @@ class XBlockVisibilityTestCase(TestCase): ...@@ -260,3 +261,57 @@ class XBlockVisibilityTestCase(TestCase):
modulestore().publish(location, self.dummy_user) modulestore().publish(location, self.dummy_user)
return vertical 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): ...@@ -187,6 +187,28 @@ def is_xblock_visible_to_students(xblock):
return True 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): def add_extra_panel_tab(tab_type, course):
""" """
Used to add the panel tab to a course if it does not exist. Used to add the panel tab to a course if it does not exist.
......
...@@ -4,6 +4,8 @@ from __future__ import absolute_import ...@@ -4,6 +4,8 @@ from __future__ import absolute_import
import hashlib import hashlib
import logging import logging
from uuid import uuid4 from uuid import uuid4
from datetime import datetime
from pytz import UTC
from collections import OrderedDict from collections import OrderedDict
from functools import partial from functools import partial
...@@ -26,6 +28,9 @@ from xmodule.modulestore.django import modulestore ...@@ -26,6 +28,9 @@ from xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError
from xmodule.modulestore.inheritance import own_metadata from xmodule.modulestore.inheritance import own_metadata
from xmodule.x_module import PREVIEW_VIEWS, STUDIO_VIEW, STUDENT_VIEW 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 django.contrib.auth.models import User
from util.date_utils import get_default_time_display 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 ...@@ -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) 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): def safe_get_username(user_id):
""" """
Guard against bad user_ids, like the infamous "**replace_user**". 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 ...@@ -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_on": get_default_time_display(xblock.published_date) if xblock.published_date else None,
"published_by": safe_get_username(xblock.published_by), "published_by": safe_get_username(xblock.published_by),
'studio_url': xblock_studio_url(xblock), '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: if data is not None:
xblock_info["data"] = data xblock_info["data"] = data
...@@ -677,3 +688,15 @@ def _create_xblock_child_info(xblock, include_children_predicate=NEVER): ...@@ -677,3 +688,15 @@ def _create_xblock_child_info(xblock, include_children_predicate=NEVER):
) for child in xblock.get_children() ) for child in xblock.get_children()
] ]
return child_info 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 ...@@ -31,11 +31,6 @@ define(["backbone", "underscore", "js/utils/module"], function(Backbone, _, Modu
*/ */
"has_changes": null, "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. * True iff a published version of the xblock exists.
*/ */
"published": null, "published": null,
...@@ -61,12 +56,18 @@ define(["backbone", "underscore", "js/utils/module"], function(Backbone, _, Modu ...@@ -61,12 +56,18 @@ define(["backbone", "underscore", "js/utils/module"], function(Backbone, _, Modu
*/ */
"published_by": null, "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. * 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, "release_date": null,
/** /**
* The xblock which is determining the release date. For instance, for a unit, * 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 will either be the parent subsection or the grandparent section.
* This can be null if the release date is unscheduled.
*/ */
"release_date_from":null "release_date_from":null
}, },
......
...@@ -102,6 +102,8 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin ...@@ -102,6 +102,8 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
publishButtonCss = ".action-publish", publishButtonCss = ".action-publish",
discardChangesButtonCss = ".action-discard", discardChangesButtonCss = ".action-discard",
lastDraftCss = ".wrapper-last-draft", lastDraftCss = ".wrapper-last-draft",
releaseDateTitleCss = ".wrapper-release .title",
releaseDateContentCss = ".wrapper-release .copy",
lastRequest, promptSpies, sendDiscardChangesToServer; lastRequest, promptSpies, sendDiscardChangesToServer;
lastRequest = function() { return requests[requests.length - 1]; }; lastRequest = function() { return requests[requests.length - 1]; };
...@@ -276,6 +278,43 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin ...@@ -276,6 +278,43 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
expect(containerPage.$(lastDraftCss).text()). expect(containerPage.$(lastDraftCss).text()).
toContain("Draft saved on Jul 02, 2014 at 14:20 UTC by joe"); 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 () { describe("PublishHistory", function () {
......
...@@ -84,7 +84,10 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/ ...@@ -84,7 +84,10 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/
edited_on: this.model.get('edited_on'), edited_on: this.model.get('edited_on'),
edited_by: this.model.get('edited_by'), edited_by: this.model.get('edited_by'),
published_on: this.model.get('published_on'), 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; return this;
......
...@@ -25,14 +25,30 @@ ...@@ -25,14 +25,30 @@
</p> </p>
</div> </div>
<!--To be added in STUD-1829--> <!--TODO this needs strikeout styles once staff lock exists-->
<!--<div class="wrapper-release bar-mod-content">--> <div class="wrapper-release bar-mod-content">
<!--<h5 class="title">Will Release:</h5>--> <h5 class="title">
<!--<p class="copy">--> <% if (published && release_date) {
<!--<span class="release-date">July 25, 2014</span> with--> if (released_to_students) { %>
<!--<span class="release-with">Section "Week 1"</span>--> <%= gettext("Released:") %>
<!--</p>--> <% } else { %>
<!--</div>--> <%= 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--> <!--To be added in STUD-1830-->
<!--<div class="wrapper-visibility bar-mod-content">--> <!--<div class="wrapper-visibility bar-mod-content">-->
......
...@@ -22,6 +22,7 @@ log = logging.getLogger(__name__) ...@@ -22,6 +22,7 @@ log = logging.getLogger(__name__)
# Make '_' a no-op so we can scrape strings # Make '_' a no-op so we can scrape strings
_ = lambda text: text _ = lambda text: text
DEFAULT_START_DATE = datetime(2030, 1, 1, tzinfo=UTC())
class StringOrDate(Date): class StringOrDate(Date):
def from_json(self, value): def from_json(self, value):
...@@ -170,7 +171,7 @@ class CourseFields(object): ...@@ -170,7 +171,7 @@ class CourseFields(object):
enrollment_start = Date(help="Date that enrollment for this class is opened", scope=Scope.settings) 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) 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", start = Date(help="Start time when this module is visible",
default=datetime(2030, 1, 1, tzinfo=UTC()), default=DEFAULT_START_DATE,
scope=Scope.settings) scope=Scope.settings)
end = Date(help="Date that this class ends", scope=Scope.settings) end = Date(help="Date that this class ends", scope=Scope.settings)
advertised_start = String( 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