- Slug generation (for Blog, BlogPost and Page) reworked so a page can set its own slug rules and all content types get better AJAX-ified slug generation now that it is (and can be) content type aware (except for BlogPost, it needs some context about its parent so it can gen a unique slug for a post within a given blog)

- Page edit actions changed over to use id instead of slug (others should follow suit since slug uniqueness is only guaranteed among published content items...where applicable, there is no notion of a "published" blog)

--HG--
extra : convert_revision : svn%3A5ff7c347-ad56-4c35-b696-ccb81de16e03/trunk%4045722
This commit is contained in:
skewed
2010-01-20 01:19:16 +00:00
parent 28cb6715af
commit 03d7d076f2
19 changed files with 332 additions and 112 deletions

View File

@@ -20,35 +20,35 @@ namespace Orchard.Core.Tests.Common.Services {
private IRoutableService _routableService; private IRoutableService _routableService;
[Test] //[Test]
public void BeginningSlashesShouldBeReplacedByADash() { //public void BeginningSlashesShouldBeReplacedByADash() {
Assert.That(_routableService.Slugify("/slug"), Is.EqualTo("-slug")); // Assert.That(_routableService.Slugify("/slug"), Is.EqualTo("-slug"));
Assert.That(_routableService.Slugify("//slug"), Is.EqualTo("-slug")); // Assert.That(_routableService.Slugify("//slug"), Is.EqualTo("-slug"));
Assert.That(_routableService.Slugify("//////////////slug"), Is.EqualTo("-slug")); // Assert.That(_routableService.Slugify("//////////////slug"), Is.EqualTo("-slug"));
} //}
[Test] //[Test]
public void MultipleSlashesShouldBecomeOne() { //public void MultipleSlashesShouldBecomeOne() {
Assert.That(_routableService.Slugify("/slug//with///lots/of////s/lashes"), Is.EqualTo("-slug/with/lots/of/s/lashes")); // Assert.That(_routableService.Slugify("/slug//with///lots/of////s/lashes"), Is.EqualTo("-slug/with/lots/of/s/lashes"));
Assert.That(_routableService.Slugify("slug/with/a/couple//slashes"), Is.EqualTo("slug/with/a/couple/slashes")); // Assert.That(_routableService.Slugify("slug/with/a/couple//slashes"), Is.EqualTo("slug/with/a/couple/slashes"));
} //}
[Test] //[Test]
public void InvalidCharactersShouldBeReplacedByADash() { //public void InvalidCharactersShouldBeReplacedByADash() {
Assert.That(_routableService.Slugify( // Assert.That(_routableService.Slugify(
"Please do not use any of the following characters in your slugs: \":\", \"/\", \"?\", \"#\", \"[\", \"]\", \"@\", \"!\", \"$\", \"&\", \"'\", \"(\", \")\", \"*\", \"+\", \",\", \";\", \"=\""), // "Please do not use any of the following characters in your slugs: \":\", \"/\", \"?\", \"#\", \"[\", \"]\", \"@\", \"!\", \"$\", \"&\", \"'\", \"(\", \")\", \"*\", \"+\", \",\", \";\", \"=\""),
Is.EqualTo( // Is.EqualTo(
"Please-do-not-use-any-of-the-following-characters-in-your-slugs-\"-\"-\"/\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"")); // "Please-do-not-use-any-of-the-following-characters-in-your-slugs-\"-\"-\"/\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\""));
} //}
[Test] //[Test]
public void VeryLongStringTruncatedTo1000Chars() { //public void VeryLongStringTruncatedTo1000Chars() {
var veryVeryLongSlug = "this is a very long slug..."; // var veryVeryLongSlug = "this is a very long slug...";
for (var i = 0; i < 100; i++) // for (var i = 0; i < 100; i++)
veryVeryLongSlug += "aaaaaaaaaa"; // veryVeryLongSlug += "aaaaaaaaaa";
Assert.That(veryVeryLongSlug.Length, Is.AtLeast(1001)); // Assert.That(veryVeryLongSlug.Length, Is.AtLeast(1001));
Assert.That(_routableService.Slugify(veryVeryLongSlug).Length, Is.EqualTo(1000)); // Assert.That(_routableService.Slugify(veryVeryLongSlug).Length, Is.EqualTo(1000));
} //}
} }
} }

