diff --git a/src/Orchard.Web/Modules/Orchard.CustomForms/AdminMenu.cs b/src/Orchard.Web/Modules/Orchard.CustomForms/AdminMenu.cs new file mode 100644 index 000000000..ba3beb998 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.CustomForms/AdminMenu.cs @@ -0,0 +1,18 @@ +using Orchard.Localization; +using Orchard.Security; +using Orchard.UI.Navigation; + +namespace Orchard.CustomForms { + public class AdminMenu : INavigationProvider { + public Localizer T { get; set; } + public string MenuName { get { return "admin"; } } + + public void GetNavigation(NavigationBuilder builder) { + builder.Add(T("Forms"), "4", + menu => menu + .Add(T("Manage Forms"), "1.0", + item => item.Action("Index", "Admin", new { area = "Orchard.CustomForms" }).Permission(StandardPermissions.SiteOwner)) + ); + } + } +} diff --git a/src/Orchard.Web/Modules/Orchard.CustomForms/Controllers/AdminController.cs b/src/Orchard.Web/Modules/Orchard.CustomForms/Controllers/AdminController.cs new file mode 100644 index 000000000..ffbd89837 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.CustomForms/Controllers/AdminController.cs @@ -0,0 +1,145 @@ +using System.Collections.Generic; +using System.Linq; +using System.Web.Mvc; +using System.Web.Routing; +using Orchard.ContentManagement; +using Orchard.Core.Common.Models; +using Orchard.Core.Title.Models; +using Orchard.CustomForms.Models; +using Orchard.CustomForms.ViewModels; +using Orchard.DisplayManagement; +using Orchard.Localization; +using Orchard.Security; +using System; +using Orchard.Settings; +using Orchard.UI.Navigation; + +namespace Orchard.CustomForms.Controllers { + [ValidateInput(false)] + public class AdminController : Controller { + private readonly ISiteService _siteService; + + public AdminController( + IOrchardServices services, + IShapeFactory shapeFactory, + ISiteService siteService) { + _siteService = siteService; + Services = services; + + T = NullLocalizer.Instance; + Shape = shapeFactory; + } + + dynamic Shape { get; set; } + public IOrchardServices Services { get; set; } + public Localizer T { get; set; } + + public ActionResult Index(CustomFormIndexOptions options, PagerParameters pagerParameters) { + if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner, T("Not authorized to list custom forms"))) + return new HttpUnauthorizedResult(); + + var pager = new Pager(_siteService.GetSiteSettings(), pagerParameters); + + // default options + if (options == null) + options = new CustomFormIndexOptions(); + + var query = Services.ContentManager.Query().ForType("CustomForm", "CustomFormWidget"); + + switch (options.Filter) { + case CustomFormFilter.All: + break; + } + + var pagerShape = Shape.Pager(pager).TotalItemCount(query.Count()); + + switch (options.Order) { + case CustomFormOrder.Creation: + query = query.Join().OrderByDescending(u => u.CreatedUtc); + break; + } + + var results = query + .Slice(pager.GetStartIndex(), pager.PageSize); + + var model = new CustomFormIndexViewModel { + CustomForms = results.Select(x => new CustomFormEntry { CustomForm = x.As() }).ToList(), + Options = options, + Pager = pagerShape + }; + + // maintain previous route data when generating page links + var routeData = new RouteData(); + routeData.Values.Add("Options.Filter", options.Filter); + routeData.Values.Add("Options.Search", options.Search); + routeData.Values.Add("Options.Order", options.Order); + + pagerShape.RouteData(routeData); + + return View(model); + } + + [HttpPost] + [Core.Contents.Controllers.FormValueRequired("submit.BulkEdit")] + public ActionResult Index(FormCollection input) { + if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner, T("Not authorized to manage customForm"))) + return new HttpUnauthorizedResult(); + + var viewModel = new CustomFormIndexViewModel { CustomForms = new List(), Options = new CustomFormIndexOptions() }; + UpdateModel(viewModel); + + var checkedEntries = viewModel.CustomForms.Where(c => c.IsChecked); + switch (viewModel.Options.BulkAction) { + case CustomFormBulkAction.None: + break; + case CustomFormBulkAction.Publish: + foreach (var entry in checkedEntries) { + Services.ContentManager.Publish(Services.ContentManager.Get(entry.CustomForm.Id)); + } + break; + case CustomFormBulkAction.Unpublish: + foreach (var entry in checkedEntries) { + Services.ContentManager.Unpublish(Services.ContentManager.Get(entry.CustomForm.Id)); + } + break; + case CustomFormBulkAction.Delete: + foreach (var entry in checkedEntries) { + Services.ContentManager.Remove(Services.ContentManager.Get(entry.CustomForm.Id)); + } + break; + } + + return Index(viewModel.Options, new PagerParameters()); + } + + public ActionResult Item(int id, PagerParameters pagerParameters) { + if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner, T("Not authorized to manage customForm"))) + return new HttpUnauthorizedResult(); + + Pager pager = new Pager(_siteService.GetSiteSettings(), pagerParameters); + + var formPart = Services.ContentManager.Get(id, VersionOptions.Published).As(); + if (formPart == null) + return HttpNotFound(); + + var submissions = Services.ContentManager + .Query() + .ForVersion(VersionOptions.Latest) + .Where(x => x.Container.Id == id) + .OrderByDescending(x => x.CreatedUtc) + .Slice(pager.GetStartIndex(), pager.PageSize) + .Select(b => Services.ContentManager.BuildDisplay(b, "SummaryAdmin")); + + var shape = Services.New.CustomFormList(); + + var list = Shape.List(); + list.AddRange(submissions); + + var totalItemCount = Services.ContentManager + .Query().Where(x => x.Container == formPart.ContentItem.Record).Count(); + shape.Pager(Services.New.Pager(pager).TotalItemCount(totalItemCount)); + shape.List(list); + return View(shape); + } + } +} diff --git a/src/Orchard.Web/Modules/Orchard.CustomForms/Controllers/ItemController.cs b/src/Orchard.Web/Modules/Orchard.CustomForms/Controllers/ItemController.cs new file mode 100644 index 000000000..4d4f09f09 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.CustomForms/Controllers/ItemController.cs @@ -0,0 +1,183 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Web.Mvc; +using Orchard.ContentManagement; +using Orchard.ContentManagement.Aspects; +using Orchard.Core.Contents.Settings; +using Orchard.CustomForms.Models; +using Orchard.CustomForms.Rules; +using Orchard.Data; +using Orchard.DisplayManagement; +using Orchard.Localization; +using Orchard.Logging; +using Orchard.Mvc.Extensions; +using Orchard.Themes; +using Orchard.Tokens; +using Orchard.UI.Notify; + +namespace Orchard.CustomForms.Controllers { + [Themed(true)] + public class ItemController : Controller, IUpdateModel { + private readonly IContentManager _contentManager; + private readonly ITransactionManager _transactionManager; + private readonly IRulesManager _rulesManager; + private readonly ITokenizer _tokenizer; + + public ItemController( + IOrchardServices orchardServices, + IContentManager contentManager, + ITransactionManager transactionManager, + IShapeFactory shapeFactory, + IRulesManager rulesManager, + ITokenizer tokenizer) { + Services = orchardServices; + _contentManager = contentManager; + _transactionManager = transactionManager; + _rulesManager = rulesManager; + _tokenizer = tokenizer; + T = NullLocalizer.Instance; + Logger = NullLogger.Instance; + Shape = shapeFactory; + } + + dynamic Shape { get; set; } + public IOrchardServices Services { get; private set; } + public Localizer T { get; set; } + public ILogger Logger { get; set; } + + public ActionResult Create(int id) { + var form = _contentManager.Get(id); + + if(form == null || !form.Has()) { + return HttpNotFound(); + } + + var customForm = form.As(); + + var contentItem = _contentManager.New(customForm.ContentType); + + if(!contentItem.Has()) { + throw new OrchardException(T("The content type must have CommonPart attached")); + } + + if (!Services.Authorizer.Authorize(Permissions.CreateSubmitPermission(customForm.ContentType), contentItem, T("Cannot create content"))) + return new HttpUnauthorizedResult(); + + dynamic model = _contentManager.BuildEditor(contentItem); + + model + .ContenItem(form) + .ReturnUrl(Url.RouteUrl(_contentManager.GetItemMetadata(form).DisplayRouteValues)); + + // Casting to avoid invalid (under medium trust) reflection over the protected View method and force a static invocation. + return View((object)model); + } + + [HttpPost, ActionName("Create")] + [FormValueRequired("submit.Save")] + public ActionResult CreatePOST(int id, string returnUrl) { + return CreatePOST(id, returnUrl, contentItem => { + if (!contentItem.Has() && !contentItem.TypeDefinition.Settings.GetModel().Draftable) + _contentManager.Publish(contentItem); + }); + } + + [HttpPost, ActionName("Create")] + [FormValueRequired("submit.Publish")] + public ActionResult CreateAndPublishPOST(int id, string returnUrl) { + var form = _contentManager.Get(id); + + if (form == null || !form.Has()) { + return HttpNotFound(); + } + + var customForm = form.As(); + + // pass a dummy content to the authorization check to check for "own" variations + var dummyContent = _contentManager.New(customForm.ContentType); + + if (!Services.Authorizer.Authorize(Permissions.CreateSubmitPermission(customForm.ContentType), dummyContent, T("Couldn't create content"))) + return new HttpUnauthorizedResult(); + + return CreatePOST(id, returnUrl, contentItem => _contentManager.Publish(contentItem)); + } + + private ActionResult CreatePOST(int id, string returnUrl, Action conditionallyPublish) { + var form = _contentManager.Get(id); + + if (form == null || !form.Has()) { + return HttpNotFound(); + } + + var customForm = form.As(); + + var contentItem = _contentManager.New(customForm.ContentType); + + if (!Services.Authorizer.Authorize(Permissions.CreateSubmitPermission(customForm.ContentType), contentItem, T("Couldn't create content"))) + return new HttpUnauthorizedResult(); + + _contentManager.Create(contentItem, VersionOptions.Draft); + + dynamic model = _contentManager.UpdateEditor(contentItem, this); + + if (!ModelState.IsValid) { + _transactionManager.Cancel(); + + // if custom form is inside a widget, we display the form itself + if (form.ContentType == "CustomFormWidget") {} + + // Casting to avoid invalid (under medium trust) reflection over the protected View method and force a static invocation. + return View((object)model); + } + + contentItem.As().Container = customForm.ContentItem; + + // triggers any event + _rulesManager.TriggerEvent("CustomForm", "Submitted", + () => new Dictionary { { "Content", contentItem } }); + + if (customForm.Redirect) { + returnUrl = _tokenizer.Replace(customForm.RedirectUrl, new Dictionary { { "Content", contentItem } }); + } + + // save the submitted form + if (!customForm.SaveContentItem) { + Services.ContentManager.Remove(contentItem); + } + else { + conditionallyPublish(contentItem); + } + + // writes a confirmation message + if (customForm.CustomMessage) { + if (!String.IsNullOrWhiteSpace(customForm.Message)) { + Services.Notifier.Information(T(customForm.Message)); + } + } + + return this.RedirectLocal(returnUrl, () => Redirect(Request.RawUrl)); + } + + 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()); + } + } + + public class FormValueRequiredAttribute : ActionMethodSelectorAttribute { + private readonly string _submitButtonName; + + public FormValueRequiredAttribute(string submitButtonName) { + _submitButtonName = submitButtonName; + } + + public override bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo) { + var value = controllerContext.HttpContext.Request.Form[_submitButtonName]; + return !string.IsNullOrEmpty(value); + } + } +} diff --git a/src/Orchard.Web/Modules/Orchard.CustomForms/Drivers/CustomFormPartDriver.cs b/src/Orchard.Web/Modules/Orchard.CustomForms/Drivers/CustomFormPartDriver.cs new file mode 100644 index 000000000..d059c8fc0 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.CustomForms/Drivers/CustomFormPartDriver.cs @@ -0,0 +1,60 @@ +using System.Linq; +using System.Web.Mvc; +using Orchard.ContentManagement; +using Orchard.ContentManagement.Aspects; +using Orchard.ContentManagement.Drivers; +using Orchard.ContentManagement.MetaData; +using Orchard.CustomForms.Models; +using Orchard.CustomForms.ViewModels; + +namespace Orchard.CustomForms.Drivers { + public class CustomFormPartDriver : ContentPartDriver { + private readonly IContentDefinitionManager _contentDefinitionManager; + private readonly IOrchardServices _orchardServices; + + public CustomFormPartDriver( + IContentDefinitionManager contentDefinitionManager, + IOrchardServices orchardServices) { + _contentDefinitionManager = contentDefinitionManager; + _orchardServices = orchardServices; + } + + protected override DriverResult Display(CustomFormPart part, string displayType, dynamic shapeHelper) { + // this method is used by the widget to render the form when it is displayed + + var contentItem = _orchardServices.ContentManager.New(part.ContentType); + + if (!contentItem.Has()) { + return null; + } + + return ContentShape("Parts_CustomForm_Wrapper", () => { + return shapeHelper.Parts_CustomForm_Wrapper() + .Editor(_orchardServices.ContentManager.BuildEditor(contentItem)) + .ContenItem(part) + .ReturnUrl(part.Redirect ? part.RedirectUrl : _orchardServices.WorkContext.HttpContext.Request.RawUrl); + }); + } + + protected override DriverResult Editor(CustomFormPart part, dynamic shapeHelper) { + return ContentShape("Parts_CustomForm_Fields", () => { + var contentTypes = _contentDefinitionManager.ListTypeDefinitions().Select(x => x.Name).OrderBy(x => x); + var viewModel = new CustomFormPartEditViewModel { + ContentTypes = contentTypes, + CustomFormPart = part + }; + + return shapeHelper.EditorTemplate(TemplateName: "Parts.CustomForm.Fields", Model: viewModel, Prefix: Prefix); + }); + } + + protected override DriverResult Editor(CustomFormPart part, IUpdateModel updater, dynamic shapeHelper) { + var viewModel = new CustomFormPartEditViewModel { + CustomFormPart = part + }; + + updater.TryUpdateModel(viewModel, Prefix, null, null); + return Editor(part, shapeHelper); + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.CustomForms/Handlers/CustomFormPartHandler.cs b/src/Orchard.Web/Modules/Orchard.CustomForms/Handlers/CustomFormPartHandler.cs new file mode 100644 index 000000000..b986935ab --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.CustomForms/Handlers/CustomFormPartHandler.cs @@ -0,0 +1,25 @@ +using System.Web.Routing; +using Orchard.CustomForms.Models; +using Orchard.Data; +using Orchard.ContentManagement.Handlers; + +namespace Orchard.CustomForms.Handlers { + public class CustomFormPartHandler : ContentHandler { + public CustomFormPartHandler(IRepository customFormRepository) { + Filters.Add(StorageFilter.For(customFormRepository)); + } + + protected override void GetItemMetadata(GetContentItemMetadataContext context) { + if(context.ContentItem.ContentType != "CustomForm") { + return; + } + + context.Metadata.DisplayRouteValues = new RouteValueDictionary { + {"Area", "Orchard.CustomForms"}, + {"Controller", "Item"}, + {"Action", "Create"}, + {"Id", context.ContentItem.Id} + }; + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.CustomForms/Migrations.cs b/src/Orchard.Web/Modules/Orchard.CustomForms/Migrations.cs new file mode 100644 index 000000000..d297fa1c5 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.CustomForms/Migrations.cs @@ -0,0 +1,52 @@ +using Orchard.ContentManagement.MetaData; +using Orchard.Core.Contents.Extensions; +using Orchard.Data.Migration; + +namespace Orchard.CustomForms { + public class Migrations : DataMigrationImpl { + public int Create() { + ContentDefinitionManager.AlterTypeDefinition("CustomForm", + cfg => cfg + .WithPart("CommonPart") + .WithPart("TitlePart") + .WithPart("AutoroutePart", builder => builder + .WithSetting("AutorouteSettings.AllowCustomPattern", "true") + .WithSetting("AutorouteSettings.AutomaticAdjustmentOnEdit", "false") + .WithSetting("AutorouteSettings.PatternDefinitions", "[{Name:'Title', Pattern: '{Content.Slug}', Description: 'my-form'}]") + .WithSetting("AutorouteSettings.DefaultPatternIndex", "0")) + .WithPart("MenuPart") + .WithPart("CustomFormPart") + .DisplayedAs("Custom Form") + .Draftable() + ); + + SchemaBuilder.CreateTable("CustomFormPartRecord", table => table.ContentPartVersionRecord() + .Column("ContentType", c => c.WithLength(255)) + .Column("CustomMessage") + .Column("Message", c => c.Unlimited()) + .Column("Redirect") + .Column("RedirectUrl", c => c.Unlimited()) + .Column("SaveContentItem") + ); + + return 1; + } + + public int UpdateFrom1() { + ContentDefinitionManager.AlterTypeDefinition("CustomFormWidget", + cfg => cfg + .WithPart("WidgetPart") + .WithPart("CommonPart") + .WithPart("IdentityPart") + .WithPart("CustomFormPart") + .WithSetting("Stereotype", "Widget") + ); + + return 2; + } + + public void Uninstall() { + ContentDefinitionManager.DeleteTypeDefinition("CustomForm"); + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.CustomForms/Models/CustomFormPart.cs b/src/Orchard.Web/Modules/Orchard.CustomForms/Models/CustomFormPart.cs new file mode 100644 index 000000000..bc05b3738 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.CustomForms/Models/CustomFormPart.cs @@ -0,0 +1,43 @@ +using System.ComponentModel.DataAnnotations; +using Orchard.ContentManagement; +using Orchard.ContentManagement.Aspects; +using Orchard.Core.Title.Models; + +namespace Orchard.CustomForms.Models { + public class CustomFormPart : ContentPart { + [Required] + public string ContentType { + get { return Record.ContentType; } + set { Record.ContentType = value; } + } + + public bool SaveContentItem { + get { return Record.SaveContentItem; } + set { Record.SaveContentItem = value; } + } + + public bool CustomMessage { + get { return Record.CustomMessage; } + set { Record.CustomMessage = value; } + } + + public string Message { + get { return Record.Message; } + set { Record.Message = value; } + } + + public bool Redirect { + get { return Record.Redirect; } + set { Record.Redirect = value; } + } + + public string RedirectUrl { + get { return Record.RedirectUrl; } + set { Record.RedirectUrl = value; } + } + + public string Title { + get { return this.As().Title; } + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.CustomForms/Models/CustomFormPartRecord.cs b/src/Orchard.Web/Modules/Orchard.CustomForms/Models/CustomFormPartRecord.cs new file mode 100644 index 000000000..5f153f750 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.CustomForms/Models/CustomFormPartRecord.cs @@ -0,0 +1,18 @@ +using System.ComponentModel.DataAnnotations; +using Orchard.ContentManagement.Records; +using Orchard.Data.Conventions; + +namespace Orchard.CustomForms.Models { + public class CustomFormPartRecord : ContentPartRecord { + [StringLength(255)] + public virtual string ContentType { get; set; } + + [StringLengthMax] + public virtual string Message { get; set; } + public virtual bool CustomMessage { get; set; } + public virtual bool SaveContentItem { get; set; } + [StringLengthMax] + public virtual string RedirectUrl { get; set; } + public virtual bool Redirect { get; set; } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.CustomForms/Module.txt b/src/Orchard.Web/Modules/Orchard.CustomForms/Module.txt new file mode 100644 index 000000000..b6fdb1cc4 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.CustomForms/Module.txt @@ -0,0 +1,13 @@ +Name: Custom Forms +AntiForgery: enabled +Author: The Orchard Team +Website: http://orchardcustomforms.codeplex.com +Version: 1.5 +OrchardVersion: 1.4 +Description: Create custom forms like contact forms or content contributions. +Features: + Orchard.CustomForms: + Name: Custom Forms + Description: Create custom forms like contact forms or content contributions. + Category: Forms + Dependencies: Contents, Orchard.Tokens diff --git a/src/Orchard.Web/Modules/Orchard.CustomForms/Orchard.CustomForms.csproj b/src/Orchard.Web/Modules/Orchard.CustomForms/Orchard.CustomForms.csproj new file mode 100644 index 000000000..9b91b3450 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.CustomForms/Orchard.CustomForms.csproj @@ -0,0 +1,177 @@ + + + + + Debug + AnyCPU + 9.0.30729 + 2.0 + {2CF067CA-064B-43C6-8B88-5E3B99A65F1D} + {349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc} + Library + Properties + Orchard.CustomForms + Orchard.CustomForms + v4.0 + false + + + 4.0 + + + + false + + + true + full + false + bin\ + DEBUG;TRACE + prompt + 4 + AllRules.ruleset + + + pdbonly + true + bin\ + TRACE + prompt + 4 + AllRules.ruleset + + + + + + + 3.5 + + + + False + ..\..\..\..\lib\aspnetmvc\System.Web.Mvc.dll + + + + + + + + + + + + + + + + + + + {2D1D92BB-4555-4CBE-8D0E-63563D6CE4C6} + Orchard.Framework + + + {9916839C-39FC-4CEB-A5AF-89CA7E87119F} + Orchard.Core + + + {6F759635-13D7-4E94-BCC9-80445D63F117} + Orchard.Tokens + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Designer + + + + + + + + + + 10.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + + + + + $(ProjectDir)\..\Manifests + + + + + + + + + + + + False + True + 51352 + / + + + False + True + http://orchard.codeplex.com + False + + + + + \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.CustomForms/Permissions.cs b/src/Orchard.Web/Modules/Orchard.CustomForms/Permissions.cs new file mode 100644 index 000000000..ced3a381c --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.CustomForms/Permissions.cs @@ -0,0 +1,92 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using Orchard.ContentManagement; +using Orchard.ContentManagement.MetaData; +using Orchard.ContentManagement.MetaData.Models; +using Orchard.CustomForms.Models; +using Orchard.Environment.Extensions.Models; +using Orchard.Security.Permissions; + +namespace Orchard.CustomForms { + public class Permissions : IPermissionProvider { + private static readonly Permission SubmitAnyForm = new Permission { Description = "Submit any forms", Name = "Submit" }; + private static readonly Permission SubmitForm = new Permission { Description = "Submit {0} forms", Name = "Submit_{0}", ImpliedBy = new[] { SubmitAnyForm } }; + + private readonly IContentDefinitionManager _contentDefinitionManager; + private readonly IContentManager _contentManager; + + public virtual Feature Feature { get; set; } + + public Permissions(IContentDefinitionManager contentDefinitionManager, IContentManager contentManager) { + _contentDefinitionManager = contentDefinitionManager; + _contentManager = contentManager; + } + + public IEnumerable GetPermissions() { + // manage rights only for Creatable types + var formTypes = _contentManager.Query().List().Select(x => x.ContentType).Distinct(); + + foreach (var contentType in formTypes) { + var typeDefinition = _contentDefinitionManager.GetTypeDefinition(contentType); + if(typeDefinition == null) { + continue; + } + + yield return CreateSubmitPermission(typeDefinition); + } + + yield return SubmitAnyForm; + } + + public IEnumerable GetDefaultStereotypes() { + return new[] { + new PermissionStereotype { + Name = "Administrator", + Permissions = new[] { SubmitAnyForm } + }, + new PermissionStereotype { + Name = "Editor", + Permissions = new[] { SubmitAnyForm } + }, + new PermissionStereotype { + Name = "Moderator", + Permissions = new[] { SubmitAnyForm } + }, + new PermissionStereotype { + Name = "Author", + Permissions = new[] { SubmitAnyForm } + }, + new PermissionStereotype { + Name = "Contributor", + Permissions = new[] { SubmitAnyForm } + } + }; + } + + /// + /// Generates a permission dynamically for a content type + /// + public static Permission CreateSubmitPermission(ContentTypeDefinition typeDefinition) { + return new Permission { + Name = String.Format(SubmitForm.Name, typeDefinition.Name), + Description = String.Format(SubmitForm.Description, typeDefinition.DisplayName), + Category = "Custom Forms", + ImpliedBy = new [] { SubmitForm } + }; + } + + /// + /// Generates a permission dynamically for a content type + /// + public static Permission CreateSubmitPermission(string contentType) { + return new Permission { + Name = String.Format(SubmitForm.Name, contentType), + Description = String.Format(SubmitForm.Description, contentType), + Category = "Custom Forms", + ImpliedBy = new[] { SubmitForm } + }; + } + + } +} diff --git a/src/Orchard.Web/Modules/Orchard.CustomForms/Placement.info b/src/Orchard.Web/Modules/Orchard.CustomForms/Placement.info new file mode 100644 index 000000000..c563f12b9 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.CustomForms/Placement.info @@ -0,0 +1,4 @@ + + + + diff --git a/src/Orchard.Web/Modules/Orchard.CustomForms/Properties/AssemblyInfo.cs b/src/Orchard.Web/Modules/Orchard.CustomForms/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..5dabee457 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.CustomForms/Properties/AssemblyInfo.cs @@ -0,0 +1,35 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Security; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Orchard.CustomForms")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyProduct("Orchard")] +[assembly: AssemblyCopyright("")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("e7ce3952-50fa-4887-9cf5-10ec8fd6f930")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Revision and Build Numbers +// by using the '*' as shown below: +[assembly: AssemblyVersion("1.5")] +[assembly: AssemblyFileVersion("1.5")] diff --git a/src/Orchard.Web/Modules/Orchard.CustomForms/Rules/CustomFormEvents.cs b/src/Orchard.Web/Modules/Orchard.CustomForms/Rules/CustomFormEvents.cs new file mode 100644 index 000000000..2f8b6cb0c --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.CustomForms/Rules/CustomFormEvents.cs @@ -0,0 +1,55 @@ +using System; +using System.Linq; +using Orchard.ContentManagement; +using Orchard.Events; +using Orchard.Localization; + +namespace Orchard.CustomForms.Rules { + public interface IEventProvider : IEventHandler { + void Describe(dynamic describe); + } + + public class CustomFormEvents : IEventProvider { + public CustomFormEvents() { + T = NullLocalizer.Instance; + } + + public Localizer T { get; set; } + + public void Describe(dynamic describe) { + Func contentHasPart = ContentHasPart; + + describe.For("CustomForm", T("Custom Forms"), T("Custom Forms")) + .Element("Submitted", T("Custom Form Submitted"), T("Custom Form is submitted."), contentHasPart, (Func)(context => T("When a custom form for types ({0}) is submitted.", FormatPartsList(context))), "SelectContentTypes") + ; + } + + private string FormatPartsList(dynamic context) { + var contenttypes = context.Properties["contenttypes"]; + + if (String.IsNullOrEmpty(contenttypes)) { + return T("Any").Text; + } + + return contenttypes; + } + + private static bool ContentHasPart(dynamic context) { + string contenttypes = context.Properties["contenttypes"]; + var content = context.Tokens["Content"] as IContent; + + // "" means 'any' + if (String.IsNullOrEmpty(contenttypes)) { + return true; + } + + if (content == null) { + return false; + } + + var contentTypes = contenttypes.Split(new[] { ',' }); + + return contentTypes.Any(contentType => content.ContentItem.TypeDefinition.Name == contentType); + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.CustomForms/Rules/IRulesManager.cs b/src/Orchard.Web/Modules/Orchard.CustomForms/Rules/IRulesManager.cs new file mode 100644 index 000000000..f8a58f7fb --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.CustomForms/Rules/IRulesManager.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using Orchard.Events; + +namespace Orchard.CustomForms.Rules { + public interface IRulesManager : IEventHandler { + void TriggerEvent(string category, string type, Func> tokensContext); + } + +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.CustomForms/Scripts/Web.config b/src/Orchard.Web/Modules/Orchard.CustomForms/Scripts/Web.config new file mode 100644 index 000000000..770adfab5 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.CustomForms/Scripts/Web.config @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Orchard.Web/Modules/Orchard.CustomForms/Security/AuthorizationEventHandler.cs b/src/Orchard.Web/Modules/Orchard.CustomForms/Security/AuthorizationEventHandler.cs new file mode 100644 index 000000000..1e382a730 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.CustomForms/Security/AuthorizationEventHandler.cs @@ -0,0 +1,26 @@ +using JetBrains.Annotations; +using Orchard.ContentManagement; +using Orchard.CustomForms.Models; +using Orchard.Security; + +namespace Orchard.CustomForms.Security { + /// + /// Alters the Edit permission requested by the Contents module before editing a form. Returns a Submit permission instead. + /// + [UsedImplicitly] + public class AuthorizationEventHandler : IAuthorizationServiceEventHandler { + public void Checking(CheckAccessContext context) { } + public void Complete(CheckAccessContext context) { } + + public void Adjust(CheckAccessContext context) { + if (!context.Granted + && context.Permission.Name == Orchard.Core.Contents.Permissions.EditContent.Name + && context.Content != null + && context.Content.ContentItem.ContentType == "CustomForm") { + + context.Adjusted = true; + context.Permission = Permissions.CreateSubmitPermission(context.Content.ContentItem.As().ContentType); + } + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.CustomForms/Styles/Web.config b/src/Orchard.Web/Modules/Orchard.CustomForms/Styles/Web.config new file mode 100644 index 000000000..770adfab5 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.CustomForms/Styles/Web.config @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Orchard.Web/Modules/Orchard.CustomForms/ViewModels/CustomFormIndexViewModel.cs b/src/Orchard.Web/Modules/Orchard.CustomForms/ViewModels/CustomFormIndexViewModel.cs new file mode 100644 index 000000000..d366a8682 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.CustomForms/ViewModels/CustomFormIndexViewModel.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using Orchard.CustomForms.Models; + +namespace Orchard.CustomForms.ViewModels { + + public class CustomFormIndexViewModel { + public IList CustomForms { get; set; } + public CustomFormIndexOptions Options { get; set; } + public dynamic Pager { get; set; } + } + + public class CustomFormEntry { + public CustomFormPart CustomForm { get; set; } + public bool IsChecked { get; set; } + } + + public class CustomFormIndexOptions { + public string Search { get; set; } + public CustomFormOrder Order { get; set; } + public CustomFormFilter Filter { get; set; } + public CustomFormBulkAction BulkAction { get; set; } + } + + public enum CustomFormOrder { + Name, + Creation + } + + public enum CustomFormFilter { + All, + } + + public enum CustomFormBulkAction { + None, + Publish, + Unpublish, + Delete + } +} diff --git a/src/Orchard.Web/Modules/Orchard.CustomForms/ViewModels/CustomFormPartEditViewModel.cs b/src/Orchard.Web/Modules/Orchard.CustomForms/ViewModels/CustomFormPartEditViewModel.cs new file mode 100644 index 000000000..54f13328e --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.CustomForms/ViewModels/CustomFormPartEditViewModel.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; +using Orchard.CustomForms.Models; + +namespace Orchard.CustomForms.ViewModels { + public class CustomFormPartEditViewModel { + public IEnumerable ContentTypes { get; set; } + public CustomFormPart CustomFormPart { get; set; } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.CustomForms/Views/Admin/Index.cshtml b/src/Orchard.Web/Modules/Orchard.CustomForms/Views/Admin/Index.cshtml new file mode 100644 index 000000000..d6b32910f --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.CustomForms/Views/Admin/Index.cshtml @@ -0,0 +1,81 @@ +@using Orchard.CustomForms.ViewModels +@using Orchard.Utility.Extensions +@model Orchard.CustomForms.ViewModels.CustomFormIndexViewModel + +@{ + var formIndex = 0; + + var pageSizes = new List() { 10, 50, 100 }; + var defaultPageSize = WorkContext.CurrentSite.PageSize; + if(!pageSizes.Contains(defaultPageSize)) { + pageSizes.Add(defaultPageSize); + } +} + +

@Html.TitleForPage(T("Manage Custom Forms").ToString())

+@using (Html.BeginFormAntiForgeryPost()) { + @Html.ValidationSummary() +
@Html.ActionLink(T("Add a new Custom Form").ToString(), "Create", "Admin", new { area = "Contents", id = "CustomForm", returUrl = Request.RawUrl }, new { @class = "button primaryAction" })
+ +
+ + + +
+
+ + + + + + +
+
+ + + + + + + + + + @foreach (var entry in Model.CustomForms) { + + + + + + + formIndex++; + } +
 ↓@T("Title")@T("Content Type") 
+ + + + @Html.ItemEditLink(entry.CustomForm.Title, entry.CustomForm.ContentItem) + @if (entry.CustomForm.ContentItem.ContentType == "CustomFormWidget") { + @T("-") @T("Widget") + } + + @entry.CustomForm.ContentType.CamelFriendly() + + @Html.ItemEditLink(T("Edit").Text, entry.CustomForm.ContentItem) @T(" | ") + @Html.Link(T("Delete").Text, Url.ItemRemoveUrl(entry.CustomForm.ContentItem, new { returnUrl = Request.RawUrl }), new { itemprop = "RemoveUrl UnsafeUrl" }) @T(" | ") + @Html.ActionLink(T("Submissions").Text, "Item", "Admin", new { area = "Orchard.CustomForms", id = entry.CustomForm.Id }, new { }) +
+ @Display(Model.Pager) +
+} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.CustomForms/Views/Admin/Item.cshtml b/src/Orchard.Web/Modules/Orchard.CustomForms/Views/Admin/Item.cshtml new file mode 100644 index 000000000..00b210a22 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.CustomForms/Views/Admin/Item.cshtml @@ -0,0 +1,30 @@ +@using Orchard.Core.Contents.ViewModels; +@using Orchard.Utility.Extensions; + +@{ + Model.List.Classes.Add("content-items"); +} + +@if (Model.List.Items.Count > 0) { + using (Html.BeginFormAntiForgeryPost(Url.Action("Item", "Admin", new { area = "Orchard.CustomForms", id = "" }))) { +
+ + + @Html.Hidden("returnUrl", ViewContext.RequestContext.HttpContext.Request.ToUrlString()) + +
+
+ @Display(Model.List) +
+ } +} +else { +
@T("There are no submissions for this form.")
+} + +@Display(Model.Pager) \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.CustomForms/Views/EditorTemplates/Parts.CustomForm.Fields.cshtml b/src/Orchard.Web/Modules/Orchard.CustomForms/Views/EditorTemplates/Parts.CustomForm.Fields.cshtml new file mode 100644 index 000000000..e54f59a1d --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.CustomForms/Views/EditorTemplates/Parts.CustomForm.Fields.cshtml @@ -0,0 +1,43 @@ +@using Orchard.Utility.Extensions +@model Orchard.CustomForms.ViewModels.CustomFormPartEditViewModel + +@Display.TokenHint() + +
+ + + @T("Select the content type which will be used to render the custom form.") + @Html.ValidationMessageFor(m => m.CustomFormPart.ContentType) +
+ +
+ @Html.EditorFor(m => m.CustomFormPart.SaveContentItem) + + @T("Check if you want to save the content item associated to the form. Leave empty if you just want to trigger an action on the event.") +
+ +
+ @Html.EditorFor(m => m.CustomFormPart.CustomMessage) + + +
+ @Html.LabelFor(m => m.CustomFormPart.Message) + @Html.TextBoxFor(m => m.CustomFormPart.Message, new { @class = "text large"}) + @T("The custom message to display to the user when the form is submitted. Leave empty if no messages should be displayed.") +
+
+ +
+ @Html.EditorFor(m => m.CustomFormPart.Redirect) + + +
+ @Html.LabelFor(m => m.CustomFormPart.RedirectUrl) + @Html.TextBoxFor(m => m.CustomFormPart.RedirectUrl, new { @class = "text large tokenized" }) + @T("The url the user should be redirected to once the form is successfully submitted. e.g.: ~/About") +
+
\ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.CustomForms/Views/Item/Create.cshtml b/src/Orchard.Web/Modules/Orchard.CustomForms/Views/Item/Create.cshtml new file mode 100644 index 000000000..06b418dc2 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.CustomForms/Views/Item/Create.cshtml @@ -0,0 +1,19 @@ +@using Orchard.ContentManagement +@{ + ContentItem customForm = Model.ContentItem; + string returnUrl = Model.ReturnUrl; + + // remove default Save/Publish buttons + Model.Zones["Sidebar"].Items.Clear(); +} + +@using (Html.BeginFormAntiForgeryPost(returnUrl)) { + @Html.ValidationSummary() + // Model is a Shape, calling Display() so that it is rendered using the most specific template for its Shape type + @Display(Model) + +
+ +
+} + diff --git a/src/Orchard.Web/Modules/Orchard.CustomForms/Views/Item/Edit.cshtml b/src/Orchard.Web/Modules/Orchard.CustomForms/Views/Item/Edit.cshtml new file mode 100644 index 000000000..db7c3fab8 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.CustomForms/Views/Item/Edit.cshtml @@ -0,0 +1,5 @@ +@using (Html.BeginFormAntiForgeryPost()) { + @Html.ValidationSummary() + // Model is a Shape, calling Display() so that it is rendered using the most specific template for its Shape type + @Display(Model) +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.CustomForms/Views/Parts.CustomForm.Edit.cshtml b/src/Orchard.Web/Modules/Orchard.CustomForms/Views/Parts.CustomForm.Edit.cshtml new file mode 100644 index 000000000..8200b82f4 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.CustomForms/Views/Parts.CustomForm.Edit.cshtml @@ -0,0 +1,6 @@ +@using Orchard.ContentManagement; + +@using (Html.BeginFormAntiForgeryPost(Url.Action("Create", "Item", new { area = "Orchard.CustomForms", id = Model.ContentPart.ContentType }))) { + @Html.ValidationSummary() + @Display(Model.Editor) +} diff --git a/src/Orchard.Web/Modules/Orchard.CustomForms/Views/Parts.CustomForm.SummaryAdmin.cshtml b/src/Orchard.Web/Modules/Orchard.CustomForms/Views/Parts.CustomForm.SummaryAdmin.cshtml new file mode 100644 index 000000000..806f67777 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.CustomForms/Views/Parts.CustomForm.SummaryAdmin.cshtml @@ -0,0 +1,8 @@ +@using Orchard.ContentManagement; +@{ + ContentItem contentItem = Model.ContentItem; +} + +
+ @T("Renders a form for {0}", Model.ContentPart.ContentType) +
\ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.CustomForms/Views/Parts.CustomForm.Wrapper.cshtml b/src/Orchard.Web/Modules/Orchard.CustomForms/Views/Parts.CustomForm.Wrapper.cshtml new file mode 100644 index 000000000..04f86083d --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.CustomForms/Views/Parts.CustomForm.Wrapper.cshtml @@ -0,0 +1,21 @@ +@using Orchard.ContentManagement; + +@{ + ContentItem customForm = Model.ContentItem; + string returnUrl = Model.ReturnUrl; + + // remove default Save/Publish buttons + Model.Editor.Zones["Sidebar"].Items.Clear(); +} + +@using (Html.BeginFormAntiForgeryPost(Url.Action("Create", "Item", new { area = "Orchard.CustomForms", id = Model.ContentItem.Id }))) { + @Html.ValidationSummary() + // Model is a Shape, calling Display() so that it is rendered using the most specific template for its Shape type + @Display(Model.Editor) + + @Html.Hidden("returnUrl", returnUrl); + +
+ +
+} diff --git a/src/Orchard.Web/Modules/Orchard.CustomForms/Views/Web.config b/src/Orchard.Web/Modules/Orchard.CustomForms/Views/Web.config new file mode 100644 index 000000000..b7d215131 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.CustomForms/Views/Web.config @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Orchard.Web/Modules/Orchard.CustomForms/Web.config b/src/Orchard.Web/Modules/Orchard.CustomForms/Web.config new file mode 100644 index 000000000..1f2698ca2 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.CustomForms/Web.config @@ -0,0 +1,41 @@ + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +