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