Merge remote-tracking branch 'origin/temp8-3336-publish-branch' into temp8-224-db-updates-sched-publishing-with-variants

# Conflicts:
#	src/Umbraco.Core/Services/Implement/ContentService.cs
This commit is contained in:
Shannon
2018-11-07 22:09:51 +11:00
4 changed files with 598 additions and 136 deletions

View File

@@ -362,12 +362,34 @@ namespace Umbraco.Core.Services
/// </remarks>
PublishResult SavePublishing(IContent content, int userId = 0, bool raiseEvents = true);
/*
fixme - document this better + test
If the item being published is Invariant and it has Variant descendants and
we are NOT forcing publishing of anything not published - the result will be that the Variant cultures that are
already published (but may contain a draft) are published. Any cultures that don't have a published version are not published
fixme: now, if publishing '*' then all cultures
If the item being published is Invariant and it has Variant descendants and
we ARE forcing publishing of anything not published - the result will be that all Variant cultures are
published regardless of whether they don't have any current published versions
If the item being published is Variant and it has Invariant descendants and
we are NOT forcing publishing of anything not published - the result will be that all Invariant documents are
published that already have a published versions, regardless of what cultures are selected to be published
If the item being published is Variant and it has Invariant descendants and
we ARE forcing publishing of anything not published - the result will be that all Invariant documents are
published regardless of whether they have a published version or not and regardless of what cultures are selected to be published
*/
/// <summary>
/// Saves and publishes a document branch.
/// </summary>
/// <param name="content">The root document.</param>
/// <param name="force">A value indicating whether to force-publish documents that are not already published.</param>
/// <param name="culture">A culture, or "*" for all cultures.</param>
/// <param name="userId">The identifier of the user performing the operation.</param>
/// <remarks>
/// <para>Unless specified, all cultures are re-published. Otherwise, one culture can be specified. To act on more
/// that one culture, see the other overload of this method.</para>
/// than one culture, see the other overloads of this method.</para>
/// <para>The <paramref name="force"/> parameter determines which documents are published. When <c>false</c>,
/// only those documents that are already published, are republished. When <c>true</c>, all documents are
/// published.</para>
@@ -377,19 +399,38 @@ namespace Umbraco.Core.Services
/// <summary>
/// Saves and publishes a document branch.
/// </summary>
/// <param name="content">The root document.</param>
/// <param name="force">A value indicating whether to force-publish documents that are not already published.</param>
/// <param name="cultures">The cultures to publish.</param>
/// <param name="userId">The identifier of the user performing the operation.</param>
/// <remarks>
/// <para>The <paramref name="force"/> parameter determines which documents are published. When <c>false</c>,
/// only those documents that are already published, are republished. When <c>true</c>, all documents are
/// published.</para>
/// </remarks>
IEnumerable<PublishResult> SaveAndPublishBranch(IContent content, bool force, string[] cultures, int userId = 0);
/// <summary>
/// Saves and publishes a document branch.
/// </summary>
/// <param name="content">The root document.</param>
/// <param name="force">A value indicating whether to force-publish documents that are not already published.</param>
/// <param name="editing">A function determining whether a document has changes to publish.</param>
/// <param name="publishCultures">A function publishing cultures.</param>
/// <param name="userId">The identifier of the user performing the operation.</param>
/// <remarks>
/// <para>The <paramref name="force"/> parameter determines which documents are published. When <c>false</c>,
/// only those documents that are already published, are republished. When <c>true</c>, all documents are
/// published.</para>
/// <para>The <paramref name="editing"/> parameter is a function which determines whether a document has
/// values to publish (else there is no need to publish it). If one wants to publish only a selection of
/// changes to publish (else there is no need to publish it). If one wants to publish only a selection of
/// cultures, one may want to check that only properties for these cultures have changed. Otherwise, other
/// cultures may trigger an unwanted republish.</para>
/// <para>The <paramref name="publishCultures"/> parameter is a function to execute to publish cultures, on
/// each document. It can publish all, one, or a selection of cultures. It returns a boolean indicating
/// whether the cultures could be published.</para>
/// </remarks>
IEnumerable<PublishResult> SaveAndPublishBranch(IContent content, bool force, Func<IContent, bool> editing, Func<IContent, bool> publishCultures, int userId = 0);
IEnumerable<PublishResult> SaveAndPublishBranch(IContent content, bool force, Func<IContent, HashSet<string>> shouldPublish, Func<IContent, HashSet<string>, bool> publishCultures, int userId = 0);
/// <summary>
/// Unpublishes a document.

View File

