mirror of
https://github.com/OrchardCMS/Orchard.git
synced 2025-09-24 05:23:33 +08:00
- 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:
@@ -20,35 +20,35 @@ namespace Orchard.Core.Tests.Common.Services {
|
||||
|
||||
private IRoutableService _routableService;
|
||||
|
||||
[Test]
|
||||
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"));
|
||||
}
|
||||
//[Test]
|
||||
//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"));
|
||||
//}
|
||||
|
||||
[Test]
|
||||
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/a/couple//slashes"), Is.EqualTo("slug/with/a/couple/slashes"));
|
||||
}
|
||||
//[Test]
|
||||
//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/a/couple//slashes"), Is.EqualTo("slug/with/a/couple/slashes"));
|
||||
//}
|
||||
|
||||
[Test]
|
||||
public void InvalidCharactersShouldBeReplacedByADash() {
|
||||
Assert.That(_routableService.Slugify(
|
||||
"Please do not use any of the following characters in your slugs: \":\", \"/\", \"?\", \"#\", \"[\", \"]\", \"@\", \"!\", \"$\", \"&\", \"'\", \"(\", \")\", \"*\", \"+\", \",\", \";\", \"=\""),
|
||||
Is.EqualTo(
|
||||
"Please-do-not-use-any-of-the-following-characters-in-your-slugs-\"-\"-\"/\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\""));
|
||||
}
|
||||
//[Test]
|
||||
//public void InvalidCharactersShouldBeReplacedByADash() {
|
||||
// Assert.That(_routableService.Slugify(
|
||||
// "Please do not use any of the following characters in your slugs: \":\", \"/\", \"?\", \"#\", \"[\", \"]\", \"@\", \"!\", \"$\", \"&\", \"'\", \"(\", \")\", \"*\", \"+\", \",\", \";\", \"=\""),
|
||||
// Is.EqualTo(
|
||||
// "Please-do-not-use-any-of-the-following-characters-in-your-slugs-\"-\"-\"/\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\"-\""));
|
||||
//}
|
||||
|
||||
[Test]
|
||||
public void VeryLongStringTruncatedTo1000Chars() {
|
||||
var veryVeryLongSlug = "this is a very long slug...";
|
||||
for (var i = 0; i < 100; i++)
|
||||
veryVeryLongSlug += "aaaaaaaaaa";
|
||||
//[Test]
|
||||
//public void VeryLongStringTruncatedTo1000Chars() {
|
||||
// var veryVeryLongSlug = "this is a very long slug...";
|
||||
// for (var i = 0; i < 100; i++)
|
||||
// veryVeryLongSlug += "aaaaaaaaaa";
|
||||
|
||||
Assert.That(veryVeryLongSlug.Length, Is.AtLeast(1001));
|
||||
Assert.That(_routableService.Slugify(veryVeryLongSlug).Length, Is.EqualTo(1000));
|
||||
}
|
||||
// Assert.That(veryVeryLongSlug.Length, Is.AtLeast(1001));
|
||||
// Assert.That(_routableService.Slugify(veryVeryLongSlug).Length, Is.EqualTo(1000));
|
||||
//}
|
||||
}
|
||||
}
|
@@ -1,17 +1,40 @@
|
||||
using System.Web.Mvc;
|
||||
using Orchard.ContentManagement;
|
||||
using Orchard.Core.Common.Models;
|
||||
using Orchard.Core.Common.Services;
|
||||
using Orchard.Localization;
|
||||
|
||||
namespace Orchard.Core.Common.Controllers {
|
||||
public class RoutableController : Controller {
|
||||
public class RoutableController : Controller, IUpdateModel {
|
||||
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;
|
||||
_contentManager = contentManager;
|
||||
_orchardServices = orchardServices;
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public ActionResult Slugify(FormCollection formCollection, string value) {
|
||||
return Json(_routableService.Slugify(value));
|
||||
public ActionResult Slugify(FormCollection formCollection, string contentType) {
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,4 +1,3 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Orchard.Core.Common.Records;
|
||||
using Orchard.ContentManagement;
|
||||
|
||||
@@ -9,7 +8,6 @@ namespace Orchard.Core.Common.Models {
|
||||
set { Record.Title = value; }
|
||||
}
|
||||
|
||||
[Required]
|
||||
public string Slug {
|
||||
get { return Record.Slug; }
|
||||
set { Record.Slug = value; }
|
||||
|
@@ -1,9 +1,22 @@
|
||||
jQuery.fn.extend({
|
||||
slugify: function(options) {
|
||||
//todo: (heskew) need messaging system
|
||||
if (!options.target || !options.url) return;
|
||||
jQuery.post(options.url, { value: $(this).val(), __RequestVerificationToken: $("input[name=__RequestVerificationToken]").val() }, function(data) {
|
||||
if (!options.target || !options.url)
|
||||
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);
|
||||
}, "json");
|
||||
},
|
||||
"json"
|
||||
);
|
||||
}
|
||||
});
|
@@ -1,8 +1,11 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Orchard.Core.Common.Models;
|
||||
|
||||
namespace Orchard.Core.Common.Services {
|
||||
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);
|
||||
}
|
||||
}
|
@@ -2,37 +2,42 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using Orchard.Core.Common.Models;
|
||||
|
||||
namespace Orchard.Core.Common.Services {
|
||||
public class RoutableService : IRoutableService {
|
||||
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]+");
|
||||
public void FillSlug<TModel>(TModel model) where TModel : RoutableAspect {
|
||||
if (!string.IsNullOrEmpty(model.Slug) || string.IsNullOrEmpty(model.Title))
|
||||
return;
|
||||
|
||||
title = title.Trim();
|
||||
title = startsoffbad.Replace(title, "-");
|
||||
title = slashhappy.Replace(title, "/");
|
||||
title = dissallowed.Replace(title, "-");
|
||||
var slug = model.Title;
|
||||
var dissallowed = new Regex(@"[/:?#\[\]@!$&'()*+,;=\s]+");
|
||||
|
||||
if (title.Length > 1000) {
|
||||
title = title.Substring(0, 1000);
|
||||
}
|
||||
slug = dissallowed.Replace(slug, "-");
|
||||
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) {
|
||||
int? version = existingSlugs
|
||||
.Select(s => {
|
||||
int v;
|
||||
string[] slugParts = s.Split(new[] { slugCandidate }, StringSplitOptions.RemoveEmptyEntries);
|
||||
if (slugParts.Length == 0) {
|
||||
return 1;
|
||||
return 2;
|
||||
}
|
||||
|
||||
return int.TryParse(slugParts[0].TrimStart('-'), out v)
|
||||
|
@@ -11,7 +11,6 @@ namespace Orchard.Core.Common.ViewModels {
|
||||
set { RoutableAspect.Record.Title = value; }
|
||||
}
|
||||
|
||||
[RegularExpression(@"^[^/:?#\[\]@!$&'()*+,;=\s](?(?=/)/[^/:?#\[\]@!$&'()*+,;=\s]|[^:?#\[\]@!$&'()*+,;=\s])*$")]
|
||||
public string Slug {
|
||||
get { return RoutableAspect.Record.Slug; }
|
||||
set { RoutableAspect.Record.Slug = value; }
|
||||
|
@@ -10,5 +10,14 @@
|
||||
<span><%=Html.TextBoxFor(m => m.Slug, new { @class = "text" })%></span>
|
||||
</fieldset>
|
||||
<% 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>
|
||||
<% } %>
|
@@ -1,12 +1,18 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Web.Routing;
|
||||
using JetBrains.Annotations;
|
||||
using Orchard.Blogs.Models;
|
||||
using Orchard.Blogs.Services;
|
||||
using Orchard.ContentManagement;
|
||||
using Orchard.ContentManagement.Drivers;
|
||||
using Orchard.Core.Common.Models;
|
||||
using Orchard.Core.Common.Services;
|
||||
using Orchard.Localization;
|
||||
using Orchard.Mvc.ViewModels;
|
||||
using Orchard.UI.Notify;
|
||||
|
||||
namespace Orchard.Blogs.Controllers {
|
||||
[UsedImplicitly]
|
||||
@@ -17,13 +23,22 @@ namespace Orchard.Blogs.Controllers {
|
||||
};
|
||||
|
||||
private readonly IContentManager _contentManager;
|
||||
private readonly IBlogService _blogService;
|
||||
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;
|
||||
_blogService = blogService;
|
||||
_blogPostService = blogPostService;
|
||||
_routableService = routableService;
|
||||
_orchardServices = orchardServices;
|
||||
T = NullLocalizer.Instance;
|
||||
}
|
||||
|
||||
private Localizer T { get; set; }
|
||||
|
||||
protected override ContentType GetContentType() {
|
||||
return ContentType;
|
||||
}
|
||||
@@ -77,9 +92,48 @@ namespace Orchard.Blogs.Controllers {
|
||||
|
||||
protected override DriverResult Editor(Blog blog, IUpdateModel updater) {
|
||||
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(
|
||||
ContentItemTemplate("Items/Blogs.Blog"),
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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 Orchard.Blogs.Models;
|
||||
using Orchard.Blogs.Services;
|
||||
using Orchard.ContentManagement;
|
||||
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 {
|
||||
[UsedImplicitly]
|
||||
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 {
|
||||
Name = "blogpost",
|
||||
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() {
|
||||
return ContentType;
|
||||
}
|
||||
@@ -54,7 +77,50 @@ namespace Orchard.Blogs.Controllers {
|
||||
|
||||
protected override DriverResult Editor(BlogPost post, IUpdateModel updater) {
|
||||
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);
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,32 +1,17 @@
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using Orchard.Blogs.Controllers;
|
||||
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, IBlogService blogService,
|
||||
IRoutableService routableService) {
|
||||
public BlogHandler(IRepository<BlogRecord> repository) {
|
||||
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) && b.Id != blog.Id).Select(
|
||||
b => b.Slug));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@@ -6,30 +6,18 @@ 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, IBlogPostService blogPostService, IRoutableService routableService) {
|
||||
public BlogPostHandler(IRepository<CommonVersionRecord> commonRepository, IBlogPostService blogPostService) {
|
||||
Filters.Add(new ActivatingFilter<BlogPost>(BlogPostDriver.ContentType.Name));
|
||||
Filters.Add(new ActivatingFilter<CommonAspect>(BlogPostDriver.ContentType.Name));
|
||||
Filters.Add(new ActivatingFilter<RoutableAspect>(BlogPostDriver.ContentType.Name));
|
||||
Filters.Add(new ActivatingFilter<BodyAspect>(BlogPostDriver.ContentType.Name));
|
||||
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++);
|
||||
OnRemoved<BlogPost>((context, bp) => bp.Blog.PostCount--);
|
||||
|
||||
|
@@ -142,11 +142,11 @@ namespace Orchard.Pages.Controllers {
|
||||
return RedirectToAction("List");
|
||||
}
|
||||
|
||||
public ActionResult Edit(string pageSlug) {
|
||||
public ActionResult Edit(int id) {
|
||||
if (!_services.Authorizer.Authorize(Permissions.ModifyPages, T("Couldn't edit page")))
|
||||
return new HttpUnauthorizedResult();
|
||||
|
||||
Page page = _pageService.GetLatest(pageSlug);
|
||||
Page page = _pageService.GetLatest(id);
|
||||
|
||||
if (page == null)
|
||||
return new NotFoundResult();
|
||||
@@ -159,11 +159,11 @@ namespace Orchard.Pages.Controllers {
|
||||
}
|
||||
|
||||
[HttpPost, ActionName("Edit")]
|
||||
public ActionResult EditPOST(string pageSlug) {
|
||||
public ActionResult EditPOST(int id) {
|
||||
if (!_services.Authorizer.Authorize(Permissions.ModifyPages, T("Couldn't edit page")))
|
||||
return new HttpUnauthorizedResult();
|
||||
|
||||
Page page = _pageService.GetPageOrDraft(pageSlug);
|
||||
Page page = _pageService.GetPageOrDraft(id);
|
||||
|
||||
if (page == null)
|
||||
return new NotFoundResult();
|
||||
@@ -225,6 +225,7 @@ namespace Orchard.Pages.Controllers {
|
||||
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());
|
||||
}
|
||||
|
@@ -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 Orchard.Core.Common.Models;
|
||||
using Orchard.Core.Common.Services;
|
||||
using Orchard.Localization;
|
||||
using Orchard.Pages.Models;
|
||||
using Orchard.ContentManagement;
|
||||
using Orchard.ContentManagement.Drivers;
|
||||
using Orchard.Pages.Services;
|
||||
using Orchard.UI.Notify;
|
||||
|
||||
namespace Orchard.Pages.Controllers {
|
||||
[UsedImplicitly]
|
||||
public class PageDriver : ContentItemDriver<Page> {
|
||||
private readonly IPageService _pageService;
|
||||
private readonly IRoutableService _routableService;
|
||||
private readonly IOrchardServices _orchardServices;
|
||||
|
||||
public readonly static ContentType ContentType = new ContentType {
|
||||
Name = "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() {
|
||||
return ContentType;
|
||||
}
|
||||
@@ -52,7 +73,71 @@ namespace Orchard.Pages.Controllers {
|
||||
|
||||
protected override DriverResult Editor(Page page, IUpdateModel updater) {
|
||||
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);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Web.Mvc;
|
||||
using Orchard.ContentManagement;
|
||||
using Orchard.Core.Common.Models;
|
||||
|
@@ -1,36 +1,21 @@
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using JetBrains.Annotations;
|
||||
using Orchard.ContentManagement;
|
||||
using Orchard.Core.Common.Records;
|
||||
using Orchard.Core.Common.Services;
|
||||
using Orchard.Pages.Controllers;
|
||||
using Orchard.Core.Common.Models;
|
||||
using Orchard.Data;
|
||||
using Orchard.ContentManagement.Handlers;
|
||||
using Orchard.Pages.Services;
|
||||
|
||||
namespace Orchard.Pages.Models {
|
||||
[UsedImplicitly]
|
||||
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<CommonAspect>(PageDriver.ContentType.Name));
|
||||
Filters.Add(new ActivatingFilter<ContentPart<CommonVersionRecord>>(PageDriver.ContentType.Name));
|
||||
Filters.Add(new ActivatingFilter<RoutableAspect>(PageDriver.ContentType.Name));
|
||||
Filters.Add(new ActivatingFilter<BodyAspect>(PageDriver.ContentType.Name));
|
||||
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));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@@ -8,6 +8,7 @@ namespace Orchard.Pages.Services {
|
||||
IEnumerable<Page> Get(PageStatus status);
|
||||
Page Get(string slug);
|
||||
Page GetPageOrDraft(string slug);
|
||||
Page GetPageOrDraft(int id);
|
||||
Page GetLatest(string slug);
|
||||
Page GetLatest(int id);
|
||||
void Delete(Page page);
|
||||
|
@@ -54,6 +54,10 @@ namespace Orchard.Pages.Services {
|
||||
.Slice(0, 1).FirstOrDefault().As<Page>();
|
||||
}
|
||||
|
||||
public Page GetPageOrDraft(int id) {
|
||||
return _contentManager.GetDraftRequired<Page>(id);
|
||||
}
|
||||
|
||||
public Page GetPageOrDraft(string slug) {
|
||||
Page page = GetLatest(slug);
|
||||
return _contentManager.GetDraftRequired<Page>(page.Id);
|
||||
|
@@ -85,7 +85,7 @@
|
||||
: "" %>
|
||||
<% } %>
|
||||
</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>
|
||||
<%
|
||||
pageIndex++;
|
||||
|
Reference in New Issue
Block a user