Fixes #U4-414, #U4-1669 - publish dialog now uses new apis and ensures documents are validated during publishing. This also

enhances many other things in the publishing API including giving a status result for each item, ensuring that child items
do not try to get published when the parent item fails (based on a subset of rules)
This commit is contained in:
Shannon Deminick
2013-02-11 03:55:58 +06:00
parent ded940a757
commit c23be1fd96
16 changed files with 594 additions and 91 deletions

View File

@@ -9,7 +9,7 @@ namespace Umbraco.Core.Publishing
public abstract class BasePublishingStrategy : IPublishingStrategy
{
protected internal abstract Attempt<PublishStatus> PublishInternal(IContent content, int userId);
internal abstract Attempt<PublishStatus> PublishInternal(IContent content, int userId);
/// <summary>
/// Publishes a list of content items
@@ -23,10 +23,10 @@ namespace Umbraco.Core.Publishing
/// </param>
/// <param name="validateContent">If true this will validate each content item before trying to publish it, if validation fails it will not be published.</param>
/// <returns></returns>
protected internal abstract IEnumerable<Attempt<PublishStatus>> PublishWithChildrenInternal(
internal abstract IEnumerable<Attempt<PublishStatus>> PublishWithChildrenInternal(
IEnumerable<IContent> content, int userId, bool includeUnpublishedDocuments = true, bool validateContent = false);
protected internal abstract IEnumerable<Attempt<PublishStatus>> UnPublishInternal(IEnumerable<IContent> content, int userId);
internal abstract IEnumerable<Attempt<PublishStatus>> UnPublishInternal(IEnumerable<IContent> content, int userId);
public abstract bool Publish(IContent content, int userId);
public abstract bool PublishWithChildren(IEnumerable<IContent> content, int userId);

View File

@@ -5,10 +5,10 @@ namespace Umbraco.Core.Publishing
/// <summary>
/// The result of publishing a content item
/// </summary>
public class PublishStatus
internal class PublishStatus
{
public IContent ContentItem { get; private set; }
public PublishStatusType StatusType { get; private set; }
public PublishStatusType StatusType { get; internal set; }
public PublishStatus(IContent content, PublishStatusType statusType)
{

View File

@@ -3,43 +3,48 @@ namespace Umbraco.Core.Publishing
/// <summary>
/// A status type of the result of publishing a content item
/// </summary>
public enum PublishStatusType
internal enum PublishStatusType
{
/// <summary>
/// The publishing was successful.
/// </summary>
Success,
Success = 0,
/// <summary>
/// The item was already published
/// </summary>
SuccessAlreadyPublished = 1,
/// <summary>
/// The content could not be published because it's ancestor path isn't published
/// </summary>
FailedPathNotPublished,
FailedPathNotPublished = 10,
/// <summary>
/// The content item was scheduled to be un-published and it has expired so we cannot force it to be
/// published again as part of a bulk publish operation.
/// </summary>
FailedHasExpired,
FailedHasExpired = 11,
/// <summary>
/// The content item is scheduled to be released in the future and therefore we cannot force it to
/// be published during a bulk publish operation.
/// </summary>
FailedAwaitingRelease,
FailedAwaitingRelease = 12,
/// <summary>
/// The content item is in the trash, it cannot be published
/// </summary>
FailedIsTrashed,
FailedIsTrashed = 13,
/// <summary>
/// The publish action has been cancelled by an event handler
/// </summary>
FailedCancelledByEvent,
FailedCancelledByEvent = 14,
/// <summary>
/// The content item contains invalid data (has not passed validation requirements)
/// </summary>
FailedContentInvalid
FailedContentInvalid = 15
}
}

View File

@@ -19,7 +19,7 @@ namespace Umbraco.Core.Publishing
/// </summary>
/// <param name="content"><see cref="IContent"/> to publish</param>
/// <param name="userId">Id of the User issueing the publish operation</param>
protected internal override Attempt<PublishStatus> PublishInternal(IContent content, int userId)
internal override Attempt<PublishStatus> PublishInternal(IContent content, int userId)
{
if (Publishing.IsRaisedEventCancelled(new PublishEventArgs<IContent>(content), this))
{
@@ -99,7 +99,7 @@ namespace Umbraco.Core.Publishing
/// level and so on. If we detect that the above rule applies when the document publishing is cancelled we'll add it to the list of
/// parentsIdsCancelled so that it's children don't get published.
/// </remarks>
protected internal override IEnumerable<Attempt<PublishStatus>> PublishWithChildrenInternal(
internal override IEnumerable<Attempt<PublishStatus>> PublishWithChildrenInternal(
IEnumerable<IContent> content, int userId, bool includeUnpublishedDocuments = true, bool validateContent = false)
{
var statuses = new List<Attempt<PublishStatus>>();
@@ -114,6 +114,13 @@ namespace Umbraco.Core.Publishing
// much difference because we iterate over them all anyways?? Morten?
// Because we're grouping I think this will execute all the queries anyways so need to fetch it all first.
var fetchedContent = content.ToArray();
//We're going to populate the statuses with all content that is already published because below we are only going to iterate over
// content that is not published. We'll set the status to "AlreadyPublished"
statuses.AddRange(fetchedContent.Where(x => x.Published)
.Select(x => new Attempt<PublishStatus>(true, new PublishStatus(x, PublishStatusType.SuccessAlreadyPublished))));
//group by level and iterate over each level (sorted ascending)
var levelGroups = fetchedContent.GroupBy(x => x.Level);
foreach (var level in levelGroups.OrderBy(x => x.Key))
{
@@ -315,7 +322,7 @@ namespace Umbraco.Core.Publishing
/// <param name="content">An enumerable list of <see cref="IContent"/></param>
/// <param name="userId">Id of the User issueing the unpublish operation</param>
/// <returns>A list of publish statuses</returns>
protected internal override IEnumerable<Attempt<PublishStatus>> UnPublishInternal(IEnumerable<IContent> content, int userId)
internal override IEnumerable<Attempt<PublishStatus>> UnPublishInternal(IEnumerable<IContent> content, int userId)
{
var result = new List<Attempt<PublishStatus>>();

View File

@@ -435,7 +435,8 @@ namespace Umbraco.Core.Services
/// <returns>True if publishing succeeded, otherwise False</returns>
public bool Publish(IContent content, int userId = 0)
{
return SaveAndPublishDo(content, false, userId);
var result = SaveAndPublishDo(content, false, userId);
return result.Success;
}
/// <summary>
@@ -473,7 +474,8 @@ namespace Umbraco.Core.Services
/// <returns>True if publishing succeeded, otherwise False</returns>
public bool SaveAndPublish(IContent content, int userId = 0, bool raiseEvents = true)
{
return SaveAndPublishDo(content, false, userId, raiseEvents);
var result = SaveAndPublishDo(content, false, userId, raiseEvents);
return result.Success;
}
/// <summary>
@@ -1026,7 +1028,7 @@ namespace Umbraco.Core.Services
/// <param name="omitCacheRefresh">Optional boolean to avoid having the cache refreshed when calling this Publish method. By default this method will not update the cache.</param>
/// <param name="userId">Optional Id of the User issueing the publishing</param>
/// <returns>True if publishing succeeded, otherwise False</returns>
internal bool Publish(IContent content, bool omitCacheRefresh = true, int userId = 0)
internal Attempt<PublishStatus> Publish(IContent content, bool omitCacheRefresh = true, int userId = 0)
{
return SaveAndPublishDo(content, omitCacheRefresh, userId);
}
@@ -1038,10 +1040,12 @@ namespace Umbraco.Core.Services
/// <param name="omitCacheRefresh">Optional boolean to avoid having the cache refreshed when calling this Publish method. By default this method will not update the cache.</param>
/// <param name="userId">Optional Id of the User issueing the publishing</param>
/// <param name="includeUnpublished">If set to true, this will also publish descendants that are completely unpublished, normally this will only publish children that have previously been published</param>
/// <param name="validateContent">If true this will validate the content before publishing</param>
/// <returns>True if publishing succeeded, otherwise False</returns>
internal IEnumerable<Attempt<PublishStatus>> PublishWithChildren(IContent content, bool omitCacheRefresh = true, int userId = 0, bool includeUnpublished = false)
internal IEnumerable<Attempt<PublishStatus>> PublishWithChildren(
IContent content, bool omitCacheRefresh = true, int userId = 0, bool includeUnpublished = false, bool validateContent = false)
{
return PublishWithChildrenDo(content, omitCacheRefresh, userId, includeUnpublished);
return PublishWithChildrenDo(content, omitCacheRefresh, userId, includeUnpublished, validateContent);
}
/// <summary>
@@ -1064,7 +1068,7 @@ namespace Umbraco.Core.Services
/// <param name="userId">Optional Id of the User issueing the publishing</param>
/// <param name="raiseEvents">Optional boolean indicating whether or not to raise save events.</param>
/// <returns>True if publishing succeeded, otherwise False</returns>
internal bool SaveAndPublish(IContent content, bool omitCacheRefresh = true, int userId = 0, bool raiseEvents = true)
internal Attempt<PublishStatus> SaveAndPublish(IContent content, bool omitCacheRefresh = true, int userId = 0, bool raiseEvents = true)
{
return SaveAndPublishDo(content, omitCacheRefresh, userId, raiseEvents);
}
@@ -1151,21 +1155,25 @@ namespace Umbraco.Core.Services
return published;
}
/// <summary>
/// Publishes a <see cref="IContent"/> object and all its children
/// </summary>
/// <param name="content">The <see cref="IContent"/> to publish along with its children</param>
/// <param name="omitCacheRefresh">Optional boolean to avoid having the cache refreshed when calling this Publish method. By default this method will update the cache.</param>
/// <param name="userId">Optional Id of the User issueing the publishing</param>
/// <param name="includeUnpublished">If set to true, this will also publish descendants that are completely unpublished, normally this will only publish children that have previously been published</param>
/// <returns>
/// A list of publish statues. If the parent document is not valid or cannot be published because it's parent(s) is not published
/// then the list will only contain one status item, otherwise it will contain status items for it and all of it's descendants that
/// are to be published.
/// </returns>
private IEnumerable<Attempt<PublishStatus>> PublishWithChildrenDo(IContent content, bool omitCacheRefresh = false, int userId = 0, bool includeUnpublished = false)
/// <summary>
/// Publishes a <see cref="IContent"/> object and all its children
/// </summary>
/// <param name="content">The <see cref="IContent"/> to publish along with its children</param>
/// <param name="omitCacheRefresh">Optional boolean to avoid having the cache refreshed when calling this Publish method. By default this method will update the cache.</param>
/// <param name="userId">Optional Id of the User issueing the publishing</param>
/// <param name="includeUnpublished">If set to true, this will also publish descendants that are completely unpublished, normally this will only publish children that have previously been published</param>
/// <param name="validateContent">If set to true will ensure the content is valid before publishing</param>
/// <returns>
/// A list of publish statues. If the parent document is not valid or cannot be published because it's parent(s) is not published
/// then the list will only contain one status item, otherwise it will contain status items for it and all of it's descendants that
/// are to be published.
/// </returns>
private IEnumerable<Attempt<PublishStatus>> PublishWithChildrenDo(
IContent content, bool omitCacheRefresh = false, int userId = 0, bool includeUnpublished = false, bool validateContent = false)
{
var result = new List<Attempt<PublishStatus>>();
if (content == null) throw new ArgumentNullException("content");
var result = new List<Attempt<PublishStatus>>();
//Check if parent is published (although not if its a root node) - if parent isn't published this Content cannot be published
if (content.ParentId != -1 && content.ParentId != -20 && IsPublishable(content) == false)
@@ -1197,14 +1205,16 @@ namespace Umbraco.Core.Services
list.AddRange(GetDescendants(content));
//Publish and then update the database with new status
var publishedOutcome = _publishingStrategy.PublishWithChildrenInternal(list, userId, includeUnpublished).ToArray();
var publishedOutcome = _publishingStrategy.PublishWithChildrenInternal(list, userId, includeUnpublished, validateContent).ToArray();
var uow = _uowProvider.GetUnitOfWork();
using (var repository = _repositoryFactory.CreateContentRepository(uow))
{
//Only loop through content where the Published property has been updated
//foreach (var item in list.Where(x => ((ICanBeDirty)x).IsPropertyDirty("Published")))
foreach (var item in publishedOutcome.Where(x => x.Success))
//Only loop through content that was successfully published, was not already published and where the Published property has been updated
foreach (var item in publishedOutcome.Where(
x => x.Success
&& x.Result.StatusType != PublishStatusType.SuccessAlreadyPublished
&& ((ICanBeDirty) x.Result.ContentItem).IsPropertyDirty("Published")))
{
item.Result.ContentItem.WriterId = userId;
repository.AddOrUpdate(item.Result.ContentItem);
@@ -1275,17 +1285,19 @@ namespace Umbraco.Core.Services
/// <param name="userId">Optional Id of the User issueing the publishing</param>
/// <param name="raiseEvents">Optional boolean indicating whether or not to raise save events.</param>
/// <returns>True if publishing succeeded, otherwise False</returns>
private bool SaveAndPublishDo(IContent content, bool omitCacheRefresh = false, int userId = 0, bool raiseEvents = true)
private Attempt<PublishStatus> SaveAndPublishDo(IContent content, bool omitCacheRefresh = false, int userId = 0, bool raiseEvents = true)
{
if(raiseEvents)
{
if (Saving.IsRaisedEventCancelled(new SaveEventArgs<IContent>(content), this))
return false;
{
return new Attempt<PublishStatus>(false, new PublishStatus(content, PublishStatusType.FailedCancelledByEvent));
}
}
//Has this content item previously been published? If so, we don't need to refresh the children
var previouslyPublished = HasPublishedVersion(content.Id);
var validForPublishing = true;
var publishStatus = new PublishStatus(content, PublishStatusType.Success); //initially set to success
//Check if parent is published (although not if its a root node) - if parent isn't published this Content cannot be published
if (content.ParentId != -1 && content.ParentId != -20 && IsPublishable(content) == false)
@@ -1294,7 +1306,7 @@ namespace Umbraco.Core.Services
string.Format(
"Content '{0}' with Id '{1}' could not be published because its parent is not published.",
content.Name, content.Id));
validForPublishing = false;
publishStatus.StatusType = PublishStatusType.FailedPathNotPublished;
}
//Content contains invalid property values and can therefore not be published - fire event?
@@ -1304,11 +1316,16 @@ namespace Umbraco.Core.Services
string.Format(
"Content '{0}' with Id '{1}' could not be published because of invalid properties.",
content.Name, content.Id));
validForPublishing = false;
publishStatus.StatusType = PublishStatusType.FailedContentInvalid;
}
//Publish and then update the database with new status
bool published = validForPublishing && _publishingStrategy.Publish(content, userId);
var publishResult = _publishingStrategy.PublishInternal(content, userId);
//set our publish status to the publish result
publishStatus.StatusType = publishResult.Result.StatusType;
//we are successfully published if the flag is success (and for good measure we'll check the publish result)
bool published = publishStatus.StatusType == PublishStatusType.Success && publishResult.Success;
var uow = _uowProvider.GetUnitOfWork();
using (var repository = _repositoryFactory.CreateContentRepository(uow))
@@ -1361,7 +1378,9 @@ namespace Umbraco.Core.Services
//Save xml to db and call following method to fire event through PublishingStrategy to update cache
if (published && omitCacheRefresh == false)
{
_publishingStrategy.PublishingFinalized(content);
}
//We need to check if children and their publish state to ensure that we 'republish' content that was previously published
if (published && omitCacheRefresh == false && previouslyPublished == false && HasChildren(content.Id))
@@ -1373,7 +1392,7 @@ namespace Umbraco.Core.Services
Audit.Add(AuditTypes.Publish, "Save and Publish performed by user", userId, content.Id);
return published;
return new Attempt<PublishStatus>(publishStatus.StatusType == PublishStatusType.Success, publishStatus);
}
/// <summary>

View File

@@ -653,6 +653,11 @@ To manage your website, simply open the umbraco back office and start adding con
<key alias="paSimpleHelp">If you just want to setup simple protection using a single login and password</key>
</area>
<area alias="publish">
<key alias="contentPublishedFailedInvalid">
<![CDATA[
%0% could not be published because some of it's data did not pass validation rules.
]]>
</key>
<key alias="contentPublishedFailedByEvent">
<![CDATA[
%0% could not be published, due to a 3rd party extension cancelling the action.

View File

@@ -652,6 +652,11 @@ To manage your website, simply open the umbraco back office and start adding con
<key alias="paSimpleHelp">If you just want to setup simple protection using a single login and password</key>
</area>
<area alias="publish">
<key alias="contentPublishedFailedInvalid">
<![CDATA[
%0% could not be published because some of it's data did not pass validation rules.
]]>
</key>
<key alias="contentPublishedFailedByEvent">
<![CDATA[
%0% could not be published, due to a 3rd party extension cancelling the action.

View File

@@ -8,8 +8,29 @@ namespace Umbraco.Web.UI.Umbraco.Dialogs
{
public partial class Publish : UmbracoEnsuredPage
{
protected int TotalNodesToPublish { get; private set; }
protected string PageName { get; private set; }
protected int DocumentId { get; private set; }
protected override void OnInit(EventArgs e)
{
base.OnInit(e);
int id;
if (!int.TryParse(Request.GetItemAsString("id"), out id))
{
throw new InvalidOperationException("The id value must be an integer");
}
var doc = Services.ContentService.GetById(id);
if (doc == null)
{
throw new InvalidOperationException("No document found with id " + id);
}
DocumentId = doc.Id;
PageName = doc.Name;
}
}
}

View File

@@ -76,8 +76,15 @@
</div>
<div id="feedbackMsg" data-bind="visible: processStatus() == 'complete'">
<div data-bind="css: { success: isSuccessful(), error: !isSuccessful() }">asdfasdfasf</div>
<p><a href='#'><%=umbraco.ui.Text("closeThisWindow") %></a></p>
<div data-bind="css: { success: isSuccessful(), error: !isSuccessful() }">
<span data-bind="text: resultMessage, visible: resultMessages().length == 0"></span>
<ul data-bind="foreach: resultMessages, visible: resultMessages().length > 1">
<li data-bind="text: message"></li>
</ul>
</div>
<p>
<a href='#' data-bind="click: closeDialog"><%=umbraco.ui.Text("closeThisWindow") %></a>
</p>
</div>

View File

@@ -0,0 +1,346 @@
(function ($) {
//extensions to base classes such as String and extension methods for jquery.
//NOTE: jquery must be loaded before this file.
//create guid object on the window (which makes it global)
if (window.Guid == null) {
window.Guid = {
generate: function () {
///<summary>generates a new Guid</summary>
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
};
}
if (!window.__debug__) {
window.__debug__ = function (msg, category, isErr) {
///<summary>global method to send debug statements to console that is cross browser (or at least checks if its possible)</summary>
if (((typeof console) != "undefined") && console.log && console.error) {
if (isErr) console.error(category + ": " + msg);
else console.log(category + ": " + msg);
}
};
}
if (!String.prototype.startsWith) {
String.prototype.startsWith = function (str) {
///<summary>startsWith extension method for string</summary>
return this.substr(0, str.length) === str;
};
}
if (!String.prototype.endsWith) {
String.prototype.endsWith = function (str) {
///<summary>endsWith extension method for string</summary>
return this.substr(this.length - str.length) === str;
};
}
if (!String.prototype.utf8Encode) {
String.prototype.utf8Encode = function () {
///<summary>UTF8 encoder for string</summary>
var str = this.replace(/\r\n/g, "\n");
var utftext = "";
for (var n = 0; n < str.length; n++) {
var c = str.charCodeAt(n);
if (c < 128) {
utftext += String.fromCharCode(c);
}
else if ((c > 127) && (c < 2048)) {
utftext += String.fromCharCode((c >> 6) | 192);
utftext += String.fromCharCode((c & 63) | 128);
}
else {
utftext += String.fromCharCode((c >> 12) | 224);
utftext += String.fromCharCode(((c >> 6) & 63) | 128);
utftext += String.fromCharCode((c & 63) | 128);
}
}
return utftext;
};
}
if (!String.prototype.utf8Decode) {
String.prototype.utf8Decode = function () {
var utftext = this;
var string = "";
var i = 0;
var c = c1 = c2 = 0;
while (i < utftext.length) {
c = utftext.charCodeAt(i);
if (c < 128) {
string += String.fromCharCode(c);
i++;
}
else if ((c > 191) && (c < 224)) {
c2 = utftext.charCodeAt(i + 1);
string += String.fromCharCode(((c & 31) << 6) | (c2 & 63));
i += 2;
}
else {
c2 = utftext.charCodeAt(i + 1);
c3 = utftext.charCodeAt(i + 2);
string += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63));
i += 3;
}
}
return string;
};
}
if (!String.prototype.base64Encode) {
String.prototype.base64Encode = function () {
///<summary>Base64 encoder for string</summary>
var keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
var output = "";
var chr1, chr2, chr3, enc1, enc2, enc3, enc4;
var i = 0;
var input = this.utf8Encode();
while (i < input.length) {
chr1 = input.charCodeAt(i++);
chr2 = input.charCodeAt(i++);
chr3 = input.charCodeAt(i++);
enc1 = chr1 >> 2;
enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
enc4 = chr3 & 63;
if (isNaN(chr2)) {
enc3 = enc4 = 64;
} else if (isNaN(chr3)) {
enc4 = 64;
}
output = output +
keyStr.charAt(enc1) + keyStr.charAt(enc2) +
keyStr.charAt(enc3) + keyStr.charAt(enc4);
}
return output;
};
}
if (!String.prototype.base64Decode) {
String.prototype.base64Decode = function () {
///<summary>Base64 decoder for string</summary>
var input = this;
var keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
var output = "";
var chr1, chr2, chr3;
var enc1, enc2, enc3, enc4;
var i = 0;
input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");
while (i < input.length) {
enc1 = keyStr.indexOf(input.charAt(i++));
enc2 = keyStr.indexOf(input.charAt(i++));
enc3 = keyStr.indexOf(input.charAt(i++));
enc4 = keyStr.indexOf(input.charAt(i++));
chr1 = (enc1 << 2) | (enc2 >> 4);
chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
chr3 = ((enc3 & 3) << 6) | enc4;
output = output + String.fromCharCode(chr1);
if (enc3 != 64) {
output = output + String.fromCharCode(chr2);
}
if (enc4 != 64) {
output = output + String.fromCharCode(chr3);
}
}
return output.utf8Decode();
};
}
if (!Math.randomRange) {
Math.randomRange = function (from, to) {
///<summary>randomRange extension for math</summary>
return Math.floor(Math.random() * (to - from + 1) + from);
};
}
if (!String.prototype.toCamelCase) {
String.prototype.toCamelCase = function () {
///<summary>toCamelCase extension method for string</summary>
var s = this.toPascalCase();
if ($.trim(s) == "")
return "";
if (s.length > 1) {
var regex = /^([A-Z]*)([A-Z].*)/g;
if (s.match(regex)) {
var match = regex.exec(s);
s = match[1].toLowerCase() + match[2];
s = s.substr(0, 1).toLowerCase() + s.substr(1);
}
} else {
s = s.toLowerCase();
}
return s;
};
}
if (!String.prototype.toPascalCase) {
String.prototype.toPascalCase = function () {
///<summary>toPascalCase extension method for string</summary>
var s = "";
$.each($.trim(this).split(/[\s\.-]+/g), function (idx, val) {
if ($.trim(val) == "")
return;
if (val.length > 1)
s += val.substr(0, 1).toUpperCase() + val.substr(1);
else
s += val.toUpperCase();
});
return s;
};
}
if (!String.prototype.toUmbracoAlias) {
String.prototype.toUmbracoAlias = function () {
///<summary>///<summary>toUmbracoAlias extension method for string</summary></summary>
var s = this.replace(/[^a-zA-Z0-9\s\.-]+/g, ''); // Strip none alphanumeric chars
return s.toCamelCase(); // Convert to camelCase
};
}
if (!String.prototype.toFunction) {
String.prototype.toFunction = function () {
var arr = this.split(".");
var fn = (window || this);
for (var i = 0, len = arr.length; i < len; i++) {
fn = fn[arr[i]];
}
if (typeof fn !== "function") {
throw new Error("function not found");
}
return fn;
};
}
//sets defaults for ajax
$.ajaxSetup({
dataType: 'json',
cache: false,
contentType: 'application/json; charset=utf-8',
error: function (x, t, e) {
if (x.status.toString().startsWith("500")) {
$u.Sys.ApiMgr.getApp().handleAjaxException(x, t, e);
}
}
});
$.fn.getAllAttributes = function () {
///<summary>extension method to get all attributes of a selected element</summary>
if ($(this).length != 1) {
throw "the getAllAttributes method can only be called when matching one jQuery selector";
};
var el = $(this).get(0);
var arr = [];
for (var i = 0, attrs = el.attributes; i < attrs.length; i++) {
arr.push({ name: attrs.item(i).nodeName, value: attrs.item(i).nodeValue });
}
return arr;
};
$.fn.outerHtml = function () {
///<summary>extension to get the 'outer html' of an element</summary>
if ($(this).length != 1) {
throw "the getAllAttributes method can only be called when matching one jQuery selector";
};
var nodeName = $(_opts.content).get(0).nodeName.toLowerCase();
//start creating raw html
var outerHtml = "<" + nodeName;
//get all the attributes/values from the original element and add them to the new one
var allAttributes = $(_opts.content).getAllAttributes();
for (var a in allAttributes) {
outerHtml += " " + allAttributes[a].name + "='" + allAttributes[a].value + "'";
}
outerHtml += ">";
outerHtml += $(_opts.content).html();
outerHtml += "</" + nodeName + ">";
return outerHtml;
};
$.fn.focusFirst = function () {
///<summary>extension to focus the first editable field in a form</summary>
return $(this).each(function () {
if ($(this).get(0).nodeName.toLowerCase() != "form") {
throw "The focusFirst method can only be applied to a form element";
}
var first = $(this).find(":input:enabled:visible").not(":submit").not(":button").not(":file").not(":image").not(":radio");
if (first.length > 0) {
$(first[0]).focus();
}
});
};
$.fn.getAttributes = function () {
///<summary>Extension method to return all of the attributes for an element</summary>
var attributes = [];
if (!this.length)
return this;
$.each(this[0].attributes, function (index, attr) {
attributes.push({ name: attr.name, value: attr.value });
});
return attributes;
};
//defaults that need to be set on ready
$(document).ready(function () {
//adds a default ignore parameter to jquery validation
if ($.validator) {
$.validator.setDefaults({ ignore: ".ignore" });
}
//adds a "re-parse" method to the Unobtrusive JS framework since Parse doesn't actually reparse
if ($.validator && $.validator.unobtrusive) {
$.validator.unobtrusive.reParse = function ($selector) {
$selector.removeData("validator");
$.validator.unobtrusive.parse($selector);
};
}
});
})(jQuery);

View File

@@ -20,4 +20,9 @@
#feedbackMsg > div {
padding: 5px;
}
#feedbackMsg ul {
margin: 0;
padding-left: 15px;
}

View File

@@ -6,6 +6,7 @@
//private methods/variables
_opts: null,
_koViewModel: null,
// Constructor
constructor: function () {
@@ -24,11 +25,13 @@
var self = this;
//The knockout js view model for the selected item
var koViewModel = {
self._koViewModel = {
publishAll: ko.observable(false),
includeUnpublished: ko.observable(false),
processStatus: ko.observable("init"),
isSuccessful: ko.observable(false),
resultMessages: ko.observableArray(),
resultMessage: ko.observable(""), //if there's only one result message
closeDialog: function () {
UmbClientMgr.closeModalWindow();
},
@@ -37,25 +40,34 @@
$.post(self._opts.restServiceLocation + "PublishDocument",
JSON.stringify({
documentId: self._opts.documentId
documentId: self._opts.documentId,
publishDescendants: self._koViewModel.publishAll(),
includeUnpublished: self._koViewModel.includeUnpublished()
}),
function (e) {
if (e.success) {
self.submitSuccess(e.message, e.header);
} else {
self.submitFailure(e.message, e.header);
self._koViewModel.processStatus("complete");
self._koViewModel.isSuccessful(e.success);
var msgs = e.message.trim().split("\r\n");
if (msgs.length > 1) {
for (var m in msgs) {
self._koViewModel.resultMessages.push({ message: msgs[m] });
}
}
else {
self._koViewModel.resultMessage(msgs[0]);
}
});
}
};
//ensure includeUnpublished is always false if publishAll is ever false
koViewModel.publishAll.subscribe(function (newValue) {
self._koViewModel.publishAll.subscribe(function (newValue) {
if (newValue === false) {
koViewModel.includeUnpublished(false);
self._koViewModel.includeUnpublished(false);
}
});
ko.applyBindings(koViewModel);
ko.applyBindings(self._koViewModel);
}

View File

@@ -1730,6 +1730,7 @@
<DesignTime>True</DesignTime>
<DependentUpon>Reference.map</DependentUpon>
</Compile>
<Compile Include="WebServices\BulkPublishController.cs" />
<Compile Include="WebServices\FolderBrowserService.cs" />
<Compile Include="WebServices\SaveFileController.cs" />
<Content Include="umbraco.presentation\umbracobase\readme.txt" />

View File

@@ -0,0 +1,98 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web.Mvc;
using Umbraco.Core;
using Umbraco.Core.Models;
using Umbraco.Core.Publishing;
using Umbraco.Core.Services;
using Umbraco.Web.Mvc;
using umbraco;
namespace Umbraco.Web.WebServices
{
/// <summary>
/// A REST controller used for the publish dialog in order to publish bulk items at once
/// </summary>
public class BulkPublishController : UmbracoAuthorizedController
{
/// <summary>
/// Publishes an document
/// </summary>
/// <param name="documentId"></param>
/// <param name="publishDescendants">true to publish descendants as well</param>
/// <param name="includeUnpublished">true to publish documents that are unpublished</param>
/// <returns>A Json array containing objects with the child id's of the document and it's current published status</returns>
[HttpPost]
public JsonResult PublishDocument(int documentId, bool publishDescendants, bool includeUnpublished)
{
var doc = Services.ContentService.GetById(documentId);
var contentService = (ContentService) Services.ContentService;
if (!publishDescendants)
{
var result = contentService.SaveAndPublish(doc, false);
return Json(new
{
success = result.Success,
message = GetMessageForStatus(result.Result)
});
}
else
{
var result = ((ContentService) Services.ContentService)
.PublishWithChildren(doc, false, UmbracoUser.Id, includeUnpublished, true)
.ToArray();
return Json(new
{
success = result.All(x => x.Success),
message = GetMessageForStatuses(result.Select(x => x.Result), doc)
});
}
}
private string GetMessageForStatuses(IEnumerable<PublishStatus> statuses, IContent doc)
{
//if all are successful then just say it was successful
if (statuses.All(x => ((int) x.StatusType) < 10))
{
return ui.Text("publish", "nodePublishAll", doc.Name, UmbracoUser);
}
//if they are not all successful the we'll add each error message to the output (one per line)
var sb = new StringBuilder();
foreach (var msg in statuses
.Where(x => ((int)x.StatusType) >= 10)
.Select(GetMessageForStatus)
.Where(msg => !msg.IsNullOrWhiteSpace()))
{
sb.AppendLine(msg.Trim());
}
return sb.ToString();
}
private string GetMessageForStatus(PublishStatus status)
{
switch (status.StatusType)
{
case PublishStatusType.Success:
case PublishStatusType.SuccessAlreadyPublished:
return ui.Text("publish", "nodePublish", status.ContentItem.Name, UmbracoUser);
case PublishStatusType.FailedPathNotPublished:
return ui.Text("publish", "contentPublishedFailedByParent",
string.Format("{0} ({1})", status.ContentItem.Name, status.ContentItem.Id), UmbracoUser);
case PublishStatusType.FailedHasExpired:
case PublishStatusType.FailedAwaitingRelease:
case PublishStatusType.FailedIsTrashed:
return ""; //we will not notify about this type of failure... or should we ?
case PublishStatusType.FailedCancelledByEvent:
return ui.Text("publish", "contentPublishedFailedByEvent",
string.Format("{0} ({1})", status.ContentItem.Name, status.ContentItem.Id), UmbracoUser);
case PublishStatusType.FailedContentInvalid:
return ui.Text("publish", "contentPublishedFailedInvalid",
string.Format("{0} ({1})", status.ContentItem.Name, status.ContentItem.Id), UmbracoUser);
default:
return status.StatusType.ToString();
}
}
}
}

View File

@@ -1,49 +1,21 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Web.Mvc;
using Umbraco.Core.IO;
using Umbraco.Core.Logging;
using Umbraco.Core.Services;
using Umbraco.Web.Cache;
using Umbraco.Web.Macros;
using Umbraco.Web.Mvc;
using umbraco;
using umbraco.BasePages;
using umbraco.cms.businesslogic.macro;
using umbraco.cms.businesslogic.template;
using umbraco.presentation.cache;
using Umbraco.Core;
using Template = umbraco.cms.businesslogic.template.Template;
namespace Umbraco.Web.WebServices
{
/// <summary>
/// A REST controller used for the publish dialog in order to publish bulk items at once
/// </summary>
public class BulkPublishController : UmbracoAuthorizedController
{
/// <summary>
/// Publishes an document
/// </summary>
/// <param name="documentId"></param>
/// <returns>A Json array containing objects with the child id's of the document and it's current published status</returns>
[HttpPost]
public JsonResult PublishDocument(int documentId, bool publishChildren)
{
var doc = Services.ContentService.GetById(documentId);
var result = ((ContentService)Services.ContentService).PublishWithChildren(doc, true, UmbracoUser.Id);
if (Services.ContentService.PublishWithChildren(doc, UmbracoUser.Id))
{
}
return null;
}
}
/// <summary>
/// A REST controller used to save files such as templates, partial views, macro files, etc...
/// </summary>

View File

@@ -802,11 +802,11 @@ namespace umbraco.cms.businesslogic.web
if (!e.Cancel)
{
var result = ((ContentService)ApplicationContext.Current.Services.ContentService).Publish(Content, false, u.Id);
_published = result;
_published = result.Success;
FireAfterPublish(e);
return result;
return result.Success;
}
else
{
@@ -944,7 +944,7 @@ namespace umbraco.cms.businesslogic.web
//Now we need to fire the After publish event
FireAfterPublish(publishArgs);
return result;
return result.Success;
}
return false;