@@ -953,6 +953,17 @@ namespace Umbraco.Core.Services.Implement
/// <inheritdoc />
public PublishResult SavePublishing(IContent content, int userId = 0, bool raiseEvents = true)
{
using (var scope = ScopeProvider.CreateScope())
{
scope.WriteLock(Constants.Locks.ContentTree);
var result = SavePublishingInternal(scope, content, userId, raiseEvents);
scope.Complete();
return result;
}
}
private PublishResult SavePublishingInternal(IScope scope, IContent content, int userId = 0, bool raiseEvents = true)
{
var evtMsgs = EventMessagesFactory.Get();
PublishResult publishResult = null;
@@ -960,7 +971,7 @@ namespace Umbraco.Core.Services.Implement
// nothing set = republish it all
if (content.PublishedState != PublishedState.Publishing && content.PublishedState != PublishedState.Unpublishing)
((Content) content).PublishedState = PublishedState.Publishing;
((Content)content).PublishedState = PublishedState.Publishing;
// state here is either Publishing or Unpublishing
var publishing = content.PublishedState == PublishedState.Publishing;
@@ -978,27 +989,22 @@ namespace Umbraco.Core.Services.Implement
using (var scope = ScopeProvider.CreateScope())
{
var isNew = !content.HasIdentity;
var changeType = isNew ? TreeChangeTypes.RefreshNode : TreeChangeTypes.RefreshBranch;
var previouslyPublished = content.HasIdentity && content.Published;
var isNew = !content.HasIdentity;
var changeType = isNew ? TreeChangeTypes.RefreshNode : TreeChangeTypes.RefreshBranch;
var previouslyPublished = content.HasIdentity && content.Published;
scope.WriteLock(Constants.Locks.ContentTree);
// always save
var saveEventArgs = new SaveEventArgs<IContent>(content, evtMsgs);
if (raiseEvents && scope.Events.DispatchCancelable(Saving, this, saveEventArgs, "Saving"))
{
scope.Complete();
// always save
var saveEventArgs = new SaveEventArgs<IContent>(content, evtMsgs);
if (raiseEvents && scope.Events.DispatchCancelable(Saving, this, saveEventArgs, "Saving"))
return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content);
}
if (publishing)
{
if (publishing)
{
culturesUnpublishing = content.GetCulturesUnpublishing();
// ensure that the document can be published, and publish handling events, business rules, etc
publishResult = StrategyCanPublish(scope, content, userId, /*checkPath:*/ true, evtMsgs);
if (publishResult.Success)
publishResult = StrategyCanPublish(scope, content, userId, /*checkPath:*/ true, evtMsgs);
if (publishResult.Success)
{
culturesPublishing = variesByCulture
? content.PublishCultureInfos.Where(x => x.Value.IsDirty()).Select(x => x.Key).ToList()
@@ -1025,20 +1031,20 @@ namespace Umbraco.Core.Services.Implement
}
}
if (unpublishing)
{
var newest = GetById(content.Id); // ensure we have the newest version - in scope
if (content.VersionId != newest.VersionId) // but use the original object if it's already the newest version
content = newest;
if (unpublishing)
{
var newest = GetById(content.Id); // ensure we have the newest version - in scope
if (content.VersionId != newest.VersionId) // but use the original object if it's already the newest version
content = newest;
if (content.Published)
{
// ensure that the document can be unpublished, and unpublish
// handling events, business rules, etc
// note: StrategyUnpublish flips the PublishedState to Unpublishing!
// note: This unpublishes the entire document (not different variants)
unpublishResult = StrategyCanUnpublish(scope, content, userId, evtMsgs);
if (unpublishResult.Success)
if (content.Published)
{
// ensure that the document can be unpublished, and unpublish
// handling events, business rules, etc
// note: StrategyUnpublish flips the PublishedState to Unpublishing!
// note: This unpublishes the entire document (not different variants)
unpublishResult = StrategyCanUnpublish(scope, content, userId, evtMsgs);
if (unpublishResult.Success)
unpublishResult = StrategyUnpublish(scope, content, userId, evtMsgs);
else
{
@@ -1046,37 +1052,37 @@ namespace Umbraco.Core.Services.Implement
((Content)content).Published = content.Published; // reset published state = save unchanged
}
}
else
{
// already unpublished - optimistic concurrency collision, really,
// and I am not sure at all what we should do, better die fast, else
// we may end up corrupting the db
throw new InvalidOperationException("Concurrency collision.");
}
}
// save, always
if (content.HasIdentity == false)
content.CreatorId = userId;
content.WriterId = userId;
// saving does NOT change the published version, unless PublishedState is Publishing or Unpublishing
_documentRepository.Save(content);
// raise the Saved event, always
if (raiseEvents)
else
{
saveEventArgs.CanCancel = false;
scope.Events.Dispatch(Saved, this, saveEventArgs, "Saved");
// already unpublished - optimistic concurrency collision, really,
// and I am not sure at all what we should do, better die fast, else
// we may end up corrupting the db
throw new InvalidOperationException("Concurrency collision.");
}
}
if (unpublishing) // we have tried to unpublish
// save, always
if (content.HasIdentity == false)
content.CreatorId = userId;
content.WriterId = userId;
// saving does NOT change the published version, unless PublishedState is Publishing or Unpublishing
_documentRepository.Save(content);
// raise the Saved event, always
if (raiseEvents)
{
saveEventArgs.CanCancel = false;
scope.Events.Dispatch(Saved, this, saveEventArgs, "Saved");
}
if (unpublishing) // we have tried to unpublish
{
if (unpublishResult.Success) // and succeeded, trigger events
{
if (unpublishResult.Success) // and succeeded, trigger events
{
// events and audit
scope.Events.Dispatch(Unpublished, this, new PublishEventArgs<IContent>(content, false, false), "Unpublished");
scope.Events.Dispatch(TreeChanged, this, new TreeChange<IContent>(content, TreeChangeTypes.RefreshBranch).ToEventArgs());
// events and audit
scope.Events.Dispatch(Unpublished, this, new PublishEventArgs<IContent>(content, false, false), "Unpublished");
scope.Events.Dispatch(TreeChanged, this, new TreeChange<IContent>(content, TreeChangeTypes.RefreshBranch).ToEventArgs());
if (culturesUnpublishing != null)
{
@@ -1091,35 +1097,33 @@ namespace Umbraco.Core.Services.Implement
else
Audit(AuditType.Unpublish, userId, content.Id);
scope.Complete();
return new PublishResult(PublishResultType.SuccessUnpublish, evtMsgs, content);
}
// or, failed
scope.Events.Dispatch(TreeChanged, this, new TreeChange<IContent>(content, changeType).ToEventArgs());
scope.Complete(); // compete the save
return new PublishResult(PublishResultType.FailedUnpublish, evtMsgs, content); // bah
}
if (publishing) // we have tried to publish
// or, failed
scope.Events.Dispatch(TreeChanged, this, new TreeChange<IContent>(content, changeType).ToEventArgs());
return new PublishResult(PublishResultType.FailedUnpublish, evtMsgs, content); // bah
}
if (publishing) // we have tried to publish
{
if (publishResult.Success) // and succeeded, trigger events
{
if (publishResult.Success) // and succeeded, trigger events
if (isNew == false && previouslyPublished == false)
changeType = TreeChangeTypes.RefreshBranch; // whole branch
// invalidate the node/branch
scope.Events.Dispatch(TreeChanged, this, new TreeChange<IContent>(content, changeType).ToEventArgs());
scope.Events.Dispatch(Published, this, new PublishEventArgs<IContent>(content, false, false), "Published");
// if was not published and now is... descendants that were 'published' (but
// had an unpublished ancestor) are 're-published' ie not explicitely published
// but back as 'published' nevertheless
if (isNew == false && previouslyPublished == false && HasChildren(content.Id))
{
if (isNew == false && previouslyPublished == false)
changeType = TreeChangeTypes.RefreshBranch; // whole branch
// invalidate the node/branch
scope.Events.Dispatch(TreeChanged, this, new TreeChange<IContent>(content, changeType).ToEventArgs());
scope.Events.Dispatch(Published, this, new PublishEventArgs<IContent>(content, false, false), "Published");
// if was not published and now is... descendants that were 'published' (but
// had an unpublished ancestor) are 're-published' ie not explicitely published
// but back as 'published' nevertheless
if (isNew == false && previouslyPublished == false && HasChildren(content.Id))
{
var descendants = GetPublishedDescendantsLocked(content).ToArray();
scope.Events.Dispatch(Published, this, new PublishEventArgs<IContent>(descendants, false, false), "Published");
}
var descendants = GetPublishedDescendantsLocked(content).ToArray();
scope.Events.Dispatch(Published, this, new PublishEventArgs<IContent>(descendants, false, false), "Published");
}
switch(publishResult.Result)
{
@@ -1146,9 +1150,8 @@ namespace Umbraco.Core.Services.Implement
break;
}
scope.Complete();
return publishResult;
}
return publishResult;
}
}
@@ -1167,10 +1170,8 @@ namespace Umbraco.Core.Services.Implement
}
// or, failed
scope.Events.Dispatch(TreeChanged, this, new TreeChange<IContent>(content, changeType).ToEventArgs());
scope.Complete(); // compete the save
scope.Events.Dispatch(TreeChanged, this, new TreeChange<IContent>(content, changeType).ToEventArgs());
return publishResult;
}
}
/// <inheritdoc />
@@ -1272,16 +1273,44 @@ namespace Umbraco.Core.Services.Implement
// note: EditedValue and PublishedValue are objects here, so it is important to .Equals()
// and not to == them, else we would be comparing references, and that is a bad thing
bool IsEditing(IContent c, string l)
=> c.PublishName != c.Name ||
c.PublishedCultures.Where(x => x.InvariantEquals(l)).Any(x => c.GetCultureName(x) != c.GetPublishName(x)) ||
c.Properties.Any(x => x.Values.Where(y => culture == "*" || y.Culture.InvariantEquals(l)).Any(y => !y.EditedValue.Equals(y.PublishedValue)));
// determines whether the document is edited, and thus needs to be published,
// for the specified culture (it may be edited for other cultures and that
// should not trigger a publish).
HashSet<string> ShouldPublish(IContent c)
{
if (c.ContentType.VariesByCulture())
{
// variant content type
// add culture if edited, and already published or forced
if (c.IsCultureEdited(culture) && (c.IsCulturePublished(culture) || force))
return new HashSet<string> { culture.ToLowerInvariant() };
}
else
{
// invariant content type
// add "*" if edited, and already published or forced
if (c.Edited && (c.Published || force))
return new HashSet<string> { "*" };
}
return SaveAndPublishBranch(content, force, document => IsEditing(document, culture), document => document.PublishCulture(culture), userId);
return new HashSet<string>();
}
// publish the specified cultures
bool PublishCultures(IContent c, HashSet<string> culturesToPublish)
{
// variant content type - publish specified cultures
// invariant content type - publish only the invariant culture
return c.ContentType.VariesByCulture()
? culturesToPublish.All(c.PublishCulture)
: c.PublishCulture();
}
return SaveAndPublishBranch(content, force, ShouldPublish, PublishCultures, userId);
}
// fixme - make this public once we know it works + document
private IEnumerable<PublishResult> SaveAndPublishBranch(IContent content, bool force, string[] cultures, int userId = 0)
/// <inheritdoc />
public IEnumerable<PublishResult> SaveAndPublishBranch(IContent content, bool force, string[] cultures, int userId = 0)
{
// note: EditedValue and PublishedValue are objects here, so it is important to .Equals()
// and not to == them, else we would be comparing references, and that is a bad thing
@@ -1291,35 +1320,47 @@ namespace Umbraco.Core.Services.Implement
// determines whether the document is edited, and thus needs to be published,
// for the specified cultures (it may be edited for other cultures and that
// should not trigger a publish).
bool IsEdited(IContent c)
HashSet<string> ShouldPublish(IContent c)
{
if (cultures.Length == 0)
var culturesToPublish = new HashSet<string>();
if (c.ContentType.VariesByCulture())
{
// nothing = everything
return c.PublishName != c.Name ||
c.PublishedCultures.Any(x => c.GetCultureName(x) != c.GetPublishName(x)) ||
c.Properties.Any(x => x.Values.Any(y => !y.EditedValue.Equals(y.PublishedValue)));
// variant content type
// add cultures which are edited, and already published or forced
foreach (var culture in cultures)
{
if (c.IsCultureEdited(culture) && (c.IsCulturePublished(culture) || force))
culturesToPublish.Add(culture.ToLowerInvariant());
}
}
else
{
// invariant content type
// add "*" if edited, and already published or forced
if (c.Edited && (c.Published || force))
culturesToPublish.Add("*");
}
return c.PublishName != c.Name ||
c.PublishedCultures.Where(x => cultures.Contains(x, StringComparer.InvariantCultureIgnoreCase)).Any(x => c.GetCultureName(x) != c.GetPublishName(x)) ||
c.Properties.Any(x => x.Values.Where(y => cultures.Contains(y.Culture, StringComparer.InvariantCultureIgnoreCase)).Any(y => !y.EditedValue.Equals(y.PublishedValue)));
return culturesToPublish;
}
// publish the specified cultures
bool PublishCultures(IContent c)
bool PublishCultures(IContent c, HashSet<string> culturesToPublish)
{
return cultures.Length == 0
? c.PublishCulture() // nothing = everything
: cultures.All(c.PublishCulture);
// variant content type - publish specified cultures
// invariant content type - publish only the invariant culture
return c.ContentType.VariesByCulture()
? culturesToPublish.All(c.PublishCulture)
: c.PublishCulture();
}
return SaveAndPublishBranch(content, force, IsEdited, PublishCultures, userId);
return SaveAndPublishBranch(content, force, ShouldPublish, PublishCultures, userId);
}
/// <inheritdoc />
public IEnumerable<PublishResult> SaveAndPublishBranch(IContent document, bool force,
Func<IContent, bool> editing, Func<IContent, bool> publishCultures, int userId = 0)
Func<IContent, HashSet<string>> shouldPublish, Func<IContent, HashSet<string>, bool> publishCultures, int userId = 0)
{
var evtMsgs = EventMessagesFactory.Get();
var results = new List<PublishResult>();
@@ -1332,14 +1373,14 @@ namespace Umbraco.Core.Services.Implement
// fixme events?!
if (!document.HasIdentity)
throw new InvalidOperationException("Do not branch-publish a new document.");
throw new InvalidOperationException("Cannot not branch-publish a new document.");
var publishedState = ((Content) document).PublishedState;
if (publishedState == PublishedState.Publishing)
throw new InvalidOperationException("Do not publish values when publishing branches.");
throw new InvalidOperationException("Cannot mix PublishCulture and SaveAndPublishBranch.");
// deal with the branch root - if it fails, abort
var result = SaveAndPublishBranchOne(scope, document, editing, publishCultures, true, publishedDocuments, evtMsgs, userId);
var result = SaveAndPublishBranchOne(scope, document, shouldPublish, publishCultures, true, publishedDocuments, evtMsgs, userId);
results.Add(result);
if (!result.Success) return results;
@@ -1347,35 +1388,36 @@ namespace Umbraco.Core.Services.Implement
// if one fails, abort its branch
var exclude = new HashSet<int>();
const int pageSize = 500;
int count;
var page = 0;
var total = long.MaxValue;
while (page * pageSize < total)
const int pageSize = 100;
do
{
var descendants = GetPagedDescendants(document.Id, page++, pageSize, out total);
foreach (var d in descendants)
count = 0;
// important to order by Path ASC so make it explicit in case defaults change
// ReSharper disable once RedundantArgumentDefaultValue
foreach (var d in GetPagedDescendants(document.Id, page, pageSize, out _, ordering: Ordering.By("Path", Direction.Ascending)))
{
// if parent is excluded, exclude document and ignore
// if not forcing, and not publishing, exclude document and ignore
if (exclude.Contains(d.ParentId) || !force && !d.Published)
count++;
// if parent is excluded, exclude child too
if (exclude.Contains(d.ParentId))
{
exclude.Add(d.Id);
continue;
}
// no need to check path here,
// 1. because we know the parent is path-published (we just published it)
// 2. because it would not work as nothing's been written out to the db until the uow completes
result = SaveAndPublishBranchOne(scope, d, editing, publishCultures, false, publishedDocuments, evtMsgs, userId);
// no need to check path here, parent has to be published here
result = SaveAndPublishBranchOne(scope, d, shouldPublish, publishCultures, false, publishedDocuments, evtMsgs, userId);
results.Add(result);
if (result.Success) continue;
// abort branch
// if we could not publish the document, cut its branch
exclude.Add(d.Id);
}
}
page++;
} while (count > 0);
scope.Events.Dispatch(TreeChanged, this, new TreeChange<IContent>(document, TreeChangeTypes.RefreshBranch).ToEventArgs());
scope.Events.Dispatch(Published, this, new PublishEventArgs<IContent>(publishedDocuments, false, false), "Published");
@@ -1387,21 +1429,31 @@ namespace Umbraco.Core.Services.Implement
return results;
}
// shouldPublish: a function determining whether the document has changes that need to be published
// note - 'force' is handled by 'editing'
// publishValues: a function publishing values (using the appropriate PublishCulture calls)
private PublishResult SaveAndPublishBranchOne(IScope scope, IContent document,
Func<IContent, bool> editing, Func<IContent, bool> publishValues,
Func<IContent, HashSet<string>> shouldPublish, Func<IContent, HashSet<string>, bool> publishCultures,
bool checkPath,
List<IContent> publishedDocuments,
ICollection<IContent> publishedDocuments,
EventMessages evtMsgs, int userId)
{
// if already published, and values haven't changed - i.e. not changing anything
// nothing to do - fixme - unless we *want* to bump dates?
if (document.Published && (editing == null || !editing(document)))
// use 'shouldPublish' to determine whether there are changes to be published
// if the document has no changes to be published - nothing to
// for an invariant content, shouldPublish may contain "*" to indicate changes
var culturesToPublish = shouldPublish?.Invoke(document);
if (culturesToPublish == null || culturesToPublish.Count == 0)
return new PublishResult(PublishResultType.SuccessPublishAlready, evtMsgs, document);
// there are changes to be published - publish them with 'publishCultures'
// publish & check if values are valid
if (publishValues != null && !publishValues(document))
if (publishCultures != null && !publishCultures(document, culturesToPublish))
return new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, document);
return SavePublishingInternal(scope, document, userId);
// fixme deal with the rest of this code!
// check if we can publish
var result = StrategyCanPublish(scope, document, userId, checkPath, evtMsgs);
if (!result.Success)

