mirror of
https://github.com/OrchardCMS/Orchard.git
synced 2026-01-19 17:51:45 +08:00
Merge from dev
--HG-- branch : dev
This commit is contained in:
@@ -6,16 +6,20 @@ using Orchard.ContentManagement.Aspects;
|
||||
using Orchard.Core.Common.Models;
|
||||
using Orchard.Core.Routable.Models;
|
||||
using Orchard.Core.Routable.ViewModels;
|
||||
using Orchard.Data;
|
||||
using Orchard.Localization;
|
||||
using Orchard.Mvc.ViewModels;
|
||||
|
||||
namespace Orchard.Core.Routable.Controllers {
|
||||
[ValidateInput(false)]
|
||||
public class ItemController : Controller {
|
||||
public class ItemController : Controller, IUpdateModel {
|
||||
private readonly IContentManager _contentManager;
|
||||
private readonly ITransactionManager _transactionManager;
|
||||
private readonly IRoutablePathConstraint _routablePathConstraint;
|
||||
|
||||
public ItemController(IContentManager contentManager, IRoutablePathConstraint routablePathConstraint) {
|
||||
public ItemController(IContentManager contentManager, ITransactionManager transactionManager, IRoutablePathConstraint routablePathConstraint) {
|
||||
_contentManager = contentManager;
|
||||
_transactionManager = transactionManager;
|
||||
_routablePathConstraint = routablePathConstraint;
|
||||
}
|
||||
|
||||
@@ -47,5 +51,39 @@ namespace Orchard.Core.Routable.Controllers {
|
||||
itemViewModel.TemplateName = "Items/Contents.Item";
|
||||
}
|
||||
}
|
||||
|
||||
public ActionResult Slugify(string contentType, int? id, int? containerId) {
|
||||
const string slug = "";
|
||||
ContentItem contentItem = null;
|
||||
|
||||
if (string.IsNullOrEmpty(contentType))
|
||||
return Json(slug);
|
||||
|
||||
if (id != null)
|
||||
contentItem = _contentManager.Get((int)id, VersionOptions.Latest);
|
||||
|
||||
if (contentItem == null) {
|
||||
contentItem = _contentManager.New(contentType);
|
||||
|
||||
if (containerId != null) {
|
||||
var containerItem = _contentManager.Get((int)containerId);
|
||||
contentItem.As<ICommonAspect>().Container = containerItem;
|
||||
}
|
||||
}
|
||||
|
||||
_contentManager.UpdateEditorModel(contentItem, this);
|
||||
_transactionManager.Cancel();
|
||||
|
||||
return Json(contentItem.As<IRoutableAspect>().Slug ?? 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,61 +1,99 @@
|
||||
using JetBrains.Annotations;
|
||||
using Orchard.ContentManagement;
|
||||
using Orchard.ContentManagement.Aspects;
|
||||
using Orchard.ContentManagement.Drivers;
|
||||
using Orchard.Core.Common.Models;
|
||||
using Orchard.Core.Common.ViewModels;
|
||||
using Orchard.Core.Common.Services;
|
||||
using Orchard.Core.Routable.Models;
|
||||
using Orchard.Core.Routable.Services;
|
||||
using Orchard.Core.Routable.ViewModels;
|
||||
using Orchard.Localization;
|
||||
using Orchard.UI.Notify;
|
||||
|
||||
namespace Orchard.Core.Routable.Drivers {
|
||||
public class RoutableDriver : ContentPartDriver<IsRoutable> {
|
||||
protected override DriverResult Editor(IsRoutable part, IUpdateModel updater) {
|
||||
part.Record.Title = "Routable #" + part.ContentItem.Id;
|
||||
part.Record.Slug = "routable" + part.ContentItem.Id;
|
||||
part.Record.Path = "routable" + part.ContentItem.Id;
|
||||
return base.Editor(part, updater);
|
||||
private readonly IOrchardServices _services;
|
||||
private readonly IRoutableService _routableService;
|
||||
|
||||
public RoutableDriver(IOrchardServices services, IRoutableService routableService) {
|
||||
_services = services;
|
||||
_routableService = routableService;
|
||||
T = NullLocalizer.Instance;
|
||||
}
|
||||
|
||||
//private const string TemplateName = "Parts/Common.Routable";
|
||||
|
||||
//private readonly IOrchardServices _services;
|
||||
//private readonly IRoutableService _routableService;
|
||||
//public Localizer T { get; set; }
|
||||
private const string TemplateName = "Parts/Routable.IsRoutable";
|
||||
|
||||
//protected override string Prefix {
|
||||
// get { return "Routable"; }
|
||||
//}
|
||||
public Localizer T { get; set; }
|
||||
|
||||
//public Routable(IOrchardServices services, IRoutableService routableService)
|
||||
//{
|
||||
// _services = services;
|
||||
// _routableService = routableService;
|
||||
protected override string Prefix {
|
||||
get { return "Routable"; }
|
||||
}
|
||||
|
||||
// T = NullLocalizer.Instance;
|
||||
//}
|
||||
int? GetContainerId(IContent item) {
|
||||
var commonAspect = item.As<ICommonAspect>();
|
||||
if (commonAspect != null && commonAspect.Container != null) {
|
||||
return commonAspect.Container.ContentItem.Id;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
//protected override DriverResult Editor(RoutableAspect part) {
|
||||
// var model = new RoutableEditorViewModel { Prefix = Prefix, RoutableAspect = part };
|
||||
// return ContentPartTemplate(model, TemplateName, Prefix).Location("primary", "before.5");
|
||||
//}
|
||||
string GetContainerSlug(IContent item) {
|
||||
var commonAspect = item.As<ICommonAspect>();
|
||||
if (commonAspect != null && commonAspect.Container != null) {
|
||||
var routable = commonAspect.Container.As<IRoutableAspect>();
|
||||
if (routable != null) {
|
||||
return routable.Slug;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
//protected override DriverResult Editor(RoutableAspect part, IUpdateModel updater) {
|
||||
// var model = new RoutableEditorViewModel { Prefix = Prefix, RoutableAspect = part };
|
||||
// updater.TryUpdateModel(model, Prefix, null, null);
|
||||
protected override DriverResult Editor(IsRoutable part) {
|
||||
var model = new RoutableEditorViewModel {
|
||||
ContentType = part.ContentItem.ContentType,
|
||||
Id = part.ContentItem.Id,
|
||||
Slug = part.Slug,
|
||||
Title = part.Title,
|
||||
ContainerId = GetContainerId(part),
|
||||
};
|
||||
|
||||
// if (!_routableService.IsSlugValid(part.Slug)){
|
||||
// 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());
|
||||
// }
|
||||
// TEMP: path format patterns replaces this logic
|
||||
var path = part.Record.Path;
|
||||
if (path.EndsWith(part.Slug)) {
|
||||
model.DisplayLeadingPath = path.Substring(0, path.Length - part.Slug.Length);
|
||||
}
|
||||
|
||||
// string originalSlug = part.Slug;
|
||||
// if(!_routableService.ProcessSlug(part)) {
|
||||
// _services.Notifier.Warning(T("Slugs in conflict. \"{0}\" is already set for a previously created {2} so now it has the slug \"{1}\"",
|
||||
// originalSlug, part.Slug, part.ContentItem.ContentType));
|
||||
// }
|
||||
|
||||
// return ContentPartTemplate(model, TemplateName, Prefix).Location("primary", "before.5");
|
||||
//}
|
||||
return ContentPartTemplate(model, TemplateName, Prefix).Location("primary", "before.5");
|
||||
}
|
||||
|
||||
protected override DriverResult Editor(IsRoutable part, IUpdateModel updater) {
|
||||
|
||||
var model = new RoutableEditorViewModel();
|
||||
updater.TryUpdateModel(model, Prefix, null, null);
|
||||
part.Title = model.Title;
|
||||
part.Slug = model.Slug;
|
||||
|
||||
// TEMP: path format patterns replaces this logic
|
||||
var containerSlug = GetContainerSlug(part);
|
||||
if (string.IsNullOrEmpty(containerSlug)) {
|
||||
part.Record.Path = model.Slug;
|
||||
}
|
||||
else {
|
||||
part.Record.Path = containerSlug + "/" + model.Slug;
|
||||
}
|
||||
|
||||
if (!_routableService.IsSlugValid(part.Slug)) {
|
||||
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());
|
||||
}
|
||||
|
||||
string originalSlug = part.Slug;
|
||||
if (!_routableService.ProcessSlug(part)) {
|
||||
_services.Notifier.Warning(T("Slugs in conflict. \"{0}\" is already set for a previously created {2} so now it has the slug \"{1}\"",
|
||||
originalSlug, part.Slug, part.ContentItem.ContentType));
|
||||
}
|
||||
|
||||
return Editor(part);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
24
src/Orchard.Web/Core/Routable/Scripts/jquery.slugify.js
Normal file
24
src/Orchard.Web/Core/Routable/Scripts/jquery.slugify.js
Normal file
@@ -0,0 +1,24 @@
|
||||
jQuery.fn.extend({
|
||||
slugify: function(options) {
|
||||
//todo: (heskew) need messaging system
|
||||
if (!options.target || !options.url)
|
||||
return;
|
||||
|
||||
var args = {
|
||||
"contentType": options.contentType,
|
||||
"id": options.id,
|
||||
"containerId": options.containerId,
|
||||
__RequestVerificationToken: $("input[name=__RequestVerificationToken]").val()
|
||||
};
|
||||
args[$(this).attr("name")] = $(this).val();
|
||||
|
||||
jQuery.post(
|
||||
options.url,
|
||||
args,
|
||||
function(data) {
|
||||
options.target.val(data);
|
||||
},
|
||||
"json"
|
||||
);
|
||||
}
|
||||
});
|
||||
28
src/Orchard.Web/Core/Routable/Services/IRoutableService.cs
Normal file
28
src/Orchard.Web/Core/Routable/Services/IRoutableService.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Orchard.Core.Routable.Models;
|
||||
|
||||
namespace Orchard.Core.Routable.Services {
|
||||
public interface IRoutableService : IDependency {
|
||||
void FillSlug<TModel>(TModel model) where TModel : IsRoutable;
|
||||
void FillSlug<TModel>(TModel model, Func<string, string> generateSlug) where TModel : IsRoutable;
|
||||
string GenerateUniqueSlug(string slugCandidate, IEnumerable<string> existingSlugs);
|
||||
|
||||
/// <summary>
|
||||
/// Returns any content item of the specified content type with similar slugs
|
||||
/// </summary>
|
||||
IEnumerable<IsRoutable> GetSimilarSlugs(string contentType, string slug);
|
||||
|
||||
/// <summary>
|
||||
/// Validates the given slug
|
||||
/// </summary>
|
||||
bool IsSlugValid(string slug);
|
||||
|
||||
/// <summary>
|
||||
/// Defines the slug of a RoutableAspect and validate its unicity
|
||||
/// </summary>
|
||||
/// <returns>True if the slug has been created, False if a conflict occured</returns>
|
||||
bool ProcessSlug(IsRoutable part);
|
||||
|
||||
}
|
||||
}
|
||||
113
src/Orchard.Web/Core/Routable/Services/RoutableService.cs
Normal file
113
src/Orchard.Web/Core/Routable/Services/RoutableService.cs
Normal file
@@ -0,0 +1,113 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using JetBrains.Annotations;
|
||||
using Orchard.ContentManagement;
|
||||
using Orchard.Core.Common.Models;
|
||||
using Orchard.Core.Routable.Models;
|
||||
using Orchard.Localization;
|
||||
using Orchard.UI.Notify;
|
||||
|
||||
namespace Orchard.Core.Routable.Services {
|
||||
[UsedImplicitly]
|
||||
public class RoutableService : IRoutableService {
|
||||
private readonly IContentManager _contentManager;
|
||||
|
||||
public RoutableService(IContentManager contentManager) {
|
||||
_contentManager = contentManager;
|
||||
}
|
||||
|
||||
public void FillSlug<TModel>(TModel model) where TModel : IsRoutable {
|
||||
if (!string.IsNullOrEmpty(model.Slug) || string.IsNullOrEmpty(model.Title))
|
||||
return;
|
||||
|
||||
var slug = model.Title;
|
||||
var dissallowed = new Regex(@"[/:?#\[\]@!$&'()*+,;=\s]+");
|
||||
|
||||
slug = dissallowed.Replace(slug, "-");
|
||||
slug = slug.Trim('-');
|
||||
|
||||
if (slug.Length > 1000)
|
||||
slug = slug.Substring(0, 1000);
|
||||
|
||||
model.Slug = slug.ToLowerInvariant();
|
||||
}
|
||||
|
||||
public void FillSlug<TModel>(TModel model, Func<string, string> generateSlug) where TModel : IsRoutable {
|
||||
if (!string.IsNullOrEmpty(model.Slug) || string.IsNullOrEmpty(model.Title))
|
||||
return;
|
||||
|
||||
model.Slug = generateSlug(model.Title).ToLowerInvariant();
|
||||
}
|
||||
|
||||
public string GenerateUniqueSlug(string slugCandidate, IEnumerable<string> existingSlugs) {
|
||||
if (existingSlugs == null || !existingSlugs.Contains(slugCandidate))
|
||||
return slugCandidate;
|
||||
|
||||
int? version = existingSlugs.Select(s => GetSlugVersion(slugCandidate, s)).OrderBy(i => i).LastOrDefault();
|
||||
|
||||
return version != null
|
||||
? string.Format("{0}-{1}", slugCandidate, version)
|
||||
: slugCandidate;
|
||||
}
|
||||
|
||||
private static int? GetSlugVersion(string slugCandidate, string slug) {
|
||||
int v;
|
||||
string[] slugParts = slug.Split(new []{slugCandidate}, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
if (slugParts.Length == 0)
|
||||
return 2;
|
||||
|
||||
return int.TryParse(slugParts[0].TrimStart('-'), out v)
|
||||
? (int?)++v
|
||||
: null;
|
||||
}
|
||||
|
||||
public IEnumerable<IsRoutable> GetSimilarSlugs(string contentType, string slug)
|
||||
{
|
||||
return
|
||||
_contentManager.Query(contentType).Join<RoutableRecord>()
|
||||
.List()
|
||||
.Select(i => i.As<IsRoutable>())
|
||||
.Where(routable => routable.Slug.StartsWith(slug, StringComparison.OrdinalIgnoreCase)) // todo: for some reason the filter doesn't work within the query, even without StringComparison or StartsWith
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
public bool IsSlugValid(string slug) {
|
||||
// see http://tools.ietf.org/html/rfc3987 for prohibited chars
|
||||
return slug == null || String.IsNullOrEmpty(slug.Trim()) || Regex.IsMatch(slug, @"^[^/:?#\[\]@!$&'()*+,;=\s]+$");
|
||||
}
|
||||
|
||||
public bool ProcessSlug(IsRoutable part)
|
||||
{
|
||||
FillSlug(part);
|
||||
|
||||
if (string.IsNullOrEmpty(part.Slug))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var slugsLikeThis = GetSimilarSlugs(part.ContentItem.ContentType, part.Slug);
|
||||
|
||||
// If the part is already a valid content item, don't include it in the list
|
||||
// of slug to consider for conflict detection
|
||||
if (part.ContentItem.Id != 0)
|
||||
slugsLikeThis = slugsLikeThis.Where(p => p.ContentItem.Id != part.ContentItem.Id);
|
||||
|
||||
//todo: (heskew) need better messages
|
||||
if (slugsLikeThis.Count() > 0)
|
||||
{
|
||||
var originalSlug = part.Slug;
|
||||
//todo: (heskew) make auto-uniqueness optional
|
||||
part.Slug = GenerateUniqueSlug(part.Slug, slugsLikeThis.Select(p => p.Slug));
|
||||
|
||||
if (originalSlug != part.Slug) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
using Orchard.Mvc.ViewModels;
|
||||
|
||||
namespace Orchard.Core.Routable.ViewModels {
|
||||
public class RoutableDisplayViewModel : BaseViewModel {
|
||||
public class RoutableDisplayViewModel : BaseViewModel {
|
||||
public ContentItemViewModel<IRoutableAspect> Routable {get;set;}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Orchard.Core.Routable.ViewModels {
|
||||
public class RoutableEditorViewModel {
|
||||
|
||||
public int Id { get; set; }
|
||||
public string ContentType { get; set; }
|
||||
|
||||
[Required]
|
||||
public string Title { get; set; }
|
||||
public string Slug { get; set; }
|
||||
public int? ContainerId { get; set; }
|
||||
|
||||
public string DisplayLeadingPath { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<%@ Control Language="C#" Inherits="Orchard.Mvc.ViewUserControl<Orchard.Core.Routable.ViewModels.RoutableEditorViewModel>" %>
|
||||
<%@ Import Namespace="Orchard.Utility.Extensions"%>
|
||||
<%@ Import Namespace="Orchard.ContentManagement.Extenstions"%>
|
||||
|
||||
<% Html.RegisterFootScript("jquery.slugify.js"); %>
|
||||
<fieldset>
|
||||
<%=Html.LabelFor(m => m.Title) %>
|
||||
<%=Html.TextBoxFor(m => m.Title, new { @class = "large text" }) %>
|
||||
</fieldset>
|
||||
<fieldset class="permalink">
|
||||
<label class="sub" for="Slug"><%=_Encoded("Permalink")%><br /><span><%=Html.Encode(Request.ToRootUrlString())%>/<%:Model.DisplayLeadingPath %></span></label>
|
||||
<span><%=Html.TextBoxFor(m => m.Slug, new { @class = "text" })%></span>
|
||||
</fieldset>
|
||||
|
||||
|
||||
<% using (this.Capture("end-of-page-scripts")) { %>
|
||||
<script type="text/javascript">
|
||||
$(function(){
|
||||
//pull slug input from tab order
|
||||
$("#<%:Html.FieldIdFor(m=>m.Slug)%>").attr("tabindex",-1);
|
||||
$("#<%:Html.FieldIdFor(m=>m.Title)%>").blur(function(){
|
||||
$(this).slugify({
|
||||
target:$("#<%:Html.FieldIdFor(m=>m.Slug)%>"),
|
||||
url:"<%=Url.Action("Slugify","Item",new RouteValueDictionary{{"Area","Routable"}})%>",
|
||||
contentType:"<%=Model.ContentType %>",
|
||||
id:"<%=Model.Id %>" <%if (Model.ContainerId != null) { %>,
|
||||
containerId:<%=Model.ContainerId %><%} %>
|
||||
})
|
||||
})
|
||||
})</script>
|
||||
<% } %>
|
||||
Reference in New Issue
Block a user