Undoes the comma encoding for tags and istead adds support to store the tags as json or csv (U4-4741)

This commit is contained in:
Shannon
2014-05-15 12:49:03 +10:00
parent 157428dbc8
commit 1c7d83f589
13 changed files with 190 additions and 47 deletions

View File

@@ -9,6 +9,8 @@ using System.Linq;
using System.Web;
using System.Xml;
using System.Xml.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Umbraco.Core.Configuration;
using Umbraco.Core.Configuration.UmbracoSettings;
using Umbraco.Core.IO;
@@ -539,7 +541,7 @@ namespace Umbraco.Core.Models
return ApplicationContext.Current.Services.ContentService.HasPublishedVersion(content.Id);
}
#region Tag methods
///// <summary>
@@ -567,6 +569,21 @@ namespace Umbraco.Core.Models
/// <param name="tagGroup">The group/category to assign the tags, the default value is "default"</param>
/// <returns></returns>
public static void SetTags(this IContentBase content, string propertyTypeAlias, IEnumerable<string> tags, bool replaceTags, string tagGroup = "default")
{
content.SetTags(TagCacheStorageType.Csv, propertyTypeAlias, tags, replaceTags, tagGroup);
}
/// <summary>
/// Sets tags for the property - will add tags to the tags table and set the property value to be the comma delimited value of the tags.
/// </summary>
/// <param name="content">The content item to assign the tags to</param>
/// <param name="storageType">The tag storage type in cache (default is csv)</param>
/// <param name="propertyTypeAlias">The property alias to assign the tags to</param>
/// <param name="tags">The tags to assign</param>
/// <param name="replaceTags">True to replace the tags on the current property with the tags specified or false to merge them with the currently assigned ones</param>
/// <param name="tagGroup">The group/category to assign the tags, the default value is "default"</param>
/// <returns></returns>
public static void SetTags(this IContentBase content, TagCacheStorageType storageType, string propertyTypeAlias, IEnumerable<string> tags, bool replaceTags, string tagGroup = "default")
{
var property = content.Properties[propertyTypeAlias];
if (property == null)
@@ -583,15 +600,39 @@ namespace Umbraco.Core.Models
//ensure the property value is set to the same thing
if (replaceTags)
{
property.Value = string.Join(",", trimmedTags);
switch (storageType)
{
case TagCacheStorageType.Csv:
property.Value = string.Join(",", trimmedTags);
break;
case TagCacheStorageType.Json:
//json array
property.Value = JsonConvert.SerializeObject(trimmedTags);
break;
}
}
else
{
var currTags = property.Value.ToString().Split(new[] {','}, StringSplitOptions.RemoveEmptyEntries)
switch (storageType)
{
case TagCacheStorageType.Csv:
var currTags = property.Value.ToString().Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
.Select(x => x.Trim());
property.Value = string.Join(",", trimmedTags.Union(currTags));
property.Value = string.Join(",", trimmedTags.Union(currTags));
break;
case TagCacheStorageType.Json:
var currJson = JsonConvert.DeserializeObject<JArray>(property.Value.ToString());
//need to append the new ones
foreach (var tag in trimmedTags)
{
currJson.Add(tag);
}
//json array
property.Value = JsonConvert.SerializeObject(currJson);
break;
}
}
}
/// <summary>
@@ -664,7 +705,7 @@ namespace Umbraco.Core.Models
{
return ApplicationContext.Current.Services.PackagingService.Export(media, true, raiseEvents: false);
}
/// <summary>
/// Creates the xml representation for the <see cref="IContent"/> object
/// </summary>
@@ -687,10 +728,10 @@ namespace Umbraco.Core.Models
{
return ((PackagingService)(ApplicationContext.Current.Services.PackagingService)).Export(member);
}
#endregion
}
}

View File

@@ -0,0 +1,9 @@
namespace Umbraco.Core.Models
{
internal enum PropertyTagBehavior
{
Replace,
Remove,
Merge
}
}

View File

@@ -33,11 +33,4 @@ namespace Umbraco.Core.Models
public IEnumerable<Tuple<string, string>> Tags { get; set; }
}
internal enum PropertyTagBehavior
{
Replace,
Remove,
Merge
}
}

View File

@@ -0,0 +1,8 @@
namespace Umbraco.Core.Models
{
public enum TagCacheStorageType
{
Csv,
Json
}
}

View File

