diff --git a/src/Orchard.Web/Modules/Orchard.Tags/Drivers/TagCloudDriver.cs b/src/Orchard.Web/Modules/Orchard.Tags/Drivers/TagCloudDriver.cs new file mode 100644 index 000000000..9e815c0c9 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Tags/Drivers/TagCloudDriver.cs @@ -0,0 +1,38 @@ +using Orchard.ContentManagement; +using Orchard.ContentManagement.Drivers; +using Orchard.Environment.Extensions; +using Orchard.Tags.Models; + +namespace Orchard.Tags.Drivers { + [OrchardFeature("Orchard.Tags.TagCloud")] + public class TagCloudDriver : ContentPartDriver { + + protected override string Prefix { + get { + return "tagcloud"; + } + } + + protected override DriverResult Display(TagCloudPart part, string displayType, dynamic shapeHelper) { + return ContentShape("Parts_TagCloud", + () => shapeHelper.Parts_TagCloud( + TagCounts: part.TagCounts, + ContentPart: part, + ContentItem: part.ContentItem)); + } + + protected override DriverResult Editor(TagCloudPart part, dynamic shapeHelper) { + + return ContentShape("Parts_TagCloud_Edit", + () => shapeHelper.EditorTemplate( + TemplateName: "Parts/TagCloud", + Model: part, + Prefix: Prefix)); + } + + protected override DriverResult Editor(TagCloudPart part, IUpdateModel updater, dynamic shapeHelper) { + updater.TryUpdateModel(part, Prefix, null, null); + return Editor(part, shapeHelper); + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Tags/Handlers/TagCloudHandler.cs b/src/Orchard.Web/Modules/Orchard.Tags/Handlers/TagCloudHandler.cs new file mode 100644 index 000000000..786dc5f38 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Tags/Handlers/TagCloudHandler.cs @@ -0,0 +1,33 @@ +using System.Linq; +using Orchard.Caching; +using Orchard.ContentManagement.Handlers; +using Orchard.Environment.Extensions; +using Orchard.Tags.Models; +using Orchard.Tags.Services; + +namespace Orchard.Tags.Handlers { + [OrchardFeature("Orchard.Tags.TagCloud")] + public class TagCloudHandler : ContentHandler { + private readonly ISignals _signals; + + public TagCloudHandler( + ITagCloudService tagCloudService, + ISignals signals) { + + _signals = signals; + + OnInitializing((context, part) => part + ._tagCountField.Loader(tags => + tagCloudService.GetPopularTags(part.Buckets, part.Slug).ToList() + )); + + OnPublished((context, part) => InvalidateTagCloudCache()); + OnRemoved((context, part) => InvalidateTagCloudCache()); + OnUnpublished((context, part) => InvalidateTagCloudCache()); + } + + public void InvalidateTagCloudCache() { + _signals.Trigger(TagCloudService.TagCloudTagsChanged); + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Tags/Migrations.cs b/src/Orchard.Web/Modules/Orchard.Tags/Migrations.cs index 09d93c7c1..4ebc22f9d 100644 --- a/src/Orchard.Web/Modules/Orchard.Tags/Migrations.cs +++ b/src/Orchard.Web/Modules/Orchard.Tags/Migrations.cs @@ -1,6 +1,7 @@ using Orchard.ContentManagement.MetaData; using Orchard.Core.Contents.Extensions; using Orchard.Data.Migration; +using Orchard.Environment.Extensions; namespace Orchard.Tags { public class TagsDataMigration : DataMigrationImpl { @@ -36,4 +37,22 @@ namespace Orchard.Tags { return 2; } } + + [OrchardFeature("Orchard.Tags.TagCloud")] + public class TagCloudMigrations : DataMigrationImpl { + + public int Create() { + + ContentDefinitionManager.AlterTypeDefinition( + "TagCloud", + cfg => cfg + .WithPart("TagCloudPart") + .WithPart("CommonPart") + .WithPart("WidgetPart") + .WithSetting("Stereotype", "Widget") + ); + + return 1; + } + } } \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Tags/Models/TagCloudPart.cs b/src/Orchard.Web/Modules/Orchard.Tags/Models/TagCloudPart.cs new file mode 100644 index 000000000..e82fa3e87 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Tags/Models/TagCloudPart.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using Orchard.ContentManagement; +using Orchard.ContentManagement.Utilities; +using Orchard.Environment.Extensions; + +namespace Orchard.Tags.Models { + [OrchardFeature("Orchard.Tags.TagCloud")] + public class TagCloudPart : ContentPart { + internal readonly LazyField> _tagCountField = new LazyField>(); + + public IList TagCounts { get { return _tagCountField.Value; } } + + public int Buckets { + get { return this.Retrieve(r => r.Buckets, 5); } + set { this.Store(r => r.Buckets, value); } + } + + public string Slug { + get { return this.Retrieve(r => r.Slug); } + set { this.Store(r => r.Slug, value); } + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Tags/Models/TagCount.cs b/src/Orchard.Web/Modules/Orchard.Tags/Models/TagCount.cs new file mode 100644 index 000000000..c3ddb71d1 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Tags/Models/TagCount.cs @@ -0,0 +1,12 @@ + +namespace Orchard.Tags.Models { + public class TagCount { + public TagCount() { + Bucket = 1; + } + + public string TagName { get; set; } + public int Count { get; set; } + public int Bucket { get; set; } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Tags/Module.txt b/src/Orchard.Web/Modules/Orchard.Tags/Module.txt index 254103bc0..d3a6def0f 100644 --- a/src/Orchard.Web/Modules/Orchard.Tags/Module.txt +++ b/src/Orchard.Web/Modules/Orchard.Tags/Module.txt @@ -15,3 +15,8 @@ Features: Description: Adds tags to the RSS feeds. Dependencies: Orchard.Tags, Feeds Category: Syndication + Orchard.Tags.TagCloud: + Name: Tag Cloud + Description: Adds a tag cloud widget. + Dependencies: Orchard.Tags, Orchard.Autoroute + Category: Navigation diff --git a/src/Orchard.Web/Modules/Orchard.Tags/Orchard.Tags.csproj b/src/Orchard.Web/Modules/Orchard.Tags/Orchard.Tags.csproj index 2ee056431..71416474e 100644 --- a/src/Orchard.Web/Modules/Orchard.Tags/Orchard.Tags.csproj +++ b/src/Orchard.Web/Modules/Orchard.Tags/Orchard.Tags.csproj @@ -67,16 +67,22 @@ + + + + Code + + @@ -118,6 +124,10 @@ {9916839C-39FC-4CEB-A5AF-89CA7E87119F} Orchard.Core + + {66FCCD76-2761-47E3-8D11-B45D0001DDAA} + Orchard.Autoroute + @@ -136,7 +146,12 @@ - + + + + + + 10.0 $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) diff --git a/src/Orchard.Web/Modules/Orchard.Tags/Placement.info b/src/Orchard.Web/Modules/Orchard.Tags/Placement.info index c528accb2..79ca00149 100644 --- a/src/Orchard.Web/Modules/Orchard.Tags/Placement.info +++ b/src/Orchard.Web/Modules/Orchard.Tags/Placement.info @@ -6,4 +6,8 @@ + + + + diff --git a/src/Orchard.Web/Modules/Orchard.Tags/Services/ITagCloudService.cs b/src/Orchard.Web/Modules/Orchard.Tags/Services/ITagCloudService.cs new file mode 100644 index 000000000..36f74e55f --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Tags/Services/ITagCloudService.cs @@ -0,0 +1,8 @@ +using System.Collections.Generic; +using Orchard.Tags.Models; + +namespace Orchard.Tags.Services { + public interface ITagCloudService : IDependency { + IEnumerable GetPopularTags(int buckets, string slug); + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Tags/Services/TagCloudService.cs b/src/Orchard.Web/Modules/Orchard.Tags/Services/TagCloudService.cs new file mode 100644 index 000000000..4c50ae05a --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Tags/Services/TagCloudService.cs @@ -0,0 +1,127 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Orchard.Autoroute.Models; +using Orchard.Caching; +using Orchard.ContentManagement; +using Orchard.Core.Common.Models; +using Orchard.Data; +using Orchard.Environment.Extensions; +using Orchard.Tags.Models; + +namespace Orchard.Tags.Services { + [OrchardFeature("Orchard.Tags.TagCloud")] + public class TagCloudService : ITagCloudService { + private readonly IRepository _contentTagRepository; + private readonly IRepository _autorouteRepository; + private readonly IContentManager _contentManager; + private readonly ICacheManager _cacheManager; + private readonly ISignals _signals; + internal static readonly string TagCloudTagsChanged = "Orchard.Tags.TagCloud.TagsChanged"; + + public TagCloudService( + IRepository contentTagRepository, + IRepository autorouteRepository, + IContentManager contentManager, + ICacheManager cacheManager, + ISignals signals) { + + _contentTagRepository = contentTagRepository; + _autorouteRepository = autorouteRepository; + _contentManager = contentManager; + _cacheManager = cacheManager; + _signals = signals; + } + + public IEnumerable GetPopularTags(int buckets, string slug) { + var cacheKey = "Orchard.Tags.TagCloud." + (slug ?? "") + '.' + buckets; + return _cacheManager.Get(cacheKey, + ctx => { + ctx.Monitor(_signals.When(TagCloudTagsChanged)); + IEnumerable tagCounts; + if (string.IsNullOrWhiteSpace(slug)) { + tagCounts = (from tc in _contentTagRepository.Table + where tc.TagsPartRecord.ContentItemRecord.Versions.Any(v => v.Published) + group tc by tc.TagRecord.TagName + into g + select new TagCount { + TagName = g.Key, + Count = g.Count() + }).ToList(); + } + else { + if (slug == "/") { + slug = ""; + } + + var containerId = _autorouteRepository.Table + .Where(c => c.DisplayAlias == slug) + .Select(x => x.Id) + .ToList() // don't try to optimize with slicing as there should be only one result + .FirstOrDefault(); + + if (containerId == 0) { + return new List(); + } + + tagCounts = _contentManager + .Query(VersionOptions.Published) + .Join() + .Where(t => t.Container.Id == containerId) + .List() + .SelectMany(t => t.CurrentTags) + .GroupBy(t => t) + .Select(g => new TagCount { + TagName = g.Key, + Count = g.Count() + }) + .ToList(); + + if (!tagCounts.Any()) { + return new List(); + } + } + + // initialize centroids with a linear distribution + var centroids = new int[buckets]; + var maxCount = tagCounts.Max(tc => tc.Count); + var minCount = tagCounts.Min(tc => tc.Count); + var maxDistance = maxCount - minCount; + for (int i = 0; i < centroids.Length; i++) { + centroids[i] = maxDistance/buckets * (i+1); + } + + var balanced = false; + var loops = 0; + + // loop until equilibrium or instability + while (!balanced && loops++ < 50) { + balanced = true; + // assign to closest buckets + foreach (var tagCount in tagCounts) { + // look for closest bucket + var currentDistance = Math.Abs(tagCount.Count - centroids[tagCount.Bucket - 1]); + for(int i=0; i x.Bucket == i + 1).ToArray(); + if (target.Any()) { + centroids[i] = (int)target.Average(x => x.Count); + } + } + } + + return tagCounts; + }); + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Tags/Views/EditorTemplates/Parts/TagCloud.cshtml b/src/Orchard.Web/Modules/Orchard.Tags/Views/EditorTemplates/Parts/TagCloud.cshtml new file mode 100644 index 000000000..932694e2e --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Tags/Views/EditorTemplates/Parts/TagCloud.cshtml @@ -0,0 +1,20 @@ +@model Orchard.Tags.Models.TagCloudPart + +
+ Tag Cloud + +
+ @Html.LabelFor(model => model.Buckets) + @Html.TextBoxFor(model => model.Buckets, new { @class = "text small" }) + @Html.ValidationMessageFor(model => model.Buckets) + @T("The number of buckets determines how many different sizes of tags will get rendered in the cloud.") +
+ +
+ @Html.LabelFor(model => model.Slug) + @Html.TextBoxFor(model => model.Slug, new { @class = "text medium" }) + @Html.ValidationMessageFor(model => model.Slug) + @T("Enter the slug of the container for which you want the tag cloud, e.g. blog, /, or leave empty for the cloud to be scoped to the whole site. ") +
+ +
\ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Tags/Views/Parts/TagCloud.cshtml b/src/Orchard.Web/Modules/Orchard.Tags/Views/Parts/TagCloud.cshtml new file mode 100644 index 000000000..8dde42e1c --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Tags/Views/Parts/TagCloud.cshtml @@ -0,0 +1,12 @@ +@using Orchard.Tags.Models +
    +@foreach (TagCount tagCount in Model.TagCounts) { +
  • + @Html.ActionLink(tagCount.TagName, "Search", "Home", new { + area = "Orchard.Tags", + tagName = tagCount.TagName + }, null + ) +
  • +} +
\ No newline at end of file diff --git a/src/Orchard.Web/Themes/TheThemeMachine/Styles/Site.css b/src/Orchard.Web/Themes/TheThemeMachine/Styles/Site.css index 3a7c137d0..0266ef901 100644 --- a/src/Orchard.Web/Themes/TheThemeMachine/Styles/Site.css +++ b/src/Orchard.Web/Themes/TheThemeMachine/Styles/Site.css @@ -639,3 +639,30 @@ button:focus, .button:focus { background: -webkit-gradient(linear, left top, left bottom, from(#e1e1e1), to(#ebebeb)); background:-moz-linear-gradient(top , #e1e1e1, #ebebeb); } + +/* Tag Cloud +***************************************************************/ +.tag-cloud li { + list-style: none; + display: inline; +} + +.tag-cloud-tag-1 { + font-size: 1em; +} + +.tag-cloud-tag-2 { + font-size: 1.1em; +} + +.tag-cloud-tag-3 { + font-size: 1.2em; +} + +.tag-cloud-tag-4 { + font-size: 1.4em; +} + +.tag-cloud-tag-5 { + font-size: 1.5em; +}