Merge remote-tracking branch 'origin/temp8-224-db-updates-sched-publishing-with-variants' into temp8-226-sched-pub-angular

This commit is contained in:
Shannon
2018-11-14 17:40:04 +11:00
8 changed files with 165 additions and 129 deletions

View File

@@ -217,7 +217,7 @@ namespace Umbraco.Core.Models
public bool WasCulturePublished(string culture)
// just check _publishInfosOrig - a copy of _publishInfos
// a non-available culture could not become published anyways
=> _publishInfosOrig != null && _publishInfosOrig.ContainsKey(culture);
=> _publishInfosOrig != null && _publishInfosOrig.ContainsKey(culture);
// adjust dates to sync between version, cultures etc
// used by the repo when persisting
@@ -297,13 +297,10 @@ namespace Umbraco.Core.Models
if (_publishInfos == null) return;
_publishInfos.Remove(culture);
//we need to set the culture name to be dirty so we know it's being modified
//fixme is there a better way to do this, not as far as i know.
// fixme why do we need this?
SetCultureName(GetCultureName(culture), culture);
if (_publishInfos.Count == 0) _publishInfos = null;
// set the culture to be dirty - it's been modified
TouchCultureInfo(culture);
}
// sets a publish edited
@@ -522,7 +519,6 @@ namespace Umbraco.Core.Models
clonedContent._schedule = (ContentScheduleCollection)_schedule.DeepClone(); //manually deep clone
clonedContent._schedule.CollectionChanged += clonedContent.ScheduleCollectionChanged; //re-assign correct event handler
}
}
}
}

View File

@@ -159,7 +159,7 @@ namespace Umbraco.Core.Models
/// <inheritdoc />
[DataMember]
public virtual IReadOnlyDictionary<string, ContentCultureInfos> CultureInfos => _cultureInfos ?? NoInfos;
/// <inheritdoc />
public string GetCultureName(string culture)
{
@@ -222,6 +222,12 @@ namespace Umbraco.Core.Models
_cultureInfos = null;
}
protected void TouchCultureInfo(string culture)
{
if (_cultureInfos == null || !_cultureInfos.TryGetValue(culture, out var infos)) return;
_cultureInfos.AddOrUpdate(culture, infos.Name, DateTime.Now);
}
// internal for repository
internal void SetCultureInfo(string culture, string name, DateTime date)
{
@@ -235,7 +241,7 @@ namespace Umbraco.Core.Models
{
_cultureInfos = new ContentCultureInfosCollection();
_cultureInfos.CollectionChanged += CultureInfosCollectionChanged;
}
}
_cultureInfos.AddOrUpdate(culture, name, date);
}
@@ -368,7 +374,7 @@ namespace Umbraco.Core.Models
#endregion
#region Validation
/// <inheritdoc />
public virtual Property[] ValidateProperties(string culture = "*")
{
@@ -496,7 +502,6 @@ namespace Umbraco.Core.Models
clonedContent._properties = (PropertyCollection) _properties.DeepClone(); //manually deep clone
clonedContent._properties.CollectionChanged += clonedContent.PropertiesChanged; //re-assign correct event handler
}
}
}
}

View File

