From 436d773987f2c09679614b99adc90f6a741003b5 Mon Sep 17 00:00:00 2001
From: Ben McMorran
Date: Mon, 14 Jul 2014 10:19:52 -0400
Subject: [PATCH] Displays release date of unit in sidebar
---
.../contentstore/tests/test_utils.py | 57 ++++++++++++++++++-
cms/djangoapps/contentstore/utils.py | 22 +++++++
cms/djangoapps/contentstore/views/item.py | 23 ++++++++
cms/static/js/models/xblock_info.js | 11 ++--
.../views/pages/container_subviews_spec.js | 39 +++++++++++++
.../js/views/pages/container_subviews.js | 5 +-
cms/templates/js/publish-xblock.underscore | 32 ++++++++---
common/lib/xmodule/xmodule/course_module.py | 3 +-
8 files changed, 176 insertions(+), 16 deletions(-)
diff --git a/cms/djangoapps/contentstore/tests/test_utils.py b/cms/djangoapps/contentstore/tests/test_utils.py
index 0857fdd4ce..c87db7e076 100644
--- a/cms/djangoapps/contentstore/tests/test_utils.py
+++ b/cms/djangoapps/contentstore/tests/test_utils.py
@@ -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)
diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py
index 54109d9243..9b1b935cee 100644
--- a/cms/djangoapps/contentstore/utils.py
+++ b/cms/djangoapps/contentstore/utils.py
@@ -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.
diff --git a/cms/djangoapps/contentstore/views/item.py b/cms/djangoapps/contentstore/views/item.py
index f7eb285bcc..efd6ef6526 100644
--- a/cms/djangoapps/contentstore/views/item.py
+++ b/cms/djangoapps/contentstore/views/item.py
@@ -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)
diff --git a/cms/static/js/models/xblock_info.js b/cms/static/js/models/xblock_info.js
index 1005e0dccd..46fccebf73 100644
--- a/cms/static/js/models/xblock_info.js
+++ b/cms/static/js/models/xblock_info.js
@@ -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
},
diff --git a/cms/static/js/spec/views/pages/container_subviews_spec.js b/cms/static/js/spec/views/pages/container_subviews_spec.js
index 505e80467e..8789473ec6 100644
--- a/cms/static/js/spec/views/pages/container_subviews_spec.js
+++ b/cms/static/js/spec/views/pages/container_subviews_spec.js
@@ -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 () {
diff --git a/cms/static/js/views/pages/container_subviews.js b/cms/static/js/views/pages/container_subviews.js
index 8bc9335155..da61925849 100644
--- a/cms/static/js/views/pages/container_subviews.js
+++ b/cms/static/js/views/pages/container_subviews.js
@@ -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;
diff --git a/cms/templates/js/publish-xblock.underscore b/cms/templates/js/publish-xblock.underscore
index 13cd1b3f65..70352ad3b7 100644
--- a/cms/templates/js/publish-xblock.underscore
+++ b/cms/templates/js/publish-xblock.underscore
@@ -25,14 +25,30 @@
-
-
-
-
-
-
-
-
+
+
+
+ <% if (published && release_date) {
+ if (released_to_students) { %>
+ <%= gettext("Released:") %>
+ <% } else { %>
+ <%= gettext("Scheduled:") %>
+ <% }
+ } else { %>
+ <%= gettext("Release:") %>
+ <% } %>
+
+
+ <% if (release_date) { %>
+ <% var message = gettext("%(release_date)s with %(section_or_subsection)s") %>
+ <%= interpolate(message, {
+ release_date: '' + release_date + '',
+ section_or_subsection: '' + release_date_from + '' }, true) %>
+ <% } else { %>
+ <%= gettext("Unscheduled") %>
+ <% } %>
+
+
diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py
index 1d71819f2d..89f49f9ce2 100644
--- a/common/lib/xmodule/xmodule/course_module.py
+++ b/common/lib/xmodule/xmodule/course_module.py
@@ -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(