From 58e4e2398a827c3712907844e327e79896c92246 Mon Sep 17 00:00:00 2001 From: Stephan Date: Thu, 12 Apr 2018 22:53:04 +0200 Subject: [PATCH] Variant names, availability, etc --- .../Install/DatabaseSchemaCreator.cs | 3 +- .../Migrations/Upgrade/UmbracoPlan.cs | 8 +- .../V_8_0_0/AddContentVariationTable.cs | 18 +++ src/Umbraco.Core/Models/Content.cs | 124 +++++++++++++---- src/Umbraco.Core/Models/ContentBase.cs | 54 ++++++-- src/Umbraco.Core/Models/ContentTypeBase.cs | 7 +- src/Umbraco.Core/Models/IContent.cs | 37 ++++- src/Umbraco.Core/Models/IContentBase.cs | 34 ++++- src/Umbraco.Core/Models/PropertyType.cs | 7 +- .../Persistence/Constants-DatabaseSchema.cs | 1 + .../Dtos/ContentVersionCultureVariationDto.cs | 48 +++++++ .../Factories/ContentBaseFactory.cs | 1 - .../Persistence/Mappers/ContentMapper.cs | 2 - .../Implement/DocumentBlueprintRepository.cs | 4 +- .../Implement/DocumentRepository.cs | 128 +++++++++++++++++- .../Implement/LanguageRepository.cs | 13 +- src/Umbraco.Core/Umbraco.Core.csproj | 2 + src/Umbraco.Tests/Models/VariationTests.cs | 29 ++++ .../Repositories/ContentRepositoryTest.cs | 5 +- .../Repositories/ContentTypeRepositoryTest.cs | 10 +- .../Repositories/DomainRepositoryTest.cs | 2 +- .../PublicAccessRepositoryTest.cs | 3 +- .../Repositories/TagRepositoryTest.cs | 3 +- .../Repositories/TemplateRepositoryTest.cs | 3 +- .../Repositories/UserRepositoryTest.cs | 5 +- .../Services/ContentServicePerformanceTest.cs | 20 +-- .../Services/ContentServiceTests.cs | 67 ++++++++- .../Models/Mapping/VariationResolver.cs | 4 +- 28 files changed, 555 insertions(+), 87 deletions(-) create mode 100644 src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/AddContentVariationTable.cs create mode 100644 src/Umbraco.Core/Persistence/Dtos/ContentVersionCultureVariationDto.cs diff --git a/src/Umbraco.Core/Migrations/Install/DatabaseSchemaCreator.cs b/src/Umbraco.Core/Migrations/Install/DatabaseSchemaCreator.cs index cc9c0f3893..6ce300845c 100644 --- a/src/Umbraco.Core/Migrations/Install/DatabaseSchemaCreator.cs +++ b/src/Umbraco.Core/Migrations/Install/DatabaseSchemaCreator.cs @@ -81,7 +81,8 @@ namespace Umbraco.Core.Migrations.Install typeof (KeyValueDto), typeof (UserLoginDto), typeof (ConsentDto), - typeof (AuditEntryDto) + typeof (AuditEntryDto), + typeof (ContentVersionCultureVariationDto) }; /// diff --git a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs index a23be132fd..8bec74173e 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs @@ -116,9 +116,10 @@ namespace Umbraco.Core.Migrations.Upgrade Chain("{9DF05B77-11D1-475C-A00A-B656AF7E0908}"); Chain("{6FE3EF34-44A0-4992-B379-B40BC4EF1C4D}"); Chain("{7F59355A-0EC9-4438-8157-EB517E6D2727}"); + Chain("{66B6821A-0DE3-4DF8-A6A4-65ABD211EDDE}"); // must chain to v8 final state (see at end of file) - Chain("{79591E91-01EA-43F7-AC58-7BD286DB1E77}"); + Chain("{941B2ABA-2D06-4E04-81F5-74224F1DB037}"); // UPGRADE FROM 7, MORE RECENT @@ -199,10 +200,13 @@ namespace Umbraco.Core.Migrations.Upgrade // mergin from 7.10.0 Chain("{79591E91-01EA-43F7-AC58-7BD286DB1E77}"); + // 8.0.0 + Chain("{941B2ABA-2D06-4E04-81F5-74224F1DB037}"); + // FINAL STATE - MUST MATCH LAST ONE ABOVE ! // whenever this changes, update all references in this file! - Add(string.Empty, "{79591E91-01EA-43F7-AC58-7BD286DB1E77}"); + Add(string.Empty, "{941B2ABA-2D06-4E04-81F5-74224F1DB037}"); } } } diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/AddContentVariationTable.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/AddContentVariationTable.cs new file mode 100644 index 0000000000..ee0a4d7c8a --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/AddContentVariationTable.cs @@ -0,0 +1,18 @@ +using Umbraco.Core.Persistence.Dtos; + +namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 +{ + public class AddContentVariationTable : MigrationBase + { + public AddContentVariationTable(IMigrationContext context) + : base(context) + { } + + public override void Migrate() + { + Create.Table(); + + // fixme - data migration? + } + } +} diff --git a/src/Umbraco.Core/Models/Content.cs b/src/Umbraco.Core/Models/Content.cs index f468d8be90..00e5258953 100644 --- a/src/Umbraco.Core/Models/Content.cs +++ b/src/Umbraco.Core/Models/Content.cs @@ -1,8 +1,9 @@ using System; -using System.ComponentModel; +using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Runtime.Serialization; +using Umbraco.Core.Composing; namespace Umbraco.Core.Models { @@ -19,8 +20,10 @@ namespace Umbraco.Core.Models private PublishedState _publishedState; private DateTime? _releaseDate; private DateTime? _expireDate; - private string _nodeName;//NOTE Once localization is introduced this will be the non-localized Node Name. + private HashSet _cultures; + private Dictionary _publishNames; + private static readonly Dictionary NoPublishNames = new Dictionary(); private static readonly Lazy Ps = new Lazy(); /// @@ -80,7 +83,6 @@ namespace Umbraco.Core.Models public readonly PropertyInfo PublishedSelector = ExpressionHelper.GetPropertyInfo(x => x.Published); public readonly PropertyInfo ReleaseDateSelector = ExpressionHelper.GetPropertyInfo(x => x.ReleaseDate); public readonly PropertyInfo ExpireDateSelector = ExpressionHelper.GetPropertyInfo(x => x.ExpireDate); - public readonly PropertyInfo NodeNameSelector = ExpressionHelper.GetPropertyInfo(x => x.NodeName); } /// @@ -161,7 +163,6 @@ namespace Umbraco.Core.Models [IgnoreDataMember] public bool Edited { get; internal set; } - /// /// The date this Content should be released and thus be published /// @@ -182,19 +183,6 @@ namespace Umbraco.Core.Models set => SetPropertyValueAndDetectChanges(value, ref _expireDate, Ps.Value.ExpireDateSelector); } - /// - /// Name of the Node (non-localized). - /// - /// - /// This Property is kept internal until localization is introduced. - /// - [DataMember] - internal string NodeName - { - get => _nodeName; - set => SetPropertyValueAndDetectChanges(value, ref _nodeName, Ps.Value.NodeNameSelector); - } - /// /// Gets the ContentType used by this content object /// @@ -209,10 +197,81 @@ namespace Umbraco.Core.Models [IgnoreDataMember] public ITemplate PublishTemplate { get; internal set; } - + [IgnoreDataMember] public string PublishName { get; internal set; } + /// + [IgnoreDataMember] + public IReadOnlyDictionary PublishNames => _publishNames ?? NoPublishNames; + + /// + public string GetPublishName(string languageId) + { + if (languageId == null) return PublishName; + if (_publishNames == null) return null; + return _publishNames.TryGetValue(languageId, out var name) ? name : null; + } + + // sets a publish name + // internal for repositories + internal void SetPublishName(string languageId, string name) + { + if (languageId == null) + { + PublishName = name; + return; + } + + // private method, assume that culture is valid + + if (_publishNames == null) + _publishNames = new Dictionary(); + + _publishNames[languageId] = name; + } + + // clears a publish name + private void ClearPublishName(string languageId) + { + if (languageId == null) + { + PublishName = null; + return; + } + + _publishNames.Remove(languageId); + if (_publishNames.Count == 0) + _publishNames = null; + } + + /// + public bool IsCultureAvailable(string languageId) + { + return _cultures != null && _cultures.Contains(languageId); + } + + // fixme but what's setting it? + // fixme a culture is available when published = when setting the publish name (merge?) + // internal for repositories + internal void SetCultureAvailability(string languageId, bool available) + { + if (available) + { + if (_cultures == null) _cultures = new HashSet(StringComparer.OrdinalIgnoreCase); + + // private method, assume that culture is valid + _cultures.Add(languageId); + } + else + { + if (_cultures == null) return; + + _cultures.Remove(languageId); + if (_cultures.Count == 0) _cultures = null; + } + } + [IgnoreDataMember] public int PublishedVersionId { get; internal set; } @@ -233,12 +292,24 @@ namespace Umbraco.Core.Models /// public virtual bool PublishValues(int? languageId = null, string segment = null) - { - if (Validate(languageId, segment).Any()) - return false; + { + // fixme MUST refactor it all here! + // the variation should be supported by the content type + + //ContentType.ValidateVariation(languageId, segment, throwIfInvalid: true); - foreach (var property in Properties) + //if (Validate(languageId, segment).Any()) + // return false; + + foreach (var property in Properties.Where(x => x.PropertyType.ValidateVariation(languageId, segment, throwIfInvalid: false))) property.PublishValue(languageId, segment); + + // Name and PublishName are managed in the repository + // but Names and PublishNames must be managed here, + // as they depend on the variation + var languageIsoCode = Current.Services.LocalizationService.GetLanguageById(languageId.Value).IsoCode; + SetPublishName(languageIsoCode, GetName(languageIsoCode)); + _publishedState = PublishedState.Publishing; return true; } @@ -266,8 +337,15 @@ namespace Umbraco.Core.Models /// public virtual void ClearPublishedValues(int? languageId = null, string segment = null) { - foreach (var property in Properties) + foreach (var property in Properties.Where(x => x.PropertyType.ValidateVariation(languageId, segment, throwIfInvalid: false))) property.ClearPublishedValue(languageId, segment); + + // Name and PublishName are managed in the repository + // but Names and PublishNames must be managed here, + // as they depend on the variation + var languageIsoCode = Current.Services.LocalizationService.GetLanguageById(languageId.Value).IsoCode; + ClearPublishName(languageIsoCode); + _publishedState = PublishedState.Publishing; } diff --git a/src/Umbraco.Core/Models/ContentBase.cs b/src/Umbraco.Core/Models/ContentBase.cs index a1df720a83..dc9b76dc9c 100644 --- a/src/Umbraco.Core/Models/ContentBase.cs +++ b/src/Umbraco.Core/Models/ContentBase.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Collections.Specialized; -using System.ComponentModel; using System.Diagnostics; using System.Linq; using System.Reflection; @@ -24,12 +23,8 @@ namespace Umbraco.Core.Models private int _contentTypeId; protected IContentTypeComposition ContentTypeBase; private int _writerId; - - // fixme need to deal with localized names, how? - // for the time being, this is the node text = unique - private string _name; - private PropertyCollection _properties; + private Dictionary _names; /// /// Initializes a new instance of the class. @@ -59,7 +54,7 @@ namespace Umbraco.Core.Models Id = 0; // no identity VersionId = 0; // no versions - Name = _name = name; + Name = name; _contentTypeId = contentType.Id; _properties = properties ?? throw new ArgumentNullException(nameof(properties)); _properties.EnsurePropertyTypes(PropertyTypes); @@ -70,7 +65,9 @@ namespace Umbraco.Core.Models { public readonly PropertyInfo DefaultContentTypeIdSelector = ExpressionHelper.GetPropertyInfo(x => x.ContentTypeId); public readonly PropertyInfo PropertyCollectionSelector = ExpressionHelper.GetPropertyInfo(x => x.Properties); - public readonly PropertyInfo WriterSelector = ExpressionHelper.GetPropertyInfo(x => x.WriterId); + public readonly PropertyInfo WriterSelector = ExpressionHelper.GetPropertyInfo(x => x.WriterId); + // fixme how can that work / comparison? + public readonly PropertyInfo NamesSelector = ExpressionHelper.GetPropertyInfo>(x => x.Names); } protected void PropertiesChanged(object sender, NotifyCollectionChangedEventArgs e) @@ -124,6 +121,47 @@ namespace Umbraco.Core.Models } } + /// + [DataMember] + public virtual IReadOnlyDictionary Names + { + get => _names; + set + { + foreach (var (languageId, name) in value) + SetName(languageId, name); + } + } + + /// + public virtual void SetName(string languageId, string name) + { + if (languageId == null) + { + Name = name; + return; + } + + if ((ContentTypeBase.Variations & (ContentVariation.CultureNeutral | ContentVariation.CultureSegment)) == 0) + throw new NotSupportedException("Content type does not support varying name by culture."); + + // fixme validate language? + + if (_names == null) + _names = new Dictionary(); + + _names[languageId] = name; + OnPropertyChanged(Ps.Value.NamesSelector); + } + + /// + public virtual string GetName(string languageId) + { + if (languageId == null) return Name; + if (_names == null) return null; + return _names.TryGetValue(languageId, out var name) ? name : null; + } + /// /// Gets the enumeration of property groups for the entity. /// fixme is a proxy, kill this diff --git a/src/Umbraco.Core/Models/ContentTypeBase.cs b/src/Umbraco.Core/Models/ContentTypeBase.cs index 350ae7be1f..ec7eddbaf6 100644 --- a/src/Umbraco.Core/Models/ContentTypeBase.cs +++ b/src/Umbraco.Core/Models/ContentTypeBase.cs @@ -19,9 +19,6 @@ namespace Umbraco.Core.Models [DebuggerDisplay("Id: {Id}, Name: {Name}, Alias: {Alias}")] public abstract class ContentTypeBase : TreeEntityBase, IContentTypeBase { - //fixme this should be invariant by default but for demo purposes and until the UI is updated to support changing a property type we'll make this neutral by default - private const ContentVariation DefaultVaryBy = ContentVariation.CultureNeutral; - private static readonly Lazy Ps = new Lazy(); private string _alias; @@ -49,7 +46,7 @@ namespace Umbraco.Core.Models _propertyTypes = new PropertyTypeCollection(IsPublishing); _propertyTypes.CollectionChanged += PropertyTypesChanged; - _variations = DefaultVaryBy; + _variations = ContentVariation.InvariantNeutral; } protected ContentTypeBase(IContentTypeBase parent) @@ -70,7 +67,7 @@ namespace Umbraco.Core.Models _propertyTypes = new PropertyTypeCollection(IsPublishing); _propertyTypes.CollectionChanged += PropertyTypesChanged; - _variations = DefaultVaryBy; + _variations = ContentVariation.InvariantNeutral; } /// diff --git a/src/Umbraco.Core/Models/IContent.cs b/src/Umbraco.Core/Models/IContent.cs index ff270458bb..6f2fba0da9 100644 --- a/src/Umbraco.Core/Models/IContent.cs +++ b/src/Umbraco.Core/Models/IContent.cs @@ -1,11 +1,14 @@ using System; -using System.ComponentModel; +using System.Collections.Generic; namespace Umbraco.Core.Models { /// - /// Defines a Content object + /// Represents a document. /// + /// + /// A document can be published, rendered by a template. + /// public interface IContent : IContentBase { /// @@ -77,6 +80,36 @@ namespace Umbraco.Core.Models /// ContentStatus Status { get; } + /// + /// Gets a value indicating whether a given culture is available. + /// + /// + /// A culture becomes available whenever values for this culture are published, + /// and it becomes unavailable whenever values for this culture are unpublished. + /// fixme - what's setting this? + /// + bool IsCultureAvailable(string languageId); + + /// + /// Gets the name of the published version of the content for a given culture. + /// + /// + /// When editing the content, the name can change, but this will not until the content is published. + /// When is null, gets the invariant + /// language, which is the value of the property. + /// fixme - what's setting this? + /// + string GetPublishName(string languageId); + + /// + /// Gets the published names of the content. + /// + /// + /// Because a dictionary key cannot be null this cannot get the invariant + /// name, which must be get via the property. + /// + IReadOnlyDictionary PublishNames { get; } + // fixme - these two should move to some kind of service /// diff --git a/src/Umbraco.Core/Models/IContentBase.cs b/src/Umbraco.Core/Models/IContentBase.cs index 76df5b2f6f..1ef94a2d89 100644 --- a/src/Umbraco.Core/Models/IContentBase.cs +++ b/src/Umbraco.Core/Models/IContentBase.cs @@ -4,9 +4,12 @@ using Umbraco.Core.Models.Entities; namespace Umbraco.Core.Models { /// - /// Defines the base for a Content object with properties that - /// are shared between Content and Media. + /// Provides a base class for content items. /// + /// + /// Content items are documents, medias and members. + /// Content items have a content type, and properties. + /// public interface IContentBase : IUmbracoEntity { /// @@ -23,6 +26,33 @@ namespace Umbraco.Core.Models /// Gets the version identifier. /// int VersionId { get; } + + /// + /// Sets the name of the content item for a specified language. + /// + /// + /// When is null, sets the invariant + /// language, which sets the property. + /// + void SetName(string languageId, string value); + + /// + /// Gets the name of the content item for a specified language. + /// + /// + /// When is null, gets the invariant + /// language, which is the value of the property. + /// + string GetName(string languageId); + + /// + /// Gets or sets the names of the content item. + /// + /// + /// Because a dictionary key cannot be null this cannot get nor set the invariant + /// name, which must be get or set via the property. + /// + IReadOnlyDictionary Names { get; set; } /// /// List of properties, which make up all the data available for this Content object diff --git a/src/Umbraco.Core/Models/PropertyType.cs b/src/Umbraco.Core/Models/PropertyType.cs index 0ab30eabf6..8c5e318719 100644 --- a/src/Umbraco.Core/Models/PropertyType.cs +++ b/src/Umbraco.Core/Models/PropertyType.cs @@ -20,9 +20,6 @@ namespace Umbraco.Core.Models [DebuggerDisplay("Id: {Id}, Name: {Name}, Alias: {Alias}")] public class PropertyType : EntityBase, IEquatable { - //fixme this should be invariant by default but for demo purposes and until the UI is updated to support changing a property type we'll make this neutral by default - private const ContentVariation DefaultVaryBy = ContentVariation.CultureNeutral; - private static PropertySelectors _selectors; private readonly bool _forceValueStorageType; @@ -50,7 +47,7 @@ namespace Umbraco.Core.Models _propertyEditorAlias = dataType.EditorAlias; _valueStorageType = dataType.DatabaseType; - _variations = DefaultVaryBy; + _variations = ContentVariation.InvariantNeutral; } /// @@ -87,7 +84,7 @@ namespace Umbraco.Core.Models _valueStorageType = valueStorageType; _forceValueStorageType = forceValueStorageType; _alias = propertyTypeAlias == null ? null : SanitizeAlias(propertyTypeAlias); - _variations = DefaultVaryBy; + _variations = ContentVariation.InvariantNeutral; } private static PropertySelectors Selectors => _selectors ?? (_selectors = new PropertySelectors()); diff --git a/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs b/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs index a245f33dd8..68afd4f244 100644 --- a/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs +++ b/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs @@ -26,6 +26,7 @@ namespace Umbraco.Core public const string Content = TableNamePrefix + "Content"; public const string ContentVersion = TableNamePrefix + "ContentVersion"; + public const string ContentVersionCultureVariation = TableNamePrefix + "ContentVersionCultureVariation"; public const string Document = TableNamePrefix + "Document"; public const string DocumentVersion = TableNamePrefix + "DocumentVersion"; public const string MediaVersion = TableNamePrefix + "MediaVersion"; diff --git a/src/Umbraco.Core/Persistence/Dtos/ContentVersionCultureVariationDto.cs b/src/Umbraco.Core/Persistence/Dtos/ContentVersionCultureVariationDto.cs new file mode 100644 index 0000000000..524d402ecc --- /dev/null +++ b/src/Umbraco.Core/Persistence/Dtos/ContentVersionCultureVariationDto.cs @@ -0,0 +1,48 @@ +using System; +using NPoco; +using Umbraco.Core.Persistence.DatabaseAnnotations; + +namespace Umbraco.Core.Persistence.Dtos +{ + [TableName(TableName)] + [PrimaryKey("id")] + [ExplicitColumns] + internal class ContentVersionCultureVariationDto + { + private const string TableName = Constants.DatabaseSchema.Tables.ContentVersionCultureVariation; + + [Column("id")] + [PrimaryKeyColumn] + public int Id { get; set; } + + [Column("versionId")] + [ForeignKey(typeof(ContentVersionDto))] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_" + TableName + "_VersionId", ForColumns = "versionId,languageId")] + public int VersionId { get; set; } + + [Column("languageId")] + [ForeignKey(typeof(LanguageDto))] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_LanguageId")] + [NullSetting(NullSetting = NullSettings.Null)] + public int? LanguageId { get; set; } + + [Column("name")] + [NullSetting(NullSetting = NullSettings.Null)] + public string Name { get; set; } + + [Column("available")] + public bool Available { get; set; } + + [Column("availableDate")] + [NullSetting(NullSetting = NullSettings.Null)] + public DateTime? AvailableDate { get; set; } + + [Column("availableUserId")] + // [ForeignKey(typeof(UserDto))] -- there is no foreign key so we can delete users without deleting associated content + //[NullSetting(NullSetting = NullSettings.Null)] + public int AvailableUserId { get; set; } + + [Column("edited")] + public bool Edited { get; set; } + } +} diff --git a/src/Umbraco.Core/Persistence/Factories/ContentBaseFactory.cs b/src/Umbraco.Core/Persistence/Factories/ContentBaseFactory.cs index b1a0a35432..aa17facf8f 100644 --- a/src/Umbraco.Core/Persistence/Factories/ContentBaseFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/ContentBaseFactory.cs @@ -31,7 +31,6 @@ namespace Umbraco.Core.Persistence.Factories content.VersionId = contentVersionDto.Id; content.Name = contentVersionDto.Text; - content.NodeName = contentVersionDto.Text; content.Path = nodeDto.Path; content.Level = nodeDto.Level; diff --git a/src/Umbraco.Core/Persistence/Mappers/ContentMapper.cs b/src/Umbraco.Core/Persistence/Mappers/ContentMapper.cs index 5c89faf80a..5bbdc07e73 100644 --- a/src/Umbraco.Core/Persistence/Mappers/ContentMapper.cs +++ b/src/Umbraco.Core/Persistence/Mappers/ContentMapper.cs @@ -24,8 +24,6 @@ namespace Umbraco.Core.Persistence.Mappers CacheMap(src => src.Key, dto => dto.UniqueId); CacheMap(src => src.VersionId, dto => dto.Id); - - CacheMap(src => src.NodeName, dto => dto.Text); CacheMap(src => src.Name, dto => dto.Text); CacheMap(src => src.ParentId, dto => dto.ParentId); diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentBlueprintRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentBlueprintRepository.cs index 3a7f24cbe2..1d24cfd2dc 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentBlueprintRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentBlueprintRepository.cs @@ -17,8 +17,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement /// internal class DocumentBlueprintRepository : DocumentRepository, IDocumentBlueprintRepository { - public DocumentBlueprintRepository(IScopeAccessor scopeAccessor, CacheHelper cacheHelper, ILogger logger, IContentTypeRepository contentTypeRepository, ITemplateRepository templateRepository, ITagRepository tagRepository, IContentSection settings) - : base(scopeAccessor, cacheHelper, logger, contentTypeRepository, templateRepository, tagRepository, settings) + public DocumentBlueprintRepository(IScopeAccessor scopeAccessor, CacheHelper cacheHelper, ILogger logger, IContentTypeRepository contentTypeRepository, ITemplateRepository templateRepository, ITagRepository tagRepository, ILanguageRepository languageRepository, IContentSection settings) + : base(scopeAccessor, cacheHelper, logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, settings) { EnsureUniqueNaming = false; // duplicates are allowed } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs index 084e8b6f59..fc0653d2db 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs @@ -25,17 +25,19 @@ namespace Umbraco.Core.Persistence.Repositories.Implement private readonly IContentTypeRepository _contentTypeRepository; private readonly ITemplateRepository _templateRepository; private readonly ITagRepository _tagRepository; + private readonly ILanguageRepository _languageRepository; private readonly CacheHelper _cacheHelper; private PermissionRepository _permissionRepository; private readonly ContentByGuidReadRepository _contentByGuidReadRepository; private readonly IScopeAccessor _scopeAccessor; - public DocumentRepository(IScopeAccessor scopeAccessor, CacheHelper cacheHelper, ILogger logger, IContentTypeRepository contentTypeRepository, ITemplateRepository templateRepository, ITagRepository tagRepository, IContentSection settings) + public DocumentRepository(IScopeAccessor scopeAccessor, CacheHelper cacheHelper, ILogger logger, IContentTypeRepository contentTypeRepository, ITemplateRepository templateRepository, ITagRepository tagRepository, ILanguageRepository languageRepository, IContentSection settings) : base(scopeAccessor, cacheHelper, logger) { _contentTypeRepository = contentTypeRepository ?? throw new ArgumentNullException(nameof(contentTypeRepository)); _templateRepository = templateRepository ?? throw new ArgumentNullException(nameof(templateRepository)); _tagRepository = tagRepository ?? throw new ArgumentNullException(nameof(tagRepository)); + _languageRepository = languageRepository; _cacheHelper = cacheHelper; _scopeAccessor = scopeAccessor; _contentByGuidReadRepository = new ContentByGuidReadRepository(this, scopeAccessor, cacheHelper, logger); @@ -326,11 +328,19 @@ namespace Umbraco.Core.Persistence.Repositories.Implement content.Edited = dto.Edited = !dto.Published || edited; // if not published, always edited Database.Insert(dto); + // persist the variations + if ((content.ContentType.Variations & (ContentVariation.CultureNeutral | ContentVariation.CultureSegment)) > 0) + { + foreach (var variationDto in GetVariationDtos(content, publishing)) + Database.Insert(variationDto); + } + // trigger here, before we reset Published etc OnUowRefreshedEntity(new ScopedEntityEventArgs(AmbientScope, entity)); // flip the entity's published property // this also flips its published state + // note: what depends on variations (eg PublishNames) is managed directly by the content if (content.PublishedState == PublishedState.Publishing) { content.Published = true; @@ -444,15 +454,26 @@ namespace Umbraco.Core.Persistence.Repositories.Implement Database.Insert(documentVersionDto); } + var versionToDelete = publishing ? new[] { content.VersionId, content.PublishedVersionId } : new[] { content.VersionId }; + // replace the property data - var pdataToDelete = publishing ? new[] { content.VersionId, content.PublishedVersionId } : new[] { content.VersionId }; - var deletePropertyDataSql = SqlContext.Sql().Delete().WhereIn(x => x.VersionId, pdataToDelete); + var deletePropertyDataSql = Sql().Delete().WhereIn(x => x.VersionId, versionToDelete); Database.Execute(deletePropertyDataSql); var propertyDataDtos = PropertyFactory.BuildDtos(content.VersionId, publishing ? content.PublishedVersionId : 0, entity.Properties, out var edited); foreach (var propertyDataDto in propertyDataDtos) Database.Insert(propertyDataDto); + // replace the variations + if ((content.ContentType.Variations & (ContentVariation.CultureNeutral | ContentVariation.CultureSegment)) > 0) + { + var deleteVariations = Sql().Delete().WhereIn(x => x.VersionId, versionToDelete); + Database.Execute(deleteVariations); + + foreach (var variationDto in GetVariationDtos(content, publishing)) + Database.Insert(variationDto); + } + // update the document dto // at that point, when un/publishing, the entity still has its old Published value // so we need to explicitely update the dto to persist the correct value @@ -833,6 +854,15 @@ namespace Umbraco.Core.Persistence.Repositories.Implement temp.Content.ResetDirtyProperties(false); } + // set variations, if varying + temps = temps.Where(x => x.ContentType.Variations.HasFlag(ContentVariation.CultureNeutral | ContentVariation.CultureSegment)).ToList(); + if (temps.Count > 0) + { + var variations = GetVariations(temps); + foreach (var temp in temps) + SetContentVariations(temp.Content, variations); + } + return content; } @@ -853,14 +883,104 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var publishedVersionId = dto.PublishedVersionDto != null ? dto.PublishedVersionDto.Id : 0; var temp = new TempContent(dto.NodeId, versionId, publishedVersionId, contentType); - var properties = GetPropertyCollections(new List> { temp }); + var ltemp = new List> { temp }; + var properties = GetPropertyCollections(ltemp); content.Properties = properties[dto.DocumentVersionDto.Id]; + // set variations, if varying + if ((contentType.Variations & (ContentVariation.CultureNeutral | ContentVariation.CultureSegment)) > 0) + { + var variations = GetVariations(ltemp); + SetContentVariations(content, variations); + } + // reset dirty initial properties (U4-1946) content.ResetDirtyProperties(false); return content; } + private void SetContentVariations(Content content, IDictionary> variations) + { + if (variations.TryGetValue(content.VersionId, out var variation)) + foreach (var v in variation) + { + content.SetName(v.LanguageId, v.Name); + } + if (content.PublishedVersionId > 0 && variations.TryGetValue(content.PublishedVersionId, out variation)) + foreach (var v in variation) + { + content.SetPublishName(v.LanguageId, v.Name); + content.SetCultureAvailability(v.LanguageId, v.Available); + } + } + + private IDictionary> GetVariations(List> temps) + where T : class, IContentBase + { + var versions = new List(); + foreach (var temp in temps) + { + versions.Add(temp.VersionId); + if (temp.PublishedVersionId > 0) + versions.Add(temp.PublishedVersionId); + } + if (versions.Count == 0) return new Dictionary>(); + + // fixme split content (name) vs document (availability) ? + var dtos = Database.FetchByGroups(versions, 2000, batch + => Sql() + .Select() + .From() + .WhereIn(x => x.VersionId, batch)); + + var variations = new Dictionary>(); + + foreach (var dto in dtos) + { + if (!variations.TryGetValue(dto.VersionId, out var variation)) + variations[dto.VersionId] = variation = new List(); + + variation.Add(new CultureVariation + { + LanguageId = dto.LanguageId.HasValue ? _languageRepository.GetIsoCodeById(dto.LanguageId.Value) : null, + Name = dto.Name, + Available = dto.Available + }); + } + + return variations; + } + + private IEnumerable GetVariationDtos(IContent content, bool publishing) + { + // fixme what about availability? + + foreach (var (culture, name) in content.Names) + yield return new ContentVersionCultureVariationDto + { + VersionId = content.VersionId, + LanguageId = _languageRepository.GetIdByIsoCode(culture), + Name = name + }; + + if (!publishing) yield break; + + foreach (var (culture, name) in content.PublishNames) + yield return new ContentVersionCultureVariationDto + { + VersionId = content.PublishedVersionId, + LanguageId = _languageRepository.GetIdByIsoCode(culture), + Name = name + }; + } + + private class CultureVariation + { + public string LanguageId { get; set; } + public string Name { get; set; } + public bool Available { get; set; } + } + #region Utilities protected override string EnsureUniqueNodeName(int parentId, string nodeName, int id = 0) diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs index efd6166a9f..4199c2ddbd 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs @@ -20,7 +20,6 @@ namespace Umbraco.Core.Persistence.Repositories.Implement { private readonly Dictionary _codeIdMap = new Dictionary(); private readonly Dictionary _idCodeMap = new Dictionary(); - private FullDataSetRepositoryCachePolicy _cachePolicy; public LanguageRepository(IScopeAccessor scopeAccessor, CacheHelper cache, ILogger logger) : base(scopeAccessor, cache, logger) @@ -28,9 +27,11 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected override IRepositoryCachePolicy CreateCachePolicy() { - return _cachePolicy = new FullDataSetRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, GetEntityId, /*expires:*/ false); + return new FullDataSetRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, GetEntityId, /*expires:*/ false); } + private FullDataSetRepositoryCachePolicy TypedCachePolicy => (FullDataSetRepositoryCachePolicy) CachePolicy; + #region Overrides of RepositoryBase protected override ILanguage PerformGet(int id) @@ -208,7 +209,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement public ILanguage GetByIsoCode(string isoCode) { - _cachePolicy.GetAllCached(PerformGetAll); // ensure cache is populated, in a non-expensive way + TypedCachePolicy.GetAllCached(PerformGetAll); // ensure cache is populated, in a non-expensive way var id = GetIdByIsoCode(isoCode); return Get(id); } @@ -217,7 +218,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // _codeIdMap is rebuilt whenever PerformGetAll runs public int GetIdByIsoCode(string isoCode) { - _cachePolicy.GetAllCached(PerformGetAll); // ensure cache is populated, in a non-expensive way + TypedCachePolicy.GetAllCached(PerformGetAll); // ensure cache is populated, in a non-expensive way lock (_codeIdMap) { if (_codeIdMap.TryGetValue(isoCode, out var id)) return id; @@ -229,12 +230,12 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // _idCodeMap is rebuilt whenever PerformGetAll runs public string GetIsoCodeById(int id) { - _cachePolicy.GetAllCached(PerformGetAll); // ensure cache is populated, in a non-expensive way + TypedCachePolicy.GetAllCached(PerformGetAll); // ensure cache is populated, in a non-expensive way lock (_codeIdMap) // yes, we want to lock _codeIdMap { if (_idCodeMap.TryGetValue(id, out var isoCode)) return isoCode; } - throw new ArgumentException($"Id {id} does not correspond to an existing language.", nameof(id),); + throw new ArgumentException($"Id {id} does not correspond to an existing language.", nameof(id)); } } } diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 38762b17da..77b78f355d 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -335,6 +335,7 @@ + @@ -366,6 +367,7 @@ + diff --git a/src/Umbraco.Tests/Models/VariationTests.cs b/src/Umbraco.Tests/Models/VariationTests.cs index 890d917ad3..70fbea5b4d 100644 --- a/src/Umbraco.Tests/Models/VariationTests.cs +++ b/src/Umbraco.Tests/Models/VariationTests.cs @@ -153,6 +153,35 @@ namespace Umbraco.Tests.Models Assert.AreEqual("b", prop.GetValue(published: true)); } + [Test] + public void ContentNames() + { + var contentType = new ContentType(-1) { Alias = "contentType" }; + var content = new Content("content", -1, contentType) { Id = 1, VersionId = 1 }; + + Assert.Throws(() => content.SetName("fr-FR", "name-fr")); + + contentType.Variations = ContentVariation.CultureNeutral; + + content.Name = "name"; + Assert.AreEqual("name", content.GetName(null)); + content.SetName(null, "name2"); + Assert.AreEqual("name2", content.Name); + Assert.AreEqual("name2", content.GetName(null)); + + content.SetName("fr-FR", "name-fr"); + content.SetName("en-UK", "name-uk"); + + Assert.AreEqual("name-fr", content.GetName("fr-FR")); + Assert.AreEqual("name-uk", content.GetName("en-UK")); + + Assert.AreEqual(2, content.Names.Count); + Assert.IsTrue(content.Names.ContainsKey("fr-FR")); + Assert.AreEqual("name-fr", content.Names["fr-FR"]); + Assert.IsTrue(content.Names.ContainsKey("en-UK")); + Assert.AreEqual("name-uk", content.Names["en-UK"]); + } + [Test] public void ContentTests() { diff --git a/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs index f6f7048315..f01c59df03 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs @@ -65,8 +65,9 @@ namespace Umbraco.Tests.Persistence.Repositories templateRepository = new TemplateRepository(scopeAccessor, cacheHelper, Logger, Mock.Of(), Mock.Of(), Mock.Of()); var tagRepository = new TagRepository(scopeAccessor, cacheHelper, Logger); - contentTypeRepository = new ContentTypeRepository(scopeAccessor, cacheHelper, Logger, templateRepository); - var repository = new DocumentRepository(scopeAccessor, cacheHelper, Logger, contentTypeRepository, templateRepository, tagRepository, Mock.Of()); + contentTypeRepository = new ContentTypeRepository(scopeAccessor, cacheHelper, Logger, templateRepository); + var languageRepository = new LanguageRepository(scopeAccessor, cacheHelper, Logger); + var repository = new DocumentRepository(scopeAccessor, cacheHelper, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, Mock.Of()); return repository; } diff --git a/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs index 47d7395320..5e20dda79a 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs @@ -35,10 +35,12 @@ namespace Umbraco.Tests.Persistence.Repositories private DocumentRepository CreateRepository(IScopeAccessor scopeAccessor, out ContentTypeRepository contentTypeRepository) { - var templateRepository = new TemplateRepository(scopeAccessor, CacheHelper.CreateDisabledCacheHelper(), Logger, Mock.Of(), Mock.Of(), Mock.Of()); - var tagRepository = new TagRepository(scopeAccessor, CacheHelper.CreateDisabledCacheHelper(), Logger); - contentTypeRepository = new ContentTypeRepository(scopeAccessor, CacheHelper.CreateDisabledCacheHelper(), Logger, templateRepository); - var repository = new DocumentRepository(scopeAccessor, CacheHelper.CreateDisabledCacheHelper(), Logger, contentTypeRepository, templateRepository, tagRepository, Mock.Of()); + var cacheHelper = CacheHelper.CreateDisabledCacheHelper(); + var templateRepository = new TemplateRepository(scopeAccessor, cacheHelper, Logger, Mock.Of(), Mock.Of(), Mock.Of()); + var tagRepository = new TagRepository(scopeAccessor, cacheHelper, Logger); + contentTypeRepository = new ContentTypeRepository(scopeAccessor, cacheHelper, Logger, templateRepository); + var languageRepository = new LanguageRepository(scopeAccessor, cacheHelper, Logger); + var repository = new DocumentRepository(scopeAccessor, cacheHelper, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, Mock.Of()); return repository; } diff --git a/src/Umbraco.Tests/Persistence/Repositories/DomainRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/DomainRepositoryTest.cs index ae51e861ff..e88b6e3f44 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/DomainRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/DomainRepositoryTest.cs @@ -25,8 +25,8 @@ namespace Umbraco.Tests.Persistence.Repositories var templateRepository = new TemplateRepository(accessor, DisabledCache, Logger, Mock.Of(), Mock.Of(), Mock.Of()); var tagRepository = new TagRepository(accessor, DisabledCache, Logger); contentTypeRepository = new ContentTypeRepository(accessor, DisabledCache, Logger, templateRepository); - documentRepository = new DocumentRepository(accessor, DisabledCache, Logger, contentTypeRepository, templateRepository, tagRepository, Mock.Of()); languageRepository = new LanguageRepository(accessor, DisabledCache, Logger); + documentRepository = new DocumentRepository(accessor, DisabledCache, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, Mock.Of()); var domainRepository = new DomainRepository(accessor, DisabledCache, Logger); return domainRepository; } diff --git a/src/Umbraco.Tests/Persistence/Repositories/PublicAccessRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/PublicAccessRepositoryTest.cs index 6e89ddb782..39b3b0f797 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/PublicAccessRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/PublicAccessRepositoryTest.cs @@ -310,7 +310,8 @@ namespace Umbraco.Tests.Persistence.Repositories var templateRepository = new TemplateRepository(accessor, CacheHelper, Logger, Mock.Of(), Mock.Of(), Mock.Of()); var tagRepository = new TagRepository(accessor, CacheHelper, Logger); contentTypeRepository = new ContentTypeRepository(accessor, CacheHelper, Logger, templateRepository); - var repository = new DocumentRepository(accessor, CacheHelper, Logger, contentTypeRepository, templateRepository, tagRepository, Mock.Of()); + var languageRepository = new LanguageRepository(accessor, CacheHelper, Logger); + var repository = new DocumentRepository(accessor, CacheHelper, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, Mock.Of()); return repository; } diff --git a/src/Umbraco.Tests/Persistence/Repositories/TagRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/TagRepositoryTest.cs index 8c9e7ab393..216fb08ecd 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/TagRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/TagRepositoryTest.cs @@ -954,7 +954,8 @@ namespace Umbraco.Tests.Persistence.Repositories var templateRepository = new TemplateRepository(accessor, DisabledCache, Logger, Mock.Of(), Mock.Of(), Mock.Of()); var tagRepository = new TagRepository(accessor, DisabledCache, Logger); contentTypeRepository = new ContentTypeRepository(accessor, DisabledCache, Logger, templateRepository); - var repository = new DocumentRepository(accessor, DisabledCache, Logger, contentTypeRepository, templateRepository, tagRepository, Mock.Of()); + var languageRepository = new LanguageRepository(accessor, DisabledCache, Logger); + var repository = new DocumentRepository(accessor, DisabledCache, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, Mock.Of()); return repository; } diff --git a/src/Umbraco.Tests/Persistence/Repositories/TemplateRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/TemplateRepositoryTest.cs index a42ba9b70b..e288bc5eaf 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/TemplateRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/TemplateRepositoryTest.cs @@ -406,7 +406,8 @@ namespace Umbraco.Tests.Persistence.Repositories var tagRepository = new TagRepository((IScopeAccessor) provider, DisabledCache, Logger); var contentTypeRepository = new ContentTypeRepository((IScopeAccessor) provider, DisabledCache, Logger, templateRepository); - var contentRepo = new DocumentRepository((IScopeAccessor) provider, DisabledCache, Logger, contentTypeRepository, templateRepository, tagRepository, Mock.Of()); + var languageRepository = new LanguageRepository((IScopeAccessor) provider, DisabledCache, Logger); + var contentRepo = new DocumentRepository((IScopeAccessor) provider, DisabledCache, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, Mock.Of()); var contentType = MockedContentTypes.CreateSimpleContentType("umbTextpage2", "Textpage"); ServiceContext.FileService.SaveTemplate(contentType.DefaultTemplate); // else, FK violation on contentType! diff --git a/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs index fe2c946331..56d5bfbc0c 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs @@ -41,8 +41,9 @@ namespace Umbraco.Tests.Persistence.Repositories var accessor = (IScopeAccessor) provider; templateRepository = new TemplateRepository(accessor, CacheHelper, Logger, Mock.Of(), Mock.Of(), Mock.Of()); var tagRepository = new TagRepository(accessor, CacheHelper, Logger); - contentTypeRepository = new ContentTypeRepository(accessor, CacheHelper, Logger, templateRepository); - var repository = new DocumentRepository(accessor, CacheHelper, Logger, contentTypeRepository, templateRepository, tagRepository, Mock.Of()); + contentTypeRepository = new ContentTypeRepository(accessor, CacheHelper, Logger, templateRepository); + var languageRepository = new LanguageRepository(accessor, CacheHelper, Logger); + var repository = new DocumentRepository(accessor, CacheHelper, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, Mock.Of()); return repository; } diff --git a/src/Umbraco.Tests/Services/ContentServicePerformanceTest.cs b/src/Umbraco.Tests/Services/ContentServicePerformanceTest.cs index 9b37c980c8..8ce6cf5c77 100644 --- a/src/Umbraco.Tests/Services/ContentServicePerformanceTest.cs +++ b/src/Umbraco.Tests/Services/ContentServicePerformanceTest.cs @@ -163,8 +163,9 @@ namespace Umbraco.Tests.Services { var tRepository = new TemplateRepository((IScopeAccessor) provider, DisabledCache, Logger, Mock.Of(), Mock.Of(), Mock.Of()); var tagRepo = new TagRepository((IScopeAccessor) provider, DisabledCache, Logger); - var ctRepository = new ContentTypeRepository((IScopeAccessor) provider, DisabledCache, Logger, tRepository); - var repository = new DocumentRepository((IScopeAccessor) provider, DisabledCache, Logger, ctRepository, tRepository, tagRepo, Mock.Of()); + var ctRepository = new ContentTypeRepository((IScopeAccessor) provider, DisabledCache, Logger, tRepository); + var languageRepository = new LanguageRepository((IScopeAccessor) provider, DisabledCache, Logger); + var repository = new DocumentRepository((IScopeAccessor) provider, DisabledCache, Logger, ctRepository, tRepository, tagRepo, languageRepository, Mock.Of()); // Act Stopwatch watch = Stopwatch.StartNew(); @@ -195,8 +196,9 @@ namespace Umbraco.Tests.Services { var tRepository = new TemplateRepository((IScopeAccessor) provider, DisabledCache, Logger, Mock.Of(), Mock.Of(), Mock.Of()); var tagRepo = new TagRepository((IScopeAccessor) provider, DisabledCache, Logger); - var ctRepository = new ContentTypeRepository((IScopeAccessor) provider, DisabledCache, Logger, tRepository); - var repository = new DocumentRepository((IScopeAccessor) provider, DisabledCache, Logger, ctRepository, tRepository, tagRepo, Mock.Of()); + var ctRepository = new ContentTypeRepository((IScopeAccessor) provider, DisabledCache, Logger, tRepository); + var languageRepository = new LanguageRepository((IScopeAccessor) provider, DisabledCache, Logger); + var repository = new DocumentRepository((IScopeAccessor) provider, DisabledCache, Logger, ctRepository, tRepository, tagRepo, languageRepository, Mock.Of()); // Act Stopwatch watch = Stopwatch.StartNew(); @@ -225,8 +227,9 @@ namespace Umbraco.Tests.Services { var tRepository = new TemplateRepository((IScopeAccessor) provider, DisabledCache, Logger, Mock.Of(), Mock.Of(), Mock.Of()); var tagRepo = new TagRepository((IScopeAccessor) provider, DisabledCache, Logger); - var ctRepository = new ContentTypeRepository((IScopeAccessor) provider, DisabledCache, Logger, tRepository); - var repository = new DocumentRepository((IScopeAccessor) provider, DisabledCache, Logger, ctRepository, tRepository, tagRepo, Mock.Of()); + var ctRepository = new ContentTypeRepository((IScopeAccessor) provider, DisabledCache, Logger, tRepository); + var languageRepository = new LanguageRepository((IScopeAccessor) provider, DisabledCache, Logger); + var repository = new DocumentRepository((IScopeAccessor) provider, DisabledCache, Logger, ctRepository, tRepository, tagRepo, languageRepository, Mock.Of()); // Act var contents = repository.GetMany(); @@ -258,8 +261,9 @@ namespace Umbraco.Tests.Services { var tRepository = new TemplateRepository((IScopeAccessor) provider, DisabledCache, Logger, Mock.Of(), Mock.Of(), Mock.Of()); var tagRepo = new TagRepository((IScopeAccessor) provider, DisabledCache, Logger); - var ctRepository = new ContentTypeRepository((IScopeAccessor) provider, DisabledCache, Logger, tRepository); - var repository = new DocumentRepository((IScopeAccessor) provider, DisabledCache, Logger, ctRepository, tRepository, tagRepo, Mock.Of()); + var ctRepository = new ContentTypeRepository((IScopeAccessor) provider, DisabledCache, Logger, tRepository); + var languageRepository = new LanguageRepository((IScopeAccessor) provider, DisabledCache, Logger); + var repository = new DocumentRepository((IScopeAccessor) provider, DisabledCache, Logger, ctRepository, tRepository, tagRepo, languageRepository, Mock.Of()); // Act var contents = repository.GetMany(); diff --git a/src/Umbraco.Tests/Services/ContentServiceTests.cs b/src/Umbraco.Tests/Services/ContentServiceTests.cs index a67d71d081..20d68fabf9 100644 --- a/src/Umbraco.Tests/Services/ContentServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentServiceTests.cs @@ -2474,6 +2474,68 @@ namespace Umbraco.Tests.Services */ } + [Test] + public void Can_SaveAndRead_Names() + { + var languageService = ServiceContext.LocalizationService; + + var langFr = new Language("fr-FR"); + var langUk = new Language("en-UK"); + languageService.Save(langFr); + languageService.Save(langUk); + + var contentTypeService = ServiceContext.ContentTypeService; + + var contentType = contentTypeService.Get("umbTextpage"); + contentType.Variations = ContentVariation.CultureNeutral; + contentTypeService.Save(contentType); + + var contentService = ServiceContext.ContentService; + var content = contentService.Create("Home US", - 1, "umbTextpage"); + + content.SetValue("author", "Barack Obama"); + content.SetName("fr-FR", "name-fr"); + content.SetName("en-UK", "name-uk"); + + contentService.Save(content); + + var content2 = contentService.GetById(content.Id); + + Assert.AreEqual("Home US", content2.Name); + Assert.AreEqual("name-fr", content2.GetName("fr-FR")); + Assert.AreEqual("name-uk", content2.GetName("en-UK")); + + content.PublishValues(langFr.Id); + content.PublishValues(langUk.Id); + contentService.SaveAndPublish(content); + + content2 = contentService.GetById(content.Id); + + Assert.AreEqual("Home US", content2.Name); + Assert.AreEqual("name-fr", content2.GetName("fr-FR")); + Assert.AreEqual("name-uk", content2.GetName("en-UK")); + + Assert.AreEqual("Home US", content2.PublishName); + Assert.AreEqual("name-fr", content2.GetPublishName("fr-FR")); + Assert.AreEqual("name-uk", content2.GetPublishName("en-UK")); + + content.SetName(null, "Home US2"); + content.SetName("fr-FR", "name-fr2"); + content.SetName("en-UK", "name-uk2"); + + contentService.Save(content); + + content2 = contentService.GetById(content.Id); + + Assert.AreEqual("Home US2", content2.Name); + Assert.AreEqual("name-fr2", content2.GetName("fr-FR")); + Assert.AreEqual("name-uk2", content2.GetName("en-UK")); + + Assert.AreEqual("Home US", content2.PublishName); + Assert.AreEqual("name-fr", content2.GetPublishName("fr-FR")); + Assert.AreEqual("name-uk", content2.GetPublishName("en-UK")); + } + private IEnumerable CreateContentHierarchy() { var contentType = ServiceContext.ContentTypeService.Get("umbTextpage"); @@ -2512,8 +2574,9 @@ namespace Umbraco.Tests.Services var accessor = (IScopeAccessor) provider; var templateRepository = new TemplateRepository(accessor, DisabledCache, Logger, Mock.Of(), Mock.Of(), Mock.Of()); var tagRepository = new TagRepository(accessor, DisabledCache, Logger); - contentTypeRepository = new ContentTypeRepository(accessor, DisabledCache, Logger, templateRepository); - var repository = new DocumentRepository(accessor, DisabledCache, Logger, contentTypeRepository, templateRepository, tagRepository, Mock.Of()); + contentTypeRepository = new ContentTypeRepository(accessor, DisabledCache, Logger, templateRepository); + var languageRepository = new LanguageRepository(accessor, DisabledCache, Logger); + var repository = new DocumentRepository(accessor, DisabledCache, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, Mock.Of()); return repository; } } diff --git a/src/Umbraco.Web/Models/Mapping/VariationResolver.cs b/src/Umbraco.Web/Models/Mapping/VariationResolver.cs index 84bd79133a..07ddd7363b 100644 --- a/src/Umbraco.Web/Models/Mapping/VariationResolver.cs +++ b/src/Umbraco.Web/Models/Mapping/VariationResolver.cs @@ -22,7 +22,7 @@ namespace Umbraco.Web.Models.Mapping public IEnumerable Resolve(IContent source, ContentItemDisplay destination, IEnumerable destMember, ResolutionContext context) { var allLanguages = _localizationService.GetAllLanguages().OrderBy(x => x.Id).ToList(); - if (allLanguages.Count == 0) return Enumerable.Empty(); + if (allLanguages.Count == 0) return Enumerable.Empty(); var langs = context.Mapper.Map, IEnumerable>(allLanguages, null, context); var variants = langs.Select(x => new ContentVariation @@ -30,7 +30,7 @@ namespace Umbraco.Web.Models.Mapping Language = x, Mandatory = x.Mandatory, //fixme these all need to the variant values but we need to wait for the db/service changes - Name = source.Name , + Name = source.GetName(x.IsoCode), Exists = source.HasVariation(x.Id), //TODO: This needs to be wired up with new APIs when they are ready PublishedState = source.PublishedState.ToString(), //Segment = ?? We'll need to populate this one day when we support segments