@@ -185,10 +185,6 @@ namespace Umbraco.Core.Persistence.Repositories
public IEnumerable<TaggedEntity> GetTaggedEntitiesByTag(TaggableObjectTypes objectType, string tag, string tagGroup = null)
{
//ensure that we html encode any comma's so they are found!
// http://issues.umbraco.org/issue/U4-4741
var replaced = tag.Replace(",", "&#44;");
var nodeObjectType = GetNodeObjectType(objectType);
var sql = new Sql()
@@ -203,7 +199,7 @@ namespace Umbraco.Core.Persistence.Repositories
.InnerJoin<NodeDto>()
.On<NodeDto, ContentDto>(left => left.NodeId, right => right.NodeId)
.Where<NodeDto>(dto => dto.NodeObjectType == nodeObjectType)
.Where<TagDto>(dto => dto.Tag == replaced);
.Where<TagDto>(dto => dto.Tag == tag);
if (tagGroup.IsNullOrWhiteSpace() == false)
{
@@ -289,7 +285,7 @@ namespace Umbraco.Core.Persistence.Repositories
if (group.IsNullOrWhiteSpace() == false)
{
sql = sql.Where<TagDto>(dto => dto.Group == group);
}
}
var factory = new TagFactory();
@@ -333,7 +329,7 @@ namespace Umbraco.Core.Persistence.Repositories
//NOTE: There's some very clever logic in the umbraco.cms.businesslogic.Tags.Tag to insert tags where they don't exist,
// and assign where they don't exist which we've borrowed here. The queries are pretty zany but work, otherwise we'll end up
// with quite a few additional queries.
//do all this in one transaction
using (var trans = Database.GetTransaction())
{
@@ -401,7 +397,7 @@ namespace Umbraco.Core.Persistence.Repositories
public void RemoveTagsFromProperty(int contentId, int propertyTypeId, IEnumerable<ITag> tags)
{
var tagSetSql = GetTagSet(tags);
var deleteSql = string.Concat("DELETE FROM cmsTagRelationship WHERE nodeId = ",
contentId,
" AND propertyTypeId = ",
@@ -446,11 +442,7 @@ namespace Umbraco.Core.Persistence.Repositories
var array = tagsToInsert
.Select(tag =>
string.Format("select '{0}' as Tag, '{1}' as [Group]",
PetaPocoExtensions.EscapeAtSymbols(
tag.Text
.Replace("'", "''") //NOTE: I'm not sure about this apostrophe replacement but it's been like that for a long time
.Replace(",", "&#44;")), //NOTE: We need to replace commas with html encoded ones: http://issues.umbraco.org/issue/U4-4741
tag.Group))
PetaPocoExtensions.EscapeAtSymbols(tag.Text.Replace("'", "''")), tag.Group))
.ToArray();
return "(" + string.Join(" union ", array).Replace(" ", " ") + ") as TagSet";
}

View File

@@ -1,4 +1,5 @@
using System;
using Umbraco.Core.Models;
namespace Umbraco.Core.PropertyEditors
{
@@ -30,6 +31,7 @@ namespace Umbraco.Core.PropertyEditors
Delimiter = ",";
ReplaceTags = true;
TagGroup = "default";
StorageType = TagCacheStorageType.Csv;
}
/// <summary>
@@ -37,6 +39,11 @@ namespace Umbraco.Core.PropertyEditors
/// </summary>
public TagValueType ValueType { get; set; }
/// <summary>
/// Defines how to store the tags in cache (CSV or Json)
/// </summary>
public TagCacheStorageType StorageType { get; set; }
/// <summary>
/// Defines a custom delimiter, the default is a comma
/// </summary>

View File

@@ -1,4 +1,5 @@
using Umbraco.Core.Models.Editors;
using Umbraco.Core.Models;
using Umbraco.Core.Models.Editors;
namespace Umbraco.Core.PropertyEditors
{
@@ -30,8 +31,14 @@ namespace Umbraco.Core.PropertyEditors
Delimiter = tagsAttribute.Delimiter;
ReplaceTags = tagsAttribute.ReplaceTags;
TagGroup = tagsAttribute.TagGroup;
StorageType = TagCacheStorageType.Csv;
}
/// <summary>
/// Defines how to store the tags in cache (CSV or Json)
/// </summary>
public virtual TagCacheStorageType StorageType { get; private set; }
/// <summary>
/// Defines a custom delimiter, the default is a comma
/// </summary>

View File

@@ -349,12 +349,14 @@
<Compile Include="Models\MemberGroup.cs" />
<Compile Include="Models\MemberTypePropertyProfileAccess.cs" />
<Compile Include="Models\Notification.cs" />
<Compile Include="Models\PropertyTagBehavior.cs" />
<Compile Include="Models\PublishedContent\IPublishedContentExtended.cs" />
<Compile Include="Models\PublishedContent\PublishedPropertyBase.cs" />
<Compile Include="Models\PublishedContent\PublishedContentModelFactoryImpl.cs" />
<Compile Include="Models\PublishedContent\IPublishedContentModelFactory.cs" />
<Compile Include="Models\PublishedContent\PublishedContentModel.cs" />
<Compile Include="Models\PublishedContent\PublishedContentModelFactoryResolver.cs" />
<Compile Include="Models\TagCacheStorageType.cs" />
<Compile Include="Models\TaggableObjectTypes.cs" />
<Compile Include="Models\TemplateNode.cs" />
<Compile Include="Models\UmbracoEntityExtensions.cs" />

View File

@@ -12,15 +12,21 @@ angular.module("umbraco")
//load current value
$scope.currentTags = [];
if ($scope.model.value) {
$scope.currentTags = $scope.model.value.split(",");
if ($scope.model.config.storageType && $scope.model.config.storageType === "Json") {
//it's a json array already
$scope.currentTags = $scope.model.value;
}
else {
//it is csv
$scope.currentTags = $scope.model.value.split(",");
}
}
//Helper method to add a tag on enter or on typeahead select
function addTag(tagToAdd) {
if (tagToAdd.length > 0) {
if ($scope.currentTags.indexOf(tagToAdd) < 0) {
//we need to html encode any tag containing commas: http://issues.umbraco.org/issue/U4-4741
tagToAdd = tagToAdd.replace(/\,/g, "&#44;");
if ($scope.currentTags.indexOf(tagToAdd) < 0) {
$scope.currentTags.push(tagToAdd);
}
}
@@ -49,16 +55,24 @@ angular.module("umbraco")
}
};
//sync model on submit (needed since we convert an array to string)
//sync model on submit, always push up a json array
$scope.$on("formSubmitting", function (ev, args) {
$scope.model.value = $scope.currentTags.join();
$scope.model.value = $scope.currentTags;
});
//vice versa
$scope.model.onValueChanged = function (newVal, oldVal) {
//update the display val again if it has changed from the server
$scope.model.val = newVal;
$scope.currentTags = $scope.model.value.split(",");
$scope.model.value = newVal;
if ($scope.model.config.storageType && $scope.model.config.storageType === "Json") {
//it's a json array already
$scope.currentTags = $scope.model.value;
}
else {
//it is csv
$scope.currentTags = $scope.model.value.split(",");
}
};
//configure the tags data source

View File

@@ -0,0 +1,10 @@
<div >
<select name="dropDownList"
class="umb-editor umb-dropdown"
ng-model="model.value">
<option>Csv</option>
<option>Json</option>
</select>
</div>

View File

@@ -43,15 +43,15 @@ namespace Umbraco.Web.Editors
LogHelper.Error<TagExtractor>("Could not create custom " + attribute.TagPropertyDefinitionType + " tag definition", ex);
throw;
}
SetPropertyTags(content, property, convertedPropertyValue, def.Delimiter, def.ReplaceTags, def.TagGroup, attribute.ValueType);
SetPropertyTags(content, property, convertedPropertyValue, def.Delimiter, def.ReplaceTags, def.TagGroup, attribute.ValueType, def.StorageType);
}
else
{
SetPropertyTags(content, property, convertedPropertyValue, attribute.Delimiter, attribute.ReplaceTags, attribute.TagGroup, attribute.ValueType);
SetPropertyTags(content, property, convertedPropertyValue, attribute.Delimiter, attribute.ReplaceTags, attribute.TagGroup, attribute.ValueType, attribute.StorageType);
}
}
public static void SetPropertyTags(IContentBase content, Property property, object convertedPropertyValue, string delimiter, bool replaceTags, string tagGroup, TagValueType valueType)
public static void SetPropertyTags(IContentBase content, Property property, object convertedPropertyValue, string delimiter, bool replaceTags, string tagGroup, TagValueType valueType, TagCacheStorageType storageType)
{
if (convertedPropertyValue == null)
{
@@ -62,14 +62,14 @@ namespace Umbraco.Web.Editors
{
case TagValueType.FromDelimitedValue:
var tags = convertedPropertyValue.ToString().Split(new[] { delimiter }, StringSplitOptions.RemoveEmptyEntries);
content.SetTags(property.Alias, tags, replaceTags, tagGroup);
content.SetTags(storageType, property.Alias, tags, replaceTags, tagGroup);
break;
case TagValueType.CustomTagList:
//for this to work the object value must be IENumerable<string>
var stringList = convertedPropertyValue as IEnumerable<string>;
if (stringList != null)
{
content.SetTags(property.Alias, stringList, replaceTags, tagGroup);
content.SetTags(storageType, property.Alias, stringList, replaceTags, tagGroup);
}
break;
}

View File

@@ -1,10 +1,13 @@
using Umbraco.Core.Models.Editors;
using System;
using Umbraco.Core;
using Umbraco.Core.Models;
using Umbraco.Core.Models.Editors;
using Umbraco.Core.PropertyEditors;
namespace Umbraco.Web.PropertyEditors
{
/// <summary>
/// Used to dynamically change the tag group based on the pre-values
/// Used to dynamically change the tag group and storage type based on the pre-values
/// </summary>
internal class TagPropertyEditorTagDefinition : TagPropertyDefinition
{
@@ -21,5 +24,16 @@ namespace Umbraco.Web.PropertyEditors
return preVals.ContainsKey("group") ? preVals["group"].Value : "default";
}
}
public override TagCacheStorageType StorageType
{
get
{
var preVals = PropertySaving.PreValues.FormatAsDictionary();
return preVals.ContainsKey("storageType")
? Enum<TagCacheStorageType>.Parse(preVals["storageType"].Value)
: TagCacheStorageType.Csv;
}
}
}
}

