From a16ac1f0bab43ddc59d1f027c08c9472a208d62e Mon Sep 17 00:00:00 2001 From: Stephan Date: Thu, 25 Jan 2018 16:25:20 +0100 Subject: [PATCH 1/9] U4-10794 - implement basic consent service --- src/Umbraco.Core/Models/Consent.cs | 73 +++++++ src/Umbraco.Core/Models/ConsentState.cs | 38 ++++ src/Umbraco.Core/Models/IConsent.cs | 37 ++++ src/Umbraco.Core/Models/Rdbms/ConsentDto.cs | 42 ++++ .../Persistence/Factories/ConsentFactory.cs | 41 ++++ .../Persistence/Mappers/ConsentMapper.cs | 39 ++++ .../Persistence/Mappers/PetaPocoMapper.cs | 12 +- .../Initial/DatabaseSchemaCreation.cs | 5 +- .../Repositories/ConsentRepository.cs | 122 +++++++++++ .../Repositories/IConsentRepository.cs | 10 + .../Persistence/RepositoryFactory.cs | 5 + src/Umbraco.Core/Services/ConsentService.cs | 97 +++++++++ src/Umbraco.Core/Services/IConsentService.cs | 49 +++++ src/Umbraco.Core/Services/ServiceContext.cs | 41 ++-- src/Umbraco.Core/Umbraco.Core.csproj | 11 + .../Umbraco.Core.csproj.DotSettings | 2 - .../Services/ConsentServiceTests.cs | 197 ++++++++++++++++++ src/Umbraco.Tests/Umbraco.Tests.csproj | 1 + src/umbraco.sln.DotSettings | 2 +- 19 files changed, 790 insertions(+), 34 deletions(-) create mode 100644 src/Umbraco.Core/Models/Consent.cs create mode 100644 src/Umbraco.Core/Models/ConsentState.cs create mode 100644 src/Umbraco.Core/Models/IConsent.cs create mode 100644 src/Umbraco.Core/Models/Rdbms/ConsentDto.cs create mode 100644 src/Umbraco.Core/Persistence/Factories/ConsentFactory.cs create mode 100644 src/Umbraco.Core/Persistence/Mappers/ConsentMapper.cs create mode 100644 src/Umbraco.Core/Persistence/Repositories/ConsentRepository.cs create mode 100644 src/Umbraco.Core/Persistence/Repositories/IConsentRepository.cs create mode 100644 src/Umbraco.Core/Services/ConsentService.cs create mode 100644 src/Umbraco.Core/Services/IConsentService.cs delete mode 100644 src/Umbraco.Core/Umbraco.Core.csproj.DotSettings create mode 100644 src/Umbraco.Tests/Services/ConsentServiceTests.cs diff --git a/src/Umbraco.Core/Models/Consent.cs b/src/Umbraco.Core/Models/Consent.cs new file mode 100644 index 0000000000..de2f05f38d --- /dev/null +++ b/src/Umbraco.Core/Models/Consent.cs @@ -0,0 +1,73 @@ +using System; +using System.Reflection; +using System.Runtime.Serialization; +using Umbraco.Core.Models.EntityBase; + +namespace Umbraco.Core.Models +{ + /// + /// Represents a consent. + /// + [Serializable] + [DataContract(IsReference = true)] + internal class Consent : Entity, IConsent + { + private static PropertySelectors _selector; + + private Udi _source; + private Udi _action; + private ConsentState _state; + private string _comment; + + // ReSharper disable once ClassNeverInstantiated.Local + private class PropertySelectors + { + public readonly PropertyInfo SourceUdi = ExpressionHelper.GetPropertyInfo(x => x.Source); + public readonly PropertyInfo ActionUdi = ExpressionHelper.GetPropertyInfo(x => x.Action); + public readonly PropertyInfo State = ExpressionHelper.GetPropertyInfo(x => x.State); + public readonly PropertyInfo Comment = ExpressionHelper.GetPropertyInfo(x => x.Comment); + } + + private static PropertySelectors Selectors => _selector ?? (_selector = new PropertySelectors()); + + /// + public Udi Source + { + get => _source; + set + { + if (value == null) throw new ArgumentNullException(nameof(value)); + SetPropertyValueAndDetectChanges(value, ref _source, Selectors.SourceUdi); + } + } + + /// + public Udi Action + { + get => _action; + set + { + if (value == null) throw new ArgumentNullException(nameof(value)); + SetPropertyValueAndDetectChanges(value, ref _action, Selectors.ActionUdi); + } + } + + /// + public string ActionType => _action.EntityType; + + /// + public ConsentState State + { + get => _state; + // note: we probably should validate the state here, but since the + // enum is [Flags] with many combinations, this could be expensive + set => SetPropertyValueAndDetectChanges(value, ref _state, Selectors.State); + } + + public string Comment + { + get => _comment; + set => SetPropertyValueAndDetectChanges(value, ref _comment, Selectors.Comment); + } + } +} diff --git a/src/Umbraco.Core/Models/ConsentState.cs b/src/Umbraco.Core/Models/ConsentState.cs new file mode 100644 index 0000000000..460d01d0e7 --- /dev/null +++ b/src/Umbraco.Core/Models/ConsentState.cs @@ -0,0 +1,38 @@ +using System; + +namespace Umbraco.Core.Models +{ + /// + /// Represents the state of a consent. + /// + [Flags] + public enum ConsentState // : int + { + // note - this is a [Flags] enumeration + // on can create detailed flags such as: + //GrantedOptIn = Granted | 0x0001 + //GrandedByForce = Granted | 0x0002 + // + // 16 situations for each Pending/Granted/Revoked should be ok + + /// + /// There is no consent. + /// + None = 0, + + /// + /// Consent is pending and has not been granted yet. + /// + Pending = 0x10000, + + /// + /// Consent has been granted. + /// + Granted = 0x20000, + + /// + /// Consent has been revoked. + /// + Revoked = 0x30000 + } +} diff --git a/src/Umbraco.Core/Models/IConsent.cs b/src/Umbraco.Core/Models/IConsent.cs new file mode 100644 index 0000000000..9fe43c0335 --- /dev/null +++ b/src/Umbraco.Core/Models/IConsent.cs @@ -0,0 +1,37 @@ +using Umbraco.Core.Models.EntityBase; + +namespace Umbraco.Core.Models +{ + /// + /// Represents a consent. + /// + public interface IConsent : IAggregateRoot, IRememberBeingDirty + { + /// + /// Gets or sets the Udi of whoever is consenting. + /// + Udi Source { get; set; } + + /// + /// Gets or sets the Udi of the consented action. + /// + Udi Action { get; set; } + + /// + /// Gets the type of the consented action. + /// + /// This is the Udi type of and represents + /// the domain, application, scope... of the action. + string ActionType { get; } // eg "forms-actions" -> "forms-actions/publish-my-details" + + /// + /// Gets or sets the state of the consent. + /// + ConsentState State { get; set; } + + /// + /// Gets or sets - fixme - comment, or additional json data? + /// + string Comment { get; set; } + } +} diff --git a/src/Umbraco.Core/Models/Rdbms/ConsentDto.cs b/src/Umbraco.Core/Models/Rdbms/ConsentDto.cs new file mode 100644 index 0000000000..cb88692809 --- /dev/null +++ b/src/Umbraco.Core/Models/Rdbms/ConsentDto.cs @@ -0,0 +1,42 @@ +using System; +using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.DatabaseAnnotations; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; + +namespace Umbraco.Core.Models.Rdbms +{ + [TableName(TableName)] + [PrimaryKey("id")] + [ExplicitColumns] + public class ConsentDto + { + internal const string TableName = "umbracoConsent"; + + [Column("id")] + [PrimaryKeyColumn] + public int Id { get; set; } + + [Column("source")] + [Length(512)] // aligned with Deploy SignatureDto + [Index(IndexTypes.UniqueNonClustered, ForColumns = "source, action", Name = "IX_" + TableName + "_UniqueSourceAction")] + public string Source { get; set; } + + [Column("action")] + [Length(512)] // aligned with Deploy SignatureDto + public string Action { get; set; } + + [Column("actionType")] + [Length(128)] + public string ActionType { get; set; } + + [Column("updateDate")] + [Constraint(Default = SystemMethods.CurrentDateTime)] + public DateTime UpdateDate { get; set; } + + [Column("state")] + public int State { get; set; } + + [Column("comment")] + public string Comment { get; set; } + } +} diff --git a/src/Umbraco.Core/Persistence/Factories/ConsentFactory.cs b/src/Umbraco.Core/Persistence/Factories/ConsentFactory.cs new file mode 100644 index 0000000000..c5ffe4ccd0 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Factories/ConsentFactory.cs @@ -0,0 +1,41 @@ +using Umbraco.Core.Models; +using Umbraco.Core.Models.Rdbms; + +namespace Umbraco.Core.Persistence.Factories +{ + internal static class ConsentFactory + { + public static IConsent BuildEntity(ConsentDto dto) + { + var consent = new Consent + { + Id = dto.Id, + UpdateDate = dto.UpdateDate, + Source = Udi.Parse(dto.Source), + Action = Udi.Parse(dto.Action), + // ActionType derives from Action + State = (ConsentState) dto.State, // assume value is valid + Comment = dto.Comment + }; + + //on initial construction we don't want to have dirty properties tracked + // http://issues.umbraco.org/issue/U4-1946 + consent.ResetDirtyProperties(false); + return consent; + } + + public static ConsentDto BuildDto(IConsent entity) + { + return new ConsentDto + { + Id = entity.Id, + UpdateDate = entity.UpdateDate, + Source = entity.Source.ToString(), + Action = entity.Action.ToString(), + ActionType = entity.ActionType, + State = (int) entity.State, + Comment = entity.Comment + }; + } + } +} diff --git a/src/Umbraco.Core/Persistence/Mappers/ConsentMapper.cs b/src/Umbraco.Core/Persistence/Mappers/ConsentMapper.cs new file mode 100644 index 0000000000..a3f604c1ba --- /dev/null +++ b/src/Umbraco.Core/Persistence/Mappers/ConsentMapper.cs @@ -0,0 +1,39 @@ +using System.Collections.Concurrent; +using Umbraco.Core.Models; +using Umbraco.Core.Models.Rdbms; + +namespace Umbraco.Core.Persistence.Mappers +{ + /// + /// Represents a mapper for consent entities. + /// + [MapperFor(typeof(IConsent))] + [MapperFor(typeof(Consent))] + public sealed class ConsentMapper : BaseMapper + { + private static readonly ConcurrentDictionary PropertyInfoCacheInstance + = new ConcurrentDictionary(); + + /// + /// Initializes a new instance of the class. + /// + public ConsentMapper() + { + // note: why the base ctor does not invoke BuildMap is a mystery to me + BuildMap(); + } + + internal override ConcurrentDictionary PropertyInfoCache => PropertyInfoCacheInstance; + + internal override void BuildMap() + { + CacheMap(entity => entity.Id, dto => dto.Id); + CacheMap(entity => entity.UpdateDate, dto => dto.UpdateDate); + CacheMap(entity => entity.Source, dto => dto.Source); + CacheMap(entity => entity.Action, dto => dto.Action); + CacheMap(entity => entity.ActionType, dto => dto.ActionType); + CacheMap(entity => entity.State, dto => dto.State); + CacheMap(entity => entity.Comment, dto => dto.Comment); + } + } +} diff --git a/src/Umbraco.Core/Persistence/Mappers/PetaPocoMapper.cs b/src/Umbraco.Core/Persistence/Mappers/PetaPocoMapper.cs index fd516073ce..98527a19c4 100644 --- a/src/Umbraco.Core/Persistence/Mappers/PetaPocoMapper.cs +++ b/src/Umbraco.Core/Persistence/Mappers/PetaPocoMapper.cs @@ -20,6 +20,12 @@ namespace Umbraco.Core.Persistence.Mappers public Func GetFromDbConverter(PropertyInfo pi, Type sourceType) { + // Udi is handled as string + if (typeof(Udi).IsAssignableFrom(pi.PropertyType) && sourceType == typeof(string)) + return value => value is string stringValue + ? (string.IsNullOrWhiteSpace(stringValue) ? null : Udi.Parse(stringValue)) + : null; + return null; } @@ -39,7 +45,11 @@ namespace Umbraco.Core.Persistence.Mappers }; } + // Udi is handled as string + if (typeof(Udi).IsAssignableFrom(sourceType)) + return value => value?.ToString(); + return null; } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaCreation.cs b/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaCreation.cs index d1741b4ee0..2c6c54be75 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaCreation.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaCreation.cs @@ -92,7 +92,8 @@ namespace Umbraco.Core.Persistence.Migrations.Initial {52, typeof (UserGroup2NodePermissionDto) }, {53, typeof (UserGroup2AppDto) }, {54, typeof (UserStartNodeDto) }, - {55, typeof (UserLoginDto)} + {55, typeof (UserLoginDto)}, + {56, typeof (ConsentDto)} }; #endregion @@ -378,4 +379,4 @@ namespace Umbraco.Core.Persistence.Migrations.Initial #endregion } -} \ No newline at end of file +} diff --git a/src/Umbraco.Core/Persistence/Repositories/ConsentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ConsentRepository.cs new file mode 100644 index 0000000000..2a653699ec --- /dev/null +++ b/src/Umbraco.Core/Persistence/Repositories/ConsentRepository.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core.Logging; +using Umbraco.Core.Models; +using Umbraco.Core.Models.EntityBase; +using Umbraco.Core.Models.Rdbms; +using Umbraco.Core.Persistence.Factories; +using Umbraco.Core.Persistence.Querying; +using Umbraco.Core.Persistence.SqlSyntax; +using Umbraco.Core.Persistence.UnitOfWork; + +namespace Umbraco.Core.Persistence.Repositories +{ + /// + /// Represents the PetaPoco implementation of . + /// + internal class ConsentRepository : PetaPocoRepositoryBase, IConsentRepository + { + /// + /// Initializes a new instance of the class. + /// + public ConsentRepository(IScopeUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) + : base(work, cache, logger, sqlSyntax) + { } + + /// + protected override Guid NodeObjectTypeId => throw new NotSupportedException(); + + /// + protected override IConsent PerformGet(int id) + { + var sql = new Sql() + .Select("*") + .From(SqlSyntax) + .Where(x => x.Id == id, SqlSyntax); + + var dto = Database.FirstOrDefault(sql); + return dto == null ? null : ConsentFactory.BuildEntity(dto); + } + + /// + protected override IEnumerable PerformGetAll(params int[] ids) + { + if (ids.Length == 0) + { + var sql = new Sql() + .Select("*") + .From(SqlSyntax); + + return Database.Fetch(sql).Select(ConsentFactory.BuildEntity); + } + + var consents = new List(); + + foreach (var group in ids.InGroupsOf(2000)) + { + var sql = new Sql() + .Select("*") + .From(SqlSyntax) + .WhereIn(x => x.Id, group, SqlSyntax); + + consents.AddRange(Database.Fetch(sql).Select(ConsentFactory.BuildEntity)); + } + + return consents; + } + + /// + protected override IEnumerable PerformGetByQuery(IQuery query) + { + var sqlClause = GetBaseQuery(false); + var translator = new SqlTranslator(sqlClause, query); + var sql = translator.Translate(); + return Database.Fetch(sql).Select(ConsentFactory.BuildEntity); + } + + /// + protected override Sql GetBaseQuery(bool isCount) + { + return new Sql().Select(isCount ? "COUNT(*)" : "*").From(SqlSyntax); + } + + /// + protected override string GetBaseWhereClause() + { + return $"{ConsentDto.TableName}.id = @Id"; + } + + /// + protected override IEnumerable GetDeleteClauses() + { + return new[] + { + $"DELETE FROM {ConsentDto.TableName} WHERE id = @Id" + }; + } + + /// + protected override void PersistNewItem(IConsent entity) + { + ((Entity) entity).AddingEntity(); + + var dto = ConsentFactory.BuildDto(entity); + Database.Insert(dto); // table has a unique index on source+action + entity.Id = dto.Id; + entity.ResetDirtyProperties(); + } + + /// + protected override void PersistUpdatedItem(IConsent entity) + { + ((Entity) entity).UpdatingEntity(); + + var dto = ConsentFactory.BuildDto(entity); + Database.Update(dto); // table has a unique index on source+action + entity.ResetDirtyProperties(); + + IsolatedCache.ClearCacheItem(GetCacheIdKey(entity.Id)); + } + } +} diff --git a/src/Umbraco.Core/Persistence/Repositories/IConsentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IConsentRepository.cs new file mode 100644 index 0000000000..e49ad0bf76 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Repositories/IConsentRepository.cs @@ -0,0 +1,10 @@ +using Umbraco.Core.Models; + +namespace Umbraco.Core.Persistence.Repositories +{ + /// + /// Represents a repository for entities. + /// + public interface IConsentRepository : IRepositoryQueryable + { } +} diff --git a/src/Umbraco.Core/Persistence/RepositoryFactory.cs b/src/Umbraco.Core/Persistence/RepositoryFactory.cs index 34a4b1a612..af64de9d45 100644 --- a/src/Umbraco.Core/Persistence/RepositoryFactory.cs +++ b/src/Umbraco.Core/Persistence/RepositoryFactory.cs @@ -397,5 +397,10 @@ namespace Umbraco.Core.Persistence _logger, _sqlSyntax); } + + public IConsentRepository CreateConsentRepository(IScopeUnitOfWork uow) + { + return new ConsentRepository(uow, _cacheHelper, _logger, _sqlSyntax); + } } } diff --git a/src/Umbraco.Core/Services/ConsentService.cs b/src/Umbraco.Core/Services/ConsentService.cs new file mode 100644 index 0000000000..4e456f5f7a --- /dev/null +++ b/src/Umbraco.Core/Services/ConsentService.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; +using Umbraco.Core.Events; +using Umbraco.Core.Logging; +using Umbraco.Core.Models; +using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.Querying; +using Umbraco.Core.Persistence.UnitOfWork; + +namespace Umbraco.Core.Services +{ + /// + /// Implements . + /// + internal class ConsentService : ScopeRepositoryService, IConsentService + { + /// + /// Initializes a new instance of the class. + /// + public ConsentService(IScopeUnitOfWorkProvider provider, RepositoryFactory repositoryFactory, ILogger logger, IEventMessagesFactory eventMessagesFactory) + : base(provider, repositoryFactory, logger, eventMessagesFactory) + { } + + /// + public IConsent Get(int id) + { + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) + { + var repository = RepositoryFactory.CreateConsentRepository(uow); + return repository.Get(id); + } + } + + /// + public IEnumerable GetBySource(Udi source, string actionType = null) + { + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) + { + var repository = RepositoryFactory.CreateConsentRepository(uow); + var query = new Query().Where(x => x.Source == source); + if (string.IsNullOrWhiteSpace(actionType) == false) + query = query.Where(x => x.ActionType == actionType); + return repository.GetByQuery(query); + } + } + + /// + public IEnumerable GetByAction(Udi action) + { + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) + { + var repository = RepositoryFactory.CreateConsentRepository(uow); + var query = new Query().Where(x => x.Action == action); + return repository.GetByQuery(query); + } + } + + /// + public IEnumerable GetByActionType(string actionType) + { + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) + { + var repository = RepositoryFactory.CreateConsentRepository(uow); + var query = new Query().Where(x => x.ActionType == actionType); + return repository.GetByQuery(query); + } + } + + /// + public void Save(IConsent consent) + { + try + { + using (var uow = UowProvider.GetUnitOfWork()) + { + var repository = RepositoryFactory.CreateConsentRepository(uow); + repository.AddOrUpdate(consent); + uow.Commit(); + } + } + catch (Exception e) + { + throw new Exception("Failed to save consent (see inner exception).", e); + } + } + + public void Delete(IConsent consent) + { + using (var uow = UowProvider.GetUnitOfWork()) + { + var repository = RepositoryFactory.CreateConsentRepository(uow); + repository.Delete(consent); + uow.Commit(); + } + } + } +} diff --git a/src/Umbraco.Core/Services/IConsentService.cs b/src/Umbraco.Core/Services/IConsentService.cs new file mode 100644 index 0000000000..f88b97393c --- /dev/null +++ b/src/Umbraco.Core/Services/IConsentService.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using Umbraco.Core.Models; + +namespace Umbraco.Core.Services +{ + /// + /// Represents a service for handling entities. + /// + public interface IConsentService : IService + { + // notes + // + // at the moment this is just channelling CRUD to the repository but + // the consent service should implement as much of the consenting + // logic as possible + // + // we might need some Get methods by State + + /// + /// Gets the consent entity with the specified identifier. + /// + IConsent Get(int id); + + /// + /// Gets the consents of a source. + /// + IEnumerable GetBySource(Udi source, string actionType = null); + + /// + /// Gets the consents for an action. + /// + IEnumerable GetByAction(Udi action); + + /// + /// Gets the consents for an action type. + /// + IEnumerable GetByActionType(string actionType); + + /// + /// Saves a consent. + /// + void Save(IConsent consent); // fixme should it be an attempt? + + /// + /// Deletes a consent. + /// + void Delete(IConsent consent); + } +} diff --git a/src/Umbraco.Core/Services/ServiceContext.cs b/src/Umbraco.Core/Services/ServiceContext.cs index 144d548b78..24596aefc3 100644 --- a/src/Umbraco.Core/Services/ServiceContext.cs +++ b/src/Umbraco.Core/Services/ServiceContext.cs @@ -62,38 +62,13 @@ namespace Umbraco.Core.Services private Lazy _notificationService; private Lazy _externalLoginService; private Lazy _redirectUrlService; + private Lazy _consentService; internal IdkMap IdkMap { get; private set; } /// /// public ctor - will generally just be used for unit testing all items are optional and if not specified, the defaults will be used /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// public ServiceContext( IContentService contentService = null, IMediaService mediaService = null, @@ -120,7 +95,8 @@ namespace Umbraco.Core.Services IPublicAccessService publicAccessService = null, IExternalLoginService externalLoginService = null, IMigrationEntryService migrationEntryService = null, - IRedirectUrlService redirectUrlService = null) + IRedirectUrlService redirectUrlService = null, + IConsentService consentService = null) { if (migrationEntryService != null) _migrationEntryService = new Lazy(() => migrationEntryService); if (externalLoginService != null) _externalLoginService = new Lazy(() => externalLoginService); @@ -148,6 +124,7 @@ namespace Umbraco.Core.Services if (macroService != null) _macroService = new Lazy(() => macroService); if (publicAccessService != null) _publicAccessService = new Lazy(() => publicAccessService); if (redirectUrlService != null) _redirectUrlService = new Lazy(() => redirectUrlService); + if (consentService != null) _consentService = new Lazy(() => consentService); } /// @@ -310,6 +287,9 @@ namespace Umbraco.Core.Services if (_redirectUrlService == null) _redirectUrlService = new Lazy(() => new RedirectUrlService(provider, repositoryFactory, logger, eventMessagesFactory)); + + if (_consentService == null) + _consentService = new Lazy(() => new ConsentService(provider, repositoryFactory, logger, eventMessagesFactory)); } internal IEventMessagesFactory EventMessagesFactory { get; private set; } @@ -526,5 +506,10 @@ namespace Umbraco.Core.Services { get { return _redirectUrlService.Value; } } + + /// + /// Gets the implementation. + /// + public IConsentService ConsentService => _consentService.Value; } -} \ No newline at end of file +} diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 4a44ef2dcc..e406b1a96a 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -25,6 +25,7 @@ prompt 4 false + latest pdbonly @@ -363,9 +364,12 @@ + + + @@ -377,6 +381,7 @@ + @@ -541,6 +546,7 @@ + @@ -624,10 +630,13 @@ + + + @@ -724,8 +733,10 @@ + + diff --git a/src/Umbraco.Core/Umbraco.Core.csproj.DotSettings b/src/Umbraco.Core/Umbraco.Core.csproj.DotSettings deleted file mode 100644 index 662f95686e..0000000000 --- a/src/Umbraco.Core/Umbraco.Core.csproj.DotSettings +++ /dev/null @@ -1,2 +0,0 @@ - - CSharp50 \ No newline at end of file diff --git a/src/Umbraco.Tests/Services/ConsentServiceTests.cs b/src/Umbraco.Tests/Services/ConsentServiceTests.cs new file mode 100644 index 0000000000..b0819c5682 --- /dev/null +++ b/src/Umbraco.Tests/Services/ConsentServiceTests.cs @@ -0,0 +1,197 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using Umbraco.Core; +using Umbraco.Core.Deploy; +using Umbraco.Core.Models; +using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.Mappers; +using Umbraco.Tests.TestHelpers; + +namespace Umbraco.Tests.Services +{ + [TestFixture] + [DatabaseTestBehavior(DatabaseBehavior.NewDbFileAndSchemaPerFixture)] + public class ConsentServiceTests : BaseServiceTest + { + [SetUp] + public override void Initialize() + { + base.Initialize(); + } + + [TearDown] + public override void TearDown() + { + base.TearDown(); + } + + // fixme - this is weird, but the only way to register UDIs? + [UdiDefinition("user", UdiType.StringUdi)] + [UdiDefinition("app-actions", UdiType.StringUdi)] + [UdiDefinition("app2-actions", UdiType.StringUdi)] + public class TempServiceConnector : IServiceConnector + { + public IArtifact GetArtifact(Udi udi) + { + throw new System.NotImplementedException(); + } + + public IArtifact GetArtifact(object entity) + { + throw new System.NotImplementedException(); + } + + public ArtifactDeployState ProcessInit(IArtifact art, IDeployContext context) + { + throw new System.NotImplementedException(); + } + + public void Process(ArtifactDeployState dart, IDeployContext context, int pass) + { + throw new System.NotImplementedException(); + } + + public void Explode(UdiRange range, List udis) + { + throw new System.NotImplementedException(); + } + + public NamedUdiRange GetRange(Udi udi, string selector) + { + throw new System.NotImplementedException(); + } + + public NamedUdiRange GetRange(string entityType, string sid, string selector) + { + throw new System.NotImplementedException(); + } + + public bool Compare(IArtifact art1, IArtifact art2, ICollection differences = null) + { + throw new System.NotImplementedException(); + } + } + + [Test] + public void CanCrudConsent() + { + // fixme - why isn't this set by the test base class? + Database.Mapper = new PetaPocoMapper(); + + var consent = new Consent + { + Source = new StringUdi("user", "1234"), + Action = new StringUdi("app-actions", "do-something"), + State = ConsentState.Granted, + Comment = "no comment" + }; + + var consentService = ServiceContext.ConsentService; + + // can save + + consentService.Save(consent); + + Assert.AreNotEqual(0, consent.Id); + + // can get + + var consent2 = consentService.Get(consent.Id); + + Assert.AreEqual(consent.Source, consent2.Source); + Assert.AreEqual(consent.Action, consent2.Action); + Assert.AreEqual(consent.State, consent2.State); + Assert.AreEqual(consent.Comment, consent2.Comment); + + // can save more + + consentService.Save(new Consent + { + Source = new StringUdi("user", "1234"), + Action = new StringUdi("app-actions", "do-something-else"), + State = ConsentState.Granted, + Comment = "no comment" + }); + + consentService.Save(new Consent + { + Source = new StringUdi("user", "1236"), + Action = new StringUdi("app-actions", "do-something"), + State = ConsentState.Granted, + Comment = "no comment" + }); + + consentService.Save(new Consent + { + Source = new StringUdi("user", "1237"), + Action = new StringUdi("app2-actions", "do-something"), + State = ConsentState.Granted, + Comment = "no comment" + }); + + // can get by source + + var consents = consentService.GetBySource(new StringUdi("user", "1235")).ToArray(); + Assert.IsEmpty(consents); + + consents = consentService.GetBySource(new StringUdi("user", "1234")).ToArray(); + Assert.AreEqual(2, consents.Length); + Assert.IsTrue(consents.All(x => x.Source == new StringUdi("user", "1234"))); + Assert.IsTrue(consents.Any(x => x.Action == new StringUdi("app-actions", "do-something"))); + Assert.IsTrue(consents.Any(x => x.Action == new StringUdi("app-actions", "do-something-else"))); + + // can get by action + + consents = consentService.GetByAction(new StringUdi("app-actions", "do-whatever")).ToArray(); + Assert.IsEmpty(consents); + + consents = consentService.GetByAction(new StringUdi("app-actions", "do-something")).ToArray(); + Assert.AreEqual(2, consents.Length); + Assert.IsTrue(consents.All(x => x.Action == new StringUdi("app-actions", "do-something"))); + Assert.IsTrue(consents.Any(x => x.Source == new StringUdi("user", "1234"))); + Assert.IsTrue(consents.Any(x => x.Source == new StringUdi("user", "1236"))); + + // can get by action type + + consents = consentService.GetByActionType("app3-actions").ToArray(); + Assert.IsEmpty(consents); + + consents = consentService.GetByActionType("app2-actions").ToArray(); + Assert.AreEqual(1, consents.Length); + + consents = consentService.GetByActionType("app-actions").ToArray(); + Assert.AreEqual(3, consents.Length); + Assert.IsTrue(consents.Any(x => x.Action == new StringUdi("app-actions", "do-something"))); + Assert.IsTrue(consents.Any(x => x.Action == new StringUdi("app-actions", "do-something-else"))); + + // can delete + + consents = consentService.GetByActionType("app2-actions").ToArray(); + Assert.AreEqual(1, consents.Length); + consentService.Delete(consents[0]); + consents = consentService.GetByActionType("app2-actions").ToArray(); + Assert.IsEmpty(consents); + + // can update + + var date = consent.UpdateDate; + consent.State = ConsentState.Revoked; + consentService.Save(consent); + Assert.AreNotEqual(date, consent.UpdateDate); + + // cannot create duplicates + + var consent3 = new Consent + { + Source = new StringUdi("user", "1234"), + Action = new StringUdi("app-actions", "do-something"), + State = ConsentState.Granted, + Comment = "no comment" + }; + + Assert.Throws(() => consentService.Save(consent3)); + } + } +} diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 82e4f8ee8f..52261b454f 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -190,6 +190,7 @@ + diff --git a/src/umbraco.sln.DotSettings b/src/umbraco.sln.DotSettings index dd168f105b..0791fed591 100644 --- a/src/umbraco.sln.DotSettings +++ b/src/umbraco.sln.DotSettings @@ -16,5 +16,5 @@ Disposable construction HINT False - CSharp50 + CSharp72 \ No newline at end of file From e1208e5019a41049faacebd74f76a412f370c622 Mon Sep 17 00:00:00 2001 From: Stephan Date: Mon, 29 Jan 2018 11:30:01 +0100 Subject: [PATCH 2/9] U4-10794 - use string ids, not udis --- src/Umbraco.Core/Models/Consent.cs | 32 ++++-- src/Umbraco.Core/Models/IConsent.cs | 18 ++-- src/Umbraco.Core/Models/Rdbms/ConsentDto.cs | 2 +- .../Persistence/Factories/ConsentFactory.cs | 8 +- src/Umbraco.Core/Services/ConsentService.cs | 4 +- src/Umbraco.Core/Services/IConsentService.cs | 4 +- .../Plugins/PluginManagerTests.cs | 4 +- .../Services/ConsentServiceTests.cs | 100 +++++------------- 8 files changed, 71 insertions(+), 101 deletions(-) diff --git a/src/Umbraco.Core/Models/Consent.cs b/src/Umbraco.Core/Models/Consent.cs index de2f05f38d..04e878e078 100644 --- a/src/Umbraco.Core/Models/Consent.cs +++ b/src/Umbraco.Core/Models/Consent.cs @@ -14,16 +14,18 @@ namespace Umbraco.Core.Models { private static PropertySelectors _selector; - private Udi _source; - private Udi _action; + private string _source; + private string _action; + private string _actionType; private ConsentState _state; private string _comment; // ReSharper disable once ClassNeverInstantiated.Local private class PropertySelectors { - public readonly PropertyInfo SourceUdi = ExpressionHelper.GetPropertyInfo(x => x.Source); - public readonly PropertyInfo ActionUdi = ExpressionHelper.GetPropertyInfo(x => x.Action); + public readonly PropertyInfo Source = ExpressionHelper.GetPropertyInfo(x => x.Source); + public readonly PropertyInfo Action = ExpressionHelper.GetPropertyInfo(x => x.Action); + public readonly PropertyInfo ActionType = ExpressionHelper.GetPropertyInfo(x => x.ActionType); public readonly PropertyInfo State = ExpressionHelper.GetPropertyInfo(x => x.State); public readonly PropertyInfo Comment = ExpressionHelper.GetPropertyInfo(x => x.Comment); } @@ -31,29 +33,37 @@ namespace Umbraco.Core.Models private static PropertySelectors Selectors => _selector ?? (_selector = new PropertySelectors()); /// - public Udi Source + public string Source { get => _source; set { - if (value == null) throw new ArgumentNullException(nameof(value)); - SetPropertyValueAndDetectChanges(value, ref _source, Selectors.SourceUdi); + if (string.IsNullOrWhiteSpace(value)) throw new ArgumentException(nameof(value)); + SetPropertyValueAndDetectChanges(value, ref _source, Selectors.Source); } } /// - public Udi Action + public string Action { get => _action; set { - if (value == null) throw new ArgumentNullException(nameof(value)); - SetPropertyValueAndDetectChanges(value, ref _action, Selectors.ActionUdi); + if (string.IsNullOrWhiteSpace(value)) throw new ArgumentException(nameof(value)); + SetPropertyValueAndDetectChanges(value, ref _action, Selectors.Action); } } /// - public string ActionType => _action.EntityType; + public string ActionType + { + get => _actionType; + set + { + if (string.IsNullOrWhiteSpace(value)) throw new ArgumentException(nameof(value)); + SetPropertyValueAndDetectChanges(value, ref _actionType, Selectors.ActionType); + } + } /// public ConsentState State diff --git a/src/Umbraco.Core/Models/IConsent.cs b/src/Umbraco.Core/Models/IConsent.cs index 9fe43c0335..4b62d11bee 100644 --- a/src/Umbraco.Core/Models/IConsent.cs +++ b/src/Umbraco.Core/Models/IConsent.cs @@ -8,21 +8,25 @@ namespace Umbraco.Core.Models public interface IConsent : IAggregateRoot, IRememberBeingDirty { /// - /// Gets or sets the Udi of whoever is consenting. + /// Gets or sets the unique identifier of whoever is consenting. /// - Udi Source { get; set; } + /// Could be a Udi, or anything really. + string Source { get; set; } /// /// Gets or sets the Udi of the consented action. /// - Udi Action { get; set; } + /// Could be a Udi, or anything really. + string Action { get; set; } /// /// Gets the type of the consented action. /// - /// This is the Udi type of and represents - /// the domain, application, scope... of the action. - string ActionType { get; } // eg "forms-actions" -> "forms-actions/publish-my-details" + /// + /// Represents the domain, application, scope... of the action. + /// When the action is a Udi, this should be the Udi type. + /// + string ActionType { get; } /// /// Gets or sets the state of the consent. @@ -30,7 +34,7 @@ namespace Umbraco.Core.Models ConsentState State { get; set; } /// - /// Gets or sets - fixme - comment, or additional json data? + /// Gets or sets some additional free text. /// string Comment { get; set; } } diff --git a/src/Umbraco.Core/Models/Rdbms/ConsentDto.cs b/src/Umbraco.Core/Models/Rdbms/ConsentDto.cs index cb88692809..830d8f8d7d 100644 --- a/src/Umbraco.Core/Models/Rdbms/ConsentDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/ConsentDto.cs @@ -18,7 +18,7 @@ namespace Umbraco.Core.Models.Rdbms [Column("source")] [Length(512)] // aligned with Deploy SignatureDto - [Index(IndexTypes.UniqueNonClustered, ForColumns = "source, action", Name = "IX_" + TableName + "_UniqueSourceAction")] + [Index(IndexTypes.UniqueNonClustered, ForColumns = "source, actionType, action", Name = "IX_" + TableName + "_UniqueSourceAction")] public string Source { get; set; } [Column("action")] diff --git a/src/Umbraco.Core/Persistence/Factories/ConsentFactory.cs b/src/Umbraco.Core/Persistence/Factories/ConsentFactory.cs index c5ffe4ccd0..37689dfe3e 100644 --- a/src/Umbraco.Core/Persistence/Factories/ConsentFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/ConsentFactory.cs @@ -11,8 +11,8 @@ namespace Umbraco.Core.Persistence.Factories { Id = dto.Id, UpdateDate = dto.UpdateDate, - Source = Udi.Parse(dto.Source), - Action = Udi.Parse(dto.Action), + Source = dto.Source, + Action = dto.Action, // ActionType derives from Action State = (ConsentState) dto.State, // assume value is valid Comment = dto.Comment @@ -30,8 +30,8 @@ namespace Umbraco.Core.Persistence.Factories { Id = entity.Id, UpdateDate = entity.UpdateDate, - Source = entity.Source.ToString(), - Action = entity.Action.ToString(), + Source = entity.Source, + Action = entity.Action, ActionType = entity.ActionType, State = (int) entity.State, Comment = entity.Comment diff --git a/src/Umbraco.Core/Services/ConsentService.cs b/src/Umbraco.Core/Services/ConsentService.cs index 4e456f5f7a..6d8b38164a 100644 --- a/src/Umbraco.Core/Services/ConsentService.cs +++ b/src/Umbraco.Core/Services/ConsentService.cs @@ -32,7 +32,7 @@ namespace Umbraco.Core.Services } /// - public IEnumerable GetBySource(Udi source, string actionType = null) + public IEnumerable GetBySource(string source, string actionType = null) { using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { @@ -45,7 +45,7 @@ namespace Umbraco.Core.Services } /// - public IEnumerable GetByAction(Udi action) + public IEnumerable GetByAction(string action) { using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { diff --git a/src/Umbraco.Core/Services/IConsentService.cs b/src/Umbraco.Core/Services/IConsentService.cs index f88b97393c..420662165d 100644 --- a/src/Umbraco.Core/Services/IConsentService.cs +++ b/src/Umbraco.Core/Services/IConsentService.cs @@ -24,12 +24,12 @@ namespace Umbraco.Core.Services /// /// Gets the consents of a source. /// - IEnumerable GetBySource(Udi source, string actionType = null); + IEnumerable GetBySource(string source, string actionType = null); /// /// Gets the consents for an action. /// - IEnumerable GetByAction(Udi action); + IEnumerable GetByAction(string action); /// /// Gets the consents for an action type. diff --git a/src/Umbraco.Tests/Plugins/PluginManagerTests.cs b/src/Umbraco.Tests/Plugins/PluginManagerTests.cs index fa4e9d9d21..e45025f322 100644 --- a/src/Umbraco.Tests/Plugins/PluginManagerTests.cs +++ b/src/Umbraco.Tests/Plugins/PluginManagerTests.cs @@ -271,7 +271,7 @@ AnotherContentFinder public void Resolves_Assigned_Mappers() { var foundTypes1 = _manager.ResolveAssignedMapperTypes(); - Assert.AreEqual(29, foundTypes1.Count()); + Assert.AreEqual(30, foundTypes1.Count()); } [Test] @@ -390,4 +390,4 @@ AnotherContentFinder } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Tests/Services/ConsentServiceTests.cs b/src/Umbraco.Tests/Services/ConsentServiceTests.cs index b0819c5682..b00b61e083 100644 --- a/src/Umbraco.Tests/Services/ConsentServiceTests.cs +++ b/src/Umbraco.Tests/Services/ConsentServiceTests.cs @@ -1,9 +1,6 @@ using System; -using System.Collections.Generic; using System.Linq; using NUnit.Framework; -using Umbraco.Core; -using Umbraco.Core.Deploy; using Umbraco.Core.Models; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Mappers; @@ -13,6 +10,7 @@ namespace Umbraco.Tests.Services { [TestFixture] [DatabaseTestBehavior(DatabaseBehavior.NewDbFileAndSchemaPerFixture)] + [NUnit.Framework.Explicit("breaks everything!")] public class ConsentServiceTests : BaseServiceTest { [SetUp] @@ -27,53 +25,6 @@ namespace Umbraco.Tests.Services base.TearDown(); } - // fixme - this is weird, but the only way to register UDIs? - [UdiDefinition("user", UdiType.StringUdi)] - [UdiDefinition("app-actions", UdiType.StringUdi)] - [UdiDefinition("app2-actions", UdiType.StringUdi)] - public class TempServiceConnector : IServiceConnector - { - public IArtifact GetArtifact(Udi udi) - { - throw new System.NotImplementedException(); - } - - public IArtifact GetArtifact(object entity) - { - throw new System.NotImplementedException(); - } - - public ArtifactDeployState ProcessInit(IArtifact art, IDeployContext context) - { - throw new System.NotImplementedException(); - } - - public void Process(ArtifactDeployState dart, IDeployContext context, int pass) - { - throw new System.NotImplementedException(); - } - - public void Explode(UdiRange range, List udis) - { - throw new System.NotImplementedException(); - } - - public NamedUdiRange GetRange(Udi udi, string selector) - { - throw new System.NotImplementedException(); - } - - public NamedUdiRange GetRange(string entityType, string sid, string selector) - { - throw new System.NotImplementedException(); - } - - public bool Compare(IArtifact art1, IArtifact art2, ICollection differences = null) - { - throw new System.NotImplementedException(); - } - } - [Test] public void CanCrudConsent() { @@ -82,8 +33,9 @@ namespace Umbraco.Tests.Services var consent = new Consent { - Source = new StringUdi("user", "1234"), - Action = new StringUdi("app-actions", "do-something"), + Source = "user/1234", + Action = "app-actions/do-something", + ActionType = "app-actions", State = ConsentState.Granted, Comment = "no comment" }; @@ -109,49 +61,52 @@ namespace Umbraco.Tests.Services consentService.Save(new Consent { - Source = new StringUdi("user", "1234"), - Action = new StringUdi("app-actions", "do-something-else"), + Source = "user/1234", + Action = "app-actions/do-something-else", + ActionType = "app-actions", State = ConsentState.Granted, Comment = "no comment" }); consentService.Save(new Consent { - Source = new StringUdi("user", "1236"), - Action = new StringUdi("app-actions", "do-something"), + Source = "user/1236", + Action = "app-actions/do-something", + ActionType = "app-actions", State = ConsentState.Granted, Comment = "no comment" }); consentService.Save(new Consent { - Source = new StringUdi("user", "1237"), - Action = new StringUdi("app2-actions", "do-something"), + Source = "user/1237", + Action = "app2-actions/do-something", + ActionType = "app2-actions", State = ConsentState.Granted, Comment = "no comment" }); // can get by source - var consents = consentService.GetBySource(new StringUdi("user", "1235")).ToArray(); + var consents = consentService.GetBySource("user/1235").ToArray(); Assert.IsEmpty(consents); - consents = consentService.GetBySource(new StringUdi("user", "1234")).ToArray(); + consents = consentService.GetBySource("user/1234").ToArray(); Assert.AreEqual(2, consents.Length); - Assert.IsTrue(consents.All(x => x.Source == new StringUdi("user", "1234"))); - Assert.IsTrue(consents.Any(x => x.Action == new StringUdi("app-actions", "do-something"))); - Assert.IsTrue(consents.Any(x => x.Action == new StringUdi("app-actions", "do-something-else"))); + Assert.IsTrue(consents.All(x => x.Source == "user/1234")); + Assert.IsTrue(consents.Any(x => x.Action == "app-actions/do-something")); + Assert.IsTrue(consents.Any(x => x.Action == "app-actions/do-something-else")); // can get by action - consents = consentService.GetByAction(new StringUdi("app-actions", "do-whatever")).ToArray(); + consents = consentService.GetByAction("app-actions/do-whatever").ToArray(); Assert.IsEmpty(consents); - consents = consentService.GetByAction(new StringUdi("app-actions", "do-something")).ToArray(); + consents = consentService.GetByAction("app-actions/do-something").ToArray(); Assert.AreEqual(2, consents.Length); - Assert.IsTrue(consents.All(x => x.Action == new StringUdi("app-actions", "do-something"))); - Assert.IsTrue(consents.Any(x => x.Source == new StringUdi("user", "1234"))); - Assert.IsTrue(consents.Any(x => x.Source == new StringUdi("user", "1236"))); + Assert.IsTrue(consents.All(x => x.Action == "app-actions/do-something")); + Assert.IsTrue(consents.Any(x => x.Source == "user/1234")); + Assert.IsTrue(consents.Any(x => x.Source == "user/1236")); // can get by action type @@ -163,8 +118,8 @@ namespace Umbraco.Tests.Services consents = consentService.GetByActionType("app-actions").ToArray(); Assert.AreEqual(3, consents.Length); - Assert.IsTrue(consents.Any(x => x.Action == new StringUdi("app-actions", "do-something"))); - Assert.IsTrue(consents.Any(x => x.Action == new StringUdi("app-actions", "do-something-else"))); + Assert.IsTrue(consents.Any(x => x.Action == "app-actions/do-something")); + Assert.IsTrue(consents.Any(x => x.Action == "app-actions/do-something-else")); // can delete @@ -185,8 +140,9 @@ namespace Umbraco.Tests.Services var consent3 = new Consent { - Source = new StringUdi("user", "1234"), - Action = new StringUdi("app-actions", "do-something"), + Source = "user/1234", + Action = "app-actions/do-something", + ActionType = "app-actions", State = ConsentState.Granted, Comment = "no comment" }; From 5aa1b81cd471aa0fc5ca8d7e885cd67c8d01f80a Mon Sep 17 00:00:00 2001 From: Stephan Date: Mon, 29 Jan 2018 16:27:16 +0100 Subject: [PATCH 3/9] U4-10794 - revert PEtaPocoMapper changes --- .../Persistence/Mappers/PetaPocoMapper.cs | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Mappers/PetaPocoMapper.cs b/src/Umbraco.Core/Persistence/Mappers/PetaPocoMapper.cs index 98527a19c4..fd516073ce 100644 --- a/src/Umbraco.Core/Persistence/Mappers/PetaPocoMapper.cs +++ b/src/Umbraco.Core/Persistence/Mappers/PetaPocoMapper.cs @@ -20,12 +20,6 @@ namespace Umbraco.Core.Persistence.Mappers public Func GetFromDbConverter(PropertyInfo pi, Type sourceType) { - // Udi is handled as string - if (typeof(Udi).IsAssignableFrom(pi.PropertyType) && sourceType == typeof(string)) - return value => value is string stringValue - ? (string.IsNullOrWhiteSpace(stringValue) ? null : Udi.Parse(stringValue)) - : null; - return null; } @@ -45,11 +39,7 @@ namespace Umbraco.Core.Persistence.Mappers }; } - // Udi is handled as string - if (typeof(Udi).IsAssignableFrom(sourceType)) - return value => value?.ToString(); - return null; } } -} +} \ No newline at end of file From f2667c336093b6ac5102f1dec6ec876eb06b2e24 Mon Sep 17 00:00:00 2001 From: Sebastiaan Jansssen Date: Wed, 31 Jan 2018 12:47:16 +0100 Subject: [PATCH 4/9] Bumps version to 7.9.0 --- src/SolutionInfo.cs | 4 ++-- src/Umbraco.Core/Configuration/UmbracoVersion.cs | 2 +- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/SolutionInfo.cs b/src/SolutionInfo.cs index 477b5b55ea..ace98f2139 100644 --- a/src/SolutionInfo.cs +++ b/src/SolutionInfo.cs @@ -11,5 +11,5 @@ using System.Resources; [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyFileVersion("7.8.0")] -[assembly: AssemblyInformationalVersion("7.8.0-beta009")] \ No newline at end of file +[assembly: AssemblyFileVersion("7.9.0")] +[assembly: AssemblyInformationalVersion("7.9.0")] \ No newline at end of file diff --git a/src/Umbraco.Core/Configuration/UmbracoVersion.cs b/src/Umbraco.Core/Configuration/UmbracoVersion.cs index 178429d479..1e5da5f0b3 100644 --- a/src/Umbraco.Core/Configuration/UmbracoVersion.cs +++ b/src/Umbraco.Core/Configuration/UmbracoVersion.cs @@ -6,7 +6,7 @@ namespace Umbraco.Core.Configuration { public class UmbracoVersion { - private static readonly Version Version = new Version("7.8.0"); + private static readonly Version Version = new Version("7.9.0"); /// /// Gets the current version of Umbraco. diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index c933a6b115..b17ee5c61e 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -1024,9 +1024,9 @@ xcopy "$(ProjectDir)"..\packages\SqlServerCE.4.0.0.1\x86\*.* "$(TargetDir)x86\" True True - 7800 + 7900 / - http://localhost:7800 + http://localhost:7900 7790 / http://localhost:7790 From 1f6dea71779293d57ade2f2e572662efc7e5fc22 Mon Sep 17 00:00:00 2001 From: Sebastiaan Jansssen Date: Wed, 31 Jan 2018 12:47:41 +0100 Subject: [PATCH 5/9] Adds migration to create constent table --- .../AddUmbracoConsentTable.cs | 29 +++++++++++++++++++ src/Umbraco.Core/Umbraco.Core.csproj | 1 + 2 files changed, 30 insertions(+) create mode 100644 src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenNineZero/AddUmbracoConsentTable.cs diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenNineZero/AddUmbracoConsentTable.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenNineZero/AddUmbracoConsentTable.cs new file mode 100644 index 0000000000..bffa88eebd --- /dev/null +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenNineZero/AddUmbracoConsentTable.cs @@ -0,0 +1,29 @@ +using System.Linq; +using Umbraco.Core.Logging; +using Umbraco.Core.Models.Rdbms; +using Umbraco.Core.Persistence.SqlSyntax; + +namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenNineZero +{ + [Migration("7.9.0", 1, Constants.System.UmbracoMigrationName)] + public class AddUmbracoConsentTable : MigrationBase + { + public AddUmbracoConsentTable(ISqlSyntaxProvider sqlSyntax, ILogger logger) : base(sqlSyntax, logger) + { + } + + public override void Up() + { + var tables = SqlSyntax.GetTablesInSchema(Context.Database).ToArray(); + + if (tables.InvariantContains(ConsentDto.TableName)) + return; + + Create.Table(); + } + + public override void Down() + { + } + } +} diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index e406b1a96a..c8552baf9f 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -558,6 +558,7 @@ + From 118a83e56cfc8b5726f85933ab76fb83088cbd90 Mon Sep 17 00:00:00 2001 From: Sebastiaan Jansssen Date: Wed, 31 Jan 2018 13:19:30 +0100 Subject: [PATCH 6/9] Consent class should be public for 3rd parties to be able to register consents --- src/Umbraco.Core/Models/Consent.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Models/Consent.cs b/src/Umbraco.Core/Models/Consent.cs index 04e878e078..f59bc4de54 100644 --- a/src/Umbraco.Core/Models/Consent.cs +++ b/src/Umbraco.Core/Models/Consent.cs @@ -10,7 +10,7 @@ namespace Umbraco.Core.Models /// [Serializable] [DataContract(IsReference = true)] - internal class Consent : Entity, IConsent + public class Consent : Entity, IConsent { private static PropertySelectors _selector; From 971d7cf93351659438287fb4b6f89db25ce58108 Mon Sep 17 00:00:00 2001 From: Stephan Date: Wed, 31 Jan 2018 15:17:20 +0100 Subject: [PATCH 7/9] U4-10794 - refactor --- src/Umbraco.Core/Models/Consent.cs | 47 ++++-- src/Umbraco.Core/Models/ConsentExtensions.cs | 18 +++ src/Umbraco.Core/Models/ConsentState.cs | 2 +- src/Umbraco.Core/Models/IConsent.cs | 42 ++++-- src/Umbraco.Core/Models/Rdbms/ConsentDto.cs | 20 +-- .../Persistence/Factories/ConsentFactory.cs | 60 +++++--- .../Persistence/Mappers/ConsentMapper.cs | 5 +- .../Repositories/ConsentRepository.cs | 61 +++----- .../Repositories/IConsentRepository.cs | 7 +- src/Umbraco.Core/Services/ConsentService.cs | 106 ++++++------- src/Umbraco.Core/Services/IConsentService.cs | 64 ++++---- src/Umbraco.Core/Umbraco.Core.csproj | 1 + .../PropertyEditors/ImageCropperTest.cs | 10 +- .../Services/ConsentServiceTests.cs | 140 +++++++----------- 14 files changed, 300 insertions(+), 283 deletions(-) create mode 100644 src/Umbraco.Core/Models/ConsentExtensions.cs diff --git a/src/Umbraco.Core/Models/Consent.cs b/src/Umbraco.Core/Models/Consent.cs index f59bc4de54..bd0fb9b632 100644 --- a/src/Umbraco.Core/Models/Consent.cs +++ b/src/Umbraco.Core/Models/Consent.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Reflection; using System.Runtime.Serialization; using Umbraco.Core.Models.EntityBase; @@ -10,28 +11,37 @@ namespace Umbraco.Core.Models /// [Serializable] [DataContract(IsReference = true)] - public class Consent : Entity, IConsent + internal class Consent : Entity, IConsent { private static PropertySelectors _selector; + private bool _current; private string _source; + private string _context; private string _action; - private string _actionType; private ConsentState _state; private string _comment; // ReSharper disable once ClassNeverInstantiated.Local private class PropertySelectors { + public readonly PropertyInfo Current = ExpressionHelper.GetPropertyInfo(x => x.Current); public readonly PropertyInfo Source = ExpressionHelper.GetPropertyInfo(x => x.Source); + public readonly PropertyInfo Context = ExpressionHelper.GetPropertyInfo(x => x.Context); public readonly PropertyInfo Action = ExpressionHelper.GetPropertyInfo(x => x.Action); - public readonly PropertyInfo ActionType = ExpressionHelper.GetPropertyInfo(x => x.ActionType); public readonly PropertyInfo State = ExpressionHelper.GetPropertyInfo(x => x.State); public readonly PropertyInfo Comment = ExpressionHelper.GetPropertyInfo(x => x.Comment); } private static PropertySelectors Selectors => _selector ?? (_selector = new PropertySelectors()); + /// + public bool Current + { + get => _current; + set => SetPropertyValueAndDetectChanges(value, ref _current, Selectors.Current); + } + /// public string Source { @@ -43,6 +53,17 @@ namespace Umbraco.Core.Models } } + /// + public string Context + { + get => _context; + set + { + if (string.IsNullOrWhiteSpace(value)) throw new ArgumentException(nameof(value)); + SetPropertyValueAndDetectChanges(value, ref _context, Selectors.Context); + } + } + /// public string Action { @@ -54,17 +75,6 @@ namespace Umbraco.Core.Models } } - /// - public string ActionType - { - get => _actionType; - set - { - if (string.IsNullOrWhiteSpace(value)) throw new ArgumentException(nameof(value)); - SetPropertyValueAndDetectChanges(value, ref _actionType, Selectors.ActionType); - } - } - /// public ConsentState State { @@ -74,10 +84,19 @@ namespace Umbraco.Core.Models set => SetPropertyValueAndDetectChanges(value, ref _state, Selectors.State); } + /// public string Comment { get => _comment; set => SetPropertyValueAndDetectChanges(value, ref _comment, Selectors.Comment); } + + /// + public IEnumerable History => HistoryInternal; + + /// + /// Gets the previous states of this consent. + /// + public List HistoryInternal { get; set; } } } diff --git a/src/Umbraco.Core/Models/ConsentExtensions.cs b/src/Umbraco.Core/Models/ConsentExtensions.cs new file mode 100644 index 0000000000..fabeaf5809 --- /dev/null +++ b/src/Umbraco.Core/Models/ConsentExtensions.cs @@ -0,0 +1,18 @@ +namespace Umbraco.Core.Models +{ + /// + /// Provides extension methods for the interface. + /// + public static class ConsentExtensions + { + /// + /// Determines whether the consent is granted. + /// + public static bool IsGranted(this IConsent consent) => (consent.State & ConsentState.Granted) > 0; + + /// + /// Determines whether the consent is revoked. + /// + public static bool IsRevoked(this IConsent consent) => (consent.State & ConsentState.Revoked) > 0; + } +} diff --git a/src/Umbraco.Core/Models/ConsentState.cs b/src/Umbraco.Core/Models/ConsentState.cs index 460d01d0e7..ed370823f3 100644 --- a/src/Umbraco.Core/Models/ConsentState.cs +++ b/src/Umbraco.Core/Models/ConsentState.cs @@ -33,6 +33,6 @@ namespace Umbraco.Core.Models /// /// Consent has been revoked. /// - Revoked = 0x30000 + Revoked = 0x40000 } } diff --git a/src/Umbraco.Core/Models/IConsent.cs b/src/Umbraco.Core/Models/IConsent.cs index 4b62d11bee..7c06420206 100644 --- a/src/Umbraco.Core/Models/IConsent.cs +++ b/src/Umbraco.Core/Models/IConsent.cs @@ -1,41 +1,55 @@ -using Umbraco.Core.Models.EntityBase; +using System.Collections.Generic; +using Umbraco.Core.Models.EntityBase; namespace Umbraco.Core.Models { /// - /// Represents a consent. + /// Represents a consent state. /// + /// + /// A consent is fully identified by a source (whoever is consenting), a context (for + /// example, an application), and an action (whatever is consented). + /// A consent state registers the state of the consent (granted, revoked...). + /// public interface IConsent : IAggregateRoot, IRememberBeingDirty { /// - /// Gets or sets the unique identifier of whoever is consenting. + /// Determines whether the consent entity represents the current state. /// - /// Could be a Udi, or anything really. - string Source { get; set; } + bool Current { get; } /// - /// Gets or sets the Udi of the consented action. + /// Gets the unique identifier of whoever is consenting. /// - /// Could be a Udi, or anything really. - string Action { get; set; } + string Source { get; } /// - /// Gets the type of the consented action. + /// Gets the unique identifier of the context of the consent. /// /// /// Represents the domain, application, scope... of the action. /// When the action is a Udi, this should be the Udi type. /// - string ActionType { get; } + string Context { get; } /// - /// Gets or sets the state of the consent. + /// Gets the unique identifier of the consented action. /// - ConsentState State { get; set; } + string Action { get; } /// - /// Gets or sets some additional free text. + /// Gets the state of the consent. /// - string Comment { get; set; } + ConsentState State { get; } + + /// + /// Gets some additional free text. + /// + string Comment { get; } + + /// + /// Gets the previous states of this consent. + /// + IEnumerable History { get; } } } diff --git a/src/Umbraco.Core/Models/Rdbms/ConsentDto.cs b/src/Umbraco.Core/Models/Rdbms/ConsentDto.cs index 830d8f8d7d..0b6481070a 100644 --- a/src/Umbraco.Core/Models/Rdbms/ConsentDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/ConsentDto.cs @@ -16,22 +16,24 @@ namespace Umbraco.Core.Models.Rdbms [PrimaryKeyColumn] public int Id { get; set; } + [Column("current")] + public bool Current { get; set; } + [Column("source")] - [Length(512)] // aligned with Deploy SignatureDto - [Index(IndexTypes.UniqueNonClustered, ForColumns = "source, actionType, action", Name = "IX_" + TableName + "_UniqueSourceAction")] + [Length(512)] public string Source { get; set; } + [Column("context")] + [Length(128)] + public string Context { get; set; } + [Column("action")] - [Length(512)] // aligned with Deploy SignatureDto + [Length(512)] public string Action { get; set; } - [Column("actionType")] - [Length(128)] - public string ActionType { get; set; } - - [Column("updateDate")] + [Column("createDate")] [Constraint(Default = SystemMethods.CurrentDateTime)] - public DateTime UpdateDate { get; set; } + public DateTime CreateDate { get; set; } [Column("state")] public int State { get; set; } diff --git a/src/Umbraco.Core/Persistence/Factories/ConsentFactory.cs b/src/Umbraco.Core/Persistence/Factories/ConsentFactory.cs index 37689dfe3e..bf42193853 100644 --- a/src/Umbraco.Core/Persistence/Factories/ConsentFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/ConsentFactory.cs @@ -1,27 +1,50 @@ -using Umbraco.Core.Models; +using System.Collections.Generic; +using Umbraco.Core.Models; using Umbraco.Core.Models.Rdbms; namespace Umbraco.Core.Persistence.Factories { internal static class ConsentFactory { - public static IConsent BuildEntity(ConsentDto dto) + public static IEnumerable BuildEntities(IEnumerable dtos) { - var consent = new Consent - { - Id = dto.Id, - UpdateDate = dto.UpdateDate, - Source = dto.Source, - Action = dto.Action, - // ActionType derives from Action - State = (ConsentState) dto.State, // assume value is valid - Comment = dto.Comment - }; + var ix = new Dictionary(); + var output = new List(); - //on initial construction we don't want to have dirty properties tracked - // http://issues.umbraco.org/issue/U4-1946 - consent.ResetDirtyProperties(false); - return consent; + foreach (var dto in dtos) + { + var k = dto.Source + "::" + dto.Context + "::" + dto.Action; + + var consent = new Consent + { + Id = dto.Id, + Current = dto.Current, + CreateDate = dto.CreateDate, + Source = dto.Source, + Context = dto.Context, + Action = dto.Action, + State = (ConsentState) dto.State, // assume value is valid + Comment = dto.Comment + }; + + //on initial construction we don't want to have dirty properties tracked + // http://issues.umbraco.org/issue/U4-1946 + consent.ResetDirtyProperties(false); + + if (ix.TryGetValue(k, out var current)) + { + if (current.HistoryInternal == null) + current.HistoryInternal = new List(); + current.HistoryInternal.Add(consent); + } + else + { + ix[k] = consent; + output.Add(consent); + } + } + + return output; } public static ConsentDto BuildDto(IConsent entity) @@ -29,10 +52,11 @@ namespace Umbraco.Core.Persistence.Factories return new ConsentDto { Id = entity.Id, - UpdateDate = entity.UpdateDate, + Current = entity.Current, + CreateDate = entity.CreateDate, Source = entity.Source, + Context = entity.Context, Action = entity.Action, - ActionType = entity.ActionType, State = (int) entity.State, Comment = entity.Comment }; diff --git a/src/Umbraco.Core/Persistence/Mappers/ConsentMapper.cs b/src/Umbraco.Core/Persistence/Mappers/ConsentMapper.cs index a3f604c1ba..b608339cc9 100644 --- a/src/Umbraco.Core/Persistence/Mappers/ConsentMapper.cs +++ b/src/Umbraco.Core/Persistence/Mappers/ConsentMapper.cs @@ -28,10 +28,11 @@ namespace Umbraco.Core.Persistence.Mappers internal override void BuildMap() { CacheMap(entity => entity.Id, dto => dto.Id); - CacheMap(entity => entity.UpdateDate, dto => dto.UpdateDate); + CacheMap(entity => entity.Current, dto => dto.Current); + CacheMap(entity => entity.CreateDate, dto => dto.CreateDate); CacheMap(entity => entity.Source, dto => dto.Source); + CacheMap(entity => entity.Context, dto => dto.Context); CacheMap(entity => entity.Action, dto => dto.Action); - CacheMap(entity => entity.ActionType, dto => dto.ActionType); CacheMap(entity => entity.State, dto => dto.State); CacheMap(entity => entity.Comment, dto => dto.Comment); } diff --git a/src/Umbraco.Core/Persistence/Repositories/ConsentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ConsentRepository.cs index 2a653699ec..0485c76d62 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ConsentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ConsentRepository.cs @@ -27,73 +27,54 @@ namespace Umbraco.Core.Persistence.Repositories /// protected override Guid NodeObjectTypeId => throw new NotSupportedException(); + /// + public void ClearCurrent(string source, string context, string action) + { + var sql = new Sql($@"UPDATE {SqlSyntax.GetQuotedTableName(ConsentDto.TableName)} +SET {SqlSyntax.GetQuotedColumnName("current")} = @0 +WHERE {SqlSyntax.GetQuotedColumnName("source")} = @1 AND {SqlSyntax.GetQuotedColumnName("context")} = @2 AND {SqlSyntax.GetQuotedColumnName("action")} = @3 +AND {SqlSyntax.GetQuotedColumnName("current")} <> @0 ", false, source, context, action); + + Database.Execute(sql); + } + /// protected override IConsent PerformGet(int id) { - var sql = new Sql() - .Select("*") - .From(SqlSyntax) - .Where(x => x.Id == id, SqlSyntax); - - var dto = Database.FirstOrDefault(sql); - return dto == null ? null : ConsentFactory.BuildEntity(dto); + throw new NotImplementedException(); } /// protected override IEnumerable PerformGetAll(params int[] ids) { - if (ids.Length == 0) - { - var sql = new Sql() - .Select("*") - .From(SqlSyntax); - - return Database.Fetch(sql).Select(ConsentFactory.BuildEntity); - } - - var consents = new List(); - - foreach (var group in ids.InGroupsOf(2000)) - { - var sql = new Sql() - .Select("*") - .From(SqlSyntax) - .WhereIn(x => x.Id, group, SqlSyntax); - - consents.AddRange(Database.Fetch(sql).Select(ConsentFactory.BuildEntity)); - } - - return consents; + throw new NotImplementedException(); } /// protected override IEnumerable PerformGetByQuery(IQuery query) { - var sqlClause = GetBaseQuery(false); + var sqlClause = new Sql().Select("*").From(SqlSyntax); var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate(); - return Database.Fetch(sql).Select(ConsentFactory.BuildEntity); + var sql = translator.Translate().OrderByDescending(x => x.CreateDate, SqlSyntax); + return ConsentFactory.BuildEntities(Database.Fetch(sql)); } /// protected override Sql GetBaseQuery(bool isCount) { - return new Sql().Select(isCount ? "COUNT(*)" : "*").From(SqlSyntax); + throw new NotImplementedException(); } /// protected override string GetBaseWhereClause() { - return $"{ConsentDto.TableName}.id = @Id"; + throw new NotImplementedException(); } /// protected override IEnumerable GetDeleteClauses() { - return new[] - { - $"DELETE FROM {ConsentDto.TableName} WHERE id = @Id" - }; + throw new NotImplementedException(); } /// @@ -102,7 +83,7 @@ namespace Umbraco.Core.Persistence.Repositories ((Entity) entity).AddingEntity(); var dto = ConsentFactory.BuildDto(entity); - Database.Insert(dto); // table has a unique index on source+action + Database.Insert(dto); entity.Id = dto.Id; entity.ResetDirtyProperties(); } @@ -113,7 +94,7 @@ namespace Umbraco.Core.Persistence.Repositories ((Entity) entity).UpdatingEntity(); var dto = ConsentFactory.BuildDto(entity); - Database.Update(dto); // table has a unique index on source+action + Database.Update(dto); entity.ResetDirtyProperties(); IsolatedCache.ClearCacheItem(GetCacheIdKey(entity.Id)); diff --git a/src/Umbraco.Core/Persistence/Repositories/IConsentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IConsentRepository.cs index e49ad0bf76..9681bc575b 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IConsentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IConsentRepository.cs @@ -6,5 +6,10 @@ namespace Umbraco.Core.Persistence.Repositories /// Represents a repository for entities. /// public interface IConsentRepository : IRepositoryQueryable - { } + { + /// + /// Clears the current flag. + /// + void ClearCurrent(string source, string context, string action); + } } diff --git a/src/Umbraco.Core/Services/ConsentService.cs b/src/Umbraco.Core/Services/ConsentService.cs index 6d8b38164a..e36fda06c6 100644 --- a/src/Umbraco.Core/Services/ConsentService.cs +++ b/src/Umbraco.Core/Services/ConsentService.cs @@ -22,76 +22,60 @@ namespace Umbraco.Core.Services { } /// - public IConsent Get(int id) + public IConsent Register(string source, string context, string action, ConsentState state, string comment = null) { - using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) - { - var repository = RepositoryFactory.CreateConsentRepository(uow); - return repository.Get(id); - } - } + // prevent stupid states + var v = 0; + if ((state & ConsentState.Pending) > 0) v++; + if ((state & ConsentState.Granted) > 0) v++; + if ((state & ConsentState.Revoked) > 0) v++; + if (v != 1) + throw new ArgumentException("Invalid state.", nameof(state)); - /// - public IEnumerable GetBySource(string source, string actionType = null) - { - using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) + var consent = new Consent { - var repository = RepositoryFactory.CreateConsentRepository(uow); - var query = new Query().Where(x => x.Source == source); - if (string.IsNullOrWhiteSpace(actionType) == false) - query = query.Where(x => x.ActionType == actionType); - return repository.GetByQuery(query); - } - } + Current = true, + Source = source, + Context = context, + Action = action, + CreateDate = DateTime.Now, + State = state, + Comment = comment + }; - /// - public IEnumerable GetByAction(string action) - { - using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) - { - var repository = RepositoryFactory.CreateConsentRepository(uow); - var query = new Query().Where(x => x.Action == action); - return repository.GetByQuery(query); - } - } - - /// - public IEnumerable GetByActionType(string actionType) - { - using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) - { - var repository = RepositoryFactory.CreateConsentRepository(uow); - var query = new Query().Where(x => x.ActionType == actionType); - return repository.GetByQuery(query); - } - } - - /// - public void Save(IConsent consent) - { - try - { - using (var uow = UowProvider.GetUnitOfWork()) - { - var repository = RepositoryFactory.CreateConsentRepository(uow); - repository.AddOrUpdate(consent); - uow.Commit(); - } - } - catch (Exception e) - { - throw new Exception("Failed to save consent (see inner exception).", e); - } - } - - public void Delete(IConsent consent) - { using (var uow = UowProvider.GetUnitOfWork()) { var repository = RepositoryFactory.CreateConsentRepository(uow); - repository.Delete(consent); + repository.ClearCurrent(source, context, action); + repository.AddOrUpdate(consent); uow.Commit(); } + + return consent; + } + + /// + public IEnumerable Get(string source = null, string context = null, string action = null, + bool sourceStartsWith = false, bool contextStartsWith = false, bool actionStartsWith = false, + bool includeHistory = false) + { + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) + { + var repository = RepositoryFactory.CreateConsentRepository(uow); + + IQuery query = new Query(); + + if (string.IsNullOrWhiteSpace(source) == false) + query = sourceStartsWith ? query.Where(x => x.Source.StartsWith(source)) : query.Where(x => x.Source == source); + if (string.IsNullOrWhiteSpace(context) == false) + query = contextStartsWith ? query.Where(x => x.Context.StartsWith(context)) : query.Where(x => x.Context == context); + if (string.IsNullOrWhiteSpace(action) == false) + query = actionStartsWith ? query.Where(x => x.Action.StartsWith(action)) : query.Where(x => x.Action == action); + if (includeHistory == false) + query = query.Where(x => x.Current); + + return repository.GetByQuery(query); + } } } } diff --git a/src/Umbraco.Core/Services/IConsentService.cs b/src/Umbraco.Core/Services/IConsentService.cs index 420662165d..f077e75821 100644 --- a/src/Umbraco.Core/Services/IConsentService.cs +++ b/src/Umbraco.Core/Services/IConsentService.cs @@ -6,44 +6,40 @@ namespace Umbraco.Core.Services /// /// Represents a service for handling entities. /// + /// + /// Consent can be given or revoked or changed via the method, which + /// creates a new entity to track the consent. Revoking a consent is performed by + /// registering a revoked consent. + /// A consent can be revoked, by registering a revoked consent, but cannot be deleted. + /// Getter methods return the current state of a consent, i.e. the latest + /// entity that was created. + /// public interface IConsentService : IService { - // notes - // - // at the moment this is just channelling CRUD to the repository but - // the consent service should implement as much of the consenting - // logic as possible - // - // we might need some Get methods by State + /// + /// Registers consent. + /// + /// The source, i.e. whoever is consenting. + /// + /// + /// The state of the consent. + /// Additional free text. + /// The corresponding consent entity. + IConsent Register(string source, string context, string action, ConsentState state, string comment = null); /// - /// Gets the consent entity with the specified identifier. + /// Retrieves consents. /// - IConsent Get(int id); - - /// - /// Gets the consents of a source. - /// - IEnumerable GetBySource(string source, string actionType = null); - - /// - /// Gets the consents for an action. - /// - IEnumerable GetByAction(string action); - - /// - /// Gets the consents for an action type. - /// - IEnumerable GetByActionType(string actionType); - - /// - /// Saves a consent. - /// - void Save(IConsent consent); // fixme should it be an attempt? - - /// - /// Deletes a consent. - /// - void Delete(IConsent consent); + /// The optional source. + /// The optional context. + /// The optional action. + /// Determines whether is a start pattern. + /// Determines whether is a start pattern. + /// Determines whether is a start pattern. + /// Determines whether to include the history of consents. + /// Consents matching the paramters. + IEnumerable Get(string source = null, string context = null, string action = null, + bool sourceStartsWith = false, bool contextStartsWith = false, bool actionStartsWith = false, + bool includeHistory = false); } } diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index c8552baf9f..423166be56 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -365,6 +365,7 @@ + diff --git a/src/Umbraco.Tests/PropertyEditors/ImageCropperTest.cs b/src/Umbraco.Tests/PropertyEditors/ImageCropperTest.cs index 81f7ce30bd..d05862f2ad 100644 --- a/src/Umbraco.Tests/PropertyEditors/ImageCropperTest.cs +++ b/src/Umbraco.Tests/PropertyEditors/ImageCropperTest.cs @@ -21,7 +21,7 @@ namespace Umbraco.Tests.PropertyEditors [TestFixture] public class ImageCropperTest { - private const string cropperJson1 = "{\"focalPoint\": {\"left\": 0.96,\"top\": 0.80827067669172936},\"src\": \"/media/1005/img_0671.jpg\",\"crops\": [{\"alias\":\"thumb\",\"width\": 100,\"height\": 100,\"coordinates\": {\"x1\": 0.58729977382575338,\"y1\": 0.055768992440203169,\"x2\": 0,\"y2\": 0.32457553600198386}}]}"; + private const string cropperJson1 = "{\"focalPoint\": {\"left\": 0.96,\"top\": 0.80827067669172936},\"src\": \"/media/1005/img_0671.jpg\",\"crops\": [{\"alias\":\"thumb\",\"width\": 100,\"height\": 100,\"coordinates\": {\"x1\": 0.58729977382575338,\"y1\": 0.055768992440203169,\"x2\": 0,\"y2\": 0.32457553600198386}}]}"; private const string cropperJson2 = "{\"focalPoint\": {\"left\": 0.98,\"top\": 0.80827067669172936},\"src\": \"/media/1005/img_0672.jpg\",\"crops\": [{\"alias\":\"thumb\",\"width\": 100,\"height\": 100,\"coordinates\": {\"x1\": 0.58729977382575338,\"y1\": 0.055768992440203169,\"x2\": 0,\"y2\": 0.32457553600198386}}]}"; private const string cropperJson3 = "{\"focalPoint\": {\"left\": 0.98,\"top\": 0.80827067669172936},\"src\": \"/media/1005/img_0672.jpg\",\"crops\": []}"; private const string mediaPath = "/media/1005/img_0671.jpg"; @@ -56,7 +56,7 @@ namespace Umbraco.Tests.PropertyEditors index++; } - Assert.AreEqual(index, 1); + Assert.AreEqual(index, 1); } [Test] @@ -136,7 +136,7 @@ namespace Umbraco.Tests.PropertyEditors } [TestCase(cropperJson1, cropperJson1, true)] - [TestCase(cropperJson1, cropperJson2, false)] + [TestCase(cropperJson1, cropperJson2, false)] public void CanConvertImageCropperPropertyEditor(string val1, string val2, bool expected) { try @@ -266,7 +266,7 @@ namespace Umbraco.Tests.PropertyEditors var urlStringPad = mediaPath.GetCropUrl(imageCropperValue: cropperJson1, width: 300, height: 150, imageCropMode: ImageCropMode.Pad); var urlStringMax = mediaPath.GetCropUrl(imageCropperValue: cropperJson1, width: 300, height: 150, imageCropMode: ImageCropMode.Max); var urlStringStretch = mediaPath.GetCropUrl(imageCropperValue: cropperJson1, width: 300, height: 150, imageCropMode: ImageCropMode.Stretch); - + Assert.AreEqual(mediaPath + "?mode=min&width=300&height=150", urlStringMin); Assert.AreEqual(mediaPath + "?mode=boxpad&width=300&height=150", urlStringBoxPad); Assert.AreEqual(mediaPath + "?mode=pad&width=300&height=150", urlStringPad); @@ -380,4 +380,4 @@ namespace Umbraco.Tests.PropertyEditors Assert.AreEqual(mediaPath + "?mode=pad&width=400&height=400&bgcolor=fff", urlString); } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Tests/Services/ConsentServiceTests.cs b/src/Umbraco.Tests/Services/ConsentServiceTests.cs index b00b61e083..ac48cda415 100644 --- a/src/Umbraco.Tests/Services/ConsentServiceTests.cs +++ b/src/Umbraco.Tests/Services/ConsentServiceTests.cs @@ -10,7 +10,6 @@ namespace Umbraco.Tests.Services { [TestFixture] [DatabaseTestBehavior(DatabaseBehavior.NewDbFileAndSchemaPerFixture)] - [NUnit.Framework.Explicit("breaks everything!")] public class ConsentServiceTests : BaseServiceTest { [SetUp] @@ -31,123 +30,96 @@ namespace Umbraco.Tests.Services // fixme - why isn't this set by the test base class? Database.Mapper = new PetaPocoMapper(); - var consent = new Consent - { - Source = "user/1234", - Action = "app-actions/do-something", - ActionType = "app-actions", - State = ConsentState.Granted, - Comment = "no comment" - }; - var consentService = ServiceContext.ConsentService; - // can save - - consentService.Save(consent); + // can register + var consent = consentService.Register("user/1234", "app1", "do-something", ConsentState.Granted, "no comment"); Assert.AreNotEqual(0, consent.Id); - // can get + Assert.IsTrue(consent.Current); + Assert.AreEqual("user/1234", consent.Source); + Assert.AreEqual("app1", consent.Context); + Assert.AreEqual("do-something", consent.Action); + Assert.AreEqual(ConsentState.Granted, consent.State); + Assert.AreEqual("no comment", consent.Comment); - var consent2 = consentService.Get(consent.Id); + Assert.IsTrue(consent.IsGranted()); - Assert.AreEqual(consent.Source, consent2.Source); - Assert.AreEqual(consent.Action, consent2.Action); - Assert.AreEqual(consent.State, consent2.State); - Assert.AreEqual(consent.Comment, consent2.Comment); + // can register more - // can save more - - consentService.Save(new Consent - { - Source = "user/1234", - Action = "app-actions/do-something-else", - ActionType = "app-actions", - State = ConsentState.Granted, - Comment = "no comment" - }); - - consentService.Save(new Consent - { - Source = "user/1236", - Action = "app-actions/do-something", - ActionType = "app-actions", - State = ConsentState.Granted, - Comment = "no comment" - }); - - consentService.Save(new Consent - { - Source = "user/1237", - Action = "app2-actions/do-something", - ActionType = "app2-actions", - State = ConsentState.Granted, - Comment = "no comment" - }); + consentService.Register("user/1234", "app1", "do-something-else", ConsentState.Granted, "no comment"); + consentService.Register("user/1236", "app1", "do-something", ConsentState.Granted, "no comment"); + consentService.Register("user/1237", "app2", "do-something", ConsentState.Granted, "no comment"); // can get by source - var consents = consentService.GetBySource("user/1235").ToArray(); + var consents = consentService.Get(source: "user/1235").ToArray(); Assert.IsEmpty(consents); - consents = consentService.GetBySource("user/1234").ToArray(); + consents = consentService.Get(source: "user/1234").ToArray(); Assert.AreEqual(2, consents.Length); Assert.IsTrue(consents.All(x => x.Source == "user/1234")); - Assert.IsTrue(consents.Any(x => x.Action == "app-actions/do-something")); - Assert.IsTrue(consents.Any(x => x.Action == "app-actions/do-something-else")); + Assert.IsTrue(consents.Any(x => x.Action == "do-something")); + Assert.IsTrue(consents.Any(x => x.Action == "do-something-else")); + + // can get by context + + consents = consentService.Get(context: "app3").ToArray(); + Assert.IsEmpty(consents); + + consents = consentService.Get(context: "app2").ToArray(); + Assert.AreEqual(1, consents.Length); + + consents = consentService.Get(context: "app1").ToArray(); + Assert.AreEqual(3, consents.Length); + Assert.IsTrue(consents.Any(x => x.Action == "do-something")); + Assert.IsTrue(consents.Any(x => x.Action == "do-something-else")); // can get by action - consents = consentService.GetByAction("app-actions/do-whatever").ToArray(); + consents = consentService.Get(action: "do-whatever").ToArray(); Assert.IsEmpty(consents); - consents = consentService.GetByAction("app-actions/do-something").ToArray(); + consents = consentService.Get(context: "app1", action: "do-something").ToArray(); Assert.AreEqual(2, consents.Length); - Assert.IsTrue(consents.All(x => x.Action == "app-actions/do-something")); + Assert.IsTrue(consents.All(x => x.Action == "do-something")); Assert.IsTrue(consents.Any(x => x.Source == "user/1234")); Assert.IsTrue(consents.Any(x => x.Source == "user/1236")); - // can get by action type + // can revoke - consents = consentService.GetByActionType("app3-actions").ToArray(); - Assert.IsEmpty(consents); + consent = consentService.Register("user/1234", "app1", "do-something", ConsentState.Revoked, "no comment"); - consents = consentService.GetByActionType("app2-actions").ToArray(); + consents = consentService.Get(source: "user/1234", context: "app1", action: "do-something").ToArray(); Assert.AreEqual(1, consents.Length); + Assert.IsTrue(consents[0].Current); + Assert.AreEqual(ConsentState.Revoked, consents[0].State); - consents = consentService.GetByActionType("app-actions").ToArray(); + // can filter + + consents = consentService.Get(context: "app1", action: "do-", actionStartsWith: true).ToArray(); Assert.AreEqual(3, consents.Length); - Assert.IsTrue(consents.Any(x => x.Action == "app-actions/do-something")); - Assert.IsTrue(consents.Any(x => x.Action == "app-actions/do-something-else")); + Assert.IsTrue(consents.All(x => x.Context == "app1")); + Assert.IsTrue(consents.All(x => x.Action.StartsWith("do-"))); - // can delete + // can get history - consents = consentService.GetByActionType("app2-actions").ToArray(); + consents = consentService.Get(source: "user/1234", context: "app1", action: "do-something", includeHistory: true).ToArray(); Assert.AreEqual(1, consents.Length); - consentService.Delete(consents[0]); - consents = consentService.GetByActionType("app2-actions").ToArray(); - Assert.IsEmpty(consents); + Assert.IsTrue(consents[0].Current); + Assert.AreEqual(ConsentState.Revoked, consents[0].State); + Assert.IsTrue(consents[0].IsRevoked()); + Assert.IsNotNull(consents[0].History); + var history = consents[0].History.ToArray(); + Assert.AreEqual(1, history.Length); + Assert.IsFalse(history[0].Current); + Assert.AreEqual(ConsentState.Granted, history[0].State); - // can update + // cannot be stupid - var date = consent.UpdateDate; - consent.State = ConsentState.Revoked; - consentService.Save(consent); - Assert.AreNotEqual(date, consent.UpdateDate); - - // cannot create duplicates - - var consent3 = new Consent - { - Source = "user/1234", - Action = "app-actions/do-something", - ActionType = "app-actions", - State = ConsentState.Granted, - Comment = "no comment" - }; - - Assert.Throws(() => consentService.Save(consent3)); + Assert.Throws(() => + consentService.Register("user/1234", "app1", "do-something", ConsentState.Granted | ConsentState.Revoked, "no comment")); } } } From 8a0485e093ce5deed1a9c3566245bafbc835ea1b Mon Sep 17 00:00:00 2001 From: Sebastiaan Jansssen Date: Thu, 1 Feb 2018 13:07:02 +0100 Subject: [PATCH 8/9] Consent class really needs to be public --- src/Umbraco.Core/Models/Consent.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Models/Consent.cs b/src/Umbraco.Core/Models/Consent.cs index bd0fb9b632..7c78c8b52c 100644 --- a/src/Umbraco.Core/Models/Consent.cs +++ b/src/Umbraco.Core/Models/Consent.cs @@ -11,7 +11,7 @@ namespace Umbraco.Core.Models /// [Serializable] [DataContract(IsReference = true)] - internal class Consent : Entity, IConsent + public class Consent : Entity, IConsent { private static PropertySelectors _selector; From 1a289be99901089ee420b14f3cddc693efe18f55 Mon Sep 17 00:00:00 2001 From: Sebastiaan Jansssen Date: Thu, 1 Feb 2018 13:09:06 +0100 Subject: [PATCH 9/9] Whoops, I see what you did there, Consent can be internal indeed, cool! --- src/Umbraco.Core/Models/Consent.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Models/Consent.cs b/src/Umbraco.Core/Models/Consent.cs index 7c78c8b52c..bd0fb9b632 100644 --- a/src/Umbraco.Core/Models/Consent.cs +++ b/src/Umbraco.Core/Models/Consent.cs @@ -11,7 +11,7 @@ namespace Umbraco.Core.Models /// [Serializable] [DataContract(IsReference = true)] - public class Consent : Entity, IConsent + internal class Consent : Entity, IConsent { private static PropertySelectors _selector;