Some work on (blog and blogpost) slug generation

--HG--
extra : convert_revision : svn%3A5ff7c347-ad56-4c35-b696-ccb81de16e03/trunk%4045497
This commit is contained in:
skewed
2010-01-15 23:44:22 +00:00
parent 88924471b7
commit ee7ed66480
11 changed files with 121 additions and 33 deletions

View File

@@ -1,28 +1,17 @@
using System.Text.RegularExpressions; using System.Web.Mvc;
using System.Web.Mvc; using Orchard.Core.Common.Services;
namespace Orchard.Core.Common.Controllers { namespace Orchard.Core.Common.Controllers {
public class RoutableController : Controller { public class RoutableController : Controller {
private readonly IRoutableService _routableService;
public RoutableController(IRoutableService routableService) {
_routableService = routableService;
}
[HttpPost] [HttpPost]
public ActionResult Slugify(FormCollection formCollection, string value) { public ActionResult Slugify(FormCollection formCollection, string value) {
if (!string.IsNullOrEmpty(value)) { return Json(_routableService.Slugify(value));
//todo: (heskew) improve - just doing multi-pass regex replaces for now with the simple rules of
// (1) can't begin with a '/', (2) can't have adjacent '/'s and (3) can't have these characters
var startsoffbad = new Regex(@"^[\s/]+");
var slashhappy = new Regex("/{2,}");
var dissallowed = new Regex(@"[:?#\[\]@!$&'()*+,;=\s]+");
value = value.Trim();
value = startsoffbad.Replace(value, "-");
value = slashhappy.Replace(value, "/");
value = dissallowed.Replace(value, "-");
if (value.Length > 1000)
value = value.Substring(0, 1000);
}
return Json(value);
} }
} }
} }

View File

