From 98832357bfa6f040dbf0267b2fb4f637aa38b760 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 10 Oct 2013 13:41:06 +1100 Subject: [PATCH] Lots of work on the member editor - creates new email address prop editor, allows text prop editor to be required based on config, fixes the section directive bug, creating change password prop ed, streamlines more of the services layer to ensure that the things that need to be public are public --- src/Umbraco.Core/Constants-PropertyEditors.cs | 5 + .../PropertyEditors/EmailValidator.cs | 31 +++++ .../Services/ContentTypeService.cs | 31 +---- .../Services/IContentTypeService.cs | 16 +-- .../Services/IMemberTypeService.cs | 13 ++- .../Services/MemberTypeService.cs | 32 +++-- src/Umbraco.Core/Umbraco.Core.csproj | 2 + .../directives/umbsections.directive.js | 1 + .../validation/valcompare.directive.js | 6 +- .../validation/valregex.directive.js | 23 +++- .../src/common/services/util.service.js | 11 +- .../src/views/datatype/edit.html | 3 +- .../changepassword.controller.js | 40 +++++++ .../changepassword/changepassword.html | 33 ++++++ .../propertyeditors/email/email.controller.js | 6 - .../views/propertyeditors/email/email.html | 16 ++- .../propertyeditors/textbox/textbox.html | 11 +- src/Umbraco.Web/Editors/ContentController.cs | 2 +- src/Umbraco.Web/Editors/MemberController.cs | 109 ++++++++++++++---- src/Umbraco.Web/ModelStateExtensions.cs | 27 +++++ .../Models/Mapping/MemberModelMapper.cs | 30 ++++- src/Umbraco.Web/Models/PagedResult.cs | 63 ---------- .../EmailAddressPropertyEditor.cs | 29 +++++ src/Umbraco.Web/Umbraco.Web.csproj | 2 +- .../WebApi/Binders/MemberBinder.cs | 15 ++- .../Filters/ContentItemValidationHelper.cs | 32 +---- 26 files changed, 393 insertions(+), 196 deletions(-) create mode 100644 src/Umbraco.Core/PropertyEditors/EmailValidator.cs create mode 100644 src/Umbraco.Web.UI.Client/src/views/propertyeditors/changepassword/changepassword.controller.js create mode 100644 src/Umbraco.Web.UI.Client/src/views/propertyeditors/changepassword/changepassword.html delete mode 100644 src/Umbraco.Web.UI.Client/src/views/propertyeditors/email/email.controller.js delete mode 100644 src/Umbraco.Web/Models/PagedResult.cs create mode 100644 src/Umbraco.Web/PropertyEditors/EmailAddressPropertyEditor.cs diff --git a/src/Umbraco.Core/Constants-PropertyEditors.cs b/src/Umbraco.Core/Constants-PropertyEditors.cs index 3c2a8d6752..e423f58c62 100644 --- a/src/Umbraco.Core/Constants-PropertyEditors.cs +++ b/src/Umbraco.Core/Constants-PropertyEditors.cs @@ -411,6 +411,11 @@ namespace Umbraco.Core /// Alias for the XPath DropDownList datatype. /// public const string XPathDropDownListAlias = "Umbraco.XPathDropDownList"; + + /// + /// Alias for the email address property editor + /// + public const string EmailAddressAlias = "Umbraco.EmailAddress"; } } } \ No newline at end of file diff --git a/src/Umbraco.Core/PropertyEditors/EmailValidator.cs b/src/Umbraco.Core/PropertyEditors/EmailValidator.cs new file mode 100644 index 0000000000..0fb6a227be --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/EmailValidator.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using Umbraco.Core.Models; + +namespace Umbraco.Core.PropertyEditors +{ + /// + /// A validator that validates an email address + /// + [ValueValidator("Email")] + internal sealed class EmailValidator : ManifestValueValidator, IPropertyValidator + { + public override IEnumerable Validate(object value, string config, PreValueCollection preValues, PropertyEditor editor) + { + var asString = value.ToString(); + + var emailVal = new EmailAddressAttribute(); + + if (emailVal.IsValid(asString) == false) + { + //TODO: localize these! + yield return new ValidationResult("Email is invalid", new[] { "value" }); + } + } + + public IEnumerable Validate(object value, PreValueCollection preValues, PropertyEditor editor) + { + return Validate(value, null, preValues, editor); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Services/ContentTypeService.cs b/src/Umbraco.Core/Services/ContentTypeService.cs index afd7da9efe..a2f27f58a2 100644 --- a/src/Umbraco.Core/Services/ContentTypeService.cs +++ b/src/Umbraco.Core/Services/ContentTypeService.cs @@ -346,36 +346,7 @@ namespace Umbraco.Core.Services Audit.Add(AuditTypes.Delete, string.Format("Delete ContentTypes performed by user"), userId, -1); } } - - /// - /// Gets an object by its Id - /// - /// Id of the to retrieve - /// - public IMemberType GetMemberType(int id) - { - using (var repository = _repositoryFactory.CreateMemberTypeRepository(_uowProvider.GetUnitOfWork())) - { - return repository.Get(id); - } - } - - /// - /// Gets an object by its Alias - /// - /// Alias of the to retrieve - /// - public IMemberType GetMemberType(string alias) - { - using (var repository = _repositoryFactory.CreateMemberTypeRepository(_uowProvider.GetUnitOfWork())) - { - var query = Query.Builder.Where(x => x.Alias == alias); - var contentTypes = repository.GetByQuery(query); - - return contentTypes.FirstOrDefault(); - } - } - + /// /// Gets an object by its Id /// diff --git a/src/Umbraco.Core/Services/IContentTypeService.cs b/src/Umbraco.Core/Services/IContentTypeService.cs index 9f596e465e..9e7c8d53f9 100644 --- a/src/Umbraco.Core/Services/IContentTypeService.cs +++ b/src/Umbraco.Core/Services/IContentTypeService.cs @@ -66,21 +66,7 @@ namespace Umbraco.Core.Services /// Deleting a will delete all the objects based on this /// Optional Id of the User deleting the ContentTypes void Delete(IEnumerable contentTypes, int userId = 0); - - /// - /// Gets an object by its Id - /// - /// Id of the to retrieve - /// - IMemberType GetMemberType(int id); - - /// - /// Gets an object by its Alias - /// - /// Alias of the to retrieve - /// - IMemberType GetMemberType(string alias); - + /// /// Gets an object by its Id /// diff --git a/src/Umbraco.Core/Services/IMemberTypeService.cs b/src/Umbraco.Core/Services/IMemberTypeService.cs index 9c45d83e68..7857e1dda0 100644 --- a/src/Umbraco.Core/Services/IMemberTypeService.cs +++ b/src/Umbraco.Core/Services/IMemberTypeService.cs @@ -12,7 +12,18 @@ namespace Umbraco.Core.Services /// An Enumerable list of objects IEnumerable GetAllMemberTypes(params int[] ids); - IMemberType GetMemberType(string alias); + /// + /// Gets an object by its Id + /// + /// Id of the to retrieve + /// IMemberType GetMemberType(int id); + + /// + /// Gets an object by its Alias + /// + /// Alias of the to retrieve + /// + IMemberType GetMemberType(string alias); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Services/MemberTypeService.cs b/src/Umbraco.Core/Services/MemberTypeService.cs index cc5eada678..81a231f7c5 100644 --- a/src/Umbraco.Core/Services/MemberTypeService.cs +++ b/src/Umbraco.Core/Services/MemberTypeService.cs @@ -37,17 +37,11 @@ namespace Umbraco.Core.Services } } - public IMemberType GetMemberType(string alias) - { - using (var repository = _repositoryFactory.CreateMemberTypeRepository(_uowProvider.GetUnitOfWork())) - { - var query = Query.Builder.Where(x => x.Alias == alias); - var memberTypes = repository.GetByQuery(query); - - return memberTypes.FirstOrDefault(); - } - } - + /// + /// Gets an object by its Id + /// + /// Id of the to retrieve + /// public IMemberType GetMemberType(int id) { using (var repository = _repositoryFactory.CreateMemberTypeRepository(_uowProvider.GetUnitOfWork())) @@ -56,5 +50,21 @@ namespace Umbraco.Core.Services } } + /// + /// Gets an object by its Alias + /// + /// Alias of the to retrieve + /// + public IMemberType GetMemberType(string alias) + { + using (var repository = _repositoryFactory.CreateMemberTypeRepository(_uowProvider.GetUnitOfWork())) + { + var query = Query.Builder.Where(x => x.Alias == alias); + var contentTypes = repository.GetByQuery(query); + + return contentTypes.FirstOrDefault(); + } + } + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index e59e6d7303..731f7a0a84 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -358,6 +358,7 @@ + @@ -450,6 +451,7 @@ + diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/umbsections.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/umbsections.directive.js index 4df4b686c8..f9c83d2d52 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/umbsections.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/umbsections.directive.js @@ -12,6 +12,7 @@ function sectionsDirective($timeout, $window, navigationService, sectionResource scope.maxSections = 7; scope.overflowingSections = 0; + scope.sections = []; function loadSections(){ sectionResource.getSections() diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valcompare.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valcompare.directive.js index 5e7f042825..1a36dcc24f 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valcompare.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valcompare.directive.js @@ -1,8 +1,10 @@ angular.module('umbraco.directives.validation') .directive('valCompare',function () { return { - require: "ngModel", - link: function(scope, elem, attrs, ctrl) { + require: "ngModel", + link: function (scope, elem, attrs, ctrl) { + + //TODO: Pretty sure this should be done using a requires ^form in the directive declaration var otherInput = elem.inheritedData("$formController")[attrs.valCompare]; ctrl.$parsers.push(function(value) { diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valregex.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valregex.directive.js index bf37d73392..d1103cdbc3 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valregex.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valregex.directive.js @@ -6,17 +6,36 @@ * NOTE: there's already an ng-pattern but this requires that a regex expression is set, not a regex string **/ function valRegex() { + return { require: 'ngModel', restrict: "A", link: function (scope, elm, attrs, ctrl) { + var flags = ""; + if (attrs.valRegexFlags) { + try { + flags = scope.$eval(attrs.valRegexFlags); + if (!flags) { + flags = attrs.valRegexFlags; + } + } + catch (e) { + flags = attrs.valRegexFlags; + } + } var regex; try { - regex = new RegExp(scope.$eval(attrs.valRegex)); + var resolved = scope.$eval(attrs.valRegex); + if (resolved) { + regex = new RegExp(resolved, flags); + } + else { + regex = new RegExp(attrs.valRegex, flags); + } } catch(e) { - regex = new RegExp(attrs.valRegex); + regex = new RegExp(attrs.valRegex, flags); } var patternValidator = function (viewValue) { diff --git a/src/Umbraco.Web.UI.Client/src/common/services/util.service.js b/src/Umbraco.Web.UI.Client/src/common/services/util.service.js index 1deb464037..a66c3b549e 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/util.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/util.service.js @@ -203,7 +203,7 @@ function umbDataFormatter() { /** formats the display model used to display the member to the model used to save the member */ formatMemberPostData: function(displayModel, action) { - //this is basically the same as for media but we need to explicitly add the username,email to the save model + //this is basically the same as for media but we need to explicitly add the username,email, password to the save model var saveModel = this.formatMediaPostData(displayModel, action); var genericTab = _.find(displayModel.tabs, function (item) { @@ -216,8 +216,17 @@ function umbDataFormatter() { var propEmail = _.find(genericTab.properties, function (item) { return item.alias === "_umb_email"; }); + var propPass = _.find(genericTab.properties, function (item) { + return item.alias === "_umb_password"; + }); saveModel.email = propEmail.value; saveModel.username = propLogin.value; + //NOTE: This would only be set for new members! + if (angular.isString(propPass.value)) { + // if we are resetting or changing passwords then that data will come from the property editor and + // it's value will be an object not just a string. + saveModel.password = propPass.value; + } return saveModel; }, diff --git a/src/Umbraco.Web.UI.Client/src/views/datatype/edit.html b/src/Umbraco.Web.UI.Client/src/views/datatype/edit.html index 4a10a5efe1..45a86c7a76 100644 --- a/src/Umbraco.Web.UI.Client/src/views/datatype/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/datatype/edit.html @@ -1,7 +1,8 @@
+ ng-submit="save()" + val-status-changed> diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/changepassword/changepassword.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/changepassword/changepassword.controller.js new file mode 100644 index 0000000000..9f60d300cd --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/changepassword/changepassword.controller.js @@ -0,0 +1,40 @@ +angular.module("umbraco").controller("Umbraco.PropertyEditors.ChangePasswordController", + function($scope) { + + //the model config will contain an object, if it does not we'll create defaults + //NOTE: We will not support doing the password regex on the client side because the regex on the server side + //based on the membership provider cannot always be ported to js from .net directly. + /* + { + requiresQuestionAnswer: true/false, + enableReset: true/false, + minPasswordLength: 10 + } + */ + + //set defaults if they are not available + if (!$scope.model.config || !$scope.model.config.requiresQuestionAnswer) { + $scope.model.config.requiresQuestionAnswer = false; + } + if (!$scope.model.config || !$scope.model.config.enableReset) { + $scope.model.config.enableReset = true; + } + if (!$scope.model.config || !$scope.model.config.minPasswordLength) { + $scope.model.config.minPasswordLength = 7; + } + + + $scope.confirm = ""; + + $scope.hasPassword = $scope.model.value !== undefined && $scope.model.value !== null && $scope.model.value !== ""; + + $scope.changing = !$scope.hasPassword; + + $scope.doChange = function() { + $scope.changing = true; + }; + + $scope.cancelChange = function() { + $scope.changing = false; + }; + }); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/changepassword/changepassword.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/changepassword/changepassword.html new file mode 100644 index 0000000000..c624a81cc2 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/changepassword/changepassword.html @@ -0,0 +1,33 @@ +
+
+ +
+ Password changing or resetting is currently not supported +
+
+
+
+ + + + Required + Minimum {{model.config.minPasswordLength}} characters + + +
+
+ + + + Passwords must match + +
+ +
+
\ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/email/email.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/email/email.controller.js deleted file mode 100644 index 3c44b970b8..0000000000 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/email/email.controller.js +++ /dev/null @@ -1,6 +0,0 @@ -angular.module("umbraco") - .controller("Umbraco.PropertyEditors.EmailController", - function($rootScope, $scope, dialogService, $routeParams, contentResource, contentTypeResource, editorContextService, notificationsService) { - - - }); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/email/email.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/email/email.html index c33db4d337..635717be99 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/email/email.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/email/email.html @@ -1,3 +1,13 @@ - -Invalid email - \ No newline at end of file +
+ + + + Required + Invalid email + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.html index b17c59dea7..377f8e3db7 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.html @@ -1,2 +1,9 @@ - - \ No newline at end of file +
+ + + Required + +
diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index 2875eb446d..83fcc44aba 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -255,7 +255,7 @@ namespace Umbraco.Web.Editors else { //publish the item and check if it worked, if not we will show a diff msg below - publishStatus = ((ContentService)Services.ContentService).SaveAndPublishInternal(contentItem.PersistedContent, (int)Security.CurrentUser.Id); + publishStatus = Services.ContentService.SaveAndPublishWithStatus(contentItem.PersistedContent, (int)Security.CurrentUser.Id); } diff --git a/src/Umbraco.Web/Editors/MemberController.cs b/src/Umbraco.Web/Editors/MemberController.cs index ae30a4d618..65f45677c9 100644 --- a/src/Umbraco.Web/Editors/MemberController.cs +++ b/src/Umbraco.Web/Editors/MemberController.cs @@ -1,15 +1,19 @@ using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Linq; using System.Net; using System.Net.Http; using System.Text; using System.Threading.Tasks; using System.Web.Http; +using System.Web.Http.Controllers; +using System.Web.Http.Filters; using System.Web.Http.ModelBinding; using AutoMapper; using Examine.LuceneEngine.SearchCriteria; using Examine.SearchCriteria; +using Umbraco.Core; using Umbraco.Core.Models; using Umbraco.Web.WebApi; using Umbraco.Web.Models.ContentEditing; @@ -77,9 +81,8 @@ namespace Umbraco.Web.Editors /// Gets an empty content item for the /// /// - /// /// - public MemberDisplay GetEmpty(string contentTypeAlias, string username, string password) + public MemberDisplay GetEmpty(string contentTypeAlias) { var contentType = Services.MemberTypeService.GetMemberType(contentTypeAlias); if (contentType == null) @@ -87,7 +90,7 @@ namespace Umbraco.Web.Editors throw new HttpResponseException(HttpStatusCode.NotFound); } - var emptyContent = new Core.Models.Member("", "", "", "", -1, contentType); + var emptyContent = new Core.Models.Member("", contentType); return Mapper.Map(emptyContent); } @@ -96,6 +99,7 @@ namespace Umbraco.Web.Editors /// /// [FileUploadCleanupFilter] + [MembershipProviderValidationFilter] public MemberDisplay PostSave( [ModelBinder(typeof(MemberBinder))] MemberSave contentItem) @@ -109,30 +113,18 @@ namespace Umbraco.Web.Editors UpdateName(contentItem); - //map the custom properties + //map the custom properties - this will already be set for new entities in our member binder contentItem.PersistedContent.Email = contentItem.Email; - //TODO: If we allow changing the alias then we'll need to change URLs, etc... in the editor, would prefer to use - // a unique id but then need to figure out how to handle that with custom membership providers - waiting on feedback from morten. - + contentItem.PersistedContent.Username = contentItem.Username; + MapPropertyValues(contentItem); - //We need to manually check the validation results here because: - // * We still need to save the entity even if there are validation value errors - // * Depending on if the entity is new, and if there are non property validation errors (i.e. the name is null) - // then we cannot continue saving, we can only display errors - // * If there are validation errors and they were attempting to publish, we can only save, NOT publish and display - // a message indicating this + //Unlike content/media - if there are errors for a member, we do NOT proceed to save them, we cannot so return the errors if (!ModelState.IsValid) - { - if (ValidationHelper.ModelHasRequiredForPersistenceErrors(contentItem) - && (contentItem.Action == ContentSaveAction.SaveNew)) - { - //ok, so the absolute mandatory data is invalid and it's new, we cannot actually continue! - // add the modelstate to the outgoing object and throw validation response - var forDisplay = Mapper.Map(contentItem.PersistedContent); - forDisplay.Errors = ModelState.ToErrorDictionary(); - throw new HttpResponseException(Request.CreateValidationErrorResponse(forDisplay)); - } + { + var forDisplay = Mapper.Map(contentItem.PersistedContent); + forDisplay.Errors = ModelState.ToErrorDictionary(); + throw new HttpResponseException(Request.CreateValidationErrorResponse(forDisplay)); } //save the item @@ -175,4 +167,75 @@ namespace Umbraco.Web.Editors return Request.CreateResponse(HttpStatusCode.OK); } } + + /// + /// This validates the submitted data in regards to the current membership provider + /// + internal class MembershipProviderValidationFilterAttribute : ActionFilterAttribute + { + public override void OnActionExecuting(HttpActionContext actionContext) + { + base.OnActionExecuting(actionContext); + + var membershipProvider = Membership.Providers[Constants.Conventions.Member.UmbracoMemberProviderName]; + if (membershipProvider == null) + { + throw new InvalidOperationException("No membership provider found with name " + Constants.Conventions.Member.UmbracoMemberProviderName); + } + + var contentItem = (MemberSave) actionContext.ActionArguments["contentItem"]; + + var validEmail = ValidateUniqueEmail(contentItem, membershipProvider, actionContext); + if (validEmail == false) + { + actionContext.ModelState.AddPropertyError(new ValidationResult("Email address is already in use"), "umb_email"); + } + } + + internal bool ValidateUniqueEmail(MemberSave contentItem, MembershipProvider membershipProvider, HttpActionContext actionContext) + { + if (contentItem == null) throw new ArgumentNullException("contentItem"); + if (membershipProvider == null) throw new ArgumentNullException("membershipProvider"); + + if (membershipProvider.RequiresUniqueEmail == false) + { + return true; + } + + int totalRecs; + var existingByEmail = membershipProvider.FindUsersByEmail(contentItem.Email.Trim(), 0, int.MaxValue, out totalRecs); + switch (contentItem.Action) + { + case ContentSaveAction.Save: + //ok, we're updating the member, we need to check if they are changing their email and if so, does it exist already ? + if (contentItem.PersistedContent.Email.InvariantEquals(contentItem.Email.Trim()) == false) + { + //they are changing their email + if (existingByEmail.Cast().Select(x => x.Email) + .Any(x => x.InvariantEquals(contentItem.Email.Trim()))) + { + //the user cannot use this email + return false; + } + } + break; + case ContentSaveAction.SaveNew: + //check if the user's email already exists + if (existingByEmail.Cast().Select(x => x.Email) + .Any(x => x.InvariantEquals(contentItem.Email.Trim()))) + { + //the user cannot use this email + return false; + } + break; + case ContentSaveAction.Publish: + case ContentSaveAction.PublishNew: + default: + //we don't support this for members + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + return true; + } + } } diff --git a/src/Umbraco.Web/ModelStateExtensions.cs b/src/Umbraco.Web/ModelStateExtensions.cs index 964b7712b4..b656f87e5a 100644 --- a/src/Umbraco.Web/ModelStateExtensions.cs +++ b/src/Umbraco.Web/ModelStateExtensions.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Linq; using System.Web.Mvc; @@ -48,6 +49,32 @@ namespace Umbraco.Web // state.AddModelError("DataValidation", errorMessage); //} + /// + /// Adds the error to model state correctly for a property so we can use it on the client side. + /// + /// + /// + /// + internal static void AddPropertyError(this System.Web.Http.ModelBinding.ModelStateDictionary modelState, ValidationResult result, string propertyAlias) + { + //if there are no member names supplied then we assume that the validation message is for the overall property + // not a sub field on the property editor + if (!result.MemberNames.Any()) + { + //add a model state error for the entire property + modelState.AddModelError(string.Format("{0}.{1}", "Properties", propertyAlias), result.ErrorMessage); + } + else + { + //there's assigned field names so we'll combine the field name with the property name + // 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); + } + } + } + public static IDictionary ToErrorDictionary(this System.Web.Http.ModelBinding.ModelStateDictionary modelState) { var modelStateError = new Dictionary(); diff --git a/src/Umbraco.Web/Models/Mapping/MemberModelMapper.cs b/src/Umbraco.Web/Models/Mapping/MemberModelMapper.cs index 001296792f..37690321c9 100644 --- a/src/Umbraco.Web/Models/Mapping/MemberModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/MemberModelMapper.cs @@ -1,4 +1,5 @@ -using AutoMapper; +using System.Collections.Generic; +using AutoMapper; using Umbraco.Core; using Umbraco.Core.Models; using Umbraco.Core.Models.Mapping; @@ -53,7 +54,9 @@ namespace Umbraco.Web.Models.Mapping config.CreateMap>() .ForMember( dto => dto.Owner, - expression => expression.ResolveUsing>()); + expression => expression.ResolveUsing>()) + //do no map the custom member properties (currently anyways, they were never there in 6.x) + .ForMember(dto => dto.Properties, expression => expression.ResolveUsing()); } /// @@ -70,7 +73,8 @@ namespace Umbraco.Web.Models.Mapping Alias = string.Format("{0}login", Constants.PropertyEditors.InternalGenericPropertiesPrefix), Label = ui.Text("login"), Value = display.Username, - View = "textbox" + View = "textbox", + Config = new Dictionary { { "IsRequired", true } } }, new ContentPropertyDisplay { @@ -84,10 +88,28 @@ namespace Umbraco.Web.Models.Mapping Alias = string.Format("{0}email", Constants.PropertyEditors.InternalGenericPropertiesPrefix), Label = ui.Text("general", "email"), Value = display.Email, - View = "textbox" + View = "email", + Config = new Dictionary {{"IsRequired", true}} }); } + /// + /// This ensures that the custom membership provider properties are not mapped (currently since they weren't there in v6) + /// + /// + /// Because these properties don't exist on the form, if we don't remove them for this map we'll get validation errors when posting data + /// + internal class MemberDtoPropertiesValueResolver : ValueResolver> + { + protected override IEnumerable ResolveCore(IMember source) + { + var exclude = Constants.Conventions.Member.StandardPropertyTypeStubs.Select(x => x.Value.Alias).ToArray(); + return source.Properties + .Where(x => exclude.Contains(x.Alias) == false) + .Select(Mapper.Map); + } + } + } } \ No newline at end of file diff --git a/src/Umbraco.Web/Models/PagedResult.cs b/src/Umbraco.Web/Models/PagedResult.cs deleted file mode 100644 index 6c2c764c6e..0000000000 --- a/src/Umbraco.Web/Models/PagedResult.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Runtime.Serialization; - -namespace Umbraco.Web.Models -{ - /// - /// Represents a paged result for a model collection - /// - /// - [DataContract(Name = "pagedCollection", Namespace = "")] - public class PagedResult - { - public PagedResult(long totalItems, long pageNumber, long pageSize) - { - TotalItems = totalItems; - PageNumber = pageNumber; - PageSize = pageSize; - - if (pageSize > 0) - { - TotalPages = (long) Math.Ceiling(totalItems/(Decimal) pageSize); - } - else - { - TotalPages = 1; - } - } - - [DataMember(Name = "pageNumber")] - public long PageNumber { get; private set; } - - [DataMember(Name = "pageSize")] - public long PageSize { get; private set; } - - [DataMember(Name = "totalPages")] - public long TotalPages { get; private set; } - - [DataMember(Name = "totalItems")] - public long TotalItems { get; private set; } - - [DataMember(Name = "items")] - public IEnumerable Items { get; set; } - - /// - /// Calculates the skip size based on the paged parameters specified - /// - /// - /// Returns 0 if the page number or page size is zero - /// - internal int SkipSize - { - get - { - if (PageNumber > 0 && PageSize > 0) - { - return Convert.ToInt32((PageNumber - 1)*PageSize); - } - return 0; - } - } - } -} \ No newline at end of file diff --git a/src/Umbraco.Web/PropertyEditors/EmailAddressPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/EmailAddressPropertyEditor.cs new file mode 100644 index 0000000000..565928435a --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/EmailAddressPropertyEditor.cs @@ -0,0 +1,29 @@ +using Umbraco.Core; +using Umbraco.Core.PropertyEditors; + +namespace Umbraco.Web.PropertyEditors +{ + [PropertyEditor(Constants.PropertyEditors.EmailAddressAlias, "Email address", "email")] + public class EmailAddressPropertyEditor : PropertyEditor + { + protected override PropertyValueEditor CreateValueEditor() + { + var editor = base.CreateValueEditor(); + //add an email address validator + editor.Validators.Add(new EmailValidator()); + return editor; + } + + protected override PreValueEditor CreatePreValueEditor() + { + return new EmailAddressePreValueEditor(); + } + + internal class EmailAddressePreValueEditor : PreValueEditor + { + [PreValueField("Required?", "boolean")] + public bool IsRequired { get; set; } + } + + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index dc7ace1be7..9b989fb336 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -327,6 +327,7 @@ + @@ -351,7 +352,6 @@ - diff --git a/src/Umbraco.Web/WebApi/Binders/MemberBinder.cs b/src/Umbraco.Web/WebApi/Binders/MemberBinder.cs index 0051bf94d8..aabfdcb7b9 100644 --- a/src/Umbraco.Web/WebApi/Binders/MemberBinder.cs +++ b/src/Umbraco.Web/WebApi/Binders/MemberBinder.cs @@ -25,7 +25,8 @@ namespace Umbraco.Web.WebApi.Binders protected override IMember GetExisting(MemberSave model) { - //TODO: We're going to remove the built-in member properties from this editor - not sure if we should be persisting them elsewhere ? + //TODO: We're going to remove the built-in member properties from this editor - We didn't support these in 6.x so + // pretty hard to support them in 7 when the member type editor is using the old APIs! var toRemove = Constants.Conventions.Member.StandardPropertyTypeStubs.Select(x => x.Value.Alias).ToArray(); var member = ApplicationContext.Services.MemberService.GetByUsername(model.Username); @@ -38,11 +39,21 @@ namespace Umbraco.Web.WebApi.Binders protected override IMember CreateNew(MemberSave model) { - var contentType = ApplicationContext.Services.ContentTypeService.GetMemberType(model.ContentTypeAlias); + var contentType = ApplicationContext.Services.MemberTypeService.GetMemberType(model.ContentTypeAlias); if (contentType == null) { throw new InvalidOperationException("No member type found wth alias " + model.ContentTypeAlias); } + + //TODO: We're going to remove the built-in member properties from this editor - We didn't support these in 6.x so + // pretty hard to support them in 7 when the member type editor is using the old APIs! + var toRemove = Constants.Conventions.Member.StandardPropertyTypeStubs.Select(x => x.Value.Alias).ToArray(); + foreach (var remove in toRemove) + { + contentType.RemovePropertyType(remove); + } + + //return the new member with the details filled in return new Member(model.Name, model.Email, model.Username, model.Password, -1, contentType); } diff --git a/src/Umbraco.Web/WebApi/Filters/ContentItemValidationHelper.cs b/src/Umbraco.Web/WebApi/Filters/ContentItemValidationHelper.cs index a68eb2cbbf..7875bfb076 100644 --- a/src/Umbraco.Web/WebApi/Filters/ContentItemValidationHelper.cs +++ b/src/Umbraco.Web/WebApi/Filters/ContentItemValidationHelper.cs @@ -139,7 +139,7 @@ namespace Umbraco.Web.WebApi.Filters foreach (var result in editor.ValueEditor.Validators.SelectMany(v => v.Validate(postedValue, preValues, editor))) { - AddError(actionContext.ModelState, result, p.Alias); + actionContext.ModelState.AddPropertyError(result, p.Alias); } //Now we need to validate the property based on the PropertyType validation (i.e. regex and required) @@ -148,7 +148,7 @@ namespace Umbraco.Web.WebApi.Filters { foreach (var result in p.PropertyEditor.ValueEditor.RequiredValidator.Validate(postedValue, "", preValues, editor)) { - AddError(actionContext.ModelState, result, p.Alias); + actionContext.ModelState.AddPropertyError(result, p.Alias); } } @@ -156,7 +156,7 @@ namespace Umbraco.Web.WebApi.Filters { foreach (var result in p.PropertyEditor.ValueEditor.RegexValidator.Validate(postedValue, p.ValidationRegExp, preValues, editor)) { - AddError(actionContext.ModelState, result, p.Alias); + actionContext.ModelState.AddPropertyError(result, p.Alias); } } } @@ -164,30 +164,6 @@ namespace Umbraco.Web.WebApi.Filters return actionContext.ModelState.IsValid; } - /// - /// Adds the error to model state correctly for a property so we can use it on the client side. - /// - /// - /// - /// - private void AddError(ModelStateDictionary modelState, ValidationResult result, string propertyAlias) - { - //if there are no member names supplied then we assume that the validation message is for the overall property - // not a sub field on the property editor - if (!result.MemberNames.Any()) - { - //add a model state error for the entire property - modelState.AddModelError(string.Format("{0}.{1}", "Properties", propertyAlias), result.ErrorMessage); - } - else - { - //there's assigned field names so we'll combine the field name with the property name - // 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); - } - } - } + } } \ No newline at end of file