mirror of
https://github.com/OrchardCMS/Orchard.git
synced 2025-09-23 21:13:35 +08:00
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:
@@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -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; }
|
||||||
|
8
src/Orchard.Web/Core/Common/Services/IRoutableService.cs
Normal file
8
src/Orchard.Web/Core/Common/Services/IRoutableService.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
54
src/Orchard.Web/Core/Common/Services/RoutableService.cs
Normal file
54
src/Orchard.Web/Core/Common/Services/RoutableService.cs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
@@ -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; }
|
||||||
|
@@ -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" />
|
||||||
|
@@ -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();
|
||||||
|
@@ -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 {
|
||||||
|
@@ -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));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -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 {
|
||||||
|
@@ -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));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
Reference in New Issue
Block a user