fix+cleanup publishing with variants

This commit is contained in:
Stephan
2018-11-08 16:33:19 +01:00
parent 4b23090e97
commit 5381eec6a4
22 changed files with 309 additions and 265 deletions

View File

@@ -51,23 +51,20 @@ namespace Umbraco.Core
}
/// <summary>
/// Method to return the cultures that have been flagged for unpublishing
/// Gets the cultures that have been flagged for unpublishing.
/// </summary>
/// <param name="content"></param>
/// <returns></returns>
/// <remarks>Gets cultures for which content.UnpublishCulture() has been invoked.</remarks>
internal static IReadOnlyList<string> GetCulturesUnpublishing(this IContent content)
{
if (!content.ContentType.VariesByCulture() && !content.IsPropertyDirty("PublishCultureInfos") && !content.Published)
// fixme/review - assuming it's || here not && ?
if (!content.Published || !content.ContentType.VariesByCulture() || !content.IsPropertyDirty("PublishCultureInfos"))
return Array.Empty<string>();
var culturesChanging = content.CultureInfos.Where(x => x.Value.IsDirty()).Select(x => x.Key);
var result = new List<string>();
foreach (var culture in culturesChanging)
if (!content.IsCulturePublished(culture) && content.WasCulturePublished(culture))
result.Add(culture);
return result;
return culturesChanging
.Where(x => !content.IsCulturePublished(x) && // is not published anymore
content.WasCulturePublished(x)) // but was published before
.ToList();
}
/// <summary>

View File

@@ -20,8 +20,6 @@ namespace Umbraco.Core.Models
private ContentScheduleCollection _schedule;
private bool _published;
private PublishedState _publishedState;
private DateTime? _releaseDate;
private DateTime? _expireDate;
private ContentCultureInfosCollection _publishInfos;
private ContentCultureInfosCollection _publishInfosOrig;
private HashSet<string> _editedCultures;
@@ -102,7 +100,7 @@ namespace Umbraco.Core.Models
_schedule = new ContentScheduleCollection();
_schedule.CollectionChanged += ScheduleCollectionChanged;
}
return _schedule ?? (_schedule = new ContentScheduleCollection());
return _schedule;
}
set
{
@@ -302,6 +300,7 @@ namespace Umbraco.Core.Models
//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;

View File

@@ -3,14 +3,16 @@ using System.Runtime.Serialization;
namespace Umbraco.Core.Models
{
/// <summary>
/// Model for scheduled content
/// Represents a scheduled action for a document.
/// </summary>
[Serializable]
[DataContract(IsReference = true)]
public class ContentSchedule : IDeepCloneable
{
/// <summary>
/// Initializes a new instance of the <see cref="ContentSchedule"/> class.
/// </summary>
public ContentSchedule(int id, string culture, DateTime date, ContentScheduleChange change)
{
Id = id;
@@ -20,32 +22,46 @@ namespace Umbraco.Core.Models
}
/// <summary>
/// The unique Id of the schedule item
/// Gets the unique identifier of the document targeted by the scheduled action.
/// </summary>
[DataMember]
public int Id { get; }
/// <summary>
/// The culture for the schedule item
/// Gets the culture of the scheduled action.
/// </summary>
/// <remarks>
/// string.Empty represents invariant culture
/// string.Empty represents the invariant culture.
/// </remarks>
[DataMember]
public string Culture { get; }
/// <summary>
/// The date for the schedule
/// Gets the date of the scheduled action.
/// </summary>
[DataMember]
public DateTime Date { get; }
/// <summary>
/// The action to take for the schedule
/// Gets the action to take.
/// </summary>
[DataMember]
public ContentScheduleChange Change { get; }
// fixme/review - must implement Equals?
// fixing ContentScheduleCollection.Equals which was broken, breaks content Can_Deep_Clone test
// because SequenceEqual on the inner sorted lists fails, because it ends up doing reference-equal
// on each content schedule - so we *have* to implement Equals for us too?
public override bool Equals(object obj)
=> obj is ContentSchedule other && Equals(other);
public bool Equals(ContentSchedule other)
{
// don't compare Ids, two ContentSchedule are equal if they are for the same change
// for the same culture, on the same date - and the collection deals w/duplicates
return Culture.InvariantEquals(other.Culture) && Date == other.Date && Change == other.Change;
}
public object DeepClone()
{
return new ContentSchedule(Id, Culture, Date, Change);

View File

@@ -1,11 +1,21 @@
namespace Umbraco.Core.Models
{
// fixme/review - should this be named DocumentScheduleAction?
// fixme/review - should values be Release and Expire not Start and Stop?
/// <summary>
/// The scheduled change types of scheduled content
/// Defines scheduled actions for documents.
/// </summary>
public enum ContentScheduleChange
{
/// <summary>
/// Release the document.
/// </summary>
Start,
/// <summary>
/// Expire the document.
/// </summary>
End
}
}

View File

@@ -193,41 +193,27 @@ namespace Umbraco.Core.Models
}
public override bool Equals(object obj)
{
if (!(obj is ContentScheduleCollection c))
return false;
return Equals(c);
}
=> obj is ContentScheduleCollection other && Equals(other);
public bool Equals(ContentScheduleCollection other)
{
var thisSched = this._schedule;
if (other == null) return false;
var thisSched = _schedule;
var thatSched = other._schedule;
var equal = false;
if (thisSched.Count == thatSched.Count)
{
equal = true;
foreach (var pair in thisSched)
{
if (thatSched.TryGetValue(pair.Key, out var val))
{
if (val.SequenceEqual(pair.Value))
{
equal = false;
break;
}
}
else
{
// Require key be present.
equal = false;
break;
}
}
}
return equal;
}
if (thisSched.Count != thatSched.Count)
return false;
// fixme/review - code was returning false *if* thatList.SequenceEqual(thisList) and not the opposite?
foreach (var (culture, thisList) in thisSched)
{
// if culture is missing, or actions differ, false
if (!thatSched.TryGetValue(culture, out var thatList) || !thatList.SequenceEqual(thisList))
return false;
}
return true;
}
}
}

