mirror of
https://github.com/OrchardCMS/Orchard.git
synced 2026-02-09 09:16:41 +08:00
Adding Orchard.Tags.TagCloud
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
23
src/Orchard.Web/Modules/Orchard.Tags/Models/TagCloudPart.cs
Normal file
23
src/Orchard.Web/Modules/Orchard.Tags/Models/TagCloudPart.cs
Normal 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); }
|
||||
}
|
||||
}
|
||||
}
|
||||
12
src/Orchard.Web/Modules/Orchard.Tags/Models/TagCount.cs
Normal file
12
src/Orchard.Web/Modules/Orchard.Tags/Models/TagCount.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
127
src/Orchard.Web/Modules/Orchard.Tags/Services/TagCloudService.cs
Normal file
127
src/Orchard.Web/Modules/Orchard.Tags/Services/TagCloudService.cs
Normal 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;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user