Adds RequiredForPersistenceAttribute to easily detect if any entity fails validation in a way that it cannot be persisted to the data store. Changes all tree events to not be jquery events and to not require a 'callback' jquery object, instead we just use angular events broadcast globally, much nicer.

This commit is contained in:
Shannon
2013-07-18 17:05:40 +10:00
parent 17eb3db336
commit d3fe37a080
14 changed files with 139 additions and 315 deletions

View File

@@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Web.ModelBinding;
namespace Umbraco.Core.Models.Validation
{
/// <summary>
/// A custom validation attribute which adds additional metadata to the property to indicate that
/// the value is required to be persisted.
/// </summary>
/// <remarks>
/// In Umbraco, we persist content even if it is invalid, however there are some properties that are absolutely required
/// in order to be persisted such as the Name of the content item. This attribute is re-usable to check for these types of
/// properties over any sort of model.
/// </remarks>
public class RequiredForPersistenceAttribute : RequiredAttribute
{
}
}

View File

@@ -251,6 +251,7 @@
<Compile Include="Models\UmbracoEntity.cs" />
<Compile Include="Models\UmbracoObjectTypes.cs" />
<Compile Include="Models\UmbracoObjectTypesExtensions.cs" />
<Compile Include="Models\Validation\RequiredForPersistenceAttribute.cs" />
<Compile Include="ObjectResolution\ApplicationEventsResolver.cs" />
<Compile Include="ObjectResolution\ResolverCollection.cs" />
<Compile Include="Persistence\Caching\InMemoryCacheProvider.cs" />

View File

