mirror of
https://github.com/OrchardCMS/Orchard.git
synced 2025-09-23 12:53:33 +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 {
|
||||
public class RoutableController : Controller {
|
||||
private readonly IRoutableService _routableService;
|
||||
|
||||
public RoutableController(IRoutableService routableService) {
|
||||
_routableService = routableService;
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public ActionResult Slugify(FormCollection formCollection, string value) {
|
||||
if (!string.IsNullOrEmpty(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);
|
||||
return Json(_routableService.Slugify(value));
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,3 +1,4 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Orchard.Core.Common.Records;
|
||||
using Orchard.ContentManagement;
|
||||
|
||||
@@ -7,6 +8,8 @@ namespace Orchard.Core.Common.Models {
|
||||
get { return Record.Title; }
|
||||
set { Record.Title = value; }
|
||||
}
|
||||
|
||||
[Required]
|
||||
public string Slug {
|
||||
get { return Record.Slug; }
|
||||
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; }
|
||||
}
|
||||
|
||||
[Required]
|
||||
public string Slug {
|
||||
get { return RoutableAspect.Record.Slug; }
|
||||
set { RoutableAspect.Record.Slug = value; }
|
||||
|
@@ -67,6 +67,8 @@
|
||||
<Compile Include="Common\Permissions.cs" />
|
||||
<Compile Include="Common\Records\CommonVersionRecord.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\Providers\CommonAspectHandler.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
|
||||
BlogPost blogPost = _services.ContentManager.Create<BlogPost>("blogpost", publishNow ? VersionOptions.Published : VersionOptions.Draft,
|
||||
bp => {
|
||||
bp.Blog = blog;
|
||||
model.BlogPost = _services.ContentManager.UpdateEditorModel(_services.ContentManager.New<BlogPost>("blogpost"), this);
|
||||
model.BlogPost.Item.Blog = blog;
|
||||
if (!publishNow && publishDate != null)
|
||||
bp.Published = publishDate.Value;
|
||||
});
|
||||
model.BlogPost = _services.ContentManager.UpdateEditorModel(blogPost, this);
|
||||
model.BlogPost.Item.Published = publishDate.Value;
|
||||
|
||||
if (!ModelState.IsValid) {
|
||||
_services.TransactionManager.Cancel();
|
||||
@@ -89,6 +86,8 @@ namespace Orchard.Blogs.Controllers {
|
||||
return View(model);
|
||||
}
|
||||
|
||||
_services.ContentManager.Create(model.BlogPost.Item.ContentItem, publishNow ? VersionOptions.Published : VersionOptions.Draft);
|
||||
|
||||
//TEMP: (erikpo) ensure information has committed for this record
|
||||
var session = _sessionLocator.For(typeof(ContentItemRecord));
|
||||
session.Flush();
|
||||
|
@@ -14,6 +14,7 @@ namespace Orchard.Blogs.Models {
|
||||
//TODO: (erikpo) Need a data type for slug
|
||||
public string Slug {
|
||||
get { return this.As<RoutableAspect>().Slug; }
|
||||
set { this.As<RoutableAspect>().Slug = value; }
|
||||
}
|
||||
|
||||
public string Description {
|
||||
|
@@ -1,17 +1,32 @@
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using Orchard.Blogs.Controllers;
|
||||
using Orchard.Core.Common.Models;
|
||||
using Orchard.Data;
|
||||
using Orchard.Blogs.Services;
|
||||
using Orchard.ContentManagement.Handlers;
|
||||
using Orchard.Core.Common.Models;
|
||||
using Orchard.Core.Common.Services;
|
||||
using Orchard.Data;
|
||||
|
||||
namespace Orchard.Blogs.Models {
|
||||
[UsedImplicitly]
|
||||
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<CommonAspect>(BlogDriver.ContentType.Name));
|
||||
Filters.Add(new ActivatingFilter<RoutableAspect>(BlogDriver.ContentType.Name));
|
||||
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 {
|
||||
get { return this.As<RoutableAspect>().Slug; }
|
||||
set { this.As<RoutableAspect>().Slug = value; }
|
||||
}
|
||||
|
||||
public Blog Blog {
|
||||
|
@@ -1,14 +1,18 @@
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using Orchard.Blogs.Controllers;
|
||||
using Orchard.Blogs.Services;
|
||||
using Orchard.ContentManagement;
|
||||
using Orchard.Core.Common.Models;
|
||||
using Orchard.ContentManagement.Handlers;
|
||||
using Orchard.Core.Common.Records;
|
||||
using Orchard.Core.Common.Services;
|
||||
using Orchard.Data;
|
||||
|
||||
namespace Orchard.Blogs.Models {
|
||||
[UsedImplicitly]
|
||||
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<CommonAspect>(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++);
|
||||
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