View File

@@ -4,20 +4,42 @@ using System.Runtime.Serialization;
namespace Umbraco.Core.Models
{
/// <summary>
/// Enum for the various statuses a Content object can have
/// Describes the states of a document, with regard to (schedule) publishing.
/// </summary>
[Serializable]
[DataContract]
public enum ContentStatus
{
// typical flow:
// Unpublished (add release date)-> AwaitingRelease (release)-> Published (expire)-> Expired
/// <summary>
/// The document is not trashed, and not published.
/// </summary>
[EnumMember]
Unpublished,
/// <summary>
/// The document is published.
/// </summary>
[EnumMember]
Published,
/// <summary>
/// The document is not trashed, not published, after being unpublished by a scheduled action.
/// </summary>
[EnumMember]
Expired,
/// <summary>
/// The document is trashed.
/// </summary>
[EnumMember]
Trashed,
/// <summary>
/// The document is not trashed, not published, and pending publication by a scheduled action.
/// </summary>
[EnumMember]
AwaitingRelease
}

View File

@@ -1,7 +1,10 @@
using System;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Runtime.Serialization;
using System.Threading;
using Umbraco.Core.Configuration;
using Umbraco.Core.Models.Entities;
namespace Umbraco.Core.Models
@@ -48,7 +51,58 @@ namespace Umbraco.Core.Models
[DataMember]
public string CultureName
{
get => _cultureName ?? CultureInfo.GetCultureInfo(IsoCode).DisplayName;
// CultureInfo.DisplayName is the name in the installed .NET language
// .NativeName is the name in culture info's language
// .EnglishName is the name in English
//
// there is no easy way to get the name in a specified culture (which would need to be installed on the server)
// this works:
// var rm = new ResourceManager("mscorlib", typeof(int).Assembly);
// var name = rm.GetString("Globalization.ci_" + culture.Name, displayCulture);
// but can we rely on it?
//
// and... DisplayName is captured and cached in culture infos returned by GetCultureInfo(), using
// the value for the current thread culture at the moment it is first retrieved - whereas creating
// a new CultureInfo() creates a new instance, which _then_ can get DisplayName again in a different
// culture
//
// I assume that, on a site, all language names should be in the SAME language, in DB,
// and that would be the umbracoDefaultUILanguage (app setting) - BUT if by accident
// ANY culture has been retrieved with another current thread culture - it's now corrupt
//
// so, the logic below ensures that the name always end up being the correct name
// see also LanguageController.GetAllCultures which is doing the same
//
// fixme/review - stop saving language names in database?
// all this, including the ugly settings injection, because se store language names in db,
// otherwise it would be ok to simply return new CultureInfo(IsoCode).DisplayName to get the name
// in whatever culture is current - could we ... not do it?
//
// but then, some tests that compare audit strings (for culture names) would need to be fixed
get
{
if (_cultureName != null) return _cultureName;
// capture
var threadUiCulture = Thread.CurrentThread.CurrentUICulture;
try
{
var globalSettings = (IGlobalSettings) Composing.Current.Container.GetInstance(typeof(IGlobalSettings));
var defaultUiCulture = CultureInfo.GetCultureInfo(globalSettings.DefaultUILanguage);
Thread.CurrentThread.CurrentUICulture = defaultUiCulture;
// get name - new-ing an instance to get proper display name
return new CultureInfo(IsoCode).DisplayName;
}
finally
{
// restore
Thread.CurrentThread.CurrentUICulture = threadUiCulture;
}
}
set => SetPropertyValueAndDetectChanges(value, ref _cultureName, Ps.Value.CultureNameSelector);
}

View File

@@ -1,5 +1,4 @@
using NPoco;
using System.Collections.Generic;
using Umbraco.Core.Persistence.DatabaseAnnotations;
namespace Umbraco.Core.Persistence.Dtos
@@ -55,6 +54,5 @@ namespace Umbraco.Core.Persistence.Dtos
[ResultColumn]
[Reference(ReferenceType.OneToOne)]
public DocumentVersionDto PublishedVersionDto { get; set; }
}
}

