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

This commit is contained in:
Shannon
2013-10-10 13:41:06 +11:00
parent 0843388a41
commit 98832357bf
26 changed files with 393 additions and 196 deletions

View File

@@ -411,6 +411,11 @@ namespace Umbraco.Core
/// Alias for the XPath DropDownList datatype.
/// </summary>
public const string XPathDropDownListAlias = "Umbraco.XPathDropDownList";
/// <summary>
/// Alias for the email address property editor
/// </summary>
public const string EmailAddressAlias = "Umbraco.EmailAddress";
}
}
}

View File

@@ -0,0 +1,31 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Umbraco.Core.Models;
namespace Umbraco.Core.PropertyEditors
{
/// <summary>
/// A validator that validates an email address
/// </summary>
[ValueValidator("Email")]
internal sealed class EmailValidator : ManifestValueValidator, IPropertyValidator
{
public override IEnumerable<ValidationResult> 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<ValidationResult> Validate(object value, PreValueCollection preValues, PropertyEditor editor)
{
return Validate(value, null, preValues, editor);
}
}
}

View File

@@ -346,36 +346,7 @@ namespace Umbraco.Core.Services
Audit.Add(AuditTypes.Delete, string.Format("Delete ContentTypes performed by user"), userId, -1);
}
}
/// <summary>
/// Gets an <see cref="IMemberType"/> object by its Id
/// </summary>
/// <param name="id">Id of the <see cref="IMemberType"/> to retrieve</param>
/// <returns><see cref="IMemberType"/></returns>
public IMemberType GetMemberType(int id)
{
using (var repository = _repositoryFactory.CreateMemberTypeRepository(_uowProvider.GetUnitOfWork()))
{
return repository.Get(id);
}
}
/// <summary>
/// Gets an <see cref="IMemberType"/> object by its Alias
/// </summary>
/// <param name="alias">Alias of the <see cref="IMemberType"/> to retrieve</param>
/// <returns><see cref="IMemberType"/></returns>
public IMemberType GetMemberType(string alias)
{
using (var repository = _repositoryFactory.CreateMemberTypeRepository(_uowProvider.GetUnitOfWork()))
{
var query = Query<IMemberType>.Builder.Where(x => x.Alias == alias);
var contentTypes = repository.GetByQuery(query);
return contentTypes.FirstOrDefault();
}
}
/// <summary>
/// Gets an <see cref="IMediaType"/> object by its Id
/// </summary>

View File

@@ -66,21 +66,7 @@ namespace Umbraco.Core.Services
/// <remarks>Deleting a <see cref="IContentType"/> will delete all the <see cref="IContent"/> objects based on this <see cref="IContentType"/></remarks>
/// <param name="userId">Optional Id of the User deleting the ContentTypes</param>
void Delete(IEnumerable<IContentType> contentTypes, int userId = 0);
/// <summary>
/// Gets an <see cref="IMemberType"/> object by its Id
/// </summary>
/// <param name="id">Id of the <see cref="IMediaType"/> to retrieve</param>
/// <returns><see cref="IMediaType"/></returns>
IMemberType GetMemberType(int id);
/// <summary>
/// Gets an <see cref="IMemberType"/> object by its Alias
/// </summary>
/// <param name="alias">Alias of the <see cref="IMediaType"/> to retrieve</param>
/// <returns><see cref="IMediaType"/></returns>
IMemberType GetMemberType(string alias);
/// <summary>
/// Gets an <see cref="IMediaType"/> object by its Id
/// </summary>

View File

@@ -12,7 +12,18 @@ namespace Umbraco.Core.Services
/// <returns>An Enumerable list of <see cref="IContentType"/> objects</returns>
IEnumerable<IMemberType> GetAllMemberTypes(params int[] ids);
IMemberType GetMemberType(string alias);
/// <summary>
/// Gets an <see cref="IMemberType"/> object by its Id
/// </summary>
/// <param name="id">Id of the <see cref="IMediaType"/> to retrieve</param>
/// <returns><see cref="IMediaType"/></returns>
IMemberType GetMemberType(int id);
/// <summary>
/// Gets an <see cref="IMemberType"/> object by its Alias
/// </summary>
/// <param name="alias">Alias of the <see cref="IMediaType"/> to retrieve</param>
/// <returns><see cref="IMediaType"/></returns>
IMemberType GetMemberType(string alias);
}
}

View File