View File

@@ -1,17 +1,40 @@
using System.Web.Mvc; using System.Web.Mvc;
using Orchard.ContentManagement;
using Orchard.Core.Common.Models;
using Orchard.Core.Common.Services; using Orchard.Core.Common.Services;
using Orchard.Localization;
namespace Orchard.Core.Common.Controllers { namespace Orchard.Core.Common.Controllers {
public class RoutableController : Controller { public class RoutableController : Controller, IUpdateModel {
private readonly IRoutableService _routableService; private readonly IRoutableService _routableService;
private readonly IContentManager _contentManager;
private readonly IOrchardServices _orchardServices;
public RoutableController(IRoutableService routableService) { public RoutableController(IRoutableService routableService, IContentManager contentManager, IOrchardServices orchardServices) {
_routableService = routableService; _routableService = routableService;
_contentManager = contentManager;
_orchardServices = orchardServices;
} }
[HttpPost] [HttpPost]
public ActionResult Slugify(FormCollection formCollection, string value) { public ActionResult Slugify(FormCollection formCollection, string contentType) {
return Json(_routableService.Slugify(value)); var slug = "";
if (string.IsNullOrEmpty(contentType))
return Json(slug);
var contentItem = _contentManager.New(contentType);
_contentManager.UpdateEditorModel(contentItem, this);
return Json(contentItem.As<RoutableAspect>().Slug);
}
bool IUpdateModel.TryUpdateModel<TModel>(TModel model, string prefix, string[] includeProperties, string[] excludeProperties) {
return TryUpdateModel(model, prefix, includeProperties, excludeProperties);
}
void IUpdateModel.AddModelError(string key, LocalizedString errorMessage) {
ModelState.AddModelError(key, errorMessage.ToString());
} }
} }
} }

View File

