Merge from dev

--HG--
branch : dev
This commit is contained in:
Phil Haack
2010-06-10 22:20:45 -07:00
60 changed files with 907 additions and 209 deletions

View File

@@ -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());
}
}
}

View File

@@ -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);
}
}
}

View 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"
);
}
});

View 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);
}
}

View 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;
}
}
}

View File

@@ -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;}
}
}

View File

@@ -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; }
}
}

View File

@@ -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>
<% } %>