Validating content type names, and displaying technical names

Work Item: 16471

--HG--
branch : dev
This commit is contained in:
Sebastien Ros
2010-11-24 15:24:11 -08:00
parent 2652425378
commit 67a45f1c09
9 changed files with 446 additions and 18 deletions

View File

@@ -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 "<a[^>]*>.*?Create new type</a>"
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 "<h1[^>]*>.*?New Content Type.*?</h1>"
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 "<h1[^>]*>.*?New Content Type.*?</h1>"
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"

276
src/Orchard.Specs/ContentTypes.feature.cs generated Normal file
View File

@@ -0,0 +1,276 @@
// ------------------------------------------------------------------------------
// <auto-generated>
// 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.
// </auto-generated>
// ------------------------------------------------------------------------------
#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 \"<a[^>]*>.*?Create new type</a>\"");
#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 \"<h1[^>]*>.*?New Content Type.*?</h1>\"");
#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 \"<h1[^>]*>.*?New Content Type.*?</h1>\"");
#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

View File

@@ -142,6 +142,11 @@
<AutoGen>True</AutoGen>
<DesignTime>True</DesignTime>
</Compile>
<Compile Include="ContentTypes.feature.cs">
<DependentUpon>ContentTypes.feature</DependentUpon>
<AutoGen>True</AutoGen>
<DesignTime>True</DesignTime>
</Compile>
<Compile Include="Hosting\ExtensionDeploymentOptions.cs" />
<Compile Include="SiteCompilation.feature.cs">
<DependentUpon>SiteCompilation.feature</DependentUpon>
@@ -228,6 +233,10 @@
<Generator>SpecFlowSingleFileGenerator</Generator>
<LastGenOutput>ContentRights.feature.cs</LastGenOutput>
</None>
<None Include="ContentTypes.feature">
<Generator>SpecFlowSingleFileGenerator</Generator>
<LastGenOutput>ContentTypes.feature.cs</LastGenOutput>
</None>
<None Include="SiteCompilation.feature">
<Generator>SpecFlowSingleFileGenerator</Generator>
<LastGenOutput>SiteCompilation.feature.cs</LastGenOutput>

View File

@@ -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) {

View File

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

View File

@@ -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<EditTypeViewModel> 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<EditPartViewModel> GetParts();
EditPartViewModel GetPart(string name);

View File

@@ -1,5 +1,6 @@
namespace Orchard.ContentTypes.ViewModels {
public class CreateTypeViewModel {
public string DisplayName { get; set; }
public string Name { get; set; }
}
}

View File

@@ -1,10 +1,40 @@
@model Orchard.ContentTypes.ViewModels.CreateTypeViewModel
<h1>@Html.TitleForPage(T("New Content Type").ToString())</h1>@using (Html.BeginFormAntiForgeryPost()) {
@Html.ValidationSummary()
<fieldset>
<label for="DisplayName">@T("Display Name")</label>
@Html.TextBoxFor(m => m.DisplayName, new {@class = "textMedium", autofocus = "autofocus"})
<label for="Name">@T("Technical Name")</label>
@Html.TextBoxFor(m => m.Name, new {@class = "text"})
</fieldset>
<fieldset>
<button class="primaryAction" type="submit">@T("Create")</button>
</fieldset>}
</fieldset>}
@using(Script.Foot()){
<script type="text/javascript">
//<![CDATA[
$(function(){
//pull slug input from tab order
$("#@Html.FieldIdFor(m=>m.Name)").attr("tabindex",-1);
$("#@Html.FieldIdFor(m=>m.DisplayName)").blur(function(){
var name = $("#@Html.FieldIdFor(m=>m.Name)");
if (name.val()) { return true; }
var displayName = $("#@Html.FieldIdFor(m=>m.DisplayName)").val();
jQuery.post(
"@Url.Action("ContentTypeName","Admin",new RouteValueDictionary{{"Area","Orchard.ContentTypes"}})",
{
displayName: $("#@Html.FieldIdFor(m=>m.DisplayName)").val(),
__RequestVerificationToken: $("input[name=__RequestVerificationToken]").val()
},
function(data) {
name.val(data);
},
"json"
);
})
})
//]]>
</script>
}

View File

@@ -12,7 +12,7 @@
@Html.ValidationSummary()
<fieldset>
<label for="DisplayName">@T("Display Name")</label>
@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 *@
<meta itemprop="DisplayName" content="@Model.DisplayName" />
@* has unintended consequences (renamging the type) - changing the name creates a new type of that name *@