View File

@@ -0,0 +1,368 @@
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using Umbraco.Core;
using Umbraco.Core.Models;
using Umbraco.Core.Services;
using Umbraco.Tests.TestHelpers;
using Umbraco.Tests.Testing;
// ReSharper disable CommentTypo
// ReSharper disable StringLiteralTypo
namespace Umbraco.Tests.Services
{
[TestFixture]
[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest, PublishedRepositoryEvents = true, WithApplication = true)]
public class ContentServicePublishBranchTests : TestWithDatabaseBase
{
[TestCase(1)] // use overload w/ culture: "*"
[TestCase(2)] // use overload w/ cultures: new [] { "*" }
public void Can_Publish_InvariantBranch(int method)
{
CreateTypes(out var iContentType, out _);
IContent iRoot = new Content("iroot", -1, iContentType);
iRoot.SetValue("ip", "iroot");
ServiceContext.ContentService.Save(iRoot);
IContent ii1 = new Content("ii1", iRoot, iContentType);
ii1.SetValue("ip", "vii1");
ServiceContext.ContentService.Save(ii1);
IContent ii2 = new Content("ii2", iRoot, iContentType);
ii2.SetValue("ip", "vii2");
ServiceContext.ContentService.Save(ii2);
// iroot !published !edited
// ii1 !published !edited
// ii2 !published !edited
// !force = publishes those that are actually published, and have changes
// here: nothing
var r = SaveAndPublishInvariantBranch(iRoot, false, method).ToArray();
AssertPublishResults(r, x => x.Content.Name,
"iroot", "ii1", "ii2");
AssertPublishResults(r, x => x.Result,
PublishResultType.SuccessAlready,
PublishResultType.SuccessAlready,
PublishResultType.SuccessAlready);
// prepare
ServiceContext.ContentService.SaveAndPublish(iRoot);
ServiceContext.ContentService.SaveAndPublish(ii1);
IContent ii11 = new Content("ii11", ii1, iContentType);
ii11.SetValue("ip", "vii11");
ServiceContext.ContentService.SaveAndPublish(ii11);
IContent ii12 = new Content("ii12", ii1, iContentType);
ii11.SetValue("ip", "vii12");
ServiceContext.ContentService.Save(ii12);
ServiceContext.ContentService.SaveAndPublish(ii2);
IContent ii21 = new Content("ii21", ii2, iContentType);
ii21.SetValue("ip", "vii21");
ServiceContext.ContentService.SaveAndPublish(ii21);
IContent ii22 = new Content("ii22", ii2, iContentType);
ii22.SetValue("ip", "vii22");
ServiceContext.ContentService.Save(ii22);
ServiceContext.ContentService.Unpublish(ii2);
// iroot published !edited
// ii1 published !edited
// ii11 published !edited
// ii12 !published !edited
// ii2 !published !edited
// ii21 (published) !edited
// ii22 !published !edited
// !force = publishes those that are actually published, and have changes
// here: nothing
r = SaveAndPublishInvariantBranch(iRoot, false, method).ToArray();
AssertPublishResults(r, x => x.Content.Name,
"iroot", "ii1", "ii11", "ii12", "ii2", "ii21", "ii22");
AssertPublishResults(r, x => x.Result,
PublishResultType.SuccessAlready,
PublishResultType.SuccessAlready,
PublishResultType.SuccessAlready,
PublishResultType.SuccessAlready,
PublishResultType.SuccessAlready,
PublishResultType.SuccessAlready,
PublishResultType.SuccessAlready);
// prepare
iRoot.SetValue("ip", "changed");
ServiceContext.ContentService.Save(iRoot);
ii11.SetValue("ip", "changed");
ServiceContext.ContentService.Save(ii11);
// iroot published edited ***
// ii1 published !edited
// ii11 published edited ***
// ii12 !published !edited
// ii2 !published !edited
// ii21 (published) !edited
// ii22 !published !edited
// !force = publishes those that are actually published, and have changes
// here: iroot and ii11
r = SaveAndPublishInvariantBranch(iRoot, false, method).ToArray();
AssertPublishResults(r, x => x.Content.Name,
"iroot", "ii1", "ii11", "ii12", "ii2", "ii21", "ii22");
AssertPublishResults(r, x => x.Result,
PublishResultType.Success,
PublishResultType.SuccessAlready,
PublishResultType.Success,
PublishResultType.SuccessAlready,
PublishResultType.SuccessAlready,
PublishResultType.SuccessAlready,
PublishResultType.SuccessAlready);
// force = publishes everything that has changes
// here: ii12, ii2, ii22 - ii21 was published already but masked
r = SaveAndPublishInvariantBranch(iRoot, true, method).ToArray();
AssertPublishResults(r, x => x.Content.Name,
"iroot", "ii1", "ii11", "ii12", "ii2", "ii21", "ii22");
AssertPublishResults(r, x => x.Result,
PublishResultType.SuccessAlready,
PublishResultType.SuccessAlready,
PublishResultType.SuccessAlready,
PublishResultType.Success,
PublishResultType.Success,
PublishResultType.SuccessAlready, // was masked
PublishResultType.Success);
ii21 = ServiceContext.ContentService.GetById(ii21.Id);
Assert.IsTrue(ii21.Published);
}
[Test]
public void Can_Publish_VariantBranch()
{
CreateTypes(out _, out var vContentType);
IContent vRoot = new Content("vroot", -1, vContentType, "de");
vRoot.SetCultureName("vroot.de", "de");
vRoot.SetCultureName("vroot.ru", "ru");
vRoot.SetCultureName("vroot.es", "es");
vRoot.SetValue("ip", "vroot");
vRoot.SetValue("vp", "vroot.de", "de");
vRoot.SetValue("vp", "vroot.ru", "ru");
vRoot.SetValue("vp", "vroot.es", "es");
ServiceContext.ContentService.Save(vRoot);
IContent iv1 = new Content("iv1", vRoot, vContentType, "de");
iv1.SetCultureName("iv1.de", "de");
iv1.SetCultureName("iv1.ru", "ru");
iv1.SetCultureName("iv1.es", "es");
iv1.SetValue("ip", "iv1");
iv1.SetValue("vp", "iv1.de", "de");
iv1.SetValue("vp", "iv1.ru", "ru");
iv1.SetValue("vp", "iv1.es", "es");
ServiceContext.ContentService.Save(iv1);
IContent iv2 = new Content("iv2", vRoot, vContentType, "de");
iv2.SetCultureName("iv2.de", "de");
iv2.SetCultureName("iv2.ru", "ru");
iv2.SetCultureName("iv2.es", "es");
iv2.SetValue("ip", "iv2");
iv2.SetValue("vp", "iv2.de", "de");
iv2.SetValue("vp", "iv2.ru", "ru");
iv2.SetValue("vp", "iv2.es", "es");
ServiceContext.ContentService.Save(iv2);
// vroot !published !edited
// iv1 !published !edited
// iv2 !published !edited
// !force = publishes those that are actually published, and have changes
// here: nothing
var r = ServiceContext.ContentService.SaveAndPublishBranch(vRoot, false).ToArray();
AssertPublishResults(r, x => x.Content.Name,
"vroot.de", "iv1.de", "iv2.de");
AssertPublishResults(r, x => x.Result,
PublishResultType.SuccessAlready,
PublishResultType.SuccessAlready,
PublishResultType.SuccessAlready);
// prepare
ServiceContext.ContentService.SaveAndPublish(vRoot, "de");
vRoot.SetValue("ip", "changed");
vRoot.SetValue("vp", "changed.de", "de");
vRoot.SetValue("vp", "changed.ru", "ru");
vRoot.SetValue("vp", "changed.es", "es");
ServiceContext.ContentService.Save(vRoot);
iv1.PublishCulture("de");
iv1.PublishCulture("ru");
ServiceContext.ContentService.SavePublishing(iv1);
iv1.SetValue("ip", "changed");
iv1.SetValue("vp", "changed.de", "de");
iv1.SetValue("vp", "changed.ru", "ru");
iv1.SetValue("vp", "changed.es", "es");
ServiceContext.ContentService.Save(iv1);
// validate
Assert.IsTrue(vRoot.Published);
Assert.IsTrue(vRoot.IsCulturePublished("de"));
Assert.IsFalse(vRoot.IsCulturePublished("ru"));
Assert.IsFalse(vRoot.IsCulturePublished("es"));
Assert.IsTrue(iv1.Published);
Assert.IsTrue(iv1.IsCulturePublished("de"));
Assert.IsTrue(iv1.IsCulturePublished("ru"));
Assert.IsFalse(vRoot.IsCulturePublished("es"));
r = ServiceContext.ContentService.SaveAndPublishBranch(vRoot, false, "de").ToArray();
AssertPublishResults(r, x => x.Content.Name,
"vroot.de", "iv1.de", "iv2.de");
AssertPublishResults(r, x => x.Result,
PublishResultType.Success,
PublishResultType.Success,
PublishResultType.SuccessAlready);
// reload - SaveAndPublishBranch has modified other instances
Reload(ref iv1);
Reload(ref iv2);
// de is published, ru and es have not been published
Assert.IsTrue(vRoot.Published);
Assert.IsTrue(vRoot.IsCulturePublished("de"));
Assert.IsFalse(vRoot.IsCulturePublished("ru"));
Assert.IsFalse(vRoot.IsCulturePublished("es"));
Assert.AreEqual("changed", vRoot.GetValue("ip", published: true)); // publishing de implies publishing invariants
Assert.AreEqual("changed.de", vRoot.GetValue("vp", "de", published: true));
// de and ru are published, es has not been published
Assert.IsTrue(iv1.Published);
Assert.IsTrue(iv1.IsCulturePublished("de"));
Assert.IsTrue(iv1.IsCulturePublished("ru"));
Assert.IsFalse(vRoot.IsCulturePublished("es"));
Assert.AreEqual("changed", iv1.GetValue("ip", published: true));
Assert.AreEqual("changed.de", iv1.GetValue("vp", "de", published: true));
Assert.AreEqual("iv1.ru", iv1.GetValue("vp", "ru", published: true));
}
[Test]
public void Can_Publish_MixedBranch()
{
// invariant root -> variant -> invariant
// variant root -> variant -> invariant
// variant root -> invariant -> variant
CreateTypes(out var iContentType, out var vContentType);
// invariant root -> invariant -> variant
IContent iRoot = new Content("iroot", -1, iContentType);
iRoot.SetValue("ip", "iroot");
ServiceContext.ContentService.SaveAndPublish(iRoot);
IContent ii1 = new Content("ii1", iRoot, iContentType);
ii1.SetValue("ip", "vii1");
ServiceContext.ContentService.SaveAndPublish(ii1);
ii1.SetValue("ip", "changed");
ServiceContext.ContentService.Save(ii1);
IContent iv11 = new Content("iv11.de", ii1, vContentType, "de");
iv11.SetValue("ip", "iv11");
iv11.SetValue("vp", "iv11.de", "de");
iv11.SetValue("vp", "iv11.ru", "ru");
iv11.SetValue("vp", "iv11.es", "es");
ServiceContext.ContentService.Save(iv11);
iv11.PublishCulture("de");
iv11.SetCultureName("iv11.ru", "ru");
iv11.PublishCulture("ru");
ServiceContext.ContentService.SavePublishing(iv11);
Assert.AreEqual("iv11.de", iv11.GetValue("vp", "de", published: true));
Assert.AreEqual("iv11.ru", iv11.GetValue("vp", "ru", published: true));
iv11.SetValue("ip", "changed");
iv11.SetValue("vp", "changed.de", "de");
iv11.SetValue("vp", "changed.ru", "ru");
ServiceContext.ContentService.Save(iv11);
var r = ServiceContext.ContentService.SaveAndPublishBranch(iRoot, false, "de").ToArray();
AssertPublishResults(r, x => x.Content.Name,
"iroot", "ii1", "iv11.de");
AssertPublishResults(r, x => x.Result,
PublishResultType.SuccessAlready,
PublishResultType.Success,
PublishResultType.Success);
// reload - SaveAndPublishBranch has modified other instances
Reload(ref ii1);
Reload(ref iv11);
// the invariant child has been published
// the variant child has been published for 'de' only
Assert.AreEqual("changed", ii1.GetValue("ip", published: true));
Assert.AreEqual("changed", iv11.GetValue("ip", published: true));
Assert.AreEqual("changed.de", iv11.GetValue("vp", "de", published: true));
Assert.AreEqual("iv11.ru", iv11.GetValue("vp", "ru", published: true));
}
private void AssertPublishResults<T>(PublishResult[] values, Func<PublishResult, T> getter, params T[] expected)
{
Assert.AreEqual(expected.Length, values.Length);
for (var i = 0; i < values.Length; i++)
{
var value = getter(values[i]);
Assert.AreEqual(expected[i], value, $"Expected {expected[i]} at {i} but got {value}.");
}
}
private void Reload(ref IContent document)
=> document = ServiceContext.ContentService.GetById(document.Id);
private void CreateTypes(out IContentType iContentType, out IContentType vContentType)
{
var langDe = new Language("de") { IsDefault = true };
ServiceContext.LocalizationService.Save(langDe);
var langRu = new Language("ru");
ServiceContext.LocalizationService.Save(langRu);
var langEs = new Language("es");
ServiceContext.LocalizationService.Save(langEs);
iContentType = new ContentType(-1)
{
Alias = "ict",
Name = "Invariant Content Type",
Variations = ContentVariation.Nothing
};
iContentType.AddPropertyType(new PropertyType(Constants.PropertyEditors.Aliases.TextBox, ValueStorageType.Nvarchar, "ip") { Variations = ContentVariation.Nothing });
ServiceContext.ContentTypeService.Save(iContentType);
vContentType = new ContentType(-1)
{
Alias = "vct",
Name = "Variant Content Type",
Variations = ContentVariation.Culture
};
vContentType.AddPropertyType(new PropertyType(Constants.PropertyEditors.Aliases.TextBox, ValueStorageType.Nvarchar, "ip") { Variations = ContentVariation.Nothing });
vContentType.AddPropertyType(new PropertyType(Constants.PropertyEditors.Aliases.TextBox, ValueStorageType.Nvarchar, "vp") { Variations = ContentVariation.Culture });
ServiceContext.ContentTypeService.Save(vContentType);
}
private IEnumerable<PublishResult> SaveAndPublishInvariantBranch(IContent content, bool force, int method)
{
// ReSharper disable RedundantArgumentDefaultValue
// ReSharper disable ArgumentsStyleOther
switch (method)
{
case 1:
return ServiceContext.ContentService.SaveAndPublishBranch(content, force, culture: "*");
case 2:
return ServiceContext.ContentService.SaveAndPublishBranch(content, force, cultures: new [] { "*" });
default:
throw new ArgumentOutOfRangeException(nameof(method));
}
// ReSharper restore RedundantArgumentDefaultValue
// ReSharper restore ArgumentsStyleOther
}
}
}

View File

@@ -130,6 +130,7 @@
<Compile Include="PublishedContent\PublishedContentSnapshotTestBase.cs" />
<Compile Include="PublishedContent\SolidPublishedSnapshot.cs" />
<Compile Include="PublishedContent\NuCacheTests.cs" />
<Compile Include="Services\ContentServicePublishBranchTests.cs" />
<Compile Include="Services\ContentTypeServiceVariantsTests.cs" />
<Compile Include="Testing\Objects\TestDataSource.cs" />
<Compile Include="Published\PublishedSnapshotTestObjects.cs" />