Gets more validation bits wired up. Server side validation is now working which is based on val-server-field since we can use this natively with the asp.net ModelState result instead of the val-server which is actually to be used with PropertyType (Property Editors). Next up is validation for the different tabs of the editor.

This commit is contained in:
Shannon
2015-10-08 17:13:38 +02:00
parent 97c352030a
commit c477f7a840
18 changed files with 149 additions and 36 deletions

View File

@@ -16,7 +16,11 @@ function valServerField(serverValidationManager) {
}
var fieldName = attr.valServerField;
var evalfieldName = scope.$eval(attr.valServerField);
if (evalfieldName) {
fieldName = evalfieldName;
}
//subscribe to the changed event of the view model. This is required because when we
// have a server error we actually invalidate the form which means it cannot be
// resubmitted. So once a field is changed that has a server error assigned to it

View File

@@ -65,7 +65,9 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica
self.handleSuccessfulSave({
scope: args.scope,
savedContent: data,
rebindCallback: rebindCallback(args.content, data)
rebindCallback: function() {
rebindCallback.apply(self, [args.content, data]);
}
});
args.scope.busy = false;
@@ -75,7 +77,9 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica
self.handleSaveError({
redirectOnFailure: true,
err: err,
rebindCallback: rebindCallback(args.content, err.data)
rebindCallback: function() {
rebindCallback.apply(self, [args.content, err.data]);
}
});
//show any notifications
if (angular.isArray(err.data.notifications)) {

View File

@@ -160,6 +160,16 @@ function formHelper(angularHelper, serverValidationManager, $timeout, notificati
handleServerValidation: function(modelState) {
for (var e in modelState) {
//This is where things get interesting....
// We need to support validation for all editor types such as both the content and content type editors.
// The Content editor ModelState is quite specific with the way that Properties are validated especially considering
// that each property is a User Developer property editor.
// The way that Content Type Editor ModelState is created is simply based on the ASP.Net validation data-annotations
// system.
// So, to do this (since we need to support backwards compat), we need to hack a little bit. For Content Properties,
// which are user defined, we know that they will exist with a prefixed ModelState of "_Properties.", so if we detect
// this, then we know it's a Property.
//the alias in model state can be in dot notation which indicates
// * the first part is the content property alias
// * the second part is the field to which the valiation msg is associated with
@@ -167,7 +177,11 @@ function formHelper(angularHelper, serverValidationManager, $timeout, notificati
//If it is not prefixed with "Properties" that means the error is for a field of the object directly.
var parts = e.split(".");
if (parts.length > 1) {
//Check if this is for content properties - specific to content/media/member editors because those are special
// user defined properties with custom controls.
if (parts.length > 1 && parts[0] === "_Properties") {
var propertyAlias = parts[1];
//if it contains 2 '.' then we will wire it up to a property's field
@@ -182,8 +196,10 @@ function formHelper(angularHelper, serverValidationManager, $timeout, notificati
}
else {
//the parts are only 1, this means its not a property but a native content property
serverValidationManager.addFieldError(parts[0], modelState[e][0]);
//Everthing else is just a 'Field'... the field name could contain any level of 'parts' though, for example:
// Groups[0].Properties[2].Alias
serverValidationManager.addFieldError(e, modelState[e][0]);
}
//add to notifications

View File

@@ -505,11 +505,19 @@ function umbDataFormatter() {
saveModel.groups = _.map(realGroups, function (g) {
var saveGroup = _.pick(g, 'inherited', 'id', 'sortOrder', 'name');
var saveProperties = _.map(g.properties, function(p) {
var realProperties = _.reject(g.properties, function (p) {
//do not include these properties
return p.propertyState === "init";
});
var saveProperties = _.map(realProperties, function (p) {
var saveProperty = _.pick(p, 'id', 'alias', 'description', 'validation', 'label', 'sortOrder', 'dataTypeId', 'groupId');
return saveProperty;
});
saveGroup.properties = saveProperties;
return saveGroup;
});

View File

@@ -29,7 +29,7 @@
<div class="umb-group-builder__group-title-wrapper">
<ng-form>
<ng-form name="groupNameForm">
<div class="umb-group-builder__group-title control-group -no-margin" ng-class="{'-active':tab.tabState=='active', '-inherited': tab.inherited}">
<i class="umb-group-builder__group-title-icon icon-navigation" ng-if="sortingMode && !tab.inherited"></i>
<input class="umb-group-builder__group-title-input" type="text" placeholder="Enter name..."
@@ -40,7 +40,8 @@
ng-disabled="tab.inherited"
umb-auto-focus
umb-auto-resize
required />
required
val-server-field="{{'Groups[' + $index + '].Name'}}" />
</div>
</ng-form>
@@ -90,19 +91,22 @@
<i class="icon icon-merge"></i> Inherited from {{property.contentTypeName}}
</div>
<div ng-if="!sortingMode">
<ng-form name="propertyTypeForm">
<div class="control-group -no-margin" ng-if="!sortingMode">
<div class="umb-group-builder__property-meta-alias">{{ property.alias }}</div>
<div class="umb-group-builder__property-meta-alias">{{ property.alias }}</div>
<div class="umb-group-builder__property-meta-label">
<textarea placeholder="Label..." ng-model="property.label" ng-disabled="property.inherited"
umb-auto-resize
required
val-server-field="{{'Groups[' + $parent.$index + '].Properties[' + $index + '].Label'}}"></textarea>
</div>
<div class="umb-group-builder__property-meta-label">
<textarea placeholder="Label..." ng-model="property.label" ng-disabled="property.inherited" umb-auto-resize></textarea>
<div class="umb-group-builder__property-meta-description">
<textarea ng-model="property.description" placeholder="Enter your description..." ng-disabled="property.inherited" umb-auto-resize></textarea>
</div>
</div>
<div class="umb-group-builder__property-meta-description">
<textarea ng-model="property.description" placeholder="Enter your description..." ng-disabled="property.inherited" umb-auto-resize></textarea>
</div>
</div>
</ng-form>
<div ng-if="sortingMode">
<i class="icon icon-navigation" ng-if="!property.inherited"></i>

View File

@@ -180,11 +180,8 @@
saveMethod: contentTypeResource.save,
scope: $scope,
content: vm.contentType,
rebindCallback: function (origContent, savedContent) {
//This is called when the data returns and we need to merge the property values from the saved content to the original content.
//re-binds all changed property values to the origContent object from the savedContent object and returns an array of changed properties.
}
//no-op for rebind callback... we don't really need to rebind for content types
rebindCallback: angular.noop
}).then(function (data) {
//success
syncTreeNode(vm.contentType, data.path);

View File

@@ -108,7 +108,7 @@ describe('contentEditingHelper tests', function () {
var allProps = contentEditingHelper.getAllProps(content);
//act
formHelper.handleServerValidation({ "Property.bodyText": ["Required"] });
formHelper.handleServerValidation({ "_Properties.bodyText": ["Required"] });
//assert
expect(serverValidationManager.items.length).toBe(1);
@@ -124,7 +124,7 @@ describe('contentEditingHelper tests', function () {
var allProps = contentEditingHelper.getAllProps(content);
//act
formHelper.handleServerValidation({ "Property.bodyText.value": ["Required"] });
formHelper.handleServerValidation({ "_Properties.bodyText.value": ["Required"] });
//assert
expect(serverValidationManager.items.length).toBe(1);
@@ -144,8 +144,8 @@ describe('contentEditingHelper tests', function () {
{
"Name": ["Required"],
"UpdateDate": ["Invalid date"],
"Property.bodyText.value": ["Required field"],
"Property.textarea": ["Invalid format"]
"_Properties.bodyText.value": ["Required field"],
"_Properties.textarea": ["Invalid format"]
});
//assert

View File

@@ -16,6 +16,7 @@ using Newtonsoft.Json;
using Umbraco.Core.PropertyEditors;
using System;
using System.Net.Http;
using Umbraco.Core.Services;
namespace Umbraco.Web.Editors
{
@@ -118,6 +119,8 @@ namespace Umbraco.Web.Editors
//TODO: This all needs to be done in a transaction!!
// Which means that all of this logic needs to take place inside the service
ContentTypeDisplay display;
if (ctId > 0)
{
//its an update to an existing
@@ -127,8 +130,8 @@ namespace Umbraco.Web.Editors
Mapper.Map(contentTypeSave, found);
ctService.Save(found);
return Mapper.Map<ContentTypeDisplay>(found);
display = Mapper.Map<ContentTypeDisplay>(found);
}
else
{
@@ -171,9 +174,15 @@ namespace Umbraco.Web.Editors
newCt.AddContentType(newCt);
ctService.Save(newCt);
}
return Mapper.Map<ContentTypeDisplay>(newCt);
display = Mapper.Map<ContentTypeDisplay>(newCt);
}
display.AddSuccessNotification(
Services.TextService.Localize("speechBubbles/contentTypeSavedHeader"),
string.Empty);
return display;
}
/// <summary>

View File

@@ -14,6 +14,7 @@ using Umbraco.Core.PropertyEditors;
using Umbraco.Core.Services;
using Umbraco.Web.Models.ContentEditing;
using Umbraco.Web.Mvc;
using Umbraco.Web.WebApi;
using Constants = Umbraco.Core.Constants;
namespace Umbraco.Web.Editors
@@ -22,6 +23,7 @@ namespace Umbraco.Web.Editors
/// Am abstract API controller providing functionality used for dealing with content and media types
/// </summary>
[PluginController("UmbracoApi")]
[PrefixlessBodyModelValidator]
public abstract class ContentTypeControllerBase : UmbracoAuthorizedJsonController
{
private ICultureDictionary _cultureDictionary;

View File

@@ -64,7 +64,7 @@ namespace Umbraco.Web
if (!result.MemberNames.Any())
{
//add a model state error for the entire property
modelState.AddModelError(string.Format("{0}.{1}", "Properties", propertyAlias), result.ErrorMessage);
modelState.AddModelError(string.Format("{0}.{1}", "_Properties", propertyAlias), result.ErrorMessage);
}
else
{
@@ -72,7 +72,7 @@ namespace Umbraco.Web
// so that we can try to match it up to a real sub field of this editor
foreach (var field in result.MemberNames)
{
modelState.AddModelError(string.Format("{0}.{1}.{2}", "Properties", propertyAlias, field), result.ErrorMessage);
modelState.AddModelError(string.Format("{0}.{1}.{2}", "_Properties", propertyAlias, field), result.ErrorMessage);
}
}
}

View File

@@ -26,9 +26,11 @@ namespace Umbraco.Web.Models.ContentEditing
public override string Alias { get; set; }
[DataMember(Name = "updateDate")]
[ReadOnly(true)]
public DateTime UpdateDate { get; set; }
[DataMember(Name = "createDate")]
[ReadOnly(true)]
public DateTime CreateDate { get; set; }
[DataMember(Name = "description")]

View File

@@ -11,7 +11,7 @@ using Umbraco.Core.Models.Validation;
namespace Umbraco.Web.Models.ContentEditing
{
[DataContract(Name = "contentType", Namespace = "")]
public class ContentTypeCompositionDisplay : ContentTypeBasic
public class ContentTypeCompositionDisplay : ContentTypeBasic, INotificationModel
{
public ContentTypeCompositionDisplay()
{

View File

@@ -26,15 +26,17 @@ namespace Umbraco.Web.Models.ContentEditing
public PropertyTypeValidation Validation { get; set; }
[DataMember(Name = "label")]
[Required]
public string Label { get; set; }
[DataMember(Name = "sortOrder")]
public int SortOrder { get; set; }
[DataMember(Name = "dataTypeId")]
[Required]
public int DataTypeId { get; set; }
//SD: Is this really required ?
//SD: Is this really needed ?
[DataMember(Name = "groupId")]
public int GroupId { get; set; }
}

View File

@@ -225,6 +225,8 @@ namespace Umbraco.Web.Models.Mapping
#region *** Used for mapping on top of an existing display object from a save object ***
config.CreateMap<ContentTypeSave, ContentTypeDisplay>()
.ForMember(dto => dto.CreateDate, expression => expression.Ignore())
.ForMember(dto => dto.UpdateDate, expression => expression.Ignore())
.ForMember(dto => dto.AllowedTemplates, expression => expression.Ignore())
.ForMember(dto => dto.DefaultTemplate, expression => expression.Ignore())
.ForMember(dto => dto.ListViewEditorName, expression => expression.Ignore())

View File

@@ -68,6 +68,10 @@ namespace Umbraco.Web.Models.Mapping
.ForMember(dto => dto.Id, expression => expression.Condition(display => (Convert.ToInt32(display.Id) > 0)))
.ForMember(dto => dto.Id, expression => expression.MapFrom(display => Convert.ToInt32(display.Id)))
//These get persisted as part of the saving procedure, nothing to do with the display model
.ForMember(dto => dto.CreateDate, expression => expression.Ignore())
.ForMember(dto => dto.UpdateDate, expression => expression.Ignore())
.ForMember(dto => dto.AllowedAsRoot, expression => expression.MapFrom(display => display.AllowAsRoot))
.ForMember(dto => dto.CreatorId, expression => expression.Ignore())
.ForMember(dto => dto.Level, expression => expression.Ignore())

View File

@@ -904,6 +904,8 @@
<Compile Include="WebApi\MemberAuthorizeAttribute.cs" />
<Compile Include="WebApi\MvcVersionCheck.cs" />
<Compile Include="WebApi\NamespaceHttpControllerSelector.cs" />
<Compile Include="WebApi\PrefixlessBodyModelValidator.cs" />
<Compile Include="WebApi\PrefixlessBodyModelValidatorAttribute.cs" />
<Compile Include="WebApi\UmbracoApiController.cs" />
<Compile Include="WebApi\UmbracoApiControllerBase.cs" />
<Compile Include="WebApi\UmbracoApiControllerResolver.cs" />

View File

@@ -0,0 +1,37 @@
using System;
using System.Web.Http.Controllers;
using System.Web.Http.Metadata;
using System.Web.Http.Validation;
namespace Umbraco.Web.WebApi
{
/// <summary>
/// By default WebApi always appends a prefix to any ModelState error but we don't want this,
/// so this is a custom validator that ensures there is no prefix set.
/// </summary>
/// <remarks>
/// We were already doing this with the content/media/members validation since we had to manually validate because we
/// were posting multi-part values. We were always passing in an empty prefix so it worked. However for other editors we
/// are validating with normal data annotations (for the most part) and we don't want the prefix there either.
/// </remarks>
internal class PrefixlessBodyModelValidator : IBodyModelValidator
{
private readonly IBodyModelValidator _innerValidator;
public PrefixlessBodyModelValidator(IBodyModelValidator innerValidator)
{
if (innerValidator == null)
{
throw new ArgumentNullException("innerValidator");
}
_innerValidator = innerValidator;
}
public bool Validate(object model, Type type, ModelMetadataProvider metadataProvider, HttpActionContext actionContext, string keyPrefix)
{
// Remove the keyPrefix but otherwise let innerValidator do what it normally does.
return _innerValidator.Validate(model, type, metadataProvider, actionContext, string.Empty);
}
}
}

View File

@@ -0,0 +1,20 @@
using System;
using System.Web.Http;
using System.Web.Http.Controllers;
using System.Web.Http.Validation;
namespace Umbraco.Web.WebApi
{
/// <summary>
/// Applying this attribute to any webapi controller will ensure that it only contains one json formatter compatible with the angular json vulnerability prevention.
/// </summary>
internal class PrefixlessBodyModelValidatorAttribute : Attribute, IControllerConfiguration
{
public virtual void Initialize(HttpControllerSettings controllerSettings, HttpControllerDescriptor controllerDescriptor)
{
//replace the normal validator with our custom one for this controller
controllerSettings.Services.Replace(typeof(IBodyModelValidator),
new PrefixlessBodyModelValidator(controllerSettings.Services.GetBodyModelValidator()));
}
}
}