diff --git a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs index ec49544976..570b923514 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs @@ -144,6 +144,7 @@ namespace Umbraco.Core.Migrations.Upgrade Chain("{EE429F1B-9B26-43CA-89F8-A86017C809A3}"); Chain("{08919C4B-B431-449C-90EC-2B8445B5C6B1}"); Chain("{7EB0254C-CB8B-4C75-B15B-D48C55B449EB}"); + Chain("{C39BF2A7-1454-4047-BBFE-89E40F66ED63}"); //FINAL diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/MakeTagsVariant.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/MakeTagsVariant.cs new file mode 100644 index 0000000000..c898187884 --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/MakeTagsVariant.cs @@ -0,0 +1,16 @@ +using Umbraco.Core.Persistence.Dtos; + +namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 +{ + public class MakeTagsVariant : MigrationBase + { + public MakeTagsVariant(IMigrationContext context) + : base(context) + { } + + public override void Migrate() + { + AddColumn("languageId"); + } + } +} diff --git a/src/Umbraco.Core/Models/ContentTagsExtensions.cs b/src/Umbraco.Core/Models/ContentTagsExtensions.cs index 8aceaac762..dd7a716520 100644 --- a/src/Umbraco.Core/Models/ContentTagsExtensions.cs +++ b/src/Umbraco.Core/Models/ContentTagsExtensions.cs @@ -15,10 +15,10 @@ namespace Umbraco.Core.Models /// The property alias. /// The tags. /// A value indicating whether to merge the tags with existing tags instead of replacing them. - /// Tags do not support variants. - public static void AssignTags(this IContentBase content, string propertyTypeAlias, IEnumerable tags, bool merge = false) + /// A culture, for multi-lingual properties. + public static void AssignTags(this IContentBase content, string propertyTypeAlias, IEnumerable tags, bool merge = false, string culture = null) { - content.GetTagProperty(propertyTypeAlias).AssignTags(tags, merge); + content.GetTagProperty(propertyTypeAlias).AssignTags(tags, merge, culture); } /// @@ -27,10 +27,10 @@ namespace Umbraco.Core.Models /// The content item. /// The property alias. /// The tags. - /// Tags do not support variants. - public static void RemoveTags(this IContentBase content, string propertyTypeAlias, IEnumerable tags) + /// A culture, for multi-lingual properties. + public static void RemoveTags(this IContentBase content, string propertyTypeAlias, IEnumerable tags, string culture = null) { - content.GetTagProperty(propertyTypeAlias).RemoveTags(tags); + content.GetTagProperty(propertyTypeAlias).RemoveTags(tags, culture); } // gets and validates the property diff --git a/src/Umbraco.Core/Models/ITag.cs b/src/Umbraco.Core/Models/ITag.cs index 6f492a2d78..f2c30b2644 100644 --- a/src/Umbraco.Core/Models/ITag.cs +++ b/src/Umbraco.Core/Models/ITag.cs @@ -20,6 +20,12 @@ namespace Umbraco.Core.Models [DataMember] string Text { get; set; } + /// + /// Gets or sets the tag language. + /// + [DataMember] + int? LanguageId { get; set; } + /// /// Gets the number of nodes tagged with this tag. /// diff --git a/src/Umbraco.Core/Models/PropertyTagsExtensions.cs b/src/Umbraco.Core/Models/PropertyTagsExtensions.cs index 26779161a1..39172fff34 100644 --- a/src/Umbraco.Core/Models/PropertyTagsExtensions.cs +++ b/src/Umbraco.Core/Models/PropertyTagsExtensions.cs @@ -38,13 +38,13 @@ namespace Umbraco.Core.Models } /// - /// Assign default tags. + /// Assign tags. /// /// The property. /// The tags. /// A value indicating whether to merge the tags with existing tags instead of replacing them. - /// Tags do not support variants. - public static void AssignTags(this Property property, IEnumerable tags, bool merge = false) + /// A culture, for multi-lingual properties. + public static void AssignTags(this Property property, IEnumerable tags, bool merge = false, string culture = null) { if (property == null) throw new ArgumentNullException(nameof(property)); @@ -52,11 +52,11 @@ namespace Umbraco.Core.Models if (configuration == null) throw new NotSupportedException($"Property with alias \"{property.Alias}\" does not support tags."); - property.AssignTags(tags, merge, configuration.StorageType, configuration.Delimiter); + property.AssignTags(tags, merge, configuration.StorageType, configuration.Delimiter, culture); } // assumes that parameters are consistent with the datatype configuration - internal static void AssignTags(this Property property, IEnumerable tags, bool merge, TagsStorageType storageType, char delimiter) + private static void AssignTags(this Property property, IEnumerable tags, bool merge, TagsStorageType storageType, char delimiter, string culture) { // set the property value var trimmedTags = tags.Select(x => x.Trim()).ToArray(); @@ -68,11 +68,11 @@ namespace Umbraco.Core.Models switch (storageType) { case TagsStorageType.Csv: - property.SetValue(string.Join(delimiter.ToString(), currentTags.Union(trimmedTags))); // csv string + property.SetValue(string.Join(delimiter.ToString(), currentTags.Union(trimmedTags)), culture); // csv string break; case TagsStorageType.Json: - property.SetValue(JsonConvert.SerializeObject(currentTags.Union(trimmedTags).ToArray())); // json array + property.SetValue(JsonConvert.SerializeObject(currentTags.Union(trimmedTags).ToArray()), culture); // json array break; } } @@ -81,23 +81,23 @@ namespace Umbraco.Core.Models switch (storageType) { case TagsStorageType.Csv: - property.SetValue(string.Join(delimiter.ToString(), trimmedTags)); // csv string + property.SetValue(string.Join(delimiter.ToString(), trimmedTags), culture); // csv string break; case TagsStorageType.Json: - property.SetValue(JsonConvert.SerializeObject(trimmedTags)); // json array + property.SetValue(JsonConvert.SerializeObject(trimmedTags), culture); // json array break; } } } /// - /// Removes default tags. + /// Removes tags. /// /// The property. /// The tags. - /// Tags do not support variants. - public static void RemoveTags(this Property property, IEnumerable tags) + /// A culture, for multi-lingual properties. + public static void RemoveTags(this Property property, IEnumerable tags, string culture = null) { if (property == null) throw new ArgumentNullException(nameof(property)); @@ -105,33 +105,33 @@ namespace Umbraco.Core.Models if (configuration == null) throw new NotSupportedException($"Property with alias \"{property.Alias}\" does not support tags."); - property.RemoveTags(tags, configuration.StorageType, configuration.Delimiter); + property.RemoveTags(tags, configuration.StorageType, configuration.Delimiter, culture); } // assumes that parameters are consistent with the datatype configuration - private static void RemoveTags(this Property property, IEnumerable tags, TagsStorageType storageType, char delimiter) + private static void RemoveTags(this Property property, IEnumerable tags, TagsStorageType storageType, char delimiter, string culture) { // already empty = nothing to do - //fixme doesn't take into account variants - var value = property.GetValue()?.ToString(); + var value = property.GetValue(culture)?.ToString(); if (string.IsNullOrWhiteSpace(value)) return; // set the property value var trimmedTags = tags.Select(x => x.Trim()).ToArray(); - var currentTags = property.GetTagsValue(storageType, delimiter); + var currentTags = property.GetTagsValue(storageType, delimiter, culture); switch (storageType) { case TagsStorageType.Csv: - property.SetValue(string.Join(delimiter.ToString(), currentTags.Except(trimmedTags))); // csv string + property.SetValue(string.Join(delimiter.ToString(), currentTags.Except(trimmedTags)), culture); // csv string break; case TagsStorageType.Json: - property.SetValue(JsonConvert.SerializeObject(currentTags.Except(trimmedTags).ToArray())); // json array + property.SetValue(JsonConvert.SerializeObject(currentTags.Except(trimmedTags).ToArray()), culture); // json array break; } } - internal static IEnumerable GetTagsValue(this Property property) + // used by ContentRepositoryBase + internal static IEnumerable GetTagsValue(this Property property, string culture = null) { if (property == null) throw new ArgumentNullException(nameof(property)); @@ -139,15 +139,14 @@ namespace Umbraco.Core.Models if (configuration == null) throw new NotSupportedException($"Property with alias \"{property.Alias}\" does not support tags."); - return property.GetTagsValue(configuration.StorageType, configuration.Delimiter); + return property.GetTagsValue(configuration.StorageType, configuration.Delimiter, culture); } - internal static IEnumerable GetTagsValue(this Property property, TagsStorageType storageType, char delimiter) + private static IEnumerable GetTagsValue(this Property property, TagsStorageType storageType, char delimiter, string culture = null) { if (property == null) throw new ArgumentNullException(nameof(property)); - //fixme doesn't take into account variants - var value = property.GetValue()?.ToString(); + var value = property.GetValue(culture)?.ToString(); if (string.IsNullOrWhiteSpace(value)) return Enumerable.Empty(); switch (storageType) @@ -158,7 +157,6 @@ namespace Umbraco.Core.Models case TagsStorageType.Json: try { - //fixme doesn't take into account variants return JsonConvert.DeserializeObject(value).Select(x => x.ToString().Trim()); } catch (JsonException) @@ -178,34 +176,33 @@ namespace Umbraco.Core.Models /// The property. /// The property value. /// The datatype configuration. - /// + /// A culture, for multi-lingual properties. + /// /// The value is either a string (delimited string) or an enumeration of strings (tag list). /// This is used both by the content repositories to initialize a property with some tag values, and by the /// content controllers to update a property with values received from the property editor. /// - internal static void SetTagsValue(this Property property, object value, TagConfiguration tagConfiguration) + internal static void SetTagsValue(this Property property, object value, TagConfiguration tagConfiguration, string culture) { if (property == null) throw new ArgumentNullException(nameof(property)); if (tagConfiguration == null) throw new ArgumentNullException(nameof(tagConfiguration)); - var merge = false; // fixme always! var storageType = tagConfiguration.StorageType; var delimiter = tagConfiguration.Delimiter; - SetTagsValue(property, value, merge, storageType, delimiter); + SetTagsValue(property, value, storageType, delimiter, culture); } // assumes that parameters are consistent with the datatype configuration // value can be an enumeration of string, or a serialized value using storageType format - // fixme merge always false here?! - private static void SetTagsValue(Property property, object value, bool merge, TagsStorageType storageType, char delimiter) + private static void SetTagsValue(Property property, object value, TagsStorageType storageType, char delimiter, string culture) { if (value == null) value = Enumerable.Empty(); // if value is already an enumeration of strings, just use it if (value is IEnumerable tags1) { - property.AssignTags(tags1, merge, storageType, delimiter); + property.AssignTags(tags1, false, storageType, delimiter, culture); return; } @@ -214,14 +211,14 @@ namespace Umbraco.Core.Models { case TagsStorageType.Csv: var tags2 = value.ToString().Split(new[] { delimiter }, StringSplitOptions.RemoveEmptyEntries); - property.AssignTags(tags2, merge, storageType, delimiter); + property.AssignTags(tags2, false, storageType, delimiter, culture); break; case TagsStorageType.Json: try { var tags3 = JsonConvert.DeserializeObject>(value.ToString()); - property.AssignTags(tags3 ?? Enumerable.Empty(), merge, storageType, delimiter); + property.AssignTags(tags3 ?? Enumerable.Empty(), false, storageType, delimiter, culture); } catch (Exception ex) { diff --git a/src/Umbraco.Core/Models/Tag.cs b/src/Umbraco.Core/Models/Tag.cs index 867d43c257..e9707e587d 100644 --- a/src/Umbraco.Core/Models/Tag.cs +++ b/src/Umbraco.Core/Models/Tag.cs @@ -16,6 +16,7 @@ namespace Umbraco.Core.Models private string _group; private string _text; + private int? _languageId; /// /// Initializes a new instance of the class. @@ -26,11 +27,12 @@ namespace Umbraco.Core.Models /// /// Initializes a new instance of the class. /// - public Tag(int id, string group, string text) + public Tag(int id, string group, string text, int? languageId = null) { Id = id; Text = text; Group = group; + LanguageId = languageId; } private static PropertySelectors Selectors => _selectors ?? (_selectors = new PropertySelectors()); @@ -39,6 +41,7 @@ namespace Umbraco.Core.Models { public readonly PropertyInfo Group = ExpressionHelper.GetPropertyInfo(x => x.Group); public readonly PropertyInfo Text = ExpressionHelper.GetPropertyInfo(x => x.Text); + public readonly PropertyInfo LanguageId = ExpressionHelper.GetPropertyInfo(x => x.LanguageId); } /// @@ -55,6 +58,13 @@ namespace Umbraco.Core.Models set => SetPropertyValueAndDetectChanges(value, ref _text, Selectors.Text); } + /// + public int? LanguageId + { + get => _languageId; + set => SetPropertyValueAndDetectChanges(value, ref _languageId, Selectors.LanguageId); + } + /// public int NodeCount { get; internal set; } } diff --git a/src/Umbraco.Core/Models/TaggedEntity.cs b/src/Umbraco.Core/Models/TaggedEntity.cs index ac194c15cd..8c4695555d 100644 --- a/src/Umbraco.Core/Models/TaggedEntity.cs +++ b/src/Umbraco.Core/Models/TaggedEntity.cs @@ -5,10 +5,13 @@ namespace Umbraco.Core.Models /// /// Represents a tagged entity. /// - /// Note that it is the properties of an entity (like Content, Media, Members, etc.) that is tagged, - /// which is why this class is composed of a list of tagged properties and an Id reference to the actual entity. + /// Note that it is the properties of an entity (like Content, Media, Members, etc.) that are tagged, + /// which is why this class is composed of a list of tagged properties and the identifier the actual entity. public class TaggedEntity { + /// + /// Initializes a new instance of the class. + /// public TaggedEntity(int entityId, IEnumerable taggedProperties) { EntityId = entityId; @@ -16,13 +19,13 @@ namespace Umbraco.Core.Models } /// - /// Id of the entity, which is tagged + /// Gets the identifier of the entity. /// - public int EntityId { get; private set; } + public int EntityId { get; } /// - /// An enumerable list of tagged properties + /// Gets the tagged properties. /// - public IEnumerable TaggedProperties { get; private set; } + public IEnumerable TaggedProperties { get; } } } diff --git a/src/Umbraco.Core/Models/TaggedProperty.cs b/src/Umbraco.Core/Models/TaggedProperty.cs index 2b9650b432..2d9fda9a4f 100644 --- a/src/Umbraco.Core/Models/TaggedProperty.cs +++ b/src/Umbraco.Core/Models/TaggedProperty.cs @@ -7,6 +7,9 @@ namespace Umbraco.Core.Models /// public class TaggedProperty { + /// + /// Initializes a new instance of the class. + /// public TaggedProperty(int propertyTypeId, string propertyTypeAlias, IEnumerable tags) { PropertyTypeId = propertyTypeId; @@ -15,18 +18,18 @@ namespace Umbraco.Core.Models } /// - /// Id of the PropertyType, which this tagged property is based on + /// Gets the identifier of the property type. /// - public int PropertyTypeId { get; private set; } + public int PropertyTypeId { get; } /// - /// Alias of the PropertyType, which this tagged property is based on + /// Gets the alias of the property type. /// - public string PropertyTypeAlias { get; private set; } + public string PropertyTypeAlias { get; } /// - /// An enumerable list of Tags for the property + /// Gets the tags. /// - public IEnumerable Tags { get; private set; } + public IEnumerable Tags { get; } } } diff --git a/src/Umbraco.Core/Persistence/Dtos/TagDto.cs b/src/Umbraco.Core/Persistence/Dtos/TagDto.cs index 15c309d9e5..f6296e4bd0 100644 --- a/src/Umbraco.Core/Persistence/Dtos/TagDto.cs +++ b/src/Umbraco.Core/Persistence/Dtos/TagDto.cs @@ -3,11 +3,13 @@ using Umbraco.Core.Persistence.DatabaseAnnotations; namespace Umbraco.Core.Persistence.Dtos { - [TableName(Constants.DatabaseSchema.Tables.Tag)] + [TableName(TableName)] [PrimaryKey("id")] [ExplicitColumns] internal class TagDto { + public const string TableName = Constants.DatabaseSchema.Tables.Tag; + [Column("id")] [PrimaryKeyColumn] public int Id { get; set; } @@ -16,9 +18,15 @@ namespace Umbraco.Core.Persistence.Dtos [Length(100)] public string Group { get; set; } + [Column("languageId")] + [ForeignKey(typeof(LanguageDto))] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_LanguageId")] + [NullSetting(NullSetting = NullSettings.Null)] + public int? LanguageId { get;set; } + [Column("tag")] [Length(200)] - [Index(IndexTypes.UniqueNonClustered, ForColumns = "group,tag", Name = "IX_cmsTags")] + [Index(IndexTypes.UniqueNonClustered, ForColumns = "group,tag,languageId", Name = "IX_cmsTags")] public string Text { get; set; } //[Column("key")] diff --git a/src/Umbraco.Core/Persistence/Factories/TagFactory.cs b/src/Umbraco.Core/Persistence/Factories/TagFactory.cs index 867e6b0ae3..10441707ec 100644 --- a/src/Umbraco.Core/Persistence/Factories/TagFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/TagFactory.cs @@ -7,7 +7,7 @@ namespace Umbraco.Core.Persistence.Factories { public static ITag BuildEntity(TagDto dto) { - var entity = new Tag(dto.Id, dto.Group, dto.Text) { NodeCount = dto.NodeCount }; + var entity = new Tag(dto.Id, dto.Group, dto.Text, dto.LanguageId) { NodeCount = dto.NodeCount }; // reset dirty initial properties (U4-1946) entity.ResetDirtyProperties(false); return entity; @@ -20,6 +20,7 @@ namespace Umbraco.Core.Persistence.Factories Id = entity.Id, Group = entity.Group, Text = entity.Text, + LanguageId = entity.LanguageId //Key = entity.Group + "/" + entity.Text // de-normalize }; } diff --git a/src/Umbraco.Core/Persistence/Mappers/TagMapper.cs b/src/Umbraco.Core/Persistence/Mappers/TagMapper.cs index 8cd2ab27d7..63f73d060a 100644 --- a/src/Umbraco.Core/Persistence/Mappers/TagMapper.cs +++ b/src/Umbraco.Core/Persistence/Mappers/TagMapper.cs @@ -23,6 +23,7 @@ namespace Umbraco.Core.Persistence.Mappers CacheMap(src => src.Id, dto => dto.Id); CacheMap(src => src.Text, dto => dto.Text); CacheMap(src => src.Group, dto => dto.Group); + CacheMap(src => src.LanguageId, dto => dto.LanguageId); } } } diff --git a/src/Umbraco.Core/Persistence/NPocoSqlExtensions.cs b/src/Umbraco.Core/Persistence/NPocoSqlExtensions.cs index a5ab62d25f..86839cdaee 100644 --- a/src/Umbraco.Core/Persistence/NPocoSqlExtensions.cs +++ b/src/Umbraco.Core/Persistence/NPocoSqlExtensions.cs @@ -589,11 +589,14 @@ namespace Umbraco.Core.Persistence /// Creates a SELECT COUNT(*) Sql statement. /// /// The origin sql. + /// An optional alias. /// The Sql statement. - public static Sql SelectCount(this Sql sql) + public static Sql SelectCount(this Sql sql, string alias = null) { if (sql == null) throw new ArgumentNullException(nameof(sql)); - return sql.Select("COUNT(*)"); + var text = "COUNT(*)"; + if (alias != null) text += " AS " + sql.SqlContext.SqlSyntax.GetQuotedColumnName(alias); + return sql.Select(text); } /// @@ -607,13 +610,29 @@ namespace Umbraco.Core.Persistence /// If is empty, all columns are counted. /// public static Sql SelectCount(this Sql sql, params Expression>[] fields) + => sql.SelectCount(null, fields); + + /// + /// Creates a SELECT COUNT Sql statement. + /// + /// The type of the DTO to count. + /// The origin sql. + /// An alias. + /// Expressions indicating the columns to count. + /// The Sql statement. + /// + /// If is empty, all columns are counted. + /// + public static Sql SelectCount(this Sql sql, string alias, params Expression>[] fields) { if (sql == null) throw new ArgumentNullException(nameof(sql)); var sqlSyntax = sql.SqlContext.SqlSyntax; var columns = fields.Length == 0 ? sql.GetColumns(withAlias: false) : fields.Select(x => sqlSyntax.GetFieldName(x)).ToArray(); - return sql.Select("COUNT (" + string.Join(", ", columns) + ")"); + var text = "COUNT (" + string.Join(", ", columns) + ")"; + if (alias != null) text += " AS " + sql.SqlContext.SqlSyntax.GetQuotedColumnName(alias); + return sql.Select(text); } /// @@ -705,6 +724,56 @@ namespace Umbraco.Core.Persistence return sql.Append(", " + string.Join(", ", sql.GetColumns(tableAlias: tableAlias, columnExpressions: fields))); } + /// + /// Adds a COUNT(*) to a SELECT Sql statement. + /// + /// The origin sql. + /// An optional alias. + /// The Sql statement. + public static Sql AndSelectCount(this Sql sql, string alias = null) + { + if (sql == null) throw new ArgumentNullException(nameof(sql)); + var text = ", COUNT(*)"; + if (alias != null) text += " AS " + sql.SqlContext.SqlSyntax.GetQuotedColumnName(alias); + return sql.Append(text); + } + + /// + /// Adds a COUNT to a SELECT Sql statement. + /// + /// The type of the DTO to count. + /// The origin sql. + /// Expressions indicating the columns to count. + /// The Sql statement. + /// + /// If is empty, all columns are counted. + /// + public static Sql AndSelectCount(this Sql sql, params Expression>[] fields) + => sql.AndSelectCount(null, fields); + + /// + /// Adds a COUNT to a SELECT Sql statement. + /// + /// The type of the DTO to count. + /// The origin sql. + /// An alias. + /// Expressions indicating the columns to count. + /// The Sql statement. + /// + /// If is empty, all columns are counted. + /// + public static Sql AndSelectCount(this Sql sql, string alias = null, params Expression>[] fields) + { + if (sql == null) throw new ArgumentNullException(nameof(sql)); + var sqlSyntax = sql.SqlContext.SqlSyntax; + var columns = fields.Length == 0 + ? sql.GetColumns(withAlias: false) + : fields.Select(x => sqlSyntax.GetFieldName(x)).ToArray(); + var text = ", COUNT (" + string.Join(", ", columns) + ")"; + if (alias != null) text += " AS " + sql.SqlContext.SqlSyntax.GetQuotedColumnName(alias); + return sql.Append(text); + } + /// /// Creates a SELECT Sql statement with a referenced Dto. /// diff --git a/src/Umbraco.Core/Persistence/Repositories/ITagRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ITagRepository.cs index 782f3f1b89..c3e6dc028b 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ITagRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ITagRepository.cs @@ -19,7 +19,8 @@ namespace Umbraco.Core.Persistence.Repositories /// When is false, the tags specified in are added to those already assigned. /// When is empty and is true, all assigned tags are removed. /// - void Assign(int contentId, int propertyTypeId, IEnumerable tags, bool replaceTags); + // TODO: replaceTags is used as 'false' in tests exclusively - should get rid of it + void Assign(int contentId, int propertyTypeId, IEnumerable tags, bool replaceTags = true); /// /// Removes assigned tags from a content property. @@ -46,54 +47,48 @@ namespace Umbraco.Core.Persistence.Repositories #region Queries + /// + /// Gets a tagged entity. + /// TaggedEntity GetTaggedEntityByKey(Guid key); + + /// + /// Gets a tagged entity. + /// TaggedEntity GetTaggedEntityById(int id); - IEnumerable GetTaggedEntitiesByTagGroup(TaggableObjectTypes objectType, string tagGroup); - - IEnumerable GetTaggedEntitiesByTag(TaggableObjectTypes objectType, string tag, string tagGroup = null); + /// Gets all entities of a type, tagged with any tag in the specified group. + IEnumerable GetTaggedEntitiesByTagGroup(TaggableObjectTypes objectType, string group, string culture = null); /// - /// Returns all tags for an entity type (content/media/member) + /// Gets all entities of a type, tagged with the specified tag. /// - /// Entity type - /// Optional group - /// - IEnumerable GetTagsForEntityType(TaggableObjectTypes objectType, string group = null); + IEnumerable GetTaggedEntitiesByTag(TaggableObjectTypes objectType, string tag, string group = null, string culture = null); /// - /// Returns all tags that exist on the content item - Content/Media/Member + /// Gets all tags for an entity type. /// - /// The content item id to get tags for - /// Optional group - /// - IEnumerable GetTagsForEntity(int contentId, string group = null); + IEnumerable GetTagsForEntityType(TaggableObjectTypes objectType, string group = null, string culture = null); /// - /// Returns all tags that exist on the content item - Content/Media/Member + /// Gets all tags attached to an entity. /// - /// The content item id to get tags for - /// Optional group - /// - IEnumerable GetTagsForEntity(Guid contentId, string group = null); + IEnumerable GetTagsForEntity(int contentId, string group = null, string culture = null); /// - /// Returns all tags that exist on the content item for the property specified - Content/Media/Member + /// Gets all tags attached to an entity. /// - /// The content item id to get tags for - /// The property alias to get tags for - /// Optional group - /// - IEnumerable GetTagsForProperty(int contentId, string propertyTypeAlias, string group = null); + IEnumerable GetTagsForEntity(Guid contentId, string group = null, string culture = null); /// - /// Returns all tags that exist on the content item for the property specified - Content/Media/Member + /// Gets all tags attached to an entity via a property. /// - /// The content item id to get tags for - /// The property alias to get tags for - /// Optional group - /// - IEnumerable GetTagsForProperty(Guid contentId, string propertyTypeAlias, string group = null); + IEnumerable GetTagsForProperty(int contentId, string propertyTypeAlias, string group = null, string culture = null); + + /// + /// Gets all tags attached to an entity via a property. + /// + IEnumerable GetTagsForProperty(Guid contentId, string propertyTypeAlias, string group = null, string culture = null); #endregion } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs index 58f58c3d84..bd7943ff1d 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs @@ -217,8 +217,26 @@ namespace Umbraco.Core.Persistence.Repositories.Implement foreach (var property in entity.Properties) { var tagConfiguration = property.GetTagConfiguration(); - if (tagConfiguration == null) continue; - tagRepo.Assign(entity.Id, property.PropertyTypeId, property.GetTagsValue().Select(x => new Tag { Group = tagConfiguration.Group, Text = x }), true); + if (tagConfiguration == null) continue; // not a tags property + + if (property.PropertyType.VariesByCulture()) + { + var tags = new List(); + foreach (var pvalue in property.Values) + { + var tagsValue = property.GetTagsValue(pvalue.Culture); + var languageId = LanguageRepository.GetIdByIsoCode(pvalue.Culture); + var cultureTags = tagsValue.Select(x => new Tag { Group = tagConfiguration.Group, Text = x, LanguageId = languageId }); + tags.AddRange(cultureTags); + } + tagRepo.Assign(entity.Id, property.PropertyTypeId, tags); + } + else + { + var tagsValue = property.GetTagsValue(); // strings + var tags = tagsValue.Select(x => new Tag { Group = tagConfiguration.Group, Text = x }); + tagRepo.Assign(entity.Id, property.PropertyTypeId, tags); + } } } @@ -541,16 +559,6 @@ namespace Umbraco.Core.Persistence.Repositories.Implement propertyDataDtos.AddRange(propertyDataDtos2); var properties = PropertyFactory.BuildEntities(compositionProperties, propertyDataDtos, temp.PublishedVersionId, LanguageRepository).ToList(); - // deal with tags - foreach (var property in properties) - { - if (!tagConfigurations.TryGetValue(property.PropertyType.PropertyEditorAlias, out var tagConfiguration)) - continue; - - //fixme doesn't take into account variants - property.SetTagsValue(property.GetValue(), tagConfiguration); - } - if (result.ContainsKey(temp.VersionId)) { if (ContentRepositoryBase.ThrowOnWarning) diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs index 09fb664ffe..e236670e74 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs @@ -111,7 +111,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement "DELETE FROM umbracoPropertyData WHERE languageId = @id", "DELETE FROM umbracoContentVersionCultureVariation WHERE languageId = @id", "DELETE FROM umbracoDocumentCultureVariation WHERE languageId = @id", - "DELETE FROM umbracoLanguage WHERE id = @id" + "DELETE FROM umbracoLanguage WHERE id = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.Tag + " WHERE languageId = @id" }; return list; } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/TagRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/TagRepository.cs index 418e3d8ac3..77e474be08 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/TagRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/TagRepository.cs @@ -11,6 +11,7 @@ using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Factories; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Scoping; +using static Umbraco.Core.Persistence.NPocoSqlExtensions.Statics; namespace Umbraco.Core.Persistence.Repositories.Implement { @@ -109,74 +110,65 @@ namespace Umbraco.Core.Persistence.Repositories.Implement #region Assign and Remove Tags /// - public void Assign(int contentId, int propertyTypeId, IEnumerable tags, bool replaceTags) + // only invoked from ContentRepositoryBase with all cultures + replaceTags being true + public void Assign(int contentId, int propertyTypeId, IEnumerable tags, bool replaceTags = true) { // to no-duplicates array var tagsA = tags.Distinct(new TagComparer()).ToArray(); - // no tags? - if (tagsA.Length == 0) + // replacing = clear all + if (replaceTags) { - // replacing = clear all - if (replaceTags) - { - var sql0 = Sql().Delete().Where(x => x.NodeId == contentId && x.PropertyTypeId == propertyTypeId); - Database.Execute(sql0); - } - - // nothing else to do - return; + var sql0 = Sql().Delete().Where(x => x.NodeId == contentId && x.PropertyTypeId == propertyTypeId); + Database.Execute(sql0); } + // no tags? nothing else to do + if (tagsA.Length == 0) + return; + // tags // using some clever logic (?) to insert tags that don't exist in 1 query + // must coalesce languageId because equality of NULLs does not exist var tagSetSql = GetTagSet(tagsA); var group = SqlSyntax.GetQuotedColumnName("group"); // insert tags - var sql1 = $@"INSERT INTO cmsTags (tag, {group}) -SELECT tagSet.tag, tagSet.{group} + var sql1 = $@"INSERT INTO cmsTags (tag, {group}, languageId) +SELECT tagSet.tag, tagSet.{group}, tagSet.languageId FROM {tagSetSql} -LEFT OUTER JOIN cmsTags ON (tagSet.tag = cmsTags.tag AND tagSet.{group} = cmsTags.{group}) +LEFT OUTER JOIN cmsTags ON (tagSet.tag = cmsTags.tag AND tagSet.{group} = cmsTags.{group} AND COALESCE(tagSet.languageId, -1) = COALESCE(cmsTags.languageId, -1)) WHERE cmsTags.id IS NULL"; Database.Execute(sql1); - // if replacing, remove everything first - if (replaceTags) - { - var sql2 = Sql().Delete().Where(x => x.NodeId == contentId && x.PropertyTypeId == propertyTypeId); - Database.Execute(sql2); - } - // insert relations - var sql3 = $@"INSERT INTO cmsTagRelationship (nodeId, propertyTypeId, tagId) + var sql2 = $@"INSERT INTO cmsTagRelationship (nodeId, propertyTypeId, tagId) SELECT {contentId}, {propertyTypeId}, tagSet2.Id FROM ( SELECT t.Id FROM {tagSetSql} - INNER JOIN cmsTags as t ON (tagSet.tag = t.tag AND tagSet.{group} = t.{group}) + INNER JOIN cmsTags as t ON (tagSet.tag = t.tag AND tagSet.{group} = t.{group} AND COALESCE(tagSet.languageId, -1) = COALESCE(t.languageId, -1)) ) AS tagSet2 LEFT OUTER JOIN cmsTagRelationship r ON (tagSet2.id = r.tagId AND r.nodeId = {contentId} AND r.propertyTypeID = {propertyTypeId}) WHERE r.tagId IS NULL"; - Database.Execute(sql3); + Database.Execute(sql2); } /// + // only invoked from tests public void Remove(int contentId, int propertyTypeId, IEnumerable tags) { var tagSetSql = GetTagSet(tags); + var group = SqlSyntax.GetQuotedColumnName("group"); - var deleteSql = string.Concat("DELETE FROM cmsTagRelationship WHERE nodeId = ", - contentId, - " AND propertyTypeId = ", - propertyTypeId, - " AND tagId IN ", - "(SELECT id FROM cmsTags INNER JOIN ", - tagSetSql, - " ON (TagSet.Tag = cmsTags.Tag and TagSet." + SqlSyntax.GetQuotedColumnName("group") + @" = cmsTags." + SqlSyntax.GetQuotedColumnName("group") + @"))"); + var deleteSql = $@"DELETE FROM cmsTagRelationship WHERE nodeId = {contentId} AND propertyTypeId = {propertyTypeId} AND tagId IN ( + SELECT id FROM cmsTags INNER JOIN {tagSetSql} ON ( + tagSet.tag = cmsTags.tag AND tagSet.{group} = cmsTags.{group} AND COALESCE(tagSet.languageId, -1) = COALESCE(cmsTags.languageId, -1) + ) + )"; Database.Execute(deleteSql); } @@ -207,13 +199,6 @@ WHERE r.tagId IS NULL"; // private string GetTagSet(IEnumerable tags) { - string EscapeSqlString(string s) - { - // why were we escaping @ symbols? - //return NPocoDatabaseExtensions.EscapeAtSymbols(s.Replace("'", "''")); - return s.Replace("'", "''"); - } - var sql = new StringBuilder(); var group = SqlSyntax.GetQuotedColumnName("group"); var first = true; @@ -226,11 +211,17 @@ WHERE r.tagId IS NULL"; else sql.Append(" UNION "); sql.Append("SELECT N'"); - sql.Append(EscapeSqlString(tag.Text)); + sql.Append(SqlSyntax.EscapeString(tag.Text)); sql.Append("' AS tag, '"); - sql.Append(EscapeSqlString(tag.Group)); + sql.Append(SqlSyntax.EscapeString(tag.Group)); sql.Append("' AS "); sql.Append(group); + sql.Append(" , "); + if (tag.LanguageId.HasValue) + sql.Append(tag.LanguageId); + else + sql.Append("NULL"); + sql.Append(" AS languageId"); } sql.Append(") AS tagSet"); @@ -244,14 +235,17 @@ WHERE r.tagId IS NULL"; public bool Equals(ITag x, ITag y) { return ReferenceEquals(x, y) // takes care of both being null - || x != null && y != null && x.Text == y.Text && x.Group == y.Group; + || x != null && y != null && x.Text == y.Text && x.Group == y.Group && x.LanguageId == y.LanguageId; } public int GetHashCode(ITag obj) { unchecked { - return (obj.Text.GetHashCode() * 397) ^ obj.Group.GetHashCode(); + var h = obj.Text.GetHashCode(); + h = h * 397 ^ obj.Group.GetHashCode(); + h = h * 397 ^ (obj.LanguageId?.GetHashCode() ?? 0); + return h; } } } @@ -264,118 +258,126 @@ WHERE r.tagId IS NULL"; // consider caching implications // add lookups for parentId or path (ie get content in tag group, that are descendants of x) + // ReSharper disable once ClassNeverInstantiated.Local + // ReSharper disable UnusedAutoPropertyAccessor.Local + private class TaggedEntityDto + { + public int NodeId { get; set; } + public string PropertyTypeAlias { get; set; } + public int PropertyTypeId { get; set; } + public int TagId { get; set; } + public string TagText { get; set; } + public string TagGroup { get; set; } + public int? TagLanguage { get; set; } + } + // ReSharper restore UnusedAutoPropertyAccessor.Local + + /// public TaggedEntity GetTaggedEntityByKey(Guid key) { - var sql = Sql() - .Select("cmsTagRelationship.nodeId, cmsPropertyType.Alias, cmsPropertyType.id as propertyTypeId, cmsTags.tag, cmsTags.id as tagId, cmsTags." + SqlSyntax.GetQuotedColumnName("group")) - .From() - .InnerJoin() - .On(left => left.TagId, right => right.Id) - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .InnerJoin() - .On(left => left.Id, right => right.PropertyTypeId) - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) + var sql = GetTaggedEntitiesSql(TaggableObjectTypes.All, "*"); + + sql = sql .Where(dto => dto.UniqueId == key); - return CreateTaggedEntityCollection(Database.Fetch(sql)).FirstOrDefault(); + return Map(Database.Fetch(sql)).FirstOrDefault(); } + /// public TaggedEntity GetTaggedEntityById(int id) { - var sql = Sql() - .Select("cmsTagRelationship.nodeId, cmsPropertyType.Alias, cmsPropertyType.id as propertyTypeId, cmsTags.tag, cmsTags.id as tagId, cmsTags." + SqlSyntax.GetQuotedColumnName("group")) - .From() - .InnerJoin() - .On(left => left.TagId, right => right.Id) - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .InnerJoin() - .On(left => left.Id, right => right.PropertyTypeId) - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) + var sql = GetTaggedEntitiesSql(TaggableObjectTypes.All, "*"); + + sql = sql .Where(dto => dto.NodeId == id); - return CreateTaggedEntityCollection(Database.Fetch(sql)).FirstOrDefault(); + return Map(Database.Fetch(sql)).FirstOrDefault(); } - public IEnumerable GetTaggedEntitiesByTagGroup(TaggableObjectTypes objectType, string tagGroup) + /// + public IEnumerable GetTaggedEntitiesByTagGroup(TaggableObjectTypes objectType, string group, string culture = null) { - var sql = Sql() - .Select("cmsTagRelationship.nodeId, cmsPropertyType.Alias, cmsPropertyType.id as propertyTypeId, cmsTags.tag, cmsTags.id as tagId, cmsTags." + SqlSyntax.GetQuotedColumnName("group")) - .From() - .InnerJoin() - .On(left => left.TagId, right => right.Id) - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .InnerJoin() - .On(left => left.Id, right => right.PropertyTypeId) - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .Where(dto => dto.Group == tagGroup); + var sql = GetTaggedEntitiesSql(objectType, culture); - if (objectType != TaggableObjectTypes.All) - { - var nodeObjectType = GetNodeObjectType(objectType); - sql = sql - .Where(dto => dto.NodeObjectType == nodeObjectType); - } + sql = sql + .Where(x => x.Group == group); - return CreateTaggedEntityCollection( - Database.Fetch(sql)); + return Map(Database.Fetch(sql)); } - public IEnumerable GetTaggedEntitiesByTag(TaggableObjectTypes objectType, string tag, string tagGroup = null) + /// + public IEnumerable GetTaggedEntitiesByTag(TaggableObjectTypes objectType, string tag, string group = null, string culture = null) { - var sql = Sql() - .Select("cmsTagRelationship.nodeId, cmsPropertyType.Alias, cmsPropertyType.id as propertyTypeId, cmsTags.tag, cmsTags.id as tagId, cmsTags." + SqlSyntax.GetQuotedColumnName("group")) - .From() - .InnerJoin() - .On(left => left.TagId, right => right.Id) - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .InnerJoin() - .On(left => left.Id, right => right.PropertyTypeId) - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) + var sql = GetTaggedEntitiesSql(objectType, culture); + + sql = sql .Where(dto => dto.Text == tag); + if (group.IsNullOrWhiteSpace() == false) + sql = sql + .Where(dto => dto.Group == group); + + return Map(Database.Fetch(sql)); + } + + private Sql GetTaggedEntitiesSql(TaggableObjectTypes objectType, string culture) + { + var sql = Sql() + .Select(x => Alias(x.NodeId, "NodeId")) + .AndSelect(x => Alias(x.Alias, "PropertyTypeAlias"), x => Alias(x.Id, "PropertyTypeId")) + .AndSelect(x => Alias(x.Id, "TagId"), x => Alias(x.Text, "TagText"), x => Alias(x.Group, "TagGroup"), x => Alias(x.LanguageId, "TagLanguage")) + .From() + .InnerJoin().On((tag, rel) => tag.Id == rel.TagId) + .InnerJoin().On((rel, content) => rel.NodeId == content.NodeId) + .InnerJoin().On((rel, prop) => rel.PropertyTypeId == prop.Id) + .InnerJoin().On((content, node) => content.NodeId == node.NodeId); + + if (culture == null) + { + sql = sql + .Where(dto => dto.LanguageId == null); + } + else if (culture != "*") + { + sql = sql + .InnerJoin().On((tag, lang) => tag.LanguageId == lang.Id) + .Where(x => x.IsoCode == culture); + } + if (objectType != TaggableObjectTypes.All) { var nodeObjectType = GetNodeObjectType(objectType); - sql = sql - .Where(dto => dto.NodeObjectType == nodeObjectType); + sql = sql.Where(dto => dto.NodeObjectType == nodeObjectType); } - if (tagGroup.IsNullOrWhiteSpace() == false) - { - sql = sql.Where(dto => dto.Group == tagGroup); - } - - return CreateTaggedEntityCollection( - Database.Fetch(sql)); + return sql; } - private IEnumerable CreateTaggedEntityCollection(IEnumerable dbResult) + private static IEnumerable Map(IEnumerable dtos) { - foreach (var node in dbResult.GroupBy(x => (int)x.nodeId)) + return dtos.GroupBy(x => x.NodeId).Select(dtosForNode => { - var properties = new List(); - foreach (var propertyType in node.GroupBy(x => new { id = (int)x.propertyTypeId, alias = (string)x.Alias })) + var taggedProperties = dtosForNode.GroupBy(x => x.PropertyTypeId).Select(dtosForProperty => { - var tags = propertyType.Select(x => new Tag((int)x.tagId, (string)x.group, (string)x.tag)); - properties.Add(new TaggedProperty(propertyType.Key.id, propertyType.Key.alias, tags)); - } - yield return new TaggedEntity(node.Key, properties); - } + string propertyTypeAlias = null; + var tags = dtosForProperty.Select(dto => + { + propertyTypeAlias = dto.PropertyTypeAlias; + return new Tag(dto.TagId, dto.TagGroup, dto.TagText, dto.TagLanguage); + }).ToList(); + return new TaggedProperty(dtosForProperty.Key, propertyTypeAlias, tags); + }).ToList(); + + return new TaggedEntity(dtosForNode.Key, taggedProperties); + }).ToList(); } - public IEnumerable GetTagsForEntityType(TaggableObjectTypes objectType, string group = null) + /// + public IEnumerable GetTagsForEntityType(TaggableObjectTypes objectType, string group = null, string culture = null) { - var sql = GetTagsQuerySelect(true); + var sql = GetTagsSql(culture, true); - sql = ApplyRelationshipJoinToTagsQuery(sql); + AddTagsSqlWhere(sql, culture); if (objectType != TaggableObjectTypes.All) { @@ -384,116 +386,126 @@ WHERE r.tagId IS NULL"; .Where(dto => dto.NodeObjectType == nodeObjectType); } - sql = ApplyGroupFilterToTagsQuery(sql, group); + if (group.IsNullOrWhiteSpace() == false) + sql = sql + .Where(dto => dto.Group == group); - sql = ApplyGroupByToTagsQuery(sql); + sql = sql + .GroupBy(x => x.Id, x => x.Text, x => x.Group, x => x.LanguageId); return ExecuteTagsQuery(sql); } - public IEnumerable GetTagsForEntity(int contentId, string group = null) + /// + public IEnumerable GetTagsForEntity(int contentId, string group = null, string culture = null) { - var sql = GetTagsQuerySelect(); + var sql = GetTagsSql(culture); - sql = ApplyRelationshipJoinToTagsQuery(sql); + AddTagsSqlWhere(sql, culture); sql = sql .Where(dto => dto.NodeId == contentId); - sql = ApplyGroupFilterToTagsQuery(sql, group); + if (group.IsNullOrWhiteSpace() == false) + sql = sql + .Where(dto => dto.Group == group); return ExecuteTagsQuery(sql); } - public IEnumerable GetTagsForEntity(Guid contentId, string group = null) + /// + public IEnumerable GetTagsForEntity(Guid contentId, string group = null, string culture = null) { - var sql = GetTagsQuerySelect(); + var sql = GetTagsSql(culture); - sql = ApplyRelationshipJoinToTagsQuery(sql); + AddTagsSqlWhere(sql, culture); sql = sql .Where(dto => dto.UniqueId == contentId); - sql = ApplyGroupFilterToTagsQuery(sql, group); + if (group.IsNullOrWhiteSpace() == false) + sql = sql + .Where(dto => dto.Group == group); return ExecuteTagsQuery(sql); } - public IEnumerable GetTagsForProperty(int contentId, string propertyTypeAlias, string group = null) + /// + public IEnumerable GetTagsForProperty(int contentId, string propertyTypeAlias, string group = null, string culture = null) { - var sql = GetTagsQuerySelect(); - - sql = ApplyRelationshipJoinToTagsQuery(sql); + var sql = GetTagsSql(culture); sql = sql - .InnerJoin() - .On(left => left.Id, right => right.PropertyTypeId) - .Where(dto => dto.NodeId == contentId) - .Where(dto => dto.Alias == propertyTypeAlias); + .InnerJoin().On((prop, rel) => prop.Id == rel.PropertyTypeId) + .Where(x => x.NodeId == contentId) + .Where(x => x.Alias == propertyTypeAlias); - sql = ApplyGroupFilterToTagsQuery(sql, group); + AddTagsSqlWhere(sql, culture); + + if (group.IsNullOrWhiteSpace() == false) + sql = sql + .Where(dto => dto.Group == group); return ExecuteTagsQuery(sql); } - public IEnumerable GetTagsForProperty(Guid contentId, string propertyTypeAlias, string group = null) + /// + public IEnumerable GetTagsForProperty(Guid contentId, string propertyTypeAlias, string group = null, string culture = null) { - var sql = GetTagsQuerySelect(); - - sql = ApplyRelationshipJoinToTagsQuery(sql); + var sql = GetTagsSql(culture); sql = sql - .InnerJoin() - .On(left => left.Id, right => right.PropertyTypeId) + .InnerJoin().On((prop, rel) => prop.Id == rel.PropertyTypeId) .Where(dto => dto.UniqueId == contentId) .Where(dto => dto.Alias == propertyTypeAlias); - sql = ApplyGroupFilterToTagsQuery(sql, group); + AddTagsSqlWhere(sql, culture); + + if (group.IsNullOrWhiteSpace() == false) + sql = sql + .Where(dto => dto.Group == group); return ExecuteTagsQuery(sql); } - private Sql GetTagsQuerySelect(bool withGrouping = false) + private Sql GetTagsSql(string culture, bool withGrouping = false) { - var sql = Sql(); + var sql = Sql() + .Select(); if (withGrouping) - { - sql = sql.Select("cmsTags.id, cmsTags.tag, cmsTags." + SqlSyntax.GetQuotedColumnName("group") + @", Count(*) NodeCount"); - } - else - { - sql = sql.Select("DISTINCT cmsTags.*"); - } + sql = sql + .AndSelectCount("NodeCount"); - return sql; - } - - private Sql ApplyRelationshipJoinToTagsQuery(Sql sql) - { - return sql + sql = sql .From() - .InnerJoin() - .On(left => left.TagId, right => right.Id) - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId); - } + .InnerJoin().On((rel, tag) => tag.Id == rel.TagId) + .InnerJoin().On((content, rel) => content.NodeId == rel.NodeId) + .InnerJoin().On((node, content) => node.NodeId == content.NodeId); - private Sql ApplyGroupFilterToTagsQuery(Sql sql, string group) - { - if (group.IsNullOrWhiteSpace() == false) + if (culture != null && culture != "*") { - sql = sql.Where(dto => dto.Group == group); + sql = sql + .InnerJoin().On((tag, lang) => tag.LanguageId == lang.Id); } return sql; } - private Sql ApplyGroupByToTagsQuery(Sql sql) + private Sql AddTagsSqlWhere(Sql sql, string culture) { - return sql.GroupBy("cmsTags.id", "cmsTags.tag", "cmsTags." + SqlSyntax.GetQuotedColumnName("group") + @""); + if (culture == null) + { + sql = sql + .Where(dto => dto.LanguageId == null); + } + else if (culture != "*") + { + sql = sql + .Where(x => x.IsoCode == culture); + } + + return sql; } private IEnumerable ExecuteTagsQuery(Sql sql) diff --git a/src/Umbraco.Core/PropertyEditors/PropertyEditorTagsExtensions.cs b/src/Umbraco.Core/PropertyEditors/PropertyEditorTagsExtensions.cs index 8f01bcab5a..3b2a9dd4e2 100644 --- a/src/Umbraco.Core/PropertyEditors/PropertyEditorTagsExtensions.cs +++ b/src/Umbraco.Core/PropertyEditors/PropertyEditorTagsExtensions.cs @@ -9,7 +9,7 @@ /// Determines whether an editor supports tags. /// public static bool IsTagsEditor(this IDataEditor editor) - => editor?.GetType().GetCustomAttribute(false) != null; + => editor.GetTagAttribute() != null; /// /// Gets the tags configuration attribute of an editor. diff --git a/src/Umbraco.Core/Services/ITagService.cs b/src/Umbraco.Core/Services/ITagService.cs index 63b7ce31a7..6be18624cb 100644 --- a/src/Umbraco.Core/Services/ITagService.cs +++ b/src/Umbraco.Core/Services/ITagService.cs @@ -5,7 +5,7 @@ using Umbraco.Core.Models; namespace Umbraco.Core.Services { /// - /// Tag service to query for tags in the tags db table. The tags returned are only relavent for published content & saved media or members + /// Tag service to query for tags in the tags db table. The tags returned are only relevant for published content & saved media or members /// /// /// If there is unpublished content with tags, those tags will not be contained. @@ -15,135 +15,84 @@ namespace Umbraco.Core.Services /// public interface ITagService : IService { - + /// + /// Gets a tagged entity. + /// TaggedEntity GetTaggedEntityById(int id); + + /// + /// Gets a tagged entity. + /// TaggedEntity GetTaggedEntityByKey(Guid key); /// - /// Gets tagged Content by a specific 'Tag Group'. + /// Gets all documents tagged with any tag in the specified group. /// - /// The contains the Id and Tags of the Content, not the actual Content item. - /// Name of the 'Tag Group' - /// An enumerable list of - IEnumerable GetTaggedContentByTagGroup(string tagGroup); + IEnumerable GetTaggedContentByTagGroup(string group, string culture = null); /// - /// Gets tagged Content by a specific 'Tag' and optional 'Tag Group'. + /// Gets all documents tagged with the specified tag. /// - /// The contains the Id and Tags of the Content, not the actual Content item. - /// Tag - /// Optional name of the 'Tag Group' - /// An enumerable list of - IEnumerable GetTaggedContentByTag(string tag, string tagGroup = null); + IEnumerable GetTaggedContentByTag(string tag, string group = null, string culture = null); /// - /// Gets tagged Media by a specific 'Tag Group'. + /// Gets all media tagged with any tag in the specified group. /// - /// The contains the Id and Tags of the Media, not the actual Media item. - /// Name of the 'Tag Group' - /// An enumerable list of - IEnumerable GetTaggedMediaByTagGroup(string tagGroup); + IEnumerable GetTaggedMediaByTagGroup(string group, string culture = null); /// - /// Gets tagged Media by a specific 'Tag' and optional 'Tag Group'. + /// Gets all media tagged with the specified tag. /// - /// The contains the Id and Tags of the Media, not the actual Media item. - /// Tag - /// Optional name of the 'Tag Group' - /// An enumerable list of - IEnumerable GetTaggedMediaByTag(string tag, string tagGroup = null); + IEnumerable GetTaggedMediaByTag(string tag, string group = null, string culture = null); /// - /// Gets tagged Members by a specific 'Tag Group'. + /// Gets all members tagged with any tag in the specified group. /// - /// The contains the Id and Tags of the Member, not the actual Member item. - /// Name of the 'Tag Group' - /// An enumerable list of - IEnumerable GetTaggedMembersByTagGroup(string tagGroup); + IEnumerable GetTaggedMembersByTagGroup(string group, string culture = null); /// - /// Gets tagged Members by a specific 'Tag' and optional 'Tag Group'. + /// Gets all members tagged with the specified tag. /// - /// The contains the Id and Tags of the Member, not the actual Member item. - /// Tag - /// Optional name of the 'Tag Group' - /// An enumerable list of - IEnumerable GetTaggedMembersByTag(string tag, string tagGroup = null); + IEnumerable GetTaggedMembersByTag(string tag, string group = null, string culture = null); /// - /// Gets every tag stored in the database + /// Gets all tags. /// - /// Optional name of the 'Tag Group' - /// An enumerable list of - IEnumerable GetAllTags(string tagGroup = null); + IEnumerable GetAllTags(string group = null, string culture = null); /// - /// Gets all tags for content items + /// Gets all document tags. /// - /// Use the optional tagGroup parameter to limit the - /// result to a specific 'Tag Group'. - /// Optional name of the 'Tag Group' - /// An enumerable list of - IEnumerable GetAllContentTags(string tagGroup = null); + IEnumerable GetAllContentTags(string group = null, string culture = null); /// - /// Gets all tags for media items + /// Gets all media tags. /// - /// Use the optional tagGroup parameter to limit the - /// result to a specific 'Tag Group'. - /// Optional name of the 'Tag Group' - /// An enumerable list of - IEnumerable GetAllMediaTags(string tagGroup = null); + IEnumerable GetAllMediaTags(string group = null, string culture = null); /// - /// Gets all tags for member items + /// Gets all member tags. /// - /// Use the optional tagGroup parameter to limit the - /// result to a specific 'Tag Group'. - /// Optional name of the 'Tag Group' - /// An enumerable list of - IEnumerable GetAllMemberTags(string tagGroup = null); + IEnumerable GetAllMemberTags(string group = null, string culture = null); /// - /// Gets all tags attached to a property by entity id + /// Gets all tags attached to an entity via a property. /// - /// Use the optional tagGroup parameter to limit the - /// result to a specific 'Tag Group'. - /// The content item id to get tags for - /// Property type alias - /// Optional name of the 'Tag Group' - /// An enumerable list of - IEnumerable GetTagsForProperty(int contentId, string propertyTypeAlias, string tagGroup = null); + IEnumerable GetTagsForProperty(int contentId, string propertyTypeAlias, string group = null, string culture = null); /// - /// Gets all tags attached to an entity (content, media or member) by entity id + /// Gets all tags attached to an entity. /// - /// Use the optional tagGroup parameter to limit the - /// result to a specific 'Tag Group'. - /// The content item id to get tags for - /// Optional name of the 'Tag Group' - /// An enumerable list of - IEnumerable GetTagsForEntity(int contentId, string tagGroup = null); + IEnumerable GetTagsForEntity(int contentId, string group = null, string culture = null); /// - /// Gets all tags attached to a property by entity id + /// Gets all tags attached to an entity via a property. /// - /// Use the optional tagGroup parameter to limit the - /// result to a specific 'Tag Group'. - /// The content item id to get tags for - /// Property type alias - /// Optional name of the 'Tag Group' - /// An enumerable list of - IEnumerable GetTagsForProperty(Guid contentId, string propertyTypeAlias, string tagGroup = null); + IEnumerable GetTagsForProperty(Guid contentId, string propertyTypeAlias, string group = null, string culture = null); /// - /// Gets all tags attached to an entity (content, media or member) by entity id + /// Gets all tags attached to an entity. /// - /// Use the optional tagGroup parameter to limit the - /// result to a specific 'Tag Group'. - /// The content item id to get tags for - /// Optional name of the 'Tag Group' - /// An enumerable list of - IEnumerable GetTagsForEntity(Guid contentId, string tagGroup = null); + IEnumerable GetTagsForEntity(Guid contentId, string group = null, string culture = null); } } diff --git a/src/Umbraco.Core/Services/Implement/TagService.cs b/src/Umbraco.Core/Services/Implement/TagService.cs index b2395502dc..e888258067 100644 --- a/src/Umbraco.Core/Services/Implement/TagService.cs +++ b/src/Umbraco.Core/Services/Implement/TagService.cs @@ -25,230 +25,147 @@ namespace Umbraco.Core.Services.Implement _tagRepository = tagRepository; } + /// public TaggedEntity GetTaggedEntityById(int id) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (ScopeProvider.CreateScope(autoComplete: true)) { return _tagRepository.GetTaggedEntityById(id); } } + /// public TaggedEntity GetTaggedEntityByKey(Guid key) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (ScopeProvider.CreateScope(autoComplete: true)) { return _tagRepository.GetTaggedEntityByKey(key); } } - /// - /// Gets tagged Content by a specific 'Tag Group'. - /// - /// The contains the Id and Tags of the Content, not the actual Content item. - /// Name of the 'Tag Group' - /// An enumerable list of - public IEnumerable GetTaggedContentByTagGroup(string tagGroup) + /// + public IEnumerable GetTaggedContentByTagGroup(string group, string culture = null) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (ScopeProvider.CreateScope(autoComplete: true)) { - return _tagRepository.GetTaggedEntitiesByTagGroup(TaggableObjectTypes.Content, tagGroup); + return _tagRepository.GetTaggedEntitiesByTagGroup(TaggableObjectTypes.Content, group, culture); } } - /// - /// Gets tagged Content by a specific 'Tag' and optional 'Tag Group'. - /// - /// The contains the Id and Tags of the Content, not the actual Content item. - /// Tag - /// Optional name of the 'Tag Group' - /// An enumerable list of - public IEnumerable GetTaggedContentByTag(string tag, string tagGroup = null) + /// + public IEnumerable GetTaggedContentByTag(string tag, string group = null, string culture = null) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (ScopeProvider.CreateScope(autoComplete: true)) { - return _tagRepository.GetTaggedEntitiesByTag(TaggableObjectTypes.Content, tag, tagGroup); + return _tagRepository.GetTaggedEntitiesByTag(TaggableObjectTypes.Content, tag, group, culture); } } - /// - /// Gets tagged Media by a specific 'Tag Group'. - /// - /// The contains the Id and Tags of the Media, not the actual Media item. - /// Name of the 'Tag Group' - /// An enumerable list of - public IEnumerable GetTaggedMediaByTagGroup(string tagGroup) + /// + public IEnumerable GetTaggedMediaByTagGroup(string group, string culture = null) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (ScopeProvider.CreateScope(autoComplete: true)) { - return _tagRepository.GetTaggedEntitiesByTagGroup(TaggableObjectTypes.Media, tagGroup); + return _tagRepository.GetTaggedEntitiesByTagGroup(TaggableObjectTypes.Media, group, culture); } } - /// - /// Gets tagged Media by a specific 'Tag' and optional 'Tag Group'. - /// - /// The contains the Id and Tags of the Media, not the actual Media item. - /// Tag - /// Optional name of the 'Tag Group' - /// An enumerable list of - public IEnumerable GetTaggedMediaByTag(string tag, string tagGroup = null) + /// + public IEnumerable GetTaggedMediaByTag(string tag, string group = null, string culture = null) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (ScopeProvider.CreateScope(autoComplete: true)) { - return _tagRepository.GetTaggedEntitiesByTag(TaggableObjectTypes.Media, tag, tagGroup); + return _tagRepository.GetTaggedEntitiesByTag(TaggableObjectTypes.Media, tag, group, culture); } } - /// - /// Gets tagged Members by a specific 'Tag Group'. - /// - /// The contains the Id and Tags of the Member, not the actual Member item. - /// Name of the 'Tag Group' - /// An enumerable list of - public IEnumerable GetTaggedMembersByTagGroup(string tagGroup) + /// + public IEnumerable GetTaggedMembersByTagGroup(string group, string culture = null) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (ScopeProvider.CreateScope(autoComplete: true)) { - return _tagRepository.GetTaggedEntitiesByTagGroup(TaggableObjectTypes.Member, tagGroup); + return _tagRepository.GetTaggedEntitiesByTagGroup(TaggableObjectTypes.Member, group, culture); } } - /// - /// Gets tagged Members by a specific 'Tag' and optional 'Tag Group'. - /// - /// The contains the Id and Tags of the Member, not the actual Member item. - /// Tag - /// Optional name of the 'Tag Group' - /// An enumerable list of - public IEnumerable GetTaggedMembersByTag(string tag, string tagGroup = null) + /// + public IEnumerable GetTaggedMembersByTag(string tag, string group = null, string culture = null) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (ScopeProvider.CreateScope(autoComplete: true)) { - return _tagRepository.GetTaggedEntitiesByTag(TaggableObjectTypes.Member, tag, tagGroup); + return _tagRepository.GetTaggedEntitiesByTag(TaggableObjectTypes.Member, tag, group, culture); } } - /// - /// Gets every tag stored in the database - /// - /// Optional name of the 'Tag Group' - /// An enumerable list of - public IEnumerable GetAllTags(string tagGroup = null) + /// + public IEnumerable GetAllTags(string group = null, string culture = null) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (ScopeProvider.CreateScope(autoComplete: true)) { - return _tagRepository.GetTagsForEntityType(TaggableObjectTypes.All, tagGroup); + return _tagRepository.GetTagsForEntityType(TaggableObjectTypes.All, group, culture); } } - /// - /// Gets all tags for content items - /// - /// Use the optional tagGroup parameter to limit the - /// result to a specific 'Tag Group'. - /// Optional name of the 'Tag Group' - /// An enumerable list of - public IEnumerable GetAllContentTags(string tagGroup = null) + /// + public IEnumerable GetAllContentTags(string group = null, string culture = null) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (ScopeProvider.CreateScope(autoComplete: true)) { - return _tagRepository.GetTagsForEntityType(TaggableObjectTypes.Content, tagGroup); + return _tagRepository.GetTagsForEntityType(TaggableObjectTypes.Content, group, culture); } } - /// - /// Gets all tags for media items - /// - /// Use the optional tagGroup parameter to limit the - /// result to a specific 'Tag Group'. - /// Optional name of the 'Tag Group' - /// An enumerable list of - public IEnumerable GetAllMediaTags(string tagGroup = null) + /// + public IEnumerable GetAllMediaTags(string group = null, string culture = null) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (ScopeProvider.CreateScope(autoComplete: true)) { - return _tagRepository.GetTagsForEntityType(TaggableObjectTypes.Media, tagGroup); + return _tagRepository.GetTagsForEntityType(TaggableObjectTypes.Media, group, culture); } } - /// - /// Gets all tags for member items - /// - /// Use the optional tagGroup parameter to limit the - /// result to a specific 'Tag Group'. - /// Optional name of the 'Tag Group' - /// An enumerable list of - public IEnumerable GetAllMemberTags(string tagGroup = null) + /// + public IEnumerable GetAllMemberTags(string group = null, string culture = null) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (ScopeProvider.CreateScope(autoComplete: true)) { - return _tagRepository.GetTagsForEntityType(TaggableObjectTypes.Member, tagGroup); + return _tagRepository.GetTagsForEntityType(TaggableObjectTypes.Member, group, culture); } } - /// - /// Gets all tags attached to a property by entity id - /// - /// Use the optional tagGroup parameter to limit the - /// result to a specific 'Tag Group'. - /// The content item id to get tags for - /// Property type alias - /// Optional name of the 'Tag Group' - /// An enumerable list of - public IEnumerable GetTagsForProperty(int contentId, string propertyTypeAlias, string tagGroup = null) + /// + public IEnumerable GetTagsForProperty(int contentId, string propertyTypeAlias, string group = null, string culture = null) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (ScopeProvider.CreateScope(autoComplete: true)) { - return _tagRepository.GetTagsForProperty(contentId, propertyTypeAlias, tagGroup); + return _tagRepository.GetTagsForProperty(contentId, propertyTypeAlias, group, culture); } } - /// - /// Gets all tags attached to an entity (content, media or member) by entity id - /// - /// Use the optional tagGroup parameter to limit the - /// result to a specific 'Tag Group'. - /// The content item id to get tags for - /// Optional name of the 'Tag Group' - /// An enumerable list of - public IEnumerable GetTagsForEntity(int contentId, string tagGroup = null) + /// + public IEnumerable GetTagsForEntity(int contentId, string group = null, string culture = null) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (ScopeProvider.CreateScope(autoComplete: true)) { - return _tagRepository.GetTagsForEntity(contentId, tagGroup); + return _tagRepository.GetTagsForEntity(contentId, group, culture); } } - /// - /// Gets all tags attached to a property by entity id - /// - /// Use the optional tagGroup parameter to limit the - /// result to a specific 'Tag Group'. - /// The content item id to get tags for - /// Property type alias - /// Optional name of the 'Tag Group' - /// An enumerable list of - public IEnumerable GetTagsForProperty(Guid contentId, string propertyTypeAlias, string tagGroup = null) + /// + public IEnumerable GetTagsForProperty(Guid contentId, string propertyTypeAlias, string group = null, string culture = null) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (ScopeProvider.CreateScope(autoComplete: true)) { - return _tagRepository.GetTagsForProperty(contentId, propertyTypeAlias, tagGroup); + return _tagRepository.GetTagsForProperty(contentId, propertyTypeAlias, group, culture); } } - /// - /// Gets all tags attached to an entity (content, media or member) by entity id - /// - /// Use the optional tagGroup parameter to limit the - /// result to a specific 'Tag Group'. - /// The content item id to get tags for - /// Optional name of the 'Tag Group' - /// An enumerable list of - public IEnumerable GetTagsForEntity(Guid contentId, string tagGroup = null) + /// + public IEnumerable GetTagsForEntity(Guid contentId, string group = null, string culture = null) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (ScopeProvider.CreateScope(autoComplete: true)) { - return _tagRepository.GetTagsForEntity(contentId, tagGroup); + return _tagRepository.GetTagsForEntity(contentId, group, culture); } } } diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index e46fca3c99..31014753af 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -363,6 +363,7 @@ + diff --git a/src/Umbraco.Tests/Composing/TypeLoaderTests.cs b/src/Umbraco.Tests/Composing/TypeLoaderTests.cs index 07625db9bf..7b2f9766f9 100644 --- a/src/Umbraco.Tests/Composing/TypeLoaderTests.cs +++ b/src/Umbraco.Tests/Composing/TypeLoaderTests.cs @@ -22,6 +22,7 @@ namespace Umbraco.Tests.Composing public class TypeLoaderTests { private TypeLoader _typeLoader; + [SetUp] public void Initialize() { @@ -53,6 +54,12 @@ namespace Umbraco.Tests.Composing public void TearDown() { _typeLoader = null; + + + // cleanup + var assDir = new FileInfo(Assembly.GetExecutingAssembly().Location).Directory; + foreach (var d in Directory.GetDirectories(Path.Combine(assDir.FullName, "TypeLoader"))) + Directory.Delete(d, true); } private DirectoryInfo PrepareFolder() diff --git a/src/Umbraco.Tests/Services/ContentServiceTagsTests.cs b/src/Umbraco.Tests/Services/ContentServiceTagsTests.cs new file mode 100644 index 0000000000..c05bde4c7c --- /dev/null +++ b/src/Umbraco.Tests/Services/ContentServiceTagsTests.cs @@ -0,0 +1,545 @@ +using System; +using System.Linq; +using LightInject; +using NUnit.Framework; +using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Core.Persistence.Repositories.Implement; +using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Services; +using Umbraco.Tests.TestHelpers.Entities; +using Umbraco.Tests.Testing; + +namespace Umbraco.Tests.Services +{ + [TestFixture] + [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest, + PublishedRepositoryEvents = true, + WithApplication = true, + Logger = UmbracoTestOptions.Logger.Console)] + public class ContentServiceTagsTests : TestWithSomeContentBase + { + public override void SetUp() + { + base.SetUp(); + ContentRepositoryBase.ThrowOnWarning = true; + } + + public override void TearDown() + { + ContentRepositoryBase.ThrowOnWarning = false; + base.TearDown(); + } + + protected override void Compose() + { + base.Compose(); + + // fixme - do it differently + Container.Register(factory => factory.GetInstance().TextService); + } + + [Test] + public void TagsCanBeInvariant() + { + var contentService = ServiceContext.ContentService; + var contentTypeService = ServiceContext.ContentTypeService; + var tagService = ServiceContext.TagService; + var contentType = MockedContentTypes.CreateSimpleContentType("umbMandatory", "Mandatory Doc Type", true); + contentType.PropertyGroups.First().PropertyTypes.Add( + new PropertyType("test", ValueStorageType.Ntext, "tags") + { + DataTypeId = 1041 + }); + contentTypeService.Save(contentType); + + IContent content1 = MockedContent.CreateSimpleContent(contentType, "Tagged content 1", -1); + content1.AssignTags("tags", new[] { "hello", "world", "another", "one" }); + contentService.SaveAndPublish(content1); + + content1 = contentService.GetById(content1.Id); + + var enTags = content1.Properties["tags"].GetTagsValue().ToArray(); + Assert.AreEqual(4, enTags.Length); + Assert.Contains("one", enTags); + Assert.AreEqual(-1, enTags.IndexOf("plus")); + + var tagGroups = tagService.GetAllTags().GroupBy(x => x.LanguageId); + foreach (var tag in tagService.GetAllTags()) + Console.WriteLine($"{tag.Group}:{tag.Text} {tag.LanguageId}"); + Assert.AreEqual(1, tagGroups.Count()); + var enTagGroup = tagGroups.FirstOrDefault(x => x.Key == null); + Assert.IsNotNull(enTagGroup); + Assert.AreEqual(4, enTagGroup.Count()); + Assert.IsTrue(enTagGroup.Any(x => x.Text == "one")); + Assert.IsFalse(enTagGroup.Any(x => x.Text == "plus")); + } + + [Test] + public void TagsCanBeVariant() + { + var languageService = ServiceContext.LocalizationService; + languageService.Save(new Language("fr-FR")); // en-US is already there + + var contentService = ServiceContext.ContentService; + var contentTypeService = ServiceContext.ContentTypeService; + var tagService = ServiceContext.TagService; + var contentType = MockedContentTypes.CreateSimpleContentType("umbMandatory", "Mandatory Doc Type", true); + contentType.PropertyGroups.First().PropertyTypes.Add( + new PropertyType("test", ValueStorageType.Ntext, "tags") + { + DataTypeId = 1041, + Variations = ContentVariation.Culture + }); + contentType.Variations = ContentVariation.Culture; + contentTypeService.Save(contentType); + + IContent content1 = MockedContent.CreateSimpleContent(contentType, "Tagged content 1", -1); + content1.SetCultureName("name-fr", "fr-FR"); + content1.SetCultureName("name-en", "en-US"); + content1.AssignTags("tags", new[] { "hello", "world", "some", "tags", "plus" }, culture: "fr-FR"); + content1.AssignTags("tags", new[] { "hello", "world", "another", "one" }, culture: "en-US"); + contentService.SaveAndPublish(content1); + + content1 = contentService.GetById(content1.Id); + + var frTags = content1.Properties["tags"].GetTagsValue("fr-FR").ToArray(); + Assert.AreEqual(5, frTags.Length); + Assert.Contains("plus", frTags); + Assert.AreEqual(-1, frTags.IndexOf("one")); + + var enTags = content1.Properties["tags"].GetTagsValue("en-US").ToArray(); + Assert.AreEqual(4, enTags.Length); + Assert.Contains("one", enTags); + Assert.AreEqual(-1, enTags.IndexOf("plus")); + + var tagGroups = tagService.GetAllTags(culture:"*").GroupBy(x => x.LanguageId); + foreach (var tag in tagService.GetAllTags()) + Console.WriteLine($"{tag.Group}:{tag.Text} {tag.LanguageId}"); + Assert.AreEqual(2, tagGroups.Count()); + var frTagGroup = tagGroups.FirstOrDefault(x => x.Key == 2); + Assert.IsNotNull(frTagGroup); + Assert.AreEqual(5, frTagGroup.Count()); + Assert.IsTrue(frTagGroup.Any(x => x.Text == "plus")); + Assert.IsFalse(frTagGroup.Any(x => x.Text == "one")); + var enTagGroup = tagGroups.FirstOrDefault(x => x.Key == 1); + Assert.IsNotNull(enTagGroup); + Assert.AreEqual(4, enTagGroup.Count()); + Assert.IsTrue(enTagGroup.Any(x => x.Text == "one")); + Assert.IsFalse(enTagGroup.Any(x => x.Text == "plus")); + } + + [Test] + public void TagsAreUpdatedWhenContentIsTrashedAndUnTrashed_One() + { + var contentService = ServiceContext.ContentService; + var contentTypeService = ServiceContext.ContentTypeService; + var tagService = ServiceContext.TagService; + var contentType = MockedContentTypes.CreateSimpleContentType("umbMandatory", "Mandatory Doc Type", true); + contentType.PropertyGroups.First().PropertyTypes.Add( + new PropertyType("test", ValueStorageType.Ntext, "tags") + { + DataTypeId = 1041 + }); + contentTypeService.Save(contentType); + + var content1 = MockedContent.CreateSimpleContent(contentType, "Tagged content 1", -1); + content1.AssignTags("tags", new[] { "hello", "world", "some", "tags", "plus" }); + contentService.SaveAndPublish(content1); + + var content2 = MockedContent.CreateSimpleContent(contentType, "Tagged content 2", -1); + content2.AssignTags("tags", new[] { "hello", "world", "some", "tags" }); + contentService.SaveAndPublish(content2); + + // verify + var tags = tagService.GetTagsForEntity(content1.Id); + Assert.AreEqual(5, tags.Count()); + var allTags = tagService.GetAllContentTags(); + Assert.AreEqual(5, allTags.Count()); + + contentService.MoveToRecycleBin(content1); + } + + [Test] + public void TagsAreUpdatedWhenContentIsTrashedAndUnTrashed_All() + { + var contentService = ServiceContext.ContentService; + var contentTypeService = ServiceContext.ContentTypeService; + var tagService = ServiceContext.TagService; + var contentType = MockedContentTypes.CreateSimpleContentType("umbMandatory", "Mandatory Doc Type", true); + contentType.PropertyGroups.First().PropertyTypes.Add( + new PropertyType("test", ValueStorageType.Ntext, "tags") + { + DataTypeId = 1041 + }); + contentTypeService.Save(contentType); + + var content1 = MockedContent.CreateSimpleContent(contentType, "Tagged content 1", -1); + content1.AssignTags("tags", new[] { "hello", "world", "some", "tags", "bam" }); + contentService.SaveAndPublish(content1); + + var content2 = MockedContent.CreateSimpleContent(contentType, "Tagged content 2", -1); + content2.AssignTags("tags", new[] { "hello", "world", "some", "tags" }); + contentService.SaveAndPublish(content2); + + // verify + var tags = tagService.GetTagsForEntity(content1.Id); + Assert.AreEqual(5, tags.Count()); + var allTags = tagService.GetAllContentTags(); + Assert.AreEqual(5, allTags.Count()); + + contentService.Unpublish(content1); + contentService.Unpublish(content2); + } + + [Test] + [Ignore("U4-8442, will need to be fixed eventually.")] + public void TagsAreUpdatedWhenContentIsTrashedAndUnTrashed_Tree() + { + var contentService = ServiceContext.ContentService; + var contentTypeService = ServiceContext.ContentTypeService; + var tagService = ServiceContext.TagService; + var contentType = MockedContentTypes.CreateSimpleContentType("umbMandatory", "Mandatory Doc Type", true); + contentType.PropertyGroups.First().PropertyTypes.Add( + new PropertyType("test", ValueStorageType.Ntext, "tags") + { + DataTypeId = 1041 + }); + contentTypeService.Save(contentType); + + var content1 = MockedContent.CreateSimpleContent(contentType, "Tagged content 1", -1); + content1.AssignTags("tags", new[] { "hello", "world", "some", "tags", "plus" }); + content1.PublishCulture(); + contentService.SaveAndPublish(content1); + + var content2 = MockedContent.CreateSimpleContent(contentType, "Tagged content 2", content1.Id); + content2.AssignTags("tags", new[] { "hello", "world", "some", "tags" }); + content2.PublishCulture(); + contentService.SaveAndPublish(content2); + + // verify + var tags = tagService.GetTagsForEntity(content1.Id); + Assert.AreEqual(5, tags.Count()); + var allTags = tagService.GetAllContentTags(); + Assert.AreEqual(5, allTags.Count()); + + contentService.MoveToRecycleBin(content1); + + // no more tags + tags = tagService.GetTagsForEntity(content1.Id); + Assert.AreEqual(0, tags.Count()); + tags = tagService.GetTagsForEntity(content2.Id); + Assert.AreEqual(0, tags.Count()); + + // no more tags + allTags = tagService.GetAllContentTags(); + Assert.AreEqual(0, allTags.Count()); + + contentService.Move(content1, -1); + + Assert.IsFalse(content1.Published); + + // no more tags + tags = tagService.GetTagsForEntity(content1.Id); + Assert.AreEqual(0, tags.Count()); + tags = tagService.GetTagsForEntity(content2.Id); + Assert.AreEqual(0, tags.Count()); + + // no more tags + allTags = tagService.GetAllContentTags(); + Assert.AreEqual(0, allTags.Count()); + + content1.PublishCulture(); + contentService.SaveAndPublish(content1); + + Assert.IsTrue(content1.Published); + + // tags are back + tags = tagService.GetTagsForEntity(content1.Id); + Assert.AreEqual(5, tags.Count()); + + // fixme tag & tree issue + // when we publish, we 'just' publish the top one and not the ones below = fails + // what we should do is... NOT clear tags when unpublishing or trashing or... + // and just update the tag service to NOT return anything related to trashed or + // unpublished entities (since trashed is set on ALL entities in the trashed branch) + tags = tagService.GetTagsForEntity(content2.Id); // including that one! + Assert.AreEqual(4, tags.Count()); + + // tags are back + allTags = tagService.GetAllContentTags(); + Assert.AreEqual(5, allTags.Count()); + } + + [Test] + public void TagsAreUpdatedWhenContentIsUnpublishedAndRePublished() + { + var contentService = ServiceContext.ContentService; + var contentTypeService = ServiceContext.ContentTypeService; + var tagService = ServiceContext.TagService; + var contentType = MockedContentTypes.CreateSimpleContentType("umbMandatory", "Mandatory Doc Type", true); + contentType.PropertyGroups.First().PropertyTypes.Add( + new PropertyType("test", ValueStorageType.Ntext, "tags") + { + DataTypeId = 1041 + }); + contentTypeService.Save(contentType); + + var content1 = MockedContent.CreateSimpleContent(contentType, "Tagged content 1", -1); + content1.AssignTags("tags", new[] { "hello", "world", "some", "tags", "bam" }); + contentService.SaveAndPublish(content1); + + var content2 = MockedContent.CreateSimpleContent(contentType, "Tagged content 2", -1); + content2.AssignTags("tags", new[] { "hello", "world", "some", "tags" }); + contentService.SaveAndPublish(content2); + + contentService.Unpublish(content1); + contentService.Unpublish(content2); + } + + [Test] + [Ignore("U4-8442, will need to be fixed eventually.")] + public void TagsAreUpdatedWhenContentIsUnpublishedAndRePublished_Tree() + { + var contentService = ServiceContext.ContentService; + var contentTypeService = ServiceContext.ContentTypeService; + var tagService = ServiceContext.TagService; + var contentType = MockedContentTypes.CreateSimpleContentType("umbMandatory", "Mandatory Doc Type", true); + contentType.PropertyGroups.First().PropertyTypes.Add( + new PropertyType("test", ValueStorageType.Ntext, "tags") + { + DataTypeId = 1041 + }); + contentTypeService.Save(contentType); + + var content1 = MockedContent.CreateSimpleContent(contentType, "Tagged content 1", -1); + content1.AssignTags("tags", new[] { "hello", "world", "some", "tags", "bam" }); + content1.PublishCulture(); + contentService.SaveAndPublish(content1); + + var content2 = MockedContent.CreateSimpleContent(contentType, "Tagged content 2", content1); + content2.AssignTags("tags", new[] { "hello", "world", "some", "tags" }); + content2.PublishCulture(); + contentService.SaveAndPublish(content2); + + contentService.Unpublish(content1); + + var tags = tagService.GetTagsForEntity(content1.Id); + Assert.AreEqual(0, tags.Count()); + + // fixme tag & tree issue + // when we (un)publish, we 'just' publish the top one and not the ones below = fails + // see similar note above + tags = tagService.GetTagsForEntity(content2.Id); + Assert.AreEqual(0, tags.Count()); + var allTags = tagService.GetAllContentTags(); + Assert.AreEqual(0, allTags.Count()); + + content1.PublishCulture(); + contentService.SaveAndPublish(content1); + + tags = tagService.GetTagsForEntity(content2.Id); + Assert.AreEqual(4, tags.Count()); + allTags = tagService.GetAllContentTags(); + Assert.AreEqual(5, allTags.Count()); + } + + [Test] + public void Create_Tag_Data_Bulk_Publish_Operation() + { + //Arrange + var contentService = ServiceContext.ContentService; + var contentTypeService = ServiceContext.ContentTypeService; + var dataTypeService = ServiceContext.DataTypeService; + + //set configuration + var dataType = dataTypeService.GetDataType(1041); + dataType.Configuration = new TagConfiguration + { + Group = "test", + StorageType = TagsStorageType.Csv + }; + + var contentType = MockedContentTypes.CreateSimpleContentType("umbMandatory", "Mandatory Doc Type", true); + contentType.PropertyGroups.First().PropertyTypes.Add( + new PropertyType("test", ValueStorageType.Ntext, "tags") + { + DataTypeId = 1041 + }); + contentTypeService.Save(contentType); + contentType.AllowedContentTypes = new[] { new ContentTypeSort(new Lazy(() => contentType.Id), 0, contentType.Alias) }; + + var content = MockedContent.CreateSimpleContent(contentType, "Tagged content", -1); + content.AssignTags("tags", new[] { "hello", "world", "some", "tags" }); + contentService.Save(content); + + var child1 = MockedContent.CreateSimpleContent(contentType, "child 1 content", content.Id); + child1.AssignTags("tags", new[] { "hello1", "world1", "some1" }); + contentService.Save(child1); + + var child2 = MockedContent.CreateSimpleContent(contentType, "child 2 content", content.Id); + child2.AssignTags("tags", new[] { "hello2", "world2" }); + contentService.Save(child2); + + // Act + contentService.SaveAndPublishBranch(content, true); + + // Assert + var propertyTypeId = contentType.PropertyTypes.Single(x => x.Alias == "tags").Id; + + using (var scope = ScopeProvider.CreateScope()) + { + Assert.AreEqual(4, scope.Database.ExecuteScalar( + "SELECT COUNT(*) FROM cmsTagRelationship WHERE nodeId=@nodeId AND propertyTypeId=@propTypeId", + new { nodeId = content.Id, propTypeId = propertyTypeId })); + + Assert.AreEqual(3, scope.Database.ExecuteScalar( + "SELECT COUNT(*) FROM cmsTagRelationship WHERE nodeId=@nodeId AND propertyTypeId=@propTypeId", + new { nodeId = child1.Id, propTypeId = propertyTypeId })); + + Assert.AreEqual(2, scope.Database.ExecuteScalar( + "SELECT COUNT(*) FROM cmsTagRelationship WHERE nodeId=@nodeId AND propertyTypeId=@propTypeId", + new { nodeId = child2.Id, propTypeId = propertyTypeId })); + + scope.Complete(); + } + } + + [Test] + public void Does_Not_Create_Tag_Data_For_Non_Published_Version() + { + var contentService = ServiceContext.ContentService; + var contentTypeService = ServiceContext.ContentTypeService; + + // create content type with a tag property + var contentType = MockedContentTypes.CreateSimpleContentType("umbMandatory", "Mandatory Doc Type", true); + contentType.PropertyGroups.First().PropertyTypes.Add(new PropertyType("test", ValueStorageType.Ntext, "tags") { DataTypeId = 1041 }); + contentTypeService.Save(contentType); + + // create a content with tags and publish + var content = MockedContent.CreateSimpleContent(contentType, "Tagged content", -1); + content.AssignTags("tags", new[] { "hello", "world", "some", "tags" }); + contentService.SaveAndPublish(content); + + // edit tags and save + content.AssignTags("tags", new[] { "another", "world" }, merge: true); + contentService.Save(content); + + // the (edit) property does contain all tags + Assert.AreEqual(5, content.Properties["tags"].GetValue().ToString().Split(',').Distinct().Count()); + + // but the database still contains the initial two tags + var propertyTypeId = contentType.PropertyTypes.Single(x => x.Alias == "tags").Id; + using (var scope = ScopeProvider.CreateScope()) + { + Assert.AreEqual(4, scope.Database.ExecuteScalar( + "SELECT COUNT(*) FROM cmsTagRelationship WHERE nodeId=@nodeId AND propertyTypeId=@propTypeId", + new { nodeId = content.Id, propTypeId = propertyTypeId })); + scope.Complete(); + } + } + + [Test] + public void Can_Replace_Tag_Data_To_Published_Content() + { + //Arrange + var contentService = ServiceContext.ContentService; + var contentTypeService = ServiceContext.ContentTypeService; + var contentType = MockedContentTypes.CreateSimpleContentType("umbMandatory", "Mandatory Doc Type", true); + contentType.PropertyGroups.First().PropertyTypes.Add( + new PropertyType("test", ValueStorageType.Ntext, "tags") + { + DataTypeId = 1041 + }); + contentTypeService.Save(contentType); + + var content = MockedContent.CreateSimpleContent(contentType, "Tagged content", -1); + + + // Act + content.AssignTags("tags", new[] { "hello", "world", "some", "tags" }); + contentService.SaveAndPublish(content); + + // Assert + Assert.AreEqual(4, content.Properties["tags"].GetValue().ToString().Split(',').Distinct().Count()); + var propertyTypeId = contentType.PropertyTypes.Single(x => x.Alias == "tags").Id; + using (var scope = ScopeProvider.CreateScope()) + { + Assert.AreEqual(4, scope.Database.ExecuteScalar( + "SELECT COUNT(*) FROM cmsTagRelationship WHERE nodeId=@nodeId AND propertyTypeId=@propTypeId", + new { nodeId = content.Id, propTypeId = propertyTypeId })); + + scope.Complete(); + } + } + + [Test] + public void Can_Append_Tag_Data_To_Published_Content() + { + //Arrange + var contentService = ServiceContext.ContentService; + var contentTypeService = ServiceContext.ContentTypeService; + var contentType = MockedContentTypes.CreateSimpleContentType("umbMandatory", "Mandatory Doc Type", true); + contentType.PropertyGroups.First().PropertyTypes.Add( + new PropertyType("test", ValueStorageType.Ntext, "tags") + { + DataTypeId = 1041 + }); + contentTypeService.Save(contentType); + var content = MockedContent.CreateSimpleContent(contentType, "Tagged content", -1); + content.AssignTags("tags", new[] { "hello", "world", "some", "tags" }); + contentService.SaveAndPublish(content); + + // Act + content.AssignTags("tags", new[] { "another", "world" }, merge: true); + contentService.SaveAndPublish(content); + + // Assert + Assert.AreEqual(5, content.Properties["tags"].GetValue().ToString().Split(',').Distinct().Count()); + var propertyTypeId = contentType.PropertyTypes.Single(x => x.Alias == "tags").Id; + using (var scope = ScopeProvider.CreateScope()) + { + Assert.AreEqual(5, scope.Database.ExecuteScalar( + "SELECT COUNT(*) FROM cmsTagRelationship WHERE nodeId=@nodeId AND propertyTypeId=@propTypeId", + new { nodeId = content.Id, propTypeId = propertyTypeId })); + + scope.Complete(); + } + } + + [Test] + public void Can_Remove_Tag_Data_To_Published_Content() + { + //Arrange + var contentService = ServiceContext.ContentService; + var contentTypeService = ServiceContext.ContentTypeService; + var contentType = MockedContentTypes.CreateSimpleContentType("umbMandatory", "Mandatory Doc Type", true); + contentType.PropertyGroups.First().PropertyTypes.Add( + new PropertyType("test", ValueStorageType.Ntext, "tags") + { + DataTypeId = 1041 + }); + contentTypeService.Save(contentType); + var content = MockedContent.CreateSimpleContent(contentType, "Tagged content", -1); + content.AssignTags("tags", new[] { "hello", "world", "some", "tags" }); + contentService.SaveAndPublish(content); + + // Act + content.RemoveTags("tags", new[] { "some", "world" }); + contentService.SaveAndPublish(content); + + // Assert + Assert.AreEqual(2, content.Properties["tags"].GetValue().ToString().Split(',').Distinct().Count()); + var propertyTypeId = contentType.PropertyTypes.Single(x => x.Alias == "tags").Id; + using (var scope = ScopeProvider.CreateScope()) + { + Assert.AreEqual(2, scope.Database.ExecuteScalar( + "SELECT COUNT(*) FROM cmsTagRelationship WHERE nodeId=@nodeId AND propertyTypeId=@propTypeId", + new { nodeId = content.Id, propTypeId = propertyTypeId })); + + scope.Complete(); + } + } + + } +} diff --git a/src/Umbraco.Tests/Services/ContentServiceTests.cs b/src/Umbraco.Tests/Services/ContentServiceTests.cs index 2c90ce90c5..494d12dbe1 100644 --- a/src/Umbraco.Tests/Services/ContentServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentServiceTests.cs @@ -193,63 +193,6 @@ namespace Umbraco.Tests.Services Assert.AreEqual(5, found.Length); } - /// - /// Ensures that we don't unpublish all nodes when a node is deleted that has an invalid path of -1 - /// Note: it is actually the MoveToRecycleBin happening on the initial deletion of a node through the UI - /// that causes the issue. - /// Regression test: http://issues.umbraco.org/issue/U4-9336 - /// - [Test] - [Ignore("not applicable to v8")] - - // fixme - this test was imported from 7.6 BUT it makes no sense for v8 - // we should trust the PATH, full stop - - public void Moving_Node_To_Recycle_Bin_With_Invalid_Path() - { - var contentService = ServiceContext.ContentService; - var root = ServiceContext.ContentService.GetById(NodeDto.NodeIdSeed + 1); - root.PublishCulture(); - Assert.IsTrue(contentService.SaveAndPublish(root).Success); - var content = contentService.CreateAndSave("Test", -1, "umbTextpage", Constants.Security.SuperUserId); - content.PublishCulture(); - Assert.IsTrue(contentService.SaveAndPublish(content).Success); - var hierarchy = CreateContentHierarchy().OrderBy(x => x.Level).ToArray(); - contentService.Save(hierarchy, Constants.Security.SuperUserId); - foreach (var c in hierarchy) - { - c.PublishCulture(); - Assert.IsTrue(contentService.SaveAndPublish(c).Success); - } - - //now make the data corrupted :/ - using (var scope = ScopeProvider.CreateScope()) - { - scope.Database.Execute("UPDATE umbracoNode SET path = '-1' WHERE id = @id", new { id = content.Id }); - scope.Complete(); - } - - //re-get with the corrupt path - content = contentService.GetById(content.Id); - - // here we get all descendants by the path of the node being moved to bin, and unpublish all of them. - // since the path is invalid, there's logic in here to fix that if it's possible and re-persist the entity. - var moveResult = ServiceContext.ContentService.MoveToRecycleBin(content); - - Assert.IsTrue(moveResult.Success); - - //re-get with the fixed/moved path - content = contentService.GetById(content.Id); - - Assert.AreEqual("-1,-20," + content.Id, content.Path); - - //re-get - hierarchy = contentService.GetByIds(hierarchy.Select(x => x.Id).ToArray()).OrderBy(x => x.Level).ToArray(); - - Assert.That(hierarchy.All(c => c.Trashed == false), Is.True); - Assert.That(hierarchy.All(c => c.Path.StartsWith("-1,-20") == false), Is.True); - } - [Test] public void Perform_Scheduled_Publishing() { @@ -501,511 +444,6 @@ namespace Umbraco.Tests.Services Assert.IsEmpty(res); } - [Test] - public void TagsAreUpdatedWhenContentIsTrashedAndUnTrashed_One() - { - var contentService = ServiceContext.ContentService; - var contentTypeService = ServiceContext.ContentTypeService; - var tagService = ServiceContext.TagService; - var contentType = MockedContentTypes.CreateSimpleContentType("umbMandatory", "Mandatory Doc Type", true); - contentType.PropertyGroups.First().PropertyTypes.Add( - new PropertyType("test", ValueStorageType.Ntext, "tags") - { - DataTypeId = 1041 - }); - contentTypeService.Save(contentType); - - var content1 = MockedContent.CreateSimpleContent(contentType, "Tagged content 1", -1); - content1.AssignTags("tags", new[] { "hello", "world", "some", "tags", "plus" }); - contentService.SaveAndPublish(content1); - - var content2 = MockedContent.CreateSimpleContent(contentType, "Tagged content 2", -1); - content2.AssignTags("tags", new[] { "hello", "world", "some", "tags" }); - contentService.SaveAndPublish(content2); - - // verify - var tags = tagService.GetTagsForEntity(content1.Id); - Assert.AreEqual(5, tags.Count()); - var allTags = tagService.GetAllContentTags(); - Assert.AreEqual(5, allTags.Count()); - - contentService.MoveToRecycleBin(content1); - - // fixme - killing the rest of this test - // this is not working consistently even in 7 when unpublishing a branch - // in 8, tags never go away - one has to check that the entity is published and not trashed - return; - - // no more tags for this entity - tags = tagService.GetTagsForEntity(content1.Id); - Assert.AreEqual(0, tags.Count()); - - // tags still assigned to content2 are still there - allTags = tagService.GetAllContentTags(); - Assert.AreEqual(4, allTags.Count()); - - contentService.Move(content1, -1); - - Assert.IsFalse(content1.Published); - - // no more tags for this entity - tags = tagService.GetTagsForEntity(content1.Id); - Assert.AreEqual(0, tags.Count()); - - // tags still assigned to content2 are still there - allTags = tagService.GetAllContentTags(); - Assert.AreEqual(4, allTags.Count()); - - content1.PublishCulture(); - contentService.SaveAndPublish(content1); - - Assert.IsTrue(content1.Published); - - // tags are back - tags = tagService.GetTagsForEntity(content1.Id); - Assert.AreEqual(5, tags.Count()); - - // tags are back - allTags = tagService.GetAllContentTags(); - Assert.AreEqual(5, allTags.Count()); - } - - [Test] - public void TagsAreUpdatedWhenContentIsTrashedAndUnTrashed_All() - { - var contentService = ServiceContext.ContentService; - var contentTypeService = ServiceContext.ContentTypeService; - var tagService = ServiceContext.TagService; - var contentType = MockedContentTypes.CreateSimpleContentType("umbMandatory", "Mandatory Doc Type", true); - contentType.PropertyGroups.First().PropertyTypes.Add( - new PropertyType("test", ValueStorageType.Ntext, "tags") - { - DataTypeId = 1041 - }); - contentTypeService.Save(contentType); - - var content1 = MockedContent.CreateSimpleContent(contentType, "Tagged content 1", -1); - content1.AssignTags("tags", new[] { "hello", "world", "some", "tags", "bam" }); - contentService.SaveAndPublish(content1); - - var content2 = MockedContent.CreateSimpleContent(contentType, "Tagged content 2", -1); - content2.AssignTags("tags", new[] { "hello", "world", "some", "tags" }); - contentService.SaveAndPublish(content2); - - // verify - var tags = tagService.GetTagsForEntity(content1.Id); - Assert.AreEqual(5, tags.Count()); - var allTags = tagService.GetAllContentTags(); - Assert.AreEqual(5, allTags.Count()); - - contentService.Unpublish(content1); - contentService.Unpublish(content2); - - // fixme - killing the rest of this test - // this is not working consistently even in 7 when unpublishing a branch - // in 8, tags never go away - one has to check that the entity is published and not trashed - return; - - // no more tags - tags = tagService.GetTagsForEntity(content1.Id); - Assert.AreEqual(0, tags.Count()); - - // no more tags - allTags = tagService.GetAllContentTags(); - Assert.AreEqual(0, allTags.Count()); - - contentService.Move(content1, -1); - contentService.Move(content2, -1); - - // no more tags - tags = tagService.GetTagsForEntity(content1.Id); - Assert.AreEqual(0, tags.Count()); - - // no more tags - allTags = tagService.GetAllContentTags(); - Assert.AreEqual(0, allTags.Count()); - - content1.PublishCulture(); - contentService.SaveAndPublish(content1); - content2.PublishCulture(); - contentService.SaveAndPublish(content2); - - // tags are back - tags = tagService.GetTagsForEntity(content1.Id); - Assert.AreEqual(5, tags.Count()); - - // tags are back - allTags = tagService.GetAllContentTags(); - Assert.AreEqual(5, allTags.Count()); - } - - [Test] - [Ignore("U4-8442, will need to be fixed eventually.")] - public void TagsAreUpdatedWhenContentIsTrashedAndUnTrashed_Tree() - { - var contentService = ServiceContext.ContentService; - var contentTypeService = ServiceContext.ContentTypeService; - var tagService = ServiceContext.TagService; - var contentType = MockedContentTypes.CreateSimpleContentType("umbMandatory", "Mandatory Doc Type", true); - contentType.PropertyGroups.First().PropertyTypes.Add( - new PropertyType("test", ValueStorageType.Ntext, "tags") - { - DataTypeId = 1041 - }); - contentTypeService.Save(contentType); - - var content1 = MockedContent.CreateSimpleContent(contentType, "Tagged content 1", -1); - content1.AssignTags("tags", new[] { "hello", "world", "some", "tags", "plus" }); - content1.PublishCulture(); - contentService.SaveAndPublish(content1); - - var content2 = MockedContent.CreateSimpleContent(contentType, "Tagged content 2", content1.Id); - content2.AssignTags("tags", new[] { "hello", "world", "some", "tags" }); - content2.PublishCulture(); - contentService.SaveAndPublish(content2); - - // verify - var tags = tagService.GetTagsForEntity(content1.Id); - Assert.AreEqual(5, tags.Count()); - var allTags = tagService.GetAllContentTags(); - Assert.AreEqual(5, allTags.Count()); - - contentService.MoveToRecycleBin(content1); - - // no more tags - tags = tagService.GetTagsForEntity(content1.Id); - Assert.AreEqual(0, tags.Count()); - tags = tagService.GetTagsForEntity(content2.Id); - Assert.AreEqual(0, tags.Count()); - - // no more tags - allTags = tagService.GetAllContentTags(); - Assert.AreEqual(0, allTags.Count()); - - contentService.Move(content1, -1); - - Assert.IsFalse(content1.Published); - - // no more tags - tags = tagService.GetTagsForEntity(content1.Id); - Assert.AreEqual(0, tags.Count()); - tags = tagService.GetTagsForEntity(content2.Id); - Assert.AreEqual(0, tags.Count()); - - // no more tags - allTags = tagService.GetAllContentTags(); - Assert.AreEqual(0, allTags.Count()); - - content1.PublishCulture(); - contentService.SaveAndPublish(content1); - - Assert.IsTrue(content1.Published); - - // tags are back - tags = tagService.GetTagsForEntity(content1.Id); - Assert.AreEqual(5, tags.Count()); - - // fixme tag & tree issue - // when we publish, we 'just' publish the top one and not the ones below = fails - // what we should do is... NOT clear tags when unpublishing or trashing or... - // and just update the tag service to NOT return anything related to trashed or - // unpublished entities (since trashed is set on ALL entities in the trashed branch) - tags = tagService.GetTagsForEntity(content2.Id); // including that one! - Assert.AreEqual(4, tags.Count()); - - // tags are back - allTags = tagService.GetAllContentTags(); - Assert.AreEqual(5, allTags.Count()); - } - - [Test] - public void TagsAreUpdatedWhenContentIsUnpublishedAndRePublished() - { - var contentService = ServiceContext.ContentService; - var contentTypeService = ServiceContext.ContentTypeService; - var tagService = ServiceContext.TagService; - var contentType = MockedContentTypes.CreateSimpleContentType("umbMandatory", "Mandatory Doc Type", true); - contentType.PropertyGroups.First().PropertyTypes.Add( - new PropertyType("test", ValueStorageType.Ntext, "tags") - { - DataTypeId = 1041 - }); - contentTypeService.Save(contentType); - - var content1 = MockedContent.CreateSimpleContent(contentType, "Tagged content 1", -1); - content1.AssignTags("tags", new[] { "hello", "world", "some", "tags", "bam" }); - contentService.SaveAndPublish(content1); - - var content2 = MockedContent.CreateSimpleContent(contentType, "Tagged content 2", -1); - content2.AssignTags("tags", new[] { "hello", "world", "some", "tags" }); - contentService.SaveAndPublish(content2); - - contentService.Unpublish(content1); - contentService.Unpublish(content2); - - // fixme - killing the rest of this test - // this is not working consistently even in 7 when unpublishing a branch - // in 8, tags never go away - one has to check that the entity is published and not trashed - return; - - var tags = tagService.GetTagsForEntity(content1.Id); - Assert.AreEqual(0, tags.Count()); - var allTags = tagService.GetAllContentTags(); - Assert.AreEqual(0, allTags.Count()); - - content2.PublishCulture(); - contentService.SaveAndPublish(content2); - - tags = tagService.GetTagsForEntity(content2.Id); - Assert.AreEqual(4, tags.Count()); - allTags = tagService.GetAllContentTags(); - Assert.AreEqual(4, allTags.Count()); - } - - [Test] - [Ignore("U4-8442, will need to be fixed eventually.")] - public void TagsAreUpdatedWhenContentIsUnpublishedAndRePublished_Tree() - { - var contentService = ServiceContext.ContentService; - var contentTypeService = ServiceContext.ContentTypeService; - var tagService = ServiceContext.TagService; - var contentType = MockedContentTypes.CreateSimpleContentType("umbMandatory", "Mandatory Doc Type", true); - contentType.PropertyGroups.First().PropertyTypes.Add( - new PropertyType("test", ValueStorageType.Ntext, "tags") - { - DataTypeId = 1041 - }); - contentTypeService.Save(contentType); - - var content1 = MockedContent.CreateSimpleContent(contentType, "Tagged content 1", -1); - content1.AssignTags("tags", new[] { "hello", "world", "some", "tags", "bam" }); - content1.PublishCulture(); - contentService.SaveAndPublish(content1); - - var content2 = MockedContent.CreateSimpleContent(contentType, "Tagged content 2", content1); - content2.AssignTags("tags", new[] { "hello", "world", "some", "tags" }); - content2.PublishCulture(); - contentService.SaveAndPublish(content2); - - contentService.Unpublish(content1); - - var tags = tagService.GetTagsForEntity(content1.Id); - Assert.AreEqual(0, tags.Count()); - - // fixme tag & tree issue - // when we (un)publish, we 'just' publish the top one and not the ones below = fails - // see similar note above - tags = tagService.GetTagsForEntity(content2.Id); - Assert.AreEqual(0, tags.Count()); - var allTags = tagService.GetAllContentTags(); - Assert.AreEqual(0, allTags.Count()); - - content1.PublishCulture(); - contentService.SaveAndPublish(content1); - - tags = tagService.GetTagsForEntity(content2.Id); - Assert.AreEqual(4, tags.Count()); - allTags = tagService.GetAllContentTags(); - Assert.AreEqual(5, allTags.Count()); - } - - [Test] - public void Create_Tag_Data_Bulk_Publish_Operation() - { - //Arrange - var contentService = ServiceContext.ContentService; - var contentTypeService = ServiceContext.ContentTypeService; - var dataTypeService = ServiceContext.DataTypeService; - - //set configuration - var dataType = dataTypeService.GetDataType(1041); - dataType.Configuration = new TagConfiguration - { - Group = "test", - StorageType = TagsStorageType.Csv - }; - - var contentType = MockedContentTypes.CreateSimpleContentType("umbMandatory", "Mandatory Doc Type", true); - contentType.PropertyGroups.First().PropertyTypes.Add( - new PropertyType("test", ValueStorageType.Ntext, "tags") - { - DataTypeId = 1041 - }); - contentTypeService.Save(contentType); - contentType.AllowedContentTypes = new[] { new ContentTypeSort(new Lazy(() => contentType.Id), 0, contentType.Alias) }; - - var content = MockedContent.CreateSimpleContent(contentType, "Tagged content", -1); - content.AssignTags("tags", new[] { "hello", "world", "some", "tags" }); - contentService.Save(content); - - var child1 = MockedContent.CreateSimpleContent(contentType, "child 1 content", content.Id); - child1.AssignTags("tags", new[] { "hello1", "world1", "some1" }); - contentService.Save(child1); - - var child2 = MockedContent.CreateSimpleContent(contentType, "child 2 content", content.Id); - child2.AssignTags("tags", new[] { "hello2", "world2" }); - contentService.Save(child2); - - // Act - contentService.SaveAndPublishBranch(content, true); - - // Assert - var propertyTypeId = contentType.PropertyTypes.Single(x => x.Alias == "tags").Id; - - using (var scope = ScopeProvider.CreateScope()) - { - Assert.AreEqual(4, scope.Database.ExecuteScalar( - "SELECT COUNT(*) FROM cmsTagRelationship WHERE nodeId=@nodeId AND propertyTypeId=@propTypeId", - new { nodeId = content.Id, propTypeId = propertyTypeId })); - - Assert.AreEqual(3, scope.Database.ExecuteScalar( - "SELECT COUNT(*) FROM cmsTagRelationship WHERE nodeId=@nodeId AND propertyTypeId=@propTypeId", - new { nodeId = child1.Id, propTypeId = propertyTypeId })); - - Assert.AreEqual(2, scope.Database.ExecuteScalar( - "SELECT COUNT(*) FROM cmsTagRelationship WHERE nodeId=@nodeId AND propertyTypeId=@propTypeId", - new { nodeId = child2.Id, propTypeId = propertyTypeId })); - - scope.Complete(); - } - } - - [Test] - public void Does_Not_Create_Tag_Data_For_Non_Published_Version() - { - var contentService = ServiceContext.ContentService; - var contentTypeService = ServiceContext.ContentTypeService; - - // create content type with a tag property - var contentType = MockedContentTypes.CreateSimpleContentType("umbMandatory", "Mandatory Doc Type", true); - contentType.PropertyGroups.First().PropertyTypes.Add(new PropertyType("test", ValueStorageType.Ntext, "tags") { DataTypeId = 1041 }); - contentTypeService.Save(contentType); - - // create a content with tags and publish - var content = MockedContent.CreateSimpleContent(contentType, "Tagged content", -1); - content.AssignTags("tags", new[] { "hello", "world", "some", "tags" }); - contentService.SaveAndPublish(content); - - // edit tags and save - content.AssignTags("tags", new[] { "another", "world" }, merge: true); - contentService.Save(content); - - // the (edit) property does contain all tags - Assert.AreEqual(5, content.Properties["tags"].GetValue().ToString().Split(',').Distinct().Count()); - - // but the database still contains the initial two tags - var propertyTypeId = contentType.PropertyTypes.Single(x => x.Alias == "tags").Id; - using (var scope = ScopeProvider.CreateScope()) - { - Assert.AreEqual(4, scope.Database.ExecuteScalar( - "SELECT COUNT(*) FROM cmsTagRelationship WHERE nodeId=@nodeId AND propertyTypeId=@propTypeId", - new { nodeId = content.Id, propTypeId = propertyTypeId })); - scope.Complete(); - } - } - - [Test] - public void Can_Replace_Tag_Data_To_Published_Content() - { - //Arrange - var contentService = ServiceContext.ContentService; - var contentTypeService = ServiceContext.ContentTypeService; - var contentType = MockedContentTypes.CreateSimpleContentType("umbMandatory", "Mandatory Doc Type", true); - contentType.PropertyGroups.First().PropertyTypes.Add( - new PropertyType("test", ValueStorageType.Ntext, "tags") - { - DataTypeId = 1041 - }); - contentTypeService.Save(contentType); - - var content = MockedContent.CreateSimpleContent(contentType, "Tagged content", -1); - - - // Act - content.AssignTags("tags", new[] { "hello", "world", "some", "tags" }); - contentService.SaveAndPublish(content); - - // Assert - Assert.AreEqual(4, content.Properties["tags"].GetValue().ToString().Split(',').Distinct().Count()); - var propertyTypeId = contentType.PropertyTypes.Single(x => x.Alias == "tags").Id; - using (var scope = ScopeProvider.CreateScope()) - { - Assert.AreEqual(4, scope.Database.ExecuteScalar( - "SELECT COUNT(*) FROM cmsTagRelationship WHERE nodeId=@nodeId AND propertyTypeId=@propTypeId", - new { nodeId = content.Id, propTypeId = propertyTypeId })); - - scope.Complete(); - } - } - - [Test] - public void Can_Append_Tag_Data_To_Published_Content() - { - //Arrange - var contentService = ServiceContext.ContentService; - var contentTypeService = ServiceContext.ContentTypeService; - var contentType = MockedContentTypes.CreateSimpleContentType("umbMandatory", "Mandatory Doc Type", true); - contentType.PropertyGroups.First().PropertyTypes.Add( - new PropertyType("test", ValueStorageType.Ntext, "tags") - { - DataTypeId = 1041 - }); - contentTypeService.Save(contentType); - var content = MockedContent.CreateSimpleContent(contentType, "Tagged content", -1); - content.AssignTags("tags", new[] { "hello", "world", "some", "tags" }); - contentService.SaveAndPublish(content); - - // Act - content.AssignTags("tags", new[] { "another", "world" }, merge: true); - contentService.SaveAndPublish(content); - - // Assert - Assert.AreEqual(5, content.Properties["tags"].GetValue().ToString().Split(',').Distinct().Count()); - var propertyTypeId = contentType.PropertyTypes.Single(x => x.Alias == "tags").Id; - using (var scope = ScopeProvider.CreateScope()) - { - Assert.AreEqual(5, scope.Database.ExecuteScalar( - "SELECT COUNT(*) FROM cmsTagRelationship WHERE nodeId=@nodeId AND propertyTypeId=@propTypeId", - new { nodeId = content.Id, propTypeId = propertyTypeId })); - - scope.Complete(); - } - } - - [Test] - public void Can_Remove_Tag_Data_To_Published_Content() - { - //Arrange - var contentService = ServiceContext.ContentService; - var contentTypeService = ServiceContext.ContentTypeService; - var contentType = MockedContentTypes.CreateSimpleContentType("umbMandatory", "Mandatory Doc Type", true); - contentType.PropertyGroups.First().PropertyTypes.Add( - new PropertyType("test", ValueStorageType.Ntext, "tags") - { - DataTypeId = 1041 - }); - contentTypeService.Save(contentType); - var content = MockedContent.CreateSimpleContent(contentType, "Tagged content", -1); - content.AssignTags("tags", new[] { "hello", "world", "some", "tags" }); - contentService.SaveAndPublish(content); - - // Act - content.RemoveTags("tags", new[] { "some", "world" }); - contentService.SaveAndPublish(content); - - // Assert - Assert.AreEqual(2, content.Properties["tags"].GetValue().ToString().Split(',').Distinct().Count()); - var propertyTypeId = contentType.PropertyTypes.Single(x => x.Alias == "tags").Id; - using (var scope = ScopeProvider.CreateScope()) - { - Assert.AreEqual(2, scope.Database.ExecuteScalar( - "SELECT COUNT(*) FROM cmsTagRelationship WHERE nodeId=@nodeId AND propertyTypeId=@propTypeId", - new { nodeId = content.Id, propTypeId = propertyTypeId })); - - scope.Complete(); - } - } - [Test] public void Can_Remove_Property_Type() { @@ -1134,7 +572,6 @@ namespace Umbraco.Tests.Services Assert.That(contents.Count(), Is.GreaterThanOrEqualTo(2)); } - [Test] public void Can_Get_All_Versions_Of_Content() { diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 322be82ca7..e901f3689e 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -131,6 +131,7 @@ + diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertyvalidator.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertyvalidator.directive.js index c1a2999017..37303d22ad 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertyvalidator.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertyvalidator.directive.js @@ -36,7 +36,7 @@ function valPropertyValidator(serverValidationManager) { // Validation method function validate (viewValue) { - // Calls the validition method + // Calls the validation method var result = scope.valPropertyValidator(); if (!result.errorKey || result.isValid === undefined || !result.errorMsg) { throw "The result object from valPropertyValidator does not contain required properties: isValid, errorKey, errorMsg"; @@ -55,6 +55,9 @@ function valPropertyValidator(serverValidationManager) { propCtrl.setPropertyError(result.errorMsg); } } + + // parsers are expected to return a value + return (result.isValid) ? viewValue : undefined; }; // Parsers are called as soon as the value in the form input is modified diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/tags/tags.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/tags/tags.controller.js index bcf27294f3..01a188c847 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/tags/tags.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/tags/tags.controller.js @@ -142,6 +142,13 @@ angular.module("umbraco") }); } + function currentCulture(scope) { + while (scope && !scope.activeVariant) + scope = scope.$parent; + if (!scope || !scope.activeVariant) return null; + return scope.activeVariant.language.culture; + } + var tagsHound = new Bloodhound({ datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'), queryTokenizer: Bloodhound.tokenizers.whitespace, @@ -150,14 +157,14 @@ angular.module("umbraco") }, //pre-fetch the tags for this category prefetch: { - url: umbRequestHelper.getApiUrl("tagsDataBaseUrl", "GetTags", [{ tagGroup: $scope.model.config.group }]), + url: umbRequestHelper.getApiUrl("tagsDataBaseUrl", "GetTags", { tagGroup: $scope.model.config.group, culture: currentCulture($scope) }), //TTL = 5 minutes ttl: 300000, filter: dataTransform }, //dynamically get the tags for this category (they may have changed on the server) remote: { - url: umbRequestHelper.getApiUrl("tagsDataBaseUrl", "GetTags", [{ tagGroup: $scope.model.config.group }]), + url: umbRequestHelper.getApiUrl("tagsDataBaseUrl", "GetTags", { tagGroup: $scope.model.config.group, culture: currentCulture($scope) }), filter: dataTransform } }); diff --git a/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs b/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs index 23d8e2cc4e..a3c2bf494a 100644 --- a/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs +++ b/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs @@ -254,7 +254,7 @@ namespace Umbraco.Web.Editors }, { "tagsDataBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl( - controller => controller.GetTags("")) + controller => controller.GetTags("", "")) }, { "examineMgmtBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl( diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index 53ecddd015..5f33a31872 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -1778,7 +1778,8 @@ namespace Umbraco.Web.Editors contentSave, propertyCollection, (save, property) => Varies(property) ? property.GetValue(variant.Culture) : property.GetValue(), //get prop val - (save, property, v) => { if (Varies(property)) property.SetValue(v, variant.Culture); else property.SetValue(v); }); //set prop val + (save, property, v) => { if (Varies(property)) property.SetValue(v, variant.Culture); else property.SetValue(v); }, //set prop val + variant.Culture); variantIndex++; } diff --git a/src/Umbraco.Web/Editors/ContentControllerBase.cs b/src/Umbraco.Web/Editors/ContentControllerBase.cs index 0f174b6bbd..09d91a6436 100644 --- a/src/Umbraco.Web/Editors/ContentControllerBase.cs +++ b/src/Umbraco.Web/Editors/ContentControllerBase.cs @@ -39,17 +39,12 @@ namespace Umbraco.Web.Editors /// /// Maps the dto property values to the persisted model /// - /// - /// - /// - /// - /// - /// internal void MapPropertyValuesForPersistence( TSaved contentItem, ContentPropertyCollectionDto dto, Func getPropertyValue, - Action savePropertyValue) + Action savePropertyValue, + string culture) where TPersisted : IContentBase where TSaved : IContentSave { @@ -70,7 +65,7 @@ namespace Umbraco.Web.Editors // get the property var property = contentItem.PersistedContent.Properties[propertyDto.Alias]; - + // prepare files, if any matching property and culture var files = contentItem.UploadedFiles .Where(x => x.PropertyAlias == propertyDto.Alias && x.Culture == propertyDto.Culture) @@ -96,8 +91,8 @@ namespace Umbraco.Web.Editors { var tagConfiguration = ConfigurationEditor.ConfigurationAs(propertyDto.DataType.Configuration); if (tagConfiguration.Delimiter == default) tagConfiguration.Delimiter = tagAttribute.Delimiter; - //fixme how is this supposed to work with variants? - property.SetTagsValue(value, tagConfiguration); + var tagCulture = property.PropertyType.VariesByCulture() ? culture : null; + property.SetTagsValue(value, tagConfiguration, tagCulture); } else savePropertyValue(contentItem, property, value); diff --git a/src/Umbraco.Web/Editors/MediaController.cs b/src/Umbraco.Web/Editors/MediaController.cs index 9aebc11dc6..4f4c37910e 100644 --- a/src/Umbraco.Web/Editors/MediaController.cs +++ b/src/Umbraco.Web/Editors/MediaController.cs @@ -483,8 +483,9 @@ namespace Umbraco.Web.Editors MapPropertyValuesForPersistence( contentItem, contentItem.PropertyCollectionDto, - (save, property) => property.GetValue(), //get prop val - (save, property, v) => property.SetValue(v)); //set prop val + (save, property) => property.GetValue(), //get prop val + (save, property, v) => property.SetValue(v), //set prop val + null); // media are all invariant //We need to manually check the validation results here because: // * We still need to save the entity even if there are validation value errors diff --git a/src/Umbraco.Web/Editors/MemberController.cs b/src/Umbraco.Web/Editors/MemberController.cs index 6117db8857..e287006992 100644 --- a/src/Umbraco.Web/Editors/MemberController.cs +++ b/src/Umbraco.Web/Editors/MemberController.cs @@ -400,8 +400,9 @@ namespace Umbraco.Web.Editors base.MapPropertyValuesForPersistence( contentItem, contentItem.PropertyCollectionDto, - (save, property) => property.GetValue(), //get prop val - (save, property, v) => property.SetValue(v)); //set prop val + (save, property) => property.GetValue(), //get prop val + (save, property, v) => property.SetValue(v), //set prop val + null); // member are all invariant } /// diff --git a/src/Umbraco.Web/ITagQuery.cs b/src/Umbraco.Web/ITagQuery.cs index 3d0757a695..031061ad01 100644 --- a/src/Umbraco.Web/ITagQuery.cs +++ b/src/Umbraco.Web/ITagQuery.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; using Umbraco.Web.Models; @@ -8,76 +7,53 @@ namespace Umbraco.Web public interface ITagQuery { /// - /// Returns all content that is tagged with the specified tag value and optional tag group + /// Gets all documents tagged with the specified tag. /// - /// - /// - /// - IEnumerable GetContentByTag(string tag, string tagGroup = null); + IEnumerable GetContentByTag(string tag, string group = null, string culture = null); /// - /// Returns all content that has been tagged with any tag in the specified group + /// Gets all documents tagged with any tag in the specified group. /// - /// - /// - IEnumerable GetContentByTagGroup(string tagGroup); + IEnumerable GetContentByTagGroup(string group, string culture = null); /// - /// Returns all Media that is tagged with the specified tag value and optional tag group + /// Gets all media tagged with the specified tag. /// - /// - /// - /// - IEnumerable GetMediaByTag(string tag, string tagGroup = null); + IEnumerable GetMediaByTag(string tag, string group = null, string culture = null); /// - /// Returns all Media that has been tagged with any tag in the specified group + /// Gets all media tagged with any tag in the specified group. /// - /// - /// - IEnumerable GetMediaByTagGroup(string tagGroup); + IEnumerable GetMediaByTagGroup(string group, string culture); /// - /// Get every tag stored in the database (with optional group) + /// Gets all tags. /// - IEnumerable GetAllTags(string group = null); + IEnumerable GetAllTags(string group = null, string culture = null); /// - /// Get all tags for content items (with optional group) + /// Gets all document tags. /// - /// - /// - IEnumerable GetAllContentTags(string group = null); + IEnumerable GetAllContentTags(string group = null, string culture = null); /// - /// Get all tags for media items (with optional group) + /// Gets all media tags. /// - /// - /// - IEnumerable GetAllMediaTags(string group = null); + IEnumerable GetAllMediaTags(string group = null, string culture = null); /// - /// Get all tags for member items (with optional group) + /// Gets all member tags. /// - /// - /// - IEnumerable GetAllMemberTags(string group = null); + IEnumerable GetAllMemberTags(string group = null, string culture = null); /// - /// Returns all tags attached to a property by entity id + /// Gets all tags attached to an entity via a property. /// - /// - /// - /// - /// - IEnumerable GetTagsForProperty(int contentId, string propertyTypeAlias, string tagGroup = null); + IEnumerable GetTagsForProperty(int contentId, string propertyTypeAlias, string group = null, string culture = null); /// - /// Returns all tags attached to an entity (content, media or member) by entity id + /// Gets all tags attached to an entity. /// - /// - /// - /// - IEnumerable GetTagsForEntity(int contentId, string tagGroup = null); + IEnumerable GetTagsForEntity(int contentId, string group = null, string culture = null); } } diff --git a/src/Umbraco.Web/PropertyEditors/TagsDataController.cs b/src/Umbraco.Web/PropertyEditors/TagsDataController.cs index dc0a0787dd..bc0c281f98 100644 --- a/src/Umbraco.Web/PropertyEditors/TagsDataController.cs +++ b/src/Umbraco.Web/PropertyEditors/TagsDataController.cs @@ -15,9 +15,10 @@ namespace Umbraco.Web.PropertyEditors [PluginController("UmbracoApi")] public class TagsDataController : UmbracoAuthorizedApiController { - public IEnumerable GetTags(string tagGroup) + public IEnumerable GetTags(string tagGroup, string culture) { - return Umbraco.TagQuery.GetAllTags(tagGroup); + if (culture == string.Empty) culture = null; + return Umbraco.TagQuery.GetAllTags(tagGroup, culture); } } } diff --git a/src/Umbraco.Web/TagQuery.cs b/src/Umbraco.Web/TagQuery.cs index 9527e72453..e79640bb4d 100644 --- a/src/Umbraco.Web/TagQuery.cs +++ b/src/Umbraco.Web/TagQuery.cs @@ -9,195 +9,92 @@ using Umbraco.Web.Models; namespace Umbraco.Web { /// - /// A class that exposes methods used to query tag data in views + /// Implements . /// public class TagQuery : ITagQuery { - - //TODO: This class also acts as a wrapper for ITagQuery due to breaking changes, need to fix in - // version 8: http://issues.umbraco.org/issue/U4-6899 - private readonly ITagQuery _wrappedQuery; - private readonly ITagService _tagService; private readonly IPublishedContentQuery _contentQuery; - /// - /// Constructor for wrapping ITagQuery, see http://issues.umbraco.org/issue/U4-6899 + /// Initializes a new instance of the class. /// - /// - internal TagQuery(ITagQuery wrappedQuery) - { - if (wrappedQuery == null) throw new ArgumentNullException("wrappedQuery"); - _wrappedQuery = wrappedQuery; - } - - /// - /// Constructor - /// - /// - /// public TagQuery(ITagService tagService, IPublishedContentQuery contentQuery) { - if (tagService == null) throw new ArgumentNullException("tagService"); - if (contentQuery == null) throw new ArgumentNullException("contentQuery"); - _tagService = tagService; - _contentQuery = contentQuery; + _tagService = tagService ?? throw new ArgumentNullException(nameof(tagService)); + _contentQuery = contentQuery ?? throw new ArgumentNullException(nameof(contentQuery)); } - /// - /// Returns all content that is tagged with the specified tag value and optional tag group - /// - /// - /// - /// - public IEnumerable GetContentByTag(string tag, string tagGroup = null) + /// + public IEnumerable GetContentByTag(string tag, string group = null, string culture = null) { - //TODO: http://issues.umbraco.org/issue/U4-6899 - if (_wrappedQuery != null) return _wrappedQuery.GetContentByTag(tag, tagGroup); - - var ids = _tagService.GetTaggedContentByTag(tag, tagGroup) + var ids = _tagService.GetTaggedContentByTag(tag, group, culture) .Select(x => x.EntityId); return _contentQuery.Content(ids) .Where(x => x != null); } - /// - /// Returns all content that has been tagged with any tag in the specified group - /// - /// - /// - public IEnumerable GetContentByTagGroup(string tagGroup) + /// + public IEnumerable GetContentByTagGroup(string group, string culture = null) { - //TODO: http://issues.umbraco.org/issue/U4-6899 - if (_wrappedQuery != null) return _wrappedQuery.GetContentByTagGroup(tagGroup); - - var ids = _tagService.GetTaggedContentByTagGroup(tagGroup) + var ids = _tagService.GetTaggedContentByTagGroup(group, culture) .Select(x => x.EntityId); return _contentQuery.Content(ids) .Where(x => x != null); } - /// - /// Returns all Media that is tagged with the specified tag value and optional tag group - /// - /// - /// - /// - public IEnumerable GetMediaByTag(string tag, string tagGroup = null) + /// + public IEnumerable GetMediaByTag(string tag, string group = null, string culture = null) { - //TODO: http://issues.umbraco.org/issue/U4-6899 - if (_wrappedQuery != null) return _wrappedQuery.GetMediaByTag(tag, tagGroup); - - var ids = _tagService.GetTaggedMediaByTag(tag, tagGroup) + var ids = _tagService.GetTaggedMediaByTag(tag, group, culture) .Select(x => x.EntityId); return _contentQuery.Media(ids) .Where(x => x != null); } - /// - /// Returns all Media that has been tagged with any tag in the specified group - /// - /// - /// - public IEnumerable GetMediaByTagGroup(string tagGroup) + /// + public IEnumerable GetMediaByTagGroup(string group, string culture = null) { - //TODO: http://issues.umbraco.org/issue/U4-6899 - if (_wrappedQuery != null) return _wrappedQuery.GetMediaByTagGroup(tagGroup); - - var ids = _tagService.GetTaggedMediaByTagGroup(tagGroup) + var ids = _tagService.GetTaggedMediaByTagGroup(group, culture) .Select(x => x.EntityId); return _contentQuery.Media(ids) .Where(x => x != null); } - //TODO: Should prob implement these, requires a bit of work on the member service to do this, - // also not sure if its necessary ? - //public IEnumerable GetMembersByTag(string tag, string tagGroup = null) - //{ - //} - - //public IEnumerable GetMembersByTagGroup(string tagGroup) - //{ - //} - - /// - /// Get every tag stored in the database (with optional group) - /// - public IEnumerable GetAllTags(string group = null) + /// + public IEnumerable GetAllTags(string group = null, string culture = null) { - //TODO: http://issues.umbraco.org/issue/U4-6899 - if (_wrappedQuery != null) return _wrappedQuery.GetAllTags(group); - - return Mapper.Map>(_tagService.GetAllTags(group)); + return Mapper.Map>(_tagService.GetAllTags(group, culture)); } - /// - /// Get all tags for content items (with optional group) - /// - /// - /// - public IEnumerable GetAllContentTags(string group = null) + /// + public IEnumerable GetAllContentTags(string group = null, string culture = null) { - //TODO: http://issues.umbraco.org/issue/U4-6899 - if (_wrappedQuery != null) return _wrappedQuery.GetAllContentTags(group); - - return Mapper.Map>(_tagService.GetAllContentTags(group)); + return Mapper.Map>(_tagService.GetAllContentTags(group, culture)); } - /// - /// Get all tags for media items (with optional group) - /// - /// - /// - public IEnumerable GetAllMediaTags(string group = null) + /// + public IEnumerable GetAllMediaTags(string group = null, string culture = null) { - //TODO: http://issues.umbraco.org/issue/U4-6899 - if (_wrappedQuery != null) return _wrappedQuery.GetAllMediaTags(group); - - return Mapper.Map>(_tagService.GetAllMediaTags(group)); + return Mapper.Map>(_tagService.GetAllMediaTags(group, culture)); } - /// - /// Get all tags for member items (with optional group) - /// - /// - /// - public IEnumerable GetAllMemberTags(string group = null) + /// + public IEnumerable GetAllMemberTags(string group = null, string culture = null) { - //TODO: http://issues.umbraco.org/issue/U4-6899 - if (_wrappedQuery != null) return _wrappedQuery.GetAllMemberTags(group); - - return Mapper.Map>(_tagService.GetAllMemberTags(group)); + return Mapper.Map>(_tagService.GetAllMemberTags(group, culture)); } - /// - /// Returns all tags attached to a property by entity id - /// - /// - /// - /// - /// - public IEnumerable GetTagsForProperty(int contentId, string propertyTypeAlias, string tagGroup = null) + /// + public IEnumerable GetTagsForProperty(int contentId, string propertyTypeAlias, string group = null, string culture = null) { - //TODO: http://issues.umbraco.org/issue/U4-6899 - if (_wrappedQuery != null) return _wrappedQuery.GetTagsForProperty(contentId, propertyTypeAlias, tagGroup); - - return Mapper.Map>(_tagService.GetTagsForProperty(contentId, propertyTypeAlias, tagGroup)); + return Mapper.Map>(_tagService.GetTagsForProperty(contentId, propertyTypeAlias, group, culture)); } - /// - /// Returns all tags attached to an entity (content, media or member) by entity id - /// - /// - /// - /// - public IEnumerable GetTagsForEntity(int contentId, string tagGroup = null) + /// + public IEnumerable GetTagsForEntity(int contentId, string group = null, string culture = null) { - //TODO: http://issues.umbraco.org/issue/U4-6899 - if (_wrappedQuery != null) return _wrappedQuery.GetTagsForEntity(contentId, tagGroup); - - return Mapper.Map>(_tagService.GetTagsForEntity(contentId, tagGroup)); + return Mapper.Map>(_tagService.GetTagsForEntity(contentId, group, culture)); } } } diff --git a/src/Umbraco.Web/UmbracoHelper.cs b/src/Umbraco.Web/UmbracoHelper.cs index aa30fff0ec..1ce7e51a7f 100644 --- a/src/Umbraco.Web/UmbracoHelper.cs +++ b/src/Umbraco.Web/UmbracoHelper.cs @@ -67,7 +67,7 @@ namespace Umbraco.Web if (appCache == null) throw new ArgumentNullException(nameof(appCache)); _umbracoContext = umbracoContext; - _tag = new TagQuery(tagQuery); + _tag = tagQuery; _dataTypeService = dataTypeService; _cultureDictionary = cultureDictionary; _componentRenderer = componentRenderer;