@@ -8,15 +8,13 @@ namespace Umbraco.Core.Persistence.Repositories
public interface IDocumentRepository : IContentRepository<int, IContent>, IReadRepository<Guid, IContent>
{
/// <summary>
/// Clears the publishing schedule for all entries before this date
/// Clears the publishing schedule for all entries having an a date before (lower than, or equal to) a specified date.
/// </summary>
/// <param name="date"></param>
void ClearSchedule(DateTime date);
/// <summary>
/// Gets a collection of <see cref="IContent"/> objects, which has an expiration date less than or equal to today.
/// Gets <see cref="IContent"/> objects having an expiration date before (lower than, or equal to) a specified date.
/// </summary>
/// <returns></returns>
/// <remarks>
/// The content returned from this method may be culture variant, in which case the resulting <see cref="IContent.ContentSchedule"/> should be queried
/// for which culture(s) have been scheduled.
@@ -24,9 +22,8 @@ namespace Umbraco.Core.Persistence.Repositories
IEnumerable<IContent> GetContentForExpiration(DateTime date);
/// <summary>
/// Gets a collection of <see cref="IContent"/> objects, which has a release date less than or equal to today.
/// Gets <see cref="IContent"/> objects having a release date before (lower than, or equal to) a specified date.
/// </summary>
/// <returns>An Enumerable list of <see cref="TEntity"/> objects</returns>
/// <remarks>
/// The content returned from this method may be culture variant, in which case the resulting <see cref="IContent.ContentSchedule"/> should be queried
/// for which culture(s) have been scheduled.

View File

@@ -955,7 +955,7 @@ namespace Umbraco.Core.Services.Implement
}
}
private PublishResult SavePublishingInternal(IScope scope, IContent content, int userId = 0, bool raiseEvents = true)
private PublishResult SavePublishingInternal(IScope scope, IContent content, int userId = 0, bool raiseEvents = true, bool branchOne = false, bool branchRoot = false)
{
var evtMsgs = EventMessagesFactory.Get();
PublishResult publishResult = null;
@@ -996,7 +996,7 @@ namespace Umbraco.Core.Services.Implement
: null;
// ensure that the document can be published, and publish handling events, business rules, etc
publishResult = StrategyCanPublish(scope, content, userId, /*checkPath:*/ true, culturesPublishing, culturesUnpublishing, evtMsgs);
publishResult = StrategyCanPublish(scope, content, userId, /*checkPath:*/ (!branchOne || branchRoot), culturesPublishing, culturesUnpublishing, evtMsgs);
if (publishResult.Success)
{
// note: StrategyPublish flips the PublishedState to Publishing!
@@ -1004,7 +1004,11 @@ namespace Umbraco.Core.Services.Implement
}
else
{
//check for mandatory culture missing, if this is the case we'll switch the unpublishing flag
// in a branch, just give up
if (branchOne && !branchRoot)
return publishResult;
//check for mandatory culture missing, and then unpublish document as a whole
if (publishResult.Result == PublishResultType.FailedPublishMandatoryCultureMissing)
{
publishing = false;
@@ -1015,15 +1019,17 @@ namespace Umbraco.Core.Services.Implement
}
//fixme - casting
((Content)content).Published = content.Published; // reset published state = save unchanged - fixme doh?
// reset published state from temp values (publishing, unpublishing) to original value
// (published, unpublished) in order to save the document, unchanged
((Content)content).Published = content.Published;
}
}
if (unpublishing)
if (unpublishing) // won't happen in a branch
{
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; // fixme confusing should just die here - else we'll miss some changes
if (content.VersionId != newest.VersionId)
return new PublishResult(PublishResultType.FailedPublishConcurrencyViolation, evtMsgs, content);
if (content.Published)
{
@@ -1037,7 +1043,9 @@ namespace Umbraco.Core.Services.Implement
else
{
//fixme - casting
((Content)content).Published = content.Published; // reset published state = save unchanged
// reset published state from temp values (publishing, unpublishing) to original value
// (published, unpublished) in order to save the document, unchanged
((Content)content).Published = content.Published;
}
}
else
@@ -1064,7 +1072,7 @@ namespace Umbraco.Core.Services.Implement
scope.Events.Dispatch(Saved, this, saveEventArgs, "Saved");
}
if (unpublishing) // we have tried to unpublish
if (unpublishing) // we have tried to unpublish - won't happen in a branch
{
if (unpublishResult.Success) // and succeeded, trigger events
{
@@ -1101,13 +1109,14 @@ namespace Umbraco.Core.Services.Implement
changeType = TreeChangeTypes.RefreshBranch; // whole branch
// invalidate the node/branch
scope.Events.Dispatch(TreeChanged, this, new TreeChange<IContent>(content, changeType).ToEventArgs());
if (!branchOne || branchRoot)
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 (!branchOne && isNew == false && previouslyPublished == false && HasChildren(content.Id))
{
var descendants = GetPublishedDescendantsLocked(content).ToArray();
scope.Events.Dispatch(Published, this, new PublishEventArgs<IContent>(descendants, false, false), "Published");
@@ -1140,11 +1149,14 @@ namespace Umbraco.Core.Services.Implement
return publishResult;
}
}
// should not happen
if (branchOne && !branchRoot)
throw new Exception("panic");
//if publishing didn't happen or if it has failed, we still need to log which cultures were saved
if (publishResult == null || !publishResult.Success)
if (!branchOne && (publishResult == null || !publishResult.Success))
{
if (culturesChanging != null)
{
@@ -1154,7 +1166,9 @@ namespace Umbraco.Core.Services.Implement
Audit(AuditType.SaveVariant, userId, content.Id, $"Saved languages: {langs}", langs);
}
else
{
Audit(AuditType.Save, userId, content.Id);
}
}
// or, failed
@@ -1164,6 +1178,12 @@ namespace Umbraco.Core.Services.Implement
/// <inheritdoc />
public IEnumerable<PublishResult> PerformScheduledPublish(DateTime date)
=> PerformScheduledPublishInternal(date).ToList();
// beware! this method yields results, so the returned IEnumerable *must* be
// enumerated for anything to happen - dangerous, so private + exposed via
// the public method above, which forces ToList().
private IEnumerable<PublishResult> PerformScheduledPublishInternal(DateTime date)
{
var evtMsgs = EventMessagesFactory.Get();
@@ -1171,61 +1191,64 @@ namespace Umbraco.Core.Services.Implement
{
scope.WriteLock(Constants.Locks.ContentTree);
var now = date;
foreach (var d in _documentRepository.GetContentForRelease(now))
foreach (var d in _documentRepository.GetContentForRelease(date))
{
PublishResult result;
if (d.ContentType.VariesByCulture())
{
//find which cultures have pending schedules
var pendingCultures = d.ContentSchedule.GetPending(ContentScheduleChange.Start, now)
var pendingCultures = d.ContentSchedule.GetPending(ContentScheduleChange.Start, date)
.Select(x => x.Culture)
.Distinct()
.ToList();
foreach (var c in pendingCultures)
var publishing = true;
foreach (var culture in pendingCultures)
{
//Clear this schedule for this culture
d.ContentSchedule.Clear(c, ContentScheduleChange.Start, now);
if (!d.Trashed)
d.PublishCulture(c); //set the culture to be published
d.ContentSchedule.Clear(culture, ContentScheduleChange.Start, date);
if (d.Trashed) continue; // won't publish
publishing &= d.PublishCulture(culture); //set the culture to be published
if (!publishing) break; // no point continuing
}
if (pendingCultures.Count > 0)
{
if (!d.Trashed)
result = SavePublishing(d, d.WriterId);
else
result = new PublishResult(PublishResultType.FailedPublishPathNotPublished, evtMsgs, d);
if (d.Trashed)
result = new PublishResult(PublishResultType.FailedPublishIsTrashed, evtMsgs, d);
else if (!publishing)
result = new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, d);
else
result = SavePublishing(d, d.WriterId);
if (result.Success == false)
Logger.Error<ContentService>(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result);
yield return result;
}
if (result.Success == false)
Logger.Error<ContentService>(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result);
yield return result;
}
else
{
//Clear this schedule
d.ContentSchedule.Clear(ContentScheduleChange.Start, now);
if (!d.Trashed)
result = SaveAndPublish(d, userId: d.WriterId);
else
result = new PublishResult(PublishResultType.FailedPublishPathNotPublished, evtMsgs, d);
d.ContentSchedule.Clear(ContentScheduleChange.Start, date);
result = d.Trashed
? new PublishResult(PublishResultType.FailedPublishIsTrashed, evtMsgs, d)
: SaveAndPublish(d, userId: d.WriterId);
if (result.Success == false)
Logger.Error<ContentService>(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result);
yield return result;
}
}
foreach (var d in _documentRepository.GetContentForExpiration(now))
foreach (var d in _documentRepository.GetContentForExpiration(date))
{
PublishResult result;
if (d.ContentType.VariesByCulture())
{
//find which cultures have pending schedules
var pendingCultures = d.ContentSchedule.GetPending(ContentScheduleChange.End, now)
var pendingCultures = d.ContentSchedule.GetPending(ContentScheduleChange.End, date)
.Select(x => x.Culture)
.Distinct()
.ToList();
@@ -1233,7 +1256,7 @@ namespace Umbraco.Core.Services.Implement
foreach (var c in pendingCultures)
{
//Clear this schedule for this culture
d.ContentSchedule.Clear(c, ContentScheduleChange.End, now);
d.ContentSchedule.Clear(c, ContentScheduleChange.End, date);
//set the culture to be published
d.UnpublishCulture(c);
}
@@ -1249,7 +1272,7 @@ namespace Umbraco.Core.Services.Implement
else
{
//Clear this schedule
d.ContentSchedule.Clear(ContentScheduleChange.End, now);
d.ContentSchedule.Clear(ContentScheduleChange.End, date);
result = Unpublish(d, userId: d.WriterId);
if (result.Success == false)
Logger.Error<ContentService>(null, "Failed to unpublish document id={DocumentId}, reason={Reason}.", d.Id, result.Result);
@@ -1259,7 +1282,7 @@ namespace Umbraco.Core.Services.Implement
}
_documentRepository.ClearSchedule(now);
_documentRepository.ClearSchedule(date);
scope.Complete();
}
@@ -1420,8 +1443,6 @@ namespace Umbraco.Core.Services.Implement
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");
Audit(AuditType.Publish, userId, document.Id, "Branch published");
scope.Complete();
@@ -1452,28 +1473,7 @@ namespace Umbraco.Core.Services.Implement
if (publishCultures != null && !publishCultures(document, culturesToPublish))
return new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, document);
// fixme - this is totally kinda wrong
var culturesPublishing = document.ContentType.VariesByCulture()
? document.PublishCultureInfos.Where(x => x.Value.IsDirty()).Select(x => x.Key).ToList()
: null;
var result = StrategyCanPublish(scope, document, userId, /*checkPath:*/ isRoot, culturesPublishing, Array.Empty<string>(), evtMsgs);
if (!result.Success)
return result;
result = StrategyPublish(scope, document, userId, culturesPublishing, Array.Empty<string>(), evtMsgs);
if (!result.Success)
throw new Exception("panic");
if (document.HasIdentity == false)
document.CreatorId = userId;
document.WriterId = userId;
_documentRepository.Save(document);
publishedDocuments.Add(document);
// fixme - but then, we have all the audit thing to run
// so... it would be better to re-run the internal thing?
return result;
// we want _some part_ of it but not all of it
return SavePublishingInternal(scope, document, userId);
return SavePublishingInternal(scope, document, userId, branchOne: true, branchRoot: isRoot);
}
#endregion
@@ -1979,29 +1979,29 @@ namespace Umbraco.Core.Services.Implement
var culturesChanging = content.ContentType.VariesByCulture()
? string.Join(",", content.CultureInfos.Where(x => x.Value.IsDirty()).Select(x => x.Key))
: null;
//TODO: Currently there's no way to change track which variant properties have changed, we only have change
// tracking enabled on all values on the Property which doesn't allow us to know which variants have changed.
// in this particular case, determining which cultures have changed works with the above with names since it will
// have always changed if it's been saved in the back office but that's not really fail safe.
//Save before raising event
// fixme - nesting uow?
var saveResult = Save(content, userId);
if (saveResult.Success)
{
sendToPublishEventArgs.CanCancel = false;
scope.Events.Dispatch(SentToPublish, this, sendToPublishEventArgs);
if (culturesChanging != null)
Audit(AuditType.SendToPublishVariant, userId, content.Id, $"Send To Publish for cultures: {culturesChanging}", culturesChanging);
else
Audit(AuditType.SendToPublish, content.WriterId, content.Id);
}
// fixme here, on only on success?
// always complete (but maybe return a failed status)
scope.Complete();
if (!saveResult.Success)
return saveResult.Success;
sendToPublishEventArgs.CanCancel = false;
scope.Events.Dispatch(SentToPublish, this, sendToPublishEventArgs);
if (culturesChanging != null)
Audit(AuditType.SendToPublishVariant, userId, content.Id, $"Send To Publish for cultures: {culturesChanging}", culturesChanging);
else
Audit(AuditType.SendToPublish, content.WriterId, content.Id);
return saveResult.Success;
}
}

