Adding Orchard.Tags.TagCloud

This commit is contained in:
Sebastien Ros
2014-02-20 14:04:45 -08:00
parent 8463034c47
commit 6fd1156c2a
13 changed files with 344 additions and 1 deletions

View File

@@ -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<TagCloudPart> {
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);
}
}
}

View File

@@ -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<TagCloudPart>((context, part) => part
._tagCountField.Loader(tags =>
tagCloudService.GetPopularTags(part.Buckets, part.Slug).ToList()
));
OnPublished<TagsPart>((context, part) => InvalidateTagCloudCache());
OnRemoved<TagsPart>((context, part) => InvalidateTagCloudCache());
OnUnpublished<TagsPart>((context, part) => InvalidateTagCloudCache());
}
public void InvalidateTagCloudCache() {
_signals.Trigger(TagCloudService.TagCloudTagsChanged);
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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<IList<TagCount>> _tagCountField = new LazyField<IList<TagCount>>();
public IList<TagCount> 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); }
}
}
}

View File

@@ -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; }
}
}

View File

@@ -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

View File

@@ -67,16 +67,22 @@
<ItemGroup>
<Compile Include="AdminMenu.cs" />
<Compile Include="Controllers\AdminController.cs" />
<Compile Include="Drivers\TagCloudDriver.cs" />
<Compile Include="Feeds\TagsFeedItemBuilder.cs" />
<Compile Include="Handlers\TagCloudHandler.cs" />
<Compile Include="Migrations.cs" />
<Compile Include="Models\ContentTagRecord.cs" />
<Compile Include="Models\TagCloudPart.cs" />
<Compile Include="Models\TagCount.cs" />
<Compile Include="Models\TagsPartRecord.cs" />
<Compile Include="Projections\TagsFilter.cs">
<SubType>Code</SubType>
</Compile>
<Compile Include="Projections\TagsFilterForms.cs" />
<Compile Include="ResourceManifest.cs" />
<Compile Include="Services\ITagCloudService.cs" />
<Compile Include="Services\ITagService.cs" />
<Compile Include="Services\TagCloudService.cs" />
<Compile Include="Services\XmlRpcHandler.cs" />
<Compile Include="ViewModels\EditTagsViewModel.cs" />
<Compile Include="Controllers\HomeController.cs" />
@@ -118,6 +124,10 @@
<Project>{9916839C-39FC-4CEB-A5AF-89CA7E87119F}</Project>
<Name>Orchard.Core</Name>
</ProjectReference>
<ProjectReference Include="..\Orchard.Autoroute\Orchard.Autoroute.csproj">
<Project>{66FCCD76-2761-47E3-8D11-B45D0001DDAA}</Project>
<Name>Orchard.Autoroute</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<Content Include="Placement.info">
@@ -136,7 +146,12 @@
<ItemGroup>
<Content Include="Scripts\Web.config" />
</ItemGroup>
<ItemGroup />
<ItemGroup>
<Content Include="Views\Parts\TagCloud.cshtml" />
</ItemGroup>
<ItemGroup>
<Content Include="Views\EditorTemplates\Parts\TagCloud.cshtml" />
</ItemGroup>
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">10.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>

View File

@@ -6,4 +6,8 @@
<Match DisplayType="Summary">
<Place Parts_Tags_ShowTags="Header:after.7"/>
</Match>
<Place Parts_TagCloud="Content:5"/>
<Place Parts_TagCloud_Edit="Content:7"/>
</Placement>

View File

@@ -0,0 +1,8 @@
using System.Collections.Generic;
using Orchard.Tags.Models;
namespace Orchard.Tags.Services {
public interface ITagCloudService : IDependency {
IEnumerable<TagCount> GetPopularTags(int buckets, string slug);
}
}

View File

@@ -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<ContentTagRecord> _contentTagRepository;
private readonly IRepository<AutoroutePartRecord> _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<ContentTagRecord> contentTagRepository,
IRepository<AutoroutePartRecord> autorouteRepository,
IContentManager contentManager,
ICacheManager cacheManager,
ISignals signals) {
_contentTagRepository = contentTagRepository;
_autorouteRepository = autorouteRepository;
_contentManager = contentManager;
_cacheManager = cacheManager;
_signals = signals;
}
public IEnumerable<TagCount> GetPopularTags(int buckets, string slug) {
var cacheKey = "Orchard.Tags.TagCloud." + (slug ?? "") + '.' + buckets;
return _cacheManager.Get(cacheKey,
ctx => {
ctx.Monitor(_signals.When(TagCloudTagsChanged));
IEnumerable<TagCount> 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<TagCount>();
}
tagCounts = _contentManager
.Query<TagsPart, TagsPartRecord>(VersionOptions.Published)
.Join<CommonPartRecord>()
.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<TagCount>();
}
}
// 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<buckets; i++) {
var distance = Math.Abs(tagCount.Count - centroids[i]);
if (distance < currentDistance) {
tagCount.Bucket = i + 1;
currentDistance = distance;
balanced = false;
}
}
}
// recalculate centroids
for (int i = 0; i < buckets; i++) {
var target = tagCounts.Where(x => x.Bucket == i + 1).ToArray();
if (target.Any()) {
centroids[i] = (int)target.Average(x => x.Count);
}
}
}
return tagCounts;
});
}
}
}

View File

@@ -0,0 +1,20 @@
@model Orchard.Tags.Models.TagCloudPart
<fieldset>
<legend>Tag Cloud</legend>
<div class="group">
@Html.LabelFor(model => model.Buckets)
@Html.TextBoxFor(model => model.Buckets, new { @class = "text small" })
@Html.ValidationMessageFor(model => model.Buckets)
<span class="hint">@T("The number of buckets determines how many different sizes of tags will get rendered in the cloud.")</span>
</div>
<div class="group">
@Html.LabelFor(model => model.Slug)
@Html.TextBoxFor(model => model.Slug, new { @class = "text medium" })
@Html.ValidationMessageFor(model => model.Slug)
<span class="hint">@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. ")</span>
</div>
</fieldset>

View File

@@ -0,0 +1,12 @@
@using Orchard.Tags.Models
<ul class="tag-cloud">
@foreach (TagCount tagCount in Model.TagCounts) {
<li class="tag-cloud-tag tag-cloud-tag-@tagCount.Bucket">
@Html.ActionLink(tagCount.TagName, "Search", "Home", new {
area = "Orchard.Tags",
tagName = tagCount.TagName
}, null
)
</li>
}
</ul>

View File

@@ -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;
}