@@ -37,17 +37,11 @@ namespace Umbraco.Core.Services
}
}
public IMemberType GetMemberType(string alias)
{
using (var repository = _repositoryFactory.CreateMemberTypeRepository(_uowProvider.GetUnitOfWork()))
{
var query = Query<IMemberType>.Builder.Where(x => x.Alias == alias);
var memberTypes = repository.GetByQuery(query);
return memberTypes.FirstOrDefault();
}
}
/// <summary>
/// Gets an <see cref="IMemberType"/> object by its Id
/// </summary>
/// <param name="id">Id of the <see cref="IMemberType"/> to retrieve</param>
/// <returns><see cref="IMemberType"/></returns>
public IMemberType GetMemberType(int id)
{
using (var repository = _repositoryFactory.CreateMemberTypeRepository(_uowProvider.GetUnitOfWork()))
@@ -56,5 +50,21 @@ namespace Umbraco.Core.Services
}
}
/// <summary>
/// Gets an <see cref="IMemberType"/> object by its Alias
/// </summary>
/// <param name="alias">Alias of the <see cref="IMemberType"/> to retrieve</param>
/// <returns><see cref="IMemberType"/></returns>
public IMemberType GetMemberType(string alias)
{
using (var repository = _repositoryFactory.CreateMemberTypeRepository(_uowProvider.GetUnitOfWork()))
{
var query = Query<IMemberType>.Builder.Where(x => x.Alias == alias);
var contentTypes = repository.GetByQuery(query);
return contentTypes.FirstOrDefault();
}
}
}
}

View File

@@ -358,6 +358,7 @@
<Compile Include="Models\Membership\MembershipExtensions.cs" />
<Compile Include="Models\Membership\UmbracoMembershipMember.cs" />
<Compile Include="Models\Membership\UmbracoMembershipUser.cs" />
<Compile Include="Models\PagedResult.cs" />
<Compile Include="Models\PreValue.cs" />
<Compile Include="Models\MemberType.cs" />
<Compile Include="Models\PreValueCollection.cs" />
@@ -450,6 +451,7 @@
<Compile Include="Persistence\Repositories\TagsRepository.cs" />
<Compile Include="PropertyEditors\BackwardsCompatibleData.cs" />
<Compile Include="PropertyEditors\BackwardsCompatibleDataType.cs" />
<Compile Include="PropertyEditors\EmailValidator.cs" />
<Compile Include="PropertyEditors\IParameterEditor.cs" />
<Compile Include="PropertyEditors\LegacyParameterEditorAliasConverter.cs" />
<Compile Include="PropertyEditors\LegacyPropertyEditorIdToAliasConverter.cs" />

View File

@@ -12,6 +12,7 @@ function sectionsDirective($timeout, $window, navigationService, sectionResource
scope.maxSections = 7;
scope.overflowingSections = 0;
scope.sections = [];
function loadSections(){
sectionResource.getSections()

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,8 @@
<form novalidate name="contentForm"
ng-controller="Umbraco.Editors.DataType.EditController"
ng-show="loaded"
ng-submit="save()">
ng-submit="save()"
val-status-changed>
<umb-panel val-show-validation>
<umb-header>

View File

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

View File

@@ -0,0 +1,33 @@
<div ng-controller="Umbraco.PropertyEditors.ChangePasswordController" ng-switch="changing">
<div ng-switch-when="false">
<!--<a href="" ng-click="doChange()">Change password</a> -->
<div class="control-group warning">
<span class="help-block">Password changing or resetting is currently not supported</span>
</div>
</div>
<div ng-switch-when="true">
<div class="control-group">
<input type="text" name="password" ng-model="model.value" id="{{model.alias}}"
class="umb-editor umb-textstring textstring"
required
val-server
ng-minlength="{{model.config.minPasswordLength}}" />
<span class="help-inline" val-msg-for="password" val-toggle-msg="required">Required</span>
<span class="help-inline" val-msg-for="password" val-toggle-msg="minlength">Minimum {{model.config.minPasswordLength}} characters</span>
<span class="help-inline" val-msg-for="password" val-toggle-msg="valServer"></span>
</div>
<div class="control-group">
<input type="text" name="confirmpassword" ng-model="model.confirm" id="{{model.alias}}"
class="umb-editor umb-textstring textstring"
val-compare="password" />
<span class="help-inline" val-msg-for="confirmpassword" val-toggle-msg="valCompare">Passwords must match</span>
</div>
<!--<a href="" ng-click="cancelChange()" ng-show="hasPassword">Cancel</a> -->
</div>
</div>

View File

@@ -1,6 +0,0 @@
angular.module("umbraco")
.controller("Umbraco.PropertyEditors.EmailController",
function($rootScope, $scope, dialogService, $routeParams, contentResource, contentTypeResource, editorContextService, notificationsService) {
});

View File

@@ -1,3 +1,13 @@
<input type="email" name="textbox" ng-model="model.value" id="{{model.alias}}" class="umb-editor umb-textstring textstring" val-server="value" />
<span class="help-inline" val-msg-for="textbox" val-toggle-msg="email">Invalid email</span>
<span class="help-inline" val-msg-for="textbox" val-toggle-msg="valServer"></span>
<div>
<input type="email" name="textbox"
ng-model="model.value"
id="{{model.alias}}"
class="umb-editor umb-textstring textstring"
ng-required="model.config.IsRequired"
val-server="value" />
<span class="help-inline" val-msg-for="textbox" val-toggle-msg="required">Required</span>
<span class="help-inline" val-msg-for="textbox" val-toggle-msg="email">Invalid email</span>
<span class="help-inline" val-msg-for="textbox" val-toggle-msg="valServer"></span>
</div>

View File

@@ -1,2 +1,9 @@
<input type="text" name="textbox" ng-model="model.value" id="{{model.alias}}" class="umb-editor umb-textstring textstring" val-server="value" />
<span class="help-inline" val-msg-for="textbox" val-toggle-msg="valServer"></span>
<div>
<input type="text" name="textbox" ng-model="model.value" id="{{model.alias}}"
class="umb-editor umb-textstring textstring"
val-server="value"
ng-required="model.config.IsRequired" />
<span class="help-inline" val-msg-for="textbox" val-toggle-msg="required">Required</span>
<span class="help-inline" val-msg-for="textbox" val-toggle-msg="valServer"></span>
</div>

View File

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

View File

@@ -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
/// </summary>
/// <param name="contentTypeAlias"></param>
/// <param name="parentId"></param>
/// <returns></returns>
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<IMember, MemberDisplay>(emptyContent);
}
@@ -96,6 +99,7 @@ namespace Umbraco.Web.Editors
/// </summary>
/// <returns></returns>
[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<IMember, MemberDisplay>(contentItem.PersistedContent);
forDisplay.Errors = ModelState.ToErrorDictionary();
throw new HttpResponseException(Request.CreateValidationErrorResponse(forDisplay));
}
{
var forDisplay = Mapper.Map<IMember, MemberDisplay>(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);
}
}
/// <summary>
/// This validates the submitted data in regards to the current membership provider
/// </summary>
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<MembershipUser>().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<MembershipUser>().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;
}
}
}

