From feeb1c56cfac74d00110875624f4a0bcf55706b7 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 20 Sep 2018 14:58:15 +1000 Subject: [PATCH 01/10] wip for IDocumentEntitySlim changes --- .../Models/Entities/DocumentEntitySlim.cs | 21 +++++++++++++ .../Models/Entities/IContentEntitySlim.cs | 2 +- .../Models/Entities/IDocumentEntitySlim.cs | 31 ++++++++++++------- .../Implement/EntityRepository.cs | 3 +- 4 files changed, 44 insertions(+), 13 deletions(-) diff --git a/src/Umbraco.Core/Models/Entities/DocumentEntitySlim.cs b/src/Umbraco.Core/Models/Entities/DocumentEntitySlim.cs index 5b63ad81c5..39ece5fa10 100644 --- a/src/Umbraco.Core/Models/Entities/DocumentEntitySlim.cs +++ b/src/Umbraco.Core/Models/Entities/DocumentEntitySlim.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; namespace Umbraco.Core.Models.Entities { @@ -8,13 +9,32 @@ namespace Umbraco.Core.Models.Entities public class DocumentEntitySlim : ContentEntitySlim, IDocumentEntitySlim { private static readonly IReadOnlyDictionary Empty = new Dictionary(); + private IReadOnlyDictionary _cultureNames; + private IEnumerable _publishedCultures; + private IEnumerable _editedCultures; + + /// public IReadOnlyDictionary CultureNames { get => _cultureNames ?? Empty; set => _cultureNames = value; } + /// + public IEnumerable PublishedCultures + { + get => _publishedCultures ?? Enumerable.Empty(); + set => _publishedCultures = value; + } + + /// + public IEnumerable EditedCultures + { + get => _editedCultures ?? Enumerable.Empty(); + set => _editedCultures = value; + } + public ContentVariation Variations { get; set; } /// @@ -22,5 +42,6 @@ namespace Umbraco.Core.Models.Entities /// public bool Edited { get; set; } + } } diff --git a/src/Umbraco.Core/Models/Entities/IContentEntitySlim.cs b/src/Umbraco.Core/Models/Entities/IContentEntitySlim.cs index dc986a4cd9..9ab557b02c 100644 --- a/src/Umbraco.Core/Models/Entities/IContentEntitySlim.cs +++ b/src/Umbraco.Core/Models/Entities/IContentEntitySlim.cs @@ -20,4 +20,4 @@ /// string ContentTypeThumbnail { get; } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Core/Models/Entities/IDocumentEntitySlim.cs b/src/Umbraco.Core/Models/Entities/IDocumentEntitySlim.cs index 6b72fd4a2b..38fd9a02f1 100644 --- a/src/Umbraco.Core/Models/Entities/IDocumentEntitySlim.cs +++ b/src/Umbraco.Core/Models/Entities/IDocumentEntitySlim.cs @@ -7,26 +7,35 @@ namespace Umbraco.Core.Models.Entities /// public interface IDocumentEntitySlim : IContentEntitySlim { - //fixme we need to supply more information than this and change this property name. This will need to include Published/Editor per variation since we need this information for the tree + /// + /// Gets the variant name for each culture + /// IReadOnlyDictionary CultureNames { get; } + /// + /// Gets the published cultures. + /// + IEnumerable PublishedCultures { get; } + + /// + /// Gets the edited cultures. + /// + IEnumerable EditedCultures { get; } + + /// + /// Gets the content variation of the content type. + /// ContentVariation Variations { get; } /// - /// At least one variation is published + /// Gets a value indicating whether the content is published. /// - /// - /// If the document is invariant, this simply means there is a published version - /// - bool Published { get; set; } + bool Published { get; } /// - /// At least one variation has pending changes + /// Gets a value indicating whether the content has been edited. /// - /// - /// If the document is invariant, this simply means there is pending changes - /// - bool Edited { get; set; } + bool Edited { get; } } } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs index 340eecb538..7dd2b7ff27 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs @@ -901,10 +901,11 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var entity = new DocumentEntitySlim(); BuildContentEntity(entity, dto); - //fixme we need to set these statuses for each variant, see notes in IDocumentEntitySlim + //fill in the invariant info entity.Edited = dto.Edited; entity.Published = dto.Published; + //fill in the variant info if (dto.Variations.VariesByCulture() && dto.VariationInfo != null && dto.VariationInfo.Count > 0) { var variantInfo = new Dictionary(StringComparer.InvariantCultureIgnoreCase); From 448850b2d05435ca2d2d88b68055a4891e89c5e0 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 21 Sep 2018 01:21:52 +1000 Subject: [PATCH 02/10] I may have figured out how to get publish information into the IDocumentEntitySlim --- .../Implement/EntityRepository.cs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs index 7dd2b7ff27..70fca3fd34 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs @@ -460,6 +460,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement public int LanguageId { get; set; } [Column("versionCultureName")] public string Name { get; set; } + [Column("versionCultureEdited")] + public bool Edited { get; set; } } // ReSharper disable once ClassNeverInstantiated.Local @@ -522,7 +524,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement .AndSelect( x => Alias(x.Id, "versionCultureId"), x => Alias(x.LanguageId, "versionCultureLangId"), - x => Alias(x.Name, "versionCultureName")); + x => Alias(x.Name, "versionCultureName"), + x => Alias(x.Edited, "versionCultureEdited")); } } @@ -905,6 +908,9 @@ namespace Umbraco.Core.Persistence.Repositories.Implement entity.Edited = dto.Edited; entity.Published = dto.Published; + var publishedCultures = new List(); + var editedCultures = new List(); + //fill in the variant info if (dto.Variations.VariesByCulture() && dto.VariationInfo != null && dto.VariationInfo.Count > 0) { @@ -914,9 +920,20 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var isoCode = _langRepository.GetIsoCodeById(info.LanguageId); if (isoCode != null) variantInfo[isoCode] = info.Name; + + if (dto.Published) + { + publishedCultures.Add(isoCode); + if (info.Edited) + { + editedCultures.Add(isoCode); + } + } } entity.CultureNames = variantInfo; entity.Variations = dto.Variations; + entity.PublishedCultures = publishedCultures; + entity.EditedCultures = editedCultures; } return entity; } From c65480cf77287d1dc6c6676662ee0b30b08d1f56 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 21 Sep 2018 12:28:29 +1000 Subject: [PATCH 03/10] making an attempt at a single SQL statement to get publish info for variants ... but failing, will revert --- .../Implement/EntityRepository.cs | 45 ++++++++++++++----- 1 file changed, 33 insertions(+), 12 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs index 70fca3fd34..ed764b328b 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs @@ -3,12 +3,14 @@ using System.Collections.Generic; using System.Linq; using NPoco; using Umbraco.Core.Models; +using Umbraco.Core.Persistence; using Umbraco.Core.Models.Entities; using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Scoping; using static Umbraco.Core.Persistence.NPocoSqlExtensions.Statics; +using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Repositories.Implement { @@ -25,7 +27,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement { private readonly IScopeAccessor _scopeAccessor; private readonly ILanguageRepository _langRepository; - + public EntityRepository(IScopeAccessor scopeAccessor, ILanguageRepository langRepository) { _scopeAccessor = scopeAccessor; @@ -34,6 +36,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected IUmbracoDatabase Database => _scopeAccessor.AmbientScope.Database; protected Sql Sql() => _scopeAccessor.AmbientScope.SqlContext.Sql(); + protected ISqlSyntaxProvider SqlSyntax => _scopeAccessor.AmbientScope.SqlContext.SqlSyntax; #region Repository @@ -460,8 +463,11 @@ namespace Umbraco.Core.Persistence.Repositories.Implement public int LanguageId { get; set; } [Column("versionCultureName")] public string Name { get; set; } - [Column("versionCultureEdited")] - public bool Edited { get; set; } + + [Column("versionCurrent")] + public bool VersionCurrent { get; set; } + [Column("versionPublished")] + public bool VersionPublished { get; set; } } // ReSharper disable once ClassNeverInstantiated.Local @@ -524,8 +530,11 @@ namespace Umbraco.Core.Persistence.Repositories.Implement .AndSelect( x => Alias(x.Id, "versionCultureId"), x => Alias(x.LanguageId, "versionCultureLangId"), - x => Alias(x.Name, "versionCultureName"), - x => Alias(x.Edited, "versionCultureEdited")); + x => Alias(x.Name, "versionCultureName")) + .AndSelect( + x => Alias(x.Current, "versionCurrent")) + .AndSelect( + x => Alias(x.Published, "versionPublished")); } } @@ -534,8 +543,12 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (isContent || isMedia) { + if (isMedia) + sql.InnerJoin().On((left, right) => left.NodeId == right.NodeId && right.Current); + else + sql.InnerJoin().On((left, right) => left.NodeId == right.NodeId); + sql - .InnerJoin().On((left, right) => left.NodeId == right.NodeId && right.Current) .InnerJoin().On((left, right) => left.NodeId == right.NodeId) .LeftJoin().On((left, right) => left.ContentTypeId == right.NodeId); } @@ -554,12 +567,18 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (isContent) sql - .LeftJoin().On((left, right) => left.Id == right.VersionId); + .LeftJoin().On((left, right) => left.Id == right.VersionId) + .LeftJoin().On((left, right) => left.Id == right.Id); } filter?.Invoke(sql); + if (isContent) + { + sql.Where($"{SqlSyntax.GetFieldName(x => x.Current)} = 1 OR {SqlSyntax.GetFieldName(x => x.Published)} = 1"); + } + return sql; } @@ -617,7 +636,9 @@ namespace Umbraco.Core.Persistence.Repositories.Implement { sql .AndBy(x => x.Published, x => x.Edited) - .AndBy(x => x.Id, x => x.LanguageId, x => x.Name); + .AndBy(x => x.Id, x => x.LanguageId, x => x.Name) + .AndBy(x => x.Id, x => x.Current) + .AndBy(x => x.Id, x => x.Published); } @@ -924,11 +945,11 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (dto.Published) { publishedCultures.Add(isoCode); - if (info.Edited) - { - editedCultures.Add(isoCode); - } } + //if (info.Edited) + //{ + // editedCultures.Add(isoCode); + //} } entity.CultureNames = variantInfo; entity.Variations = dto.Variations; From 19e435224394c80e678a8f7883d953af762b3889 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 21 Sep 2018 15:28:36 +1000 Subject: [PATCH 04/10] Revert "making an attempt at a single SQL statement to get publish info for variants ... but failing, will revert" This reverts commit c65480cf77287d1dc6c6676662ee0b30b08d1f56. --- .../Implement/EntityRepository.cs | 45 +++++-------------- 1 file changed, 12 insertions(+), 33 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs index ed764b328b..70fca3fd34 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs @@ -3,14 +3,12 @@ using System.Collections.Generic; using System.Linq; using NPoco; using Umbraco.Core.Models; -using Umbraco.Core.Persistence; using Umbraco.Core.Models.Entities; using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Scoping; using static Umbraco.Core.Persistence.NPocoSqlExtensions.Statics; -using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Repositories.Implement { @@ -27,7 +25,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement { private readonly IScopeAccessor _scopeAccessor; private readonly ILanguageRepository _langRepository; - + public EntityRepository(IScopeAccessor scopeAccessor, ILanguageRepository langRepository) { _scopeAccessor = scopeAccessor; @@ -36,7 +34,6 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected IUmbracoDatabase Database => _scopeAccessor.AmbientScope.Database; protected Sql Sql() => _scopeAccessor.AmbientScope.SqlContext.Sql(); - protected ISqlSyntaxProvider SqlSyntax => _scopeAccessor.AmbientScope.SqlContext.SqlSyntax; #region Repository @@ -463,11 +460,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement public int LanguageId { get; set; } [Column("versionCultureName")] public string Name { get; set; } - - [Column("versionCurrent")] - public bool VersionCurrent { get; set; } - [Column("versionPublished")] - public bool VersionPublished { get; set; } + [Column("versionCultureEdited")] + public bool Edited { get; set; } } // ReSharper disable once ClassNeverInstantiated.Local @@ -530,11 +524,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement .AndSelect( x => Alias(x.Id, "versionCultureId"), x => Alias(x.LanguageId, "versionCultureLangId"), - x => Alias(x.Name, "versionCultureName")) - .AndSelect( - x => Alias(x.Current, "versionCurrent")) - .AndSelect( - x => Alias(x.Published, "versionPublished")); + x => Alias(x.Name, "versionCultureName"), + x => Alias(x.Edited, "versionCultureEdited")); } } @@ -543,12 +534,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (isContent || isMedia) { - if (isMedia) - sql.InnerJoin().On((left, right) => left.NodeId == right.NodeId && right.Current); - else - sql.InnerJoin().On((left, right) => left.NodeId == right.NodeId); - sql + .InnerJoin().On((left, right) => left.NodeId == right.NodeId && right.Current) .InnerJoin().On((left, right) => left.NodeId == right.NodeId) .LeftJoin().On((left, right) => left.ContentTypeId == right.NodeId); } @@ -567,18 +554,12 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (isContent) sql - .LeftJoin().On((left, right) => left.Id == right.VersionId) - .LeftJoin().On((left, right) => left.Id == right.Id); + .LeftJoin().On((left, right) => left.Id == right.VersionId); } filter?.Invoke(sql); - if (isContent) - { - sql.Where($"{SqlSyntax.GetFieldName(x => x.Current)} = 1 OR {SqlSyntax.GetFieldName(x => x.Published)} = 1"); - } - return sql; } @@ -636,9 +617,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement { sql .AndBy(x => x.Published, x => x.Edited) - .AndBy(x => x.Id, x => x.LanguageId, x => x.Name) - .AndBy(x => x.Id, x => x.Current) - .AndBy(x => x.Id, x => x.Published); + .AndBy(x => x.Id, x => x.LanguageId, x => x.Name); } @@ -945,11 +924,11 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (dto.Published) { publishedCultures.Add(isoCode); + if (info.Edited) + { + editedCultures.Add(isoCode); + } } - //if (info.Edited) - //{ - // editedCultures.Add(isoCode); - //} } entity.CultureNames = variantInfo; entity.Variations = dto.Variations; From a39e77d1cfc480de8ccde45176adca653903b5a4 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 21 Sep 2018 15:49:37 +1000 Subject: [PATCH 05/10] Gets culture publishing info in IDocumentEntitySlim and has the tree showing the correct publish state, but missing 'edited' flag since i think that requires yet-another-query --- .../Dtos/ContentVersionCultureVariationDto.cs | 5 +- .../Persistence/NPocoSqlExtensions.cs | 21 +- .../Implement/EntityRepository.cs | 478 +++++------------- .../Trees/ContentTreeController.cs | 24 +- 4 files changed, 140 insertions(+), 388 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Dtos/ContentVersionCultureVariationDto.cs b/src/Umbraco.Core/Persistence/Dtos/ContentVersionCultureVariationDto.cs index a4e51b913e..27107b0afc 100644 --- a/src/Umbraco.Core/Persistence/Dtos/ContentVersionCultureVariationDto.cs +++ b/src/Umbraco.Core/Persistence/Dtos/ContentVersionCultureVariationDto.cs @@ -42,7 +42,8 @@ namespace Umbraco.Core.Persistence.Dtos [NullSetting(NullSetting = NullSettings.Null)] public int? PublishedUserId { get => _publishedUserId == 0 ? null : _publishedUserId; set => _publishedUserId = value; } //return null if zero - [Column("edited")] - public bool Edited { get; set; } + // fixme: I've commented this out, it's never used, need to review + //[Column("edited")] + //public bool Edited { get; set; } } } diff --git a/src/Umbraco.Core/Persistence/NPocoSqlExtensions.cs b/src/Umbraco.Core/Persistence/NPocoSqlExtensions.cs index 0d3faa0d68..61f46683d8 100644 --- a/src/Umbraco.Core/Persistence/NPocoSqlExtensions.cs +++ b/src/Umbraco.Core/Persistence/NPocoSqlExtensions.cs @@ -76,26 +76,7 @@ namespace Umbraco.Core.Persistence var (s, a) = sql.SqlContext.Visit(predicate, alias); return sql.Where(s, a); } - - /// - /// Appends an AND clause to a WHERE Sql statement. - /// - /// The type of the Dto. - /// The Sql statement. - /// A predicate to transform and append to the Sql statement. - /// An optional alias for the table. - /// The Sql statement. - /// - /// Chaining .Where(...).Where(...) in NPoco works because it merges the two WHERE statements, - /// however if the first statement is not an explicit WHERE statement, chaining fails and two WHERE - /// statements appear in the resulting Sql. This allows for adding an AND clause without problems. - /// - public static Sql AndWhere(this Sql sql, Expression> predicate, string alias = null) - { - var (s, a) = sql.SqlContext.Visit(predicate, alias); - return sql.Append("AND (" + s + ")", a); - } - + /// /// Appends a WHERE clause to the Sql statement. /// diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs index 70fca3fd34..74ecb23071 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs @@ -9,6 +9,7 @@ using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Scoping; using static Umbraco.Core.Persistence.NPocoSqlExtensions.Statics; +using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Repositories.Implement { @@ -34,6 +35,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected IUmbracoDatabase Database => _scopeAccessor.AmbientScope.Database; protected Sql Sql() => _scopeAccessor.AmbientScope.SqlContext.Sql(); + protected ISqlSyntaxProvider SqlSyntax => _scopeAccessor.AmbientScope.SqlContext.SqlSyntax; #region Repository @@ -57,83 +59,13 @@ namespace Umbraco.Core.Persistence.Repositories.Implement //fixme - we should be able to do sql = sql.OrderBy(x => Alias(x.NodeId, "NodeId")); but we can't because the OrderBy extension don't support Alias currently sql = sql.OrderBy("NodeId"); - //IEnumerable result; - // - //if (isMedia) - //{ - // //Treat media differently for now, as an Entity it will be returned with ALL of it's properties in the AdditionalData bag! - // var pagedResult = UnitOfWork.Database.Page(pageIndex + 1, pageSize, pagedSql); - - // var ids = pagedResult.Items.Select(x => (int)x.id).InGroupsOf(2000); - // var entities = pagedResult.Items.Select(BuildEntityFromDynamic).Cast().ToList(); - - // //Now we need to merge in the property data since we need paging and we can't do this the way that the big media query was working before - // foreach (var idGroup in ids) - // { - // var propSql = GetPropertySql(Constants.ObjectTypes.Media) - // .WhereIn(x => x.NodeId, idGroup) - // .OrderBy(x => x.NodeId); - - // //This does NOT fetch all data into memory in a list, this will read - // // over the records as a data reader, this is much better for performance and memory, - // // but it means that during the reading of this data set, nothing else can be read - // // from SQL server otherwise we'll get an exception. - // var allPropertyData = UnitOfWork.Database.Query(propSql); - - // //keep track of the current property data item being enumerated - // var propertyDataSetEnumerator = allPropertyData.GetEnumerator(); - // var hasCurrent = false; // initially there is no enumerator.Current - - // try - // { - // //This must be sorted by node id (which is done by SQL) because this is how we are sorting the query to lookup property types above, - // // which allows us to more efficiently iterate over the large data set of property values. - // foreach (var entity in entities) - // { - // // assemble the dtos for this def - // // use the available enumerator.Current if any else move to next - // while (hasCurrent || propertyDataSetEnumerator.MoveNext()) - // { - // if (propertyDataSetEnumerator.Current.nodeId == entity.Id) - // { - // hasCurrent = false; // enumerator.Current is not available - - // //the property data goes into the additional data - // entity.AdditionalData[propertyDataSetEnumerator.Current.propertyTypeAlias] = new UmbracoEntity.EntityProperty - // { - // PropertyEditorAlias = propertyDataSetEnumerator.Current.propertyEditorAlias, - // Value = StringExtensions.IsNullOrWhiteSpace(propertyDataSetEnumerator.Current.textValue) - // ? propertyDataSetEnumerator.Current.varcharValue - // : StringExtensions.ConvertToJsonIfPossible(propertyDataSetEnumerator.Current.textValue) - // }; - // } - // else - // { - // hasCurrent = true; // enumerator.Current is available for another def - // break; // no more propertyDataDto for this def - // } - // } - // } - // } - // finally - // { - // propertyDataSetEnumerator.Dispose(); - // } - // } - - // result = entities; - //} - //else - //{ - // var pagedResult = UnitOfWork.Database.Page(pageIndex + 1, pageSize, pagedSql); - // result = pagedResult.Items.Select(BuildEntityFromDynamic).Cast().ToList(); - //} var page = Database.Page(pageIndex + 1, pageSize, sql); var dtos = page.Items; var entities = dtos.Select(x => BuildEntity(isContent, isMedia, x)).ToArray(); - - //TODO: For isContent will we need to build up the variation info? + + if (isContent) + BuildVariantInfo(entities); if (isMedia) BuildProperties(entities, dtos); @@ -158,7 +90,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement ddto => ddto.VariationInfo, ddto => ddto.VersionId, sql); - return dtos.Count == 0 ? null : BuildDocumentEntity(dtos[0]); + + return dtos.Count == 0 ? null : BuildVariantInfo(BuildDocumentEntity(dtos[0]))[0]; } var dto = Database.FirstOrDefault(sql); @@ -220,9 +153,10 @@ namespace Umbraco.Core.Persistence.Repositories.Implement dto => dto.VariationInfo, dto => dto.VersionId, sql); + return cdtos.Count == 0 ? Enumerable.Empty() - : cdtos.Select(BuildDocumentEntity).ToArray(); + : BuildVariantInfo(cdtos.Select(BuildDocumentEntity).ToArray()).ToList(); } var dtos = Database.Fetch(sql); @@ -323,7 +257,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement private void BuildProperties(EntitySlim[] entities, List dtos) { - var versionIds = dtos.Select(x => x.VersionId).Distinct().ToArray(); + var versionIds = dtos.Select(x => x.VersionId).Distinct().ToList(); var pdtos = Database.FetchByGroups(versionIds, 2000, GetPropertyData); var xentity = entities.ToDictionary(x => x.Id, x => x); // nodeId -> entity @@ -346,10 +280,54 @@ namespace Umbraco.Core.Persistence.Repositories.Implement entity.AdditionalData[pdto.PropertyTypeDto.Alias] = new EntitySlim.PropertySlim(pdto.PropertyTypeDto.DataTypeDto.EditorAlias, value); } + private void BuildVariantInfo(EntitySlim[] entities) + { + BuildVariantInfo((DocumentEntitySlim[])entities); + } + + private DocumentEntitySlim[] BuildVariantInfo(params DocumentEntitySlim[] entities) + { + if (entities.Any(x => x.Variations.VariesByCulture())) + { + //each EntitySlim at this stage is an DocumentEntitySlim + + var dtos = Database.FetchByGroups(entities.Select(x => x.Id), 2000, GetVariantPublishedInfo) + .GroupBy(x => x.NodeId) + .ToDictionary(x => x.Key, x => (IEnumerable)x); + + foreach (var e in entities.OfType().Where(x => x.Variations.VariesByCulture()).OrderBy(x => x.Id)) + { + //fixme: how do i get this info? Seems that requires another query since that is how I think it's done in the DocumentRepository + //e.EditedCultures = + e.PublishedCultures = dtos[e.Id].Where(x => x.VersionPublished).Select(x => x.IsoCode).Distinct().ToList(); + } + } + + return entities; + } + #endregion #region Sql + private Sql GetVariantPublishedInfo(IEnumerable ids) + { + var sql = Sql(); + sql + .Select(x => x.NodeId, x => Alias(x.Id, "versionId"), x => Alias(x.Current, "versionCurrent")) + .AndSelect(x => Alias(x.Id, "versionCultureId")) + .AndSelect(x => x.IsoCode) + .AndSelect(x => Alias(x.Published, "versionPublished")) + .From() + .InnerJoin().On(x => x.NodeId, x => x.NodeId) + .InnerJoin().On(x => x.Id, x => x.VersionId) + .InnerJoin().On(x => x.Id, x => x.Id) + .InnerJoin().On(x => x.LanguageId, x => x.Id) + .Where(x => x.NodeObjectType == Constants.ObjectTypes.Document) + .WhereIn(x => x.NodeId, ids) + .Where($"{SqlSyntax.GetFieldName(x => x.Current)} = 1 OR {SqlSyntax.GetFieldName(x => x.Published)} = 1"); + return sql; + } // gets the full sql for a given object type and a given unique id protected Sql GetFullSqlForEntityType(bool isContent, bool isMedia, Guid objectType, Guid uniqueId) { @@ -371,24 +349,6 @@ namespace Umbraco.Core.Persistence.Repositories.Implement return AddGroupBy(isContent, isMedia, sql); } - // fixme kill this nonsense - //// gets the SELECT + FROM + WHERE sql - //// to get all property data for all items of the specified object type - //private Sql GetPropertySql(Guid objectType) - //{ - // return Sql() - // .Select(x => x.VersionId, x => x.TextValue, x => x.VarcharValue) - // .AndSelect(x => x.NodeId) - // .AndSelect(x => x.PropertyEditorAlias) - // .AndSelect(x => Alias(x.Alias, "propertyTypeAlias")) - // .From() - // .InnerJoin().On((left, right) => left.VersionId == right.Id) - // .InnerJoin().On((left, right) => left.NodeId == right.NodeId) - // .InnerJoin().On(dto => dto.PropertyTypeId, dto => dto.Id) - // .InnerJoin().On(dto => dto.DataTypeId, dto => dto.DataTypeId) - // .Where(x => x.NodeObjectType == objectType); - //} - private Sql GetPropertyData(int versionId) { return Sql() @@ -410,89 +370,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement .OrderBy(x => x.VersionId); } - // fixme - wtf is this? - //private Sql GetFullSqlForMedia(Sql entitySql, Action> filter = null) - //{ - // //this will add any varcharValue property to the output which can be added to the additional properties - - // var sql = GetPropertySql(Constants.ObjectTypes.Media); - - // filter?.Invoke(sql); - - // // We're going to create a query to query against the entity SQL - // // because we cannot group by nText columns and we have a COUNT in the entitySql we cannot simply left join - // // the entitySql query, we have to join the wrapped query to get the ntext in the result - - // var wrappedSql = Sql() - // .Append("SELECT * FROM (") - // .Append(entitySql) - // .Append(") tmpTbl LEFT JOIN (") - // .Append(sql) - // .Append(") as property ON id = property.nodeId") - // .OrderBy("sortOrder, id"); - - // return wrappedSql; - //} - - - /// - /// The DTO used to fetch results for a content item with its variation info - /// - private class ContentEntityDto : BaseDto - { - public ContentVariation Variations { get; set; } - - [ResultColumn, Reference(ReferenceType.Many)] - public List VariationInfo { get; set; } - - public bool Published { get; set; } - public bool Edited { get; set; } - } - - /// - /// The DTO used in the 1:M result for content variation info - /// - private class ContentEntityVariationInfoDto - { - [Column("versionCultureId")] - public int VersionCultureId { get; set; } - [Column("versionCultureLangId")] - public int LanguageId { get; set; } - [Column("versionCultureName")] - public string Name { get; set; } - [Column("versionCultureEdited")] - public bool Edited { get; set; } - } - - // ReSharper disable once ClassNeverInstantiated.Local - /// - /// the DTO corresponding to fields selected by GetBase - /// - private class BaseDto - { - // ReSharper disable UnusedAutoPropertyAccessor.Local - // ReSharper disable UnusedMember.Local - public int NodeId { get; set; } - public bool Trashed { get; set; } - public int ParentId { get; set; } - public int? UserId { get; set; } - public int Level { get; set; } - public string Path { get; set; } - public int SortOrder { get; set; } - public Guid UniqueId { get; set; } - public string Text { get; set; } - public Guid NodeObjectType { get; set; } - public DateTime CreateDate { get; set; } - public int Children { get; set; } - public int VersionId { get; set; } - public string Alias { get; set; } - public string Icon { get; set; } - public string Thumbnail { get; set; } - public bool IsContainer { get; set; } - - // ReSharper restore UnusedAutoPropertyAccessor.Local - // ReSharper restore UnusedMember.Local - } + // gets the base SELECT + FROM [+ filter] sql // always from the 'current' content version @@ -524,8 +402,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement .AndSelect( x => Alias(x.Id, "versionCultureId"), x => Alias(x.LanguageId, "versionCultureLangId"), - x => Alias(x.Name, "versionCultureName"), - x => Alias(x.Edited, "versionCultureEdited")); + x => Alias(x.Name, "versionCultureName")); } } @@ -537,7 +414,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement sql .InnerJoin().On((left, right) => left.NodeId == right.NodeId && right.Current) .InnerJoin().On((left, right) => left.NodeId == right.NodeId) - .LeftJoin().On((left, right) => left.ContentTypeId == right.NodeId); + .InnerJoin().On((left, right) => left.ContentTypeId == right.NodeId); } if (isContent) @@ -652,184 +529,73 @@ namespace Umbraco.Core.Persistence.Repositories.Implement public string TextValue { get; set; } } - // fixme kill + /// - /// This is a special relator in that it is not returning a DTO but a real resolved entity and that it accepts - /// a dynamic instance. + /// The DTO used to fetch results for a content item with its variation info /// - /// - /// We're doing this because when we query the db, we want to use dynamic so that it returns all available fields not just the ones - /// defined on the entity so we can them to additional data - /// - //internal class UmbracoEntityRelator - //{ - // internal UmbracoEntity Current; + private class ContentEntityDto : BaseDto + { + public ContentVariation Variations { get; set; } - // public IEnumerable MapAll(IEnumerable input) - // { - // UmbracoEntity entity; + [ResultColumn, Reference(ReferenceType.Many)] + public List VariationInfo { get; set; } - // foreach (var x in input) - // { - // entity = Map(x); - // if (entity != null) yield return entity; - // } + public bool Published { get; set; } + public bool Edited { get; set; } + } - // entity = Map((dynamic) null); - // if (entity != null) yield return entity; - // } + private class VariationPublishInfoDto + { + public int NodeId { get; set; } + public int VersionId { get; set; } + public bool VersionCurrent { get; set; } + public string IsoCode { get; set; } + public int VersionCultureId { get; set; } + public bool VersionPublished { get; set; } + } - // // must be called one last time with null in order to return the last one! - // public UmbracoEntity Map(dynamic a) - // { - // // Terminating call. Since we can return null from this function - // // we need to be ready for NPoco to callback later with null - // // parameters - // if (a == null) - // return Current; + /// + /// The DTO used in the 1:M result for content variation info + /// + private class ContentEntityVariationInfoDto + { + [Column("versionCultureId")] + public int VersionCultureId { get; set; } + [Column("versionCultureLangId")] + public int LanguageId { get; set; } + [Column("versionCultureName")] + public string Name { get; set; } + } - // string pPropertyEditorAlias = a.propertyEditorAlias; - // var pExists = pPropertyEditorAlias != null; - // string pPropertyAlias = a.propertyTypeAlias; - // string pTextValue = a.textValue; - // string pNVarcharValue = a.varcharValue; - - // // Is this the same UmbracoEntity as the current one we're processing - // if (Current != null && Current.Key == a.uniqueID) - // { - // if (pExists && pPropertyAlias.IsNullOrWhiteSpace() == false) - // { - // // Add this UmbracoProperty to the current additional data - // Current.AdditionalData[pPropertyAlias] = new UmbracoEntity.EntityProperty - // { - // PropertyEditorAlias = pPropertyEditorAlias, - // Value = pTextValue.IsNullOrWhiteSpace() - // ? pNVarcharValue - // : pTextValue.ConvertToJsonIfPossible() - // }; - // } - - // // Return null to indicate we're not done with this UmbracoEntity yet - // return null; - // } - - // // This is a different UmbracoEntity to the current one, or this is the - // // first time through and we don't have a Tab yet - - // // Save the current UmbracoEntityDto - // var prev = Current; - - // // Setup the new current UmbracoEntity - - // Current = BuildEntityFromDynamic(a); - - // if (pExists && pPropertyAlias.IsNullOrWhiteSpace() == false) - // { - // //add the property/create the prop list if null - // Current.AdditionalData[pPropertyAlias] = new UmbracoEntity.EntityProperty - // { - // PropertyEditorAlias = pPropertyEditorAlias, - // Value = pTextValue.IsNullOrWhiteSpace() - // ? pNVarcharValue - // : pTextValue.ConvertToJsonIfPossible() - // }; - // } - - // // Return the now populated previous UmbracoEntity (or null if first time through) - // return prev; - // } - //} - - // fixme need to review what's below - // comes from 7.6, similar to what's in VersionableRepositoryBase - // not sure it really makes sense... - - //private class EntityDefinitionCollection : KeyedCollection - //{ - // protected override int GetKeyForItem(EntityDefinition item) - // { - // return item.Id; - // } - - // /// - // /// if this key already exists if it does then we need to check - // /// if the existing item is 'older' than the new item and if that is the case we'll replace the older one - // /// - // /// - // /// - // public bool AddOrUpdate(EntityDefinition item) - // { - // if (Dictionary == null) - // { - // Add(item); - // return true; - // } - - // var key = GetKeyForItem(item); - // if (TryGetValue(key, out EntityDefinition found)) - // { - // //it already exists and it's older so we need to replace it - // if (item.VersionId > found.VersionId) - // { - // var currIndex = Items.IndexOf(found); - // if (currIndex == -1) - // throw new IndexOutOfRangeException("Could not find the item in the list: " + found.Id); - - // //replace the current one with the newer one - // SetItem(currIndex, item); - // return true; - // } - // //could not add or update - // return false; - // } - - // Add(item); - // return true; - // } - - // private bool TryGetValue(int key, out EntityDefinition val) - // { - // if (Dictionary != null) return Dictionary.TryGetValue(key, out val); - - // val = null; - // return false; - // } - //} - - // fixme wtf is this, why dynamics here, this is horrible !! - //private class EntityDefinition - //{ - // private readonly dynamic _entity; - // private readonly bool _isContent; - // private readonly bool _isMedia; - - // public EntityDefinition(dynamic entity, bool isContent, bool isMedia) - // { - // _entity = entity; - // _isContent = isContent; - // _isMedia = isMedia; - // } - - // public IUmbracoEntity BuildFromDynamic() - // { - // return BuildEntityFromDynamic(_entity); - // } - - // public int Id => _entity.id; - - // public int VersionId - // { - // get - // { - // if (_isContent || _isMedia) - // { - // return _entity.versionId; - // } - // return _entity.id; - // } - // } - //} + // ReSharper disable once ClassNeverInstantiated.Local + /// + /// the DTO corresponding to fields selected by GetBase + /// + private class BaseDto + { + // ReSharper disable UnusedAutoPropertyAccessor.Local + // ReSharper disable UnusedMember.Local + public int NodeId { get; set; } + public bool Trashed { get; set; } + public int ParentId { get; set; } + public int? UserId { get; set; } + public int Level { get; set; } + public string Path { get; set; } + public int SortOrder { get; set; } + public Guid UniqueId { get; set; } + public string Text { get; set; } + public Guid NodeObjectType { get; set; } + public DateTime CreateDate { get; set; } + public int Children { get; set; } + public int VersionId { get; set; } + public string Alias { get; set; } + public string Icon { get; set; } + public string Thumbnail { get; set; } + public bool IsContainer { get; set; } + // ReSharper restore UnusedAutoPropertyAccessor.Local + // ReSharper restore UnusedMember.Local + } #endregion #region Factory @@ -914,21 +680,15 @@ namespace Umbraco.Core.Persistence.Repositories.Implement //fill in the variant info if (dto.Variations.VariesByCulture() && dto.VariationInfo != null && dto.VariationInfo.Count > 0) { + //fixme: Currently require making a 2nd query to fill in publish status information for variants + var variantInfo = new Dictionary(StringComparer.InvariantCultureIgnoreCase); foreach (var info in dto.VariationInfo) { var isoCode = _langRepository.GetIsoCodeById(info.LanguageId); if (isoCode != null) variantInfo[isoCode] = info.Name; - - if (dto.Published) - { - publishedCultures.Add(isoCode); - if (info.Edited) - { - editedCultures.Add(isoCode); - } - } + } entity.CultureNames = variantInfo; entity.Variations = dto.Variations; diff --git a/src/Umbraco.Web/Trees/ContentTreeController.cs b/src/Umbraco.Web/Trees/ContentTreeController.cs index 33a347d3d4..878a277177 100644 --- a/src/Umbraco.Web/Trees/ContentTreeController.cs +++ b/src/Umbraco.Web/Trees/ContentTreeController.cs @@ -47,10 +47,10 @@ namespace Umbraco.Web.Trees /// protected override TreeNode GetSingleTreeNode(IEntitySlim entity, string parentId, FormDataCollection queryStrings) { - var langId = queryStrings?["culture"]; + var culture = queryStrings?["culture"]; var allowedUserOptions = GetAllowedUserMenuItemsForNode(entity); - if (CanUserAccessNode(entity, allowedUserOptions, langId)) + if (CanUserAccessNode(entity, allowedUserOptions, culture)) { //Special check to see if it ia a container, if so then we'll hide children. var isContainer = entity.IsContainer; // && (queryStrings.Get("isDialog") != "true"); @@ -74,11 +74,21 @@ namespace Umbraco.Web.Trees { var documentEntity = (IDocumentEntitySlim) entity; - //fixme we need these statuses per variant but to do that we need to fix the issues listed in IDocumentEntitySlim - if (!documentEntity.Published) - node.SetNotPublishedStyle(); - //if (documentEntity.Edited) - // node.SetHasUnpublishedVersionStyle(); + if (!documentEntity.Variations.VariesByCulture()) + { + if (!documentEntity.Published) + node.SetNotPublishedStyle(); + if (documentEntity.Edited) + node.SetHasUnpublishedVersionStyle(); + } + else + { + if (!culture.IsNullOrWhiteSpace()) + { + if (!documentEntity.PublishedCultures.Contains(culture)) + node.SetNotPublishedStyle(); + } + } node.AdditionalData.Add("contentType", documentEntity.ContentTypeAlias); } From 5f805b0804ec84a67add17d9f8137535d0b89594 Mon Sep 17 00:00:00 2001 From: Stephan Date: Tue, 25 Sep 2018 10:55:06 +0200 Subject: [PATCH 06/10] Extend NPoco fluent querying --- .../Persistence/NPocoSqlExtensions.cs | 36 ++++++++ .../Querying/PocoToSqlExpressionVisitor.cs | 89 +++++++++++++++++++ 2 files changed, 125 insertions(+) diff --git a/src/Umbraco.Core/Persistence/NPocoSqlExtensions.cs b/src/Umbraco.Core/Persistence/NPocoSqlExtensions.cs index 61f46683d8..d97c748b6f 100644 --- a/src/Umbraco.Core/Persistence/NPocoSqlExtensions.cs +++ b/src/Umbraco.Core/Persistence/NPocoSqlExtensions.cs @@ -392,6 +392,23 @@ namespace Umbraco.Core.Persistence #region Joins + /// + /// Appends a CROSS JOIN clause to the Sql statement. + /// + /// The type of the Dto. + /// The Sql statement. + /// An optional alias for the joined table. + /// The Sql statement. + public static Sql CrossJoin(this Sql sql, string alias = null) + { + var type = typeof(TDto); + var tableName = type.GetTableName(); + var join = sql.SqlContext.SqlSyntax.GetQuotedTableName(tableName); + if (alias != null) join += " " + sql.SqlContext.SqlSyntax.GetQuotedTableName(alias); + + return sql.Append("CROSS JOIN " + join); + } + /// /// Appends an INNER JOIN clause to the Sql statement. /// @@ -533,6 +550,25 @@ namespace Umbraco.Core.Persistence return sqlJoin.On(onExpression, expresionist.GetSqlParameters()); } + /// + /// Appends an ON clause to a SqlJoin statement. + /// + /// The type of Dto 1. + /// The type of Dto 2. + /// The type of Dto 3. + /// The SqlJoin statement. + /// A predicate to transform and use as the ON clause body. + /// An optional alias for Dto 1 table. + /// An optional alias for Dto 2 table. + /// An optional alias for Dto 3 table. + /// The Sql statement. + public static Sql On(this Sql.SqlJoinClause sqlJoin, Expression> predicate, string aliasLeft = null, string aliasRight = null, string aliasOther = null) + { + var expresionist = new PocoToSqlExpressionVisitor(sqlJoin.SqlContext, aliasLeft, aliasRight, aliasOther); + var onExpression = expresionist.Visit(predicate); + return sqlJoin.On(onExpression, expresionist.GetSqlParameters()); + } + #endregion #region Select diff --git a/src/Umbraco.Core/Persistence/Querying/PocoToSqlExpressionVisitor.cs b/src/Umbraco.Core/Persistence/Querying/PocoToSqlExpressionVisitor.cs index 4d33977c72..971b65c220 100644 --- a/src/Umbraco.Core/Persistence/Querying/PocoToSqlExpressionVisitor.cs +++ b/src/Umbraco.Core/Persistence/Querying/PocoToSqlExpressionVisitor.cs @@ -167,4 +167,93 @@ namespace Umbraco.Core.Persistence.Querying } } + /// + /// Represents an expression tree parser used to turn strongly typed expressions into SQL statements. + /// + /// The type of DTO 1. + /// The type of DTO 2. + /// The type of DTO 3. + /// This visitor is stateful and cannot be reused. + internal class PocoToSqlExpressionVisitor : ExpressionVisitorBase + { + private readonly PocoData _pocoData1, _pocoData2, _pocoData3; + private readonly string _alias1, _alias2, _alias3; + private string _parameterName1, _parameterName2, _parameterName3; + + public PocoToSqlExpressionVisitor(ISqlContext sqlContext, string alias1, string alias2, string alias3) + : base(sqlContext.SqlSyntax) + { + _pocoData1 = sqlContext.PocoDataFactory.ForType(typeof(TDto1)); + _pocoData2 = sqlContext.PocoDataFactory.ForType(typeof(TDto2)); + _pocoData3 = sqlContext.PocoDataFactory.ForType(typeof(TDto3)); + _alias1 = alias1; + _alias2 = alias2; + _alias3 = alias3; + } + + protected override string VisitLambda(LambdaExpression lambda) + { + if (lambda.Parameters.Count == 3) + { + _parameterName1 = lambda.Parameters[0].Name; + _parameterName2 = lambda.Parameters[1].Name; + _parameterName3 = lambda.Parameters[2].Name; + } + else if (lambda.Parameters.Count == 2) + { + _parameterName1 = lambda.Parameters[0].Name; + _parameterName2 = lambda.Parameters[1].Name; + } + else + { + _parameterName1 = _parameterName2 = null; + } + return base.VisitLambda(lambda); + } + + protected override string VisitMemberAccess(MemberExpression m) + { + if (m.Expression != null) + { + if (m.Expression.NodeType == ExpressionType.Parameter) + { + var pex = (ParameterExpression)m.Expression; + + if (pex.Name == _parameterName1) + return Visited ? string.Empty : GetFieldName(_pocoData1, m.Member.Name, _alias1); + + if (pex.Name == _parameterName2) + return Visited ? string.Empty : GetFieldName(_pocoData2, m.Member.Name, _alias2); + + if (pex.Name == _parameterName3) + return Visited ? string.Empty : GetFieldName(_pocoData3, m.Member.Name, _alias3); + } + else if (m.Expression.NodeType == ExpressionType.Convert) + { + // here: which _pd should we use?! + throw new NotSupportedException(); + //return Visited ? string.Empty : GetFieldName(_pd, m.Member.Name); + } + } + + var member = Expression.Convert(m, typeof(object)); + var lambda = Expression.Lambda>(member); + var getter = lambda.Compile(); + var o = getter(); + + SqlParameters.Add(o); + + // execute if not already compiled + return Visited ? string.Empty : "@" + (SqlParameters.Count - 1); + } + + protected virtual string GetFieldName(PocoData pocoData, string name, string alias) + { + var column = pocoData.Columns.FirstOrDefault(x => x.Value.MemberInfoData.Name == name); + var tableName = SqlSyntax.GetQuotedTableName(alias ?? pocoData.TableInfo.TableName); + var columnName = SqlSyntax.GetQuotedColumnName(column.Value.ColumnName); + + return tableName + "." + columnName; + } + } } From 3d19acb97eb17e395a73e3e78b2d9414a2bffd9d Mon Sep 17 00:00:00 2001 From: Stephan Date: Tue, 25 Sep 2018 10:55:33 +0200 Subject: [PATCH 07/10] Variants in EntityRepository --- .../Implement/EntityRepository.cs | 224 ++++++++---------- 1 file changed, 100 insertions(+), 124 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs index 74ecb23071..e6f1798f50 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs @@ -59,13 +59,12 @@ namespace Umbraco.Core.Persistence.Repositories.Implement //fixme - we should be able to do sql = sql.OrderBy(x => Alias(x.NodeId, "NodeId")); but we can't because the OrderBy extension don't support Alias currently sql = sql.OrderBy("NodeId"); - var page = Database.Page(pageIndex + 1, pageSize, sql); var dtos = page.Items; var entities = dtos.Select(x => BuildEntity(isContent, isMedia, x)).ToArray(); if (isContent) - BuildVariantInfo(entities); + BuildVariants(entities.Cast()); if (isMedia) BuildProperties(entities, dtos); @@ -86,12 +85,9 @@ namespace Umbraco.Core.Persistence.Repositories.Implement //isContent is going to return a 1:M result now with the variants so we need to do different things if (isContent) { - var dtos = Database.FetchOneToMany( - ddto => ddto.VariationInfo, - ddto => ddto.VersionId, - sql); - - return dtos.Count == 0 ? null : BuildVariantInfo(BuildDocumentEntity(dtos[0]))[0]; + var cdtos = Database.Fetch(sql); + + return cdtos.Count == 0 ? null : BuildVariants(BuildDocumentEntity(cdtos[0])); } var dto = Database.FirstOrDefault(sql); @@ -149,14 +145,11 @@ namespace Umbraco.Core.Persistence.Repositories.Implement //isContent is going to return a 1:M result now with the variants so we need to do different things if (isContent) { - var cdtos = Database.FetchOneToMany( - dto => dto.VariationInfo, - dto => dto.VersionId, - sql); + var cdtos = Database.Fetch(sql); return cdtos.Count == 0 ? Enumerable.Empty() - : BuildVariantInfo(cdtos.Select(BuildDocumentEntity).ToArray()).ToList(); + : BuildVariants(cdtos.Select(BuildDocumentEntity)).ToList(); } var dtos = Database.Fetch(sql); @@ -280,54 +273,90 @@ namespace Umbraco.Core.Persistence.Repositories.Implement entity.AdditionalData[pdto.PropertyTypeDto.Alias] = new EntitySlim.PropertySlim(pdto.PropertyTypeDto.DataTypeDto.EditorAlias, value); } - private void BuildVariantInfo(EntitySlim[] entities) - { - BuildVariantInfo((DocumentEntitySlim[])entities); - } + private DocumentEntitySlim BuildVariants(DocumentEntitySlim entity) + => BuildVariants(new[] { entity }).First(); - private DocumentEntitySlim[] BuildVariantInfo(params DocumentEntitySlim[] entities) + private IEnumerable BuildVariants(IEnumerable entities) { - if (entities.Any(x => x.Variations.VariesByCulture())) + List v = null; + var entitiesList = entities.ToList(); + foreach (var e in entitiesList) { - //each EntitySlim at this stage is an DocumentEntitySlim - - var dtos = Database.FetchByGroups(entities.Select(x => x.Id), 2000, GetVariantPublishedInfo) - .GroupBy(x => x.NodeId) - .ToDictionary(x => x.Key, x => (IEnumerable)x); - - foreach (var e in entities.OfType().Where(x => x.Variations.VariesByCulture()).OrderBy(x => x.Id)) - { - //fixme: how do i get this info? Seems that requires another query since that is how I think it's done in the DocumentRepository - //e.EditedCultures = - e.PublishedCultures = dtos[e.Id].Where(x => x.VersionPublished).Select(x => x.IsoCode).Distinct().ToList(); - } + if (e.Variations.VariesByCulture()) + (v ?? (v = new List())).Add(e); } - return entities; + if (v == null) return entitiesList; + + // fetch all variant info dtos + var dtos = Database.FetchByGroups(v.Select(x => x.Id), 2000, GetVariantInfos); + + // group by node id (each group contains all languages) + var xdtos = dtos.GroupBy(x => x.NodeId).ToDictionary(x => x.Key, x => x); + + foreach (var e in v) + { + // since we're only iterating on entities that vary, we must have something + var edtos = xdtos[e.Id]; + + e.CultureNames = edtos.Where(x => x.CultureAvailable).ToDictionary(x => x.IsoCode, x => x.Name); + e.PublishedCultures = edtos.Where(x => x.CulturePublished).Select(x => x.IsoCode); + e.EditedCultures = edtos.Where(x => x.CultureAvailable && x.CultureEdited).Select(x => x.IsoCode); + } + + return entitiesList; } #endregion #region Sql - private Sql GetVariantPublishedInfo(IEnumerable ids) + protected Sql GetVariantInfos(IEnumerable ids) { - var sql = Sql(); - sql - .Select(x => x.NodeId, x => Alias(x.Id, "versionId"), x => Alias(x.Current, "versionCurrent")) - .AndSelect(x => Alias(x.Id, "versionCultureId")) + // this ... is an interesting query - could we make it simpler? probably, by having DocumentCultureVariationDto + // handle 'available' and 'published' in addition to 'edited' - would take (a bit) more time to save a document, + // but would make querying way faster + + return Sql() + .Select(x => x.NodeId) .AndSelect(x => x.IsoCode) - .AndSelect(x => Alias(x.Published, "versionPublished")) + .AndSelect("doc", x => Alias(x.Published, "DocumentPublished"), x => Alias(x.Edited, "DocumentEdited")) + .AndSelect("ccv", x => Alias(x.Id, "CultureAvailableData"), x => Alias(x.Name, "Name")) + .AndSelect("pcv", x => Alias(x.Id, "CulturePublishedData")) + .AndSelect("dcv", x => Alias(x.Edited, "CultureEditedData")) + + // from node x language .From() - .InnerJoin().On(x => x.NodeId, x => x.NodeId) - .InnerJoin().On(x => x.Id, x => x.VersionId) - .InnerJoin().On(x => x.Id, x => x.Id) - .InnerJoin().On(x => x.LanguageId, x => x.Id) - .Where(x => x.NodeObjectType == Constants.ObjectTypes.Document) - .WhereIn(x => x.NodeId, ids) - .Where($"{SqlSyntax.GetFieldName(x => x.Current)} = 1 OR {SqlSyntax.GetFieldName(x => x.Published)} = 1"); - return sql; + .CrossJoin() + + // join to document - always exists - indicates global document published/edited status + .InnerJoin("doc") + .On((node, doc) => node.NodeId == doc.NodeId, aliasRight: "doc") + + // left-join do document variation - matches cultures that are *available* + indicates when *edited* + .LeftJoin("dcv") + .On((node, dcv, lang) => node.NodeId == dcv.NodeId && lang.Id == dcv.LanguageId, aliasRight: "dcv") + + // join to current version - always exists + // left-join to current version variations - indicates which cultures are *available* + .InnerJoin("cv") + .On((node, ev) => node.NodeId == ev.NodeId && ev.Current, aliasRight: "cv") + .LeftJoin("ccv") + .On((cv, ccv, lang) => cv.Id == ccv.VersionId && lang.Id == ccv.LanguageId, "cv", "ccv") + + // left-join to published version - exists when whole node is published + // left-join to published version variations - matches cultures that are *published* + .LeftJoin(nested => nested.InnerJoin("dv") + .On((pv, dv) => pv.Id == dv.Id && dv.Published, "pv", "dv"), + "pv") + .On((node, pv) => node.NodeId == pv.NodeId, aliasRight: "pv") + .LeftJoin("pcv") + .On((pv, pcv, lang) => pv.Id == pcv.VersionId && lang.Id == pcv.LanguageId, "pv", "pcv") + + // for selected nodes + .WhereIn(x => x.NodeId, ids); } + // gets the full sql for a given object type and a given unique id protected Sql GetFullSqlForEntityType(bool isContent, bool isMedia, Guid objectType, Guid uniqueId) { @@ -368,9 +397,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement .InnerJoin().On((left, right) => left.DataTypeId == right.NodeId) .WhereIn(x => x.VersionId, versionIds) .OrderBy(x => x.VersionId); - } - - + } // gets the base SELECT + FROM [+ filter] sql // always from the 'current' content version @@ -397,12 +424,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (isContent) { sql - .AndSelect(x => x.Published, x => x.Edited) - //This MUST come last in the select statements since we will end up with a 1:M query - .AndSelect( - x => Alias(x.Id, "versionCultureId"), - x => Alias(x.LanguageId, "versionCultureLangId"), - x => Alias(x.Name, "versionCultureName")); + .AndSelect(x => x.Published, x => x.Edited); } } @@ -428,10 +450,6 @@ namespace Umbraco.Core.Persistence.Repositories.Implement { sql .LeftJoin("child").On((left, right) => left.NodeId == right.ParentId, aliasRight: "child"); - - if (isContent) - sql - .LeftJoin().On((left, right) => left.Id == right.VersionId); } @@ -493,8 +511,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (isContent) { sql - .AndBy(x => x.Published, x => x.Edited) - .AndBy(x => x.Id, x => x.LanguageId, x => x.Name); + .AndBy(x => x.Published, x => x.Edited); } @@ -537,34 +554,28 @@ namespace Umbraco.Core.Persistence.Repositories.Implement { public ContentVariation Variations { get; set; } - [ResultColumn, Reference(ReferenceType.Many)] - public List VariationInfo { get; set; } - public bool Published { get; set; } public bool Edited { get; set; } } - private class VariationPublishInfoDto + public class VariantInfoDto { public int NodeId { get; set; } - public int VersionId { get; set; } - public bool VersionCurrent { get; set; } public string IsoCode { get; set; } - public int VersionCultureId { get; set; } - public bool VersionPublished { get; set; } - } - - /// - /// The DTO used in the 1:M result for content variation info - /// - private class ContentEntityVariationInfoDto - { - [Column("versionCultureId")] - public int VersionCultureId { get; set; } - [Column("versionCultureLangId")] - public int LanguageId { get; set; } - [Column("versionCultureName")] public string Name { get; set; } + public bool DocumentPublished { get; set; } + public bool DocumentEdited { get; set; } + + public int? CultureAvailableData { get; set; } + public int? CulturePublishedData { get; set; } + public bool? CultureEditedData { get; set; } + + [ResultColumn] + public bool CultureAvailable => CultureAvailableData.HasValue; + [ResultColumn] + public bool CulturePublished => CulturePublishedData.HasValue; + [ResultColumn] + public bool CultureEdited => CultureEditedData ?? false; } // ReSharper disable once ClassNeverInstantiated.Local @@ -648,53 +659,18 @@ namespace Umbraco.Core.Persistence.Repositories.Implement private DocumentEntitySlim BuildDocumentEntity(BaseDto dto) { + // EntitySlim does not track changes + var entity = new DocumentEntitySlim(); + BuildContentEntity(entity, dto); + if (dto is ContentEntityDto contentDto) { - return BuildDocumentEntity(contentDto); + // fill in the invariant info + entity.Edited = contentDto.Edited; + entity.Published = contentDto.Published; + entity.Variations = contentDto.Variations; } - // EntitySlim does not track changes - var entity = new DocumentEntitySlim(); - BuildContentEntity(entity, dto); - return entity; - } - - /// - /// Builds the from a and ensures the AdditionalData is populated with variant info - /// - /// - /// - private DocumentEntitySlim BuildDocumentEntity(ContentEntityDto dto) - { - // EntitySlim does not track changes - var entity = new DocumentEntitySlim(); - BuildContentEntity(entity, dto); - - //fill in the invariant info - entity.Edited = dto.Edited; - entity.Published = dto.Published; - - var publishedCultures = new List(); - var editedCultures = new List(); - - //fill in the variant info - if (dto.Variations.VariesByCulture() && dto.VariationInfo != null && dto.VariationInfo.Count > 0) - { - //fixme: Currently require making a 2nd query to fill in publish status information for variants - - var variantInfo = new Dictionary(StringComparer.InvariantCultureIgnoreCase); - foreach (var info in dto.VariationInfo) - { - var isoCode = _langRepository.GetIsoCodeById(info.LanguageId); - if (isoCode != null) - variantInfo[isoCode] = info.Name; - - } - entity.CultureNames = variantInfo; - entity.Variations = dto.Variations; - entity.PublishedCultures = publishedCultures; - entity.EditedCultures = editedCultures; - } return entity; } From c7910500581a7a9fa5b314dbfe6b7b224ed09161 Mon Sep 17 00:00:00 2001 From: Stephan Date: Tue, 25 Sep 2018 16:55:00 +0200 Subject: [PATCH 08/10] Denormalize variants for perfs --- .../Migrations/Upgrade/UmbracoPlan.cs | 1 + .../Upgrade/V_8_0_0/RefactorVariantsModel.cs | 79 +++++++++++++++++++ src/Umbraco.Core/Models/Content.cs | 4 +- .../Dtos/ContentVersionCultureVariationDto.cs | 4 - .../Dtos/DocumentCultureVariationDto.cs | 18 +++++ .../Implement/DocumentRepository.cs | 16 ++-- .../Implement/EntityRepository.cs | 39 ++------- src/Umbraco.Core/Umbraco.Core.csproj | 1 + 8 files changed, 117 insertions(+), 45 deletions(-) create mode 100644 src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/RefactorVariantsModel.cs diff --git a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs index 09846ed6e2..0933c0cba7 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs @@ -118,6 +118,7 @@ namespace Umbraco.Core.Migrations.Upgrade Chain("{517CE9EA-36D7-472A-BF4B-A0D6FB1B8F89}"); // from 7.12.0 Chain("{BBD99901-1545-40E4-8A5A-D7A675C7D2F2}"); // from 7.12.0 Chain("{2C87AA47-D1BC-4ECB-8A73-2D8D1046C27F}"); + Chain("{B19BF0F2-E1C6-4AEB-A146-BC559D97A2C6}"); //FINAL diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/RefactorVariantsModel.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/RefactorVariantsModel.cs new file mode 100644 index 0000000000..aa498583ff --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/RefactorVariantsModel.cs @@ -0,0 +1,79 @@ +using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.Dtos; + +namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 +{ + public class RefactorVariantsModel : MigrationBase + { + public RefactorVariantsModel(IMigrationContext context) + : base(context) + { } + + public override void Migrate() + { + Delete.Column("edited").FromTable(Constants.DatabaseSchema.Tables.ContentVersionCultureVariation).Do(); + + + // add available column + AddColumn("available", out var sqls); + + // so far, only those cultures that were available had records in the table + Update.Table(DocumentCultureVariationDto.TableName).Set(new { available = true }).AllRows().Do(); + + foreach (var sql in sqls) Execute.Sql(sql).Do(); + + + // add published column + AddColumn("published", out sqls); + + // make it false by default + Update.Table(DocumentCultureVariationDto.TableName).Set(new { published = false }).AllRows().Do(); + + // now figure out whether these available cultures are published, too + var getPublished = Sql() + .Select(x => x.NodeId) + .AndSelect(x => x.LanguageId) + .From() + .InnerJoin().On((node, cv) => node.NodeId == cv.NodeId) + .InnerJoin().On((cv, dv) => cv.Id == dv.Id && dv.Published) + .InnerJoin().On((cv, ccv) => cv.Id == ccv.VersionId); + + foreach (var dto in Database.Fetch(getPublished)) + Database.Execute(Sql() + .Update(u => u.Set(x => x.Published, true)) + .Where(x => x.NodeId == dto.NodeId && x.LanguageId == dto.LanguageId)); + + foreach (var sql in sqls) Execute.Sql(sql).Do(); + + // so far, it was kinda impossible to make a culture unavailable again, + // so we *should* not have anything published but not available - ignore + + + // add name column + AddColumn("name"); + + // so far, every record in the table mapped to an available culture + var getNames = Sql() + .Select(x => x.NodeId) + .AndSelect(x => x.LanguageId, x => x.Name) + .From() + .InnerJoin().On((node, cv) => node.NodeId == cv.NodeId && cv.Current) + .InnerJoin().On((cv, ccv) => cv.Id == ccv.VersionId); + + foreach (var dto in Database.Fetch(getNames)) + Database.Execute(Sql() + .Update(u => u.Set(x => x.Name, dto.Name)) + .Where(x => x.NodeId == dto.NodeId && x.LanguageId == dto.LanguageId)); + } + + // ReSharper disable once ClassNeverInstantiated.Local + // ReSharper disable UnusedAutoPropertyAccessor.Local + private class TempDto + { + public int NodeId { get; set; } + public int LanguageId { get; set; } + public string Name { get; set; } + } + // ReSharper restore UnusedAutoPropertyAccessor.Local + } +} diff --git a/src/Umbraco.Core/Models/Content.cs b/src/Umbraco.Core/Models/Content.cs index dd379e02f8..238d87b186 100644 --- a/src/Umbraco.Core/Models/Content.cs +++ b/src/Umbraco.Core/Models/Content.cs @@ -269,7 +269,7 @@ namespace Umbraco.Core.Models if (_publishInfos == null) _publishInfos = new Dictionary(StringComparer.OrdinalIgnoreCase); - _publishInfos[culture] = (name, date); + _publishInfos[culture.ToLowerInvariant()] = (name, date); } private void ClearPublishInfos() @@ -294,7 +294,7 @@ namespace Umbraco.Core.Models throw new ArgumentNullOrEmptyException(nameof(culture)); if (_editedCultures == null) _editedCultures = new HashSet(StringComparer.OrdinalIgnoreCase); - _editedCultures.Add(culture); + _editedCultures.Add(culture.ToLowerInvariant()); } // sets all publish edited diff --git a/src/Umbraco.Core/Persistence/Dtos/ContentVersionCultureVariationDto.cs b/src/Umbraco.Core/Persistence/Dtos/ContentVersionCultureVariationDto.cs index 27107b0afc..cd6c66d5f0 100644 --- a/src/Umbraco.Core/Persistence/Dtos/ContentVersionCultureVariationDto.cs +++ b/src/Umbraco.Core/Persistence/Dtos/ContentVersionCultureVariationDto.cs @@ -41,9 +41,5 @@ namespace Umbraco.Core.Persistence.Dtos [ForeignKey(typeof(UserDto))] [NullSetting(NullSetting = NullSettings.Null)] public int? PublishedUserId { get => _publishedUserId == 0 ? null : _publishedUserId; set => _publishedUserId = value; } //return null if zero - - // fixme: I've commented this out, it's never used, need to review - //[Column("edited")] - //public bool Edited { get; set; } } } diff --git a/src/Umbraco.Core/Persistence/Dtos/DocumentCultureVariationDto.cs b/src/Umbraco.Core/Persistence/Dtos/DocumentCultureVariationDto.cs index 78e819e714..ed61ea5622 100644 --- a/src/Umbraco.Core/Persistence/Dtos/DocumentCultureVariationDto.cs +++ b/src/Umbraco.Core/Persistence/Dtos/DocumentCultureVariationDto.cs @@ -28,7 +28,25 @@ namespace Umbraco.Core.Persistence.Dtos [Ignore] public string Culture { get; set; } + // authority on whether a culture has been edited [Column("edited")] public bool Edited { get; set; } + + // de-normalized for perfs + // (means there is a current content version culture variation for the language) + [Column("available")] + public bool Available { get; set; } + + // de-normalized for perfs + // (means there is a published content version culture variation for the language) + [Column("published")] + public bool Published { get; set; } + + // de-normalized for perfs + // (when available, copies name from current content version culture variation for the language) + // (otherwise, it's the published one, 'cos we need to have one) + [Column("name")] + [NullSetting(NullSetting = NullSettings.Null)] + public string Name { get; set; } } } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs index 7ce4482fb4..033301a8e8 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs @@ -8,14 +8,12 @@ using Umbraco.Core.Exceptions; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; -using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Factories; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence.SqlSyntax; using Umbraco.Core.Scoping; using Umbraco.Core.Services; -using static Umbraco.Core.Persistence.NPocoSqlExtensions.Statics; namespace Umbraco.Core.Persistence.Repositories.Implement { @@ -1099,16 +1097,22 @@ namespace Umbraco.Core.Persistence.Repositories.Implement private IEnumerable GetDocumentVariationDtos(IContent content, bool publishing, HashSet editedCultures) { - foreach (var (culture, name) in content.CultureNames) + var allCultures = content.AvailableCultures.Union(content.PublishedCultures); // union = distinct + foreach (var culture in allCultures) yield return new DocumentCultureVariationDto { NodeId = content.Id, LanguageId = LanguageRepository.GetIdByIsoCode(culture) ?? throw new InvalidOperationException("Not a valid culture."), Culture = culture, - // if not published, always edited - // no need to check for availability: it *is* available since it is in content.CultureNames - Edited = !content.IsCulturePublished(culture) || (editedCultures != null && editedCultures.Contains(culture)) + Name = content.GetCultureName(culture) ?? content.GetPublishName(culture), + + // note: can't use IsCultureEdited at that point - hasn't been updated yet - see PersistUpdatedItem + + Available = content.IsCultureAvailable(culture), + Published = content.IsCulturePublished(culture), + Edited = content.IsCultureAvailable(culture) && + (!content.IsCulturePublished(culture) || (editedCultures != null && editedCultures.Contains(culture))) }; } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs index e6f1798f50..fb8c2732e6 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs @@ -313,17 +313,13 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected Sql GetVariantInfos(IEnumerable ids) { - // this ... is an interesting query - could we make it simpler? probably, by having DocumentCultureVariationDto - // handle 'available' and 'published' in addition to 'edited' - would take (a bit) more time to save a document, - // but would make querying way faster - return Sql() .Select(x => x.NodeId) .AndSelect(x => x.IsoCode) .AndSelect("doc", x => Alias(x.Published, "DocumentPublished"), x => Alias(x.Edited, "DocumentEdited")) - .AndSelect("ccv", x => Alias(x.Id, "CultureAvailableData"), x => Alias(x.Name, "Name")) - .AndSelect("pcv", x => Alias(x.Id, "CulturePublishedData")) - .AndSelect("dcv", x => Alias(x.Edited, "CultureEditedData")) + .AndSelect("dcv", + x => Alias(x.Available, "CultureAvailable"), x => Alias(x.Published, "CulturePublished"), x => Alias(x.Edited, "CultureEdited"), + x => Alias(x.Name, "Name")) // from node x language .From() @@ -337,22 +333,6 @@ namespace Umbraco.Core.Persistence.Repositories.Implement .LeftJoin("dcv") .On((node, dcv, lang) => node.NodeId == dcv.NodeId && lang.Id == dcv.LanguageId, aliasRight: "dcv") - // join to current version - always exists - // left-join to current version variations - indicates which cultures are *available* - .InnerJoin("cv") - .On((node, ev) => node.NodeId == ev.NodeId && ev.Current, aliasRight: "cv") - .LeftJoin("ccv") - .On((cv, ccv, lang) => cv.Id == ccv.VersionId && lang.Id == ccv.LanguageId, "cv", "ccv") - - // left-join to published version - exists when whole node is published - // left-join to published version variations - matches cultures that are *published* - .LeftJoin(nested => nested.InnerJoin("dv") - .On((pv, dv) => pv.Id == dv.Id && dv.Published, "pv", "dv"), - "pv") - .On((node, pv) => node.NodeId == pv.NodeId, aliasRight: "pv") - .LeftJoin("pcv") - .On((pv, pcv, lang) => pv.Id == pcv.VersionId && lang.Id == pcv.LanguageId, "pv", "pcv") - // for selected nodes .WhereIn(x => x.NodeId, ids); } @@ -566,16 +546,9 @@ namespace Umbraco.Core.Persistence.Repositories.Implement public bool DocumentPublished { get; set; } public bool DocumentEdited { get; set; } - public int? CultureAvailableData { get; set; } - public int? CulturePublishedData { get; set; } - public bool? CultureEditedData { get; set; } - - [ResultColumn] - public bool CultureAvailable => CultureAvailableData.HasValue; - [ResultColumn] - public bool CulturePublished => CulturePublishedData.HasValue; - [ResultColumn] - public bool CultureEdited => CultureEditedData ?? false; + public bool CultureAvailable { get; set; } + public bool CulturePublished { get; set; } + public bool CultureEdited { get; set; } } // ReSharper disable once ClassNeverInstantiated.Local diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 76ed089dec..d1303c37e5 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -359,6 +359,7 @@ + From 14bd8a0851ee9f45c0f6a2be199826318e73d380 Mon Sep 17 00:00:00 2001 From: Stephan Date: Tue, 25 Sep 2018 18:05:14 +0200 Subject: [PATCH 09/10] Cleanup some DTOs --- src/Umbraco.Core/Models/ContentBase.cs | 8 +++++++- src/Umbraco.Core/Models/IContentBase.cs | 4 ++-- .../Dtos/ContentVersionCultureVariationDto.cs | 11 +++++------ .../Persistence/Dtos/ContentVersionDto.cs | 4 ++-- src/Umbraco.Core/Persistence/Dtos/NodeDto.cs | 6 +++--- .../Repositories/Implement/DocumentRepository.cs | 15 ++++++++++++--- .../Models/Mapping/ContentMapperProfile.cs | 2 +- .../NuCache/PublishedSnapshotService.cs | 2 +- 8 files changed, 33 insertions(+), 19 deletions(-) diff --git a/src/Umbraco.Core/Models/ContentBase.cs b/src/Umbraco.Core/Models/ContentBase.cs index 56a31cd76d..bf2fd580d9 100644 --- a/src/Umbraco.Core/Models/ContentBase.cs +++ b/src/Umbraco.Core/Models/ContentBase.cs @@ -167,7 +167,7 @@ namespace Umbraco.Core.Models } /// - public DateTime? GetCultureDate(string culture) + public DateTime? GetUpdateDate(string culture) { if (culture.IsNullOrWhiteSpace()) return null; if (!ContentTypeBase.VariesByCulture()) return null; @@ -202,6 +202,12 @@ namespace Umbraco.Core.Models } } + internal void TouchCulture(string culture) + { + if (ContentTypeBase.VariesByCulture() && _cultureInfos != null && _cultureInfos.TryGetValue(culture, out var infos)) + _cultureInfos[culture] = (infos.Name, DateTime.Now); + } + protected void ClearCultureInfos() { _cultureInfos = null; diff --git a/src/Umbraco.Core/Models/IContentBase.cs b/src/Umbraco.Core/Models/IContentBase.cs index de1b2666d5..460bd521d4 100644 --- a/src/Umbraco.Core/Models/IContentBase.cs +++ b/src/Umbraco.Core/Models/IContentBase.cs @@ -80,13 +80,13 @@ namespace Umbraco.Core.Models bool IsCultureAvailable(string culture); /// - /// Gets the date a culture was created. + /// Gets the date a culture was updated. /// /// /// When is null, returns null. /// If the specified culture is not available, returns null. /// - DateTime? GetCultureDate(string culture); + DateTime? GetUpdateDate(string culture); /// /// List of properties, which make up all the data available for this Content object diff --git a/src/Umbraco.Core/Persistence/Dtos/ContentVersionCultureVariationDto.cs b/src/Umbraco.Core/Persistence/Dtos/ContentVersionCultureVariationDto.cs index cd6c66d5f0..1dca820a6c 100644 --- a/src/Umbraco.Core/Persistence/Dtos/ContentVersionCultureVariationDto.cs +++ b/src/Umbraco.Core/Persistence/Dtos/ContentVersionCultureVariationDto.cs @@ -10,7 +10,7 @@ namespace Umbraco.Core.Persistence.Dtos internal class ContentVersionCultureVariationDto { public const string TableName = Constants.DatabaseSchema.Tables.ContentVersionCultureVariation; - private int? _publishedUserId; + private int? _updateUserId; [Column("id")] [PrimaryKeyColumn] @@ -33,13 +33,12 @@ namespace Umbraco.Core.Persistence.Dtos [Column("name")] public string Name { get; set; } - [Column("date")] - public DateTime Date { get; set; } + [Column("date")] // fixme: db rename to 'updateDate' + public DateTime UpdateDate { get; set; } - // fixme want? - [Column("availableUserId")] + [Column("availableUserId")] // fixme: db rename to 'updateDate' [ForeignKey(typeof(UserDto))] [NullSetting(NullSetting = NullSettings.Null)] - public int? PublishedUserId { get => _publishedUserId == 0 ? null : _publishedUserId; set => _publishedUserId = value; } //return null if zero + public int? UpdateUserId { get => _updateUserId == 0 ? null : _updateUserId; set => _updateUserId = value; } //return null if zero } } diff --git a/src/Umbraco.Core/Persistence/Dtos/ContentVersionDto.cs b/src/Umbraco.Core/Persistence/Dtos/ContentVersionDto.cs index 3ec40c74b3..a13bf921d9 100644 --- a/src/Umbraco.Core/Persistence/Dtos/ContentVersionDto.cs +++ b/src/Umbraco.Core/Persistence/Dtos/ContentVersionDto.cs @@ -21,11 +21,11 @@ namespace Umbraco.Core.Persistence.Dtos [ForeignKey(typeof(ContentDto))] public int NodeId { get; set; } - [Column("versionDate")] + [Column("versionDate")] // fixme: db rename to 'updateDate' [Constraint(Default = SystemMethods.CurrentDateTime)] public DateTime VersionDate { get; set; } - [Column("userId")] + [Column("userId")] // fixme: db rename to 'updateUserId' [ForeignKey(typeof(UserDto))] [NullSetting(NullSetting = NullSettings.Null)] public int? UserId { get => _userId == 0 ? null : _userId; set => _userId = value; } //return null if zero diff --git a/src/Umbraco.Core/Persistence/Dtos/NodeDto.cs b/src/Umbraco.Core/Persistence/Dtos/NodeDto.cs index 10450f2bf4..56da821360 100644 --- a/src/Umbraco.Core/Persistence/Dtos/NodeDto.cs +++ b/src/Umbraco.Core/Persistence/Dtos/NodeDto.cs @@ -45,7 +45,7 @@ namespace Umbraco.Core.Persistence.Dtos [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_Trashed")] public bool Trashed { get; set; } - [Column("nodeUser")] // fixme dbfix rename userId + [Column("nodeUser")] // fixme: db rename to 'createUserId' [ForeignKey(typeof(UserDto))] [NullSetting(NullSetting = NullSettings.Null)] public int? UserId { get => _userId == 0 ? null : _userId; set => _userId = value; } //return null if zero @@ -54,10 +54,10 @@ namespace Umbraco.Core.Persistence.Dtos [NullSetting(NullSetting = NullSettings.Null)] public string Text { get; set; } - [Column("nodeObjectType")] // fixme dbfix rename objectType + [Column("nodeObjectType")] // fixme: db rename to 'objectType' [NullSetting(NullSetting = NullSettings.Null)] [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_ObjectType")] - public Guid? NodeObjectType { get; set; } // fixme dbfix rename ObjectType + public Guid? NodeObjectType { get; set; } [Column("createDate")] [Constraint(Default = SystemMethods.CurrentDateTime)] diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs index 033301a8e8..fb1a33eb62 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs @@ -503,8 +503,17 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // names also impact 'edited' foreach (var (culture, name) in content.CultureNames) if (name != content.GetPublishName(culture)) + { + edited = true; (editedCultures ?? (editedCultures = new HashSet(StringComparer.OrdinalIgnoreCase))).Add(culture); + // fixme - change tracking + // at the moment, we don't do any dirty tracking on property values, so we don't know whether the + // culture has just been edited or not, so we don't update its update date - that date only changes + // when the name is set, and it all works because the controller does it - but, if someone uses a + // service to change a property value and save (without setting name), the update date does not change. + } + // replace the content version variations (rather than updating) // only need to delete for the version that existed, the new version (if any) has no property data yet var deleteContentVariations = Sql().Delete().Where(x => x.VersionId == versionToDelete); @@ -1031,7 +1040,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement { Culture = LanguageRepository.GetIsoCodeById(dto.LanguageId), Name = dto.Name, - Date = dto.Date + Date = dto.UpdateDate }); } @@ -1076,7 +1085,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement LanguageId = LanguageRepository.GetIdByIsoCode(culture) ?? throw new InvalidOperationException("Not a valid culture."), Culture = culture, Name = name, - Date = content.GetCultureDate(culture) ?? DateTime.MinValue // we *know* there is a value + UpdateDate = content.GetUpdateDate(culture) ?? DateTime.MinValue // we *know* there is a value }; // if not publishing, we're just updating the 'current' (non-published) version, @@ -1091,7 +1100,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement LanguageId = LanguageRepository.GetIdByIsoCode(culture) ?? throw new InvalidOperationException("Not a valid culture."), Culture = culture, Name = name, - Date = content.GetPublishDate(culture) ?? DateTime.MinValue // we *know* there is a value + UpdateDate = content.GetPublishDate(culture) ?? DateTime.MinValue // we *know* there is a value }; } diff --git a/src/Umbraco.Web/Models/Mapping/ContentMapperProfile.cs b/src/Umbraco.Web/Models/Mapping/ContentMapperProfile.cs index d4d570f6af..8b23763763 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentMapperProfile.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentMapperProfile.cs @@ -108,7 +108,7 @@ namespace Umbraco.Web.Models.Mapping // if we don't have a date for a culture, it means the culture is not available, and // hey we should probably not be mapping it, but it's too late, return a fallback date - var date = source.GetCultureDate(culture); + var date = source.GetUpdateDate(culture); return date ?? source.UpdateDate; } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs index d5d90b8479..8786753e4f 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs @@ -1196,7 +1196,7 @@ namespace Umbraco.Web.PublishedCache.NuCache foreach (var (culture, name) in names) { - cultureData[culture] = new CultureVariation { Name = name, Date = content.GetCultureDate(culture) ?? DateTime.MinValue }; + cultureData[culture] = new CultureVariation { Name = name, Date = content.GetUpdateDate(culture) ?? DateTime.MinValue }; } //the dictionary that will be serialized From 83501ba1eb16d22f5063dddc5e8416caf98740be Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 27 Sep 2018 19:30:31 +1000 Subject: [PATCH 10/10] Gets tree node states showing correctly for variants. --- src/Umbraco.Web/Models/Trees/TreeNodeExtensions.cs | 2 +- src/Umbraco.Web/Trees/ContentTreeController.cs | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web/Models/Trees/TreeNodeExtensions.cs b/src/Umbraco.Web/Models/Trees/TreeNodeExtensions.cs index 73329276af..9f8ca9b84d 100644 --- a/src/Umbraco.Web/Models/Trees/TreeNodeExtensions.cs +++ b/src/Umbraco.Web/Models/Trees/TreeNodeExtensions.cs @@ -54,7 +54,7 @@ /// Sets the node style to show that it is has unpublished versions (but is currently published) /// /// - public static void SetHasUnpublishedVersionStyle(this TreeNode treeNode) + public static void SetHasPendingVersionStyle(this TreeNode treeNode) { if (treeNode.CssClasses.Contains("has-unpublished-version") == false) { diff --git a/src/Umbraco.Web/Trees/ContentTreeController.cs b/src/Umbraco.Web/Trees/ContentTreeController.cs index 878a277177..280f36dacb 100644 --- a/src/Umbraco.Web/Trees/ContentTreeController.cs +++ b/src/Umbraco.Web/Trees/ContentTreeController.cs @@ -78,8 +78,8 @@ namespace Umbraco.Web.Trees { if (!documentEntity.Published) node.SetNotPublishedStyle(); - if (documentEntity.Edited) - node.SetHasUnpublishedVersionStyle(); + else if (documentEntity.Edited) + node.SetHasPendingVersionStyle(); } else { @@ -87,6 +87,8 @@ namespace Umbraco.Web.Trees { if (!documentEntity.PublishedCultures.Contains(culture)) node.SetNotPublishedStyle(); + else if (documentEntity.EditedCultures.Contains(culture)) + node.SetHasPendingVersionStyle(); } }