@@ -4,7 +4,7 @@
* @restrict E
**/
angular.module("umbraco.directives")
.directive('umbTree', function ($compile, $log, $q, treeService, notificationsService, $timeout) {
.directive('umbTree', function ($compile, $log, $q, $rootScope, treeService, notificationsService, $timeout) {
return {
restrict: 'E',
@@ -15,8 +15,7 @@ angular.module("umbraco.directives")
section: '@',
showoptions: '@',
showheader: '@',
cachekey: '@',
callback: '='
cachekey: '@'
},
compile: function (element, attrs) {
@@ -34,7 +33,7 @@ angular.module("umbraco.directives")
'</div>';
}
template += '<ul>' +
'<umb-tree-item ng-repeat="child in tree.root.children" node="child" callback="callback" section="{{section}}" ng-animate="animation()"></umb-tree-item>' +
'<umb-tree-item ng-repeat="child in tree.root.children" node="child" section="{{section}}" ng-animate="animation()"></umb-tree-item>' +
'</ul>' +
'</li>' +
'</ul>';
@@ -56,9 +55,7 @@ angular.module("umbraco.directives")
/** Helper function to emit tree events */
function emitEvent(eventName, args) {
if (scope.callback) {
$(scope.callback).trigger(eventName, args);
}
$rootScope.$broadcast(eventName, args);
}
/** Method to load in the tree data */

View File

@@ -18,7 +18,7 @@
</example>
*/
angular.module("umbraco.directives")
.directive('umbTreeItem', function($compile, $http, $templateCache, $interpolate, $log, $location, treeService, notificationsService) {
.directive('umbTreeItem', function ($compile, $http, $templateCache, $interpolate, $log, $location, $rootScope, treeService, notificationsService) {
return {
restrict: 'E',
replace: true,
@@ -46,11 +46,9 @@ angular.module("umbraco.directives")
var enableDeleteAnimations = true;
/** Helper function to emit tree events */
function emitEvent(eventName, args){
if(scope.callback){
$(scope.callback).trigger(eventName,args);
}
function emitEvent(eventName, args) {
$rootScope.$broadcast(eventName, args);
}
/**

View File

@@ -20,10 +20,7 @@ angular.module('umbraco.services')
.factory('navigationService', function ($rootScope, $routeParams, $log, $location, $q, dialogService, treeService, notificationsService) {
//TODO: would be nicer to set all of the options here first instead of implicitly below!
var ui = {
//tree event handler everyone can subscribe to (TODO: but why not use angular events instead of jquery ?)
tree: $({})
};
var ui = {};
function setMode(mode) {
switch (mode) {

View File

@@ -3,7 +3,8 @@ angular.module("umbraco").controller("Umbraco.Dialogs.ContentPickerController",
function ($scope) {
$scope.treeCallback = $({});
$scope.treeCallback.bind("treeNodeSelect", function(event, args){
//$scope.treeCallback.bind("treeNodeSelect", function(event, args){
$scope.$on("treeNodeSelect", function (ev, args) {
args.event.preventDefault();
args.event.stopPropagation();

View File

@@ -33,19 +33,15 @@ function NavigationController($scope,$rootScope, $location, $log, navigationServ
});
//this reacts to the options item in the tree
navigationService.ui.tree.bind("treeOptionsClick", function (ev, args) {
ev.stopPropagation();
ev.preventDefault();
$scope.$on("treeOptionsClick", function (ev, args) {
$scope.currentNode = args.node;
args.scope = $scope;
navigationService.showMenu(ev, args);
});
//this reacts to tree items themselves being clicked
//the tree directive should not contain any handling, simply just bubble events
navigationService.ui.tree.bind("treeNodeSelect", function (ev, args) {
$scope.$on("treeNodeSelect", function (ev, args) {
var n = args.node;

View File

@@ -65,7 +65,7 @@
<!-- the tree -->
<div id="tree" class="span5 umb-scrollable umb-panel" auto-scale="0" ng-animate="'slide'">
<umb-tree callback="nav.ui.tree" section="{{nav.ui.currentSection}}" ></umb-tree>
<umb-tree section="{{nav.ui.currentSection}}" ></umb-tree>
</div>
<!-- The context menu -->

View File

@@ -71,6 +71,7 @@
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>AllRules.ruleset</CodeAnalysisRuleSet>
<Prefer32Bit>false</Prefer32Bit>
<IntermediateOutputPath>C:\Users\Shannon\AppData\Local\Temp\vs24C6.tmp\Debug\</IntermediateOutputPath>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<OutputPath>bin\</OutputPath>
@@ -96,6 +97,7 @@
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>AllRules.ruleset</CodeAnalysisRuleSet>
<Prefer32Bit>false</Prefer32Bit>
<IntermediateOutputPath>C:\Users\Shannon\AppData\Local\Temp\vs24C6.tmp\Release\</IntermediateOutputPath>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\UmbracoExamine\UmbracoExamine.csproj">

View File

@@ -149,7 +149,7 @@ namespace Umbraco.Web.Editors
// a message indicating this
if (!ModelState.IsValid)
{
if (ModelState["Name"] != null && ModelState["Name"].Errors.Any()
if (ValidationHelper.ModelHasRequiredForPersistenceErrors(contentItem)
&& (contentItem.Action == ContentSaveAction.SaveNew || contentItem.Action == ContentSaveAction.PublishNew))
{
//ok, so the absolute mandatory data is invalid and it's new, we cannot actually continue!
@@ -157,6 +157,7 @@ namespace Umbraco.Web.Editors
var forDisplay = _contentModelMapper.ToContentItemDisplay(contentItem.PersistedContent);
forDisplay.Errors = ModelState.ToErrorDictionary();
throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.Forbidden, forDisplay));
}
//if the model state is not valid we cannot publish so change it to save

View File

@@ -0,0 +1,31 @@
using System;
using System.ComponentModel;
using System.Linq;
using Umbraco.Core.Models.Validation;
namespace Umbraco.Web.Editors
{
internal class ValidationHelper
{
/// <summary>
/// This will check if any properties of the model are attributed with the RequiredForPersistenceAttribute attribute and if they are it will
/// check if that property validates, if it doesn't it means that the current model cannot be persisted because it doesn't have the necessary information
/// to be saved.
/// </summary>
/// <param name="model"></param>
/// <returns></returns>
/// <remarks>
/// This is normally used for things like content creating when the name is empty since we cannot actually create a content item when the name is empty.
/// This is similar but different from the standard Required validator since we still allow content to be saved when validation fails but there are some
/// content fields that are absolutely mandatory for creating/saving.
/// </remarks>
internal static bool ModelHasRequiredForPersistenceErrors(object model)
{
var requiredForPersistenceProperties = TypeDescriptor.GetProperties(model).Cast<PropertyDescriptor>()
.Where(x => x.Attributes.Cast<Attribute>().Any(a => a is RequiredForPersistenceAttribute));
var validator = new RequiredForPersistenceAttribute();
return requiredForPersistenceProperties.Any(p => !validator.IsValid(p.GetValue(model)));
}
}
}

View File

@@ -4,6 +4,7 @@ using System.ComponentModel.DataAnnotations;
using System.Runtime.Serialization;
using Newtonsoft.Json;
using Umbraco.Core.Models;
using Umbraco.Core.Models.Validation;
using Umbraco.Web.WebApi;
namespace Umbraco.Web.Models.ContentEditing
@@ -32,7 +33,7 @@ namespace Umbraco.Web.Models.ContentEditing
public int Id { get; set; }
[DataMember(Name = "name", IsRequired = true)]
[Required(AllowEmptyStrings = false, ErrorMessage = "Required")]
[RequiredForPersistence(AllowEmptyStrings = false, ErrorMessage = "Required")]
public string Name { get; set; }
[DataMember(Name = "properties")]

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,14 @@
using System;
using System.ComponentModel;
using System.Linq;
using System.Web;
using System.Web.Http;
using System.Web.Http.ModelBinding;
using Umbraco.Core;
using Umbraco.Core.Models;
using Umbraco.Core.Models.Validation;
using Umbraco.Core.Services;
using Umbraco.Web.Models.ContentEditing;
namespace Umbraco.Web.WebApi
{