diff --git a/cms/djangoapps/contentstore/views/item.py b/cms/djangoapps/contentstore/views/item.py index 51bfd23214..2a08be6b4f 100644 --- a/cms/djangoapps/contentstore/views/item.py +++ b/cms/djangoapps/contentstore/views/item.py @@ -566,8 +566,10 @@ def create_xblock_info(usage_key, xblock, data=None, metadata=None): "category": xblock.category, "has_changes": modulestore().has_changes(usage_key), "published": publish_state in (PublishState.public, PublishState.draft), - "edited_on": get_default_time_display(xblock.edited_on) if xblock.edited_on else None, - "edited_by": safe_get_username(xblock.edited_by) + "edited_on": get_default_time_display(xblock.subtree_edited_on) if xblock.subtree_edited_on else None, + "edited_by": safe_get_username(xblock.subtree_edited_by), + "published_on": get_default_time_display(xblock.published_date) if xblock.published_date else None, + "published_by": safe_get_username(xblock.published_by), } if data is not None: xblock_info["data"] = data diff --git a/cms/static/js/models/xblock_info.js b/cms/static/js/models/xblock_info.js index 0bb6cc3df9..5dfc4c21df 100644 --- a/cms/static/js/models/xblock_info.js +++ b/cms/static/js/models/xblock_info.js @@ -32,14 +32,21 @@ define(["backbone", "js/utils/module"], function(Backbone, ModuleUtils) { */ "locked": null, /** - * Date of last edit to this xblock. Will be the latest change to either the draft - * or the published version. + * Date of the last edit to this xblock or any of its descendants. */ "edited_on":null, /** - * User who last edited the xblock. + * User who last edited the xblock or any of its descendants. */ "edited_by":null, + /** + * Date of the last publish of this xblock, or null if never published. + */ + "published_on": null, + /** + * User who last published the xblock, or null if never published. + */ + "published_by": null, /** * If the xblock is published, the date on which it will be released to students. */ 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 6170abee73..37e8130f8a 100644 --- a/cms/static/js/spec/views/pages/container_subviews_spec.js +++ b/cms/static/js/spec/views/pages/container_subviews_spec.js @@ -12,6 +12,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin beforeEach(function () { edit_helpers.installTemplate('xblock-string-field-editor'); edit_helpers.installTemplate('publish-xblock'); + edit_helpers.installTemplate('publish-history'); appendSetFixtures(mockContainerPage); model = new XBlockInfo({ @@ -130,6 +131,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin draftBit = "draft", publishButtonCss = ".action-publish", discardChangesButtonCss = ".action-discard", + lastDraftCss = ".wrapper-last-draft", request, lastRequest, promptSpies, sendDiscardChangesToServer; lastRequest = function() { return requests[requests.length - 1]; }; @@ -280,6 +282,49 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin expect(requests.length).toEqual(numRequests); expect(containerPage.$(discardChangesButtonCss)).not.toHaveClass('is-disabled'); }); + + it('renders the last published date and user when there are no changes', function () { + renderContainerPage(mockContainerXBlockHtml, this); + fetch({ "id": "locator-container", "has_changes": false, + "edited_on": "Jun 30, 2014 at 14:20 UTC", "edited_by": "joe", + "published_on": "Jul 01, 2014 at 12:45 UTC", "published_by": "amako"}); + expect(containerPage.$(lastDraftCss).text()). + toContain("Last published Jul 01, 2014 at 12:45 UTC by amako"); + }); + + it('renders the last saved date and user when there are changes', function () { + renderContainerPage(mockContainerXBlockHtml, this); + fetch({ "id": "locator-container", "has_changes": true, + "edited_on": "Jul 02, 2014 at 14:20 UTC", "edited_by": "joe", + "published_on": "Jul 01, 2014 at 12:45 UTC", "published_by": "amako"}); + expect(containerPage.$(lastDraftCss).text()). + toContain("Draft saved on Jul 02, 2014 at 14:20 UTC by joe"); + }); + }); + + describe("PublishHistory", function () { + var lastPublishCss = ".wrapper-last-publish"; + + it('renders the last published date and user when the block is published', function () { + renderContainerPage(mockContainerXBlockHtml, this); + fetch({ "id": "locator-container", "published": true, + "published_on": "Jul 01, 2014 at 12:45 UTC", "published_by": "amako" }); + expect(containerPage.$(lastPublishCss).text()). + toContain("Last published Jul 01, 2014 at 12:45 UTC by amako"); + }); + + it('renders never published when the block is unpublished', function () { + renderContainerPage(mockContainerXBlockHtml, this); + fetch({ "id": "locator-container", "published": false, + "published_on": "Jul 01, 2014 at 12:45 UTC", "published_by": "amako" }); + expect(containerPage.$(lastPublishCss).text()).toContain("Never published"); + }); + + it('renders correctly when the block is published without publish info', function () { + renderContainerPage(mockContainerXBlockHtml, this); + fetch({ "id": "locator-container", "published": true, "published_on": null, "published_by": null}); + expect(containerPage.$(lastPublishCss).text()).toContain("Previously published"); + }); }); }); }); diff --git a/cms/static/js/views/pages/container.js b/cms/static/js/views/pages/container.js index 40cf5dbcc6..689da2a74a 100644 --- a/cms/static/js/views/pages/container.js +++ b/cms/static/js/views/pages/container.js @@ -34,6 +34,12 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/contai }); this.xblockPublisher.render(); + this.publishHistory = new ContainerSubviews.PublishHistory({ + el: this.$('#publish-history'), + model: this.model + }); + this.publishHistory.render(); + // No need to render initially. This is only used for updating state // when the unit changes visibility. this.visibilityState = new ContainerSubviews.VisibilityStateController({ diff --git a/cms/static/js/views/pages/container_subviews.js b/cms/static/js/views/pages/container_subviews.js index 742f505dbf..1d27d2ba2b 100644 --- a/cms/static/js/views/pages/container_subviews.js +++ b/cms/static/js/views/pages/container_subviews.js @@ -108,7 +108,9 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/feedba has_changes: this.model.get('has_changes'), published: this.model.get('published'), 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_by: this.model.get('published_by') })); return this; @@ -174,9 +176,40 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/feedba } }); + /** + * PublishHistory displays when and by whom the xblock was last published, if it ever was. + */ + var PublishHistory = BaseView.extend({ + // takes XBlockInfo as a model + + initialize: function () { + BaseView.prototype.initialize.call(this); + this.template = this.loadTemplate('publish-history'); + this.model.on('sync', this.onSync, this); + }, + + onSync: function(e) { + if (e.changedAttributes() && (('published' in e.changedAttributes()) || + ('published_on' in e.changedAttributes()) || ('published_by' in e.changedAttributes()))) { + this.render(); + } + }, + + render: function () { + this.$el.html(this.template({ + published: this.model.get('published'), + published_on: this.model.get('published_on'), + published_by: this.model.get('published_by') + })); + + return this; + } + }); + return { 'VisibilityStateController': VisibilityStateController, 'PreviewActionController': PreviewActionController, - 'Publisher': Publisher + 'Publisher': Publisher, + 'PublishHistory': PublishHistory }; }); // end define(); diff --git a/cms/static/sass/views/_container.scss b/cms/static/sass/views/_container.scss index 8c4df2b0aa..71574881b1 100644 --- a/cms/static/sass/views/_container.scss +++ b/cms/static/sass/views/_container.scss @@ -139,6 +139,11 @@ .wrapper-last-draft { padding: ($baseline*.75) ($baseline*.75) ($baseline/4) ($baseline*.75); + + .date, + .user { + font-weight: 600; + } } .wrapper-release { @@ -184,6 +189,26 @@ } + // versioning widget + .unit-publish-history { + + .wrapper-last-publish { + margin-bottom: $baseline; + padding: ($baseline*.75); + background-color: $white; + + .copy { + @extend %t-copy-sub2; + color: $gray; + } + + .date, + .user { + font-weight: 600; + } + } + } + // location widget .unit-location { @extend %bar-module; diff --git a/cms/templates/container.html b/cms/templates/container.html index 24a67428b8..d11a8b0b80 100644 --- a/cms/templates/container.html +++ b/cms/templates/container.html @@ -24,7 +24,7 @@ from django.utils.translation import ugettext as _ templates = ["basic-modal", "modal-button", "edit-xblock-modal", "editor-mode-button", "upload-dialog", "image-modal", "add-xblock-component", "add-xblock-component-button", "add-xblock-component-menu", - "add-xblock-component-menu-problem", "xblock-string-field-editor", "publish-xblock"] + "add-xblock-component-menu-problem", "xblock-string-field-editor", "publish-xblock", "publish-history"] %> <%block name="header_extras"> % for template_name in templates: @@ -142,6 +142,7 @@ templates = ["basic-modal", "modal-button", "edit-xblock-modal", % endif % if is_unit_page:
+