@@ -1,4 +1,3 @@
using System.ComponentModel.DataAnnotations;
using Orchard.Core.Common.Records; using Orchard.Core.Common.Records;
using Orchard.ContentManagement; using Orchard.ContentManagement;
@@ -9,7 +8,6 @@ namespace Orchard.Core.Common.Models {
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

@@ -1,9 +1,22 @@
jQuery.fn.extend({ jQuery.fn.extend({
slugify: function(options) { slugify: function(options) {
//todo: (heskew) need messaging system //todo: (heskew) need messaging system
if (!options.target || !options.url) return; if (!options.target || !options.url)
jQuery.post(options.url, { value: $(this).val(), __RequestVerificationToken: $("input[name=__RequestVerificationToken]").val() }, function(data) { return;
var args = {
"contentType": options.contentType,
__RequestVerificationToken: $("input[name=__RequestVerificationToken]").val()
};
args[$(this).attr("name")] = $(this).val();
jQuery.post(
options.url,
args,
function(data) {
options.target.val(data); options.target.val(data);
}, "json"); },
"json"
);
} }
}); });

View File

@@ -1,8 +1,11 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using Orchard.Core.Common.Models;
namespace Orchard.Core.Common.Services { namespace Orchard.Core.Common.Services {
public interface IRoutableService : IDependency { public interface IRoutableService : IDependency {
string Slugify(string title); void FillSlug<TModel>(TModel model) where TModel : RoutableAspect;
void FillSlug<TModel>(TModel model, Func<string, string> generateSlug) where TModel : RoutableAspect;
string GenerateUniqueSlug(string slugCandidate, IEnumerable<string> existingSlugs); string GenerateUniqueSlug(string slugCandidate, IEnumerable<string> existingSlugs);
} }
} }

View File

@@ -2,37 +2,42 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Orchard.Core.Common.Models;
namespace Orchard.Core.Common.Services { namespace Orchard.Core.Common.Services {
public class RoutableService : IRoutableService { public class RoutableService : IRoutableService {
public string Slugify(string title) { public void FillSlug<TModel>(TModel model) where TModel : RoutableAspect {
if (!string.IsNullOrEmpty(title)) { if (!string.IsNullOrEmpty(model.Slug) || string.IsNullOrEmpty(model.Title))
//todo: (heskew) improve - just doing multi-pass regex replaces for now with the simple rules of return;
// (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(); var slug = model.Title;
title = startsoffbad.Replace(title, "-"); var dissallowed = new Regex(@"[/:?#\[\]@!$&'()*+,;=\s]+");
title = slashhappy.Replace(title, "/");
title = dissallowed.Replace(title, "-");
if (title.Length > 1000) { slug = dissallowed.Replace(slug, "-");
title = title.Substring(0, 1000); slug = slug.Trim('-');
}
if (slug.Length > 1000)
slug = slug.Substring(0, 1000);
model.Slug = slug;
} }
return title; public void FillSlug<TModel>(TModel model, Func<string, string> generateSlug) where TModel : RoutableAspect {
if (!string.IsNullOrEmpty(model.Slug) || string.IsNullOrEmpty(model.Title))
return;
model.Slug = generateSlug(model.Title);
} }
public string GenerateUniqueSlug(string slugCandidate, IEnumerable<string> existingSlugs) { public string GenerateUniqueSlug(string slugCandidate, IEnumerable<string> existingSlugs) {
int? version = existingSlugs int? version = existingSlugs
.Select(s => { .Select(s => {
int v; int v;
string[] slugParts = s.Split(new[] { slugCandidate }, StringSplitOptions.RemoveEmptyEntries); string[] slugParts = s.Split(new[] { slugCandidate }, StringSplitOptions.RemoveEmptyEntries);
if (slugParts.Length == 0) { if (slugParts.Length == 0) {
return 1; return 2;
} }
return int.TryParse(slugParts[0].TrimStart('-'), out v) return int.TryParse(slugParts[0].TrimStart('-'), out v)

View File

@@ -11,7 +11,6 @@ namespace Orchard.Core.Common.ViewModels {
set { RoutableAspect.Record.Title = value; } set { RoutableAspect.Record.Title = value; }
} }
[RegularExpression(@"^[^/:?#\[\]@!$&'()*+,;=\s](?(?=/)/[^/:?#\[\]@!$&'()*+,;=\s]|[^:?#\[\]@!$&'()*+,;=\s])*$")]
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

@@ -10,5 +10,14 @@
<span><%=Html.TextBoxFor(m => m.Slug, new { @class = "text" })%></span> <span><%=Html.TextBoxFor(m => m.Slug, new { @class = "text" })%></span>
</fieldset> </fieldset>
<% using (this.Capture("end-of-page-scripts")) { %> <% using (this.Capture("end-of-page-scripts")) { %>
<script type="text/javascript">$(function(){$("input#Routable_Title").blur(function(){$(this).slugify({target:$("input#Routable_Slug"),url:"<%=Url.Action("Slugify", "Routable", new {area = "Common"}) %>"})})})</script> <script type="text/javascript">
$(function(){
$("input#Routable_Title").blur(function(){
$(this).slugify({
target:$("input#Routable_Slug"),
url:"<%=Url.Action("Slugify", "Routable", new {area = "Common"}) %>",
contentType:"<%=Model.RoutableAspect.ContentItem.ContentType %>"
})
})
})</script>
<% } %> <% } %>

View File

@@ -1,12 +1,18 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions;
using System.Web.Routing; using System.Web.Routing;
using JetBrains.Annotations; using JetBrains.Annotations;
using Orchard.Blogs.Models; using Orchard.Blogs.Models;
using Orchard.Blogs.Services; using Orchard.Blogs.Services;
using Orchard.ContentManagement; using Orchard.ContentManagement;
using Orchard.ContentManagement.Drivers; using Orchard.ContentManagement.Drivers;
using Orchard.Core.Common.Models;
using Orchard.Core.Common.Services;
using Orchard.Localization;
using Orchard.Mvc.ViewModels; using Orchard.Mvc.ViewModels;
using Orchard.UI.Notify;
namespace Orchard.Blogs.Controllers { namespace Orchard.Blogs.Controllers {
[UsedImplicitly] [UsedImplicitly]
@@ -17,13 +23,22 @@ namespace Orchard.Blogs.Controllers {
}; };
private readonly IContentManager _contentManager; private readonly IContentManager _contentManager;
private readonly IBlogService _blogService;
private readonly IBlogPostService _blogPostService; private readonly IBlogPostService _blogPostService;
private readonly IRoutableService _routableService;
private readonly IOrchardServices _orchardServices;
public BlogDriver(IContentManager contentManager, IBlogPostService blogPostService) { public BlogDriver(IContentManager contentManager, IBlogService blogService, IBlogPostService blogPostService, IRoutableService routableService, IOrchardServices orchardServices) {
_contentManager = contentManager; _contentManager = contentManager;
_blogService = blogService;
_blogPostService = blogPostService; _blogPostService = blogPostService;
_routableService = routableService;
_orchardServices = orchardServices;
T = NullLocalizer.Instance;
} }
private Localizer T { get; set; }
protected override ContentType GetContentType() { protected override ContentType GetContentType() {
return ContentType; return ContentType;
} }
@@ -77,9 +92,48 @@ namespace Orchard.Blogs.Controllers {
protected override DriverResult Editor(Blog blog, IUpdateModel updater) { protected override DriverResult Editor(Blog blog, IUpdateModel updater) {
updater.TryUpdateModel(blog, Prefix, null, null); updater.TryUpdateModel(blog, Prefix, null, null);
//todo: (heskew) something better needs to be done with this...still feels shoehorned in here
ProcessSlug(blog, updater);
return Combined( return Combined(
ContentItemTemplate("Items/Blogs.Blog"), ContentItemTemplate("Items/Blogs.Blog"),
ContentPartTemplate(blog, "Parts/Blogs.Blog.Fields").Location("primary", "1")); ContentPartTemplate(blog, "Parts/Blogs.Blog.Fields").Location("primary", "1"));
} }
private void ProcessSlug(Blog blog, IUpdateModel updater) {
_routableService.FillSlug(blog.As<RoutableAspect>());
if (string.IsNullOrEmpty(blog.Slug)) {
return;
// OR
// updater.AddModelError("Routable.Slug", T("The slug is required.").ToString());
// return;
}
if (!Regex.IsMatch(blog.Slug, @"^[^/:?#\[\]@!$&'()*+,;=\s]+$")) {
//todo: (heskew) get rid of the hard-coded prefix
updater.AddModelError("Routable.Slug", T("Please do not use any of the following characters in your slugs: \"/\", \":\", \"?\", \"#\", \"[\", \"]\", \"@\", \"!\", \"$\", \"&\", \"'\", \"(\", \")\", \"*\", \"+\", \",\", \";\", \"=\". No spaces are allowed (please use dashes or underscores instead).").ToString());
}
var slugsLikeThis = _blogService.Get().Where(
b => b.Slug.StartsWith(blog.Slug, StringComparison.OrdinalIgnoreCase) &&
b.Id != blog.Id).Select(b => b.Slug);
//todo: (heskew) need better messages
if (slugsLikeThis.Count() > 0) {
var originalSlug = blog.Slug;
//todo: (heskew) make auto-uniqueness optional
blog.Slug = _routableService.GenerateUniqueSlug(blog.Slug, slugsLikeThis);
if (originalSlug != blog.Slug)
_orchardServices.Notifier.Warning(T("Slugs in conflict. \"{0}\" is already set for a previously created blog so this blog now has the slug \"{1}\"",
originalSlug, blog.Slug));
}
}
} }
} }

View File

@@ -1,17 +1,40 @@
using System.Web.Routing; using System;
using System.Linq;
using System.Text.RegularExpressions;
using System.Web.Routing;
using JetBrains.Annotations; using JetBrains.Annotations;
using Orchard.Blogs.Models; using Orchard.Blogs.Models;
using Orchard.Blogs.Services;
using Orchard.ContentManagement; using Orchard.ContentManagement;
using Orchard.ContentManagement.Drivers; using Orchard.ContentManagement.Drivers;
using Orchard.Core.Common.Models;
using Orchard.Core.Common.Services;
using Orchard.Localization;
using Orchard.UI.Notify;
namespace Orchard.Blogs.Controllers { namespace Orchard.Blogs.Controllers {
[UsedImplicitly] [UsedImplicitly]
public class BlogPostDriver : ContentItemDriver<BlogPost> { public class BlogPostDriver : ContentItemDriver<BlogPost> {
private readonly IBlogService _blogService;
private readonly IBlogPostService _blogPostService;
private readonly IRoutableService _routableService;
private readonly IOrchardServices _orchardServices;
public readonly static ContentType ContentType = new ContentType { public readonly static ContentType ContentType = new ContentType {
Name = "blogpost", Name = "blogpost",
DisplayName = "Blog Post" DisplayName = "Blog Post"
}; };
public BlogPostDriver(IBlogService blogService, IBlogPostService blogPostService, IRoutableService routableService, IOrchardServices orchardServices) {
_blogService = blogService;
_blogPostService = blogPostService;
_routableService = routableService;
_orchardServices = orchardServices;
T = NullLocalizer.Instance;
}
private Localizer T { get; set; }
protected override ContentType GetContentType() { protected override ContentType GetContentType() {
return ContentType; return ContentType;
} }
@@ -54,7 +77,50 @@ namespace Orchard.Blogs.Controllers {
protected override DriverResult Editor(BlogPost post, IUpdateModel updater) { protected override DriverResult Editor(BlogPost post, IUpdateModel updater) {
updater.TryUpdateModel(post, Prefix, null, null); updater.TryUpdateModel(post, Prefix, null, null);
//todo: (heskew) something better needs to be done with this...still feels shoehorned in here
ProcessSlug(post, updater);
return Editor(post); return Editor(post);
} }
private void ProcessSlug(BlogPost post, IUpdateModel updater) {
_routableService.FillSlug(post.As<RoutableAspect>());
if (string.IsNullOrEmpty(post.Slug)) {
return;
// OR
//updater.AddModelError("Routable.Slug", T("The slug is required.").ToString());
//return;
}
if (!Regex.IsMatch(post.Slug, @"^[^/:?#\[\]@!$&'()*+,;=\s]+$")) {
//todo: (heskew) get rid of the hard-coded prefix
updater.AddModelError("Routable.Slug", T("Please do not use any of the following characters in your slugs: \"/\", \":\", \"?\", \"#\", \"[\", \"]\", \"@\", \"!\", \"$\", \"&\", \"'\", \"(\", \")\", \"*\", \"+\", \",\", \";\", \"=\". No spaces are allowed (please use dashes or underscores instead).").ToString());
return;
}
var slugsLikeThis = _blogPostService.Get(post.Blog, VersionOptions.Published).Where(
p => p.Slug.StartsWith(post.Slug, StringComparison.OrdinalIgnoreCase) &&
p.Id != post.Id).Select(p => p.Slug);
//todo: (heskew) need better messages
if (slugsLikeThis.Count() > 0) {
//todo: (heskew) need better messages
_orchardServices.Notifier.Warning(T("A different blog post is already published with this same slug."));
if (post.ContentItem.VersionRecord.Published) {
var originalSlug = post.Slug;
//todo: (heskew) make auto-uniqueness optional
post.Slug = _routableService.GenerateUniqueSlug(post.Slug, slugsLikeThis);
if (originalSlug != post.Slug)
_orchardServices.Notifier.Warning(T("Slugs in conflict. \"{0}\" is already set for a previously created blog post so this post now has the slug \"{1}\"",
originalSlug, post.Slug));
}
}
}
} }
} }

View File

@@ -1,32 +1,17 @@
using System.Linq;
using JetBrains.Annotations; using JetBrains.Annotations;
using Orchard.Blogs.Controllers; using Orchard.Blogs.Controllers;
using Orchard.Blogs.Services;
using Orchard.ContentManagement.Handlers; using Orchard.ContentManagement.Handlers;
using Orchard.Core.Common.Models; using Orchard.Core.Common.Models;
using Orchard.Core.Common.Services;
using Orchard.Data; 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, IBlogService blogService, public BlogHandler(IRepository<BlogRecord> repository) {
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) && b.Id != blog.Id).Select(
b => b.Slug));
});
} }
} }
} }