View File

@@ -113,7 +113,7 @@
FailedPublishContentInvalid = FailedPublish | 8,
/// <summary>
/// The document cannot be published because it has no publishing flags or values.
/// The document could not be published because it has no publishing flags or values.
/// </summary>
FailedPublishNothingToPublish = FailedPublish | 9, // in ContentService.StrategyCanPublish - fixme weird
@@ -122,6 +122,11 @@
/// </summary>
FailedPublishMandatoryCultureMissing = FailedPublish | 10, // in ContentService.SavePublishing
/// <summary>
/// The document could not be published because it has been modified by another user.
/// </summary>
FailedPublishConcurrencyViolation = FailedPublish | 11,
#endregion
#region Failed - Unpublish

View File

@@ -222,26 +222,9 @@ namespace Umbraco.Web.Cache
internal static void HandleEvents(IEnumerable<IEventDefinition> events)
{
// fixme remove this in v8, this is a backwards compat hack and is needed because when we are using Deploy, the events will be raised on a background
//thread which means that cache refreshers will also execute on a background thread and in many cases developers may be using UmbracoContext.Current in their
//cache refresher handlers, so before we execute all of the events, we'll ensure a context
UmbracoContext tempContext = null;
if (UmbracoContext.Current == null)
{
var httpContext = new HttpContextWrapper(HttpContext.Current ?? new HttpContext(new SimpleWorkerRequest("temp.aspx", "", new StringWriter())));
tempContext = UmbracoContext.EnsureContext(
Current.UmbracoContextAccessor,
httpContext,
null,
new WebSecurity(httpContext, Current.Services.UserService, UmbracoConfig.For.GlobalSettings()),
UmbracoConfig.For.UmbracoSettings(),
Current.UrlProviders,
UmbracoConfig.For.GlobalSettings(),
Current.Container.GetInstance<IVariationContextAccessor>(),
true);
}
try
// ensure we run with an UmbracoContext, because this may run in a background task,
// yet developers may be using the 'current' UmbracoContext in the event handlers
using (UmbracoContext.EnsureContext())
{
foreach (var e in events)
{
@@ -251,10 +234,6 @@ namespace Umbraco.Web.Cache
handler.Invoke(null, new[] { e.Sender, e.Args });
}
}
finally
{
tempContext?.Dispose();
}
}
#endregion

