diff --git a/src/Orchard.Web/Core/Common/Drivers/CommonDriver.cs b/src/Orchard.Web/Core/Common/Drivers/CommonDriver.cs new file mode 100644 index 000000000..b8078ffce --- /dev/null +++ b/src/Orchard.Web/Core/Common/Drivers/CommonDriver.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web; +using Orchard.ContentManagement; +using Orchard.ContentManagement.Drivers; +using Orchard.Core.Common.Models; +using Orchard.Core.Common.ViewModels; +using Orchard.Localization; +using Orchard.Security; +using Orchard.Services; + +namespace Orchard.Core.Common.Drivers { + public class CommonDriver : ContentPartDriver { + private readonly IContentManager _contentManager; + private readonly IAuthenticationService _authenticationService; + private readonly IAuthorizationService _authorizationService; + private readonly IMembershipService _membershipService; + private readonly IClock _clock; + + public CommonDriver( + IContentManager contentManager, + IAuthenticationService authenticationService, + IAuthorizationService authorizationService, + IMembershipService membershipService, + IClock clock) { + _contentManager = contentManager; + _authenticationService = authenticationService; + _authorizationService = authorizationService; + _membershipService = membershipService; + _clock = clock; + T = NullLocalizer.Instance; + } + + public Localizer T { get; set; } + + protected override DriverResult Editor(CommonAspect part) { + return Combined(OwnerEditor(part, null), ContainerEditor(part, null)); + } + + protected override DriverResult Editor(CommonAspect instance, ContentManagement.IUpdateModel updater) { + // this event is hooked so the modified timestamp is changed when an edit-post occurs. + instance.ModifiedUtc = _clock.UtcNow; + instance.VersionModifiedUtc = _clock.UtcNow; + + return Combined(OwnerEditor(instance, updater), ContainerEditor(instance, updater)); + } + + DriverResult OwnerEditor(CommonAspect part, IUpdateModel updater) { + var currentUser = _authenticationService.GetAuthenticatedUser(); + if (!_authorizationService.TryCheckAccess(Permissions.ChangeOwner, currentUser, part)) { + return null; + } + + var model = new OwnerEditorViewModel(); + if (part.Owner != null) + model.Owner = part.Owner.UserName; + + if (updater != null) { + var priorOwner = model.Owner; + updater.TryUpdateModel(model, "CommonAspect", null, null); + + if (model.Owner != null && model.Owner != priorOwner) { + var newOwner = _membershipService.GetUser(model.Owner); + if (newOwner == null) { + updater.AddModelError("CommonAspect.Owner", T("Invalid user name")); + } + else { + part.Owner = newOwner; + } + } + } + + return ContentPartTemplate(model, "Parts/Common.Owner", "CommonAspect").Location("primary", "10"); + } + + DriverResult ContainerEditor(CommonAspect part, IUpdateModel updater) { + var currentUser = _authenticationService.GetAuthenticatedUser(); + if (!_authorizationService.TryCheckAccess(Permissions.ChangeOwner, currentUser, part)) { + return null; + } + + var model = new ContainerEditorViewModel(); + if (part.Container != null) + model.ContainerId = part.Container.ContentItem.Id; + + if (updater != null) { + var priorContainerId = model.ContainerId; + updater.TryUpdateModel(model, "CommonAspect", null, null); + + if (model.ContainerId != null && model.ContainerId != priorContainerId) { + var newContainer = _contentManager.Get((int)model.ContainerId, VersionOptions.Latest); + if (newContainer == null) { + updater.AddModelError("CommonAspect.ContainerId", T("Invalid container")); + } + else { + part.Container = newContainer; + } + } + } + return ContentPartTemplate(model, "Parts/Common.Container", "CommonAspect").Location("primary", "10.1"); + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Core/Common/Handlers/CommonAspectHandler.cs b/src/Orchard.Web/Core/Common/Handlers/CommonAspectHandler.cs index f3db7095e..7ec96d6da 100644 --- a/src/Orchard.Web/Core/Common/Handlers/CommonAspectHandler.cs +++ b/src/Orchard.Web/Core/Common/Handlers/CommonAspectHandler.cs @@ -52,8 +52,8 @@ namespace Orchard.Core.Common.Handlers { OnPublishing>(AssignPublishingDates); //OnGetDisplayViewModel(); - OnGetEditorViewModel(GetEditor); - OnUpdateEditorViewModel(UpdateEditor); + //OnGetEditorViewModel(GetEditor); + //OnUpdateEditorViewModel(UpdateEditor); OnIndexing((context, commonAspect) => context.IndexDocument .Add("type", commonAspect.ContentItem.ContentType).Analyze(false) @@ -157,48 +157,48 @@ namespace Orchard.Core.Common.Handlers { } - private void GetEditor(BuildEditorModelContext context, CommonAspect instance) { - var currentUser = _authenticationService.GetAuthenticatedUser(); - if (!_authorizationService.TryCheckAccess(Permissions.ChangeOwner, currentUser, instance)) { - return; - } - var viewModel = new OwnerEditorViewModel(); - if (instance.Owner != null) - viewModel.Owner = instance.Owner.UserName; + //private void GetEditor(BuildEditorModelContext context, CommonAspect instance) { + // var currentUser = _authenticationService.GetAuthenticatedUser(); + // if (!_authorizationService.TryCheckAccess(Permissions.ChangeOwner, currentUser, instance)) { + // return; + // } + // var viewModel = new OwnerEditorViewModel(); + // if (instance.Owner != null) + // viewModel.Owner = instance.Owner.UserName; - context.AddEditor(new TemplateViewModel(viewModel, "CommonAspect") { TemplateName = "Parts/Common.Owner", ZoneName = "primary", Position = "999" }); - } + // context.AddEditor(new TemplateViewModel(viewModel, "CommonAspect") { TemplateName = "Parts/Common.Owner", ZoneName = "primary", Position = "999" }); + //} - private void UpdateEditor(UpdateEditorModelContext context, CommonAspect instance) { - // this event is hooked so the modified timestamp is changed when an edit-post occurs. - // kind of a loose rule of thumb. may not be sufficient - instance.ModifiedUtc = _clock.UtcNow; - instance.VersionModifiedUtc = _clock.UtcNow; + //private void UpdateEditor(UpdateEditorModelContext context, CommonAspect instance) { + // // this event is hooked so the modified timestamp is changed when an edit-post occurs. + // // kind of a loose rule of thumb. may not be sufficient + // instance.ModifiedUtc = _clock.UtcNow; + // instance.VersionModifiedUtc = _clock.UtcNow; - var currentUser = _authenticationService.GetAuthenticatedUser(); - if (!_authorizationService.TryCheckAccess(Permissions.ChangeOwner, currentUser, instance)) { - return; - } + // var currentUser = _authenticationService.GetAuthenticatedUser(); + // if (!_authorizationService.TryCheckAccess(Permissions.ChangeOwner, currentUser, instance)) { + // return; + // } - var viewModel = new OwnerEditorViewModel(); - if (instance.Owner != null) - viewModel.Owner = instance.Owner.UserName; + // var viewModel = new OwnerEditorViewModel(); + // if (instance.Owner != null) + // viewModel.Owner = instance.Owner.UserName; - var priorOwner = viewModel.Owner; - context.Updater.TryUpdateModel(viewModel, "CommonAspect", null, null); + // var priorOwner = viewModel.Owner; + // context.Updater.TryUpdateModel(viewModel, "CommonAspect", null, null); - if (viewModel.Owner != null && viewModel.Owner != priorOwner) { - var newOwner = _membershipService.GetUser(viewModel.Owner); - if (newOwner == null) { - context.Updater.AddModelError("CommonAspect.Owner", T("Invalid user name")); - } - else { - instance.Owner = newOwner; - } - } + // if (viewModel.Owner != null && viewModel.Owner != priorOwner) { + // var newOwner = _membershipService.GetUser(viewModel.Owner); + // if (newOwner == null) { + // context.Updater.AddModelError("CommonAspect.Owner", T("Invalid user name")); + // } + // else { + // instance.Owner = newOwner; + // } + // } - context.AddEditor(new TemplateViewModel(viewModel, "CommonAspect") { TemplateName = "Parts/Common.Owner", ZoneName = "primary", Position = "999" }); - } + // context.AddEditor(new TemplateViewModel(viewModel, "CommonAspect") { TemplateName = "Parts/Common.Owner", ZoneName = "primary", Position = "999" }); + //} } } \ No newline at end of file diff --git a/src/Orchard.Web/Core/Common/ViewModels/ContainerEditorViewModel.cs b/src/Orchard.Web/Core/Common/ViewModels/ContainerEditorViewModel.cs new file mode 100644 index 000000000..1cdb19a7d --- /dev/null +++ b/src/Orchard.Web/Core/Common/ViewModels/ContainerEditorViewModel.cs @@ -0,0 +1,8 @@ +using System.ComponentModel.DataAnnotations; + +namespace Orchard.Core.Common.ViewModels { + public class ContainerEditorViewModel { + + public int? ContainerId { get; set; } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Core/Common/Views/EditorTemplates/Parts/Common.Container.ascx b/src/Orchard.Web/Core/Common/Views/EditorTemplates/Parts/Common.Container.ascx new file mode 100644 index 000000000..fbefbac2d --- /dev/null +++ b/src/Orchard.Web/Core/Common/Views/EditorTemplates/Parts/Common.Container.ascx @@ -0,0 +1,7 @@ +<%@ Control Language="C#" Inherits="Orchard.Mvc.ViewUserControl" %> +<%@ Import Namespace="Orchard.Core.Common.ViewModels" %> +
+ <%=Html.LabelFor(m=>m.ContainerId) %> + <%=Html.EditorFor(m=>m.ContainerId) %> + <%=Html.ValidationMessageFor(m=>m.ContainerId) %> +
diff --git a/src/Orchard.Web/Core/Orchard.Core.csproj b/src/Orchard.Web/Core/Orchard.Core.csproj index 86497731a..4077ae6aa 100644 --- a/src/Orchard.Web/Core/Orchard.Core.csproj +++ b/src/Orchard.Web/Core/Orchard.Core.csproj @@ -66,9 +66,11 @@ + + @@ -155,8 +157,11 @@ + + + @@ -201,6 +206,7 @@ + @@ -214,6 +220,8 @@ + + diff --git a/src/Orchard.Web/Core/Routable/Controllers/ItemController.cs b/src/Orchard.Web/Core/Routable/Controllers/ItemController.cs index 856f53052..690ad156f 100644 --- a/src/Orchard.Web/Core/Routable/Controllers/ItemController.cs +++ b/src/Orchard.Web/Core/Routable/Controllers/ItemController.cs @@ -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().Container = containerItem; + } + } + + _contentManager.UpdateEditorModel(contentItem, this); + _transactionManager.Cancel(); + + return Json(contentItem.As().Slug ?? slug); + } + + + bool IUpdateModel.TryUpdateModel(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()); + } } } \ No newline at end of file diff --git a/src/Orchard.Web/Core/Routable/Drivers/RoutableDriver.cs b/src/Orchard.Web/Core/Routable/Drivers/RoutableDriver.cs index b3a2f6e02..b315fdd11 100644 --- a/src/Orchard.Web/Core/Routable/Drivers/RoutableDriver.cs +++ b/src/Orchard.Web/Core/Routable/Drivers/RoutableDriver.cs @@ -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 { - 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(); + 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(); + if (commonAspect != null && commonAspect.Container != null) { + var routable = commonAspect.Container.As(); + 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); + } } } \ No newline at end of file diff --git a/src/Orchard.Web/Core/Routable/Scripts/jquery.slugify.js b/src/Orchard.Web/Core/Routable/Scripts/jquery.slugify.js new file mode 100644 index 000000000..070202125 --- /dev/null +++ b/src/Orchard.Web/Core/Routable/Scripts/jquery.slugify.js @@ -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" + ); + } +}); \ No newline at end of file diff --git a/src/Orchard.Web/Core/Routable/Services/IRoutableService.cs b/src/Orchard.Web/Core/Routable/Services/IRoutableService.cs new file mode 100644 index 000000000..4daa265b6 --- /dev/null +++ b/src/Orchard.Web/Core/Routable/Services/IRoutableService.cs @@ -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 model) where TModel : IsRoutable; + void FillSlug(TModel model, Func generateSlug) where TModel : IsRoutable; + string GenerateUniqueSlug(string slugCandidate, IEnumerable existingSlugs); + + /// + /// Returns any content item of the specified content type with similar slugs + /// + IEnumerable GetSimilarSlugs(string contentType, string slug); + + /// + /// Validates the given slug + /// + bool IsSlugValid(string slug); + + /// + /// Defines the slug of a RoutableAspect and validate its unicity + /// + /// True if the slug has been created, False if a conflict occured + bool ProcessSlug(IsRoutable part); + + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Core/Routable/Services/RoutableService.cs b/src/Orchard.Web/Core/Routable/Services/RoutableService.cs new file mode 100644 index 000000000..4b27b4d1e --- /dev/null +++ b/src/Orchard.Web/Core/Routable/Services/RoutableService.cs @@ -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 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 model, Func 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 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 GetSimilarSlugs(string contentType, string slug) + { + return + _contentManager.Query(contentType).Join() + .List() + .Select(i => i.As()) + .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; + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Core/Routable/ViewModels/RoutableDisplayViewModel.cs b/src/Orchard.Web/Core/Routable/ViewModels/RoutableDisplayViewModel.cs index e07d266f5..7dafe4573 100644 --- a/src/Orchard.Web/Core/Routable/ViewModels/RoutableDisplayViewModel.cs +++ b/src/Orchard.Web/Core/Routable/ViewModels/RoutableDisplayViewModel.cs @@ -2,7 +2,7 @@ using Orchard.Mvc.ViewModels; namespace Orchard.Core.Routable.ViewModels { - public class RoutableDisplayViewModel : BaseViewModel { + public class RoutableDisplayViewModel : BaseViewModel { public ContentItemViewModel Routable {get;set;} } } \ No newline at end of file diff --git a/src/Orchard.Web/Core/Routable/ViewModels/RoutableEditorViewModel.cs b/src/Orchard.Web/Core/Routable/ViewModels/RoutableEditorViewModel.cs new file mode 100644 index 000000000..16bb56c98 --- /dev/null +++ b/src/Orchard.Web/Core/Routable/ViewModels/RoutableEditorViewModel.cs @@ -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; } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Core/Routable/Views/EditorTemplates/Parts/Routable.IsRoutable.ascx b/src/Orchard.Web/Core/Routable/Views/EditorTemplates/Parts/Routable.IsRoutable.ascx new file mode 100644 index 000000000..7eb8638d5 --- /dev/null +++ b/src/Orchard.Web/Core/Routable/Views/EditorTemplates/Parts/Routable.IsRoutable.ascx @@ -0,0 +1,31 @@ +<%@ Control Language="C#" Inherits="Orchard.Mvc.ViewUserControl" %> +<%@ Import Namespace="Orchard.Utility.Extensions"%> +<%@ Import Namespace="Orchard.ContentManagement.Extenstions"%> + +<% Html.RegisterFootScript("jquery.slugify.js"); %> +
+ <%=Html.LabelFor(m => m.Title) %> + <%=Html.TextBoxFor(m => m.Title, new { @class = "large text" }) %> +
+ + + +<% using (this.Capture("end-of-page-scripts")) { %> + +<% } %> diff --git a/src/Orchard/Mvc/Html/HtmlHelperExtensions.cs b/src/Orchard/Mvc/Html/HtmlHelperExtensions.cs index c302658aa..b38925545 100644 --- a/src/Orchard/Mvc/Html/HtmlHelperExtensions.cs +++ b/src/Orchard/Mvc/Html/HtmlHelperExtensions.cs @@ -23,6 +23,14 @@ namespace Orchard.Mvc.Html { return Reflect.NameOf(html.ViewData.Model, expression); } + public static string FieldNameFor(this HtmlHelper html, Expression> expression) { + return html.ViewData.TemplateInfo.GetFullHtmlFieldName(ExpressionHelper.GetExpressionText(expression)); + } + public static string FieldIdFor(this HtmlHelper html, Expression> expression) { + return html.ViewData.TemplateInfo.GetFullHtmlFieldId(ExpressionHelper.GetExpressionText(expression)); + } + + public static MvcHtmlString SelectOption(this HtmlHelper html, T currentValue, T optionValue, string text) { return SelectOption(html, optionValue, object.Equals(optionValue, currentValue), text); }