View File

@@ -6,30 +6,18 @@ 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, IBlogPostService blogPostService, IRoutableService routableService) { public BlogPostHandler(IRepository<CommonVersionRecord> commonRepository, IBlogPostService blogPostService) {
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));
Filters.Add(new ActivatingFilter<BodyAspect>(BlogPostDriver.ContentType.Name)); Filters.Add(new ActivatingFilter<BodyAspect>(BlogPostDriver.ContentType.Name));
Filters.Add(new StorageFilter<CommonVersionRecord>(commonRepository)); Filters.Add(new StorageFilter<CommonVersionRecord>(commonRepository));
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) && bp.Id != blogPost.Id).Select(
bp => bp.Slug));
});
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--);

View File

@@ -142,11 +142,11 @@ namespace Orchard.Pages.Controllers {
return RedirectToAction("List"); return RedirectToAction("List");
} }
public ActionResult Edit(string pageSlug) { public ActionResult Edit(int id) {
if (!_services.Authorizer.Authorize(Permissions.ModifyPages, T("Couldn't edit page"))) if (!_services.Authorizer.Authorize(Permissions.ModifyPages, T("Couldn't edit page")))
return new HttpUnauthorizedResult(); return new HttpUnauthorizedResult();
Page page = _pageService.GetLatest(pageSlug); Page page = _pageService.GetLatest(id);
if (page == null) if (page == null)
return new NotFoundResult(); return new NotFoundResult();
@@ -159,11 +159,11 @@ namespace Orchard.Pages.Controllers {
} }
[HttpPost, ActionName("Edit")] [HttpPost, ActionName("Edit")]
public ActionResult EditPOST(string pageSlug) { public ActionResult EditPOST(int id) {
if (!_services.Authorizer.Authorize(Permissions.ModifyPages, T("Couldn't edit page"))) if (!_services.Authorizer.Authorize(Permissions.ModifyPages, T("Couldn't edit page")))
return new HttpUnauthorizedResult(); return new HttpUnauthorizedResult();
Page page = _pageService.GetPageOrDraft(pageSlug); Page page = _pageService.GetPageOrDraft(id);
if (page == null) if (page == null)
return new NotFoundResult(); return new NotFoundResult();
@@ -225,6 +225,7 @@ namespace Orchard.Pages.Controllers {
bool IUpdateModel.TryUpdateModel<TModel>(TModel model, string prefix, string[] includeProperties, string[] excludeProperties) { bool IUpdateModel.TryUpdateModel<TModel>(TModel model, string prefix, string[] includeProperties, string[] excludeProperties) {
return TryUpdateModel(model, prefix, includeProperties, excludeProperties); return TryUpdateModel(model, prefix, includeProperties, excludeProperties);
} }
void IUpdateModel.AddModelError(string key, LocalizedString errorMessage) { void IUpdateModel.AddModelError(string key, LocalizedString errorMessage) {
ModelState.AddModelError(key, errorMessage.ToString()); ModelState.AddModelError(key, errorMessage.ToString());
} }

View File

@@ -1,17 +1,38 @@
using System.Web.Routing; using System;
using System.Linq;
using System.Text.RegularExpressions;
using System.Web.Routing;
using JetBrains.Annotations; using JetBrains.Annotations;
using Orchard.Core.Common.Models;
using Orchard.Core.Common.Services;
using Orchard.Localization;
using Orchard.Pages.Models; using Orchard.Pages.Models;
using Orchard.ContentManagement; using Orchard.ContentManagement;
using Orchard.ContentManagement.Drivers; using Orchard.ContentManagement.Drivers;
using Orchard.Pages.Services;
using Orchard.UI.Notify;
namespace Orchard.Pages.Controllers { namespace Orchard.Pages.Controllers {
[UsedImplicitly] [UsedImplicitly]
public class PageDriver : ContentItemDriver<Page> { public class PageDriver : ContentItemDriver<Page> {
private readonly IPageService _pageService;
private readonly IRoutableService _routableService;
private readonly IOrchardServices _orchardServices;
public readonly static ContentType ContentType = new ContentType { public readonly static ContentType ContentType = new ContentType {
Name = "page", Name = "page",
DisplayName = "Page" DisplayName = "Page"
}; };
public PageDriver(IPageService pageService, IRoutableService routableService, IOrchardServices orchardServices) {
_pageService = pageService;
_routableService = routableService;
_orchardServices = orchardServices;
T = NullLocalizer.Instance;
}
private Localizer T { get; set; }
protected override ContentType GetContentType() { protected override ContentType GetContentType() {
return ContentType; return ContentType;
} }
@@ -52,7 +73,71 @@ namespace Orchard.Pages.Controllers {
protected override DriverResult Editor(Page page, IUpdateModel updater) { protected override DriverResult Editor(Page page, IUpdateModel updater) {
updater.TryUpdateModel(page, Prefix, null, null); updater.TryUpdateModel(page, Prefix, null, null);
//todo: (heskew) something better needs to be done with this...still feels shoehorned in here
ProcessSlug(page, updater);
return Editor(page); return Editor(page);
} }
private void ProcessSlug(Page page, IUpdateModel updater) {
_routableService.FillSlug(page.As<RoutableAspect>(), Slugify);
if (string.IsNullOrEmpty(page.Slug)) {
return;
// OR
//updater.AddModelError("Routable.Slug", T("The slug is required.").ToString());
//return;
}
if (!Regex.IsMatch(page.Slug, @"^[^/:?#\[\]@!$&'()*+,;=\s](?(?=/)/[^/:?#\[\]@!$&'()*+,;=\s]|[^:?#\[\]@!$&'()*+,;=\s])*$")) {
updater.AddModelError("Routable.Slug", T("Please do not use any of the following characters in your slugs: \":\", \"?\", \"#\", \"[\", \"]\", \"@\", \"!\", \"$\", \"&\", \"'\", \"(\", \")\", \"*\", \"+\", \",\", \";\", \"=\". No spaces are allowed (please use dashes or underscores instead).").ToString());
return;
}
var slugsLikeThis = _pageService.Get(PageStatus.Published).Where(
p => p.Slug.StartsWith(page.Slug, StringComparison.OrdinalIgnoreCase) &&
p.Id != page.Id).Select(p => p.Slug);
if (slugsLikeThis.Count() > 0) {
//todo: (heskew) need better messages
_orchardServices.Notifier.Warning(T("A different page is already published with this same slug."));
if (page.ContentItem.VersionRecord == null || page.ContentItem.VersionRecord.Published) {
var originalSlug = page.Slug;
//todo: (heskew) make auto-uniqueness optional
page.Slug = _routableService.GenerateUniqueSlug(page.Slug, slugsLikeThis);
//todo: (heskew) need better messages
if (originalSlug != page.Slug)
_orchardServices.Notifier.Warning(T("Slugs in conflict. \"{0}\" is already set for a previously published page so this page now has the slug \"{1}\"",
originalSlug, page.Slug));
}
}
}
private static string Slugify(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 = startsoffbad.Replace(value, "-");
value = slashhappy.Replace(value, "/");
value = dissallowed.Replace(value, "-");
value = value.Trim('-');
if (value.Length > 1000)
value = value.Substring(0, 1000);
}
return value;
}
} }
} }

View File

@@ -1,4 +1,5 @@
using System; using System;
using System.ComponentModel.DataAnnotations;
using System.Web.Mvc; using System.Web.Mvc;
using Orchard.ContentManagement; using Orchard.ContentManagement;
using Orchard.Core.Common.Models; using Orchard.Core.Common.Models;

View File

@@ -1,36 +1,21 @@
using System.Linq; using JetBrains.Annotations;
using JetBrains.Annotations;
using Orchard.ContentManagement; using Orchard.ContentManagement;
using Orchard.Core.Common.Records; using Orchard.Core.Common.Records;
using Orchard.Core.Common.Services;
using Orchard.Pages.Controllers; using Orchard.Pages.Controllers;
using Orchard.Core.Common.Models; using Orchard.Core.Common.Models;
using Orchard.Data; using Orchard.Data;
using Orchard.ContentManagement.Handlers; using Orchard.ContentManagement.Handlers;
using Orchard.Pages.Services;
namespace Orchard.Pages.Models { namespace Orchard.Pages.Models {
[UsedImplicitly] [UsedImplicitly]
public class PageHandler : ContentHandler { public class PageHandler : ContentHandler {
public PageHandler(IRepository<CommonVersionRecord> commonRepository, IPageService pageService, IRoutableService routableService) { public PageHandler(IRepository<CommonVersionRecord> commonRepository) {
Filters.Add(new ActivatingFilter<Page>(PageDriver.ContentType.Name)); Filters.Add(new ActivatingFilter<Page>(PageDriver.ContentType.Name));
Filters.Add(new ActivatingFilter<CommonAspect>(PageDriver.ContentType.Name)); Filters.Add(new ActivatingFilter<CommonAspect>(PageDriver.ContentType.Name));
Filters.Add(new ActivatingFilter<ContentPart<CommonVersionRecord>>(PageDriver.ContentType.Name)); Filters.Add(new ActivatingFilter<ContentPart<CommonVersionRecord>>(PageDriver.ContentType.Name));
Filters.Add(new ActivatingFilter<RoutableAspect>(PageDriver.ContentType.Name)); Filters.Add(new ActivatingFilter<RoutableAspect>(PageDriver.ContentType.Name));
Filters.Add(new ActivatingFilter<BodyAspect>(PageDriver.ContentType.Name)); Filters.Add(new ActivatingFilter<BodyAspect>(PageDriver.ContentType.Name));
Filters.Add(new StorageFilter<CommonVersionRecord>(commonRepository)); Filters.Add(new StorageFilter<CommonVersionRecord>(commonRepository));
OnCreating<Page>((context, page) =>
{
string slug = !string.IsNullOrEmpty(page.Slug)
? page.Slug
: routableService.Slugify(page.Title);
page.Slug = routableService.GenerateUniqueSlug(slug,
pageService.Get(PageStatus.Published).Where(
p => p.Slug.StartsWith(slug) && p.Id != page.Id).Select(
p => p.Slug));
});
} }
} }
} }

View File

@@ -8,6 +8,7 @@ namespace Orchard.Pages.Services {
IEnumerable<Page> Get(PageStatus status); IEnumerable<Page> Get(PageStatus status);
Page Get(string slug); Page Get(string slug);
Page GetPageOrDraft(string slug); Page GetPageOrDraft(string slug);
Page GetPageOrDraft(int id);
Page GetLatest(string slug); Page GetLatest(string slug);
Page GetLatest(int id); Page GetLatest(int id);
void Delete(Page page); void Delete(Page page);

View File

@@ -54,6 +54,10 @@ namespace Orchard.Pages.Services {
.Slice(0, 1).FirstOrDefault().As<Page>(); .Slice(0, 1).FirstOrDefault().As<Page>();
} }
public Page GetPageOrDraft(int id) {
return _contentManager.GetDraftRequired<Page>(id);
}
public Page GetPageOrDraft(string slug) { public Page GetPageOrDraft(string slug) {
Page page = GetLatest(slug); Page page = GetLatest(slug);
return _contentManager.GetDraftRequired<Page>(page.Id); return _contentManager.GetDraftRequired<Page>(page.Id);

View File

@@ -85,7 +85,7 @@
: "" %> : "" %>
<% } %> <% } %>
</td> </td>
<td><%=Html.ActionLink(T("Edit").ToString(), "Edit", new { pageSlug = pageEntry.Page.Slug }) %></td> <td><%=Html.ActionLink(T("Edit").ToString(), "Edit", new { id = pageEntry.PageId }) %></td>
</tr> </tr>
<% <%
pageIndex++; pageIndex++;