View File

@@ -53,17 +53,36 @@ namespace Umbraco.Web.Scheduling
try
{
// run
// fixme context & events during scheduled publishing?
// in v7 we create an UmbracoContext and an HttpContext, and cache instructions
// are batched, and we have to explicitly flush them, how is it going to work here?
var result = _contentService.PerformScheduledPublish(DateTime.Now).ToList();
if (result.Count > 0)
foreach(var grouped in result.GroupBy(x => x.Result))
_logger.Info<ScheduledPublishing>("Scheduled publishing result: '{StatusCount}' items with status {Status}", grouped.Count(), grouped.Key);
// ensure we run with an UmbracoContext, because this may run in a background task,
// yet developers may be using the 'current' UmbracoContext in the event handlers
//
// fixme
// - or maybe not, CacheRefresherComponent already ensures a context when handling events
// - UmbracoContext 'current' needs to be refactored and cleaned up
// - batched messenger should not depend on a current HttpContext
// but then what should be its "scope"? could we attach it to scopes?
// - and we should definitively *not* have to flush it here (should be auto)
//
using (var tempContext = UmbracoContext.EnsureContext())
{
try
{
// run
var result = _contentService.PerformScheduledPublish(DateTime.Now);
foreach (var grouped in result.GroupBy(x => x.Result))
_logger.Info<ScheduledPublishing>("Scheduled publishing result: '{StatusCount}' items with status {Status}", grouped.Count(), grouped.Key);
}
finally
{
// if running on a temp context, we have to flush the messenger
if (tempContext != null && Composing.Current.ServerMessenger is BatchedDatabaseServerMessenger m)
m.FlushBatch();
}
}
}
catch (Exception ex)
{
// important to catch *everything* to ensure the task repeats
_logger.Error<ScheduledPublishing>(ex, "Failed.");
}