View File

@@ -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);
//}
/// <summary>
/// Adds the error to model state correctly for a property so we can use it on the client side.
/// </summary>
/// <param name="modelState"></param>
/// <param name="result"></param>
/// <param name="propertyAlias"></param>
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<string, object> ToErrorDictionary(this System.Web.Http.ModelBinding.ModelStateDictionary modelState)
{
var modelStateError = new Dictionary<string, object>();

View File

@@ -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<IMember, ContentItemDto<IMember>>()
.ForMember(
dto => dto.Owner,
expression => expression.ResolveUsing<OwnerResolver<IMember>>());
expression => expression.ResolveUsing<OwnerResolver<IMember>>())
//do no map the custom member properties (currently anyways, they were never there in 6.x)
.ForMember(dto => dto.Properties, expression => expression.ResolveUsing<MemberDtoPropertiesValueResolver>());
}
/// <summary>
@@ -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<string, object> { { "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<string, object> {{"IsRequired", true}}
});
}
/// <summary>
/// This ensures that the custom membership provider properties are not mapped (currently since they weren't there in v6)
/// </summary>
/// <remarks>
/// 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
/// </remarks>
internal class MemberDtoPropertiesValueResolver : ValueResolver<IMember, IEnumerable<ContentPropertyDto>>
{
protected override IEnumerable<ContentPropertyDto> 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<Property, ContentPropertyDto>);
}
}
}
}

View File

@@ -1,63 +0,0 @@
using System;
using System.Collections.Generic;
using System.Runtime.Serialization;
namespace Umbraco.Web.Models
{
/// <summary>
/// Represents a paged result for a model collection
/// </summary>
/// <typeparam name="T"></typeparam>
[DataContract(Name = "pagedCollection", Namespace = "")]
public class PagedResult<T>
{
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<T> Items { get; set; }
/// <summary>
/// Calculates the skip size based on the paged parameters specified
/// </summary>
/// <remarks>
/// Returns 0 if the page number or page size is zero
/// </remarks>
internal int SkipSize
{
get
{
if (PageNumber > 0 && PageSize > 0)
{
return Convert.ToInt32((PageNumber - 1)*PageSize);
}
return 0;
}
}
}
}

View File

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

View File

@@ -327,6 +327,7 @@
<Compile Include="Models\Mapping\MacroModelMapper.cs" />
<Compile Include="Models\Mapping\MemberModelMapper.cs" />
<Compile Include="PropertyEditors\ColorListPreValueEditor.cs" />
<Compile Include="PropertyEditors\EmailAddressPropertyEditor.cs" />
<Compile Include="PropertyEditors\ListViewPropertyEditor.cs" />
<Compile Include="PropertyEditors\ParameterEditors\TextAreaParameterEditor.cs" />
<Compile Include="PropertyEditors\ParameterEditors\TextParameterEditor.cs" />
@@ -351,7 +352,6 @@
<Compile Include="Models\Mapping\DataTypeModelMapper.cs" />
<Compile Include="Models\Mapping\EntityModelMapper.cs" />
<Compile Include="Models\Mapping\PreValueDisplayResolver.cs" />
<Compile Include="Models\PagedResult.cs" />
<Compile Include="PropertyEditors\CheckBoxListPropertyEditor.cs" />
<Compile Include="PropertyEditors\ColorPickerPropertyEditor.cs" />
<Compile Include="PropertyEditors\DatePropertyEditor.cs" />

View File

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

View File

@@ -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;
}
/// <summary>
/// Adds the error to model state correctly for a property so we can use it on the client side.
/// </summary>
/// <param name="modelState"></param>
/// <param name="result"></param>
/// <param name="propertyAlias"></param>
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);
}
}
}
}
}