@@ -1,3 +1,4 @@
using System.ComponentModel.DataAnnotations;
using Orchard.Core.Common.Records; using Orchard.Core.Common.Records;
using Orchard.ContentManagement; using Orchard.ContentManagement;
@@ -7,6 +8,8 @@ namespace Orchard.Core.Common.Models {
get { return Record.Title; } get { return Record.Title; }
set { Record.Title = value; } set { Record.Title = value; }
} }
[Required]
public string Slug { public string Slug {
get { return Record.Slug; } get { return Record.Slug; }
set { Record.Slug = value; } set { Record.Slug = value; }

View File

@@ -0,0 +1,8 @@
using System.Collections.Generic;
namespace Orchard.Core.Common.Services {
public interface IRoutableService : IDependency {
string Slugify(string title);
string GenerateUniqueSlug(string slugCandidate, IEnumerable<string> existingSlugs);
}
}

View File

@@ -0,0 +1,54 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
namespace Orchard.Core.Common.Services {
public class RoutableService : IRoutableService {
#region IRoutableService Members
public string Slugify(string title) {
if (!string.IsNullOrEmpty(title)) {
//todo: (heskew) improve - just doing multi-pass regex replaces for now with the simple rules of
// (1) can't begin with a '/', (2) can't have adjacent '/'s and (3) can't have these characters
var startsoffbad = new Regex(@"^[\s/]+");
var slashhappy = new Regex("/{2,}");
var dissallowed = new Regex(@"[:?#\[\]@!$&'()*+,;=\s]+");
title = title.Trim();
title = startsoffbad.Replace(title, "-");
title = slashhappy.Replace(title, "/");
title = dissallowed.Replace(title, "-");
if (title.Length > 1000) {
title = title.Substring(0, 1000);
}
}
return title;
}
public string GenerateUniqueSlug(string slugCandidate, IEnumerable<string> existingSlugs) {
int? version = existingSlugs
.Select(s => {
int v;
string[] slugParts = s.Split(new[] { slugCandidate }, StringSplitOptions.RemoveEmptyEntries);
if (slugParts.Length == 0) {
return 0;
}
return int.TryParse(slugParts[0].TrimStart('-'), out v)
? (int?) ++v
: null;
})
.OrderBy(i => i)
.LastOrDefault();
return version != null
? string.Format("{0}-{1}", slugCandidate, version)
: slugCandidate;
}
#endregion
}
}

View File

@@ -11,7 +11,6 @@ namespace Orchard.Core.Common.ViewModels {
set { RoutableAspect.Record.Title = value; } set { RoutableAspect.Record.Title = value; }
} }
[Required]
public string Slug { public string Slug {
get { return RoutableAspect.Record.Slug; } get { return RoutableAspect.Record.Slug; }
set { RoutableAspect.Record.Slug = value; } set { RoutableAspect.Record.Slug = value; }

View File

@@ -67,6 +67,8 @@
<Compile Include="Common\Permissions.cs" /> <Compile Include="Common\Permissions.cs" />
<Compile Include="Common\Records\CommonVersionRecord.cs" /> <Compile Include="Common\Records\CommonVersionRecord.cs" />
<Compile Include="Common\Routes.cs" /> <Compile Include="Common\Routes.cs" />
<Compile Include="Common\Services\IRoutableService.cs" />
<Compile Include="Common\Services\RoutableService.cs" />
<Compile Include="Common\Utilities\LazyField.cs" /> <Compile Include="Common\Utilities\LazyField.cs" />
<Compile Include="Common\Providers\CommonAspectHandler.cs" /> <Compile Include="Common\Providers\CommonAspectHandler.cs" />
<Compile Include="Common\Models\CommonAspect.cs" /> <Compile Include="Common\Models\CommonAspect.cs" />

View File

@@ -75,13 +75,10 @@ namespace Orchard.Blogs.Controllers {
} }
//TODO: (erikpo) Evaluate if publish options should be moved into create or out of create to keep it clean //TODO: (erikpo) Evaluate if publish options should be moved into create or out of create to keep it clean
BlogPost blogPost = _services.ContentManager.Create<BlogPost>("blogpost", publishNow ? VersionOptions.Published : VersionOptions.Draft, model.BlogPost = _services.ContentManager.UpdateEditorModel(_services.ContentManager.New<BlogPost>("blogpost"), this);
bp => { model.BlogPost.Item.Blog = blog;
bp.Blog = blog; if (!publishNow && publishDate != null)
if (!publishNow && publishDate != null) model.BlogPost.Item.Published = publishDate.Value;
bp.Published = publishDate.Value;
});
model.BlogPost = _services.ContentManager.UpdateEditorModel(blogPost, this);
if (!ModelState.IsValid) { if (!ModelState.IsValid) {
_services.TransactionManager.Cancel(); _services.TransactionManager.Cancel();
@@ -89,6 +86,8 @@ namespace Orchard.Blogs.Controllers {
return View(model); return View(model);
} }
_services.ContentManager.Create(model.BlogPost.Item.ContentItem, publishNow ? VersionOptions.Published : VersionOptions.Draft);
//TEMP: (erikpo) ensure information has committed for this record //TEMP: (erikpo) ensure information has committed for this record
var session = _sessionLocator.For(typeof(ContentItemRecord)); var session = _sessionLocator.For(typeof(ContentItemRecord));
session.Flush(); session.Flush();

View File

@@ -14,6 +14,7 @@ namespace Orchard.Blogs.Models {
//TODO: (erikpo) Need a data type for slug //TODO: (erikpo) Need a data type for slug
public string Slug { public string Slug {
get { return this.As<RoutableAspect>().Slug; } get { return this.As<RoutableAspect>().Slug; }
set { this.As<RoutableAspect>().Slug = value; }
} }
public string Description { public string Description {

View File

@@ -1,17 +1,32 @@
using System.Linq;
using JetBrains.Annotations; using JetBrains.Annotations;
using Orchard.Blogs.Controllers; using Orchard.Blogs.Controllers;
using Orchard.Core.Common.Models; using Orchard.Blogs.Services;
using Orchard.Data;
using Orchard.ContentManagement.Handlers; using Orchard.ContentManagement.Handlers;
using Orchard.Core.Common.Models;
using Orchard.Core.Common.Services;
using Orchard.Data;
namespace Orchard.Blogs.Models { namespace Orchard.Blogs.Models {
[UsedImplicitly] [UsedImplicitly]
public class BlogHandler : ContentHandler { public class BlogHandler : ContentHandler {
public BlogHandler(IRepository<BlogRecord> repository) { public BlogHandler(IRepository<BlogRecord> repository, IBlogService blogService,
IRoutableService routableService) {
Filters.Add(new ActivatingFilter<Blog>(BlogDriver.ContentType.Name)); Filters.Add(new ActivatingFilter<Blog>(BlogDriver.ContentType.Name));
Filters.Add(new ActivatingFilter<CommonAspect>(BlogDriver.ContentType.Name)); Filters.Add(new ActivatingFilter<CommonAspect>(BlogDriver.ContentType.Name));
Filters.Add(new ActivatingFilter<RoutableAspect>(BlogDriver.ContentType.Name)); Filters.Add(new ActivatingFilter<RoutableAspect>(BlogDriver.ContentType.Name));
Filters.Add(new StorageFilter<BlogRecord>(repository)); Filters.Add(new StorageFilter<BlogRecord>(repository));
OnCreating<Blog>((context, blog) => {
string slug = !string.IsNullOrEmpty(blog.Slug)
? blog.Slug
: routableService.Slugify(blog.Name);
blog.Slug = routableService.GenerateUniqueSlug(slug,
blogService.Get().Where(
b => b.Slug.StartsWith(slug)).Select(
b => b.Slug));
});
} }
} }
} }

View File

@@ -16,6 +16,7 @@ namespace Orchard.Blogs.Models {
public string Slug { public string Slug {
get { return this.As<RoutableAspect>().Slug; } get { return this.As<RoutableAspect>().Slug; }
set { this.As<RoutableAspect>().Slug = value; }
} }
public Blog Blog { public Blog Blog {

View File

@@ -1,14 +1,18 @@
using System.Linq;
using JetBrains.Annotations; using JetBrains.Annotations;
using Orchard.Blogs.Controllers; using Orchard.Blogs.Controllers;
using Orchard.Blogs.Services;
using Orchard.ContentManagement;
using Orchard.Core.Common.Models; using Orchard.Core.Common.Models;
using Orchard.ContentManagement.Handlers; using Orchard.ContentManagement.Handlers;
using Orchard.Core.Common.Records; using Orchard.Core.Common.Records;
using Orchard.Core.Common.Services;
using Orchard.Data; using Orchard.Data;
namespace Orchard.Blogs.Models { namespace Orchard.Blogs.Models {
[UsedImplicitly] [UsedImplicitly]
public class BlogPostHandler : ContentHandler { public class BlogPostHandler : ContentHandler {
public BlogPostHandler(IRepository<CommonVersionRecord> commonRepository) { public BlogPostHandler(IRepository<CommonVersionRecord> commonRepository, IBlogPostService blogPostService, IRoutableService routableService) {
Filters.Add(new ActivatingFilter<BlogPost>(BlogPostDriver.ContentType.Name)); Filters.Add(new ActivatingFilter<BlogPost>(BlogPostDriver.ContentType.Name));
Filters.Add(new ActivatingFilter<CommonAspect>(BlogPostDriver.ContentType.Name)); Filters.Add(new ActivatingFilter<CommonAspect>(BlogPostDriver.ContentType.Name));
Filters.Add(new ActivatingFilter<RoutableAspect>(BlogPostDriver.ContentType.Name)); Filters.Add(new ActivatingFilter<RoutableAspect>(BlogPostDriver.ContentType.Name));
@@ -17,6 +21,19 @@ namespace Orchard.Blogs.Models {
OnCreated<BlogPost>((context, bp) => bp.Blog.PostCount++); OnCreated<BlogPost>((context, bp) => bp.Blog.PostCount++);
OnRemoved<BlogPost>((context, bp) => bp.Blog.PostCount--); OnRemoved<BlogPost>((context, bp) => bp.Blog.PostCount--);
OnCreating<BlogPost>((context, blogPost) =>
{
string slug = !string.IsNullOrEmpty(blogPost.Slug)
? blogPost.Slug
: routableService.Slugify(blogPost.Title);
blogPost.Slug = routableService.GenerateUniqueSlug(slug,
blogPostService.Get(blogPost.Blog, VersionOptions.Published).Where(
bp => bp.Slug.StartsWith(slug)).Select(
bp => bp.Slug));
});
} }
} }
} }