View File

@@ -175,20 +175,15 @@ namespace Umbraco.Core.Persistence.Factories
public static IEnumerable<ContentScheduleDto> BuildScheduleDto(IContent entity, ILanguageRepository languageRepository)
{
var schedule = new List<ContentScheduleDto>();
foreach(var s in entity.ContentSchedule.FullSchedule)
{
schedule.Add(new ContentScheduleDto
return entity.ContentSchedule.FullSchedule.Select(x =>
new ContentScheduleDto
{
Action = s.Change.ToString(),
Date = s.Date,
Action = x.Change.ToString(),
Date = x.Date,
NodeId = entity.Id,
LanguageId = languageRepository.GetIdByIsoCode(s.Culture, false),
Id = s.Id
LanguageId = languageRepository.GetIdByIsoCode(x.Culture, false),
Id = x.Id
});
}
return schedule;
}
/// <summary>

View File

@@ -83,11 +83,16 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
var translator = new SqlTranslator<IContent>(sqlClause, query);
var sql = translator.Translate();
AddGetByQueryOrderBy(sql);
return MapDtosToContent(Database.Fetch<DocumentDto>(sql));
}
private void AddGetByQueryOrderBy(Sql<ISqlContext> sql)
{
sql // fixme why - this should be Path
.OrderBy<NodeDto>(x => x.Level)
.OrderBy<NodeDto>(x => x.SortOrder);
return MapDtosToContent(Database.Fetch<DocumentDto>(sql));
}
protected override Sql<ISqlContext> GetBaseQuery(QueryType queryType)
@@ -321,7 +326,6 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
var contentVersionDto = dto.DocumentVersionDto.ContentVersionDto;
contentVersionDto.NodeId = nodeDto.NodeId;
contentVersionDto.Current = !publishing;
contentVersionDto.Text = content.Name; //fixme is this requried? isn't this mapped in the ContentBaseFactory?
Database.Insert(contentVersionDto);
content.VersionId = contentVersionDto.Id;
@@ -367,11 +371,11 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
Database.Insert(dto);
//insert the schedule
var scheduleDto = ContentBaseFactory.BuildScheduleDto(content, LanguageRepository);
foreach (var c in scheduleDto)
var scheduleDtos = ContentBaseFactory.BuildScheduleDto(content, LanguageRepository);
foreach (var scheduleDto in scheduleDtos)
{
c.NodeId = nodeDto.NodeId;
Database.Insert(c);
scheduleDto.NodeId = nodeDto.NodeId;
Database.Insert(scheduleDto);
}
// persist the variations
@@ -502,7 +506,6 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
{
documentVersionDto.Published = true; // now published
contentVersionDto.Current = false; // no more current
contentVersionDto.Text = content.Name;
}
Database.Update(contentVersionDto);
Database.Update(documentVersionDto);
@@ -596,22 +599,22 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
if (content.IsPropertyDirty("ContentSchedule"))
{
//update the schedule, get the existing one so we know what to update
var dtoSchedule = ContentBaseFactory.BuildScheduleDto(content, LanguageRepository);
var existingSched = Database.Fetch<int>(Sql()
.Select<ContentScheduleDto>(x => x.Id)
.From<ContentScheduleDto>()
.Where<ContentScheduleDto>(x => x.NodeId == content.Id));
var scheduleDtos = ContentBaseFactory.BuildScheduleDto(content, LanguageRepository).ToList();
//remove any that no longer exist
var schedToRemove = existingSched.Except(dtoSchedule.Select(x => x.Id));
foreach (var c in schedToRemove)
Database.DeleteWhere<ContentScheduleDto>("id = @id", new { id = c });
var ids = scheduleDtos.Where(x => x.Id > 0).Select(x => x.Id).Distinct();
Database.Execute(Sql()
.Delete<ContentScheduleDto>()
.Where<ContentScheduleDto>(x => x.NodeId == content.Id)
.WhereNotIn<ContentScheduleDto>(x => x.Id, ids));
//add/update the rest
foreach (var c in dtoSchedule)
foreach (var scheduleDto in scheduleDtos)
{
if (c.Id == 0)
Database.Insert(c);
if (scheduleDto.Id == 0)
Database.Insert(scheduleDto);
else
Database.Update(c);
Database.Update(scheduleDto);
}
}
@@ -907,47 +910,41 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
/// <inheritdoc />
public IEnumerable<IContent> GetContentForRelease(DateTime date)
{
var sqlSchedule = Sql()
.Select<ContentScheduleDto>(x => x.NodeId)
.From<ContentScheduleDto>()
.Where<ContentScheduleDto>(x =>
x.Action == ContentScheduleChange.Start.ToString()
&& x.Date <= date);
var action = ContentScheduleChange.Start.ToString();
//fixme - If we don't cast to IEnumerable<T> or do a ToArray, the Expression Visitor will FAIL!
// in the ExpressionVisitorBase.VisitMethodCall where the switch checks for "Contains" for some reason
// the 'special case' that redirects to `SqlIn` fails because the m.Arguments.Count is only ONE instead of TWO,
// no time to investigate right now.
// fixed here: https://github.com/umbraco/Umbraco-CMS/pull/3516/files so when that's merged we don't need the AsEnumerable() thing
var scheduledIds = Database.Fetch<int>(sqlSchedule);
if (scheduledIds.Count == 0) return Enumerable.Empty<IContent>();
var e = scheduledIds.AsEnumerable();
// fixme/review - code would blow if more than 2000 items
// fixme/review - isn't this simpler?
var sql = GetBaseQuery(QueryType.Many)
.WhereIn<NodeDto>(x => x.NodeId, Sql()
.Select<ContentScheduleDto>(x => x.NodeId)
.From<ContentScheduleDto>()
.Where<ContentScheduleDto>(x => x.Action == action && x.Date <= date));
var query = Query<IContent>().Where(x => x.Published == false && e.Contains(x.Id));
return Get(query);
sql.Where<NodeDto>(x => !x.Trashed); // fixme/review - shouldn't we exclude trashed nodes?
sql.Where<DocumentDto>(x => !x.Published);
AddGetByQueryOrderBy(sql);
return MapDtosToContent(Database.Fetch<DocumentDto>(sql));
}
/// <inheritdoc />
public IEnumerable<IContent> GetContentForExpiration(DateTime date)
{
var sqlSchedule = Sql()
.Select<ContentScheduleDto>(x => x.NodeId)
.From<ContentScheduleDto>()
.Where<ContentScheduleDto>(x =>
x.Action == ContentScheduleChange.End.ToString()
&& x.Date <= date);
var action = ContentScheduleChange.End.ToString();
//fixme - If we don't cast to IEnumerable<T> or do a ToArray, the Expression Visitor will FAIL!
// in the ExpressionVisitorBase.VisitMethodCall where the switch checks for "Contains" for some reason
// the 'special case' that redirects to `SqlIn` fails because the m.Arguments.Count is only ONE instead of TWO,
// no time to investigate right now.
// fixed here: https://github.com/umbraco/Umbraco-CMS/pull/3516/files so when that's merged we don't need the AsEnumerable() thing
var scheduledIds = Database.Fetch<int>(sqlSchedule);
if (scheduledIds.Count == 0) return Enumerable.Empty<IContent>();
var e = scheduledIds.AsEnumerable();
// fixme/review - see as above
var sql = GetBaseQuery(QueryType.Many)
.WhereIn<NodeDto>(x => x.NodeId, Sql()
.Select<ContentScheduleDto>(x => x.NodeId)
.From<ContentScheduleDto>()
.Where<ContentScheduleDto>(x => x.Action == action && x.Date <= date));
var query = Query<IContent>().Where(x => x.Published && e.Contains(x.Id));
return Get(query);
sql.Where<DocumentDto>(x => x.Published);
AddGetByQueryOrderBy(sql);
return MapDtosToContent(Database.Fetch<DocumentDto>(sql));
}
#endregion
@@ -1160,23 +1157,27 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
private IDictionary<int, ContentScheduleCollection> GetContentSchedule(params int[] contentIds)
{
var result = new Dictionary<int, ContentScheduleCollection>();
foreach(var ids in contentIds.InGroupsOf(2000))
{
var sql = Sql().Select<ContentScheduleDto>().From<ContentScheduleDto>().Where<ContentScheduleDto>(x => ids.Contains(x.NodeId));
var scheduleDto = Database.Fetch<ContentScheduleDto>(sql);
foreach (var dto in scheduleDto)
{
var schedule = new ContentScheduleCollection();
schedule.Add(new ContentSchedule(dto.Id,
LanguageRepository.GetIsoCodeById(dto.LanguageId) ?? string.Empty,
dto.Date,
dto.Action == ContentScheduleChange.Start.ToString()
? ContentScheduleChange.Start
: ContentScheduleChange.End));
result[dto.NodeId] = schedule;
}
var scheduleDtos = Database.FetchByGroups<ContentScheduleDto, int>(contentIds, 2000, batch => Sql()
.Select<ContentScheduleDto>()
.From<ContentScheduleDto>()
.WhereIn<ContentScheduleDto>(x => x.NodeId, batch));
foreach (var scheduleDto in scheduleDtos)
{
// fixme/review - code was adding a new collection on each dto?
if (!result.TryGetValue(scheduleDto.NodeId, out var col))
col = result[scheduleDto.NodeId] = new ContentScheduleCollection();
col.Add(new ContentSchedule(scheduleDto.Id,
LanguageRepository.GetIsoCodeById(scheduleDto.LanguageId) ?? string.Empty,
scheduleDto.Date,
scheduleDto.Action == ContentScheduleChange.Start.ToString()
? ContentScheduleChange.Start
: ContentScheduleChange.End));
}
return result;
}

View File

@@ -236,7 +236,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
// fast way of getting an id for an isoCode - avoiding cloning
// _codeIdMap is rebuilt whenever PerformGetAll runs
public int? GetIdByIsoCode(string isoCode, bool throwOnNotFound)
public int? GetIdByIsoCode(string isoCode, bool throwOnNotFound = true)
{
if (isoCode == null) return null;

View File

@@ -1,39 +0,0 @@
using System;
using System.Linq;
using Umbraco.Core.Logging;
using Umbraco.Core.Models;
using Umbraco.Core.Services;
using Umbraco.Core.Services.Implement;
namespace Umbraco.Core.Publishing
{
/// <summary>
/// Used to perform scheduled publishing/unpublishing
/// </summary>
internal class ScheduledPublisher
{
private readonly IContentService _contentService;
private readonly ILogger _logger;
private readonly IUserService _userService;
public ScheduledPublisher(IContentService contentService, ILogger logger, IUserService userService)
{
_contentService = contentService;
_logger = logger;
_userService = userService;
}
/// <summary>
/// Processes scheduled operations
/// </summary>
/// <returns>
/// Returns the number of items successfully completed
/// </returns>
public int CheckPendingAndProcess()
{
var results = _contentService.PerformScheduledPublish(DateTime.Now);
return results.Count(x => x.Success);
}
}
}

View File

@@ -132,7 +132,7 @@ namespace Umbraco.Core.Services
IEnumerable<IContent> GetRootContent();
/// <summary>
/// Gets documents with an expiration date greater then today.
/// Gets documents having an expiration date before (lower than, or equal to) a specified date.
/// </summary>
/// <returns>An Enumerable list of <see cref="IContent"/> objects</returns>
/// <remarks>
@@ -142,7 +142,7 @@ namespace Umbraco.Core.Services
IEnumerable<IContent> GetContentForExpiration(DateTime date);
/// <summary>
/// Gets documents with a release date greater then today.
/// Gets documents having a release date before (lower than, or equal to) a specified date.
/// </summary>
/// <returns>An Enumerable list of <see cref="IContent"/> objects</returns>
/// <remarks>
@@ -362,23 +362,7 @@ 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
*/
// fixme/review - should SaveAndPublishBranch always publish the root document of the branch, even when !force?
/// <summary>
/// Saves and publishes a document branch.
@@ -392,7 +376,7 @@ namespace Umbraco.Core.Services
/// 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>
/// published. The root of the branch is always published, regardless of <paramref name="force"/>.</para>
/// </remarks>
IEnumerable<PublishResult> SaveAndPublishBranch(IContent content, bool force, string culture = "*", int userId = 0);
@@ -406,7 +390,7 @@ namespace Umbraco.Core.Services
/// <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>
/// published. The root of the branch is always published, regardless of <paramref name="force"/>.</para>
/// </remarks>
IEnumerable<PublishResult> SaveAndPublishBranch(IContent content, bool force, string[] cultures, int userId = 0);
@@ -421,7 +405,7 @@ namespace Umbraco.Core.Services
/// <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>
/// published. The root of the branch is always published, regardless of <paramref name="force"/>.</para>
/// <para>The <paramref name="editing"/> parameter is a function which determines whether a document has
/// 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

View File

@@ -2,8 +2,6 @@
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Umbraco.Core;
using Umbraco.Core.Composing;
using Umbraco.Core.Events;
using Umbraco.Core.Exceptions;
using Umbraco.Core.IO;
@@ -942,9 +940,7 @@ namespace Umbraco.Core.Services.Implement
}
// finally, "save publishing"
// what happens next depends on whether the content can be published or not
var saved = SavePublishing(content, userId);
return saved;
return SavePublishing(content, userId);
}
/// <inheritdoc />
@@ -970,6 +966,7 @@ namespace Umbraco.Core.Services.Implement
((Content)content).PublishedState = PublishedState.Publishing;
// state here is either Publishing or Unpublishing
// (even though, Publishing to unpublish a culture may end up unpublishing everything)
var publishing = content.PublishedState == PublishedState.Publishing;
var unpublishing = content.PublishedState == PublishedState.Unpublishing;
@@ -1014,11 +1011,11 @@ namespace Umbraco.Core.Services.Implement
unpublishing = content.Published; // if not published yet, nothing to do
// we may end up in a state where we won't publish nor unpublish
// keep going, though, as we want to save anways
// keep going, though, as we want to save anyways
}
//fixme - casting
((Content)content).Published = content.Published; // reset published state = save unchanged
((Content)content).Published = content.Published; // reset published state = save unchanged - fixme doh?
}
}
@@ -1026,7 +1023,7 @@ namespace Umbraco.Core.Services.Implement
{
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;
content = newest; // fixme confusing should just die here - else we'll miss some changes
if (content.Published)
{
@@ -1269,18 +1266,20 @@ namespace Umbraco.Core.Services.Implement
// should not trigger a publish).
HashSet<string> ShouldPublish(IContent c)
{
var isRoot = c.Id == content.Id;
if (c.ContentType.VariesByCulture())
{
// variant content type
// add culture if edited, and already published or forced
if (c.IsCultureEdited(culture) && (c.IsCulturePublished(culture) || force))
if (c.IsCultureEdited(culture) && (c.IsCulturePublished(culture) || force || isRoot))
return new HashSet<string> { culture.ToLowerInvariant() };
}
else
{
// invariant content type
// add "*" if edited, and already published or forced
if (c.Edited && (c.Published || force))
if (c.Edited && (c.Published || force || isRoot))
return new HashSet<string> { "*" };
}
@@ -1314,6 +1313,7 @@ namespace Umbraco.Core.Services.Implement
HashSet<string> ShouldPublish(IContent c)
{
var culturesToPublish = new HashSet<string>();
var isRoot = c.Id == content.Id;
if (c.ContentType.VariesByCulture())
{
@@ -1321,7 +1321,7 @@ namespace Umbraco.Core.Services.Implement
// add cultures which are edited, and already published or forced
foreach (var culture in cultures)
{
if (c.IsCultureEdited(culture) && (c.IsCulturePublished(culture) || force))
if (c.IsCultureEdited(culture) && (c.IsCulturePublished(culture) || force || isRoot))
culturesToPublish.Add(culture.ToLowerInvariant());
}
}
@@ -1329,7 +1329,7 @@ namespace Umbraco.Core.Services.Implement
{
// invariant content type
// add "*" if edited, and already published or forced
if (c.Edited && (c.Published || force))
if (c.Edited && (c.Published || force || isRoot))
culturesToPublish.Add("*");
}
@@ -1425,7 +1425,7 @@ namespace Umbraco.Core.Services.Implement
// publishValues: a function publishing values (using the appropriate PublishCulture calls)
private PublishResult SaveAndPublishBranchOne(IScope scope, IContent document,
Func<IContent, HashSet<string>> shouldPublish, Func<IContent, HashSet<string>, bool> publishCultures,
bool checkPath,
bool isRoot,
ICollection<IContent> publishedDocuments,
EventMessages evtMsgs, int userId)
{
@@ -1442,8 +1442,28 @@ namespace Umbraco.Core.Services.Implement
if (publishCultures != null && !publishCultures(document, culturesToPublish))
return new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, document);
return SavePublishingInternal(scope, document, userId);
// 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);
}
#endregion

View File

@@ -1,8 +1,7 @@
namespace Umbraco.Core.Services
{
/// <summary>
/// A value indicating the result of publishing a content item.
/// A value indicating the result of publishing or unpublishing a document.
/// </summary>
public enum PublishResultType : byte
{
@@ -10,15 +9,19 @@
// every failure codes as >128 - see OperationResult and OperationResultType for details.
#region Success - Publish
/// <summary>
/// The publishing was successful.
/// The document was successfully published.
/// </summary>
SuccessPublish = 0,
/// <summary>
/// The specified document culture was successfully published.
/// </summary>
SuccessPublishCulture = 1,
/// <summary>
/// The item was already published.
/// The document was already published.
/// </summary>
SuccessPublishAlready = 2,
@@ -27,22 +30,22 @@
#region Success - Unpublish
/// <summary>
/// The unpublishing was successful.
/// The document was successfully unpublished.
/// </summary>
SuccessUnpublish = 3,
/// <summary>
/// The item was already unpublished.
/// The document was already unpublished.
/// </summary>
SuccessUnpublishAlready = 4,
/// <summary>
/// The specified variant was unpublished, the content item itself remains published.
/// The specified document culture was unpublished, the document item itself remains published.
/// </summary>
SuccessUnpublishCulture = 5,
/// <summary>
/// The specified variant was a mandatory culture therefore it was unpublished and the content item itself is unpublished
/// The specified document culture was unpublished, and was a mandatory culture, therefore the document itself was unpublished.
/// </summary>
SuccessUnpublishMandatoryCulture = 6,
@@ -51,13 +54,12 @@
#region Success - Mixed
/// <summary>
/// A variant content item has a culture published and another culture unpublished in the same operation
/// Specified document cultures were successfully published and unpublished (in the same operation).
/// </summary>
SuccessMixedCulture = 7,
#endregion
#region Failed - Publish
/// <summary>
@@ -67,36 +69,36 @@
FailedPublish = 128,
/// <summary>
/// The content could not be published because it's ancestor path isn't published.
/// The document could not be published because its ancestor path is not published.
/// </summary>
FailedPublishPathNotPublished = FailedPublish | 1,
/// <summary>
/// The content item was scheduled to be un-published and it has expired so we cannot force it to be
/// The document has expired so we cannot force it to be
/// published again as part of a bulk publish operation.
/// </summary>
FailedPublishHasExpired = FailedPublish | 2,
/// <summary>
/// The content item is scheduled to be released in the future and therefore we cannot force it to
/// The document is scheduled to be released in the future and therefore we cannot force it to
/// be published during a bulk publish operation.
/// </summary>
FailedPublishAwaitingRelease = FailedPublish | 3,
/// <summary>
/// A culture on the content item was scheduled to be un-published and it has expired so we cannot force it to be
/// A document culture has expired so we cannot force it to be
/// published again as part of a bulk publish operation.
/// </summary>
FailedPublishCultureHasExpired = FailedPublish | 4,
/// <summary>
/// A culture on the content item is scheduled to be released in the future and therefore we cannot force it to
/// A document culture is scheduled to be released in the future and therefore we cannot force it to
/// be published during a bulk publish operation.
/// </summary>
FailedPublishCultureAwaitingRelease = FailedPublish | 5,
/// <summary>
/// The content item could not be published because it is in the trash.
/// The document could not be published because it is in the trash.
/// </summary>
FailedPublishIsTrashed = FailedPublish | 6,
@@ -106,17 +108,17 @@
FailedPublishCancelledByEvent = FailedPublish | 7,
/// <summary>
/// The content item could not be published because it contains invalid data (has not passed validation requirements).
/// The document could not be published because it contains invalid data (has not passed validation requirements).
/// </summary>
FailedPublishContentInvalid = FailedPublish | 8,
/// <summary>
/// Cannot publish a document that has no publishing flags or values
/// The document cannot be published because it has no publishing flags or values.
/// </summary>
FailedPublishNothingToPublish = FailedPublish | 9, // in ContentService.StrategyCanPublish - fixme weird
/// <summary>
/// Some mandatory cultures are missing.
/// The document could not be published because some mandatory cultures are missing.
/// </summary>
FailedPublishMandatoryCultureMissing = FailedPublish | 10, // in ContentService.SavePublishing
@@ -125,7 +127,7 @@
#region Failed - Unpublish
/// <summary>
/// Unpublish failed
/// The document could not be unpublished.
/// </summary>
FailedUnpublish = FailedPublish | 11, // in ContentService.SavePublishing
@@ -135,6 +137,5 @@
FailedUnpublishCancelledByEvent = FailedPublish | 12,
#endregion
}
}

View File

@@ -1268,7 +1268,6 @@
<Compile Include="PropertyEditors\ValueConverters\TinyMceValueConverter.cs" />
<Compile Include="PropertyEditors\ValueConverters\UploadPropertyConverter.cs" />
<Compile Include="PropertyEditors\ValueConverters\YesNoValueConverter.cs" />
<Compile Include="Publishing\ScheduledPublisher.cs" />
<Compile Include="ReflectionUtilities.cs" />
<Compile Include="RenderingEngine.cs" />
<Compile Include="RuntimeLevel.cs" />

View File

@@ -37,13 +37,13 @@ namespace Umbraco.Tests.Services
// ii2 !published !edited
// !force = publishes those that are actually published, and have changes
// here: nothing
// here: root (root is always published)
var r = SaveAndPublishInvariantBranch(iRoot, false, method).ToArray();
AssertPublishResults(r, x => x.Content.Name,
"iroot", "ii1", "ii2");
AssertPublishResults(r, x => x.Result,
PublishResultType.SuccessPublishAlready,
PublishResultType.SuccessPublish,
PublishResultType.SuccessPublishAlready,
PublishResultType.SuccessPublishAlready);
@@ -220,8 +220,8 @@ namespace Umbraco.Tests.Services
AssertPublishResults(r, x => x.Content.Name,
"vroot.de", "iv1.de", "iv2.de");
AssertPublishResults(r, x => x.Result,
PublishResultType.SuccessPublish,
PublishResultType.SuccessPublish,
PublishResultType.SuccessPublishCulture,
PublishResultType.SuccessPublishCulture,
PublishResultType.SuccessPublishAlready);
// reload - SaveAndPublishBranch has modified other instances
@@ -291,7 +291,7 @@ namespace Umbraco.Tests.Services
AssertPublishResults(r, x => x.Result,
PublishResultType.SuccessPublishAlready,
PublishResultType.SuccessPublish,
PublishResultType.SuccessPublish);
PublishResultType.SuccessPublishCulture);
// reload - SaveAndPublishBranch has modified other instances
Reload(ref ii1);

View File

@@ -351,10 +351,11 @@ namespace Umbraco.Tests.Services
content.ContentSchedule.Add(null, DateTime.Now.AddHours(2));
contentService.Save(content, Constants.Security.SuperUserId);
Assert.AreEqual(1, content.ContentSchedule.FullSchedule.Count);
content = contentService.GetById(content.Id);
var sched = content.ContentSchedule.FullSchedule;
Assert.AreEqual(1, sched.Count());
Assert.AreEqual(1, sched.Count);
Assert.AreEqual(1, sched.Count(x => x.Culture == string.Empty));
content.ContentSchedule.Clear(ContentScheduleChange.End);
contentService.Save(content, Constants.Security.SuperUserId);
@@ -363,7 +364,7 @@ namespace Umbraco.Tests.Services
// Assert
content = contentService.GetById(content.Id);
sched = content.ContentSchedule.FullSchedule;
Assert.AreEqual(0, sched.Count());
Assert.AreEqual(0, sched.Count);
Assert.IsTrue(contentService.SaveAndPublish(content).Success);
}

View File

@@ -3,6 +3,7 @@ using System.Globalization;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Web.Http;
using AutoMapper;
using Umbraco.Core;
@@ -28,10 +29,14 @@ namespace Umbraco.Web.Editors
[HttpGet]
public IDictionary<string, string> GetAllCultures()
{
return
CultureInfo.GetCultures(CultureTypes.AllCultures)
.Where(x => !x.Name.IsNullOrWhiteSpace())
.OrderBy(x => x.DisplayName).ToDictionary(x => x.Name, x => x.DisplayName);
// get cultures - new-ing instances to get proper display name,
// in the current culture, and not the cached one
// (see notes in Language class about culture info names)
return CultureInfo.GetCultures(CultureTypes.AllCultures)
.Where(x => !x.Name.IsNullOrWhiteSpace())
.Select(x => new CultureInfo(x.Name)) // important!
.OrderBy(x => x.DisplayName)
.ToDictionary(x => x.Name, x => x.DisplayName);
}
/// <summary>

View File

@@ -1,7 +1,6 @@
using System;
using Umbraco.Core;
using Umbraco.Core.Logging;
using Umbraco.Core.Publishing;
using Umbraco.Core.Services;
using Umbraco.Core.Sync;
@@ -12,16 +11,14 @@ namespace Umbraco.Web.Scheduling
private readonly IRuntimeState _runtime;
private readonly IContentService _contentService;
private readonly ILogger _logger;
private readonly IUserService _userService;
public ScheduledPublishing(IBackgroundTaskRunner<RecurringTaskBase> runner, int delayMilliseconds, int periodMilliseconds,
IRuntimeState runtime, IContentService contentService, ILogger logger, IUserService userService)
IRuntimeState runtime, IContentService contentService, ILogger logger)
: base(runner, delayMilliseconds, periodMilliseconds)
{
_runtime = runtime;
_contentService = contentService;
_logger = logger;
_userService = userService;
}
public override bool PerformRun()
@@ -58,9 +55,8 @@ namespace Umbraco.Web.Scheduling
// 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 explicitely flush them, how is it going to work here?
var publisher = new ScheduledPublisher(_contentService, _logger, _userService);
var count = publisher.CheckPendingAndProcess();
// are batched, and we have to explicitly flush them, how is it going to work here?
_contentService.PerformScheduledPublish(DateTime.Now);
}
catch (Exception ex)
{

View File

@@ -26,7 +26,6 @@ namespace Umbraco.Web.Scheduling
{
private IRuntimeState _runtime;
private IContentService _contentService;
private IUserService _userService;
private IAuditService _auditService;
private ILogger _logger;
private ProfilingLogger _proflog;
@@ -45,13 +44,12 @@ namespace Umbraco.Web.Scheduling
private IBackgroundTask[] _tasks;
public void Initialize(IRuntimeState runtime,
IContentService contentService, IAuditService auditService, IUserService userService,
IContentService contentService, IAuditService auditService,
HealthCheckCollection healthChecks, HealthCheckNotificationMethodCollection notifications,
IScopeProvider scopeProvider, ILogger logger, ProfilingLogger proflog)
{
_runtime = runtime;
_contentService = contentService;
_userService = userService;
_auditService = auditService;
_scopeProvider = scopeProvider;
_logger = logger;
@@ -118,7 +116,7 @@ namespace Umbraco.Web.Scheduling
{
// scheduled publishing/unpublishing
// install on all, will only run on non-replica servers
var task = new ScheduledPublishing(_publishingRunner, 60000, 60000, _runtime, _contentService, _logger, _userService);
var task = new ScheduledPublishing(_publishingRunner, 60000, 60000, _runtime, _contentService, _logger);
_publishingRunner.TryAdd(task);
return task;
}

View File

@@ -1,8 +1,9 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/FilterSettingsManager/CoverageFilterXml/@EntryValue">&lt;data&gt;&lt;IncludeFilters /&gt;&lt;ExcludeFilters /&gt;&lt;/data&gt;</s:String>
<s:String x:Key="/Default/FilterSettingsManager/AttributeFilterXml/@EntryValue">&lt;data /&gt;</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/StructuralSearch/Pattern/=2DA32DA040A7D74599ABE288C7224CF0/Comment/@EntryValue">Disposable construction</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/StructuralSearch/Pattern/=2DA32DA040A7D74599ABE288C7224CF0/Severity/@EntryValue">HINT</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/StructuralSearch/Pattern/=37A0B37A0ABAA34AA5CB32A93653C4FE/@KeyIndexDefined">False</s:Boolean>
<s:String x:Key="/Default/CodeInspection/CSharpLanguageProject/LanguageLevel/@EntryValue">CSharp71</s:String></wpf:ResourceDictionary>
</wpf:ResourceDictionary>
<s:String x:Key="/Default/CodeInspection/CSharpLanguageProject/LanguageLevel/@EntryValue">CSharp71</s:String>
<s:Boolean x:Key="/Default/UserDictionary/Words/=unpublish/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=unpublishing/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>