View File

@@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Web;
using System.Web.Hosting;
using Umbraco.Core;
using Umbraco.Core.Configuration;
using Umbraco.Core.Configuration.UmbracoSettings;
@@ -10,6 +12,7 @@ using Umbraco.Web.PublishedCache;
using Umbraco.Web.Routing;
using Umbraco.Web.Runtime;
using Umbraco.Web.Security;
using LightInject;
namespace Umbraco.Web
{
@@ -91,6 +94,35 @@ namespace Umbraco.Web
return umbracoContextAccessor.UmbracoContext = new UmbracoContext(httpContext, publishedSnapshotService, webSecurity, umbracoSettings, urlProviders, globalSettings, variationContextAccessor);
}
/// <summary>
/// Gets a disposable object representing the presence of a current UmbracoContext.
/// </summary>
/// <remarks>
/// <para>The disposable object should be used in a using block: using (UmbracoContext.EnsureContext()) { ... }.</para>
/// <para>If an actual current UmbracoContext is already present, the disposable object is null and this method does nothing.</para>
/// <para>Otherwise, a temporary, dummy UmbracoContext is created and registered in the accessor. And disposed and removed from the accessor.</para>
/// </remarks>
internal static IDisposable EnsureContext() // keep this internal for now!
{
if (Composing.Current.UmbracoContext != null) return null;
var httpContext = new HttpContextWrapper(System.Web.HttpContext.Current ?? new HttpContext(new SimpleWorkerRequest("temp.aspx", "", new StringWriter())));
return EnsureContext(
Composing.Current.UmbracoContextAccessor,
httpContext,
null,
new WebSecurity(httpContext, Composing.Current.Services.UserService, UmbracoConfig.For.GlobalSettings()),
UmbracoConfig.For.UmbracoSettings(),
Composing.Current.UrlProviders,
UmbracoConfig.For.GlobalSettings(),
Composing.Current.Container.GetInstance<IVariationContextAccessor>(),
true);
// when the context will be disposed, it will be removed from the accessor
// (see DisposeResources)
}
// initializes a new instance of the UmbracoContext class
// internal for unit tests
// otherwise it's used by EnsureContext above
@@ -215,6 +247,9 @@ namespace Umbraco.Web
/// </summary>
public HttpContextBase HttpContext { get; }
/// <summary>
/// Gets the variation context accessor.
/// </summary>
public IVariationContextAccessor VariationContextAccessor { get; }
/// <summary>