View File

@@ -1,10 +1,15 @@
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using Newtonsoft.Json.Linq;
using Umbraco.Core;
using Umbraco.Core.Models;
using Umbraco.Core.Models.Editors;
using Umbraco.Core.PropertyEditors;
namespace Umbraco.Web.PropertyEditors
{
[SupportTags(typeof(TagPropertyEditorTagDefinition))]
[SupportTags(typeof(TagPropertyEditorTagDefinition), ValueType = TagValueType.CustomTagList)]
[PropertyEditor(Constants.PropertyEditors.TagsAlias, "Tags", "tags")]
public class TagsPropertyEditor : PropertyEditor
{
@@ -12,7 +17,8 @@ namespace Umbraco.Web.PropertyEditors
{
_defaultPreVals = new Dictionary<string, object>
{
{"group", "default"}
{"group", "default"},
{"storageType", TagCacheStorageType.Csv.ToString()}
};
}
@@ -27,11 +33,36 @@ namespace Umbraco.Web.PropertyEditors
set { _defaultPreVals = value; }
}
protected override PropertyValueEditor CreateValueEditor()
{
return new TagPropertyValueEditor(base.CreateValueEditor());
}
protected override PreValueEditor CreatePreValueEditor()
{
return new TagPreValueEditor();
}
internal class TagPropertyValueEditor : PropertyValueEditorWrapper
{
public TagPropertyValueEditor(PropertyValueEditor wrapped)
: base(wrapped)
{
}
/// <summary>
/// This needs to return IEnumerable{string}
/// </summary>
/// <param name="editorValue"></param>
/// <param name="currentValue"></param>
/// <returns></returns>
public override object ConvertEditorToDb(ContentPropertyData editorValue, object currentValue)
{
var json = editorValue.Value as JArray;
return json == null ? null : json.Select(x => x.Value<string>());
}
}
internal class TagPreValueEditor : PreValueEditor
{
public TagPreValueEditor()
@@ -43,11 +74,26 @@ namespace Umbraco.Web.PropertyEditors
Name = "Tag group",
View = "requiredfield"
});
Fields.Add(new PreValueField(new ManifestPropertyValidator {Type = "Required"})
{
Description = "Select whether to store the tags in cache as CSV (default) or as JSON. The only benefits of storage as JSON is that you are able to have commas in a tag value but this will require parsing the json in your views or using a property value converter",
Key = "storageType",
Name = "Storage Type",
View = "views/propertyeditors/tags/tags.prevalues.html"
});
}
public override IDictionary<string, object> ConvertDbToEditor(IDictionary<string, object> defaultPreVals, Core.Models.PreValueCollection persistedPreVals)
public override IDictionary<string, object> ConvertDbToEditor(IDictionary<string, object> defaultPreVals, PreValueCollection persistedPreVals)
{
var result = base.ConvertDbToEditor(defaultPreVals, persistedPreVals);
//This is required because we've added this pre-value so old installs that don't have it will need to have a default.
if (result.ContainsKey("storageType") == false || result["storageType"] == null || result["storageType"].ToString().IsNullOrWhiteSpace())
{
result["storageType"] = TagCacheStorageType.Csv.ToString();
}
return result;
}
}