From 67a45f1c09993dc959571687c862e9394f177b1c Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Wed, 24 Nov 2010 15:24:11 -0800 Subject: [PATCH] Validating content type names, and displaying technical names Work Item: 16471 --HG-- branch : dev --- src/Orchard.Specs/ContentTypes.feature | 84 ++++++ src/Orchard.Specs/ContentTypes.feature.cs | 276 ++++++++++++++++++ src/Orchard.Specs/Orchard.Specs.csproj | 9 + .../Controllers/AdminController.cs | 23 +- .../Services/ContentDefinitionService.cs | 33 ++- .../Services/IContentDefinitionService.cs | 4 +- .../ViewModels/CreateTypeViewModel.cs | 1 + .../Views/Admin/Create.cshtml | 32 +- .../Views/Admin/Edit.cshtml | 2 +- 9 files changed, 446 insertions(+), 18 deletions(-) create mode 100644 src/Orchard.Specs/ContentTypes.feature create mode 100644 src/Orchard.Specs/ContentTypes.feature.cs diff --git a/src/Orchard.Specs/ContentTypes.feature b/src/Orchard.Specs/ContentTypes.feature new file mode 100644 index 000000000..cb994d201 --- /dev/null +++ b/src/Orchard.Specs/ContentTypes.feature @@ -0,0 +1,84 @@ +Feature: Content Types + In order to add new types to my site + As an adminitrator + I want to create create content types + +Scenario: I can create a new content type + Given I have installed Orchard + And I have installed "Orchard.ContentTypes" + When I go to "Admin/ContentTypes" + Then I should see "]*>.*?Create new type" + When I go to "Admin/ContentTypes/Create" + And I fill in + | name | value | + | DisplayName | Event | + | Name | Event | + And I hit "Create" + And I go to "Admin/ContentTypes/" + Then I should see "Event" + +Scenario: I can't create a content type with an already existing name + Given I have installed Orchard + And I have installed "Orchard.ContentTypes" + When I go to "Admin/ContentTypes/Create" + And I fill in + | name | value | + | DisplayName | Event | + | Name | Event | + And I hit "Create" + And I go to "Admin/ContentTypes/" + Then I should see "Event" + When I go to "Admin/ContentTypes/Create" + And I fill in + | name | value | + | DisplayName | Event | + | Name | Event-2 | + And I hit "Create" + Then I should see "]*>.*?New Content Type.*?" + And I should see "validation-summary-errors" + +Scenario: I can't create a content type with an already existing technical name + Given I have installed Orchard + And I have installed "Orchard.ContentTypes" + When I go to "Admin/ContentTypes/Create" + And I fill in + | name | value | + | DisplayName | Dinner | + | Name | Dinner | + And I hit "Create" + And I go to "Admin/ContentTypes/" + Then I should see "Dinner" + When I go to "Admin/ContentTypes/Create" + And I fill in + | name | value | + | DisplayName | Dinner2 | + | Name | Dinner | + And I hit "Create" + Then I should see "]*>.*?New Content Type.*?" + And I should see "validation-summary-errors" + +Scenario: I can't rename a content type with an already existing name + Given I have installed Orchard + And I have installed "Orchard.ContentTypes" + When I go to "Admin/ContentTypes/Create" + And I fill in + | name | value | + | DisplayName | Dinner | + | Name | Dinner | + And I hit "Create" + And I go to "Admin/ContentTypes/" + Then I should see "Dinner" + When I go to "Admin/ContentTypes/Create" + And I fill in + | name | value | + | DisplayName | Event | + | Name | Event | + And I hit "Create" + And I go to "Admin/ContentTypes/" + Then I should see "Event" + When I go to "Admin/ContentTypes/Edit/Dinner" + And I fill in + | name | value | + | DisplayName | Event | + And I hit "Save" + Then I should see "validation-summary-errors" diff --git a/src/Orchard.Specs/ContentTypes.feature.cs b/src/Orchard.Specs/ContentTypes.feature.cs new file mode 100644 index 000000000..1d5dc2ee1 --- /dev/null +++ b/src/Orchard.Specs/ContentTypes.feature.cs @@ -0,0 +1,276 @@ +// ------------------------------------------------------------------------------ +// +// This code was generated by SpecFlow (http://www.specflow.org/). +// SpecFlow Version:1.3.0.0 +// Runtime Version:4.0.30319.1 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +// ------------------------------------------------------------------------------ +#region Designer generated code +namespace Orchard.Specs +{ + using TechTalk.SpecFlow; + + + [System.CodeDom.Compiler.GeneratedCodeAttribute("TechTalk.SpecFlow", "1.3.0.0")] + [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [NUnit.Framework.TestFixtureAttribute()] + [NUnit.Framework.DescriptionAttribute("Content Types")] + public partial class ContentTypesFeature + { + + private static TechTalk.SpecFlow.ITestRunner testRunner; + +#line 1 "ContentTypes.feature" +#line hidden + + [NUnit.Framework.TestFixtureSetUpAttribute()] + public virtual void FeatureSetup() + { + testRunner = TechTalk.SpecFlow.TestRunnerManager.GetTestRunner(); + TechTalk.SpecFlow.FeatureInfo featureInfo = new TechTalk.SpecFlow.FeatureInfo(new System.Globalization.CultureInfo("en-US"), "Content Types", "In order to add new types to my site\r\nAs an adminitrator\r\nI want to create create" + + " content types", ((string[])(null))); + testRunner.OnFeatureStart(featureInfo); + } + + [NUnit.Framework.TestFixtureTearDownAttribute()] + public virtual void FeatureTearDown() + { + testRunner.OnFeatureEnd(); + testRunner = null; + } + + public virtual void ScenarioSetup(TechTalk.SpecFlow.ScenarioInfo scenarioInfo) + { + testRunner.OnScenarioStart(scenarioInfo); + } + + [NUnit.Framework.TearDownAttribute()] + public virtual void ScenarioTearDown() + { + testRunner.OnScenarioEnd(); + } + + [NUnit.Framework.TestAttribute()] + [NUnit.Framework.DescriptionAttribute("I can create a new content type")] + public virtual void ICanCreateANewContentType() + { + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("I can create a new content type", ((string[])(null))); +#line 6 +this.ScenarioSetup(scenarioInfo); +#line 7 +testRunner.Given("I have installed Orchard"); +#line 8 +testRunner.And("I have installed \"Orchard.ContentTypes\""); +#line 9 +testRunner.When("I go to \"Admin/ContentTypes\""); +#line 10 +testRunner.Then("I should see \"]*>.*?Create new type\""); +#line 11 +testRunner.When("I go to \"Admin/ContentTypes/Create\""); +#line hidden + TechTalk.SpecFlow.Table table1 = new TechTalk.SpecFlow.Table(new string[] { + "name", + "value"}); + table1.AddRow(new string[] { + "DisplayName", + "Event"}); + table1.AddRow(new string[] { + "Name", + "Event"}); +#line 12 +testRunner.And("I fill in", ((string)(null)), table1); +#line 16 +testRunner.And("I hit \"Create\""); +#line 17 +testRunner.And("I go to \"Admin/ContentTypes/\""); +#line 18 +testRunner.Then("I should see \"Event\""); +#line hidden + testRunner.CollectScenarioErrors(); + } + + [NUnit.Framework.TestAttribute()] + [NUnit.Framework.DescriptionAttribute("I can\'t create a content type with an already existing name")] + public virtual void ICanTCreateAContentTypeWithAnAlreadyExistingName() + { + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("I can\'t create a content type with an already existing name", ((string[])(null))); +#line 20 +this.ScenarioSetup(scenarioInfo); +#line 21 +testRunner.Given("I have installed Orchard"); +#line 22 +testRunner.And("I have installed \"Orchard.ContentTypes\""); +#line 23 +testRunner.When("I go to \"Admin/ContentTypes/Create\""); +#line hidden + TechTalk.SpecFlow.Table table2 = new TechTalk.SpecFlow.Table(new string[] { + "name", + "value"}); + table2.AddRow(new string[] { + "DisplayName", + "Event"}); + table2.AddRow(new string[] { + "Name", + "Event"}); +#line 24 +testRunner.And("I fill in", ((string)(null)), table2); +#line 28 +testRunner.And("I hit \"Create\""); +#line 29 +testRunner.And("I go to \"Admin/ContentTypes/\""); +#line 30 +testRunner.Then("I should see \"Event\""); +#line 31 +testRunner.When("I go to \"Admin/ContentTypes/Create\""); +#line hidden + TechTalk.SpecFlow.Table table3 = new TechTalk.SpecFlow.Table(new string[] { + "name", + "value"}); + table3.AddRow(new string[] { + "DisplayName", + "Event"}); + table3.AddRow(new string[] { + "Name", + "Event-2"}); +#line 32 +testRunner.And("I fill in", ((string)(null)), table3); +#line 36 +testRunner.And("I hit \"Create\""); +#line 37 +testRunner.Then("I should see \"]*>.*?New Content Type.*?\""); +#line 38 +testRunner.And("I should see \"validation-summary-errors\""); +#line hidden + testRunner.CollectScenarioErrors(); + } + + [NUnit.Framework.TestAttribute()] + [NUnit.Framework.DescriptionAttribute("I can\'t create a content type with an already existing technical name")] + public virtual void ICanTCreateAContentTypeWithAnAlreadyExistingTechnicalName() + { + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("I can\'t create a content type with an already existing technical name", ((string[])(null))); +#line 40 +this.ScenarioSetup(scenarioInfo); +#line 41 +testRunner.Given("I have installed Orchard"); +#line 42 +testRunner.And("I have installed \"Orchard.ContentTypes\""); +#line 43 +testRunner.When("I go to \"Admin/ContentTypes/Create\""); +#line hidden + TechTalk.SpecFlow.Table table4 = new TechTalk.SpecFlow.Table(new string[] { + "name", + "value"}); + table4.AddRow(new string[] { + "DisplayName", + "Dinner"}); + table4.AddRow(new string[] { + "Name", + "Dinner"}); +#line 44 +testRunner.And("I fill in", ((string)(null)), table4); +#line 48 +testRunner.And("I hit \"Create\""); +#line 49 +testRunner.And("I go to \"Admin/ContentTypes/\""); +#line 50 +testRunner.Then("I should see \"Dinner\""); +#line 51 +testRunner.When("I go to \"Admin/ContentTypes/Create\""); +#line hidden + TechTalk.SpecFlow.Table table5 = new TechTalk.SpecFlow.Table(new string[] { + "name", + "value"}); + table5.AddRow(new string[] { + "DisplayName", + "Dinner2"}); + table5.AddRow(new string[] { + "Name", + "Dinner"}); +#line 52 +testRunner.And("I fill in", ((string)(null)), table5); +#line 56 +testRunner.And("I hit \"Create\""); +#line 57 +testRunner.Then("I should see \"]*>.*?New Content Type.*?\""); +#line 58 +testRunner.And("I should see \"validation-summary-errors\""); +#line hidden + testRunner.CollectScenarioErrors(); + } + + [NUnit.Framework.TestAttribute()] + [NUnit.Framework.DescriptionAttribute("I can\'t rename a content type with an already existing name")] + public virtual void ICanTRenameAContentTypeWithAnAlreadyExistingName() + { + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("I can\'t rename a content type with an already existing name", ((string[])(null))); +#line 60 +this.ScenarioSetup(scenarioInfo); +#line 61 +testRunner.Given("I have installed Orchard"); +#line 62 +testRunner.And("I have installed \"Orchard.ContentTypes\""); +#line 63 +testRunner.When("I go to \"Admin/ContentTypes/Create\""); +#line hidden + TechTalk.SpecFlow.Table table6 = new TechTalk.SpecFlow.Table(new string[] { + "name", + "value"}); + table6.AddRow(new string[] { + "DisplayName", + "Dinner"}); + table6.AddRow(new string[] { + "Name", + "Dinner"}); +#line 64 +testRunner.And("I fill in", ((string)(null)), table6); +#line 68 +testRunner.And("I hit \"Create\""); +#line 69 +testRunner.And("I go to \"Admin/ContentTypes/\""); +#line 70 +testRunner.Then("I should see \"Dinner\""); +#line 71 +testRunner.When("I go to \"Admin/ContentTypes/Create\""); +#line hidden + TechTalk.SpecFlow.Table table7 = new TechTalk.SpecFlow.Table(new string[] { + "name", + "value"}); + table7.AddRow(new string[] { + "DisplayName", + "Event"}); + table7.AddRow(new string[] { + "Name", + "Event"}); +#line 72 +testRunner.And("I fill in", ((string)(null)), table7); +#line 76 +testRunner.And("I hit \"Create\""); +#line 77 +testRunner.And("I go to \"Admin/ContentTypes/\""); +#line 78 +testRunner.Then("I should see \"Event\""); +#line 79 +testRunner.When("I go to \"Admin/ContentTypes/Edit/Dinner\""); +#line hidden + TechTalk.SpecFlow.Table table8 = new TechTalk.SpecFlow.Table(new string[] { + "name", + "value"}); + table8.AddRow(new string[] { + "DisplayName", + "Event"}); +#line 80 +testRunner.And("I fill in", ((string)(null)), table8); +#line 83 +testRunner.And("I hit \"Save\""); +#line 84 +testRunner.Then("I should see \"validation-summary-errors\""); +#line hidden + testRunner.CollectScenarioErrors(); + } + } +} +#endregion diff --git a/src/Orchard.Specs/Orchard.Specs.csproj b/src/Orchard.Specs/Orchard.Specs.csproj index 748a34584..ef1f40764 100644 --- a/src/Orchard.Specs/Orchard.Specs.csproj +++ b/src/Orchard.Specs/Orchard.Specs.csproj @@ -142,6 +142,11 @@ True True + + ContentTypes.feature + True + True + SiteCompilation.feature @@ -228,6 +233,10 @@ SpecFlowSingleFileGenerator ContentRights.feature.cs + + SpecFlowSingleFileGenerator + ContentTypes.feature.cs + SpecFlowSingleFileGenerator SiteCompilation.feature.cs diff --git a/src/Orchard.Web/Modules/Orchard.ContentTypes/Controllers/AdminController.cs b/src/Orchard.Web/Modules/Orchard.ContentTypes/Controllers/AdminController.cs index f4d408d2e..61cc18316 100644 --- a/src/Orchard.Web/Modules/Orchard.ContentTypes/Controllers/AdminController.cs +++ b/src/Orchard.Web/Modules/Orchard.ContentTypes/Controllers/AdminController.cs @@ -47,8 +47,12 @@ namespace Orchard.ContentTypes.Controllers { if(String.IsNullOrWhiteSpace(viewModel.DisplayName)) { ModelState.AddModelError("DisplayName", T("The Content Type name can't be empty.").ToString()); } + + if ( _contentDefinitionService.GetTypes().Any(t => String.Equals(t.Name.Trim(), viewModel.Name.Trim(), StringComparison.OrdinalIgnoreCase)) ) { + ModelState.AddModelError("Name", T("A type with the same technical name already exists.").ToString()); + } - if(_contentDefinitionService.GetTypes().Any(t => t.DisplayName == viewModel.DisplayName)) { + if ( _contentDefinitionService.GetTypes().Any(t => String.Equals(t.DisplayName.Trim(), viewModel.DisplayName.Trim(), StringComparison.OrdinalIgnoreCase)) ) { ModelState.AddModelError("DisplayName", T("A type with the same name already exists.").ToString()); } @@ -57,13 +61,19 @@ namespace Orchard.ContentTypes.Controllers { return View(viewModel); } - var typeViewModel = _contentDefinitionService.AddType(viewModel); + var contentTypeDefinition = _contentDefinitionService.AddType(viewModel.Name, viewModel.DisplayName); + var typeViewModel = new EditTypeViewModel(contentTypeDefinition); + Services.Notifier.Information(T("The \"{0}\" content type has been created.", typeViewModel.DisplayName)); return RedirectToAction("Edit", new { id = typeViewModel.Name }); } + public ActionResult ContentTypeName(string displayName) { + return Json(_contentDefinitionService.GenerateName(displayName)); + } + public ActionResult Edit(string id) { if (!Services.Authorizer.Authorize(Permissions.CreateContentTypes, T("Not allowed to edit a content type."))) return new HttpUnauthorizedResult(); @@ -90,9 +100,18 @@ namespace Orchard.ContentTypes.Controllers { TryUpdateModel(edited); typeViewModel.DisplayName = edited.DisplayName; + if ( String.IsNullOrWhiteSpace(typeViewModel.DisplayName) ) { + ModelState.AddModelError("DisplayName", T("The Content Type name can't be empty.").ToString()); + } + + if ( _contentDefinitionService.GetTypes().Any(t => String.Equals(t.DisplayName.Trim(), typeViewModel.DisplayName.Trim(), StringComparison.OrdinalIgnoreCase) && !String.Equals(t.Name, id)) ) { + ModelState.AddModelError("DisplayName", T("A type with the same name already exists.").ToString()); + } + if (!ModelState.IsValid) return View(typeViewModel); + _contentDefinitionService.AlterType(typeViewModel, this); if (!ModelState.IsValid) { diff --git a/src/Orchard.Web/Modules/Orchard.ContentTypes/Services/ContentDefinitionService.cs b/src/Orchard.Web/Modules/Orchard.ContentTypes/Services/ContentDefinitionService.cs index 9c3d936af..de42386e3 100644 --- a/src/Orchard.Web/Modules/Orchard.ContentTypes/Services/ContentDefinitionService.cs +++ b/src/Orchard.Web/Modules/Orchard.ContentTypes/Services/ContentDefinitionService.cs @@ -65,18 +65,24 @@ namespace Orchard.ContentTypes.Services { return viewModel; } - public EditTypeViewModel AddType(CreateTypeViewModel typeViewModel) { - var name = GenerateName(typeViewModel.DisplayName); + public ContentTypeDefinition AddType(string name, string displayName) { + if(String.IsNullOrWhiteSpace(displayName)) { + throw new ArgumentException("displayName"); + } - while (_contentDefinitionManager.GetTypeDefinition(name) != null) + if(String.IsNullOrWhiteSpace(name)) { + name = GenerateName(displayName); + } + + while ( _contentDefinitionManager.GetTypeDefinition(name) != null ) name = VersionName(name); - var contentTypeDefinition = new ContentTypeDefinition(name, typeViewModel.DisplayName); + var contentTypeDefinition = new ContentTypeDefinition(name, displayName); _contentDefinitionManager.StoreTypeDefinition(contentTypeDefinition); _contentDefinitionManager.AlterTypeDefinition(name, cfg => cfg.Creatable().Draftable()); - return new EditTypeViewModel(contentTypeDefinition); + return contentTypeDefinition; } public void AlterType(EditTypeViewModel typeViewModel, IUpdateModel updateModel) { @@ -210,20 +216,21 @@ namespace Orchard.ContentTypes.Services { } //gratuitously stolen from the RoutableService - private static string GenerateName(string displayName) { - if (string.IsNullOrWhiteSpace(displayName)) - return ""; + public string GenerateName(string name) { + if ( string.IsNullOrWhiteSpace(name) ) + return String.Empty; - var name = displayName; - //todo: might need to be made more restrictive depending on how name is used (like as an XML node name, for instance) - var dissallowed = new Regex(@"[/:?#\[\]@!$&'()*+,;=\s]+"); + var dissallowed = new Regex(@"[/:?#\[\]@!$&'()*+,;=\s\""<>]+"); - name = dissallowed.Replace(name, "-"); - name = name.Trim('-'); + name = dissallowed.Replace(name, String.Empty); + name = name.Trim(); if (name.Length > 128) name = name.Substring(0, 128); + while ( _contentDefinitionManager.GetTypeDefinition(name) != null ) + name = VersionName(name); + return name; } diff --git a/src/Orchard.Web/Modules/Orchard.ContentTypes/Services/IContentDefinitionService.cs b/src/Orchard.Web/Modules/Orchard.ContentTypes/Services/IContentDefinitionService.cs index 70c59e4a7..44fc6b8ae 100644 --- a/src/Orchard.Web/Modules/Orchard.ContentTypes/Services/IContentDefinitionService.cs +++ b/src/Orchard.Web/Modules/Orchard.ContentTypes/Services/IContentDefinitionService.cs @@ -1,17 +1,19 @@ using System.Collections.Generic; using Orchard.ContentManagement; using Orchard.ContentManagement.MetaData; +using Orchard.ContentManagement.MetaData.Models; using Orchard.ContentTypes.ViewModels; namespace Orchard.ContentTypes.Services { public interface IContentDefinitionService : IDependency { IEnumerable GetTypes(); EditTypeViewModel GetType(string name); - EditTypeViewModel AddType(CreateTypeViewModel typeViewModel); + ContentTypeDefinition AddType(string name, string displayName); void AlterType(EditTypeViewModel typeViewModel, IUpdateModel updater); void RemoveType(string name); void AddPartToType(string partName, string typeName); void RemovePartFromType(string partName, string typeName); + string GenerateName(string displayName); IEnumerable GetParts(); EditPartViewModel GetPart(string name); diff --git a/src/Orchard.Web/Modules/Orchard.ContentTypes/ViewModels/CreateTypeViewModel.cs b/src/Orchard.Web/Modules/Orchard.ContentTypes/ViewModels/CreateTypeViewModel.cs index 29a57e6e6..a53768699 100644 --- a/src/Orchard.Web/Modules/Orchard.ContentTypes/ViewModels/CreateTypeViewModel.cs +++ b/src/Orchard.Web/Modules/Orchard.ContentTypes/ViewModels/CreateTypeViewModel.cs @@ -1,5 +1,6 @@ namespace Orchard.ContentTypes.ViewModels { public class CreateTypeViewModel { public string DisplayName { get; set; } + public string Name { get; set; } } } diff --git a/src/Orchard.Web/Modules/Orchard.ContentTypes/Views/Admin/Create.cshtml b/src/Orchard.Web/Modules/Orchard.ContentTypes/Views/Admin/Create.cshtml index fe3758232..847f21fa9 100644 --- a/src/Orchard.Web/Modules/Orchard.ContentTypes/Views/Admin/Create.cshtml +++ b/src/Orchard.Web/Modules/Orchard.ContentTypes/Views/Admin/Create.cshtml @@ -1,10 +1,40 @@ @model Orchard.ContentTypes.ViewModels.CreateTypeViewModel +

@Html.TitleForPage(T("New Content Type").ToString())

@using (Html.BeginFormAntiForgeryPost()) { @Html.ValidationSummary()
@Html.TextBoxFor(m => m.DisplayName, new {@class = "textMedium", autofocus = "autofocus"}) + + @Html.TextBoxFor(m => m.Name, new {@class = "text"})
-
} \ No newline at end of file + } + +@using(Script.Foot()){ + +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.ContentTypes/Views/Admin/Edit.cshtml b/src/Orchard.Web/Modules/Orchard.ContentTypes/Views/Admin/Edit.cshtml index 22520e69c..7e50efb4b 100644 --- a/src/Orchard.Web/Modules/Orchard.ContentTypes/Views/Admin/Edit.cshtml +++ b/src/Orchard.Web/Modules/Orchard.ContentTypes/Views/Admin/Edit.cshtml @@ -12,7 +12,7 @@ @Html.ValidationSummary()
- @Html.TextBoxFor(m => m.DisplayName, new { @class = "textMedium" }) + @Html.TextBoxFor(m => m.DisplayName, new { @class = "textMedium" }) @T("Technical name: {0}", Model.Name) @* todo: if we continue to go down the midrodata route, some helpers would be nice *@ @* has unintended consequences (renamging the type) - changing the name creates a new type of that name *@