diff --git a/src/Umbraco.Core/Composing/LazyCollectionBuilderBase.cs b/src/Umbraco.Core/Composing/LazyCollectionBuilderBase.cs index 50d76b47a2..a1a06621e9 100644 --- a/src/Umbraco.Core/Composing/LazyCollectionBuilderBase.cs +++ b/src/Umbraco.Core/Composing/LazyCollectionBuilderBase.cs @@ -65,37 +65,6 @@ namespace Umbraco.Core.Composing return This; } - /// - /// Removes a type from the collection. - /// - /// The type to remove. - /// The builder. - public TBuilder Remove() - where T : TItem - { - Configure(types => - { - var type = typeof(T); - if (types.Contains(type)) types.Remove(type); - }); - return This; - } - - /// - /// Removes a type from the collection. - /// - /// The type to remove. - /// The builder. - public TBuilder Remove(Type type) - { - Configure(types => - { - EnsureType(type, "remove"); - if (types.Contains(type)) types.Remove(type); - }); - return This; - } - /// /// Adds a types producer to the collection. /// diff --git a/src/Umbraco.Core/Composing/TypeLoader.cs b/src/Umbraco.Core/Composing/TypeLoader.cs index 2a7bf69e8b..6a212a5a71 100644 --- a/src/Umbraco.Core/Composing/TypeLoader.cs +++ b/src/Umbraco.Core/Composing/TypeLoader.cs @@ -41,6 +41,16 @@ namespace Umbraco.Core.Composing private static LocalTempStorage _localTempStorage; private static string _fileBasePath; + /// + /// Initializes a new instance of the class. + /// + /// The application runtime cache. + /// Files storage mode. + /// A profiling logger. + public TypeLoader(IRuntimeCacheProvider runtimeCache, LocalTempStorage localTempStorage, ProfilingLogger logger) + : this(runtimeCache, localTempStorage, logger, true) + { } + /// /// Initializes a new instance of the class. /// @@ -48,7 +58,7 @@ namespace Umbraco.Core.Composing /// Files storage mode. /// A profiling logger. /// Whether to detect changes using hashes. - internal TypeLoader(IRuntimeCacheProvider runtimeCache, LocalTempStorage localTempStorage, IProfilingLogger logger, bool detectChanges = true) + internal TypeLoader(IRuntimeCacheProvider runtimeCache, LocalTempStorage localTempStorage, ProfilingLogger logger, bool detectChanges) { _runtimeCache = runtimeCache ?? throw new ArgumentNullException(nameof(runtimeCache)); _localTempStorage = localTempStorage == LocalTempStorage.Unknown ? LocalTempStorage.Default : localTempStorage; @@ -405,39 +415,6 @@ namespace Umbraco.Core.Composing } } - //private string GetFilePath(string extension) - //{ - // string path; - // switch (_globalSettings.LocalTempStorageLocation) - // { - // case LocalTempStorage.AspNetTemp: - // path = Path.Combine(HttpRuntime.CodegenDir, "UmbracoData", "umbraco-types." + extension); - // break; - // case LocalTempStorage.EnvironmentTemp: - // // include the appdomain hash is just a safety check, for example if a website is moved from worker A to worker B and then back - // // to worker A again, in theory the %temp% folder should already be empty but we really want to make sure that its not - // // utilizing an old path - assuming we cannot have SHA1 collisions on AppDomainAppId - // var appDomainHash = HttpRuntime.AppDomainAppId.ToSHA1(); - // var cachePath = Path.Combine(Environment.ExpandEnvironmentVariables("%temp%"), "UmbracoData", appDomainHash); - // path = Path.Combine(cachePath, "umbraco-types." + extension); - // break; - // case LocalTempStorage.Default: - // default: - // var tempFolder = IOHelper.MapPath("~/App_Data/TEMP/TypesCache"); - // path = Path.Combine(tempFolder, "umbraco-types." + NetworkHelper.FileSafeMachineName + "." + extension); - // break; - // } - - // // ensure that the folder exists - // var directory = Path.GetDirectoryName(path); - // if (directory == null) - // throw new InvalidOperationException($"Could not determine folder for file \"{path}\"."); - // if (Directory.Exists(directory) == false) - // Directory.CreateDirectory(directory); - - // return path; - //} - // internal for tests internal void WriteCache() { diff --git a/src/Umbraco.Core/IO/IOHelper.cs b/src/Umbraco.Core/IO/IOHelper.cs index 2f1675e08a..7773f378a5 100644 --- a/src/Umbraco.Core/IO/IOHelper.cs +++ b/src/Umbraco.Core/IO/IOHelper.cs @@ -284,11 +284,12 @@ namespace Umbraco.Core.IO binFolder = Path.Combine(GetRootDirectorySafe(), "bin"); -#if DEBUG + // do this all the time (no #if DEBUG) because Umbraco release + // can be used in tests by an app (eg Deploy) being debugged var debugFolder = Path.Combine(binFolder, "debug"); if (Directory.Exists(debugFolder)) return debugFolder; -#endif + var releaseFolder = Path.Combine(binFolder, "release"); if (Directory.Exists(releaseFolder)) return releaseFolder; diff --git a/src/Umbraco.Core/Logging/Serilog/SerilogLogger.cs b/src/Umbraco.Core/Logging/Serilog/SerilogLogger.cs index a7c3f1fab0..3fd6d18278 100644 --- a/src/Umbraco.Core/Logging/Serilog/SerilogLogger.cs +++ b/src/Umbraco.Core/Logging/Serilog/SerilogLogger.cs @@ -203,7 +203,6 @@ namespace Umbraco.Core.Logging.Serilog private static bool IsTimeoutThreadAbortException(Exception exception) { if (!(exception is ThreadAbortException abort)) return false; - if (abort.ExceptionState == null) return false; var stateType = abort.ExceptionState.GetType(); diff --git a/src/Umbraco.Core/Migrations/Expressions/Execute/ExecuteBuilder.cs b/src/Umbraco.Core/Migrations/Expressions/Execute/ExecuteBuilder.cs index 0637d2e597..0ba2499c44 100644 --- a/src/Umbraco.Core/Migrations/Expressions/Execute/ExecuteBuilder.cs +++ b/src/Umbraco.Core/Migrations/Expressions/Execute/ExecuteBuilder.cs @@ -1,6 +1,7 @@ using NPoco; using Umbraco.Core.Migrations.Expressions.Common; using Umbraco.Core.Migrations.Expressions.Execute.Expressions; +using Umbraco.Core.Persistence; namespace Umbraco.Core.Migrations.Expressions.Execute { @@ -12,7 +13,16 @@ namespace Umbraco.Core.Migrations.Expressions.Execute { } /// - public void Do() => Expression.Execute(); + public void Do() + { + // slightly awkward, but doing it right would mean a *lot* + // of changes for MigrationExpressionBase + + if (Expression.SqlObject == null) + Expression.Execute(); + else + Expression.ExecuteSqlObject(); + } /// public IExecutableBuilder Sql(string sqlStatement) @@ -20,5 +30,12 @@ namespace Umbraco.Core.Migrations.Expressions.Execute Expression.SqlStatement = sqlStatement; return this; } + + /// + public IExecutableBuilder Sql(Sql sql) + { + Expression.SqlObject = sql; + return this; + } } } diff --git a/src/Umbraco.Core/Migrations/Expressions/Execute/Expressions/ExecuteSqlStatementExpression.cs b/src/Umbraco.Core/Migrations/Expressions/Execute/Expressions/ExecuteSqlStatementExpression.cs index b5c1fbdf6b..8b5da4f270 100644 --- a/src/Umbraco.Core/Migrations/Expressions/Execute/Expressions/ExecuteSqlStatementExpression.cs +++ b/src/Umbraco.Core/Migrations/Expressions/Execute/Expressions/ExecuteSqlStatementExpression.cs @@ -1,4 +1,5 @@ using NPoco; +using Umbraco.Core.Persistence; namespace Umbraco.Core.Migrations.Expressions.Execute.Expressions { @@ -10,6 +11,13 @@ namespace Umbraco.Core.Migrations.Expressions.Execute.Expressions public virtual string SqlStatement { get; set; } + public virtual Sql SqlObject { get; set; } + + public void ExecuteSqlObject() + { + Execute(SqlObject); + } + protected override string GetSql() { return SqlStatement; diff --git a/src/Umbraco.Core/Migrations/Expressions/Execute/IExecuteBuilder.cs b/src/Umbraco.Core/Migrations/Expressions/Execute/IExecuteBuilder.cs index 5747eb2c1a..7f575fd3f8 100644 --- a/src/Umbraco.Core/Migrations/Expressions/Execute/IExecuteBuilder.cs +++ b/src/Umbraco.Core/Migrations/Expressions/Execute/IExecuteBuilder.cs @@ -1,4 +1,6 @@ -using Umbraco.Core.Migrations.Expressions.Common; +using NPoco; +using Umbraco.Core.Migrations.Expressions.Common; +using Umbraco.Core.Persistence; namespace Umbraco.Core.Migrations.Expressions.Execute { @@ -12,5 +14,10 @@ namespace Umbraco.Core.Migrations.Expressions.Execute /// Specifies the Sql statement to execute. /// IExecutableBuilder Sql(string sqlStatement); + + /// + /// Specifies the Sql statement to execute. + /// + IExecutableBuilder Sql(Sql sql); } } diff --git a/src/Umbraco.Core/Migrations/Install/DatabaseBuilder.cs b/src/Umbraco.Core/Migrations/Install/DatabaseBuilder.cs index 341469a696..e2c34c31cb 100644 --- a/src/Umbraco.Core/Migrations/Install/DatabaseBuilder.cs +++ b/src/Umbraco.Core/Migrations/Install/DatabaseBuilder.cs @@ -33,6 +33,9 @@ namespace Umbraco.Core.Migrations.Install private DatabaseSchemaResult _databaseSchemaValidationResult; + /// + /// Initializes a new instance of the class. + /// public DatabaseBuilder(IScopeProvider scopeProvider, IGlobalSettings globalSettings, IUmbracoDatabaseFactory databaseFactory, IRuntimeState runtime, ILogger logger, IMigrationBuilder migrationBuilder, IKeyValueService keyValueService, PostMigrationCollection postMigrations) { _scopeProvider = scopeProvider; @@ -49,23 +52,20 @@ namespace Umbraco.Core.Migrations.Install /// /// Gets a value indicating whether the database is configured. It does not necessarily - /// mean that it is possible to connect, nor that Umbraco is installed, nor - /// up-to-date. + /// mean that it is possible to connect, nor that Umbraco is installed, nor up-to-date. /// public bool IsDatabaseConfigured => _databaseFactory.Configured; /// - /// Gets a value indicating whether it is possible to connect to the database. + /// Gets a value indicating whether it is possible to connect to the configured database. + /// It does not necessarily mean that Umbraco is installed, nor up-to-date. /// - public bool CanConnect => _databaseFactory.CanConnect; + public bool CanConnectToDatabase => _databaseFactory.CanConnect; - // that method was originally created by Per in DatabaseHelper- tests the db connection for install - // fixed by Shannon to not-ignore the provider - // fixed by Stephan as part of the v8 persistence cleanup, now using provider names + SqlCe exception - // moved by Stephan to DatabaseBuilder - // probably needs to be cleaned up - - public bool CheckConnection(string databaseType, string connectionString, string server, string database, string login, string password, bool integratedAuth) + /// + /// Verifies whether a it is possible to connect to a database. + /// + public bool CanConnect(string databaseType, string connectionString, string server, string database, string login, string password, bool integratedAuth) { // we do not test SqlCE connection if (databaseType.InvariantContains("sqlce")) @@ -93,7 +93,7 @@ namespace Umbraco.Core.Migrations.Install return DbConnectionExtensions.IsConnectionAvailable(connectionString, providerName); } - public bool HasSomeNonDefaultUser() + internal bool HasSomeNonDefaultUser() { using (var scope = _scopeProvider.CreateScope()) { @@ -371,17 +371,24 @@ namespace Umbraco.Core.Migrations.Install #region Database Schema - internal DatabaseSchemaResult ValidateDatabaseSchema() + /// + /// Validates the database schema. + /// + /// + /// This assumes that the database exists and the connection string is + /// configured and it is possible to connect to the database. + /// + internal DatabaseSchemaResult ValidateSchema() { using (var scope = _scopeProvider.CreateScope()) { - var result = ValidateDatabaseSchema(scope); + var result = ValidateSchema(scope); scope.Complete(); return result; } } - private DatabaseSchemaResult ValidateDatabaseSchema(IScope scope) + private DatabaseSchemaResult ValidateSchema(IScope scope) { if (_databaseFactory.Configured == false) return new DatabaseSchemaResult(_databaseFactory.SqlContext.SqlSyntax); @@ -396,17 +403,24 @@ namespace Umbraco.Core.Migrations.Install return _databaseSchemaValidationResult; } - internal Result CreateDatabaseSchemaAndData() + /// + /// Creates the database schema and inserts initial data. + /// + /// + /// This assumes that the database exists and the connection string is + /// configured and it is possible to connect to the database. + /// + public Result CreateSchemaAndData() { using (var scope = _scopeProvider.CreateScope()) { - var result = CreateDatabaseSchemaAndData(scope); + var result = CreateSchemaAndData(scope); scope.Complete(); return result; } } - private Result CreateDatabaseSchemaAndData(IScope scope) + private Result CreateSchemaAndData(IScope scope) { try { @@ -422,28 +436,14 @@ namespace Umbraco.Core.Migrations.Install // If MySQL, we're going to ensure that database calls are maintaining proper casing as to remove the necessity for checks // for case insensitive queries. In an ideal situation (which is what we're striving for), all calls would be case sensitive. - - /* - var supportsCaseInsensitiveQueries = SqlSyntax.SupportsCaseInsensitiveQueries(database); - if (supportsCaseInsensitiveQueries == false) - { - message = "

 

The database you're trying to use does not support case insensitive queries.
We currently do not support these types of databases.

" + - "

You can fix this by changing the following setting in your my.ini file in your MySQL installation directory:

" + - "
lower_case_table_names=1

" + - "

Note: Make sure to check with your hosting provider if they support case insensitive queries as well.

" + - "

For more technical information on case sensitivity in MySQL, have a look at " + - "the documentation on the subject

"; - - return new Result { Message = message, Success = false, Percentage = "15" }; - } - */ - - var message = GetResultMessageForMySql(); - var schemaResult = ValidateDatabaseSchema(); - var installedSchemaVersion = schemaResult.DetermineInstalledVersion(); + var message = database.DatabaseType.IsMySql() ? ResultMessageForMySql : ""; + var schemaResult = ValidateSchema(); + var hasInstalledVersion = schemaResult.DetermineHasInstalledVersion(); + //var installedSchemaVersion = schemaResult.DetermineInstalledVersion(); + //var hasInstalledVersion = !installedSchemaVersion.Equals(new Version(0, 0, 0)); //If Configuration Status is empty and the determined version is "empty" its a new install - otherwise upgrade the existing - if (string.IsNullOrEmpty(_globalSettings.ConfigurationStatus) && installedSchemaVersion.Equals(new Version(0, 0, 0))) + if (string.IsNullOrEmpty(_globalSettings.ConfigurationStatus) && !hasInstalledVersion) { if (_runtime.Level == RuntimeLevel.Run) throw new Exception("Umbraco is already configured!"); @@ -475,8 +475,15 @@ namespace Umbraco.Core.Migrations.Install } } - // This assumes all of the previous checks are done! - internal Result UpgradeSchemaAndData() + /// + /// Upgrades the database schema and data by running migrations. + /// + /// + /// This assumes that the database exists and the connection string is + /// configured and it is possible to connect to the database. + /// Runs whichever migrations need to run. + /// + public Result UpgradeSchemaAndData() { try { @@ -488,56 +495,11 @@ namespace Umbraco.Core.Migrations.Install _logger.Info("Database upgrade started"); - //var database = scope.Database; - //var supportsCaseInsensitiveQueries = SqlSyntax.SupportsCaseInsensitiveQueries(database); - - var message = GetResultMessageForMySql(); - - // fixme - remove this code - //var schemaResult = ValidateDatabaseSchema(); - // - //var installedSchemaVersion = new SemVersion(schemaResult.DetermineInstalledVersion()); - //var installedMigrationVersion = schemaResult.DetermineInstalledVersionByMigrations(migrationEntryService); - //var targetVersion = UmbracoVersion.Current; - // - ////In some cases - like upgrading from 7.2.6 -> 7.3, there will be no migration information in the database and therefore it will - //// return a version of 0.0.0 and we don't necessarily want to run all migrations from 0 -> 7.3, so we'll just ensure that the - //// migrations are run for the target version - //if (installedMigrationVersion == new SemVersion(new Version(0, 0, 0)) && installedSchemaVersion > new SemVersion(new Version(0, 0, 0))) - //{ - // //set the installedMigrationVersion to be one less than the target so the latest migrations are guaranteed to execute - // installedMigrationVersion = new SemVersion(targetVersion.SubtractRevision()); - //} - // - ////Figure out what our current installed version is. If the web.config doesn't have a version listed, then we'll use the minimum - //// version detected between the schema installed and the migrations listed in the migration table. - //// If there is a version in the web.config, we'll take the minimum between the listed migration in the db and what - //// is declared in the web.config. - // - //var currentInstalledVersion = string.IsNullOrEmpty(GlobalSettings.ConfigurationStatus) - // //Take the minimum version between the detected schema version and the installed migration version - // ? new[] { installedSchemaVersion, installedMigrationVersion }.Min() - // //Take the minimum version between the installed migration version and the version specified in the config - // : new[] { SemVersion.Parse(GlobalSettings.ConfigurationStatus), installedMigrationVersion }.Min(); - // - ////Ok, another edge case here. If the current version is a pre-release, - //// then we want to ensure all migrations for the current release are executed. - //if (currentInstalledVersion.Prerelease.IsNullOrWhiteSpace() == false) - //{ - // currentInstalledVersion = new SemVersion(currentInstalledVersion.GetVersion().SubtractRevision()); - //} + var message = _scopeProvider.SqlContext.DatabaseType.IsMySql() ? ResultMessageForMySql : ""; // upgrade - var upgrader = new UmbracoUpgrader(_scopeProvider, _migrationBuilder, _keyValueService, _postMigrations, _logger); - upgrader.Execute(); - - // fixme remove this code - //var runner = new MigrationRunner(_scopeProvider, builder, migrationEntryService, _logger, currentInstalledVersion, UmbracoVersion.SemanticVersion, Constants.System.UmbracoMigrationName); - //var upgraded = runner.Execute(/*upgrade:true*/); - //if (upgraded == false) - //{ - // throw new ApplicationException("Upgrading failed, either an error occurred during the upgrade process or an event canceled the upgrade process, see log for full details"); - //} + var upgrader = new UmbracoUpgrader(); + upgrader.Execute(_scopeProvider, _migrationBuilder, _keyValueService, _logger, _postMigrations); message = message + "

Upgrade completed!

"; @@ -553,47 +515,14 @@ namespace Umbraco.Core.Migrations.Install } } - private string GetResultMessageForMySql() - { - if (_databaseFactory.GetType() == typeof(MySqlSyntaxProvider)) - { - return "

 

Congratulations, the database step ran successfully!

" + - "

Note: You're using MySQL and the database instance you're connecting to seems to support case insensitive queries.

" + - "

However, your hosting provider may not support this option. Umbraco does not currently support MySQL installs that do not support case insensitive queries

" + - "

Make sure to check with your hosting provider if they support case insensitive queries as well.

" + - "

They can check this by looking for the following setting in the my.ini file in their MySQL installation directory:

" + - "
lower_case_table_names=1

" + - "

For more technical information on case sensitivity in MySQL, have a look at " + - "the documentation on the subject

"; - } - return string.Empty; - } - - /* - private string GetResultMessageForMySql(bool? supportsCaseInsensitiveQueries) - { - if (supportsCaseInsensitiveQueries == null) - { - return "

 

Warning! Could not check if your database type supports case insensitive queries.
We currently do not support these databases that do not support case insensitive queries.

" + - "

You can check this by looking for the following setting in your my.ini file in your MySQL installation directory:

" + - "
lower_case_table_names=1

" + - "

Note: Make sure to check with your hosting provider if they support case insensitive queries as well.

" + - "

For more technical information on case sensitivity in MySQL, have a look at " + - "the documentation on the subject

"; - } - if (SqlSyntax.GetType() == typeof(MySqlSyntaxProvider)) - { - return "

 

Congratulations, the database step ran successfully!

" + - "

Note: You're using MySQL and the database instance you're connecting to seems to support case insensitive queries.

" + - "

However, your hosting provider may not support this option. Umbraco does not currently support MySQL installs that do not support case insensitive queries

" + - "

Make sure to check with your hosting provider if they support case insensitive queries as well.

" + - "

They can check this by looking for the following setting in the my.ini file in their MySQL installation directory:

" + - "
lower_case_table_names=1

" + - "

For more technical information on case sensitivity in MySQL, have a look at " + - "the documentation on the subject

"; - } - return string.Empty; - }*/ + private const string ResultMessageForMySql = "

 

Congratulations, the database step ran successfully!

" + + "

Note: You're using MySQL and the database instance you're connecting to seems to support case insensitive queries.

" + + "

However, your hosting provider may not support this option. Umbraco does not currently support MySQL installs that do not support case insensitive queries

" + + "

Make sure to check with your hosting provider if they support case insensitive queries as well.

" + + "

They can check this by looking for the following setting in the my.ini file in their MySQL installation directory:

" + + "
lower_case_table_names=1

" + + "

For more technical information on case sensitivity in MySQL, have a look at " + + "the documentation on the subject

"; private Attempt CheckReadyForInstall() { @@ -629,11 +558,29 @@ namespace Umbraco.Core.Migrations.Install }; } - internal class Result + /// + /// Represents the result of a database creation or upgrade. + /// + public class Result { + /// + /// Gets or sets a value indicating whether an upgrade is required. + /// public bool RequiresUpgrade { get; set; } + + /// + /// Gets or sets the message returned by the operation. + /// public string Message { get; set; } + + /// + /// Gets or sets a value indicating whether the operation succeeded. + /// public bool Success { get; set; } + + /// + /// Gets or sets an install progress pseudo-percentage. + /// public string Percentage { get; set; } } diff --git a/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs b/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs index eb7cafcb01..f32ea1cb6f 100644 --- a/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs +++ b/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs @@ -321,9 +321,9 @@ namespace Umbraco.Core.Migrations.Install { // on install, initialize the umbraco migration plan with the final state - var plan = new UmbracoPlan(); - var stateValueKey = Upgrader.GetStateValueKey(plan); - var finalState = plan.FinalState; + var upgrader = new UmbracoUpgrader(); + var stateValueKey = upgrader.StateValueKey; + var finalState = upgrader.Plan.FinalState; _database.Insert(Constants.DatabaseSchema.Tables.KeyValue, "key", false, new KeyValueDto { Key = stateValueKey, Value = finalState, Updated = DateTime.Now }); } diff --git a/src/Umbraco.Core/Migrations/Install/DatabaseSchemaCreator.cs b/src/Umbraco.Core/Migrations/Install/DatabaseSchemaCreator.cs index 042c6117aa..5525cc4a50 100644 --- a/src/Umbraco.Core/Migrations/Install/DatabaseSchemaCreator.cs +++ b/src/Umbraco.Core/Migrations/Install/DatabaseSchemaCreator.cs @@ -142,9 +142,8 @@ namespace Umbraco.Core.Migrations.Install { var result = new DatabaseSchemaResult(SqlSyntax); - //get the db index defs - result.DbIndexDefinitions = SqlSyntax.GetDefinedIndexes(_database) - .Select(x => new DbIndexDefinition(x)).ToArray(); + result.IndexDefinitions.AddRange(SqlSyntax.GetDefinedIndexes(_database) + .Select(x => new DbIndexDefinition(x))); result.TableDefinitions.AddRange(OrderedTables .Select(x => DefinitionFactory.GetTableDefinition(x, SqlSyntax))); @@ -283,7 +282,7 @@ namespace Umbraco.Core.Migrations.Install { //These are just column indexes NOT constraints or Keys //var colIndexesInDatabase = result.DbIndexDefinitions.Where(x => x.IndexName.InvariantStartsWith("IX_")).Select(x => x.IndexName).ToList(); - var colIndexesInDatabase = result.DbIndexDefinitions.Select(x => x.IndexName).ToList(); + var colIndexesInDatabase = result.IndexDefinitions.Select(x => x.IndexName).ToList(); var indexesInSchema = result.TableDefinitions.SelectMany(x => x.Indexes.Select(y => y.Name)).ToList(); //Add valid and invalid index differences to the result object diff --git a/src/Umbraco.Core/Migrations/Install/DatabaseSchemaResult.cs b/src/Umbraco.Core/Migrations/Install/DatabaseSchemaResult.cs index 0ec27cf0b1..4c68addebc 100644 --- a/src/Umbraco.Core/Migrations/Install/DatabaseSchemaResult.cs +++ b/src/Umbraco.Core/Migrations/Install/DatabaseSchemaResult.cs @@ -2,153 +2,55 @@ using System.Collections.Generic; using System.Linq; using System.Text; -using Umbraco.Core.Configuration; using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Migrations.Install { - public class DatabaseSchemaResult + /// + /// Represents ... + /// + internal class DatabaseSchemaResult { - private readonly ISqlSyntaxProvider _sqlSyntax; + private readonly bool _isMySql; public DatabaseSchemaResult(ISqlSyntaxProvider sqlSyntax) { - _sqlSyntax = sqlSyntax; + _isMySql = sqlSyntax is MySqlSyntaxProvider; + Errors = new List>(); TableDefinitions = new List(); ValidTables = new List(); ValidColumns = new List(); ValidConstraints = new List(); ValidIndexes = new List(); + IndexDefinitions = new List(); } - public List> Errors { get; set; } + public List> Errors { get; } - public List TableDefinitions { get; set; } + public List TableDefinitions { get; } - public List ValidTables { get; set; } + // fixme TableDefinitions are those that should be there, IndexDefinitions are those that... are in DB? + internal List IndexDefinitions { get; } - public List ValidColumns { get; set; } + public List ValidTables { get; } - public List ValidConstraints { get; set; } + public List ValidColumns { get; } - public List ValidIndexes { get; set; } + public List ValidConstraints { get; } - internal IEnumerable DbIndexDefinitions { get; set; } + public List ValidIndexes { get; } /// - /// Determines the version of the currently installed database by detecting the current database structure + /// Determines whether the database contains an installed version. /// - /// - /// A with Major and Minor values for - /// non-empty database, otherwise "0.0.0" for empty databases. - /// - public Version DetermineInstalledVersion() + /// + /// A database contains an installed version when it contains at least one valid table. + /// + public bool DetermineHasInstalledVersion() { - // v8 = kill versions older than 7 - - //If (ValidTables.Count == 0) database is empty and we return -> new Version(0, 0, 0); - if (ValidTables.Count == 0) - return new Version(0, 0, 0); - - // FIXME - but the whole detection is borked really - return new Version(8, 0, 0); - - //If Errors is empty or if TableDefinitions tables + columns correspond to valid tables + columns then we're at current version - if (Errors.Any() == false || - (TableDefinitions.All(x => ValidTables.Contains(x.Name)) - && TableDefinitions.SelectMany(definition => definition.Columns).All(x => ValidColumns.Contains(x.Name)))) - return UmbracoVersion.Current; - - //If Errors contains umbracoApp or umbracoAppTree its pre-6.0.0 -> new Version(4, 10, 0); - if (Errors.Any(x => x.Item1.Equals("Table") && (x.Item2.InvariantEquals("umbracoApp") || x.Item2.InvariantEquals("umbracoAppTree")))) - { - //If Errors contains umbracoUser2app or umbracoAppTree foreignkey to umbracoApp exists its pre-4.8.0 -> new Version(4, 7, 0); - if (Errors.Any(x => - x.Item1.Equals("Constraint") - && (x.Item2.InvariantContains("umbracoUser2app_umbracoApp") - || x.Item2.InvariantContains("umbracoAppTree_umbracoApp")))) - { - return new Version(4, 7, 0); - } - - return new Version(4, 8, 0); - } - - //if the error is for umbracoServer - if (Errors.Any(x => x.Item1.Equals("Table") && (x.Item2.InvariantEquals("umbracoServer")))) - { - return new Version(6, 0, 0); - } - - //if the error indicates a problem with the column cmsMacroProperty.macroPropertyType then it is not version 7 - // since these columns get removed in v7 - if (Errors.Any(x => x.Item1.Equals("Column") && (x.Item2.InvariantEquals("cmsMacroProperty,macroPropertyType")))) - { - //if the error is for this IX_umbracoNodeTrashed which is added in 6.2 AND in 7.1 but we do not have the above columns - // then it must mean that we aren't on 6.2 so must be 6.1 - if (Errors.Any(x => x.Item1.Equals("Index") && (x.Item2.InvariantEquals("IX_umbracoNodeTrashed")))) - { - return new Version(6, 1, 0); - } - else - { - //if there are no errors for that index, then the person must have 6.2 installed - return new Version(6, 2, 0); - } - } - - //if the error indicates a problem with the constraint FK_cms-OBSOLETE-Content_cmsContentType_nodeId then it is not version 7.2 - // since this gets added in 7.2.0 so it must be the previous version - if (Errors.Any(x => x.Item1.Equals("Constraint") && (x.Item2.InvariantEquals("FK_cms-OBSOLETE-Content_cmsContentType_nodeId")))) - { - return new Version(7, 0, 0); - } - - //if the error is for umbracoAccess it must be the previous version to 7.3 since that is when it is added - if (Errors.Any(x => x.Item1.Equals("Table") && (x.Item2.InvariantEquals("umbracoAccess")))) - { - return new Version(7, 2, 0); - } - - //if the error is for cms-OBSOLETE-PropertyData.dataDecimal it must be the previous version to 7.4 since that is when it is added - if (Errors.Any(x => x.Item1.Equals("Column") && (x.Item2.InvariantEquals("cms-OBSOLETE-PropertyData,dataDecimal")))) - { - return new Version(7, 3, 0); - } - - //if the error is for umbracoRedirectUrl it must be the previous version to 7.5 since that is when it is added - if (Errors.Any(x => x.Item1.Equals("Table") && (x.Item2.InvariantEquals("umbracoRedirectUrl")))) - { - return new Version(7, 4, 0); - } - - //if the error indicates a problem with the column cmsMacroProperty.uniquePropertyId then it is not version 7.6 since that is when it is added - if (Errors.Any(x => x.Item1.Equals("Column") && (x.Item2.InvariantEquals("cmsMacroProperty,uniquePropertyId")))) - { - return new Version(7, 5, 0); - } - - //if the error is for umbracoUserGroup it must be the previous version to 7.7 since that is when it is added - if (Errors.Any(x => x.Item1.Equals("Table") && (x.Item2.InvariantEquals("umbracoUserStartNode")))) - { - return new Version(7, 6, 0); - } - - //if the error is for cmsMedia it must be the previous version to 7.8 since that is when it is added - if (Errors.Any(x => x.Item1.Equals("Table") && (x.Item2.InvariantEquals("umbracoMedia")))) - { - return new Version(7, 7, 0); - } - - //if the error is for isSensitive column it must be the previous version to 7.9 since that is when it is added - if (Errors.Any(x => x.Item1.Equals("Column") && (x.Item2.InvariantEquals("cmsMemberType,isSensitive")))) - { - return new Version(7, 8, 0); - } - - return UmbracoVersion.Current; + return ValidTables.Count > 0; } /// @@ -200,9 +102,9 @@ namespace Umbraco.Core.Migrations.Install sb.AppendLine(" "); } - if (_sqlSyntax is MySqlSyntaxProvider) + if (_isMySql) { - sb.AppendLine("Please note that the constraints could not be validated because the current dataprovider is MySql."); + sb.AppendLine("Please note that the constraints could not be validated because the current data provider is MySql."); } return sb.ToString(); diff --git a/src/Umbraco.Core/Migrations/MigrationExpressionBase.cs b/src/Umbraco.Core/Migrations/MigrationExpressionBase.cs index 6ac92a07aa..8b5d9cc78c 100644 --- a/src/Umbraco.Core/Migrations/MigrationExpressionBase.cs +++ b/src/Umbraco.Core/Migrations/MigrationExpressionBase.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; using System.Text; using NPoco; using Umbraco.Core.Logging; @@ -88,6 +87,31 @@ namespace Umbraco.Core.Migrations expression.Execute(); } + protected void Execute(Sql sql) + { + if (_executed) + throw new InvalidOperationException("This expression has already been executed."); + _executed = true; + + if (sql == null) + { + Logger.Info(GetType(), $"SQL [{Context.Index}]: "); + } + else + { + Logger.Info(GetType(), $"SQL [{Context.Index}]: {sql.ToText()}"); + Database.Execute(sql); + } + + Context.Index++; + + if (_expressions == null) + return; + + foreach (var expression in _expressions) + expression.Execute(); + } + private void ExecuteStatement(StringBuilder stmtBuilder) { var stmt = stmtBuilder.ToString(); diff --git a/src/Umbraco.Core/Migrations/MigrationPlan.cs b/src/Umbraco.Core/Migrations/MigrationPlan.cs index 5c999ad6ef..2e2bc8b661 100644 --- a/src/Umbraco.Core/Migrations/MigrationPlan.cs +++ b/src/Umbraco.Core/Migrations/MigrationPlan.cs @@ -4,6 +4,7 @@ using System.Linq; using Umbraco.Core.Exceptions; using Umbraco.Core.Logging; using Umbraco.Core.Scoping; +using Type = System.Type; namespace Umbraco.Core.Migrations { @@ -12,8 +13,6 @@ namespace Umbraco.Core.Migrations /// public class MigrationPlan { - private readonly IMigrationBuilder _migrationBuilder; - private readonly ILogger _logger; private readonly Dictionary _transitions = new Dictionary(); private string _prevState; @@ -23,64 +22,24 @@ namespace Umbraco.Core.Migrations /// Initializes a new instance of the class. /// /// The name of the plan. - /// The plan cannot be executed. Use this constructor e.g. when only validating the plan, - /// or trying to get its final state, without actually needing to execute it. public MigrationPlan(string name) { if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullOrEmptyException(nameof(name)); Name = name; - - // ReSharper disable once VirtualMemberCallInConstructor - // (accepted) - DefinePlan(); } /// - /// Initializes a new instance of the class. + /// Gets the transitions. /// - /// The name of the plan. - /// A migration builder. - /// A logger. - /// The plan can be executed. - public MigrationPlan(string name, IMigrationBuilder migrationBuilder, ILogger logger) - { - if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullOrEmptyException(nameof(name)); - Name = name; - _migrationBuilder = migrationBuilder ?? throw new ArgumentNullException(nameof(migrationBuilder)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - - // ReSharper disable once VirtualMemberCallInConstructor - // (accepted) - DefinePlan(); - } - - /// - /// Defines the plan. - /// - protected virtual void DefinePlan() { } + public IReadOnlyDictionary Transitions => _transitions; /// /// Gets the name of the plan. /// public string Name { get; } - /// - /// Adds an empty migration from source to target state. - /// - public MigrationPlan Add(string sourceState, string targetState) - => Add(sourceState, targetState); - - /// - /// Adds a migration from source to target state. - /// - public MigrationPlan Add(string sourceState, string targetState) - where TMigration : IMigration - => Add(sourceState, targetState, typeof(TMigration)); - - /// - /// Adds a migration from source to target state. - /// - public MigrationPlan Add(string sourceState, string targetState, Type migration) + // adds a transition + private MigrationPlan Add(string sourceState, string targetState, Type migration) { if (sourceState == null) throw new ArgumentNullException(nameof(sourceState)); if (string.IsNullOrWhiteSpace(targetState)) throw new ArgumentNullOrEmptyException(nameof(targetState)); @@ -113,26 +72,26 @@ namespace Umbraco.Core.Migrations } /// - /// Chains an empty migration from chain to target state. - /// - public MigrationPlan Chain(string targetState) - => Chain(targetState); + /// Adds a transition to a target state through an empty migration. + /// + public MigrationPlan To(string targetState) + => To(targetState); /// - /// Chains a migration from chain to target state. + /// Adds a transition to a target state through a migration. /// - public MigrationPlan Chain(string targetState) + public MigrationPlan To(string targetState) where TMigration : IMigration - => Chain(targetState, typeof(TMigration)); + => To(targetState, typeof(TMigration)); /// - /// Chains a migration from chain to target state. + /// Adds a transition to a target state through a migration. /// - public MigrationPlan Chain(string targetState, Type migration) + public MigrationPlan To(string targetState, Type migration) => Add(_prevState, targetState, migration); /// - /// Sets the chain state. + /// Sets the starting state. /// public MigrationPlan From(string sourceState) { @@ -141,19 +100,16 @@ namespace Umbraco.Core.Migrations } /// - /// Copies a chain. + /// Adds transitions to a target state by copying transitions from a start state to an end state. /// - /// Copies the chain going from startState to endState, with new states going from sourceState to targetState. - public MigrationPlan CopyChain(string sourceState, string startState, string endState, string targetState) + public MigrationPlan To(string targetState, string startState, string endState) { - if (sourceState == null) throw new ArgumentNullException(nameof(sourceState)); if (string.IsNullOrWhiteSpace(startState)) throw new ArgumentNullOrEmptyException(nameof(startState)); if (string.IsNullOrWhiteSpace(endState)) throw new ArgumentNullOrEmptyException(nameof(endState)); if (string.IsNullOrWhiteSpace(targetState)) throw new ArgumentNullOrEmptyException(nameof(targetState)); - if (sourceState == targetState) throw new ArgumentException("Source and target states cannot be identical."); + if (startState == endState) throw new ArgumentException("Start and end states cannot be identical."); - sourceState = sourceState.Trim(); startState = startState.Trim(); endState = endState.Trim(); targetState = targetState.Trim(); @@ -168,26 +124,18 @@ namespace Umbraco.Core.Migrations visited.Add(state); if (!_transitions.TryGetValue(state, out var transition)) - throw new InvalidOperationException($"There is no transition from state \"{sourceState}\"."); + throw new InvalidOperationException($"There is no transition from state \"{state}\"."); var newTargetState = transition.TargetState == endState ? targetState : Guid.NewGuid().ToString("B").ToUpper(); - Add(sourceState, newTargetState, transition.MigrationType); - sourceState = newTargetState; + To(newTargetState, transition.MigrationType); state = transition.TargetState; } return this; } - /// - /// Copies a chain. - /// - /// Copies the chain going from startState to endState, with new states going from chain to targetState. - public MigrationPlan CopyChain(string startState, string endState, string targetState) - => CopyChain(_prevState, startState, endState, targetState); - /// /// Gets the initial state. /// @@ -260,50 +208,92 @@ namespace Umbraco.Core.Migrations /// /// A scope. /// The state to start execution at. + /// A migration builder. + /// A logger. /// The final state. /// The plan executes within the scope, which must then be completed. - public string Execute(IScope scope, string fromState) + public string Execute(IScope scope, string fromState, IMigrationBuilder migrationBuilder, ILogger logger) { Validate(); - if (_migrationBuilder == null || _logger == null) - throw new InvalidOperationException("Cannot execute a non-executing plan."); + if (migrationBuilder == null) throw new ArgumentNullException(nameof(migrationBuilder)); + if (logger == null) throw new ArgumentNullException(nameof(logger)); - _logger.Info("Starting '{MigrationName}'...", Name); + logger.Info("Starting '{MigrationName}'...", Name); var origState = fromState ?? string.Empty; - _logger.Info("At {OrigState}", string.IsNullOrWhiteSpace(origState) ? "origin": origState); + logger.Info("At {OrigState}", string.IsNullOrWhiteSpace(origState) ? "origin": origState); if (!_transitions.TryGetValue(origState, out var transition)) throw new Exception($"Unknown state \"{origState}\"."); - var context = new MigrationContext(scope.Database, _logger); + var context = new MigrationContext(scope.Database, logger); while (transition != null) { - var migration = _migrationBuilder.Build(transition.MigrationType, context); + var migration = migrationBuilder.Build(transition.MigrationType, context); migration.Migrate(); var nextState = transition.TargetState; origState = nextState; - _logger.Info("At {OrigState}", origState); + logger.Info("At {OrigState}", origState); if (!_transitions.TryGetValue(origState, out transition)) throw new Exception($"Unknown state \"{origState}\"."); } - _logger.Info("Done (pending scope completion)."); + logger.Info("Done (pending scope completion)."); + + // safety check + if (origState != _finalState) + throw new Exception($"Internal error, reached state {origState} which is not final state {_finalState}"); + + return origState; + } + + /// + /// Follows a path (for tests and debugging). + /// + /// Does the same thing Execute does, but does not actually execute migrations. + internal string FollowPath(string fromState, string toState = null) + { + toState = toState.NullOrWhiteSpaceAsNull(); + + Validate(); + + var origState = fromState ?? string.Empty; + + if (!_transitions.TryGetValue(origState, out var transition)) + throw new Exception($"Unknown state \"{origState}\"."); + + while (transition != null) + { + var nextState = transition.TargetState; + origState = nextState; + + if (nextState == toState) + { + transition = null; + continue; + } + + if (!_transitions.TryGetValue(origState, out transition)) + throw new Exception($"Unknown state \"{origState}\"."); + } + + // safety check + if (origState != (toState ?? _finalState)) + throw new Exception($"Internal error, reached state {origState} which is not state {toState ?? _finalState}"); - // fixme - what about post-migrations? return origState; } /// /// Represents a plan transition. /// - private class Transition + public class Transition { /// /// Initializes a new instance of the class. diff --git a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs index 53a86f3524..ca9ff3fb72 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs @@ -2,7 +2,6 @@ using System.Configuration; using Semver; using Umbraco.Core.Configuration; -using Umbraco.Core.Logging; using Umbraco.Core.Migrations.Upgrade.V_7_12_0; using Umbraco.Core.Migrations.Upgrade.V_8_0_0; @@ -18,14 +17,9 @@ namespace Umbraco.Core.Migrations.Upgrade /// public UmbracoPlan() : base(Constants.System.UmbracoUpgradePlanName) - { } - - /// - /// Initializes a new instance of the class. - /// - public UmbracoPlan(IMigrationBuilder migrationBuilder, ILogger logger) - : base(Constants.System.UmbracoUpgradePlanName, migrationBuilder, logger) - { } + { + DefinePlan(); + } /// /// @@ -61,8 +55,8 @@ namespace Umbraco.Core.Migrations.Upgrade } } - /// - protected override void DefinePlan() + // define the plan + protected void DefinePlan() { // MODIFYING THE PLAN // @@ -85,66 +79,75 @@ namespace Umbraco.Core.Migrations.Upgrade // upgrades from 7 to 8, and then takes care of all eventual upgrades // From("{init-7.10.0}"); - Chain("{7C447271-CA3F-4A6A-A913-5D77015655CB}"); - Chain("{CBFF58A2-7B50-4F75-8E98-249920DB0F37}"); - Chain("{3D18920C-E84D-405C-A06A-B7CEE52FE5DD}"); - Chain("{FB0A5429-587E-4BD0-8A67-20F0E7E62FF7}"); - Chain("{F0C42457-6A3B-4912-A7EA-F27ED85A2092}"); - Chain("{8640C9E4-A1C0-4C59-99BB-609B4E604981}"); - Chain("{DD1B99AF-8106-4E00-BAC7-A43003EA07F8}"); - Chain("{9DF05B77-11D1-475C-A00A-B656AF7E0908}"); - Chain("{6FE3EF34-44A0-4992-B379-B40BC4EF1C4D}"); - Chain("{7F59355A-0EC9-4438-8157-EB517E6D2727}"); + To("{7C447271-CA3F-4A6A-A913-5D77015655CB}"); + To("{CBFF58A2-7B50-4F75-8E98-249920DB0F37}"); + To("{3D18920C-E84D-405C-A06A-B7CEE52FE5DD}"); + To("{FB0A5429-587E-4BD0-8A67-20F0E7E62FF7}"); + To("{F0C42457-6A3B-4912-A7EA-F27ED85A2092}"); + To("{8640C9E4-A1C0-4C59-99BB-609B4E604981}"); + To("{DD1B99AF-8106-4E00-BAC7-A43003EA07F8}"); + To("{9DF05B77-11D1-475C-A00A-B656AF7E0908}"); + To("{6FE3EF34-44A0-4992-B379-B40BC4EF1C4D}"); + To("{7F59355A-0EC9-4438-8157-EB517E6D2727}"); + //To("{941B2ABA-2D06-4E04-81F5-74224F1DB037}"); // AddVariationTables1 has been superseded by AddVariationTables2 + To("{76DF5CD7-A884-41A5-8DC6-7860D95B1DF5}"); - // AddVariationTables1 has been superceeded by AddVariationTables2 - //Chain("{941B2ABA-2D06-4E04-81F5-74224F1DB037}"); - Chain("{76DF5CD7-A884-41A5-8DC6-7860D95B1DF5}"); - // however, provide a path out of the old state - Add("{941B2ABA-2D06-4E04-81F5-74224F1DB037}", "{76DF5CD7-A884-41A5-8DC6-7860D95B1DF5}"); + // way out of the commented state + From("{941B2ABA-2D06-4E04-81F5-74224F1DB037}"); + To("{76DF5CD7-A884-41A5-8DC6-7860D95B1DF5}"); // resume at {76DF5CD7-A884-41A5-8DC6-7860D95B1DF5} ... - Chain("{A7540C58-171D-462A-91C5-7A9AA5CB8BFD}"); + To("{A7540C58-171D-462A-91C5-7A9AA5CB8BFD}"); + To("{3E44F712-E2E3-473A-AE49-5D7F8E67CE3F}"); // shannon added that one - let's keep it as the default path + //To("{65D6B71C-BDD5-4A2E-8D35-8896325E9151}"); // stephan added that one = merge conflict, remove + To("{4CACE351-C6B9-4F0C-A6BA-85A02BBD39E4}"); // but add it after shannon's, with a new target state - Chain("{3E44F712-E2E3-473A-AE49-5D7F8E67CE3F}"); // shannon added that one - let's keep it as the default path - //Chain("{65D6B71C-BDD5-4A2E-8D35-8896325E9151}"); // stephan added that one = merge conflict, remove, - Chain("{4CACE351-C6B9-4F0C-A6BA-85A02BBD39E4}"); // but add it after shannon's, with a new target state, - Add("{65D6B71C-BDD5-4A2E-8D35-8896325E9151}", "{4CACE351-C6B9-4F0C-A6BA-85A02BBD39E4}"); // and provide a path out of the conflict state + // way out of the commented state + From("{65D6B71C-BDD5-4A2E-8D35-8896325E9151}"); + To("{4CACE351-C6B9-4F0C-A6BA-85A02BBD39E4}"); // resume at {4CACE351-C6B9-4F0C-A6BA-85A02BBD39E4} ... - Chain("{1350617A-4930-4D61-852F-E3AA9E692173}"); - Chain("{39E5B1F7-A50B-437E-B768-1723AEC45B65}"); // from 7.12.0 - //Chain("{CF51B39B-9B9A-4740-BB7C-EAF606A7BFBF}"); // andy added that one = merge conflict, remove - Chain("{0541A62B-EF87-4CA2-8225-F0EB98ECCC9F}"); // from 7.12.0 - Chain("{EB34B5DC-BB87-4005-985E-D983EA496C38}"); // from 7.12.0 - Chain("{517CE9EA-36D7-472A-BF4B-A0D6FB1B8F89}"); // from 7.12.0 - Chain("{BBD99901-1545-40E4-8A5A-D7A675C7D2F2}"); // from 7.12.0 - //Chain("{2C87AA47-D1BC-4ECB-8A73-2D8D1046C27F}"); // stephan added that one = merge conflict, remove + To("{1350617A-4930-4D61-852F-E3AA9E692173}"); + To("{39E5B1F7-A50B-437E-B768-1723AEC45B65}"); // from 7.12.0 + //To("{CF51B39B-9B9A-4740-BB7C-EAF606A7BFBF}"); // andy added that one = merge conflict, remove + To("{0541A62B-EF87-4CA2-8225-F0EB98ECCC9F}"); // from 7.12.0 + To("{EB34B5DC-BB87-4005-985E-D983EA496C38}"); // from 7.12.0 + To("{517CE9EA-36D7-472A-BF4B-A0D6FB1B8F89}"); // from 7.12.0 + To("{BBD99901-1545-40E4-8A5A-D7A675C7D2F2}"); // from 7.12.0 + //To("{2C87AA47-D1BC-4ECB-8A73-2D8D1046C27F}"); // stephan added that one = merge conflict, remove - Chain("{8B14CEBD-EE47-4AAD-A841-93551D917F11}"); // add andy's after others, with a new target state - From("{CF51B39B-9B9A-4740-BB7C-EAF606A7BFBF}") // and provide a path out of andy's - .CopyChain("{39E5B1F7-A50B-437E-B768-1723AEC45B65}", "{BBD99901-1545-40E4-8A5A-D7A675C7D2F2}", "{8B14CEBD-EE47-4AAD-A841-93551D917F11}"); // to next + To("{8B14CEBD-EE47-4AAD-A841-93551D917F11}"); // add andy's after others, with a new target state + + // way out of andy's + From("{CF51B39B-9B9A-4740-BB7C-EAF606A7BFBF}"); + To("{8B14CEBD-EE47-4AAD-A841-93551D917F11}", "{39E5B1F7-A50B-437E-B768-1723AEC45B65}", "{BBD99901-1545-40E4-8A5A-D7A675C7D2F2}"); // resume at {8B14CEBD-EE47-4AAD-A841-93551D917F11} ... - Chain("{5F4597F4-A4E0-4AFE-90B5-6D2F896830EB}"); // add stephan's after others, with a new target state - From("{2C87AA47-D1BC-4ECB-8A73-2D8D1046C27F}") // and provide a path out of stephan's - .Chain("{5F4597F4-A4E0-4AFE-90B5-6D2F896830EB}"); // to next + To("{5F4597F4-A4E0-4AFE-90B5-6D2F896830EB}"); // add stephan's after others, with a new target state + + // way out of the commented state + From("{2C87AA47-D1BC-4ECB-8A73-2D8D1046C27F}"); + To("{5F4597F4-A4E0-4AFE-90B5-6D2F896830EB}"); // to next // resume at {5F4597F4-A4E0-4AFE-90B5-6D2F896830EB} ... - //Chain("{B19BF0F2-E1C6-4AEB-A146-BC559D97A2C6}"); - Chain("{290C18EE-B3DE-4769-84F1-1F467F3F76DA}"); - From("{B19BF0F2-E1C6-4AEB-A146-BC559D97A2C6}") - .Chain("{290C18EE-B3DE-4769-84F1-1F467F3F76DA}"); + //To("{B19BF0F2-E1C6-4AEB-A146-BC559D97A2C6}"); + To("{290C18EE-B3DE-4769-84F1-1F467F3F76DA}"); + + // way out of the commented state + From("{B19BF0F2-E1C6-4AEB-A146-BC559D97A2C6}"); + To("{290C18EE-B3DE-4769-84F1-1F467F3F76DA}"); // resume at {290C18EE-B3DE-4769-84F1-1F467F3F76DA}... - Chain("{6A2C7C1B-A9DB-4EA9-B6AB-78E7D5B722A7}"); - Chain("{77874C77-93E5-4488-A404-A630907CEEF0}"); - Chain("{8804D8E8-FE62-4E3A-B8A2-C047C2118C38}"); - Chain("{23275462-446E-44C7-8C2C-3B8C1127B07D}"); - Chain("{6B251841-3069-4AD5-8AE9-861F9523E8DA}"); - Chain("{EE429F1B-9B26-43CA-89F8-A86017C809A3}"); - Chain("{08919C4B-B431-449C-90EC-2B8445B5C6B1}"); - Chain("{7EB0254C-CB8B-4C75-B15B-D48C55B449EB}"); - Chain("{648A2D5F-7467-48F8-B309-E99CEEE00E2A}"); // fixed version + To("{6A2C7C1B-A9DB-4EA9-B6AB-78E7D5B722A7}"); + To("{77874C77-93E5-4488-A404-A630907CEEF0}"); + To("{8804D8E8-FE62-4E3A-B8A2-C047C2118C38}"); + To("{23275462-446E-44C7-8C2C-3B8C1127B07D}"); + To("{6B251841-3069-4AD5-8AE9-861F9523E8DA}"); + To("{EE429F1B-9B26-43CA-89F8-A86017C809A3}"); + To("{08919C4B-B431-449C-90EC-2B8445B5C6B1}"); + To("{7EB0254C-CB8B-4C75-B15B-D48C55B449EB}"); + To("{648A2D5F-7467-48F8-B309-E99CEEE00E2A}"); // fixed version + To("{C39BF2A7-1454-4047-BBFE-89E40F66ED63}"); //FINAL @@ -153,20 +156,20 @@ namespace Umbraco.Core.Migrations.Upgrade // and then, need to support upgrading from more recent 7.x // - From("{init-7.10.1}").Chain("{init-7.10.0}"); // same as 7.10.0 - From("{init-7.10.2}").Chain("{init-7.10.0}"); // same as 7.10.0 - From("{init-7.10.3}").Chain("{init-7.10.0}"); // same as 7.10.0 - From("{init-7.10.4}").Chain("{init-7.10.0}"); // same as 7.10.0 - From("{init-7.11.0}").Chain("{init-7.10.0}"); // same as 7.10.0 - From("{init-7.11.1}").Chain("{init-7.10.0}"); // same as 7.10.0 + From("{init-7.10.1}").To("{init-7.10.0}"); // same as 7.10.0 + From("{init-7.10.2}").To("{init-7.10.0}"); // same as 7.10.0 + From("{init-7.10.3}").To("{init-7.10.0}"); // same as 7.10.0 + From("{init-7.10.4}").To("{init-7.10.0}"); // same as 7.10.0 + From("{init-7.11.0}").To("{init-7.10.0}"); // same as 7.10.0 + From("{init-7.11.1}").To("{init-7.10.0}"); // same as 7.10.0 // 7.12.0 has migrations, define a custom chain which copies the chain // going from {init-7.10.0} to former final (1350617A) , and then goes straight to // main chain, skipping the migrations // From("{init-7.12.0}"); - // copy from copy to (former final) main chain - CopyChain("{init-7.10.0}", "{1350617A-4930-4D61-852F-E3AA9E692173}", "{BBD99901-1545-40E4-8A5A-D7A675C7D2F2}"); + // target copy from copy to (former final) + To("{BBD99901-1545-40E4-8A5A-D7A675C7D2F2}", "{init-7.10.0}", "{1350617A-4930-4D61-852F-E3AA9E692173}"); } } } diff --git a/src/Umbraco.Core/Migrations/Upgrade/UmbracoUpgrader.cs b/src/Umbraco.Core/Migrations/Upgrade/UmbracoUpgrader.cs index b24ad2a20e..fa29e80a6b 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/UmbracoUpgrader.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/UmbracoUpgrader.cs @@ -8,24 +8,41 @@ using Umbraco.Core.Services; namespace Umbraco.Core.Migrations.Upgrade { + /// + /// Represents the Umbraco upgrader. + /// public class UmbracoUpgrader : Upgrader { - public UmbracoUpgrader(IScopeProvider scopeProvider, IMigrationBuilder migrationBuilder, IKeyValueService keyValueService, PostMigrationCollection postMigrations, ILogger logger) - : base(scopeProvider, migrationBuilder, keyValueService, postMigrations, logger) + private PostMigrationCollection _postMigrations; + + /// + /// Initializes a new instance of the class. + /// + public UmbracoUpgrader() + : base(new UmbracoPlan()) { } - protected override MigrationPlan GetPlan() + /// + /// Executes. + /// + public void Execute(IScopeProvider scopeProvider, IMigrationBuilder migrationBuilder, IKeyValueService keyValueService, ILogger logger, PostMigrationCollection postMigrations) { - return new UmbracoPlan(MigrationBuilder, Logger); + _postMigrations = postMigrations; + Execute(scopeProvider, migrationBuilder, keyValueService, logger); } - protected override (SemVersion, SemVersion) GetVersions() + /// + public override void AfterMigrations(IScope scope, ILogger logger) { - // assume we have something in web.config that makes some sense - if (!SemVersion.TryParse(ConfigurationManager.AppSettings["umbracoConfigurationStatus"], out var currentVersion)) + // assume we have something in web.config that makes some sense = the origin version + if (!SemVersion.TryParse(ConfigurationManager.AppSettings["umbracoConfigurationStatus"], out var originVersion)) throw new InvalidOperationException("Could not get current version from web.config umbracoConfigurationStatus appSetting."); - return (currentVersion, UmbracoVersion.SemanticVersion); + // target version is the code version + var targetVersion = UmbracoVersion.SemanticVersion; + + foreach (var postMigration in _postMigrations) + postMigration.Execute(Name, scope, originVersion, targetVersion, logger); } } } diff --git a/src/Umbraco.Core/Migrations/Upgrade/Upgrader.cs b/src/Umbraco.Core/Migrations/Upgrade/Upgrader.cs index 974ed7b4f8..f6df52bc1e 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/Upgrader.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/Upgrader.cs @@ -1,49 +1,60 @@ using System; -using Semver; using Umbraco.Core.Logging; using Umbraco.Core.Scoping; using Umbraco.Core.Services; namespace Umbraco.Core.Migrations.Upgrade { - public abstract class Upgrader + /// + /// Represents an upgrader. + /// + public class Upgrader { - private readonly IKeyValueService _keyValueService; - private readonly PostMigrationCollection _postMigrations; - private MigrationPlan _plan; - - protected Upgrader(IScopeProvider scopeProvider, IMigrationBuilder migrationBuilder, IKeyValueService keyValueService, PostMigrationCollection postMigrations, ILogger logger) + /// + /// Initializes a new instance of the class. + /// + public Upgrader(MigrationPlan plan) { - ScopeProvider = scopeProvider ?? throw new ArgumentNullException(nameof(scopeProvider)); - MigrationBuilder = migrationBuilder ?? throw new ArgumentNullException(nameof(migrationBuilder)); - _keyValueService = keyValueService ?? throw new ArgumentNullException(nameof(keyValueService)); - _postMigrations = postMigrations ?? throw new ArgumentNullException(nameof(postMigrations)); - Logger = logger ?? throw new ArgumentNullException(nameof(logger)); + Plan = plan; } + /// + /// Gets the name of the migration plan. + /// public string Name => Plan.Name; - public string StateValueKey => GetStateValueKey(Plan); + /// + /// Gets the migration plan. + /// + public MigrationPlan Plan { get; } - protected IScopeProvider ScopeProvider { get; } + /// + /// Gets the key for the state value. + /// + public virtual string StateValueKey => "Umbraco.Core.Upgrader.State+" + Name; - protected IMigrationBuilder MigrationBuilder { get; } - - protected ILogger Logger { get; } - - protected MigrationPlan Plan => _plan ?? (_plan = GetPlan()); - - protected abstract MigrationPlan GetPlan(); - protected abstract (SemVersion, SemVersion) GetVersions(); - - public void Execute() + /// + /// Executes. + /// + /// A scope provider. + /// A migration builder. + /// A key-value service. + /// A logger. + public void Execute(IScopeProvider scopeProvider, IMigrationBuilder migrationBuilder, IKeyValueService keyValueService, ILogger logger) { + if (scopeProvider == null) throw new ArgumentNullException(nameof(scopeProvider)); + if (migrationBuilder == null) throw new ArgumentNullException(nameof(migrationBuilder)); + if (keyValueService == null) throw new ArgumentNullException(nameof(keyValueService)); + if (logger == null) throw new ArgumentNullException(nameof(logger)); + var plan = Plan; - using (var scope = ScopeProvider.CreateScope()) + using (var scope = scopeProvider.CreateScope()) { + BeforeMigrations(scope, logger); + // read current state - var currentState = _keyValueService.GetValue(StateValueKey); + var currentState = keyValueService.GetValue(StateValueKey); var forceState = false; if (currentState == null) @@ -53,25 +64,33 @@ namespace Umbraco.Core.Migrations.Upgrade } // execute plan - var state = plan.Execute(scope, currentState); + var state = plan.Execute(scope, currentState, migrationBuilder, logger); if (string.IsNullOrWhiteSpace(state)) throw new Exception("Plan execution returned an invalid null or empty state."); // save new state if (forceState) - _keyValueService.SetValue(StateValueKey, state); + keyValueService.SetValue(StateValueKey, state); else if (currentState != state) - _keyValueService.SetValue(StateValueKey, currentState, state); + keyValueService.SetValue(StateValueKey, currentState, state); - // run post-migrations - (var originVersion, var targetVersion) = GetVersions(); - foreach (var postMigration in _postMigrations) - postMigration.Execute(Name, scope, originVersion, targetVersion, Logger); + AfterMigrations(scope, logger); scope.Complete(); } } - public static string GetStateValueKey(MigrationPlan plan) => "Umbraco.Core.Upgrader.State+" + plan.Name; + /// + /// Executes as part of the upgrade scope and before all migrations have executed. + /// + public virtual void BeforeMigrations(IScope scope, ILogger logger) + { } + + /// + /// Executes as part of the upgrade scope and after all migrations have executed. + /// + public virtual void AfterMigrations(IScope scope, ILogger logger) + { } + } } diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/MakeTagsVariant.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/MakeTagsVariant.cs new file mode 100644 index 0000000000..9ccd6d5e76 --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/MakeTagsVariant.cs @@ -0,0 +1,27 @@ +using Umbraco.Core.Persistence.Dtos; + +namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 +{ + public class MakeTagsVariant : MigrationBase + { + public MakeTagsVariant(IMigrationContext context) + : base(context) + { } + + public override void Migrate() + { + AddColumn("languageId"); + + Delete.Index($"IX_{Constants.DatabaseSchema.Tables.Tag}").OnTable(Constants.DatabaseSchema.Tables.Tag).Do(); + Create.Index($"IX_{Constants.DatabaseSchema.Tables.Tag}").OnTable(Constants.DatabaseSchema.Tables.Tag) + .OnColumn("group") + .Ascending() + .OnColumn("tag") + .Ascending() + .OnColumn("languageId") + .Ascending() + .WithOptions().Unique() + .Do(); + } + } +} diff --git a/src/Umbraco.Core/Models/ContentTagsExtensions.cs b/src/Umbraco.Core/Models/ContentTagsExtensions.cs index 8aceaac762..dd7a716520 100644 --- a/src/Umbraco.Core/Models/ContentTagsExtensions.cs +++ b/src/Umbraco.Core/Models/ContentTagsExtensions.cs @@ -15,10 +15,10 @@ namespace Umbraco.Core.Models /// The property alias. /// The tags. /// A value indicating whether to merge the tags with existing tags instead of replacing them. - /// Tags do not support variants. - public static void AssignTags(this IContentBase content, string propertyTypeAlias, IEnumerable tags, bool merge = false) + /// A culture, for multi-lingual properties. + public static void AssignTags(this IContentBase content, string propertyTypeAlias, IEnumerable tags, bool merge = false, string culture = null) { - content.GetTagProperty(propertyTypeAlias).AssignTags(tags, merge); + content.GetTagProperty(propertyTypeAlias).AssignTags(tags, merge, culture); } /// @@ -27,10 +27,10 @@ namespace Umbraco.Core.Models /// The content item. /// The property alias. /// The tags. - /// Tags do not support variants. - public static void RemoveTags(this IContentBase content, string propertyTypeAlias, IEnumerable tags) + /// A culture, for multi-lingual properties. + public static void RemoveTags(this IContentBase content, string propertyTypeAlias, IEnumerable tags, string culture = null) { - content.GetTagProperty(propertyTypeAlias).RemoveTags(tags); + content.GetTagProperty(propertyTypeAlias).RemoveTags(tags, culture); } // gets and validates the property diff --git a/src/Umbraco.Core/Models/ITag.cs b/src/Umbraco.Core/Models/ITag.cs index 6f492a2d78..f2c30b2644 100644 --- a/src/Umbraco.Core/Models/ITag.cs +++ b/src/Umbraco.Core/Models/ITag.cs @@ -20,6 +20,12 @@ namespace Umbraco.Core.Models [DataMember] string Text { get; set; } + /// + /// Gets or sets the tag language. + /// + [DataMember] + int? LanguageId { get; set; } + /// /// Gets the number of nodes tagged with this tag. /// diff --git a/src/Umbraco.Core/Models/PropertyTagsExtensions.cs b/src/Umbraco.Core/Models/PropertyTagsExtensions.cs index 26779161a1..39172fff34 100644 --- a/src/Umbraco.Core/Models/PropertyTagsExtensions.cs +++ b/src/Umbraco.Core/Models/PropertyTagsExtensions.cs @@ -38,13 +38,13 @@ namespace Umbraco.Core.Models } /// - /// Assign default tags. + /// Assign tags. /// /// The property. /// The tags. /// A value indicating whether to merge the tags with existing tags instead of replacing them. - /// Tags do not support variants. - public static void AssignTags(this Property property, IEnumerable tags, bool merge = false) + /// A culture, for multi-lingual properties. + public static void AssignTags(this Property property, IEnumerable tags, bool merge = false, string culture = null) { if (property == null) throw new ArgumentNullException(nameof(property)); @@ -52,11 +52,11 @@ namespace Umbraco.Core.Models if (configuration == null) throw new NotSupportedException($"Property with alias \"{property.Alias}\" does not support tags."); - property.AssignTags(tags, merge, configuration.StorageType, configuration.Delimiter); + property.AssignTags(tags, merge, configuration.StorageType, configuration.Delimiter, culture); } // assumes that parameters are consistent with the datatype configuration - internal static void AssignTags(this Property property, IEnumerable tags, bool merge, TagsStorageType storageType, char delimiter) + private static void AssignTags(this Property property, IEnumerable tags, bool merge, TagsStorageType storageType, char delimiter, string culture) { // set the property value var trimmedTags = tags.Select(x => x.Trim()).ToArray(); @@ -68,11 +68,11 @@ namespace Umbraco.Core.Models switch (storageType) { case TagsStorageType.Csv: - property.SetValue(string.Join(delimiter.ToString(), currentTags.Union(trimmedTags))); // csv string + property.SetValue(string.Join(delimiter.ToString(), currentTags.Union(trimmedTags)), culture); // csv string break; case TagsStorageType.Json: - property.SetValue(JsonConvert.SerializeObject(currentTags.Union(trimmedTags).ToArray())); // json array + property.SetValue(JsonConvert.SerializeObject(currentTags.Union(trimmedTags).ToArray()), culture); // json array break; } } @@ -81,23 +81,23 @@ namespace Umbraco.Core.Models switch (storageType) { case TagsStorageType.Csv: - property.SetValue(string.Join(delimiter.ToString(), trimmedTags)); // csv string + property.SetValue(string.Join(delimiter.ToString(), trimmedTags), culture); // csv string break; case TagsStorageType.Json: - property.SetValue(JsonConvert.SerializeObject(trimmedTags)); // json array + property.SetValue(JsonConvert.SerializeObject(trimmedTags), culture); // json array break; } } } /// - /// Removes default tags. + /// Removes tags. /// /// The property. /// The tags. - /// Tags do not support variants. - public static void RemoveTags(this Property property, IEnumerable tags) + /// A culture, for multi-lingual properties. + public static void RemoveTags(this Property property, IEnumerable tags, string culture = null) { if (property == null) throw new ArgumentNullException(nameof(property)); @@ -105,33 +105,33 @@ namespace Umbraco.Core.Models if (configuration == null) throw new NotSupportedException($"Property with alias \"{property.Alias}\" does not support tags."); - property.RemoveTags(tags, configuration.StorageType, configuration.Delimiter); + property.RemoveTags(tags, configuration.StorageType, configuration.Delimiter, culture); } // assumes that parameters are consistent with the datatype configuration - private static void RemoveTags(this Property property, IEnumerable tags, TagsStorageType storageType, char delimiter) + private static void RemoveTags(this Property property, IEnumerable tags, TagsStorageType storageType, char delimiter, string culture) { // already empty = nothing to do - //fixme doesn't take into account variants - var value = property.GetValue()?.ToString(); + var value = property.GetValue(culture)?.ToString(); if (string.IsNullOrWhiteSpace(value)) return; // set the property value var trimmedTags = tags.Select(x => x.Trim()).ToArray(); - var currentTags = property.GetTagsValue(storageType, delimiter); + var currentTags = property.GetTagsValue(storageType, delimiter, culture); switch (storageType) { case TagsStorageType.Csv: - property.SetValue(string.Join(delimiter.ToString(), currentTags.Except(trimmedTags))); // csv string + property.SetValue(string.Join(delimiter.ToString(), currentTags.Except(trimmedTags)), culture); // csv string break; case TagsStorageType.Json: - property.SetValue(JsonConvert.SerializeObject(currentTags.Except(trimmedTags).ToArray())); // json array + property.SetValue(JsonConvert.SerializeObject(currentTags.Except(trimmedTags).ToArray()), culture); // json array break; } } - internal static IEnumerable GetTagsValue(this Property property) + // used by ContentRepositoryBase + internal static IEnumerable GetTagsValue(this Property property, string culture = null) { if (property == null) throw new ArgumentNullException(nameof(property)); @@ -139,15 +139,14 @@ namespace Umbraco.Core.Models if (configuration == null) throw new NotSupportedException($"Property with alias \"{property.Alias}\" does not support tags."); - return property.GetTagsValue(configuration.StorageType, configuration.Delimiter); + return property.GetTagsValue(configuration.StorageType, configuration.Delimiter, culture); } - internal static IEnumerable GetTagsValue(this Property property, TagsStorageType storageType, char delimiter) + private static IEnumerable GetTagsValue(this Property property, TagsStorageType storageType, char delimiter, string culture = null) { if (property == null) throw new ArgumentNullException(nameof(property)); - //fixme doesn't take into account variants - var value = property.GetValue()?.ToString(); + var value = property.GetValue(culture)?.ToString(); if (string.IsNullOrWhiteSpace(value)) return Enumerable.Empty(); switch (storageType) @@ -158,7 +157,6 @@ namespace Umbraco.Core.Models case TagsStorageType.Json: try { - //fixme doesn't take into account variants return JsonConvert.DeserializeObject(value).Select(x => x.ToString().Trim()); } catch (JsonException) @@ -178,34 +176,33 @@ namespace Umbraco.Core.Models /// The property. /// The property value. /// The datatype configuration. - /// + /// A culture, for multi-lingual properties. + /// /// The value is either a string (delimited string) or an enumeration of strings (tag list). /// This is used both by the content repositories to initialize a property with some tag values, and by the /// content controllers to update a property with values received from the property editor. /// - internal static void SetTagsValue(this Property property, object value, TagConfiguration tagConfiguration) + internal static void SetTagsValue(this Property property, object value, TagConfiguration tagConfiguration, string culture) { if (property == null) throw new ArgumentNullException(nameof(property)); if (tagConfiguration == null) throw new ArgumentNullException(nameof(tagConfiguration)); - var merge = false; // fixme always! var storageType = tagConfiguration.StorageType; var delimiter = tagConfiguration.Delimiter; - SetTagsValue(property, value, merge, storageType, delimiter); + SetTagsValue(property, value, storageType, delimiter, culture); } // assumes that parameters are consistent with the datatype configuration // value can be an enumeration of string, or a serialized value using storageType format - // fixme merge always false here?! - private static void SetTagsValue(Property property, object value, bool merge, TagsStorageType storageType, char delimiter) + private static void SetTagsValue(Property property, object value, TagsStorageType storageType, char delimiter, string culture) { if (value == null) value = Enumerable.Empty(); // if value is already an enumeration of strings, just use it if (value is IEnumerable tags1) { - property.AssignTags(tags1, merge, storageType, delimiter); + property.AssignTags(tags1, false, storageType, delimiter, culture); return; } @@ -214,14 +211,14 @@ namespace Umbraco.Core.Models { case TagsStorageType.Csv: var tags2 = value.ToString().Split(new[] { delimiter }, StringSplitOptions.RemoveEmptyEntries); - property.AssignTags(tags2, merge, storageType, delimiter); + property.AssignTags(tags2, false, storageType, delimiter, culture); break; case TagsStorageType.Json: try { var tags3 = JsonConvert.DeserializeObject>(value.ToString()); - property.AssignTags(tags3 ?? Enumerable.Empty(), merge, storageType, delimiter); + property.AssignTags(tags3 ?? Enumerable.Empty(), false, storageType, delimiter, culture); } catch (Exception ex) { diff --git a/src/Umbraco.Core/Models/PublishedContent/IPublishedContent.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedContent.cs index 5b604eff3f..0c049e81bf 100644 --- a/src/Umbraco.Core/Models/PublishedContent/IPublishedContent.cs +++ b/src/Umbraco.Core/Models/PublishedContent/IPublishedContent.cs @@ -151,7 +151,7 @@ namespace Umbraco.Core.Models.PublishedContent /// is the edited version) or false (document is published, and has not been edited, and /// what is returned is the published version). /// - bool IsDraft { get; } + bool IsDraft(string culture = null); // fixme - consider having an IsPublished flag too // so that when IsDraft is true, we can check whether there is a published version? diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentWrapped.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentWrapped.cs index 5bdeb3685d..6a69d0b9e1 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedContentWrapped.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentWrapped.cs @@ -109,7 +109,7 @@ namespace Umbraco.Core.Models.PublishedContent public virtual PublishedItemType ItemType => _content.ItemType; /// - public virtual bool IsDraft => _content.IsDraft; + public virtual bool IsDraft(string culture = null) => _content.IsDraft(culture); #endregion diff --git a/src/Umbraco.Core/Models/Tag.cs b/src/Umbraco.Core/Models/Tag.cs index 867d43c257..e9707e587d 100644 --- a/src/Umbraco.Core/Models/Tag.cs +++ b/src/Umbraco.Core/Models/Tag.cs @@ -16,6 +16,7 @@ namespace Umbraco.Core.Models private string _group; private string _text; + private int? _languageId; /// /// Initializes a new instance of the class. @@ -26,11 +27,12 @@ namespace Umbraco.Core.Models /// /// Initializes a new instance of the class. /// - public Tag(int id, string group, string text) + public Tag(int id, string group, string text, int? languageId = null) { Id = id; Text = text; Group = group; + LanguageId = languageId; } private static PropertySelectors Selectors => _selectors ?? (_selectors = new PropertySelectors()); @@ -39,6 +41,7 @@ namespace Umbraco.Core.Models { public readonly PropertyInfo Group = ExpressionHelper.GetPropertyInfo(x => x.Group); public readonly PropertyInfo Text = ExpressionHelper.GetPropertyInfo(x => x.Text); + public readonly PropertyInfo LanguageId = ExpressionHelper.GetPropertyInfo(x => x.LanguageId); } /// @@ -55,6 +58,13 @@ namespace Umbraco.Core.Models set => SetPropertyValueAndDetectChanges(value, ref _text, Selectors.Text); } + /// + public int? LanguageId + { + get => _languageId; + set => SetPropertyValueAndDetectChanges(value, ref _languageId, Selectors.LanguageId); + } + /// public int NodeCount { get; internal set; } } diff --git a/src/Umbraco.Core/Models/TaggedEntity.cs b/src/Umbraco.Core/Models/TaggedEntity.cs index ac194c15cd..8c4695555d 100644 --- a/src/Umbraco.Core/Models/TaggedEntity.cs +++ b/src/Umbraco.Core/Models/TaggedEntity.cs @@ -5,10 +5,13 @@ namespace Umbraco.Core.Models /// /// Represents a tagged entity. /// - /// Note that it is the properties of an entity (like Content, Media, Members, etc.) that is tagged, - /// which is why this class is composed of a list of tagged properties and an Id reference to the actual entity. + /// Note that it is the properties of an entity (like Content, Media, Members, etc.) that are tagged, + /// which is why this class is composed of a list of tagged properties and the identifier the actual entity. public class TaggedEntity { + /// + /// Initializes a new instance of the class. + /// public TaggedEntity(int entityId, IEnumerable taggedProperties) { EntityId = entityId; @@ -16,13 +19,13 @@ namespace Umbraco.Core.Models } /// - /// Id of the entity, which is tagged + /// Gets the identifier of the entity. /// - public int EntityId { get; private set; } + public int EntityId { get; } /// - /// An enumerable list of tagged properties + /// Gets the tagged properties. /// - public IEnumerable TaggedProperties { get; private set; } + public IEnumerable TaggedProperties { get; } } } diff --git a/src/Umbraco.Core/Models/TaggedProperty.cs b/src/Umbraco.Core/Models/TaggedProperty.cs index 2b9650b432..2d9fda9a4f 100644 --- a/src/Umbraco.Core/Models/TaggedProperty.cs +++ b/src/Umbraco.Core/Models/TaggedProperty.cs @@ -7,6 +7,9 @@ namespace Umbraco.Core.Models /// public class TaggedProperty { + /// + /// Initializes a new instance of the class. + /// public TaggedProperty(int propertyTypeId, string propertyTypeAlias, IEnumerable tags) { PropertyTypeId = propertyTypeId; @@ -15,18 +18,18 @@ namespace Umbraco.Core.Models } /// - /// Id of the PropertyType, which this tagged property is based on + /// Gets the identifier of the property type. /// - public int PropertyTypeId { get; private set; } + public int PropertyTypeId { get; } /// - /// Alias of the PropertyType, which this tagged property is based on + /// Gets the alias of the property type. /// - public string PropertyTypeAlias { get; private set; } + public string PropertyTypeAlias { get; } /// - /// An enumerable list of Tags for the property + /// Gets the tags. /// - public IEnumerable Tags { get; private set; } + public IEnumerable Tags { get; } } } diff --git a/src/Umbraco.Core/ObjectExtensions.cs b/src/Umbraco.Core/ObjectExtensions.cs index 318a826b25..1bc2fb48a5 100644 --- a/src/Umbraco.Core/ObjectExtensions.cs +++ b/src/Umbraco.Core/ObjectExtensions.cs @@ -595,7 +595,6 @@ namespace Umbraco.Core return null; } - /// /// Attempts to serialize the value to an XmlString using ToXmlString /// @@ -788,5 +787,7 @@ namespace Umbraco.Core return BoolConvertCache[type] = false; } + + } } diff --git a/src/Umbraco.Core/Persistence/Dtos/TagDto.cs b/src/Umbraco.Core/Persistence/Dtos/TagDto.cs index 15c309d9e5..f6296e4bd0 100644 --- a/src/Umbraco.Core/Persistence/Dtos/TagDto.cs +++ b/src/Umbraco.Core/Persistence/Dtos/TagDto.cs @@ -3,11 +3,13 @@ using Umbraco.Core.Persistence.DatabaseAnnotations; namespace Umbraco.Core.Persistence.Dtos { - [TableName(Constants.DatabaseSchema.Tables.Tag)] + [TableName(TableName)] [PrimaryKey("id")] [ExplicitColumns] internal class TagDto { + public const string TableName = Constants.DatabaseSchema.Tables.Tag; + [Column("id")] [PrimaryKeyColumn] public int Id { get; set; } @@ -16,9 +18,15 @@ namespace Umbraco.Core.Persistence.Dtos [Length(100)] public string Group { get; set; } + [Column("languageId")] + [ForeignKey(typeof(LanguageDto))] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_LanguageId")] + [NullSetting(NullSetting = NullSettings.Null)] + public int? LanguageId { get;set; } + [Column("tag")] [Length(200)] - [Index(IndexTypes.UniqueNonClustered, ForColumns = "group,tag", Name = "IX_cmsTags")] + [Index(IndexTypes.UniqueNonClustered, ForColumns = "group,tag,languageId", Name = "IX_cmsTags")] public string Text { get; set; } //[Column("key")] diff --git a/src/Umbraco.Core/Persistence/Dtos/TagRelationshipDto.cs b/src/Umbraco.Core/Persistence/Dtos/TagRelationshipDto.cs index 4a07b16a07..cbe4cf0cd4 100644 --- a/src/Umbraco.Core/Persistence/Dtos/TagRelationshipDto.cs +++ b/src/Umbraco.Core/Persistence/Dtos/TagRelationshipDto.cs @@ -3,11 +3,13 @@ using Umbraco.Core.Persistence.DatabaseAnnotations; namespace Umbraco.Core.Persistence.Dtos { - [TableName(Constants.DatabaseSchema.Tables.TagRelationship)] + [TableName(TableName)] [PrimaryKey("nodeId", AutoIncrement = false)] [ExplicitColumns] internal class TagRelationshipDto { + public const string TableName = Constants.DatabaseSchema.Tables.TagRelationship; + [Column("nodeId")] [PrimaryKeyColumn(AutoIncrement = false, Name = "PK_cmsTagRelationship", OnColumns = "nodeId, propertyTypeId, tagId")] [ForeignKey(typeof(ContentDto), Name = "FK_cmsTagRelationship_cmsContent", Column = "nodeId")] diff --git a/src/Umbraco.Core/Persistence/Factories/TagFactory.cs b/src/Umbraco.Core/Persistence/Factories/TagFactory.cs index 867e6b0ae3..10441707ec 100644 --- a/src/Umbraco.Core/Persistence/Factories/TagFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/TagFactory.cs @@ -7,7 +7,7 @@ namespace Umbraco.Core.Persistence.Factories { public static ITag BuildEntity(TagDto dto) { - var entity = new Tag(dto.Id, dto.Group, dto.Text) { NodeCount = dto.NodeCount }; + var entity = new Tag(dto.Id, dto.Group, dto.Text, dto.LanguageId) { NodeCount = dto.NodeCount }; // reset dirty initial properties (U4-1946) entity.ResetDirtyProperties(false); return entity; @@ -20,6 +20,7 @@ namespace Umbraco.Core.Persistence.Factories Id = entity.Id, Group = entity.Group, Text = entity.Text, + LanguageId = entity.LanguageId //Key = entity.Group + "/" + entity.Text // de-normalize }; } diff --git a/src/Umbraco.Core/Persistence/Mappers/TagMapper.cs b/src/Umbraco.Core/Persistence/Mappers/TagMapper.cs index 8cd2ab27d7..63f73d060a 100644 --- a/src/Umbraco.Core/Persistence/Mappers/TagMapper.cs +++ b/src/Umbraco.Core/Persistence/Mappers/TagMapper.cs @@ -23,6 +23,7 @@ namespace Umbraco.Core.Persistence.Mappers CacheMap(src => src.Id, dto => dto.Id); CacheMap(src => src.Text, dto => dto.Text); CacheMap(src => src.Group, dto => dto.Group); + CacheMap(src => src.LanguageId, dto => dto.LanguageId); } } } diff --git a/src/Umbraco.Core/Persistence/NPocoSqlExtensions.cs b/src/Umbraco.Core/Persistence/NPocoSqlExtensions.cs index a5ab62d25f..7aa8b707be 100644 --- a/src/Umbraco.Core/Persistence/NPocoSqlExtensions.cs +++ b/src/Umbraco.Core/Persistence/NPocoSqlExtensions.cs @@ -76,7 +76,7 @@ namespace Umbraco.Core.Persistence var (s, a) = sql.SqlContext.VisitDto(predicate, alias); return sql.Where(s, a); } - + /// /// Appends a WHERE clause to the Sql statement. /// @@ -589,11 +589,14 @@ namespace Umbraco.Core.Persistence /// Creates a SELECT COUNT(*) Sql statement. /// /// The origin sql. + /// An optional alias. /// The Sql statement. - public static Sql SelectCount(this Sql sql) + public static Sql SelectCount(this Sql sql, string alias = null) { if (sql == null) throw new ArgumentNullException(nameof(sql)); - return sql.Select("COUNT(*)"); + var text = "COUNT(*)"; + if (alias != null) text += " AS " + sql.SqlContext.SqlSyntax.GetQuotedColumnName(alias); + return sql.Select(text); } /// @@ -607,13 +610,29 @@ namespace Umbraco.Core.Persistence /// If is empty, all columns are counted. /// public static Sql SelectCount(this Sql sql, params Expression>[] fields) + => sql.SelectCount(null, fields); + + /// + /// Creates a SELECT COUNT Sql statement. + /// + /// The type of the DTO to count. + /// The origin sql. + /// An alias. + /// Expressions indicating the columns to count. + /// The Sql statement. + /// + /// If is empty, all columns are counted. + /// + public static Sql SelectCount(this Sql sql, string alias, params Expression>[] fields) { if (sql == null) throw new ArgumentNullException(nameof(sql)); var sqlSyntax = sql.SqlContext.SqlSyntax; var columns = fields.Length == 0 ? sql.GetColumns(withAlias: false) : fields.Select(x => sqlSyntax.GetFieldName(x)).ToArray(); - return sql.Select("COUNT (" + string.Join(", ", columns) + ")"); + var text = "COUNT (" + string.Join(", ", columns) + ")"; + if (alias != null) text += " AS " + sql.SqlContext.SqlSyntax.GetQuotedColumnName(alias); + return sql.Select(text); } /// @@ -643,6 +662,26 @@ namespace Umbraco.Core.Persistence return sql.Select(sql.GetColumns(columnExpressions: fields)); } + /// + /// Creates a SELECT DISTINCT Sql statement. + /// + /// The type of the DTO to select. + /// The origin sql. + /// Expressions indicating the columns to select. + /// The Sql statement. + /// + /// If is empty, all columns are selected. + /// + public static Sql SelectDistinct(this Sql sql, params Expression>[] fields) + { + if (sql == null) throw new ArgumentNullException(nameof(sql)); + var columns = sql.GetColumns(columnExpressions: fields); + sql.Append("SELECT DISTINCT " + string.Join(", ", columns)); + return sql; + } + + //this.Append("SELECT " + string.Join(", ", columns), new object[0]); + /// /// Creates a SELECT Sql statement. /// @@ -705,6 +744,56 @@ namespace Umbraco.Core.Persistence return sql.Append(", " + string.Join(", ", sql.GetColumns(tableAlias: tableAlias, columnExpressions: fields))); } + /// + /// Adds a COUNT(*) to a SELECT Sql statement. + /// + /// The origin sql. + /// An optional alias. + /// The Sql statement. + public static Sql AndSelectCount(this Sql sql, string alias = null) + { + if (sql == null) throw new ArgumentNullException(nameof(sql)); + var text = ", COUNT(*)"; + if (alias != null) text += " AS " + sql.SqlContext.SqlSyntax.GetQuotedColumnName(alias); + return sql.Append(text); + } + + /// + /// Adds a COUNT to a SELECT Sql statement. + /// + /// The type of the DTO to count. + /// The origin sql. + /// Expressions indicating the columns to count. + /// The Sql statement. + /// + /// If is empty, all columns are counted. + /// + public static Sql AndSelectCount(this Sql sql, params Expression>[] fields) + => sql.AndSelectCount(null, fields); + + /// + /// Adds a COUNT to a SELECT Sql statement. + /// + /// The type of the DTO to count. + /// The origin sql. + /// An alias. + /// Expressions indicating the columns to count. + /// The Sql statement. + /// + /// If is empty, all columns are counted. + /// + public static Sql AndSelectCount(this Sql sql, string alias = null, params Expression>[] fields) + { + if (sql == null) throw new ArgumentNullException(nameof(sql)); + var sqlSyntax = sql.SqlContext.SqlSyntax; + var columns = fields.Length == 0 + ? sql.GetColumns(withAlias: false) + : fields.Select(x => sqlSyntax.GetFieldName(x)).ToArray(); + var text = ", COUNT (" + string.Join(", ", columns) + ")"; + if (alias != null) text += " AS " + sql.SqlContext.SqlSyntax.GetQuotedColumnName(alias); + return sql.Append(text); + } + /// /// Creates a SELECT Sql statement with a referenced Dto. /// @@ -1115,12 +1204,37 @@ namespace Umbraco.Core.Persistence return string.IsNullOrWhiteSpace(attr?.Name) ? column.Name : attr.Name; } - internal static void WriteToConsole(this Sql sql) + internal static string ToText(this Sql sql) { - Console.WriteLine(sql.SQL); + var text = new StringBuilder(); + sql.ToText(text); + return text.ToString(); + } + + internal static void ToText(this Sql sql, StringBuilder text) + { + ToText(sql.SQL, sql.Arguments, text); + } + + internal static void ToText(string sql, object[] arguments, StringBuilder text) + { + text.AppendLine(sql); + + if (arguments == null || arguments.Length == 0) + return; + + text.Append(" --"); + var i = 0; - foreach (var arg in sql.Arguments) - Console.WriteLine($" @{i++}: {arg}"); + foreach (var arg in arguments) + { + text.Append(" @"); + text.Append(i++); + text.Append(":"); + text.Append(arg); + } + + text.AppendLine(); } #endregion diff --git a/src/Umbraco.Core/Persistence/Querying/ExpressionVisitorBase.cs b/src/Umbraco.Core/Persistence/Querying/ExpressionVisitorBase.cs index d313d27bbc..16bfc9b164 100644 --- a/src/Umbraco.Core/Persistence/Querying/ExpressionVisitorBase.cs +++ b/src/Umbraco.Core/Persistence/Querying/ExpressionVisitorBase.cs @@ -653,6 +653,23 @@ namespace Umbraco.Core.Persistence.Querying else throw new NotSupportedException("Expression is not a proper lambda."); + // c# 'x == null' becomes sql 'x IS NULL' which is fine + // c# 'x == y' becomes sql 'x = @0' which is fine - unless they are nullable types, + // because sql 'x = NULL' is always false and the 'IS NULL' syntax is required, + // so for comparing nullable types, we use x.SqlNullableEquals(y, fb) where fb is a fallback + // value which will be used when values are null - turning the comparison into + // sql 'COALESCE(x,fb) = COALESCE(y,fb)' - of course, fb must be a value outside + // of x and y range - and if that is not possible, then a manual comparison need + // to be written + //TODO support SqlNullableEquals with 0 parameters, using the full syntax below + case "SqlNullableEquals": + var compareTo = Visit(m.Arguments[1]); + var fallback = Visit(m.Arguments[2]); + // that would work without a fallback value but is more cumbersome + //return Visited ? string.Empty : $"((({compareTo} is null) AND ({visitedMethodObject} is null)) OR (({compareTo} is not null) AND ({visitedMethodObject} = {compareTo})))"; + // use a fallback value + return Visited ? string.Empty : $"(COALESCE({visitedMethodObject},{fallback}) = COALESCE({compareTo},{fallback}))"; + default: throw new ArgumentOutOfRangeException("No logic supported for " + m.Method.Name); diff --git a/src/Umbraco.Core/Persistence/Querying/SqlExpressionExtensions.cs b/src/Umbraco.Core/Persistence/Querying/SqlExpressionExtensions.cs index 0f9eb47d77..710997472c 100644 --- a/src/Umbraco.Core/Persistence/Querying/SqlExpressionExtensions.cs +++ b/src/Umbraco.Core/Persistence/Querying/SqlExpressionExtensions.cs @@ -9,6 +9,21 @@ namespace Umbraco.Core.Persistence.Querying /// internal static class SqlExpressionExtensions { + /// + /// Indicates whether two nullable values are equal, substituting a fallback value for nulls. + /// + /// The nullable type. + /// The value to compare. + /// The value to compare to. + /// The value to use when any value is null. + /// Do not use outside of Sql expressions. + // see usage in ExpressionVisitorBase + public static bool SqlNullableEquals(this T? value, T? other, T fallbackValue) + where T : struct + { + return (value ?? fallbackValue).Equals(other ?? fallbackValue); + } + public static bool SqlIn(this IEnumerable collection, T item) { return collection.Contains(item); diff --git a/src/Umbraco.Core/Persistence/Repositories/ITagRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ITagRepository.cs index 782f3f1b89..c3e6dc028b 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ITagRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ITagRepository.cs @@ -19,7 +19,8 @@ namespace Umbraco.Core.Persistence.Repositories /// When is false, the tags specified in are added to those already assigned. /// When is empty and is true, all assigned tags are removed. /// - void Assign(int contentId, int propertyTypeId, IEnumerable tags, bool replaceTags); + // TODO: replaceTags is used as 'false' in tests exclusively - should get rid of it + void Assign(int contentId, int propertyTypeId, IEnumerable tags, bool replaceTags = true); /// /// Removes assigned tags from a content property. @@ -46,54 +47,48 @@ namespace Umbraco.Core.Persistence.Repositories #region Queries + /// + /// Gets a tagged entity. + /// TaggedEntity GetTaggedEntityByKey(Guid key); + + /// + /// Gets a tagged entity. + /// TaggedEntity GetTaggedEntityById(int id); - IEnumerable GetTaggedEntitiesByTagGroup(TaggableObjectTypes objectType, string tagGroup); - - IEnumerable GetTaggedEntitiesByTag(TaggableObjectTypes objectType, string tag, string tagGroup = null); + /// Gets all entities of a type, tagged with any tag in the specified group. + IEnumerable GetTaggedEntitiesByTagGroup(TaggableObjectTypes objectType, string group, string culture = null); /// - /// Returns all tags for an entity type (content/media/member) + /// Gets all entities of a type, tagged with the specified tag. /// - /// Entity type - /// Optional group - /// - IEnumerable GetTagsForEntityType(TaggableObjectTypes objectType, string group = null); + IEnumerable GetTaggedEntitiesByTag(TaggableObjectTypes objectType, string tag, string group = null, string culture = null); /// - /// Returns all tags that exist on the content item - Content/Media/Member + /// Gets all tags for an entity type. /// - /// The content item id to get tags for - /// Optional group - /// - IEnumerable GetTagsForEntity(int contentId, string group = null); + IEnumerable GetTagsForEntityType(TaggableObjectTypes objectType, string group = null, string culture = null); /// - /// Returns all tags that exist on the content item - Content/Media/Member + /// Gets all tags attached to an entity. /// - /// The content item id to get tags for - /// Optional group - /// - IEnumerable GetTagsForEntity(Guid contentId, string group = null); + IEnumerable GetTagsForEntity(int contentId, string group = null, string culture = null); /// - /// Returns all tags that exist on the content item for the property specified - Content/Media/Member + /// Gets all tags attached to an entity. /// - /// The content item id to get tags for - /// The property alias to get tags for - /// Optional group - /// - IEnumerable GetTagsForProperty(int contentId, string propertyTypeAlias, string group = null); + IEnumerable GetTagsForEntity(Guid contentId, string group = null, string culture = null); /// - /// Returns all tags that exist on the content item for the property specified - Content/Media/Member + /// Gets all tags attached to an entity via a property. /// - /// The content item id to get tags for - /// The property alias to get tags for - /// Optional group - /// - IEnumerable GetTagsForProperty(Guid contentId, string propertyTypeAlias, string group = null); + IEnumerable GetTagsForProperty(int contentId, string propertyTypeAlias, string group = null, string culture = null); + + /// + /// Gets all tags attached to an entity via a property. + /// + IEnumerable GetTagsForProperty(Guid contentId, string propertyTypeAlias, string group = null, string culture = null); #endregion } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs index 58f58c3d84..bd7943ff1d 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs @@ -217,8 +217,26 @@ namespace Umbraco.Core.Persistence.Repositories.Implement foreach (var property in entity.Properties) { var tagConfiguration = property.GetTagConfiguration(); - if (tagConfiguration == null) continue; - tagRepo.Assign(entity.Id, property.PropertyTypeId, property.GetTagsValue().Select(x => new Tag { Group = tagConfiguration.Group, Text = x }), true); + if (tagConfiguration == null) continue; // not a tags property + + if (property.PropertyType.VariesByCulture()) + { + var tags = new List(); + foreach (var pvalue in property.Values) + { + var tagsValue = property.GetTagsValue(pvalue.Culture); + var languageId = LanguageRepository.GetIdByIsoCode(pvalue.Culture); + var cultureTags = tagsValue.Select(x => new Tag { Group = tagConfiguration.Group, Text = x, LanguageId = languageId }); + tags.AddRange(cultureTags); + } + tagRepo.Assign(entity.Id, property.PropertyTypeId, tags); + } + else + { + var tagsValue = property.GetTagsValue(); // strings + var tags = tagsValue.Select(x => new Tag { Group = tagConfiguration.Group, Text = x }); + tagRepo.Assign(entity.Id, property.PropertyTypeId, tags); + } } } @@ -541,16 +559,6 @@ namespace Umbraco.Core.Persistence.Repositories.Implement propertyDataDtos.AddRange(propertyDataDtos2); var properties = PropertyFactory.BuildEntities(compositionProperties, propertyDataDtos, temp.PublishedVersionId, LanguageRepository).ToList(); - // deal with tags - foreach (var property in properties) - { - if (!tagConfigurations.TryGetValue(property.PropertyType.PropertyEditorAlias, out var tagConfiguration)) - continue; - - //fixme doesn't take into account variants - property.SetTagsValue(property.GetValue(), tagConfiguration); - } - if (result.ContainsKey(temp.VersionId)) { if (ContentRepositoryBase.ThrowOnWarning) diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs index 44215b7f7e..662254d1ee 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs @@ -663,9 +663,11 @@ AND umbracoNode.id <> @id", { case ContentVariation.Culture: CopyPropertyData(null, defaultLanguageId, propertyTypeIds, impactedL); + CopyTagData(null, defaultLanguageId, propertyTypeIds, impactedL); break; case ContentVariation.Nothing: CopyPropertyData(defaultLanguageId, null, propertyTypeIds, impactedL); + CopyTagData(defaultLanguageId, null, propertyTypeIds, impactedL); break; case ContentVariation.CultureAndSegment: case ContentVariation.Segment: @@ -757,6 +759,139 @@ AND umbracoNode.id <> @id", } } + /// + private void CopyTagData(int? sourceLanguageId, int? targetLanguageId, IReadOnlyCollection propertyTypeIds, IReadOnlyCollection contentTypeIds = null) + { + // note: important to use SqlNullableEquals for nullable types, cannot directly compare language identifiers + + // fixme - should we batch then? + var whereInArgsCount = propertyTypeIds.Count + (contentTypeIds?.Count ?? 0); + if (whereInArgsCount > 2000) + throw new NotSupportedException("Too many property/content types."); + + // delete existing relations (for target language) + // do *not* delete existing tags + + var sqlSelectTagsToDelete = Sql() + .Select(x => x.Id) + .From() + .InnerJoin().On((tag, rel) => tag.Id == rel.TagId); + + if (contentTypeIds != null) + sqlSelectTagsToDelete + .InnerJoin().On((rel, content) => rel.NodeId == content.NodeId) + .WhereIn(x => x.ContentTypeId, contentTypeIds); + + sqlSelectTagsToDelete + .WhereIn(x => x.PropertyTypeId, propertyTypeIds) + .Where(x => x.LanguageId.SqlNullableEquals(targetLanguageId, -1)); + + var sqlDeleteRelations = Sql() + .Delete() + .WhereIn(x => x.TagId, sqlSelectTagsToDelete); + + Database.Execute(sqlDeleteRelations); + + // do *not* delete the tags - they could be used by other content types / property types + /* + var sqlDeleteTag = Sql() + .Delete() + .WhereIn(x => x.Id, sqlTagToDelete); + Database.Execute(sqlDeleteTag); + */ + + // copy tags from source language to target language + // target tags may exist already, so we have to check for existence here + // + // select tags to insert: tags pointed to by a relation ship, for proper property/content types, + // and of source language, and where we cannot left join to an existing tag with same text, + // group and languageId + + var targetLanguageIdS = targetLanguageId.HasValue ? targetLanguageId.ToString() : "NULL"; + var sqlSelectTagsToInsert = Sql() + .SelectDistinct(x => x.Text, x => x.Group) + .Append(", " + targetLanguageIdS) + .From(); + + sqlSelectTagsToInsert + .InnerJoin().On((tag, rel) => tag.Id == rel.TagId) + .LeftJoin("xtags").On((tag, xtag) => tag.Text == xtag.Text && tag.Group == xtag.Group && xtag.LanguageId.SqlNullableEquals(targetLanguageId, -1), aliasRight: "xtags"); + + if (contentTypeIds != null) + sqlSelectTagsToInsert + .InnerJoin().On((rel, content) => rel.NodeId == content.NodeId) + .WhereIn(x => x.ContentTypeId, contentTypeIds); + + sqlSelectTagsToInsert + .WhereIn(x => x.PropertyTypeId, propertyTypeIds) + .WhereNull(x => x.Id, "xtags") // ie, not exists + .Where(x => x.LanguageId.SqlNullableEquals(sourceLanguageId, -1)); + + var cols = Sql().Columns(x => x.Text, x => x.Group, x => x.LanguageId); + var sqlInsertTags = Sql($"INSERT INTO {TagDto.TableName} ({cols})").Append(sqlSelectTagsToInsert); + + Database.Execute(sqlInsertTags); + + // create relations to new tags + // any existing relations have been deleted above, no need to check for existence here + // + // select node id and property type id from existing relations to tags of source language, + // for proper property/content types, and select new tag id from tags, with matching text, + // and group, but for the target language + + var sqlSelectRelationsToInsert = Sql() + .SelectDistinct(x => x.NodeId, x => x.PropertyTypeId) + .AndSelect("otag", x => x.Id) + .From() + .InnerJoin().On((rel, tag) => rel.TagId == tag.Id) + .InnerJoin("otag").On((tag, otag) => tag.Text == otag.Text && tag.Group == otag.Group && otag.LanguageId.SqlNullableEquals(targetLanguageId, -1), aliasRight: "otag"); + + if (contentTypeIds != null) + sqlSelectRelationsToInsert + .InnerJoin().On((rel, content) => rel.NodeId == content.NodeId) + .WhereIn(x => x.ContentTypeId, contentTypeIds); + + sqlSelectRelationsToInsert + .Where(x => x.LanguageId.SqlNullableEquals(sourceLanguageId, -1)) + .WhereIn(x => x.PropertyTypeId, propertyTypeIds); + + var relationColumnsToInsert = Sql().Columns(x => x.NodeId, x => x.PropertyTypeId, x => x.TagId); + var sqlInsertRelations = Sql($"INSERT INTO {TagRelationshipDto.TableName} ({relationColumnsToInsert})").Append(sqlSelectRelationsToInsert); + + Database.Execute(sqlInsertRelations); + + // delete original relations - *not* the tags - all of them + // cannot really "go back" with relations, would have to do it with property values + + sqlSelectTagsToDelete = Sql() + .Select(x => x.Id) + .From() + .InnerJoin().On((tag, rel) => tag.Id == rel.TagId); + + if (contentTypeIds != null) + sqlSelectTagsToDelete + .InnerJoin().On((rel, content) => rel.NodeId == content.NodeId) + .WhereIn(x => x.ContentTypeId, contentTypeIds); + + sqlSelectTagsToDelete + .WhereIn(x => x.PropertyTypeId, propertyTypeIds) + .Where(x => !x.LanguageId.SqlNullableEquals(targetLanguageId, -1)); + + sqlDeleteRelations = Sql() + .Delete() + .WhereIn(x => x.TagId, sqlSelectTagsToDelete); + + Database.Execute(sqlDeleteRelations); + + // no + /* + var sqlDeleteTag = Sql() + .Delete() + .WhereIn(x => x.Id, sqlTagToDelete); + Database.Execute(sqlDeleteTag); + */ + } + /// /// Copies property data from one language to another. /// @@ -766,6 +901,8 @@ AND umbracoNode.id <> @id", /// The content type identifiers. private void CopyPropertyData(int? sourceLanguageId, int? targetLanguageId, IReadOnlyCollection propertyTypeIds, IReadOnlyCollection contentTypeIds = null) { + // note: important to use SqlNullableEquals for nullable types, cannot directly compare language identifiers + // // fixme - should we batch then? var whereInArgsCount = propertyTypeIds.Count + (contentTypeIds?.Count ?? 0); if (whereInArgsCount > 2000) @@ -793,11 +930,7 @@ AND umbracoNode.id <> @id", sqlDelete.WhereIn(x => x.VersionId, inSql); } - // NPoco cannot turn the clause into IS NULL with a nullable parameter - deal with it - if (targetLanguageId == null) - sqlDelete.Where(x => x.LanguageId == null); - else - sqlDelete.Where(x => x.LanguageId == targetLanguageId); + sqlDelete.Where(x => x.LanguageId.SqlNullableEquals(targetLanguageId, -1)); sqlDelete .WhereIn(x => x.PropertyTypeId, propertyTypeIds); @@ -821,11 +954,7 @@ AND umbracoNode.id <> @id", .InnerJoin().On((pdata, cversion) => pdata.VersionId == cversion.Id) .InnerJoin().On((cversion, c) => cversion.NodeId == c.NodeId); - // NPoco cannot turn the clause into IS NULL with a nullable parameter - deal with it - if (sourceLanguageId == null) - sqlSelectData.Where(x => x.LanguageId == null); - else - sqlSelectData.Where(x => x.LanguageId == sourceLanguageId); + sqlSelectData.Where(x => x.LanguageId.SqlNullableEquals(sourceLanguageId, -1)); sqlSelectData .WhereIn(x => x.PropertyTypeId, propertyTypeIds); diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs index 09fb664ffe..e236670e74 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs @@ -111,7 +111,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement "DELETE FROM umbracoPropertyData WHERE languageId = @id", "DELETE FROM umbracoContentVersionCultureVariation WHERE languageId = @id", "DELETE FROM umbracoDocumentCultureVariation WHERE languageId = @id", - "DELETE FROM umbracoLanguage WHERE id = @id" + "DELETE FROM umbracoLanguage WHERE id = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.Tag + " WHERE languageId = @id" }; return list; } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/TagRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/TagRepository.cs index 418e3d8ac3..77e474be08 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/TagRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/TagRepository.cs @@ -11,6 +11,7 @@ using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Factories; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Scoping; +using static Umbraco.Core.Persistence.NPocoSqlExtensions.Statics; namespace Umbraco.Core.Persistence.Repositories.Implement { @@ -109,74 +110,65 @@ namespace Umbraco.Core.Persistence.Repositories.Implement #region Assign and Remove Tags /// - public void Assign(int contentId, int propertyTypeId, IEnumerable tags, bool replaceTags) + // only invoked from ContentRepositoryBase with all cultures + replaceTags being true + public void Assign(int contentId, int propertyTypeId, IEnumerable tags, bool replaceTags = true) { // to no-duplicates array var tagsA = tags.Distinct(new TagComparer()).ToArray(); - // no tags? - if (tagsA.Length == 0) + // replacing = clear all + if (replaceTags) { - // replacing = clear all - if (replaceTags) - { - var sql0 = Sql().Delete().Where(x => x.NodeId == contentId && x.PropertyTypeId == propertyTypeId); - Database.Execute(sql0); - } - - // nothing else to do - return; + var sql0 = Sql().Delete().Where(x => x.NodeId == contentId && x.PropertyTypeId == propertyTypeId); + Database.Execute(sql0); } + // no tags? nothing else to do + if (tagsA.Length == 0) + return; + // tags // using some clever logic (?) to insert tags that don't exist in 1 query + // must coalesce languageId because equality of NULLs does not exist var tagSetSql = GetTagSet(tagsA); var group = SqlSyntax.GetQuotedColumnName("group"); // insert tags - var sql1 = $@"INSERT INTO cmsTags (tag, {group}) -SELECT tagSet.tag, tagSet.{group} + var sql1 = $@"INSERT INTO cmsTags (tag, {group}, languageId) +SELECT tagSet.tag, tagSet.{group}, tagSet.languageId FROM {tagSetSql} -LEFT OUTER JOIN cmsTags ON (tagSet.tag = cmsTags.tag AND tagSet.{group} = cmsTags.{group}) +LEFT OUTER JOIN cmsTags ON (tagSet.tag = cmsTags.tag AND tagSet.{group} = cmsTags.{group} AND COALESCE(tagSet.languageId, -1) = COALESCE(cmsTags.languageId, -1)) WHERE cmsTags.id IS NULL"; Database.Execute(sql1); - // if replacing, remove everything first - if (replaceTags) - { - var sql2 = Sql().Delete().Where(x => x.NodeId == contentId && x.PropertyTypeId == propertyTypeId); - Database.Execute(sql2); - } - // insert relations - var sql3 = $@"INSERT INTO cmsTagRelationship (nodeId, propertyTypeId, tagId) + var sql2 = $@"INSERT INTO cmsTagRelationship (nodeId, propertyTypeId, tagId) SELECT {contentId}, {propertyTypeId}, tagSet2.Id FROM ( SELECT t.Id FROM {tagSetSql} - INNER JOIN cmsTags as t ON (tagSet.tag = t.tag AND tagSet.{group} = t.{group}) + INNER JOIN cmsTags as t ON (tagSet.tag = t.tag AND tagSet.{group} = t.{group} AND COALESCE(tagSet.languageId, -1) = COALESCE(t.languageId, -1)) ) AS tagSet2 LEFT OUTER JOIN cmsTagRelationship r ON (tagSet2.id = r.tagId AND r.nodeId = {contentId} AND r.propertyTypeID = {propertyTypeId}) WHERE r.tagId IS NULL"; - Database.Execute(sql3); + Database.Execute(sql2); } /// + // only invoked from tests public void Remove(int contentId, int propertyTypeId, IEnumerable tags) { var tagSetSql = GetTagSet(tags); + var group = SqlSyntax.GetQuotedColumnName("group"); - var deleteSql = string.Concat("DELETE FROM cmsTagRelationship WHERE nodeId = ", - contentId, - " AND propertyTypeId = ", - propertyTypeId, - " AND tagId IN ", - "(SELECT id FROM cmsTags INNER JOIN ", - tagSetSql, - " ON (TagSet.Tag = cmsTags.Tag and TagSet." + SqlSyntax.GetQuotedColumnName("group") + @" = cmsTags." + SqlSyntax.GetQuotedColumnName("group") + @"))"); + var deleteSql = $@"DELETE FROM cmsTagRelationship WHERE nodeId = {contentId} AND propertyTypeId = {propertyTypeId} AND tagId IN ( + SELECT id FROM cmsTags INNER JOIN {tagSetSql} ON ( + tagSet.tag = cmsTags.tag AND tagSet.{group} = cmsTags.{group} AND COALESCE(tagSet.languageId, -1) = COALESCE(cmsTags.languageId, -1) + ) + )"; Database.Execute(deleteSql); } @@ -207,13 +199,6 @@ WHERE r.tagId IS NULL"; // private string GetTagSet(IEnumerable tags) { - string EscapeSqlString(string s) - { - // why were we escaping @ symbols? - //return NPocoDatabaseExtensions.EscapeAtSymbols(s.Replace("'", "''")); - return s.Replace("'", "''"); - } - var sql = new StringBuilder(); var group = SqlSyntax.GetQuotedColumnName("group"); var first = true; @@ -226,11 +211,17 @@ WHERE r.tagId IS NULL"; else sql.Append(" UNION "); sql.Append("SELECT N'"); - sql.Append(EscapeSqlString(tag.Text)); + sql.Append(SqlSyntax.EscapeString(tag.Text)); sql.Append("' AS tag, '"); - sql.Append(EscapeSqlString(tag.Group)); + sql.Append(SqlSyntax.EscapeString(tag.Group)); sql.Append("' AS "); sql.Append(group); + sql.Append(" , "); + if (tag.LanguageId.HasValue) + sql.Append(tag.LanguageId); + else + sql.Append("NULL"); + sql.Append(" AS languageId"); } sql.Append(") AS tagSet"); @@ -244,14 +235,17 @@ WHERE r.tagId IS NULL"; public bool Equals(ITag x, ITag y) { return ReferenceEquals(x, y) // takes care of both being null - || x != null && y != null && x.Text == y.Text && x.Group == y.Group; + || x != null && y != null && x.Text == y.Text && x.Group == y.Group && x.LanguageId == y.LanguageId; } public int GetHashCode(ITag obj) { unchecked { - return (obj.Text.GetHashCode() * 397) ^ obj.Group.GetHashCode(); + var h = obj.Text.GetHashCode(); + h = h * 397 ^ obj.Group.GetHashCode(); + h = h * 397 ^ (obj.LanguageId?.GetHashCode() ?? 0); + return h; } } } @@ -264,118 +258,126 @@ WHERE r.tagId IS NULL"; // consider caching implications // add lookups for parentId or path (ie get content in tag group, that are descendants of x) + // ReSharper disable once ClassNeverInstantiated.Local + // ReSharper disable UnusedAutoPropertyAccessor.Local + private class TaggedEntityDto + { + public int NodeId { get; set; } + public string PropertyTypeAlias { get; set; } + public int PropertyTypeId { get; set; } + public int TagId { get; set; } + public string TagText { get; set; } + public string TagGroup { get; set; } + public int? TagLanguage { get; set; } + } + // ReSharper restore UnusedAutoPropertyAccessor.Local + + /// public TaggedEntity GetTaggedEntityByKey(Guid key) { - var sql = Sql() - .Select("cmsTagRelationship.nodeId, cmsPropertyType.Alias, cmsPropertyType.id as propertyTypeId, cmsTags.tag, cmsTags.id as tagId, cmsTags." + SqlSyntax.GetQuotedColumnName("group")) - .From() - .InnerJoin() - .On(left => left.TagId, right => right.Id) - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .InnerJoin() - .On(left => left.Id, right => right.PropertyTypeId) - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) + var sql = GetTaggedEntitiesSql(TaggableObjectTypes.All, "*"); + + sql = sql .Where(dto => dto.UniqueId == key); - return CreateTaggedEntityCollection(Database.Fetch(sql)).FirstOrDefault(); + return Map(Database.Fetch(sql)).FirstOrDefault(); } + /// public TaggedEntity GetTaggedEntityById(int id) { - var sql = Sql() - .Select("cmsTagRelationship.nodeId, cmsPropertyType.Alias, cmsPropertyType.id as propertyTypeId, cmsTags.tag, cmsTags.id as tagId, cmsTags." + SqlSyntax.GetQuotedColumnName("group")) - .From() - .InnerJoin() - .On(left => left.TagId, right => right.Id) - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .InnerJoin() - .On(left => left.Id, right => right.PropertyTypeId) - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) + var sql = GetTaggedEntitiesSql(TaggableObjectTypes.All, "*"); + + sql = sql .Where(dto => dto.NodeId == id); - return CreateTaggedEntityCollection(Database.Fetch(sql)).FirstOrDefault(); + return Map(Database.Fetch(sql)).FirstOrDefault(); } - public IEnumerable GetTaggedEntitiesByTagGroup(TaggableObjectTypes objectType, string tagGroup) + /// + public IEnumerable GetTaggedEntitiesByTagGroup(TaggableObjectTypes objectType, string group, string culture = null) { - var sql = Sql() - .Select("cmsTagRelationship.nodeId, cmsPropertyType.Alias, cmsPropertyType.id as propertyTypeId, cmsTags.tag, cmsTags.id as tagId, cmsTags." + SqlSyntax.GetQuotedColumnName("group")) - .From() - .InnerJoin() - .On(left => left.TagId, right => right.Id) - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .InnerJoin() - .On(left => left.Id, right => right.PropertyTypeId) - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .Where(dto => dto.Group == tagGroup); + var sql = GetTaggedEntitiesSql(objectType, culture); - if (objectType != TaggableObjectTypes.All) - { - var nodeObjectType = GetNodeObjectType(objectType); - sql = sql - .Where(dto => dto.NodeObjectType == nodeObjectType); - } + sql = sql + .Where(x => x.Group == group); - return CreateTaggedEntityCollection( - Database.Fetch(sql)); + return Map(Database.Fetch(sql)); } - public IEnumerable GetTaggedEntitiesByTag(TaggableObjectTypes objectType, string tag, string tagGroup = null) + /// + public IEnumerable GetTaggedEntitiesByTag(TaggableObjectTypes objectType, string tag, string group = null, string culture = null) { - var sql = Sql() - .Select("cmsTagRelationship.nodeId, cmsPropertyType.Alias, cmsPropertyType.id as propertyTypeId, cmsTags.tag, cmsTags.id as tagId, cmsTags." + SqlSyntax.GetQuotedColumnName("group")) - .From() - .InnerJoin() - .On(left => left.TagId, right => right.Id) - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .InnerJoin() - .On(left => left.Id, right => right.PropertyTypeId) - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) + var sql = GetTaggedEntitiesSql(objectType, culture); + + sql = sql .Where(dto => dto.Text == tag); + if (group.IsNullOrWhiteSpace() == false) + sql = sql + .Where(dto => dto.Group == group); + + return Map(Database.Fetch(sql)); + } + + private Sql GetTaggedEntitiesSql(TaggableObjectTypes objectType, string culture) + { + var sql = Sql() + .Select(x => Alias(x.NodeId, "NodeId")) + .AndSelect(x => Alias(x.Alias, "PropertyTypeAlias"), x => Alias(x.Id, "PropertyTypeId")) + .AndSelect(x => Alias(x.Id, "TagId"), x => Alias(x.Text, "TagText"), x => Alias(x.Group, "TagGroup"), x => Alias(x.LanguageId, "TagLanguage")) + .From() + .InnerJoin().On((tag, rel) => tag.Id == rel.TagId) + .InnerJoin().On((rel, content) => rel.NodeId == content.NodeId) + .InnerJoin().On((rel, prop) => rel.PropertyTypeId == prop.Id) + .InnerJoin().On((content, node) => content.NodeId == node.NodeId); + + if (culture == null) + { + sql = sql + .Where(dto => dto.LanguageId == null); + } + else if (culture != "*") + { + sql = sql + .InnerJoin().On((tag, lang) => tag.LanguageId == lang.Id) + .Where(x => x.IsoCode == culture); + } + if (objectType != TaggableObjectTypes.All) { var nodeObjectType = GetNodeObjectType(objectType); - sql = sql - .Where(dto => dto.NodeObjectType == nodeObjectType); + sql = sql.Where(dto => dto.NodeObjectType == nodeObjectType); } - if (tagGroup.IsNullOrWhiteSpace() == false) - { - sql = sql.Where(dto => dto.Group == tagGroup); - } - - return CreateTaggedEntityCollection( - Database.Fetch(sql)); + return sql; } - private IEnumerable CreateTaggedEntityCollection(IEnumerable dbResult) + private static IEnumerable Map(IEnumerable dtos) { - foreach (var node in dbResult.GroupBy(x => (int)x.nodeId)) + return dtos.GroupBy(x => x.NodeId).Select(dtosForNode => { - var properties = new List(); - foreach (var propertyType in node.GroupBy(x => new { id = (int)x.propertyTypeId, alias = (string)x.Alias })) + var taggedProperties = dtosForNode.GroupBy(x => x.PropertyTypeId).Select(dtosForProperty => { - var tags = propertyType.Select(x => new Tag((int)x.tagId, (string)x.group, (string)x.tag)); - properties.Add(new TaggedProperty(propertyType.Key.id, propertyType.Key.alias, tags)); - } - yield return new TaggedEntity(node.Key, properties); - } + string propertyTypeAlias = null; + var tags = dtosForProperty.Select(dto => + { + propertyTypeAlias = dto.PropertyTypeAlias; + return new Tag(dto.TagId, dto.TagGroup, dto.TagText, dto.TagLanguage); + }).ToList(); + return new TaggedProperty(dtosForProperty.Key, propertyTypeAlias, tags); + }).ToList(); + + return new TaggedEntity(dtosForNode.Key, taggedProperties); + }).ToList(); } - public IEnumerable GetTagsForEntityType(TaggableObjectTypes objectType, string group = null) + /// + public IEnumerable GetTagsForEntityType(TaggableObjectTypes objectType, string group = null, string culture = null) { - var sql = GetTagsQuerySelect(true); + var sql = GetTagsSql(culture, true); - sql = ApplyRelationshipJoinToTagsQuery(sql); + AddTagsSqlWhere(sql, culture); if (objectType != TaggableObjectTypes.All) { @@ -384,116 +386,126 @@ WHERE r.tagId IS NULL"; .Where(dto => dto.NodeObjectType == nodeObjectType); } - sql = ApplyGroupFilterToTagsQuery(sql, group); + if (group.IsNullOrWhiteSpace() == false) + sql = sql + .Where(dto => dto.Group == group); - sql = ApplyGroupByToTagsQuery(sql); + sql = sql + .GroupBy(x => x.Id, x => x.Text, x => x.Group, x => x.LanguageId); return ExecuteTagsQuery(sql); } - public IEnumerable GetTagsForEntity(int contentId, string group = null) + /// + public IEnumerable GetTagsForEntity(int contentId, string group = null, string culture = null) { - var sql = GetTagsQuerySelect(); + var sql = GetTagsSql(culture); - sql = ApplyRelationshipJoinToTagsQuery(sql); + AddTagsSqlWhere(sql, culture); sql = sql .Where(dto => dto.NodeId == contentId); - sql = ApplyGroupFilterToTagsQuery(sql, group); + if (group.IsNullOrWhiteSpace() == false) + sql = sql + .Where(dto => dto.Group == group); return ExecuteTagsQuery(sql); } - public IEnumerable GetTagsForEntity(Guid contentId, string group = null) + /// + public IEnumerable GetTagsForEntity(Guid contentId, string group = null, string culture = null) { - var sql = GetTagsQuerySelect(); + var sql = GetTagsSql(culture); - sql = ApplyRelationshipJoinToTagsQuery(sql); + AddTagsSqlWhere(sql, culture); sql = sql .Where(dto => dto.UniqueId == contentId); - sql = ApplyGroupFilterToTagsQuery(sql, group); + if (group.IsNullOrWhiteSpace() == false) + sql = sql + .Where(dto => dto.Group == group); return ExecuteTagsQuery(sql); } - public IEnumerable GetTagsForProperty(int contentId, string propertyTypeAlias, string group = null) + /// + public IEnumerable GetTagsForProperty(int contentId, string propertyTypeAlias, string group = null, string culture = null) { - var sql = GetTagsQuerySelect(); - - sql = ApplyRelationshipJoinToTagsQuery(sql); + var sql = GetTagsSql(culture); sql = sql - .InnerJoin() - .On(left => left.Id, right => right.PropertyTypeId) - .Where(dto => dto.NodeId == contentId) - .Where(dto => dto.Alias == propertyTypeAlias); + .InnerJoin().On((prop, rel) => prop.Id == rel.PropertyTypeId) + .Where(x => x.NodeId == contentId) + .Where(x => x.Alias == propertyTypeAlias); - sql = ApplyGroupFilterToTagsQuery(sql, group); + AddTagsSqlWhere(sql, culture); + + if (group.IsNullOrWhiteSpace() == false) + sql = sql + .Where(dto => dto.Group == group); return ExecuteTagsQuery(sql); } - public IEnumerable GetTagsForProperty(Guid contentId, string propertyTypeAlias, string group = null) + /// + public IEnumerable GetTagsForProperty(Guid contentId, string propertyTypeAlias, string group = null, string culture = null) { - var sql = GetTagsQuerySelect(); - - sql = ApplyRelationshipJoinToTagsQuery(sql); + var sql = GetTagsSql(culture); sql = sql - .InnerJoin() - .On(left => left.Id, right => right.PropertyTypeId) + .InnerJoin().On((prop, rel) => prop.Id == rel.PropertyTypeId) .Where(dto => dto.UniqueId == contentId) .Where(dto => dto.Alias == propertyTypeAlias); - sql = ApplyGroupFilterToTagsQuery(sql, group); + AddTagsSqlWhere(sql, culture); + + if (group.IsNullOrWhiteSpace() == false) + sql = sql + .Where(dto => dto.Group == group); return ExecuteTagsQuery(sql); } - private Sql GetTagsQuerySelect(bool withGrouping = false) + private Sql GetTagsSql(string culture, bool withGrouping = false) { - var sql = Sql(); + var sql = Sql() + .Select(); if (withGrouping) - { - sql = sql.Select("cmsTags.id, cmsTags.tag, cmsTags." + SqlSyntax.GetQuotedColumnName("group") + @", Count(*) NodeCount"); - } - else - { - sql = sql.Select("DISTINCT cmsTags.*"); - } + sql = sql + .AndSelectCount("NodeCount"); - return sql; - } - - private Sql ApplyRelationshipJoinToTagsQuery(Sql sql) - { - return sql + sql = sql .From() - .InnerJoin() - .On(left => left.TagId, right => right.Id) - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId); - } + .InnerJoin().On((rel, tag) => tag.Id == rel.TagId) + .InnerJoin().On((content, rel) => content.NodeId == rel.NodeId) + .InnerJoin().On((node, content) => node.NodeId == content.NodeId); - private Sql ApplyGroupFilterToTagsQuery(Sql sql, string group) - { - if (group.IsNullOrWhiteSpace() == false) + if (culture != null && culture != "*") { - sql = sql.Where(dto => dto.Group == group); + sql = sql + .InnerJoin().On((tag, lang) => tag.LanguageId == lang.Id); } return sql; } - private Sql ApplyGroupByToTagsQuery(Sql sql) + private Sql AddTagsSqlWhere(Sql sql, string culture) { - return sql.GroupBy("cmsTags.id", "cmsTags.tag", "cmsTags." + SqlSyntax.GetQuotedColumnName("group") + @""); + if (culture == null) + { + sql = sql + .Where(dto => dto.LanguageId == null); + } + else if (culture != "*") + { + sql = sql + .Where(x => x.IsoCode == culture); + } + + return sql; } private IEnumerable ExecuteTagsQuery(Sql sql) diff --git a/src/Umbraco.Core/Persistence/SqlTemplate.cs b/src/Umbraco.Core/Persistence/SqlTemplate.cs index 7304f45e7f..e81da20f41 100644 --- a/src/Umbraco.Core/Persistence/SqlTemplate.cs +++ b/src/Umbraco.Core/Persistence/SqlTemplate.cs @@ -95,9 +95,10 @@ namespace Umbraco.Core.Persistence return new Sql(_sqlContext, isBuilt, _sql, args); } - internal void WriteToConsole() + internal string ToText() { - new Sql(_sqlContext, _sql, _args.Values.ToArray()).WriteToConsole(); + var sql = new Sql(_sqlContext, _sql, _args.Values.ToArray()); + return sql.ToText(); } /// diff --git a/src/Umbraco.Core/Persistence/UmbracoDatabase.cs b/src/Umbraco.Core/Persistence/UmbracoDatabase.cs index 64e4c0adca..fdf8061c8e 100644 --- a/src/Umbraco.Core/Persistence/UmbracoDatabase.cs +++ b/src/Umbraco.Core/Persistence/UmbracoDatabase.cs @@ -67,6 +67,24 @@ namespace Umbraco.Core.Persistence /// public ISqlContext SqlContext { get; } + #region Temp + + // work around NPoco issue https://github.com/schotime/NPoco/issues/517 while we wait for the fix + public override DbCommand CreateCommand(DbConnection connection, CommandType commandType, string sql, params object[] args) + { + var command = base.CreateCommand(connection, commandType, sql, args); + + if (!DatabaseType.IsSqlCe()) return command; + + foreach (DbParameter parameter in command.Parameters) + if (parameter.Value == DBNull.Value) + parameter.DbType = DbType.String; + + return command; + } + + #endregion + #region Testing, Debugging and Troubleshooting private bool _enableCount; @@ -228,24 +246,13 @@ namespace Umbraco.Core.Persistence private string CommandToString(string sql, object[] args) { - var sb = new StringBuilder(); + var text = new StringBuilder(); #if DEBUG_DATABASES - sb.Append(InstanceId); - sb.Append(": "); + text.Append(InstanceId); + text.Append(": "); #endif - sb.Append(sql); - if (args.Length > 0) - sb.Append(" --"); - var i = 0; - foreach (var arg in args) - { - sb.Append(" @"); - sb.Append(i++); - sb.Append(":"); - sb.Append(arg); - } - - return sb.ToString(); + NPocoSqlExtensions.ToText(sql, args, text); + return text.ToString(); } protected override void OnExecutedCommand(DbCommand cmd) diff --git a/src/Umbraco.Core/PropertyEditors/PropertyEditorTagsExtensions.cs b/src/Umbraco.Core/PropertyEditors/PropertyEditorTagsExtensions.cs index 8f01bcab5a..3b2a9dd4e2 100644 --- a/src/Umbraco.Core/PropertyEditors/PropertyEditorTagsExtensions.cs +++ b/src/Umbraco.Core/PropertyEditors/PropertyEditorTagsExtensions.cs @@ -9,7 +9,7 @@ /// Determines whether an editor supports tags. /// public static bool IsTagsEditor(this IDataEditor editor) - => editor?.GetType().GetCustomAttribute(false) != null; + => editor.GetTagAttribute() != null; /// /// Gets the tags configuration attribute of an editor. diff --git a/src/Umbraco.Core/Serialization/CaseInsensitiveDictionaryConverter.cs b/src/Umbraco.Core/Serialization/CaseInsensitiveDictionaryConverter.cs new file mode 100644 index 0000000000..a92d562a52 --- /dev/null +++ b/src/Umbraco.Core/Serialization/CaseInsensitiveDictionaryConverter.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using Newtonsoft.Json.Converters; + +namespace Umbraco.Core.Serialization +{ + /// + /// Marks dictionaries so they are deserialized as case-insensitive. + /// + /// + /// [JsonConverter(typeof(CaseInsensitiveDictionaryConverter{PropertyData[]}))] + /// public Dictionary{string, PropertyData[]} PropertyData {{ get; set; }} + /// + public class CaseInsensitiveDictionaryConverter : CustomCreationConverter + { + public override bool CanWrite => false; + + public override bool CanRead => true; + + public override bool CanConvert(Type objectType) => typeof(IDictionary).IsAssignableFrom(objectType); + + public override IDictionary Create(Type objectType) => new Dictionary(StringComparer.OrdinalIgnoreCase); + } +} diff --git a/src/Umbraco.Core/Services/ITagService.cs b/src/Umbraco.Core/Services/ITagService.cs index 63b7ce31a7..6be18624cb 100644 --- a/src/Umbraco.Core/Services/ITagService.cs +++ b/src/Umbraco.Core/Services/ITagService.cs @@ -5,7 +5,7 @@ using Umbraco.Core.Models; namespace Umbraco.Core.Services { /// - /// Tag service to query for tags in the tags db table. The tags returned are only relavent for published content & saved media or members + /// Tag service to query for tags in the tags db table. The tags returned are only relevant for published content & saved media or members /// /// /// If there is unpublished content with tags, those tags will not be contained. @@ -15,135 +15,84 @@ namespace Umbraco.Core.Services /// public interface ITagService : IService { - + /// + /// Gets a tagged entity. + /// TaggedEntity GetTaggedEntityById(int id); + + /// + /// Gets a tagged entity. + /// TaggedEntity GetTaggedEntityByKey(Guid key); /// - /// Gets tagged Content by a specific 'Tag Group'. + /// Gets all documents tagged with any tag in the specified group. /// - /// The contains the Id and Tags of the Content, not the actual Content item. - /// Name of the 'Tag Group' - /// An enumerable list of - IEnumerable GetTaggedContentByTagGroup(string tagGroup); + IEnumerable GetTaggedContentByTagGroup(string group, string culture = null); /// - /// Gets tagged Content by a specific 'Tag' and optional 'Tag Group'. + /// Gets all documents tagged with the specified tag. /// - /// The contains the Id and Tags of the Content, not the actual Content item. - /// Tag - /// Optional name of the 'Tag Group' - /// An enumerable list of - IEnumerable GetTaggedContentByTag(string tag, string tagGroup = null); + IEnumerable GetTaggedContentByTag(string tag, string group = null, string culture = null); /// - /// Gets tagged Media by a specific 'Tag Group'. + /// Gets all media tagged with any tag in the specified group. /// - /// The contains the Id and Tags of the Media, not the actual Media item. - /// Name of the 'Tag Group' - /// An enumerable list of - IEnumerable GetTaggedMediaByTagGroup(string tagGroup); + IEnumerable GetTaggedMediaByTagGroup(string group, string culture = null); /// - /// Gets tagged Media by a specific 'Tag' and optional 'Tag Group'. + /// Gets all media tagged with the specified tag. /// - /// The contains the Id and Tags of the Media, not the actual Media item. - /// Tag - /// Optional name of the 'Tag Group' - /// An enumerable list of - IEnumerable GetTaggedMediaByTag(string tag, string tagGroup = null); + IEnumerable GetTaggedMediaByTag(string tag, string group = null, string culture = null); /// - /// Gets tagged Members by a specific 'Tag Group'. + /// Gets all members tagged with any tag in the specified group. /// - /// The contains the Id and Tags of the Member, not the actual Member item. - /// Name of the 'Tag Group' - /// An enumerable list of - IEnumerable GetTaggedMembersByTagGroup(string tagGroup); + IEnumerable GetTaggedMembersByTagGroup(string group, string culture = null); /// - /// Gets tagged Members by a specific 'Tag' and optional 'Tag Group'. + /// Gets all members tagged with the specified tag. /// - /// The contains the Id and Tags of the Member, not the actual Member item. - /// Tag - /// Optional name of the 'Tag Group' - /// An enumerable list of - IEnumerable GetTaggedMembersByTag(string tag, string tagGroup = null); + IEnumerable GetTaggedMembersByTag(string tag, string group = null, string culture = null); /// - /// Gets every tag stored in the database + /// Gets all tags. /// - /// Optional name of the 'Tag Group' - /// An enumerable list of - IEnumerable GetAllTags(string tagGroup = null); + IEnumerable GetAllTags(string group = null, string culture = null); /// - /// Gets all tags for content items + /// Gets all document tags. /// - /// Use the optional tagGroup parameter to limit the - /// result to a specific 'Tag Group'. - /// Optional name of the 'Tag Group' - /// An enumerable list of - IEnumerable GetAllContentTags(string tagGroup = null); + IEnumerable GetAllContentTags(string group = null, string culture = null); /// - /// Gets all tags for media items + /// Gets all media tags. /// - /// Use the optional tagGroup parameter to limit the - /// result to a specific 'Tag Group'. - /// Optional name of the 'Tag Group' - /// An enumerable list of - IEnumerable GetAllMediaTags(string tagGroup = null); + IEnumerable GetAllMediaTags(string group = null, string culture = null); /// - /// Gets all tags for member items + /// Gets all member tags. /// - /// Use the optional tagGroup parameter to limit the - /// result to a specific 'Tag Group'. - /// Optional name of the 'Tag Group' - /// An enumerable list of - IEnumerable GetAllMemberTags(string tagGroup = null); + IEnumerable GetAllMemberTags(string group = null, string culture = null); /// - /// Gets all tags attached to a property by entity id + /// Gets all tags attached to an entity via a property. /// - /// Use the optional tagGroup parameter to limit the - /// result to a specific 'Tag Group'. - /// The content item id to get tags for - /// Property type alias - /// Optional name of the 'Tag Group' - /// An enumerable list of - IEnumerable GetTagsForProperty(int contentId, string propertyTypeAlias, string tagGroup = null); + IEnumerable GetTagsForProperty(int contentId, string propertyTypeAlias, string group = null, string culture = null); /// - /// Gets all tags attached to an entity (content, media or member) by entity id + /// Gets all tags attached to an entity. /// - /// Use the optional tagGroup parameter to limit the - /// result to a specific 'Tag Group'. - /// The content item id to get tags for - /// Optional name of the 'Tag Group' - /// An enumerable list of - IEnumerable GetTagsForEntity(int contentId, string tagGroup = null); + IEnumerable GetTagsForEntity(int contentId, string group = null, string culture = null); /// - /// Gets all tags attached to a property by entity id + /// Gets all tags attached to an entity via a property. /// - /// Use the optional tagGroup parameter to limit the - /// result to a specific 'Tag Group'. - /// The content item id to get tags for - /// Property type alias - /// Optional name of the 'Tag Group' - /// An enumerable list of - IEnumerable GetTagsForProperty(Guid contentId, string propertyTypeAlias, string tagGroup = null); + IEnumerable GetTagsForProperty(Guid contentId, string propertyTypeAlias, string group = null, string culture = null); /// - /// Gets all tags attached to an entity (content, media or member) by entity id + /// Gets all tags attached to an entity. /// - /// Use the optional tagGroup parameter to limit the - /// result to a specific 'Tag Group'. - /// The content item id to get tags for - /// Optional name of the 'Tag Group' - /// An enumerable list of - IEnumerable GetTagsForEntity(Guid contentId, string tagGroup = null); + IEnumerable GetTagsForEntity(Guid contentId, string group = null, string culture = null); } } diff --git a/src/Umbraco.Core/Services/Implement/TagService.cs b/src/Umbraco.Core/Services/Implement/TagService.cs index b2395502dc..e888258067 100644 --- a/src/Umbraco.Core/Services/Implement/TagService.cs +++ b/src/Umbraco.Core/Services/Implement/TagService.cs @@ -25,230 +25,147 @@ namespace Umbraco.Core.Services.Implement _tagRepository = tagRepository; } + /// public TaggedEntity GetTaggedEntityById(int id) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (ScopeProvider.CreateScope(autoComplete: true)) { return _tagRepository.GetTaggedEntityById(id); } } + /// public TaggedEntity GetTaggedEntityByKey(Guid key) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (ScopeProvider.CreateScope(autoComplete: true)) { return _tagRepository.GetTaggedEntityByKey(key); } } - /// - /// Gets tagged Content by a specific 'Tag Group'. - /// - /// The contains the Id and Tags of the Content, not the actual Content item. - /// Name of the 'Tag Group' - /// An enumerable list of - public IEnumerable GetTaggedContentByTagGroup(string tagGroup) + /// + public IEnumerable GetTaggedContentByTagGroup(string group, string culture = null) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (ScopeProvider.CreateScope(autoComplete: true)) { - return _tagRepository.GetTaggedEntitiesByTagGroup(TaggableObjectTypes.Content, tagGroup); + return _tagRepository.GetTaggedEntitiesByTagGroup(TaggableObjectTypes.Content, group, culture); } } - /// - /// Gets tagged Content by a specific 'Tag' and optional 'Tag Group'. - /// - /// The contains the Id and Tags of the Content, not the actual Content item. - /// Tag - /// Optional name of the 'Tag Group' - /// An enumerable list of - public IEnumerable GetTaggedContentByTag(string tag, string tagGroup = null) + /// + public IEnumerable GetTaggedContentByTag(string tag, string group = null, string culture = null) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (ScopeProvider.CreateScope(autoComplete: true)) { - return _tagRepository.GetTaggedEntitiesByTag(TaggableObjectTypes.Content, tag, tagGroup); + return _tagRepository.GetTaggedEntitiesByTag(TaggableObjectTypes.Content, tag, group, culture); } } - /// - /// Gets tagged Media by a specific 'Tag Group'. - /// - /// The contains the Id and Tags of the Media, not the actual Media item. - /// Name of the 'Tag Group' - /// An enumerable list of - public IEnumerable GetTaggedMediaByTagGroup(string tagGroup) + /// + public IEnumerable GetTaggedMediaByTagGroup(string group, string culture = null) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (ScopeProvider.CreateScope(autoComplete: true)) { - return _tagRepository.GetTaggedEntitiesByTagGroup(TaggableObjectTypes.Media, tagGroup); + return _tagRepository.GetTaggedEntitiesByTagGroup(TaggableObjectTypes.Media, group, culture); } } - /// - /// Gets tagged Media by a specific 'Tag' and optional 'Tag Group'. - /// - /// The contains the Id and Tags of the Media, not the actual Media item. - /// Tag - /// Optional name of the 'Tag Group' - /// An enumerable list of - public IEnumerable GetTaggedMediaByTag(string tag, string tagGroup = null) + /// + public IEnumerable GetTaggedMediaByTag(string tag, string group = null, string culture = null) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (ScopeProvider.CreateScope(autoComplete: true)) { - return _tagRepository.GetTaggedEntitiesByTag(TaggableObjectTypes.Media, tag, tagGroup); + return _tagRepository.GetTaggedEntitiesByTag(TaggableObjectTypes.Media, tag, group, culture); } } - /// - /// Gets tagged Members by a specific 'Tag Group'. - /// - /// The contains the Id and Tags of the Member, not the actual Member item. - /// Name of the 'Tag Group' - /// An enumerable list of - public IEnumerable GetTaggedMembersByTagGroup(string tagGroup) + /// + public IEnumerable GetTaggedMembersByTagGroup(string group, string culture = null) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (ScopeProvider.CreateScope(autoComplete: true)) { - return _tagRepository.GetTaggedEntitiesByTagGroup(TaggableObjectTypes.Member, tagGroup); + return _tagRepository.GetTaggedEntitiesByTagGroup(TaggableObjectTypes.Member, group, culture); } } - /// - /// Gets tagged Members by a specific 'Tag' and optional 'Tag Group'. - /// - /// The contains the Id and Tags of the Member, not the actual Member item. - /// Tag - /// Optional name of the 'Tag Group' - /// An enumerable list of - public IEnumerable GetTaggedMembersByTag(string tag, string tagGroup = null) + /// + public IEnumerable GetTaggedMembersByTag(string tag, string group = null, string culture = null) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (ScopeProvider.CreateScope(autoComplete: true)) { - return _tagRepository.GetTaggedEntitiesByTag(TaggableObjectTypes.Member, tag, tagGroup); + return _tagRepository.GetTaggedEntitiesByTag(TaggableObjectTypes.Member, tag, group, culture); } } - /// - /// Gets every tag stored in the database - /// - /// Optional name of the 'Tag Group' - /// An enumerable list of - public IEnumerable GetAllTags(string tagGroup = null) + /// + public IEnumerable GetAllTags(string group = null, string culture = null) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (ScopeProvider.CreateScope(autoComplete: true)) { - return _tagRepository.GetTagsForEntityType(TaggableObjectTypes.All, tagGroup); + return _tagRepository.GetTagsForEntityType(TaggableObjectTypes.All, group, culture); } } - /// - /// Gets all tags for content items - /// - /// Use the optional tagGroup parameter to limit the - /// result to a specific 'Tag Group'. - /// Optional name of the 'Tag Group' - /// An enumerable list of - public IEnumerable GetAllContentTags(string tagGroup = null) + /// + public IEnumerable GetAllContentTags(string group = null, string culture = null) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (ScopeProvider.CreateScope(autoComplete: true)) { - return _tagRepository.GetTagsForEntityType(TaggableObjectTypes.Content, tagGroup); + return _tagRepository.GetTagsForEntityType(TaggableObjectTypes.Content, group, culture); } } - /// - /// Gets all tags for media items - /// - /// Use the optional tagGroup parameter to limit the - /// result to a specific 'Tag Group'. - /// Optional name of the 'Tag Group' - /// An enumerable list of - public IEnumerable GetAllMediaTags(string tagGroup = null) + /// + public IEnumerable GetAllMediaTags(string group = null, string culture = null) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (ScopeProvider.CreateScope(autoComplete: true)) { - return _tagRepository.GetTagsForEntityType(TaggableObjectTypes.Media, tagGroup); + return _tagRepository.GetTagsForEntityType(TaggableObjectTypes.Media, group, culture); } } - /// - /// Gets all tags for member items - /// - /// Use the optional tagGroup parameter to limit the - /// result to a specific 'Tag Group'. - /// Optional name of the 'Tag Group' - /// An enumerable list of - public IEnumerable GetAllMemberTags(string tagGroup = null) + /// + public IEnumerable GetAllMemberTags(string group = null, string culture = null) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (ScopeProvider.CreateScope(autoComplete: true)) { - return _tagRepository.GetTagsForEntityType(TaggableObjectTypes.Member, tagGroup); + return _tagRepository.GetTagsForEntityType(TaggableObjectTypes.Member, group, culture); } } - /// - /// Gets all tags attached to a property by entity id - /// - /// Use the optional tagGroup parameter to limit the - /// result to a specific 'Tag Group'. - /// The content item id to get tags for - /// Property type alias - /// Optional name of the 'Tag Group' - /// An enumerable list of - public IEnumerable GetTagsForProperty(int contentId, string propertyTypeAlias, string tagGroup = null) + /// + public IEnumerable GetTagsForProperty(int contentId, string propertyTypeAlias, string group = null, string culture = null) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (ScopeProvider.CreateScope(autoComplete: true)) { - return _tagRepository.GetTagsForProperty(contentId, propertyTypeAlias, tagGroup); + return _tagRepository.GetTagsForProperty(contentId, propertyTypeAlias, group, culture); } } - /// - /// Gets all tags attached to an entity (content, media or member) by entity id - /// - /// Use the optional tagGroup parameter to limit the - /// result to a specific 'Tag Group'. - /// The content item id to get tags for - /// Optional name of the 'Tag Group' - /// An enumerable list of - public IEnumerable GetTagsForEntity(int contentId, string tagGroup = null) + /// + public IEnumerable GetTagsForEntity(int contentId, string group = null, string culture = null) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (ScopeProvider.CreateScope(autoComplete: true)) { - return _tagRepository.GetTagsForEntity(contentId, tagGroup); + return _tagRepository.GetTagsForEntity(contentId, group, culture); } } - /// - /// Gets all tags attached to a property by entity id - /// - /// Use the optional tagGroup parameter to limit the - /// result to a specific 'Tag Group'. - /// The content item id to get tags for - /// Property type alias - /// Optional name of the 'Tag Group' - /// An enumerable list of - public IEnumerable GetTagsForProperty(Guid contentId, string propertyTypeAlias, string tagGroup = null) + /// + public IEnumerable GetTagsForProperty(Guid contentId, string propertyTypeAlias, string group = null, string culture = null) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (ScopeProvider.CreateScope(autoComplete: true)) { - return _tagRepository.GetTagsForProperty(contentId, propertyTypeAlias, tagGroup); + return _tagRepository.GetTagsForProperty(contentId, propertyTypeAlias, group, culture); } } - /// - /// Gets all tags attached to an entity (content, media or member) by entity id - /// - /// Use the optional tagGroup parameter to limit the - /// result to a specific 'Tag Group'. - /// The content item id to get tags for - /// Optional name of the 'Tag Group' - /// An enumerable list of - public IEnumerable GetTagsForEntity(Guid contentId, string tagGroup = null) + /// + public IEnumerable GetTagsForEntity(Guid contentId, string group = null, string culture = null) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (ScopeProvider.CreateScope(autoComplete: true)) { - return _tagRepository.GetTagsForEntity(contentId, tagGroup); + return _tagRepository.GetTagsForEntity(contentId, group, culture); } } } diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 323fc49543..c5319afb51 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -380,6 +380,7 @@ + @@ -1322,6 +1323,7 @@ + diff --git a/src/Umbraco.Examine/ExamineExtensions.cs b/src/Umbraco.Examine/ExamineExtensions.cs index 3681979267..71e3e65c21 100644 --- a/src/Umbraco.Examine/ExamineExtensions.cs +++ b/src/Umbraco.Examine/ExamineExtensions.cs @@ -1,8 +1,11 @@ using System; +using System.Linq; +using Examine; using Examine.LuceneEngine.Providers; using Lucene.Net.Index; using Lucene.Net.Search; using Lucene.Net.Store; +using Umbraco.Core.Logging; namespace Umbraco.Examine { @@ -11,6 +14,32 @@ namespace Umbraco.Examine /// internal static class ExamineExtensions { + /// + /// Forcibly unlocks all lucene based indexes + /// + /// + /// This is not thread safe, use with care + /// + internal static void UnlockLuceneIndexes(this IExamineManager examineManager, ILogger logger) + { + foreach (var luceneIndexer in examineManager.Indexes.OfType()) + { + //We now need to disable waiting for indexing for Examine so that the appdomain is shutdown immediately and doesn't wait for pending + //indexing operations. We used to wait for indexing operations to complete but this can cause more problems than that is worth because + //that could end up halting shutdown for a very long time causing overlapping appdomains and many other problems. + luceneIndexer.WaitForIndexQueueOnShutdown = false; + + //we should check if the index is locked ... it shouldn't be! We are using simple fs lock now and we are also ensuring that + //the indexes are not operational unless MainDom is true + var dir = luceneIndexer.GetLuceneDirectory(); + if (IndexWriter.IsLocked(dir)) + { + logger.Info(typeof(ExamineExtensions), "Forcing index {IndexerName} to be unlocked since it was left in a locked state", luceneIndexer.Name); + IndexWriter.Unlock(dir); + } + } + } + /// /// Checks if the index can be read/opened /// diff --git a/src/Umbraco.Examine/IIndexCreator.cs b/src/Umbraco.Examine/IIndexCreator.cs new file mode 100644 index 0000000000..3b8f683990 --- /dev/null +++ b/src/Umbraco.Examine/IIndexCreator.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using Examine; + +namespace Umbraco.Examine +{ + /// + /// Creates 's + /// + public interface IIndexCreator + { + IEnumerable Create(); + } +} diff --git a/src/Umbraco.Examine/IIndexDiagnostics.cs b/src/Umbraco.Examine/IIndexDiagnostics.cs index 04ca4a6ab9..29d530c2d0 100644 --- a/src/Umbraco.Examine/IIndexDiagnostics.cs +++ b/src/Umbraco.Examine/IIndexDiagnostics.cs @@ -1,12 +1,9 @@ using System; using System.Collections.Generic; -using Examine; using Umbraco.Core; namespace Umbraco.Examine { - - /// /// Exposes diagnostic information about an index /// diff --git a/src/Umbraco.Examine/IValueSetBuilder.cs b/src/Umbraco.Examine/IValueSetBuilder.cs index 89aa907926..1c4890f404 100644 --- a/src/Umbraco.Examine/IValueSetBuilder.cs +++ b/src/Umbraco.Examine/IValueSetBuilder.cs @@ -5,18 +5,17 @@ using Umbraco.Core.Models; namespace Umbraco.Examine { /// - /// Creates a collection of to be indexed based on a collection of + /// Creates a collection of to be indexed based on a collection of /// - /// - public interface IValueSetBuilder - where TContent : IContentBase + /// + public interface IValueSetBuilder { /// - /// Creates a collection of to be indexed based on a collection of + /// Creates a collection of to be indexed based on a collection of /// /// /// - IEnumerable GetValueSets(params TContent[] content); + IEnumerable GetValueSets(params T[] content); } } diff --git a/src/Umbraco.Examine/LuceneIndexCreator.cs b/src/Umbraco.Examine/LuceneIndexCreator.cs new file mode 100644 index 0000000000..572de1e8a8 --- /dev/null +++ b/src/Umbraco.Examine/LuceneIndexCreator.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using System.IO; +using Examine; +using Examine.LuceneEngine.Directories; +using Lucene.Net.Store; +using Umbraco.Core.IO; + +namespace Umbraco.Examine +{ + /// + /// + /// Abstract class for creating Lucene based Indexes + /// + public abstract class LuceneIndexCreator : IIndexCreator + { + public abstract IEnumerable Create(); + + /// + /// Creates a file system based Lucene with the correct locking guidelines for Umbraco + /// + /// + /// + public virtual Lucene.Net.Store.Directory CreateFileSystemLuceneDirectory(string name) + { + //TODO: We should have a single AppSetting to be able to specify a default DirectoryFactory so we can have a single + //setting to configure all indexes that use this to easily swap the directory to Sync/%temp%/blog, etc... + + var dirInfo = new DirectoryInfo(Path.Combine(IOHelper.MapPath(SystemDirectories.Data), "TEMP", "ExamineIndexes", name)); + if (!dirInfo.Exists) + System.IO.Directory.CreateDirectory(dirInfo.FullName); + + var luceneDir = new SimpleFSDirectory(dirInfo); + + //we want to tell examine to use a different fs lock instead of the default NativeFSFileLock which could cause problems if the appdomain + //terminates and in some rare cases would only allow unlocking of the file if IIS is forcefully terminated. Instead we'll rely on the simplefslock + //which simply checks the existence of the lock file + // The full syntax of this is: new NoPrefixSimpleFsLockFactory(dirInfo) + // however, we are setting the DefaultLockFactory in startup so we'll use that instead since it can be managed globally. + luceneDir.SetLockFactory(DirectoryFactory.DefaultLockFactory(dirInfo)); + return luceneDir; + } + } +} diff --git a/src/Umbraco.Examine/Properties/AssemblyInfo.cs b/src/Umbraco.Examine/Properties/AssemblyInfo.cs index 6713111968..5c42a236f4 100644 --- a/src/Umbraco.Examine/Properties/AssemblyInfo.cs +++ b/src/Umbraco.Examine/Properties/AssemblyInfo.cs @@ -8,6 +8,7 @@ using System.Runtime.CompilerServices; // Umbraco Cms [assembly: InternalsVisibleTo("Umbraco.Tests")] +[assembly: InternalsVisibleTo("Umbraco.Web")] // code analysis // IDE1006 is broken, wants _value syntax for consts, etc - and it's even confusing ppl at MS, kill it diff --git a/src/Umbraco.Examine/Umbraco.Examine.csproj b/src/Umbraco.Examine/Umbraco.Examine.csproj index 0aedf6e754..66b1f09068 100644 --- a/src/Umbraco.Examine/Umbraco.Examine.csproj +++ b/src/Umbraco.Examine/Umbraco.Examine.csproj @@ -66,6 +66,7 @@ + @@ -88,6 +89,7 @@ + Properties\SolutionInfo.cs diff --git a/src/Umbraco.Tests.Benchmarks/CtorInvokeBenchmarks.cs b/src/Umbraco.Tests.Benchmarks/CtorInvokeBenchmarks.cs index 5c9541cd92..3647b57ae8 100644 --- a/src/Umbraco.Tests.Benchmarks/CtorInvokeBenchmarks.cs +++ b/src/Umbraco.Tests.Benchmarks/CtorInvokeBenchmarks.cs @@ -11,7 +11,13 @@ using Umbraco.Core; namespace Umbraco.Tests.Benchmarks { - [Config(typeof(Config))] + // some conclusions + // - ActivatorCreateInstance is slow + // - it's faster to get+invoke the ctor + // - emitting the ctor is unless if invoked only 1 + + //[Config(typeof(Config))] + [MemoryDiagnoser] public class CtorInvokeBenchmarks { private class Config : ManualConfig @@ -158,6 +164,28 @@ namespace Umbraco.Tests.Benchmarks var foo = new Foo(_foo); } + [Benchmark] + public void EmitCtor() + { + var ctor = ReflectionUtilities.EmitConstuctor>(); + var foo = ctor(_foo); + } + + [Benchmark] + public void ActivatorCreateInstance() + { + var foo = Activator.CreateInstance(typeof(Foo), _foo); + } + + [Benchmark] + public void GetAndInvokeCtor() + { + var ctorArgTypes = new[] { typeof(IFoo) }; + var type = typeof(Foo); + var ctorInfo = type.GetConstructor(ctorArgTypes); + var foo = ctorInfo.Invoke(new object[] { _foo }); + } + [Benchmark] public void InvokeCtor() { diff --git a/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj b/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj index bb14fb5a77..233da0d14b 100644 --- a/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj +++ b/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj @@ -90,7 +90,7 @@ - 0.11.2 + 0.11.3 diff --git a/src/Umbraco.Tests/Composing/TypeLoaderTests.cs b/src/Umbraco.Tests/Composing/TypeLoaderTests.cs index 79f608c1b5..6165ecdac9 100644 --- a/src/Umbraco.Tests/Composing/TypeLoaderTests.cs +++ b/src/Umbraco.Tests/Composing/TypeLoaderTests.cs @@ -23,6 +23,7 @@ namespace Umbraco.Tests.Composing public class TypeLoaderTests { private TypeLoader _typeLoader; + [SetUp] public void Initialize() { @@ -54,6 +55,12 @@ namespace Umbraco.Tests.Composing public void TearDown() { _typeLoader = null; + + + // cleanup + var assDir = new FileInfo(Assembly.GetExecutingAssembly().Location).Directory; + foreach (var d in Directory.GetDirectories(Path.Combine(assDir.FullName, "TypeLoader"))) + Directory.Delete(d, true); } private DirectoryInfo PrepareFolder() diff --git a/src/Umbraco.Tests/Migrations/AdvancedMigrationTests.cs b/src/Umbraco.Tests/Migrations/AdvancedMigrationTests.cs index 5505d7ecd7..5ff0dcffc1 100644 --- a/src/Umbraco.Tests/Migrations/AdvancedMigrationTests.cs +++ b/src/Umbraco.Tests/Migrations/AdvancedMigrationTests.cs @@ -5,6 +5,7 @@ using NUnit.Framework; using Umbraco.Core.Logging; using Umbraco.Core.Migrations; using Umbraco.Core.Migrations.Install; +using Umbraco.Core.Migrations.Upgrade; using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Services; @@ -34,11 +35,12 @@ namespace Umbraco.Tests.Migrations using (var scope = ScopeProvider.CreateScope()) { - var upgrader = new MigrationTests.TestUpgrader(ScopeProvider, builder, Mock.Of(), new PostMigrationCollection(Enumerable.Empty()), logger, - new MigrationPlan("test", builder, logger) - .Add(string.Empty, "done")); + var upgrader = new Upgrader( + new MigrationPlan("test") + .From(string.Empty) + .To("done")); - upgrader.Execute(); + upgrader.Execute(ScopeProvider, builder, Mock.Of(), logger); var helper = new DatabaseSchemaCreator(scope.Database, logger); var exists = helper.TableExists("umbracoUser"); @@ -71,12 +73,13 @@ namespace Umbraco.Tests.Migrations using (var scope = ScopeProvider.CreateScope()) { - var upgrader = new MigrationTests.TestUpgrader(ScopeProvider, builder, Mock.Of(), new PostMigrationCollection(Enumerable.Empty()), logger, - new MigrationPlan("test", builder, logger) - .Add(string.Empty, "a") - .Add("a", "done")); + var upgrader = new Upgrader( + new MigrationPlan("test") + .From(string.Empty) + .To("a") + .To("done")); - upgrader.Execute(); + upgrader.Execute(ScopeProvider, builder, Mock.Of(), logger); scope.Complete(); } } @@ -106,13 +109,14 @@ namespace Umbraco.Tests.Migrations using (var scope = ScopeProvider.CreateScope()) { - var upgrader = new MigrationTests.TestUpgrader(ScopeProvider, builder, Mock.Of(), new PostMigrationCollection(Enumerable.Empty()), logger, - new MigrationPlan("test", builder, logger) - .Add(string.Empty, "a") - .Add("a", "b") - .Add("b", "done")); + var upgrader = new Upgrader( + new MigrationPlan("test") + .From(string.Empty) + .To("a") + .To("b") + .To("done")); - upgrader.Execute(); + upgrader.Execute(ScopeProvider, builder, Mock.Of(), logger); scope.Complete(); } } @@ -142,13 +146,14 @@ namespace Umbraco.Tests.Migrations using (var scope = ScopeProvider.CreateScope()) { - var upgrader = new MigrationTests.TestUpgrader(ScopeProvider, builder, Mock.Of(), new PostMigrationCollection(Enumerable.Empty()), logger, - new MigrationPlan("test", builder, logger) - .Add(string.Empty, "a") - .Add("a", "b") - .Add("b", "done")); + var upgrader = new Upgrader( + new MigrationPlan("test") + .From(string.Empty) + .To("a") + .To("b") + .To("done")); - upgrader.Execute(); + upgrader.Execute(ScopeProvider, builder, Mock.Of(), logger); scope.Complete(); } } @@ -176,12 +181,13 @@ namespace Umbraco.Tests.Migrations using (var scope = ScopeProvider.CreateScope()) { - var upgrader = new MigrationTests.TestUpgrader(ScopeProvider, builder, Mock.Of(), new PostMigrationCollection(Enumerable.Empty()), logger, - new MigrationPlan("test", builder, logger) - .Add(string.Empty, "a") - .Add("a", "done")); + var upgrader = new Upgrader( + new MigrationPlan("test") + .From(string.Empty) + .To("a") + .To("done")); - upgrader.Execute(); + upgrader.Execute(ScopeProvider, builder, Mock.Of(), logger); scope.Complete(); } } diff --git a/src/Umbraco.Tests/Migrations/MigrationPlanTests.cs b/src/Umbraco.Tests/Migrations/MigrationPlanTests.cs index ee1b2a56f5..9aec42c252 100644 --- a/src/Umbraco.Tests/Migrations/MigrationPlanTests.cs +++ b/src/Umbraco.Tests/Migrations/MigrationPlanTests.cs @@ -1,7 +1,9 @@ using System; +using System.Linq; using Moq; using NPoco; using NUnit.Framework; +using Umbraco.Core; using Umbraco.Core.Logging; using Umbraco.Core.Migrations; using Umbraco.Core.Migrations.Upgrade; @@ -46,13 +48,10 @@ namespace Umbraco.Tests.Migrations } }); - // fixme - NOT a migration collection builder, just a migration builder - // done, remove everywhere else, and delete migrationCollection stuff entirely - - var plan = new MigrationPlan("default", migrationBuilder, logger) + var plan = new MigrationPlan("default") .From(string.Empty) - .Chain("{4A9A1A8F-0DA1-4BCF-AD06-C19D79152E35}") - .Chain("VERSION.33"); + .To("{4A9A1A8F-0DA1-4BCF-AD06-C19D79152E35}") + .To("VERSION.33"); var kvs = Mock.Of(); Mock.Get(kvs).Setup(x => x.GetValue(It.IsAny())).Returns(k => k == "Umbraco.Tests.MigrationPlan" ? string.Empty : null); @@ -64,7 +63,7 @@ namespace Umbraco.Tests.Migrations var sourceState = kvs.GetValue("Umbraco.Tests.MigrationPlan") ?? string.Empty; // execute plan - state = plan.Execute(s, sourceState); + state = plan.Execute(s, sourceState, migrationBuilder, logger); // save new state kvs.SetValue("Umbraco.Tests.MigrationPlan", sourceState, state); @@ -81,63 +80,103 @@ namespace Umbraco.Tests.Migrations [Test] public void CanAddMigrations() { - var plan = new MigrationPlan("default", Mock.Of(), Mock.Of()); - plan.Add(string.Empty, "aaa"); - plan.Add("aaa", "bbb"); - plan.Add("bbb", "ccc"); + var plan = new MigrationPlan("default"); + plan + .From(string.Empty) + .To("aaa") + .To("bbb") + .To("ccc"); } [Test] public void CannotTransitionToSameState() { - var plan = new MigrationPlan("default", Mock.Of(), Mock.Of()); + var plan = new MigrationPlan("default"); Assert.Throws(() => { - plan.Add("aaa", "aaa"); + plan.From("aaa").To("aaa"); }); } [Test] public void OnlyOneTransitionPerState() { - var plan = new MigrationPlan("default", Mock.Of(), Mock.Of()); - plan.Add("aaa", "bbb"); + var plan = new MigrationPlan("default"); + plan.From("aaa").To("bbb"); Assert.Throws(() => { - plan.Add("aaa", "ccc"); + plan.From("aaa").To("ccc"); }); } [Test] public void CannotContainTwoMoreHeads() { - var plan = new MigrationPlan("default", Mock.Of(), Mock.Of()); - plan.Add(string.Empty, "aaa"); - plan.Add("aaa", "bbb"); - plan.Add("ccc", "ddd"); + var plan = new MigrationPlan("default"); + plan + .From(string.Empty) + .To("aaa") + .To("bbb") + .From("ccc") + .To("ddd"); Assert.Throws(() => plan.Validate()); } [Test] public void CannotContainLoops() { - var plan = new MigrationPlan("default", Mock.Of(), Mock.Of()); - plan.Add(string.Empty, "aaa"); - plan.Add("aaa", "bbb"); - plan.Add("bbb", "ccc"); - plan.Add("ccc", "aaa"); + var plan = new MigrationPlan("default"); + plan + .From("aaa") + .To("bbb") + .To("ccc") + .To("aaa"); Assert.Throws(() => plan.Validate()); } [Test] public void ValidateUmbracoPlan() { - var plan = new UmbracoPlan(Mock.Of(), Mock.Of()); + var plan = new UmbracoPlan(); plan.Validate(); Console.WriteLine(plan.FinalState); Assert.IsFalse(string.IsNullOrWhiteSpace(plan.FinalState)); } + [Test] + public void CanCopyChain() + { + var plan = new MigrationPlan("default"); + plan + .From(string.Empty) + .To("aaa") + .To("bbb") + .To("ccc") + .To("ddd") + .To("eee"); + + plan + .From("xxx") + .To("yyy", "bbb", "ddd") + .To("eee"); + + WritePlanToConsole(plan); + + plan.Validate(); + Assert.AreEqual("eee", plan.FollowPath("xxx")); + Assert.AreEqual("yyy", plan.FollowPath("xxx", "yyy")); + } + + private void WritePlanToConsole(MigrationPlan plan) + { + var final = plan.Transitions.First(x => x.Value == null).Key; + + Console.WriteLine("plan \"{0}\" to final state \"{1}\":", plan.Name, final); + foreach (var (_, transition) in plan.Transitions) + if (transition != null) + Console.WriteLine(transition); + } + public class DeleteRedirectUrlTable : MigrationBase { public DeleteRedirectUrlTable(IMigrationContext context) diff --git a/src/Umbraco.Tests/Migrations/MigrationTests.cs b/src/Umbraco.Tests/Migrations/MigrationTests.cs index d06cf2244e..8e84f88265 100644 --- a/src/Umbraco.Tests/Migrations/MigrationTests.cs +++ b/src/Umbraco.Tests/Migrations/MigrationTests.cs @@ -16,28 +16,32 @@ namespace Umbraco.Tests.Migrations [TestFixture] public class MigrationTests { - public class TestUpgrader : Upgrader + public class TestUpgraderWithPostMigrations : Upgrader { - private readonly MigrationPlan _plan; + private PostMigrationCollection _postMigrations; - public TestUpgrader(IScopeProvider scopeProvider, IMigrationBuilder migrationBuilder, IKeyValueService keyValueService, PostMigrationCollection postMigrations, ILogger logger, MigrationPlan plan) - : base(scopeProvider, migrationBuilder, keyValueService, postMigrations, logger) + public TestUpgraderWithPostMigrations(MigrationPlan plan) + : base(plan) + { } + + public void Execute(IScopeProvider scopeProvider, IMigrationBuilder migrationBuilder, IKeyValueService keyValueService, ILogger logger, PostMigrationCollection postMigrations) { - _plan = plan; + _postMigrations = postMigrations; + Execute(scopeProvider, migrationBuilder, keyValueService, logger); } - protected override MigrationPlan GetPlan() + public override void AfterMigrations(IScope scope, ILogger logger) { - return _plan; - } + // run post-migrations + var originVersion = new SemVersion(0); + var targetVersion = new SemVersion(0); - protected override (SemVersion, SemVersion) GetVersions() - { - return (new SemVersion(0), new SemVersion(0)); + // run post-migrations + foreach (var postMigration in _postMigrations) + postMigration.Execute(Name, scope, originVersion, targetVersion, logger); } } - public class TestScopeProvider : IScopeProvider { private readonly IScope _scope; diff --git a/src/Umbraco.Tests/Migrations/PostMigrationTests.cs b/src/Umbraco.Tests/Migrations/PostMigrationTests.cs index d94a1cc917..95f1f8afac 100644 --- a/src/Umbraco.Tests/Migrations/PostMigrationTests.cs +++ b/src/Umbraco.Tests/Migrations/PostMigrationTests.cs @@ -50,10 +50,9 @@ namespace Umbraco.Tests.Migrations var sqlContext = new SqlContext(new SqlCeSyntaxProvider(), DatabaseType.SQLCe, Mock.Of()); var scopeProvider = new MigrationTests.TestScopeProvider(scope) { SqlContext = sqlContext }; - var u1 = new MigrationTests.TestUpgrader(scopeProvider, builder, Mock.Of(), posts, logger, - new MigrationPlan("Test", builder, logger) - .Add(string.Empty, "done")); - u1.Execute(); + var u1 = new MigrationTests.TestUpgraderWithPostMigrations( + new MigrationPlan("Test").From(string.Empty).To("done")); + u1.Execute(scopeProvider, builder, Mock.Of(), logger, posts); Assert.AreEqual(1, changed1.CountExecuted); } @@ -94,18 +93,16 @@ namespace Umbraco.Tests.Migrations var sqlContext = new SqlContext(new SqlCeSyntaxProvider(), DatabaseType.SQLCe, Mock.Of()); var scopeProvider = new MigrationTests.TestScopeProvider(scope) { SqlContext = sqlContext }; - var u1 = new MigrationTests.TestUpgrader(scopeProvider, builder, Mock.Of(), posts, logger, - new MigrationPlan("Test1", builder, logger) - .Add(string.Empty, "done")); - u1.Execute(); + var u1 = new MigrationTests.TestUpgraderWithPostMigrations( + new MigrationPlan("Test1").From(string.Empty).To("done")); + u1.Execute(scopeProvider, builder, Mock.Of(), logger, posts); Assert.AreEqual(1, changed1.CountExecuted); Assert.AreEqual(0, changed2.CountExecuted); - var u2 = new MigrationTests.TestUpgrader(scopeProvider, builder, Mock.Of(), posts, logger, - new MigrationPlan("Test2", builder, logger) - .Add(string.Empty, "done")); - u2.Execute(); + var u2 = new MigrationTests.TestUpgraderWithPostMigrations( + new MigrationPlan("Test2").From(string.Empty).To("done")); + u2.Execute(scopeProvider, builder, Mock.Of(), logger, posts); Assert.AreEqual(1, changed1.CountExecuted); Assert.AreEqual(1, changed2.CountExecuted); diff --git a/src/Umbraco.Tests/Persistence/NPocoTests/NPocoFetchTests.cs b/src/Umbraco.Tests/Persistence/NPocoTests/NPocoFetchTests.cs index f33ee563b4..64bc825c3e 100644 --- a/src/Umbraco.Tests/Persistence/NPocoTests/NPocoFetchTests.cs +++ b/src/Umbraco.Tests/Persistence/NPocoTests/NPocoFetchTests.cs @@ -403,7 +403,6 @@ namespace Umbraco.Tests.Persistence.NPocoTests .From() .Where(x => x.Id == 1); - sql.WriteToConsole(); var dto = scope.Database.Fetch(sql).FirstOrDefault(); Assert.IsNotNull(dto); Assert.AreEqual("one", dto.Name); @@ -415,7 +414,6 @@ namespace Umbraco.Tests.Persistence.NPocoTests //Assert.AreEqual("one", dto.Name); var sql3 = new Sql(sql.SQL, 1); - sql.WriteToConsole(); dto = scope.Database.Fetch(sql3).FirstOrDefault(); Assert.IsNotNull(dto); Assert.AreEqual("one", dto.Name); diff --git a/src/Umbraco.Tests/Persistence/NPocoTests/NPocoSqlExtensionsTests.cs b/src/Umbraco.Tests/Persistence/NPocoTests/NPocoSqlExtensionsTests.cs index 458479b293..59a046558c 100644 --- a/src/Umbraco.Tests/Persistence/NPocoTests/NPocoSqlExtensionsTests.cs +++ b/src/Umbraco.Tests/Persistence/NPocoTests/NPocoSqlExtensionsTests.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using NPoco; using NUnit.Framework; +using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Dtos; using Umbraco.Tests.TestHelpers; @@ -11,6 +12,80 @@ namespace Umbraco.Tests.Persistence.NPocoTests [TestFixture] public class NPocoSqlExtensionsTests : BaseUsingSqlCeSyntax { + [Test] + public void WhereTest() + { + var sql = new Sql(SqlContext) + .Select("*") + .From() + .Where(x => x.LanguageId == null); + Assert.AreEqual("SELECT *\nFROM [umbracoPropertyData]\nWHERE (([umbracoPropertyData].[languageId] is null))", sql.SQL, sql.SQL); + + sql = new Sql(SqlContext) + .Select("*") + .From() + .Where(x => x.LanguageId == 123); + Assert.AreEqual("SELECT *\nFROM [umbracoPropertyData]\nWHERE (([umbracoPropertyData].[languageId] = @0))", sql.SQL, sql.SQL); + + var id = 123; + + sql = new Sql(SqlContext) + .Select("*") + .From() + .Where(x => x.LanguageId == id); + Assert.AreEqual("SELECT *\nFROM [umbracoPropertyData]\nWHERE (([umbracoPropertyData].[languageId] = @0))", sql.SQL, sql.SQL); + + int? nid = 123; + + sql = new Sql(SqlContext) + .Select("*") + .From() + .Where(x => x.LanguageId == nid); + Assert.AreEqual("SELECT *\nFROM [umbracoPropertyData]\nWHERE (([umbracoPropertyData].[languageId] = @0))", sql.SQL, sql.SQL); + + // but the above comparison fails if @0 is null + // what we want is something similar to: + + sql = new Sql(SqlContext) + .Select("*") + .From() + .Where(x => (nid == null && x.LanguageId == null) || (nid != null && x.LanguageId == nid)); + Assert.AreEqual("SELECT *\nFROM [umbracoPropertyData]\nWHERE ((((@0 is null) AND ([umbracoPropertyData].[languageId] is null)) OR ((@1 is not null) AND ([umbracoPropertyData].[languageId] = @2))))", sql.SQL, sql.SQL); + + // new SqlNullableEquals method does it automatically + // 'course it would be nicer if '==' could do it + // see note in ExpressionVisitorBase for SqlNullableEquals + + //sql = new Sql(SqlContext) + // .Select("*") + // .From() + // .Where(x => x.LanguageId.SqlNullableEquals(nid)); + //Assert.AreEqual("SELECT *\nFROM [umbracoPropertyData]\nWHERE ((((@0 is null) AND ([umbracoPropertyData].[languageId] is null)) OR ((@0 is not null) AND ([umbracoPropertyData].[languageId] = @0))))", sql.SQL, sql.SQL); + + // but, the expression above fails with SQL CE, 'specified argument for the function is not valid' in 'isnull' function + // so... compare with fallback values + + sql = new Sql(SqlContext) + .Select("*") + .From() + .Where(x => x.LanguageId.SqlNullableEquals(nid, -1)); + Assert.AreEqual("SELECT *\nFROM [umbracoPropertyData]\nWHERE ((COALESCE([umbracoPropertyData].[languageId],@0) = COALESCE(@1,@0)))", sql.SQL, sql.SQL); + } + + [Test] + public void SqlNullableEqualsTest() + { + int? a, b; + a = b = null; + Assert.IsTrue(a.SqlNullableEquals(b, -1)); + b = 2; + Assert.IsFalse(a.SqlNullableEquals(b, -1)); + a = 2; + Assert.IsTrue(a.SqlNullableEquals(b, -1)); + b = null; + Assert.IsFalse(a.SqlNullableEquals(b, -1)); + } + [Test] public void WhereInValueFieldTest() { @@ -117,7 +192,6 @@ INNER JOIN [dto2] ON [dto1].[id] = [dto2].[dto1id]".NoCrLf(), sql.SQL.NoCrLf()); var sql = Sql() .Update(u => u.Set(x => x.EditorAlias, "Umbraco.ColorPicker")) .Where(x => x.EditorAlias == "Umbraco.ColorPickerAlias"); - sql.WriteToConsole(); } [TableName("dto1")] diff --git a/src/Umbraco.Tests/Persistence/NPocoTests/NPocoSqlTemplateTests.cs b/src/Umbraco.Tests/Persistence/NPocoTests/NPocoSqlTemplateTests.cs index ac6df7897d..b24642e88b 100644 --- a/src/Umbraco.Tests/Persistence/NPocoTests/NPocoSqlTemplateTests.cs +++ b/src/Umbraco.Tests/Persistence/NPocoTests/NPocoSqlTemplateTests.cs @@ -27,15 +27,9 @@ namespace Umbraco.Tests.Persistence.NPocoTests .From("zbThing1") .Where("id=@id", new { id = SqlTemplate.Arg("id") })).Sql(new { id = 1 }); - sql.WriteToConsole(); - var sql2 = sqlTemplates.Get("xxx", x => throw new InvalidOperationException("Should be cached.")).Sql(1); - sql2.WriteToConsole(); - var sql3 = sqlTemplates.Get("xxx", x => throw new InvalidOperationException("Should be cached.")).Sql(new { id = 1 }); - - sql3.WriteToConsole(); } [Test] @@ -75,8 +69,8 @@ namespace Umbraco.Tests.Persistence.NPocoTests Assert.AreEqual(1, sql.Arguments.Length); Assert.AreEqual(123, sql.Arguments[0]); - Assert.Throws(() => template.Sql(new { xvalue = 123 }).WriteToConsole()); - Assert.Throws(() => template.Sql(new { value = 123, xvalue = 456 }).WriteToConsole()); + Assert.Throws(() => template.Sql(new { xvalue = 123 })); + Assert.Throws(() => template.Sql(new { value = 123, xvalue = 456 })); var i = 666; @@ -121,8 +115,8 @@ namespace Umbraco.Tests.Persistence.NPocoTests Assert.AreEqual(1, sql.Arguments.Length); Assert.AreEqual(123, sql.Arguments[0]); - Assert.Throws(() => template.Sql(new { j = 123 }).WriteToConsole()); - Assert.Throws(() => template.Sql(new { i = 123, j = 456 }).WriteToConsole()); + Assert.Throws(() => template.Sql(new { j = 123 })); + Assert.Throws(() => template.Sql(new { i = 123, j = 456 })); // now with more arguments diff --git a/src/Umbraco.Tests/Persistence/NPocoTests/NPocoSqlTests.cs b/src/Umbraco.Tests/Persistence/NPocoTests/NPocoSqlTests.cs index 6fa2af74cf..a04984eb64 100644 --- a/src/Umbraco.Tests/Persistence/NPocoTests/NPocoSqlTests.cs +++ b/src/Umbraco.Tests/Persistence/NPocoTests/NPocoSqlTests.cs @@ -305,12 +305,10 @@ namespace Umbraco.Tests.Persistence.NPocoTests .From() .Where(x => x.SessionId == sessionId); - sql.WriteToConsole(); Assert.AreEqual("SELECT * FROM [umbracoUserLogin] WHERE (([umbracoUserLogin].[sessionId] = @0))", sql.SQL.NoCrLf()); sql = sql.ForUpdate(); - sql.WriteToConsole(); Assert.AreEqual("SELECT * FROM [umbracoUserLogin] WITH (UPDLOCK) WHERE (([umbracoUserLogin].[sessionId] = @0))", sql.SQL.NoCrLf()); } } diff --git a/src/Umbraco.Tests/Persistence/Querying/QueryBuilderTests.cs b/src/Umbraco.Tests/Persistence/Querying/QueryBuilderTests.cs index a9928046d7..ca6b4cd5f0 100644 --- a/src/Umbraco.Tests/Persistence/Querying/QueryBuilderTests.cs +++ b/src/Umbraco.Tests/Persistence/Querying/QueryBuilderTests.cs @@ -107,8 +107,6 @@ namespace Umbraco.Tests.Persistence.Querying var translator = new SqlTranslator(sql, query); var result = translator.Translate(); - result.WriteToConsole(); - Assert.AreEqual("-1,1046,1076,1089%", result.Arguments[0]); Assert.AreEqual(1046, result.Arguments[1]); Assert.AreEqual(true, result.Arguments[2]); diff --git a/src/Umbraco.Tests/Persistence/SchemaValidationTest.cs b/src/Umbraco.Tests/Persistence/SchemaValidationTest.cs index b8f1fab918..2c875d6afc 100644 --- a/src/Umbraco.Tests/Persistence/SchemaValidationTest.cs +++ b/src/Umbraco.Tests/Persistence/SchemaValidationTest.cs @@ -26,7 +26,6 @@ namespace Umbraco.Tests.Persistence // Assert Assert.That(result.Errors.Count, Is.EqualTo(0)); - Assert.AreEqual(result.DetermineInstalledVersion(), UmbracoVersion.Current); } } } diff --git a/src/Umbraco.Tests/Published/NestedContentTests.cs b/src/Umbraco.Tests/Published/NestedContentTests.cs index 920fa2acd5..cf00345b65 100644 --- a/src/Umbraco.Tests/Published/NestedContentTests.cs +++ b/src/Umbraco.Tests/Published/NestedContentTests.cs @@ -262,7 +262,7 @@ namespace Umbraco.Tests.Published // ReSharper disable UnassignedGetOnlyAutoProperty public override PublishedItemType ItemType { get; } - public override bool IsDraft { get; } + public override bool IsDraft(string culture = null) => false; public override IPublishedContent Parent { get; } public override IEnumerable Children { get; } public override PublishedContentType ContentType { get; } diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentDataTableTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentDataTableTests.cs index a640423515..aa9e7e4918 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentDataTableTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentDataTableTests.cs @@ -216,7 +216,7 @@ namespace Umbraco.Tests.PublishedContent public DateTime UpdateDate { get; set; } public Guid Version { get; set; } public int Level { get; set; } - public bool IsDraft { get; set; } + public bool IsDraft(string culture = null) => false; public IEnumerable Properties { get; set; } diff --git a/src/Umbraco.Tests/PublishedContent/PublishedMediaTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedMediaTests.cs index 419819b79d..c736b47e53 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedMediaTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedMediaTests.cs @@ -104,12 +104,11 @@ namespace Umbraco.Tests.PublishedContent Assert.AreEqual("
This is some content
", propVal2.ToString()); var propVal3 = publishedMedia.Value("Content"); - Assert.IsInstanceOf(propVal3); + Assert.IsInstanceOf(propVal3); Assert.AreEqual("
This is some content
", propVal3.ToString()); } [Test] - [Ignore("No point testing with Examine, should refactor this test.")] public void Ensure_Children_Sorted_With_Examine() { var rebuilder = IndexInitializer.GetMediaIndexRebuilder(Factory.GetInstance(), IndexInitializer.GetMockMediaService()); @@ -117,7 +116,9 @@ namespace Umbraco.Tests.PublishedContent using (var luceneDir = new RandomIdRamDirectory()) using (var indexer = IndexInitializer.GetUmbracoIndexer(ProfilingLogger, luceneDir, validator: new ContentValueSetValidator(true))) + using (indexer.ProcessNonAsync()) { + rebuilder.RegisterIndex(indexer.Name); rebuilder.Populate(indexer); var searcher = indexer.GetSearcher(); @@ -137,7 +138,6 @@ namespace Umbraco.Tests.PublishedContent } [Test] - [Ignore("No point testing with Examine, should refactor this test.")] public void Do_Not_Find_In_Recycle_Bin() { var rebuilder = IndexInitializer.GetMediaIndexRebuilder(Factory.GetInstance(), IndexInitializer.GetMockMediaService()); @@ -145,9 +145,10 @@ namespace Umbraco.Tests.PublishedContent using (var luceneDir = new RandomIdRamDirectory()) using (var indexer = IndexInitializer.GetUmbracoIndexer(ProfilingLogger, luceneDir, //include unpublished content since this uses the 'internal' indexer, it's up to the media cache to filter - validator: new ContentValueSetValidator(true))) + validator: new ContentValueSetValidator(false))) using (indexer.ProcessNonAsync()) { + rebuilder.RegisterIndex(indexer.Name); rebuilder.Populate(indexer); var searcher = indexer.GetSearcher(); @@ -185,7 +186,6 @@ namespace Umbraco.Tests.PublishedContent } [Test] - [Ignore("No point testing with Examine, should refactor this test.")] public void Children_With_Examine() { var rebuilder = IndexInitializer.GetMediaIndexRebuilder(Factory.GetInstance(), IndexInitializer.GetMockMediaService()); @@ -195,6 +195,7 @@ namespace Umbraco.Tests.PublishedContent validator: new ContentValueSetValidator(true))) using (indexer.ProcessNonAsync()) { + rebuilder.RegisterIndex(indexer.Name); rebuilder.Populate(indexer); var searcher = indexer.GetSearcher(); @@ -213,7 +214,6 @@ namespace Umbraco.Tests.PublishedContent } [Test] - [Ignore("No point testing with Examine, should refactor this test.")] public void Descendants_With_Examine() { var rebuilder = IndexInitializer.GetMediaIndexRebuilder(Factory.GetInstance(), IndexInitializer.GetMockMediaService()); @@ -223,6 +223,7 @@ namespace Umbraco.Tests.PublishedContent validator: new ContentValueSetValidator(true))) using (indexer.ProcessNonAsync()) { + rebuilder.RegisterIndex(indexer.Name); rebuilder.Populate(indexer); var searcher = indexer.GetSearcher(); @@ -241,7 +242,6 @@ namespace Umbraco.Tests.PublishedContent } [Test] - [Ignore("No point testing with Examine, should refactor this test.")] public void DescendantsOrSelf_With_Examine() { var rebuilder = IndexInitializer.GetMediaIndexRebuilder(Factory.GetInstance(), IndexInitializer.GetMockMediaService()); @@ -251,6 +251,7 @@ namespace Umbraco.Tests.PublishedContent validator: new ContentValueSetValidator(true))) using (indexer.ProcessNonAsync()) { + rebuilder.RegisterIndex(indexer.Name); rebuilder.Populate(indexer); var searcher = indexer.GetSearcher(); @@ -269,7 +270,6 @@ namespace Umbraco.Tests.PublishedContent } [Test] - [Ignore("No point testing with Examine, should refactor this test.")] public void Ancestors_With_Examine() { var rebuilder = IndexInitializer.GetMediaIndexRebuilder(Factory.GetInstance(), IndexInitializer.GetMockMediaService()); @@ -280,6 +280,7 @@ namespace Umbraco.Tests.PublishedContent validator: new ContentValueSetValidator(true))) using (indexer.ProcessNonAsync()) { + rebuilder.RegisterIndex(indexer.Name); rebuilder.Populate(indexer); var ctx = GetUmbracoContext("/test"); @@ -295,7 +296,6 @@ namespace Umbraco.Tests.PublishedContent } [Test] - [Ignore("No point testing with Examine, should refactor this test.")] public void AncestorsOrSelf_With_Examine() { var rebuilder = IndexInitializer.GetMediaIndexRebuilder(Factory.GetInstance(), IndexInitializer.GetMockMediaService()); @@ -305,6 +305,7 @@ namespace Umbraco.Tests.PublishedContent validator: new ContentValueSetValidator(true))) using (indexer.ProcessNonAsync()) { + rebuilder.RegisterIndex(indexer.Name); rebuilder.Populate(indexer); diff --git a/src/Umbraco.Tests/PublishedContent/SolidPublishedSnapshot.cs b/src/Umbraco.Tests/PublishedContent/SolidPublishedSnapshot.cs index efd1c6ae8b..0c4059ca7c 100644 --- a/src/Umbraco.Tests/PublishedContent/SolidPublishedSnapshot.cs +++ b/src/Umbraco.Tests/PublishedContent/SolidPublishedSnapshot.cs @@ -162,7 +162,6 @@ namespace Umbraco.Tests.PublishedContent WriterId = CreatorId = 0; CreateDate = UpdateDate = DateTime.Now; Version = Guid.Empty; - IsDraft = false; ContentType = contentType; } @@ -192,7 +191,7 @@ namespace Umbraco.Tests.PublishedContent public string GetUrl(string culture = null) => throw new NotSupportedException(); public PublishedItemType ItemType { get { return PublishedItemType.Content; } } - public bool IsDraft { get; set; } + public bool IsDraft(string culture = null) => false; #endregion diff --git a/src/Umbraco.Tests/Services/ContentServiceTagsTests.cs b/src/Umbraco.Tests/Services/ContentServiceTagsTests.cs new file mode 100644 index 0000000000..8f66e98b76 --- /dev/null +++ b/src/Umbraco.Tests/Services/ContentServiceTagsTests.cs @@ -0,0 +1,811 @@ +using System; +using System.Linq; +using LightInject; +using NUnit.Framework; +using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Core.Persistence.Repositories.Implement; +using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Services; +using Umbraco.Tests.TestHelpers.Entities; +using Umbraco.Tests.Testing; + +namespace Umbraco.Tests.Services +{ + [TestFixture] + [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest, + PublishedRepositoryEvents = true, + WithApplication = true, + Logger = UmbracoTestOptions.Logger.Console)] + public class ContentServiceTagsTests : TestWithSomeContentBase + { + public override void SetUp() + { + base.SetUp(); + ContentRepositoryBase.ThrowOnWarning = true; + } + + public override void TearDown() + { + ContentRepositoryBase.ThrowOnWarning = false; + base.TearDown(); + } + + protected override void Compose() + { + base.Compose(); + + // fixme - do it differently + Container.Register(factory => factory.GetInstance().TextService); + } + + [Test] + public void TagsCanBeInvariant() + { + var contentService = ServiceContext.ContentService; + var contentTypeService = ServiceContext.ContentTypeService; + var tagService = ServiceContext.TagService; + var contentType = MockedContentTypes.CreateSimpleContentType("umbMandatory", "Mandatory Doc Type", true); + contentType.PropertyGroups.First().PropertyTypes.Add( + new PropertyType("test", ValueStorageType.Ntext, "tags") + { + DataTypeId = 1041 + }); + contentTypeService.Save(contentType); + + IContent content1 = MockedContent.CreateSimpleContent(contentType, "Tagged content 1", -1); + content1.AssignTags("tags", new[] { "hello", "world", "another", "one" }); + contentService.SaveAndPublish(content1); + + content1 = contentService.GetById(content1.Id); + + var enTags = content1.Properties["tags"].GetTagsValue().ToArray(); + Assert.AreEqual(4, enTags.Length); + Assert.Contains("one", enTags); + Assert.AreEqual(-1, enTags.IndexOf("plus")); + + var tagGroups = tagService.GetAllTags().GroupBy(x => x.LanguageId); + foreach (var tag in tagService.GetAllTags()) + Console.WriteLine($"{tag.Group}:{tag.Text} {tag.LanguageId}"); + Assert.AreEqual(1, tagGroups.Count()); + var enTagGroup = tagGroups.FirstOrDefault(x => x.Key == null); + Assert.IsNotNull(enTagGroup); + Assert.AreEqual(4, enTagGroup.Count()); + Assert.IsTrue(enTagGroup.Any(x => x.Text == "one")); + Assert.IsFalse(enTagGroup.Any(x => x.Text == "plus")); + } + + [Test] + public void TagsCanBeVariant() + { + var languageService = ServiceContext.LocalizationService; + languageService.Save(new Language("fr-FR")); // en-US is already there + + var contentService = ServiceContext.ContentService; + var contentTypeService = ServiceContext.ContentTypeService; + var tagService = ServiceContext.TagService; + var contentType = MockedContentTypes.CreateSimpleContentType("umbMandatory", "Mandatory Doc Type", true); + contentType.PropertyGroups.First().PropertyTypes.Add( + new PropertyType("test", ValueStorageType.Ntext, "tags") + { + DataTypeId = 1041, + Variations = ContentVariation.Culture + }); + contentType.Variations = ContentVariation.Culture; + contentTypeService.Save(contentType); + + IContent content1 = MockedContent.CreateSimpleContent(contentType, "Tagged content 1", -1); + content1.SetCultureName("name-fr", "fr-FR"); + content1.SetCultureName("name-en", "en-US"); + content1.AssignTags("tags", new[] { "hello", "world", "some", "tags", "plus" }, culture: "fr-FR"); + content1.AssignTags("tags", new[] { "hello", "world", "another", "one" }, culture: "en-US"); + contentService.SaveAndPublish(content1); + + content1 = contentService.GetById(content1.Id); + + var frTags = content1.Properties["tags"].GetTagsValue("fr-FR").ToArray(); + Assert.AreEqual(5, frTags.Length); + Assert.Contains("plus", frTags); + Assert.AreEqual(-1, frTags.IndexOf("one")); + + var enTags = content1.Properties["tags"].GetTagsValue("en-US").ToArray(); + Assert.AreEqual(4, enTags.Length); + Assert.Contains("one", enTags); + Assert.AreEqual(-1, enTags.IndexOf("plus")); + + var tagGroups = tagService.GetAllTags(culture:"*").GroupBy(x => x.LanguageId); + foreach (var tag in tagService.GetAllTags()) + Console.WriteLine($"{tag.Group}:{tag.Text} {tag.LanguageId}"); + Assert.AreEqual(2, tagGroups.Count()); + var frTagGroup = tagGroups.FirstOrDefault(x => x.Key == 2); + Assert.IsNotNull(frTagGroup); + Assert.AreEqual(5, frTagGroup.Count()); + Assert.IsTrue(frTagGroup.Any(x => x.Text == "plus")); + Assert.IsFalse(frTagGroup.Any(x => x.Text == "one")); + var enTagGroup = tagGroups.FirstOrDefault(x => x.Key == 1); + Assert.IsNotNull(enTagGroup); + Assert.AreEqual(4, enTagGroup.Count()); + Assert.IsTrue(enTagGroup.Any(x => x.Text == "one")); + Assert.IsFalse(enTagGroup.Any(x => x.Text == "plus")); + } + + [Test] + public void TagsCanBecomeVariant() + { + var enId = ServiceContext.LocalizationService.GetLanguageIdByIsoCode("en-US").Value; + + var contentService = ServiceContext.ContentService; + var contentTypeService = ServiceContext.ContentTypeService; + var tagService = ServiceContext.TagService; + var contentType = MockedContentTypes.CreateSimpleContentType("umbMandatory", "Mandatory Doc Type", true); + PropertyType propertyType; + contentType.PropertyGroups.First().PropertyTypes.Add( + propertyType = new PropertyType("test", ValueStorageType.Ntext, "tags") + { + DataTypeId = 1041 + }); + contentTypeService.Save(contentType); + + IContent content1 = MockedContent.CreateSimpleContent(contentType, "Tagged content 1", -1); + content1.AssignTags("tags", new[] { "hello", "world", "another", "one" }); + contentService.SaveAndPublish(content1); + + contentType.Variations = ContentVariation.Culture; + contentTypeService.Save(contentType); + + // no changes + content1 = contentService.GetById(content1.Id); + + var tags = content1.Properties["tags"].GetTagsValue().ToArray(); + Assert.AreEqual(4, tags.Length); + Assert.Contains("one", tags); + Assert.AreEqual(-1, tags.IndexOf("plus")); + + var tagGroups = tagService.GetAllTags().GroupBy(x => x.LanguageId); + foreach (var tag in tagService.GetAllTags()) + Console.WriteLine($"{tag.Group}:{tag.Text} {tag.LanguageId}"); + Assert.AreEqual(1, tagGroups.Count()); + var enTagGroup = tagGroups.FirstOrDefault(x => x.Key == null); + Assert.IsNotNull(enTagGroup); + Assert.AreEqual(4, enTagGroup.Count()); + Assert.IsTrue(enTagGroup.Any(x => x.Text == "one")); + Assert.IsFalse(enTagGroup.Any(x => x.Text == "plus")); + + propertyType.Variations = ContentVariation.Culture; + contentTypeService.Save(contentType); + + // changes + content1 = contentService.GetById(content1.Id); + + // property value has been moved from invariant to en-US + tags = content1.Properties["tags"].GetTagsValue().ToArray(); + Assert.IsEmpty(tags); + + tags = content1.Properties["tags"].GetTagsValue("en-US").ToArray(); + Assert.AreEqual(4, tags.Length); + Assert.Contains("one", tags); + Assert.AreEqual(-1, tags.IndexOf("plus")); + + // tags have been copied from invariant to en-US + tagGroups = tagService.GetAllTags(culture: "*").GroupBy(x => x.LanguageId); + foreach (var tag in tagService.GetAllTags("*")) + Console.WriteLine($"{tag.Group}:{tag.Text} {tag.LanguageId}"); + Assert.AreEqual(1, tagGroups.Count()); + + enTagGroup = tagGroups.FirstOrDefault(x => x.Key == enId); + Assert.IsNotNull(enTagGroup); + Assert.AreEqual(4, enTagGroup.Count()); + Assert.IsTrue(enTagGroup.Any(x => x.Text == "one")); + Assert.IsFalse(enTagGroup.Any(x => x.Text == "plus")); + } + + [Test] + public void TagsCanBecomeInvariant() + { + var languageService = ServiceContext.LocalizationService; + languageService.Save(new Language("fr-FR")); // en-US is already there + + var enId = ServiceContext.LocalizationService.GetLanguageIdByIsoCode("en-US").Value; + + var contentService = ServiceContext.ContentService; + var contentTypeService = ServiceContext.ContentTypeService; + var tagService = ServiceContext.TagService; + var contentType = MockedContentTypes.CreateSimpleContentType("umbMandatory", "Mandatory Doc Type", true); + PropertyType propertyType; + contentType.PropertyGroups.First().PropertyTypes.Add( + propertyType = new PropertyType("test", ValueStorageType.Ntext, "tags") + { + DataTypeId = 1041, + Variations = ContentVariation.Culture + }); + contentType.Variations = ContentVariation.Culture; + contentTypeService.Save(contentType); + + IContent content1 = MockedContent.CreateSimpleContent(contentType, "Tagged content 1", -1); + content1.SetCultureName("name-fr", "fr-FR"); + content1.SetCultureName("name-en", "en-US"); + content1.AssignTags("tags", new[] { "hello", "world", "some", "tags", "plus" }, culture: "fr-FR"); + content1.AssignTags("tags", new[] { "hello", "world", "another", "one" }, culture: "en-US"); + contentService.SaveAndPublish(content1); + + contentType.Variations = ContentVariation.Nothing; + contentTypeService.Save(contentType); + + // changes + content1 = contentService.GetById(content1.Id); + + // property value has been moved from en-US to invariant, fr-FR tags are gone + Assert.IsEmpty(content1.Properties["tags"].GetTagsValue("fr-FR")); + Assert.IsEmpty(content1.Properties["tags"].GetTagsValue("en-US")); + + var tags = content1.Properties["tags"].GetTagsValue().ToArray(); + Assert.AreEqual(4, tags.Length); + Assert.Contains("one", tags); + Assert.AreEqual(-1, tags.IndexOf("plus")); + + // tags have been copied from en-US to invariant, fr-FR tags are gone + var tagGroups = tagService.GetAllTags(culture: "*").GroupBy(x => x.LanguageId); + foreach (var tag in tagService.GetAllTags("*")) + Console.WriteLine($"{tag.Group}:{tag.Text} {tag.LanguageId}"); + Assert.AreEqual(1, tagGroups.Count()); + + var enTagGroup = tagGroups.FirstOrDefault(x => x.Key == null); + Assert.IsNotNull(enTagGroup); + Assert.AreEqual(4, enTagGroup.Count()); + Assert.IsTrue(enTagGroup.Any(x => x.Text == "one")); + Assert.IsFalse(enTagGroup.Any(x => x.Text == "plus")); + } + + [Test] + public void TagsCanBecomeInvariant2() + { + var languageService = ServiceContext.LocalizationService; + languageService.Save(new Language("fr-FR")); // en-US is already there + + var enId = ServiceContext.LocalizationService.GetLanguageIdByIsoCode("en-US").Value; + + var contentService = ServiceContext.ContentService; + var contentTypeService = ServiceContext.ContentTypeService; + var tagService = ServiceContext.TagService; + var contentType = MockedContentTypes.CreateSimpleContentType("umbMandatory", "Mandatory Doc Type", true); + PropertyType propertyType; + contentType.PropertyGroups.First().PropertyTypes.Add( + propertyType = new PropertyType("test", ValueStorageType.Ntext, "tags") + { + DataTypeId = 1041, + Variations = ContentVariation.Culture + }); + contentType.Variations = ContentVariation.Culture; + contentTypeService.Save(contentType); + + IContent content1 = MockedContent.CreateSimpleContent(contentType, "Tagged content 1", -1); + content1.SetCultureName("name-fr", "fr-FR"); + content1.SetCultureName("name-en", "en-US"); + content1.AssignTags("tags", new[] { "hello", "world", "some", "tags", "plus" }, culture: "fr-FR"); + content1.AssignTags("tags", new[] { "hello", "world", "another", "one" }, culture: "en-US"); + contentService.SaveAndPublish(content1); + + IContent content2 = MockedContent.CreateSimpleContent(contentType, "Tagged content 2", -1); + content2.SetCultureName("name-fr", "fr-FR"); + content2.SetCultureName("name-en", "en-US"); + content2.AssignTags("tags", new[] { "hello", "world", "some", "tags", "plus" }, culture: "fr-FR"); + content2.AssignTags("tags", new[] { "hello", "world", "another", "one" }, culture: "en-US"); + contentService.SaveAndPublish(content2); + + //// pretend we already have invariant values + //using (var scope = ScopeProvider.CreateScope()) + //{ + // scope.Database.Execute("INSERT INTO [cmsTags] ([tag], [group], [languageId]) SELECT DISTINCT [tag], [group], NULL FROM [cmsTags] WHERE [languageId] IS NOT NULL"); + //} + + // this should work + propertyType.Variations = ContentVariation.Nothing; + Assert.DoesNotThrow(() => contentTypeService.Save(contentType)); + } + + [Test] + public void TagsCanBecomeInvariantByPropertyType() + { + var languageService = ServiceContext.LocalizationService; + languageService.Save(new Language("fr-FR")); // en-US is already there + + var enId = ServiceContext.LocalizationService.GetLanguageIdByIsoCode("en-US").Value; + + var contentService = ServiceContext.ContentService; + var contentTypeService = ServiceContext.ContentTypeService; + var tagService = ServiceContext.TagService; + var contentType = MockedContentTypes.CreateSimpleContentType("umbMandatory", "Mandatory Doc Type", true); + PropertyType propertyType; + contentType.PropertyGroups.First().PropertyTypes.Add( + propertyType = new PropertyType("test", ValueStorageType.Ntext, "tags") + { + DataTypeId = 1041, + Variations = ContentVariation.Culture + }); + contentType.Variations = ContentVariation.Culture; + contentTypeService.Save(contentType); + + IContent content1 = MockedContent.CreateSimpleContent(contentType, "Tagged content 1", -1); + content1.SetCultureName("name-fr", "fr-FR"); + content1.SetCultureName("name-en", "en-US"); + content1.AssignTags("tags", new[] { "hello", "world", "some", "tags", "plus" }, culture: "fr-FR"); + content1.AssignTags("tags", new[] { "hello", "world", "another", "one" }, culture: "en-US"); + contentService.SaveAndPublish(content1); + + propertyType.Variations = ContentVariation.Nothing; + contentTypeService.Save(contentType); + + // changes + content1 = contentService.GetById(content1.Id); + + // property value has been moved from en-US to invariant, fr-FR tags are gone + Assert.IsEmpty(content1.Properties["tags"].GetTagsValue("fr-FR")); + Assert.IsEmpty(content1.Properties["tags"].GetTagsValue("en-US")); + + var tags = content1.Properties["tags"].GetTagsValue().ToArray(); + Assert.AreEqual(4, tags.Length); + Assert.Contains("one", tags); + Assert.AreEqual(-1, tags.IndexOf("plus")); + + // tags have been copied from en-US to invariant, fr-FR tags are gone + var tagGroups = tagService.GetAllTags(culture: "*").GroupBy(x => x.LanguageId); + foreach (var tag in tagService.GetAllTags("*")) + Console.WriteLine($"{tag.Group}:{tag.Text} {tag.LanguageId}"); + Assert.AreEqual(1, tagGroups.Count()); + + var enTagGroup = tagGroups.FirstOrDefault(x => x.Key == null); + Assert.IsNotNull(enTagGroup); + Assert.AreEqual(4, enTagGroup.Count()); + Assert.IsTrue(enTagGroup.Any(x => x.Text == "one")); + Assert.IsFalse(enTagGroup.Any(x => x.Text == "plus")); + } + + [Test] + public void TagsCanBecomeInvariantByPropertyTypeAndBackToVariant() + { + var languageService = ServiceContext.LocalizationService; + languageService.Save(new Language("fr-FR")); // en-US is already there + + var enId = ServiceContext.LocalizationService.GetLanguageIdByIsoCode("en-US").Value; + + var contentService = ServiceContext.ContentService; + var contentTypeService = ServiceContext.ContentTypeService; + var tagService = ServiceContext.TagService; + var contentType = MockedContentTypes.CreateSimpleContentType("umbMandatory", "Mandatory Doc Type", true); + PropertyType propertyType; + contentType.PropertyGroups.First().PropertyTypes.Add( + propertyType = new PropertyType("test", ValueStorageType.Ntext, "tags") + { + DataTypeId = 1041, + Variations = ContentVariation.Culture + }); + contentType.Variations = ContentVariation.Culture; + contentTypeService.Save(contentType); + + IContent content1 = MockedContent.CreateSimpleContent(contentType, "Tagged content 1", -1); + content1.SetCultureName("name-fr", "fr-FR"); + content1.SetCultureName("name-en", "en-US"); + content1.AssignTags("tags", new[] { "hello", "world", "some", "tags", "plus" }, culture: "fr-FR"); + content1.AssignTags("tags", new[] { "hello", "world", "another", "one" }, culture: "en-US"); + contentService.SaveAndPublish(content1); + + propertyType.Variations = ContentVariation.Nothing; + contentTypeService.Save(contentType); + + //fixme: This throws due to index violations + propertyType.Variations = ContentVariation.Culture; + contentTypeService.Save(contentType); + + //TODO: Assert results + } + + [Test] + public void TagsAreUpdatedWhenContentIsTrashedAndUnTrashed_One() + { + var contentService = ServiceContext.ContentService; + var contentTypeService = ServiceContext.ContentTypeService; + var tagService = ServiceContext.TagService; + var contentType = MockedContentTypes.CreateSimpleContentType("umbMandatory", "Mandatory Doc Type", true); + contentType.PropertyGroups.First().PropertyTypes.Add( + new PropertyType("test", ValueStorageType.Ntext, "tags") + { + DataTypeId = 1041 + }); + contentTypeService.Save(contentType); + + var content1 = MockedContent.CreateSimpleContent(contentType, "Tagged content 1", -1); + content1.AssignTags("tags", new[] { "hello", "world", "some", "tags", "plus" }); + contentService.SaveAndPublish(content1); + + var content2 = MockedContent.CreateSimpleContent(contentType, "Tagged content 2", -1); + content2.AssignTags("tags", new[] { "hello", "world", "some", "tags" }); + contentService.SaveAndPublish(content2); + + // verify + var tags = tagService.GetTagsForEntity(content1.Id); + Assert.AreEqual(5, tags.Count()); + var allTags = tagService.GetAllContentTags(); + Assert.AreEqual(5, allTags.Count()); + + contentService.MoveToRecycleBin(content1); + } + + [Test] + public void TagsAreUpdatedWhenContentIsTrashedAndUnTrashed_All() + { + var contentService = ServiceContext.ContentService; + var contentTypeService = ServiceContext.ContentTypeService; + var tagService = ServiceContext.TagService; + var contentType = MockedContentTypes.CreateSimpleContentType("umbMandatory", "Mandatory Doc Type", true); + contentType.PropertyGroups.First().PropertyTypes.Add( + new PropertyType("test", ValueStorageType.Ntext, "tags") + { + DataTypeId = 1041 + }); + contentTypeService.Save(contentType); + + var content1 = MockedContent.CreateSimpleContent(contentType, "Tagged content 1", -1); + content1.AssignTags("tags", new[] { "hello", "world", "some", "tags", "bam" }); + contentService.SaveAndPublish(content1); + + var content2 = MockedContent.CreateSimpleContent(contentType, "Tagged content 2", -1); + content2.AssignTags("tags", new[] { "hello", "world", "some", "tags" }); + contentService.SaveAndPublish(content2); + + // verify + var tags = tagService.GetTagsForEntity(content1.Id); + Assert.AreEqual(5, tags.Count()); + var allTags = tagService.GetAllContentTags(); + Assert.AreEqual(5, allTags.Count()); + + contentService.Unpublish(content1); + contentService.Unpublish(content2); + } + + [Test] + [Ignore("https://github.com/umbraco/Umbraco-CMS/issues/3821 (U4-8442), will need to be fixed.")] + public void TagsAreUpdatedWhenContentIsTrashedAndUnTrashed_Tree() + { + var contentService = ServiceContext.ContentService; + var contentTypeService = ServiceContext.ContentTypeService; + var tagService = ServiceContext.TagService; + var contentType = MockedContentTypes.CreateSimpleContentType("umbMandatory", "Mandatory Doc Type", true); + contentType.PropertyGroups.First().PropertyTypes.Add( + new PropertyType("test", ValueStorageType.Ntext, "tags") + { + DataTypeId = 1041 + }); + contentTypeService.Save(contentType); + + var content1 = MockedContent.CreateSimpleContent(contentType, "Tagged content 1", -1); + content1.AssignTags("tags", new[] { "hello", "world", "some", "tags", "plus" }); + contentService.SaveAndPublish(content1); + + var content2 = MockedContent.CreateSimpleContent(contentType, "Tagged content 2", content1.Id); + content2.AssignTags("tags", new[] { "hello", "world", "some", "tags" }); + contentService.SaveAndPublish(content2); + + // verify + var tags = tagService.GetTagsForEntity(content1.Id); + Assert.AreEqual(5, tags.Count()); + var allTags = tagService.GetAllContentTags(); + Assert.AreEqual(5, allTags.Count()); + + contentService.MoveToRecycleBin(content1); + + // no more tags + tags = tagService.GetTagsForEntity(content1.Id); + Assert.AreEqual(0, tags.Count()); + tags = tagService.GetTagsForEntity(content2.Id); + Assert.AreEqual(0, tags.Count()); + + // no more tags + allTags = tagService.GetAllContentTags(); + Assert.AreEqual(0, allTags.Count()); + + contentService.Move(content1, -1); + + Assert.IsFalse(content1.Published); + + // no more tags + tags = tagService.GetTagsForEntity(content1.Id); + Assert.AreEqual(0, tags.Count()); + tags = tagService.GetTagsForEntity(content2.Id); + Assert.AreEqual(0, tags.Count()); + + // no more tags + allTags = tagService.GetAllContentTags(); + Assert.AreEqual(0, allTags.Count()); + + content1.PublishCulture(); + contentService.SaveAndPublish(content1); + + Assert.IsTrue(content1.Published); + + // tags are back + tags = tagService.GetTagsForEntity(content1.Id); + Assert.AreEqual(5, tags.Count()); + + // fixme tag & tree issue + // when we publish, we 'just' publish the top one and not the ones below = fails + // what we should do is... NOT clear tags when unpublishing or trashing or... + // and just update the tag service to NOT return anything related to trashed or + // unpublished entities (since trashed is set on ALL entities in the trashed branch) + tags = tagService.GetTagsForEntity(content2.Id); // including that one! + Assert.AreEqual(4, tags.Count()); + + // tags are back + allTags = tagService.GetAllContentTags(); + Assert.AreEqual(5, allTags.Count()); + } + + [Test] + public void TagsAreUpdatedWhenContentIsUnpublishedAndRePublished() + { + var contentService = ServiceContext.ContentService; + var contentTypeService = ServiceContext.ContentTypeService; + var tagService = ServiceContext.TagService; + var contentType = MockedContentTypes.CreateSimpleContentType("umbMandatory", "Mandatory Doc Type", true); + contentType.PropertyGroups.First().PropertyTypes.Add( + new PropertyType("test", ValueStorageType.Ntext, "tags") + { + DataTypeId = 1041 + }); + contentTypeService.Save(contentType); + + var content1 = MockedContent.CreateSimpleContent(contentType, "Tagged content 1", -1); + content1.AssignTags("tags", new[] { "hello", "world", "some", "tags", "bam" }); + contentService.SaveAndPublish(content1); + + var content2 = MockedContent.CreateSimpleContent(contentType, "Tagged content 2", -1); + content2.AssignTags("tags", new[] { "hello", "world", "some", "tags" }); + contentService.SaveAndPublish(content2); + + contentService.Unpublish(content1); + contentService.Unpublish(content2); + } + + [Test] + [Ignore("https://github.com/umbraco/Umbraco-CMS/issues/3821 (U4-8442), will need to be fixed.")] + public void TagsAreUpdatedWhenContentIsUnpublishedAndRePublished_Tree() + { + var contentService = ServiceContext.ContentService; + var contentTypeService = ServiceContext.ContentTypeService; + var tagService = ServiceContext.TagService; + var contentType = MockedContentTypes.CreateSimpleContentType("umbMandatory", "Mandatory Doc Type", true); + contentType.PropertyGroups.First().PropertyTypes.Add( + new PropertyType("test", ValueStorageType.Ntext, "tags") + { + DataTypeId = 1041 + }); + contentTypeService.Save(contentType); + + var content1 = MockedContent.CreateSimpleContent(contentType, "Tagged content 1", -1); + content1.AssignTags("tags", new[] { "hello", "world", "some", "tags", "bam" }); + contentService.SaveAndPublish(content1); + + var content2 = MockedContent.CreateSimpleContent(contentType, "Tagged content 2", content1); + content2.AssignTags("tags", new[] { "hello", "world", "some", "tags" }); + contentService.SaveAndPublish(content2); + + contentService.Unpublish(content1); + + var tags = tagService.GetTagsForEntity(content1.Id); + Assert.AreEqual(0, tags.Count()); + + // fixme tag & tree issue + // when we (un)publish, we 'just' publish the top one and not the ones below = fails + // see similar note above + tags = tagService.GetTagsForEntity(content2.Id); + Assert.AreEqual(0, tags.Count()); + var allTags = tagService.GetAllContentTags(); + Assert.AreEqual(0, allTags.Count()); + + content1.PublishCulture(); + contentService.SaveAndPublish(content1); + + tags = tagService.GetTagsForEntity(content2.Id); + Assert.AreEqual(4, tags.Count()); + allTags = tagService.GetAllContentTags(); + Assert.AreEqual(5, allTags.Count()); + } + + [Test] + public void Create_Tag_Data_Bulk_Publish_Operation() + { + //Arrange + var contentService = ServiceContext.ContentService; + var contentTypeService = ServiceContext.ContentTypeService; + var dataTypeService = ServiceContext.DataTypeService; + + //set configuration + var dataType = dataTypeService.GetDataType(1041); + dataType.Configuration = new TagConfiguration + { + Group = "test", + StorageType = TagsStorageType.Csv + }; + + var contentType = MockedContentTypes.CreateSimpleContentType("umbMandatory", "Mandatory Doc Type", true); + contentType.PropertyGroups.First().PropertyTypes.Add( + new PropertyType("test", ValueStorageType.Ntext, "tags") + { + DataTypeId = 1041 + }); + contentTypeService.Save(contentType); + contentType.AllowedContentTypes = new[] { new ContentTypeSort(new Lazy(() => contentType.Id), 0, contentType.Alias) }; + + var content = MockedContent.CreateSimpleContent(contentType, "Tagged content", -1); + content.AssignTags("tags", new[] { "hello", "world", "some", "tags" }); + contentService.Save(content); + + var child1 = MockedContent.CreateSimpleContent(contentType, "child 1 content", content.Id); + child1.AssignTags("tags", new[] { "hello1", "world1", "some1" }); + contentService.Save(child1); + + var child2 = MockedContent.CreateSimpleContent(contentType, "child 2 content", content.Id); + child2.AssignTags("tags", new[] { "hello2", "world2" }); + contentService.Save(child2); + + // Act + contentService.SaveAndPublishBranch(content, true); + + // Assert + var propertyTypeId = contentType.PropertyTypes.Single(x => x.Alias == "tags").Id; + + using (var scope = ScopeProvider.CreateScope()) + { + Assert.AreEqual(4, scope.Database.ExecuteScalar( + "SELECT COUNT(*) FROM cmsTagRelationship WHERE nodeId=@nodeId AND propertyTypeId=@propTypeId", + new { nodeId = content.Id, propTypeId = propertyTypeId })); + + Assert.AreEqual(3, scope.Database.ExecuteScalar( + "SELECT COUNT(*) FROM cmsTagRelationship WHERE nodeId=@nodeId AND propertyTypeId=@propTypeId", + new { nodeId = child1.Id, propTypeId = propertyTypeId })); + + Assert.AreEqual(2, scope.Database.ExecuteScalar( + "SELECT COUNT(*) FROM cmsTagRelationship WHERE nodeId=@nodeId AND propertyTypeId=@propTypeId", + new { nodeId = child2.Id, propTypeId = propertyTypeId })); + + scope.Complete(); + } + } + + [Test] + public void Does_Not_Create_Tag_Data_For_Non_Published_Version() + { + var contentService = ServiceContext.ContentService; + var contentTypeService = ServiceContext.ContentTypeService; + + // create content type with a tag property + var contentType = MockedContentTypes.CreateSimpleContentType("umbMandatory", "Mandatory Doc Type", true); + contentType.PropertyGroups.First().PropertyTypes.Add(new PropertyType("test", ValueStorageType.Ntext, "tags") { DataTypeId = 1041 }); + contentTypeService.Save(contentType); + + // create a content with tags and publish + var content = MockedContent.CreateSimpleContent(contentType, "Tagged content", -1); + content.AssignTags("tags", new[] { "hello", "world", "some", "tags" }); + contentService.SaveAndPublish(content); + + // edit tags and save + content.AssignTags("tags", new[] { "another", "world" }, merge: true); + contentService.Save(content); + + // the (edit) property does contain all tags + Assert.AreEqual(5, content.Properties["tags"].GetValue().ToString().Split(',').Distinct().Count()); + + // but the database still contains the initial two tags + var propertyTypeId = contentType.PropertyTypes.Single(x => x.Alias == "tags").Id; + using (var scope = ScopeProvider.CreateScope()) + { + Assert.AreEqual(4, scope.Database.ExecuteScalar( + "SELECT COUNT(*) FROM cmsTagRelationship WHERE nodeId=@nodeId AND propertyTypeId=@propTypeId", + new { nodeId = content.Id, propTypeId = propertyTypeId })); + scope.Complete(); + } + } + + [Test] + public void Can_Replace_Tag_Data_To_Published_Content() + { + //Arrange + var contentService = ServiceContext.ContentService; + var contentTypeService = ServiceContext.ContentTypeService; + var contentType = MockedContentTypes.CreateSimpleContentType("umbMandatory", "Mandatory Doc Type", true); + contentType.PropertyGroups.First().PropertyTypes.Add( + new PropertyType("test", ValueStorageType.Ntext, "tags") + { + DataTypeId = 1041 + }); + contentTypeService.Save(contentType); + + var content = MockedContent.CreateSimpleContent(contentType, "Tagged content", -1); + + + // Act + content.AssignTags("tags", new[] { "hello", "world", "some", "tags" }); + contentService.SaveAndPublish(content); + + // Assert + Assert.AreEqual(4, content.Properties["tags"].GetValue().ToString().Split(',').Distinct().Count()); + var propertyTypeId = contentType.PropertyTypes.Single(x => x.Alias == "tags").Id; + using (var scope = ScopeProvider.CreateScope()) + { + Assert.AreEqual(4, scope.Database.ExecuteScalar( + "SELECT COUNT(*) FROM cmsTagRelationship WHERE nodeId=@nodeId AND propertyTypeId=@propTypeId", + new { nodeId = content.Id, propTypeId = propertyTypeId })); + + scope.Complete(); + } + } + + [Test] + public void Can_Append_Tag_Data_To_Published_Content() + { + //Arrange + var contentService = ServiceContext.ContentService; + var contentTypeService = ServiceContext.ContentTypeService; + var contentType = MockedContentTypes.CreateSimpleContentType("umbMandatory", "Mandatory Doc Type", true); + contentType.PropertyGroups.First().PropertyTypes.Add( + new PropertyType("test", ValueStorageType.Ntext, "tags") + { + DataTypeId = 1041 + }); + contentTypeService.Save(contentType); + var content = MockedContent.CreateSimpleContent(contentType, "Tagged content", -1); + content.AssignTags("tags", new[] { "hello", "world", "some", "tags" }); + contentService.SaveAndPublish(content); + + // Act + content.AssignTags("tags", new[] { "another", "world" }, merge: true); + contentService.SaveAndPublish(content); + + // Assert + Assert.AreEqual(5, content.Properties["tags"].GetValue().ToString().Split(',').Distinct().Count()); + var propertyTypeId = contentType.PropertyTypes.Single(x => x.Alias == "tags").Id; + using (var scope = ScopeProvider.CreateScope()) + { + Assert.AreEqual(5, scope.Database.ExecuteScalar( + "SELECT COUNT(*) FROM cmsTagRelationship WHERE nodeId=@nodeId AND propertyTypeId=@propTypeId", + new { nodeId = content.Id, propTypeId = propertyTypeId })); + + scope.Complete(); + } + } + + [Test] + public void Can_Remove_Tag_Data_To_Published_Content() + { + //Arrange + var contentService = ServiceContext.ContentService; + var contentTypeService = ServiceContext.ContentTypeService; + var contentType = MockedContentTypes.CreateSimpleContentType("umbMandatory", "Mandatory Doc Type", true); + contentType.PropertyGroups.First().PropertyTypes.Add( + new PropertyType("test", ValueStorageType.Ntext, "tags") + { + DataTypeId = 1041 + }); + contentTypeService.Save(contentType); + var content = MockedContent.CreateSimpleContent(contentType, "Tagged content", -1); + content.AssignTags("tags", new[] { "hello", "world", "some", "tags" }); + contentService.SaveAndPublish(content); + + // Act + content.RemoveTags("tags", new[] { "some", "world" }); + contentService.SaveAndPublish(content); + + // Assert + Assert.AreEqual(2, content.Properties["tags"].GetValue().ToString().Split(',').Distinct().Count()); + var propertyTypeId = contentType.PropertyTypes.Single(x => x.Alias == "tags").Id; + using (var scope = ScopeProvider.CreateScope()) + { + Assert.AreEqual(2, scope.Database.ExecuteScalar( + "SELECT COUNT(*) FROM cmsTagRelationship WHERE nodeId=@nodeId AND propertyTypeId=@propTypeId", + new { nodeId = content.Id, propTypeId = propertyTypeId })); + + scope.Complete(); + } + } + + } +} diff --git a/src/Umbraco.Tests/Services/ContentServiceTests.cs b/src/Umbraco.Tests/Services/ContentServiceTests.cs index 3b07afae70..8fba16662c 100644 --- a/src/Umbraco.Tests/Services/ContentServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentServiceTests.cs @@ -193,63 +193,6 @@ namespace Umbraco.Tests.Services Assert.AreEqual(5, found.Length); } - /// - /// Ensures that we don't unpublish all nodes when a node is deleted that has an invalid path of -1 - /// Note: it is actually the MoveToRecycleBin happening on the initial deletion of a node through the UI - /// that causes the issue. - /// Regression test: http://issues.umbraco.org/issue/U4-9336 - /// - [Test] - [Ignore("not applicable to v8")] - - // fixme - this test was imported from 7.6 BUT it makes no sense for v8 - // we should trust the PATH, full stop - - public void Moving_Node_To_Recycle_Bin_With_Invalid_Path() - { - var contentService = ServiceContext.ContentService; - var root = ServiceContext.ContentService.GetById(NodeDto.NodeIdSeed + 1); - root.PublishCulture(); - Assert.IsTrue(contentService.SaveAndPublish(root).Success); - var content = contentService.CreateAndSave("Test", -1, "umbTextpage", Constants.Security.SuperUserId); - content.PublishCulture(); - Assert.IsTrue(contentService.SaveAndPublish(content).Success); - var hierarchy = CreateContentHierarchy().OrderBy(x => x.Level).ToArray(); - contentService.Save(hierarchy, Constants.Security.SuperUserId); - foreach (var c in hierarchy) - { - c.PublishCulture(); - Assert.IsTrue(contentService.SaveAndPublish(c).Success); - } - - //now make the data corrupted :/ - using (var scope = ScopeProvider.CreateScope()) - { - scope.Database.Execute("UPDATE umbracoNode SET path = '-1' WHERE id = @id", new { id = content.Id }); - scope.Complete(); - } - - //re-get with the corrupt path - content = contentService.GetById(content.Id); - - // here we get all descendants by the path of the node being moved to bin, and unpublish all of them. - // since the path is invalid, there's logic in here to fix that if it's possible and re-persist the entity. - var moveResult = ServiceContext.ContentService.MoveToRecycleBin(content); - - Assert.IsTrue(moveResult.Success); - - //re-get with the fixed/moved path - content = contentService.GetById(content.Id); - - Assert.AreEqual("-1,-20," + content.Id, content.Path); - - //re-get - hierarchy = contentService.GetByIds(hierarchy.Select(x => x.Id).ToArray()).OrderBy(x => x.Level).ToArray(); - - Assert.That(hierarchy.All(c => c.Trashed == false), Is.True); - Assert.That(hierarchy.All(c => c.Path.StartsWith("-1,-20") == false), Is.True); - } - [Test] public void Perform_Scheduled_Publishing() { @@ -501,511 +444,6 @@ namespace Umbraco.Tests.Services Assert.IsEmpty(res); } - [Test] - public void TagsAreUpdatedWhenContentIsTrashedAndUnTrashed_One() - { - var contentService = ServiceContext.ContentService; - var contentTypeService = ServiceContext.ContentTypeService; - var tagService = ServiceContext.TagService; - var contentType = MockedContentTypes.CreateSimpleContentType("umbMandatory", "Mandatory Doc Type", true); - contentType.PropertyGroups.First().PropertyTypes.Add( - new PropertyType("test", ValueStorageType.Ntext, "tags") - { - DataTypeId = 1041 - }); - contentTypeService.Save(contentType); - - var content1 = MockedContent.CreateSimpleContent(contentType, "Tagged content 1", -1); - content1.AssignTags("tags", new[] { "hello", "world", "some", "tags", "plus" }); - contentService.SaveAndPublish(content1); - - var content2 = MockedContent.CreateSimpleContent(contentType, "Tagged content 2", -1); - content2.AssignTags("tags", new[] { "hello", "world", "some", "tags" }); - contentService.SaveAndPublish(content2); - - // verify - var tags = tagService.GetTagsForEntity(content1.Id); - Assert.AreEqual(5, tags.Count()); - var allTags = tagService.GetAllContentTags(); - Assert.AreEqual(5, allTags.Count()); - - contentService.MoveToRecycleBin(content1); - - // fixme - killing the rest of this test - // this is not working consistently even in 7 when unpublishing a branch - // in 8, tags never go away - one has to check that the entity is published and not trashed - return; - - // no more tags for this entity - tags = tagService.GetTagsForEntity(content1.Id); - Assert.AreEqual(0, tags.Count()); - - // tags still assigned to content2 are still there - allTags = tagService.GetAllContentTags(); - Assert.AreEqual(4, allTags.Count()); - - contentService.Move(content1, -1); - - Assert.IsFalse(content1.Published); - - // no more tags for this entity - tags = tagService.GetTagsForEntity(content1.Id); - Assert.AreEqual(0, tags.Count()); - - // tags still assigned to content2 are still there - allTags = tagService.GetAllContentTags(); - Assert.AreEqual(4, allTags.Count()); - - content1.PublishCulture(); - contentService.SaveAndPublish(content1); - - Assert.IsTrue(content1.Published); - - // tags are back - tags = tagService.GetTagsForEntity(content1.Id); - Assert.AreEqual(5, tags.Count()); - - // tags are back - allTags = tagService.GetAllContentTags(); - Assert.AreEqual(5, allTags.Count()); - } - - [Test] - public void TagsAreUpdatedWhenContentIsTrashedAndUnTrashed_All() - { - var contentService = ServiceContext.ContentService; - var contentTypeService = ServiceContext.ContentTypeService; - var tagService = ServiceContext.TagService; - var contentType = MockedContentTypes.CreateSimpleContentType("umbMandatory", "Mandatory Doc Type", true); - contentType.PropertyGroups.First().PropertyTypes.Add( - new PropertyType("test", ValueStorageType.Ntext, "tags") - { - DataTypeId = 1041 - }); - contentTypeService.Save(contentType); - - var content1 = MockedContent.CreateSimpleContent(contentType, "Tagged content 1", -1); - content1.AssignTags("tags", new[] { "hello", "world", "some", "tags", "bam" }); - contentService.SaveAndPublish(content1); - - var content2 = MockedContent.CreateSimpleContent(contentType, "Tagged content 2", -1); - content2.AssignTags("tags", new[] { "hello", "world", "some", "tags" }); - contentService.SaveAndPublish(content2); - - // verify - var tags = tagService.GetTagsForEntity(content1.Id); - Assert.AreEqual(5, tags.Count()); - var allTags = tagService.GetAllContentTags(); - Assert.AreEqual(5, allTags.Count()); - - contentService.Unpublish(content1); - contentService.Unpublish(content2); - - // fixme - killing the rest of this test - // this is not working consistently even in 7 when unpublishing a branch - // in 8, tags never go away - one has to check that the entity is published and not trashed - return; - - // no more tags - tags = tagService.GetTagsForEntity(content1.Id); - Assert.AreEqual(0, tags.Count()); - - // no more tags - allTags = tagService.GetAllContentTags(); - Assert.AreEqual(0, allTags.Count()); - - contentService.Move(content1, -1); - contentService.Move(content2, -1); - - // no more tags - tags = tagService.GetTagsForEntity(content1.Id); - Assert.AreEqual(0, tags.Count()); - - // no more tags - allTags = tagService.GetAllContentTags(); - Assert.AreEqual(0, allTags.Count()); - - content1.PublishCulture(); - contentService.SaveAndPublish(content1); - content2.PublishCulture(); - contentService.SaveAndPublish(content2); - - // tags are back - tags = tagService.GetTagsForEntity(content1.Id); - Assert.AreEqual(5, tags.Count()); - - // tags are back - allTags = tagService.GetAllContentTags(); - Assert.AreEqual(5, allTags.Count()); - } - - [Test] - [Ignore("U4-8442, will need to be fixed eventually.")] - public void TagsAreUpdatedWhenContentIsTrashedAndUnTrashed_Tree() - { - var contentService = ServiceContext.ContentService; - var contentTypeService = ServiceContext.ContentTypeService; - var tagService = ServiceContext.TagService; - var contentType = MockedContentTypes.CreateSimpleContentType("umbMandatory", "Mandatory Doc Type", true); - contentType.PropertyGroups.First().PropertyTypes.Add( - new PropertyType("test", ValueStorageType.Ntext, "tags") - { - DataTypeId = 1041 - }); - contentTypeService.Save(contentType); - - var content1 = MockedContent.CreateSimpleContent(contentType, "Tagged content 1", -1); - content1.AssignTags("tags", new[] { "hello", "world", "some", "tags", "plus" }); - content1.PublishCulture(); - contentService.SaveAndPublish(content1); - - var content2 = MockedContent.CreateSimpleContent(contentType, "Tagged content 2", content1.Id); - content2.AssignTags("tags", new[] { "hello", "world", "some", "tags" }); - content2.PublishCulture(); - contentService.SaveAndPublish(content2); - - // verify - var tags = tagService.GetTagsForEntity(content1.Id); - Assert.AreEqual(5, tags.Count()); - var allTags = tagService.GetAllContentTags(); - Assert.AreEqual(5, allTags.Count()); - - contentService.MoveToRecycleBin(content1); - - // no more tags - tags = tagService.GetTagsForEntity(content1.Id); - Assert.AreEqual(0, tags.Count()); - tags = tagService.GetTagsForEntity(content2.Id); - Assert.AreEqual(0, tags.Count()); - - // no more tags - allTags = tagService.GetAllContentTags(); - Assert.AreEqual(0, allTags.Count()); - - contentService.Move(content1, -1); - - Assert.IsFalse(content1.Published); - - // no more tags - tags = tagService.GetTagsForEntity(content1.Id); - Assert.AreEqual(0, tags.Count()); - tags = tagService.GetTagsForEntity(content2.Id); - Assert.AreEqual(0, tags.Count()); - - // no more tags - allTags = tagService.GetAllContentTags(); - Assert.AreEqual(0, allTags.Count()); - - content1.PublishCulture(); - contentService.SaveAndPublish(content1); - - Assert.IsTrue(content1.Published); - - // tags are back - tags = tagService.GetTagsForEntity(content1.Id); - Assert.AreEqual(5, tags.Count()); - - // fixme tag & tree issue - // when we publish, we 'just' publish the top one and not the ones below = fails - // what we should do is... NOT clear tags when unpublishing or trashing or... - // and just update the tag service to NOT return anything related to trashed or - // unpublished entities (since trashed is set on ALL entities in the trashed branch) - tags = tagService.GetTagsForEntity(content2.Id); // including that one! - Assert.AreEqual(4, tags.Count()); - - // tags are back - allTags = tagService.GetAllContentTags(); - Assert.AreEqual(5, allTags.Count()); - } - - [Test] - public void TagsAreUpdatedWhenContentIsUnpublishedAndRePublished() - { - var contentService = ServiceContext.ContentService; - var contentTypeService = ServiceContext.ContentTypeService; - var tagService = ServiceContext.TagService; - var contentType = MockedContentTypes.CreateSimpleContentType("umbMandatory", "Mandatory Doc Type", true); - contentType.PropertyGroups.First().PropertyTypes.Add( - new PropertyType("test", ValueStorageType.Ntext, "tags") - { - DataTypeId = 1041 - }); - contentTypeService.Save(contentType); - - var content1 = MockedContent.CreateSimpleContent(contentType, "Tagged content 1", -1); - content1.AssignTags("tags", new[] { "hello", "world", "some", "tags", "bam" }); - contentService.SaveAndPublish(content1); - - var content2 = MockedContent.CreateSimpleContent(contentType, "Tagged content 2", -1); - content2.AssignTags("tags", new[] { "hello", "world", "some", "tags" }); - contentService.SaveAndPublish(content2); - - contentService.Unpublish(content1); - contentService.Unpublish(content2); - - // fixme - killing the rest of this test - // this is not working consistently even in 7 when unpublishing a branch - // in 8, tags never go away - one has to check that the entity is published and not trashed - return; - - var tags = tagService.GetTagsForEntity(content1.Id); - Assert.AreEqual(0, tags.Count()); - var allTags = tagService.GetAllContentTags(); - Assert.AreEqual(0, allTags.Count()); - - content2.PublishCulture(); - contentService.SaveAndPublish(content2); - - tags = tagService.GetTagsForEntity(content2.Id); - Assert.AreEqual(4, tags.Count()); - allTags = tagService.GetAllContentTags(); - Assert.AreEqual(4, allTags.Count()); - } - - [Test] - [Ignore("U4-8442, will need to be fixed eventually.")] - public void TagsAreUpdatedWhenContentIsUnpublishedAndRePublished_Tree() - { - var contentService = ServiceContext.ContentService; - var contentTypeService = ServiceContext.ContentTypeService; - var tagService = ServiceContext.TagService; - var contentType = MockedContentTypes.CreateSimpleContentType("umbMandatory", "Mandatory Doc Type", true); - contentType.PropertyGroups.First().PropertyTypes.Add( - new PropertyType("test", ValueStorageType.Ntext, "tags") - { - DataTypeId = 1041 - }); - contentTypeService.Save(contentType); - - var content1 = MockedContent.CreateSimpleContent(contentType, "Tagged content 1", -1); - content1.AssignTags("tags", new[] { "hello", "world", "some", "tags", "bam" }); - content1.PublishCulture(); - contentService.SaveAndPublish(content1); - - var content2 = MockedContent.CreateSimpleContent(contentType, "Tagged content 2", content1); - content2.AssignTags("tags", new[] { "hello", "world", "some", "tags" }); - content2.PublishCulture(); - contentService.SaveAndPublish(content2); - - contentService.Unpublish(content1); - - var tags = tagService.GetTagsForEntity(content1.Id); - Assert.AreEqual(0, tags.Count()); - - // fixme tag & tree issue - // when we (un)publish, we 'just' publish the top one and not the ones below = fails - // see similar note above - tags = tagService.GetTagsForEntity(content2.Id); - Assert.AreEqual(0, tags.Count()); - var allTags = tagService.GetAllContentTags(); - Assert.AreEqual(0, allTags.Count()); - - content1.PublishCulture(); - contentService.SaveAndPublish(content1); - - tags = tagService.GetTagsForEntity(content2.Id); - Assert.AreEqual(4, tags.Count()); - allTags = tagService.GetAllContentTags(); - Assert.AreEqual(5, allTags.Count()); - } - - [Test] - public void Create_Tag_Data_Bulk_Publish_Operation() - { - //Arrange - var contentService = ServiceContext.ContentService; - var contentTypeService = ServiceContext.ContentTypeService; - var dataTypeService = ServiceContext.DataTypeService; - - //set configuration - var dataType = dataTypeService.GetDataType(1041); - dataType.Configuration = new TagConfiguration - { - Group = "test", - StorageType = TagsStorageType.Csv - }; - - var contentType = MockedContentTypes.CreateSimpleContentType("umbMandatory", "Mandatory Doc Type", true); - contentType.PropertyGroups.First().PropertyTypes.Add( - new PropertyType("test", ValueStorageType.Ntext, "tags") - { - DataTypeId = 1041 - }); - contentTypeService.Save(contentType); - contentType.AllowedContentTypes = new[] { new ContentTypeSort(new Lazy(() => contentType.Id), 0, contentType.Alias) }; - - var content = MockedContent.CreateSimpleContent(contentType, "Tagged content", -1); - content.AssignTags("tags", new[] { "hello", "world", "some", "tags" }); - contentService.Save(content); - - var child1 = MockedContent.CreateSimpleContent(contentType, "child 1 content", content.Id); - child1.AssignTags("tags", new[] { "hello1", "world1", "some1" }); - contentService.Save(child1); - - var child2 = MockedContent.CreateSimpleContent(contentType, "child 2 content", content.Id); - child2.AssignTags("tags", new[] { "hello2", "world2" }); - contentService.Save(child2); - - // Act - contentService.SaveAndPublishBranch(content, true); - - // Assert - var propertyTypeId = contentType.PropertyTypes.Single(x => x.Alias == "tags").Id; - - using (var scope = ScopeProvider.CreateScope()) - { - Assert.AreEqual(4, scope.Database.ExecuteScalar( - "SELECT COUNT(*) FROM cmsTagRelationship WHERE nodeId=@nodeId AND propertyTypeId=@propTypeId", - new { nodeId = content.Id, propTypeId = propertyTypeId })); - - Assert.AreEqual(3, scope.Database.ExecuteScalar( - "SELECT COUNT(*) FROM cmsTagRelationship WHERE nodeId=@nodeId AND propertyTypeId=@propTypeId", - new { nodeId = child1.Id, propTypeId = propertyTypeId })); - - Assert.AreEqual(2, scope.Database.ExecuteScalar( - "SELECT COUNT(*) FROM cmsTagRelationship WHERE nodeId=@nodeId AND propertyTypeId=@propTypeId", - new { nodeId = child2.Id, propTypeId = propertyTypeId })); - - scope.Complete(); - } - } - - [Test] - public void Does_Not_Create_Tag_Data_For_Non_Published_Version() - { - var contentService = ServiceContext.ContentService; - var contentTypeService = ServiceContext.ContentTypeService; - - // create content type with a tag property - var contentType = MockedContentTypes.CreateSimpleContentType("umbMandatory", "Mandatory Doc Type", true); - contentType.PropertyGroups.First().PropertyTypes.Add(new PropertyType("test", ValueStorageType.Ntext, "tags") { DataTypeId = 1041 }); - contentTypeService.Save(contentType); - - // create a content with tags and publish - var content = MockedContent.CreateSimpleContent(contentType, "Tagged content", -1); - content.AssignTags("tags", new[] { "hello", "world", "some", "tags" }); - contentService.SaveAndPublish(content); - - // edit tags and save - content.AssignTags("tags", new[] { "another", "world" }, merge: true); - contentService.Save(content); - - // the (edit) property does contain all tags - Assert.AreEqual(5, content.Properties["tags"].GetValue().ToString().Split(',').Distinct().Count()); - - // but the database still contains the initial two tags - var propertyTypeId = contentType.PropertyTypes.Single(x => x.Alias == "tags").Id; - using (var scope = ScopeProvider.CreateScope()) - { - Assert.AreEqual(4, scope.Database.ExecuteScalar( - "SELECT COUNT(*) FROM cmsTagRelationship WHERE nodeId=@nodeId AND propertyTypeId=@propTypeId", - new { nodeId = content.Id, propTypeId = propertyTypeId })); - scope.Complete(); - } - } - - [Test] - public void Can_Replace_Tag_Data_To_Published_Content() - { - //Arrange - var contentService = ServiceContext.ContentService; - var contentTypeService = ServiceContext.ContentTypeService; - var contentType = MockedContentTypes.CreateSimpleContentType("umbMandatory", "Mandatory Doc Type", true); - contentType.PropertyGroups.First().PropertyTypes.Add( - new PropertyType("test", ValueStorageType.Ntext, "tags") - { - DataTypeId = 1041 - }); - contentTypeService.Save(contentType); - - var content = MockedContent.CreateSimpleContent(contentType, "Tagged content", -1); - - - // Act - content.AssignTags("tags", new[] { "hello", "world", "some", "tags" }); - contentService.SaveAndPublish(content); - - // Assert - Assert.AreEqual(4, content.Properties["tags"].GetValue().ToString().Split(',').Distinct().Count()); - var propertyTypeId = contentType.PropertyTypes.Single(x => x.Alias == "tags").Id; - using (var scope = ScopeProvider.CreateScope()) - { - Assert.AreEqual(4, scope.Database.ExecuteScalar( - "SELECT COUNT(*) FROM cmsTagRelationship WHERE nodeId=@nodeId AND propertyTypeId=@propTypeId", - new { nodeId = content.Id, propTypeId = propertyTypeId })); - - scope.Complete(); - } - } - - [Test] - public void Can_Append_Tag_Data_To_Published_Content() - { - //Arrange - var contentService = ServiceContext.ContentService; - var contentTypeService = ServiceContext.ContentTypeService; - var contentType = MockedContentTypes.CreateSimpleContentType("umbMandatory", "Mandatory Doc Type", true); - contentType.PropertyGroups.First().PropertyTypes.Add( - new PropertyType("test", ValueStorageType.Ntext, "tags") - { - DataTypeId = 1041 - }); - contentTypeService.Save(contentType); - var content = MockedContent.CreateSimpleContent(contentType, "Tagged content", -1); - content.AssignTags("tags", new[] { "hello", "world", "some", "tags" }); - contentService.SaveAndPublish(content); - - // Act - content.AssignTags("tags", new[] { "another", "world" }, merge: true); - contentService.SaveAndPublish(content); - - // Assert - Assert.AreEqual(5, content.Properties["tags"].GetValue().ToString().Split(',').Distinct().Count()); - var propertyTypeId = contentType.PropertyTypes.Single(x => x.Alias == "tags").Id; - using (var scope = ScopeProvider.CreateScope()) - { - Assert.AreEqual(5, scope.Database.ExecuteScalar( - "SELECT COUNT(*) FROM cmsTagRelationship WHERE nodeId=@nodeId AND propertyTypeId=@propTypeId", - new { nodeId = content.Id, propTypeId = propertyTypeId })); - - scope.Complete(); - } - } - - [Test] - public void Can_Remove_Tag_Data_To_Published_Content() - { - //Arrange - var contentService = ServiceContext.ContentService; - var contentTypeService = ServiceContext.ContentTypeService; - var contentType = MockedContentTypes.CreateSimpleContentType("umbMandatory", "Mandatory Doc Type", true); - contentType.PropertyGroups.First().PropertyTypes.Add( - new PropertyType("test", ValueStorageType.Ntext, "tags") - { - DataTypeId = 1041 - }); - contentTypeService.Save(contentType); - var content = MockedContent.CreateSimpleContent(contentType, "Tagged content", -1); - content.AssignTags("tags", new[] { "hello", "world", "some", "tags" }); - contentService.SaveAndPublish(content); - - // Act - content.RemoveTags("tags", new[] { "some", "world" }); - contentService.SaveAndPublish(content); - - // Assert - Assert.AreEqual(2, content.Properties["tags"].GetValue().ToString().Split(',').Distinct().Count()); - var propertyTypeId = contentType.PropertyTypes.Single(x => x.Alias == "tags").Id; - using (var scope = ScopeProvider.CreateScope()) - { - Assert.AreEqual(2, scope.Database.ExecuteScalar( - "SELECT COUNT(*) FROM cmsTagRelationship WHERE nodeId=@nodeId AND propertyTypeId=@propTypeId", - new { nodeId = content.Id, propTypeId = propertyTypeId })); - - scope.Complete(); - } - } - [Test] public void Can_Remove_Property_Type() { @@ -1134,7 +572,6 @@ namespace Umbraco.Tests.Services Assert.That(contents.Count(), Is.GreaterThanOrEqualTo(2)); } - [Test] public void Can_Get_All_Versions_Of_Content() { diff --git a/src/Umbraco.Tests/Services/ContentTypeServiceTests.cs b/src/Umbraco.Tests/Services/ContentTypeServiceTests.cs index 83baca5745..b1a8fa26a8 100644 --- a/src/Umbraco.Tests/Services/ContentTypeServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentTypeServiceTests.cs @@ -149,7 +149,7 @@ namespace Umbraco.Tests.Services //change the content type to be invariant, we will also update the name here to detect the copy changes doc.SetCultureName("Hello2", "en-US"); ServiceContext.ContentService.Save(doc); - contentType.Variations = ContentVariation.Nothing; + contentType.Variations = ContentVariation.Nothing; ServiceContext.ContentTypeService.Save(contentType); doc = ServiceContext.ContentService.GetById(doc.Id); //re-get @@ -373,7 +373,7 @@ namespace Umbraco.Tests.Services doc2 = ServiceContext.ContentService.GetById(doc2.Id); //re-get //this will be null because the doc type was changed back to variant but it's property types don't get changed back - Assert.IsNull(doc.GetValue("title", "en-US")); + Assert.IsNull(doc.GetValue("title", "en-US")); Assert.IsNull(doc2.GetValue("title", "en-US")); } @@ -1715,50 +1715,65 @@ namespace Umbraco.Tests.Services // Arrange var service = ServiceContext.ContentTypeService; + // create 'page' content type with a 'Content_' group var page = MockedContentTypes.CreateSimpleContentType("page", "Page", null, false, "Content_"); + Assert.IsTrue(page.PropertyGroups.Contains("Content_")); + Assert.AreEqual(3, page.PropertyTypes.Count()); service.Save(page); + + // create 'contentPage' content type as a child of 'page' var contentPage = MockedContentTypes.CreateSimpleContentType("contentPage", "Content Page", page, true); - service.Save(contentPage); - var composition = MockedContentTypes.CreateMetaContentType(); - composition.AddPropertyGroup("Content"); - service.Save(composition); - //Adding Meta-composition to child doc type - contentPage.AddContentType(composition); + Assert.AreEqual(3, contentPage.PropertyTypes.Count()); service.Save(contentPage); - // Act - var propertyTypeOne = new PropertyType(Constants.PropertyEditors.Aliases.TextBox, ValueStorageType.Ntext, "testTextbox") + // add 'Content' group to 'meta' content type + var meta = MockedContentTypes.CreateMetaContentType(); + meta.AddPropertyGroup("Content"); + Assert.AreEqual(2, meta.PropertyTypes.Count()); + service.Save(meta); + + // add 'meta' content type to 'contentPage' composition + contentPage.AddContentType(meta); + service.Save(contentPage); + + // add property 'prop1' to 'contentPage' group 'Content_' + var prop1 = new PropertyType(Constants.PropertyEditors.Aliases.TextBox, ValueStorageType.Ntext, "testTextbox") { Name = "Test Textbox", Description = "", Mandatory = false, SortOrder = 1, DataTypeId = -88 }; - var firstOneAdded = contentPage.AddPropertyType(propertyTypeOne, "Content_"); - var propertyTypeTwo = new PropertyType(Constants.PropertyEditors.Aliases.TextBox, ValueStorageType.Ntext, "anotherTextbox") + var prop1Added = contentPage.AddPropertyType(prop1, "Content_"); + Assert.IsTrue(prop1Added); + + // add property 'prop2' to 'contentPage' group 'Content' + var prop2 = new PropertyType(Constants.PropertyEditors.Aliases.TextBox, ValueStorageType.Ntext, "anotherTextbox") { Name = "Another Test Textbox", Description = "", Mandatory = false, SortOrder = 1, DataTypeId = -88 }; - var secondOneAdded = contentPage.AddPropertyType(propertyTypeTwo, "Content"); + var prop2Added = contentPage.AddPropertyType(prop2, "Content"); + Assert.IsTrue(prop2Added); + + // save 'contentPage' content type service.Save(contentPage); - Assert.That(page.PropertyGroups.Contains("Content_"), Is.True); - var propertyGroup = page.PropertyGroups["Content_"]; - page.PropertyGroups.Add(new PropertyGroup(true) { Id = propertyGroup.Id, Name = "ContentTab", SortOrder = 0}); + var group = page.PropertyGroups["Content_"]; + group.Name = "ContentTab"; // rename the group service.Save(page); + Assert.AreEqual(3, page.PropertyTypes.Count()); - // Assert - Assert.That(firstOneAdded, Is.True); - Assert.That(secondOneAdded, Is.True); + // get 'contentPage' content type again + var contentPageAgain = service.Get("contentPage"); + Assert.IsNotNull(contentPageAgain); - var contentType = service.Get("contentPage"); - Assert.That(contentType, Is.Not.Null); + // assert that 'Content_' group is still there because we don't propagate renames + var findGroup = contentPageAgain.CompositionPropertyGroups.FirstOrDefault(x => x.Name == "Content_"); + Assert.IsNotNull(findGroup); - var compositionPropertyGroups = contentType.CompositionPropertyGroups; - - // now it is still 1, because we don't propagate renames anymore - Assert.That(compositionPropertyGroups.Count(x => x.Name.Equals("Content_")), Is.EqualTo(1)); - - var propertyTypeCount = contentType.PropertyTypes.Count(); - var compPropertyTypeCount = contentType.CompositionPropertyTypes.Count(); + // count all property types (local and composed) + var propertyTypeCount = contentPageAgain.PropertyTypes.Count(); Assert.That(propertyTypeCount, Is.EqualTo(5)); + + // count composed property types + var compPropertyTypeCount = contentPageAgain.CompositionPropertyTypes.Count(); Assert.That(compPropertyTypeCount, Is.EqualTo(10)); } diff --git a/src/Umbraco.Tests/TestHelpers/Stubs/TestPublishedContent.cs b/src/Umbraco.Tests/TestHelpers/Stubs/TestPublishedContent.cs index 0faf1537b3..9c0bb61cb3 100644 --- a/src/Umbraco.Tests/TestHelpers/Stubs/TestPublishedContent.cs +++ b/src/Umbraco.Tests/TestHelpers/Stubs/TestPublishedContent.cs @@ -47,7 +47,7 @@ namespace Umbraco.Tests.TestHelpers.Stubs public string Url { get; set; } public string GetUrl(string culture = null) => throw new NotSupportedException(); public PublishedItemType ItemType => ContentType.ItemType; - public bool IsDraft { get; set; } + public bool IsDraft(string culture = null) => false; public IPublishedContent Parent { get; set; } public IEnumerable Children { get; set; } diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 0b6b65f103..a037d83eb5 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -136,6 +136,7 @@ + diff --git a/src/Umbraco.Web.UI.Client/package-lock.json b/src/Umbraco.Web.UI.Client/package-lock.json index 8fce15bcdb..a36fd4e051 100644 --- a/src/Umbraco.Web.UI.Client/package-lock.json +++ b/src/Umbraco.Web.UI.Client/package-lock.json @@ -940,7 +940,7 @@ }, "ansi-colors": { "version": "1.1.0", - "resolved": "http://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz", "integrity": "sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA==", "dev": true, "requires": { @@ -958,7 +958,7 @@ }, "ansi-escapes": { "version": "3.1.0", - "resolved": "http://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.1.0.tgz", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.1.0.tgz", "integrity": "sha512-UgAb8H9D41AQnu/PbWlCofQVcnV4Gs2bBJi9eZPxfU/hgglFh3SMDMENRIqdr7H6XFnXdoknctFByVsCOotTVw==", "dev": true }, @@ -2024,12 +2024,12 @@ }, "camelcase-keys": { "version": "2.1.0", - "resolved": "http://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", "dev": true, "requires": { - "camelcase": "2.1.1", - "map-obj": "1.0.1" + "camelcase": "^2.0.0", + "map-obj": "^1.0.0" } }, "caniuse-api": { @@ -2052,7 +2052,7 @@ }, "canonical-path": { "version": "0.0.2", - "resolved": "http://registry.npmjs.org/canonical-path/-/canonical-path-0.0.2.tgz", + "resolved": "https://registry.npmjs.org/canonical-path/-/canonical-path-0.0.2.tgz", "integrity": "sha1-4x65N6jJPuKgHfGDl5RyGQKHRXQ=", "dev": true }, @@ -2284,13 +2284,13 @@ "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "1.0.0", - "process-nextick-args": "2.0.0", - "safe-buffer": "5.1.2", - "string_decoder": "1.1.1", - "util-deprecate": "1.0.2" + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" } }, "string_decoder": { @@ -2299,7 +2299,7 @@ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, "requires": { - "safe-buffer": "5.1.2" + "safe-buffer": "~5.1.0" } } } @@ -2456,7 +2456,7 @@ }, "string_decoder": { "version": "1.1.1", - "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, "requires": { @@ -2621,7 +2621,7 @@ }, "css-color-names": { "version": "0.0.4", - "resolved": "http://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz", + "resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz", "integrity": "sha1-gIrcLnnPhHOAabZGyyDsJ762KeA=", "dev": true }, @@ -3198,7 +3198,7 @@ }, "duplexer": { "version": "0.1.1", - "resolved": "http://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=", "dev": true }, @@ -3401,7 +3401,7 @@ }, "es6-promise": { "version": "3.3.1", - "resolved": "http://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", "integrity": "sha1-oIzd6EzNvzTQJ6FFG8kdS80ophM=", "dev": true }, @@ -3750,12 +3750,12 @@ }, "expand-range": { "version": "0.1.1", - "resolved": "http://registry.npmjs.org/expand-range/-/expand-range-0.1.1.tgz", + "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-0.1.1.tgz", "integrity": "sha1-TLjtoJk8pW+k9B/ELzy7TMrf8EQ=", "dev": true, "requires": { - "is-number": "0.1.1", - "repeat-string": "0.2.2" + "is-number": "^0.1.1", + "repeat-string": "^0.2.2" } }, "is-number": { @@ -3824,24 +3824,24 @@ }, "expand-range": { "version": "1.8.2", - "resolved": "http://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz", + "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz", "integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=", "dev": true, "requires": { - "fill-range": "2.2.4" + "fill-range": "^2.1.0" }, "dependencies": { "fill-range": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.4.tgz", - "integrity": "sha512-cnrcCbj01+j2gTG921VZPnHbjmdAf8oQV/iGeV2kZxGSyfYjjTyY79ErsK1WJWMpw6DaApEX72binqJE+/d+5Q==", + "integrity": "sha1-6x53OrsFbc2N8r/favWbizqTZWU=", "dev": true, "requires": { - "is-number": "2.1.0", - "isobject": "2.1.0", - "randomatic": "3.1.1", - "repeat-element": "1.1.3", - "repeat-string": "1.6.1" + "is-number": "^2.1.0", + "isobject": "^2.0.0", + "randomatic": "^3.0.0", + "repeat-element": "^1.1.2", + "repeat-string": "^1.5.2" } }, "is-number": { @@ -3850,7 +3850,7 @@ "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=", "dev": true, "requires": { - "kind-of": "3.2.2" + "kind-of": "^3.0.2" } }, "isarray": { @@ -3874,7 +3874,7 @@ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "dev": true, "requires": { - "is-buffer": "1.1.6" + "is-buffer": "^1.1.5" } } } @@ -4483,13 +4483,13 @@ }, "fs-extra": { "version": "1.0.0", - "resolved": "http://registry.npmjs.org/fs-extra/-/fs-extra-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-1.0.0.tgz", "integrity": "sha1-zTzl9+fLYUWIP8rjGR6Yd/hYeVA=", "dev": true, "requires": { - "graceful-fs": "4.1.15", - "jsonfile": "2.4.0", - "klaw": "1.3.1" + "graceful-fs": "^4.1.2", + "jsonfile": "^2.1.0", + "klaw": "^1.0.0" }, "dependencies": { "graceful-fs": { @@ -5224,24 +5224,24 @@ "dependencies": { "readable-stream": { "version": "1.0.34", - "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", "dev": true, "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", "isarray": "0.0.1", - "string_decoder": "0.10.31" + "string_decoder": "~0.10.x" } }, "through2": { "version": "0.6.5", - "resolved": "http://registry.npmjs.org/through2/-/through2-0.6.5.tgz", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz", "integrity": "sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=", "dev": true, "requires": { - "readable-stream": "1.0.34", - "xtend": "4.0.1" + "readable-stream": ">=1.0.33-1 <1.1.0-0", + "xtend": ">=4.0.0 <4.1.0-0" } } } @@ -5254,11 +5254,11 @@ }, "glob-watcher": { "version": "0.0.6", - "resolved": "http://registry.npmjs.org/glob-watcher/-/glob-watcher-0.0.6.tgz", + "resolved": "https://registry.npmjs.org/glob-watcher/-/glob-watcher-0.0.6.tgz", "integrity": "sha1-uVtKjfdLOcgymLDAXJeLTZo7cQs=", "dev": true, "requires": { - "gaze": "0.5.2" + "gaze": "^0.5.1" } }, "glob2base": { @@ -5370,7 +5370,7 @@ }, "graceful-fs": { "version": "1.2.3", - "resolved": "http://registry.npmjs.org/graceful-fs/-/graceful-fs-1.2.3.tgz", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-1.2.3.tgz", "integrity": "sha1-FaSAaldUfLLS2/J/QuiajDRRs2Q=", "dev": true }, @@ -5382,7 +5382,7 @@ }, "lodash": { "version": "1.0.2", - "resolved": "http://registry.npmjs.org/lodash/-/lodash-1.0.2.tgz", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-1.0.2.tgz", "integrity": "sha1-j1dWDIO1n8JwvT1WG2kAQ0MOJVE=", "dev": true }, @@ -5440,11 +5440,11 @@ }, "graceful-fs": { "version": "3.0.11", - "resolved": "http://registry.npmjs.org/graceful-fs/-/graceful-fs-3.0.11.tgz", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-3.0.11.tgz", "integrity": "sha1-dhPHeKGv6mLyXGMKCG1/Osu92Bg=", "dev": true, "requires": { - "natives": "1.1.6" + "natives": "^1.1.0" } }, "graceful-readlink": { @@ -5459,19 +5459,19 @@ "integrity": "sha1-VxzkWSjdQK9lFPxAEYZgFsE4RbQ=", "dev": true, "requires": { - "archy": "1.0.0", - "chalk": "1.1.3", - "deprecated": "0.0.1", - "gulp-util": "3.0.8", - "interpret": "1.1.0", - "liftoff": "2.5.0", - "minimist": "1.2.0", - "orchestrator": "0.3.8", - "pretty-hrtime": "1.0.3", - "semver": "4.3.6", - "tildify": "1.2.0", - "v8flags": "2.1.1", - "vinyl-fs": "0.3.14" + "archy": "^1.0.0", + "chalk": "^1.0.0", + "deprecated": "^0.0.1", + "gulp-util": "^3.0.0", + "interpret": "^1.0.0", + "liftoff": "^2.1.0", + "minimist": "^1.1.0", + "orchestrator": "^0.3.0", + "pretty-hrtime": "^1.0.0", + "semver": "^4.1.0", + "tildify": "^1.0.0", + "v8flags": "^2.0.2", + "vinyl-fs": "^0.3.0" }, "dependencies": { "ansi-styles": { @@ -5486,11 +5486,11 @@ "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", "dev": true, "requires": { - "ansi-styles": "2.2.1", - "escape-string-regexp": "1.0.5", - "has-ansi": "2.0.0", - "strip-ansi": "3.0.1", - "supports-color": "2.0.0" + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" } }, "semver": { @@ -5784,7 +5784,7 @@ }, "ansi-regex": { "version": "0.2.1", - "resolved": "http://registry.npmjs.org/ansi-regex/-/ansi-regex-0.2.1.tgz", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-0.2.1.tgz", "integrity": "sha1-DY6UaWej2BQ/k+JOKYUl/BsiNfk=", "dev": true }, @@ -5796,15 +5796,15 @@ }, "chalk": { "version": "0.5.1", - "resolved": "http://registry.npmjs.org/chalk/-/chalk-0.5.1.tgz", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.5.1.tgz", "integrity": "sha1-Zjs6ZItotV0EaQ1JFnqoN4WPIXQ=", "dev": true, "requires": { - "ansi-styles": "1.1.0", - "escape-string-regexp": "1.0.5", - "has-ansi": "0.1.0", - "strip-ansi": "0.3.0", - "supports-color": "0.2.0" + "ansi-styles": "^1.1.0", + "escape-string-regexp": "^1.0.0", + "has-ansi": "^0.1.0", + "strip-ansi": "^0.3.0", + "supports-color": "^0.2.0" } }, "clone": { @@ -5848,12 +5848,12 @@ "dependencies": { "through2": { "version": "0.5.1", - "resolved": "http://registry.npmjs.org/through2/-/through2-0.5.1.tgz", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.5.1.tgz", "integrity": "sha1-390BLrnHAOIyP9M084rGIqs3Lac=", "dev": true, "requires": { - "readable-stream": "1.0.34", - "xtend": "3.0.0" + "readable-stream": "~1.0.17", + "xtend": "~3.0.0" } } } @@ -5869,7 +5869,7 @@ }, "lodash": { "version": "2.4.1", - "resolved": "http://registry.npmjs.org/lodash/-/lodash-2.4.1.tgz", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.1.tgz", "integrity": "sha1-W3cjA03aTSYuWkb7LFjXzCL3FCA=", "dev": true }, @@ -5953,41 +5953,41 @@ "dependencies": { "through2": { "version": "0.5.1", - "resolved": "http://registry.npmjs.org/through2/-/through2-0.5.1.tgz", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.5.1.tgz", "integrity": "sha1-390BLrnHAOIyP9M084rGIqs3Lac=", "dev": true, "requires": { - "readable-stream": "1.0.34", - "xtend": "3.0.0" + "readable-stream": "~1.0.17", + "xtend": "~3.0.0" } } } }, "minimist": { "version": "0.2.0", - "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.2.0.tgz", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.2.0.tgz", "integrity": "sha1-Tf/lJdriuGTGbC4jxicdev3s784=", "dev": true }, "readable-stream": { "version": "1.0.34", - "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", "dev": true, "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", "isarray": "0.0.1", - "string_decoder": "0.10.31" + "string_decoder": "~0.10.x" } }, "strip-ansi": { "version": "0.3.0", - "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-0.3.0.tgz", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.3.0.tgz", "integrity": "sha1-JfSOoiynkYfzF0pNuHWTR7sSYiA=", "dev": true, "requires": { - "ansi-regex": "0.2.1" + "ansi-regex": "^0.2.1" } }, "supports-color": { @@ -5998,12 +5998,12 @@ }, "through2": { "version": "0.6.1", - "resolved": "http://registry.npmjs.org/through2/-/through2-0.6.1.tgz", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.6.1.tgz", "integrity": "sha1-90KzKJPovSYUbnieT9LMssB6cX4=", "dev": true, "requires": { - "readable-stream": "1.0.34", - "xtend": "4.0.1" + "readable-stream": ">=1.0.27-1 <1.1.0-0", + "xtend": ">=4.0.0 <4.1.0-0" }, "dependencies": { "xtend": { @@ -6213,13 +6213,13 @@ "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "1.0.0", - "process-nextick-args": "2.0.0", - "safe-buffer": "5.1.2", - "string_decoder": "1.1.1", - "util-deprecate": "1.0.2" + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" } }, "replace-ext": { @@ -6234,7 +6234,7 @@ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, "requires": { - "safe-buffer": "5.1.2" + "safe-buffer": "~5.1.0" } }, "vinyl": { @@ -6345,11 +6345,11 @@ }, "source-map": { "version": "0.1.43", - "resolved": "http://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz", "integrity": "sha1-wkvBRspRfBRx9drL4lcbK3+eM0Y=", "dev": true, "requires": { - "amdefine": "1.0.1" + "amdefine": ">=0.0.4" } }, "vinyl-sourcemaps-apply": { @@ -6923,11 +6923,11 @@ }, "is-builtin-module": { "version": "1.0.0", - "resolved": "http://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", "dev": true, "requires": { - "builtin-modules": "1.1.1" + "builtin-modules": "^1.0.0" } }, "is-callable": { @@ -7401,11 +7401,11 @@ }, "jsonfile": { "version": "2.4.0", - "resolved": "http://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", "integrity": "sha1-NzaitCi4e72gzIO1P6PWM6NcKug=", "dev": true, "requires": { - "graceful-fs": "4.1.15" + "graceful-fs": "^4.1.6" }, "dependencies": { "graceful-fs": { @@ -7528,7 +7528,7 @@ }, "kew": { "version": "0.7.0", - "resolved": "http://registry.npmjs.org/kew/-/kew-0.7.0.tgz", + "resolved": "https://registry.npmjs.org/kew/-/kew-0.7.0.tgz", "integrity": "sha1-edk9LTM2PW/dKXCzNdkUGtWR15s=", "dev": true }, @@ -7650,15 +7650,15 @@ }, "load-json-file": { "version": "1.1.0", - "resolved": "http://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", "dev": true, "requires": { - "graceful-fs": "4.1.15", - "parse-json": "2.2.0", - "pify": "2.3.0", - "pinkie-promise": "2.0.1", - "strip-bom": "2.0.0" + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0", + "strip-bom": "^2.0.0" }, "dependencies": { "graceful-fs": { @@ -7673,7 +7673,7 @@ "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", "dev": true, "requires": { - "error-ex": "1.3.2" + "error-ex": "^1.2.0" } }, "pify": { @@ -7688,7 +7688,7 @@ "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", "dev": true, "requires": { - "is-utf8": "0.2.1" + "is-utf8": "^0.2.0" } } } @@ -8075,7 +8075,7 @@ }, "lru-cache": { "version": "2.7.3", - "resolved": "http://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=", "dev": true }, @@ -8144,26 +8144,26 @@ }, "media-typer": { "version": "0.3.0", - "resolved": "http://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", "dev": true }, "meow": { "version": "3.7.0", - "resolved": "http://registry.npmjs.org/meow/-/meow-3.7.0.tgz", + "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", "dev": true, "requires": { - "camelcase-keys": "2.1.0", - "decamelize": "1.2.0", - "loud-rejection": "1.6.0", - "map-obj": "1.0.1", - "minimist": "1.2.0", - "normalize-package-data": "2.4.0", - "object-assign": "4.1.1", - "read-pkg-up": "1.0.1", - "redent": "1.0.0", - "trim-newlines": "1.0.0" + "camelcase-keys": "^2.0.0", + "decamelize": "^1.1.2", + "loud-rejection": "^1.0.0", + "map-obj": "^1.0.1", + "minimist": "^1.1.3", + "normalize-package-data": "^2.3.4", + "object-assign": "^4.0.1", + "read-pkg-up": "^1.0.1", + "redent": "^1.0.0", + "trim-newlines": "^1.0.0" }, "dependencies": { "object-assign": { @@ -8195,13 +8195,13 @@ "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "1.0.0", - "process-nextick-args": "2.0.0", - "safe-buffer": "5.1.2", - "string_decoder": "1.1.1", - "util-deprecate": "1.0.2" + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" } }, "string_decoder": { @@ -8210,7 +8210,7 @@ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, "requires": { - "safe-buffer": "5.1.2" + "safe-buffer": "~5.1.0" } } } @@ -8313,7 +8313,7 @@ }, "mkdirp": { "version": "0.5.1", - "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "dev": true, "requires": { @@ -11461,7 +11461,7 @@ }, "os-tmpdir": { "version": "1.0.2", - "resolved": "http://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", "dev": true }, @@ -11642,7 +11642,7 @@ }, "path-is-absolute": { "version": "1.0.1", - "resolved": "http://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "dev": true }, @@ -11690,11 +11690,11 @@ }, "pause-stream": { "version": "0.0.11", - "resolved": "http://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", + "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", "integrity": "sha1-/lo0sMvOErWqaitAPuLnO2AvFEU=", "dev": true, "requires": { - "through": "2.3.8" + "through": "~2.3" } }, "pend": { @@ -12473,14 +12473,14 @@ }, "readable-stream": { "version": "1.1.14", - "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", "dev": true, "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", "isarray": "0.0.1", - "string_decoder": "0.10.31" + "string_decoder": "~0.10.x" } }, "readdirp": { @@ -12523,7 +12523,7 @@ }, "string_decoder": { "version": "1.1.1", - "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, "requires": { @@ -12998,11 +12998,11 @@ }, "safe-regex": { "version": "1.1.0", - "resolved": "http://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", "dev": true, "requires": { - "ret": "0.1.15" + "ret": "~0.1.10" } }, "safer-buffer": { @@ -13725,26 +13725,26 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "1.0.0", - "process-nextick-args": "2.0.0", - "safe-buffer": "5.1.2", - "string_decoder": "1.1.1", - "util-deprecate": "1.0.2" + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" } }, "string_decoder": { "version": "1.1.1", - "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, "requires": { - "safe-buffer": "5.1.2" + "safe-buffer": "~5.1.0" } } } @@ -13791,17 +13791,17 @@ }, "string_decoder": { "version": "0.10.31", - "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", "dev": true }, "strip-ansi": { "version": "3.0.1", - "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "dev": true, "requires": { - "ansi-regex": "2.1.1" + "ansi-regex": "^2.0.0" } }, "strip-bom": { @@ -14075,7 +14075,7 @@ }, "through": { "version": "2.3.8", - "resolved": "http://registry.npmjs.org/through/-/through-2.3.8.tgz", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", "dev": true }, @@ -14101,13 +14101,13 @@ "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "1.0.0", - "process-nextick-args": "2.0.0", - "safe-buffer": "5.1.2", - "string_decoder": "1.1.1", - "util-deprecate": "1.0.2" + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" } }, "string_decoder": { @@ -14116,7 +14116,7 @@ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, "requires": { - "safe-buffer": "5.1.2" + "safe-buffer": "~5.1.0" } } } @@ -14777,10 +14777,10 @@ "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", "dev": true, "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", "isarray": "0.0.1", - "string_decoder": "0.10.31" + "string_decoder": "~0.10.x" } }, "through2": { @@ -14789,8 +14789,8 @@ "integrity": "sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=", "dev": true, "requires": { - "readable-stream": "1.0.34", - "xtend": "4.0.1" + "readable-stream": ">=1.0.33-1 <1.1.0-0", + "xtend": ">=4.0.0 <4.1.0-0" } }, "vinyl": { @@ -14909,13 +14909,13 @@ }, "yargs": { "version": "3.10.0", - "resolved": "http://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", "dev": true, "requires": { - "camelcase": "1.2.1", - "cliui": "2.1.0", - "decamelize": "1.2.0", + "camelcase": "^1.0.2", + "cliui": "^2.1.0", + "decamelize": "^1.0.0", "window-size": "0.1.0" }, "dependencies": { diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js index 407416f2a1..f1e2150579 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js @@ -815,7 +815,7 @@ //ensure the save flag is set selectedVariant.save = true; - performSave({ saveMethod: contentResource.publish, action: "save" }).then(function (data) { + performSave({ saveMethod: $scope.saveMethod(), action: "save" }).then(function (data) { previewWindow.location.href = redirect; }, function (err) { //validation issues .... diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbtabbedcontent.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbtabbedcontent.directive.js index f83f441d66..5ebb40fac6 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbtabbedcontent.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbtabbedcontent.directive.js @@ -12,11 +12,12 @@ //expose the property/methods for other directives to use this.content = $scope.content; - - $scope.activeVariant = _.find(this.content.variants, variant => { + this.activeVariant = _.find(this.content.variants, variant => { return variant.active; }); + $scope.activeVariant = this.activeVariant; + $scope.defaultVariant = _.find(this.content.variants, variant => { return variant.language.isDefault; }); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/events/events.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/events/events.directive.js index a6a5336af0..7578ade867 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/events/events.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/events/events.directive.js @@ -95,7 +95,7 @@ angular.module('umbraco.directives') }; }) -.directive('onOutsideClick', function ($timeout) { +.directive('onOutsideClick', function ($timeout, angularHelper) { return function (scope, element, attrs) { var eventBindings = []; @@ -136,7 +136,7 @@ angular.module('umbraco.directives') return; } - scope.$apply(attrs.onOutsideClick); + angularHelper.safeApply(scope, attrs.onOutsideClick); } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/imaging/umbimagecrop.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/imaging/umbimagecrop.directive.js index ffa76a57c3..73b74ead8b 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/imaging/umbimagecrop.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/imaging/umbimagecrop.directive.js @@ -274,7 +274,7 @@ angular.module("umbraco.directives") } //// INIT ///// - $image.load(function(){ + $image.on("load", function(){ $timeout(function(){ init($image); }); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/imaging/umbimagethumbnail.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/imaging/umbimagethumbnail.directive.js index dd6dcffc31..b1bb603ace 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/imaging/umbimagethumbnail.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/imaging/umbimagethumbnail.directive.js @@ -27,34 +27,34 @@ angular.module("umbraco.directives") var $image = element.find("img"); scope.loaded = false; - $image.load(function(){ - $timeout(function(){ - $image.width("auto"); - $image.height("auto"); + $image.on("load", function() { + $timeout(function () { + $image.width("auto"); + $image.height("auto"); - scope.image = {}; - scope.image.width = $image[0].width; - scope.image.height = $image[0].height; + scope.image = {}; + scope.image.width = $image[0].width; + scope.image.height = $image[0].height; - //we force a lower thumbnail size to fit the max size - //we do not compare to the image dimensions, but the thumbs - if(scope.maxSize){ - var ratioCalculation = cropperHelper.calculateAspectRatioFit( - scope.width, - scope.height, - scope.maxSize, - scope.maxSize, - false); + //we force a lower thumbnail size to fit the max size + //we do not compare to the image dimensions, but the thumbs + if (scope.maxSize) { + var ratioCalculation = cropperHelper.calculateAspectRatioFit( + scope.width, + scope.height, + scope.maxSize, + scope.maxSize, + false); - //so if we have a max size, override the thumb sizes - scope.width = ratioCalculation.width; - scope.height = ratioCalculation.height; - } + //so if we have a max size, override the thumb sizes + scope.width = ratioCalculation.width; + scope.height = ratioCalculation.height; + } - setPreviewStyle(); - scope.loaded = true; - }); - }); + setPreviewStyle(); + scope.loaded = true; + }); + }) /// WATCHERS //// scope.$watchCollection('[crop, center]', function(newValues, oldValues){ diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/overlays/umboverlay.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/overlays/umboverlay.directive.js index 9f9f1aa21e..99b89bf8cf 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/overlays/umboverlay.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/overlays/umboverlay.directive.js @@ -124,58 +124,6 @@ - -

Content Picker

-Opens a content picker.
-view: contentpicker - - - - - - - - - - - - - -
ParamTypeDetails
model.multiPickerBooleanPick one or multiple items
- - - - - - - - - - - - - -
ReturnsTypeDetails
model.selectionArrayArray of content objects
- - -

Icon Picker

-Opens an icon picker.
-view: iconpicker - - - - - - - - - - - - - -
ReturnsTypeDetails
model.iconStringThe icon class
-

Item Picker

Opens an item picker.
view: itempicker @@ -220,170 +168,6 @@ Opens an item picker.
-

Macro Picker

-Opens a media picker.
-view: macropicker - - - - - - - - - - - - - - - -
ParamTypeDetails
model.dialogDataObjectObject which contains array of allowedMacros. Set to null to allow all.
- - - - - - - - - - - - - - - - - - - - -
ReturnsTypeDetails
model.macroParamsArrayArray of macro params
model.selectedMacroObjectThe selected macro
- -

Media Picker

-Opens a media picker.
-view: mediapicker - - - - - - - - - - - - - - - - - - - - - - - - - -
ParamTypeDetails
model.multiPickerBooleanPick one or multiple items
model.onlyImagesBooleanOnly display files that have an image file-extension
model.disableFolderSelectBooleanDisable folder selection
- - - - - - - - - - - - - - - -
ReturnsTypeDetails
model.selectedImagesArrayArray of selected images
- -

Member Group Picker

-Opens a member group picker.
-view: membergrouppicker - - - - - - - - - - - - - - - -
ParamTypeDetails
model.multiPickerBooleanPick one or multiple items
- - - - - - - - - - - - - - - - - - - - -
ReturnsTypeDetails
model.selectedMemberGroupStringThe selected member group
model.selectedMemberGroups (multiPicker)ArrayThe selected member groups
- -

Member Picker

-Opens a member picker.
-view: memberpicker - - - - - - - - - - - - - - - -
ParamTypeDetails
model.multiPickerBooleanPick one or multiple items
- - - - - - - - - - - - - - -
ReturnsTypeDetails
model.selectionArrayArray of selected members/td> -
-

YSOD

Opens an overlay to show a custom YSOD.
view: ysod diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/tags/umbtagseditor.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/tags/umbtagseditor.directive.js new file mode 100644 index 0000000000..8bad5ae8fd --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/tags/umbtagseditor.directive.js @@ -0,0 +1,277 @@ +/** +@ngdoc directive +@name umbraco.directives.directive:umbTagsEditor +**/ + +(function () { + 'use strict'; + + angular + .module('umbraco.directives') + .component('umbTagsEditor', { + transclude: true, + templateUrl: 'views/components/tags/umb-tags-editor.html', + controller: umbTagsEditorController, + controllerAs: 'vm', + bindings: { + value: "<", + config: "<", + validation: "<", + culture: " 0) { + if (vm.config.storageType === "Json") { + //json storage + vm.viewModel = JSON.parse(vm.value); + updateModelValue(vm.viewModel); + } + else { + //csv storage + + // split the csv string, and remove any duplicate values + let tempArray = vm.value.split(',').map(function (v) { + return v.trim(); + }); + + vm.viewModel = tempArray.filter(function (v, i, self) { + return self.indexOf(v) === i; + }); + + updateModelValue(vm.viewModel); + } + } + else if (angular.isArray(vm.value)) { + vm.viewModel = vm.value; + } + } + } + + function updateModelValue(val) { + if (val) { + vm.onValueChanged({ value: val }); + } + else { + vm.onValueChanged({ value: [] }); + } + } + + /** + * Method required by the valPropertyValidator directive (returns true if the property editor has at least one tag selected) + */ + function validateMandatory() { + return { + isValid: !vm.validation.mandatory || (vm.viewModel != null && vm.viewModel.length > 0), + errorMsg: "Value cannot be empty", + errorKey: "required" + }; + } + + function addTagInternal(tagToAdd) { + if (tagToAdd != null && tagToAdd.length > 0) { + if (vm.viewModel.indexOf(tagToAdd) < 0) { + vm.viewModel.push(tagToAdd); + updateModelValue(vm.viewModel); + } + } + } + + function addTagOnEnter(e) { + var code = e.keyCode || e.which; + if (code == 13) { //Enter keycode + if ($element.find('.tags-' + vm.htmlId).parent().find(".tt-menu .tt-cursor").length === 0) { + //this is required, otherwise the html form will attempt to submit. + e.preventDefault(); + addTag(); + } + } + } + function addTag() { + //ensure that we're not pressing the enter key whilst selecting a typeahead value from the drop down + //we need to use jquery because typeahead duplicates the text box + addTagInternal(vm.tagToAdd); + vm.tagToAdd = ""; + //this clears the value stored in typeahead so it doesn't try to add the text again + // https://issues.umbraco.org/issue/U4-4947 + typeahead.typeahead('val', ''); + } + + function removeTag(tag) { + var i = vm.viewModel.indexOf(tag); + + if (i >= 0) { + // Make sure to hide the prompt so it does not stay open because another item gets a new number in the array index + vm.promptIsVisible = "-1"; + + // Remove the tag from the index + vm.viewModel.splice(i, 1); + + updateModelValue(vm.viewModel); + } + } + + function showPrompt(idx, tag) { + + var i = vm.viewModel.indexOf(tag); + + // Make the prompt visible for the clicked tag only + if (i === idx) { + vm.promptIsVisible = i; + } + } + + function hidePrompt() { + vm.promptIsVisible = "-1"; + } + + //helper method to format the data for bloodhound + function dataTransform(list) { + //transform the result to what bloodhound wants + var tagList = _.map(list, function (i) { + return { value: i.text }; + }); + // remove current tags from the list + return $.grep(tagList, function (tag) { + return ($.inArray(tag.value, vm.viewModel) === -1); + }); + } + + // helper method to remove current tags + function removeCurrentTagsFromSuggestions(suggestions) { + return $.grep(suggestions, function (suggestion) { + return ($.inArray(suggestion.value, vm.viewModel) === -1); + }); + } + + + } + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertyvalidator.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertyvalidator.directive.js index c1a2999017..37303d22ad 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertyvalidator.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertyvalidator.directive.js @@ -36,7 +36,7 @@ function valPropertyValidator(serverValidationManager) { // Validation method function validate (viewValue) { - // Calls the validition method + // Calls the validation method var result = scope.valPropertyValidator(); if (!result.errorKey || result.isValid === undefined || !result.errorMsg) { throw "The result object from valPropertyValidator does not contain required properties: isValid, errorKey, errorMsg"; @@ -55,6 +55,9 @@ function valPropertyValidator(serverValidationManager) { propCtrl.setPropertyError(result.errorMsg); } } + + // parsers are expected to return a value + return (result.isValid) ? viewValue : undefined; }; // Parsers are called as soon as the value in the form input is modified diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/relationtype.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/relationtype.resource.js index f17aae0a6b..7f13a46d2f 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/relationtype.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/relationtype.resource.js @@ -35,7 +35,7 @@ function relationTypeResource($q, $http, umbRequestHelper, umbDataFormatter) { /** * @ngdoc method * @name umbraco.resources.relationTypeResource#getRelationObjectTypes - * @methodof umbraco.resources.relationTypeResource + * @methodOf umbraco.resources.relationTypeResource * * @description * Gets a list of Umbraco object types which can be associated with a relation. @@ -54,7 +54,7 @@ function relationTypeResource($q, $http, umbRequestHelper, umbDataFormatter) { /** * @ngdoc method * @name umbraco.resources.relationTypeResource#save - * @methodof umbraco.resources.relationTypeResource + * @methodOf umbraco.resources.relationTypeResource * * @description * Updates a relation type. @@ -74,7 +74,7 @@ function relationTypeResource($q, $http, umbRequestHelper, umbDataFormatter) { /** * @ngdoc method * @name umbraco.resources.relationTypeResource#create - * @methodof umbraco.resources.relationTypeResource + * @methodOf umbraco.resources.relationTypeResource * * @description * Creates a new relation type. @@ -94,7 +94,7 @@ function relationTypeResource($q, $http, umbRequestHelper, umbDataFormatter) { /** * @ngdoc method * @name umbraco.resources.relationTypeResource#deleteById - * @methodof umbraco.resources.relationTypeResource + * @methodOf umbraco.resources.relationTypeResource * * @description * Deletes a relation type with a given ID. diff --git a/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js b/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js index 650a210784..0dbd27b7a5 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js @@ -4,6 +4,76 @@ * * @description * Added in Umbraco 8.0. Application-wide service for handling infinite editing. + * + * +

Markup example

+
+    
+ + + +
+
+ +

Controller example

+
+    (function () {
+
+        "use strict";
+
+        function Controller() {
+
+            var vm = this;
+
+            vm.open = open;
+
+            function open() {
+                var mediaPickerOptions = {
+                    multiPicker: true,
+                    submit: function(model) {
+                        editorService.close();
+                    },
+                    close: function() {
+                        editorService.close();
+                    }
+                }
+                editorService.mediaPicker(mediaPickerOptions);
+            };
+        }
+
+        angular.module("umbraco").controller("My.Controller", Controller);
+    })();
+
+ +

Custom view example

+
+    (function () {
+
+        "use strict";
+
+        function Controller() {
+
+            var vm = this;
+
+            vm.open = open;
+
+            function open() {
+                var options = {
+                    view: "path/to/view.html"
+                    submit: function(model) {
+                        editorService.close();
+                    },
+                    close: function() {
+                        editorService.close();
+                    }
+                }
+                editorService.open(options);
+            };
+        }
+
+        angular.module("umbraco").controller("My.Controller", Controller);
+    })();
+
*/ (function () { "use strict"; @@ -43,6 +113,10 @@ * * @description * Method to open a new editor in infinite editing + * + * @param {Object} editor rendering options + * @param {String} editor.view Path to view + * @param {String} editor.size Sets the size of the editor ("Small"). If nothing is set it will use full width. */ function open(editor) { @@ -108,8 +182,12 @@ * * @description * Opens a media editor in infinite editing, the submit callback returns the updated content item + * @param {Object} editor rendering options * @param {String} editor.id The id of the content item * @param {Boolean} editor.create Create new content item + * @param {Function} editor.submit Callback function when the publish and close button is clicked. Returns the editor model object + * @param {Function} editor.close Callback function when the close button is clicked. + * * @returns {Object} editor object */ function contentEditor(editor) { @@ -124,6 +202,12 @@ * * @description * Opens a content picker in infinite editing, the submit callback returns an array of selected items + * + * @param {Object} editor rendering options + * @param {Boolean} editor.multiPicker Pick one or multiple items + * @param {Function} editor.submit Callback function when the submit button is clicked. Returns the editor model object + * @param {Function} editor.close Callback function when the close button is clicked. + * * @returns {Object} editor object */ function contentPicker(editor) { @@ -218,11 +302,13 @@ * * @description * Opens an embed editor in infinite editing. + * @param {Object} editor rendering options + * @param {String} editor.icon The icon class + * @param {String} editor.color The color class * @param {Callback} editor.submit Saves, submits, and closes the editor * @param {Callback} editor.close Closes the editor * @returns {Object} editor object */ - function linkPicker(editor) { editor.view = "views/common/infiniteeditors/linkpicker/linkpicker.html"; editor.size = "small"; @@ -236,6 +322,7 @@ * * @description * Opens a media editor in infinite editing, the submit callback returns the updated media item + * @param {Object} editor rendering options * @param {String} editor.id The id of the media item * @param {Boolean} editor.create Create new media item * @param {Callback} editor.submit Saves, submits, and closes the editor @@ -254,6 +341,7 @@ * * @description * Opens a media picker in infinite editing, the submit callback returns an array of selected media items + * @param {Object} editor rendering options * @param {Boolean} editor.multiPicker Pick one or multiple items * @param {Boolean} editor.onlyImages Only display files that have an image file-extension * @param {Boolean} editor.disableFolderSelect Disable folder selection @@ -276,6 +364,7 @@ * * @description * Opens an icon picker in infinite editing, the submit callback returns the selected icon + * @param {Object} editor rendering options * @param {Callback} editor.submit Submits the editor * @param {Callback} editor.close Closes the editor * @returns {Object} editor object @@ -293,6 +382,7 @@ * * @description * Opens the document type editor in infinite editing, the submit callback returns the saved document type + * @param {Object} editor rendering options * @param {Callback} editor.submit Submits the editor * @param {Callback} editor.close Closes the editor * @returns {Object} editor object @@ -309,6 +399,7 @@ * * @description * Opens the media type editor in infinite editing, the submit callback returns the saved media type + * @param {Object} editor rendering options * @param {Callback} editor.submit Submits the editor * @param {Callback} editor.close Closes the editor * @returns {Object} editor object @@ -318,24 +409,75 @@ open(editor); } + /** + * @ngdoc method + * @name umbraco.services.editorService#queryBuilder + * @methodOf umbraco.services.editorService + * + * @description + * Opens the query builder in infinite editing, the submit callback returns the generted query + * @param {Object} editor rendering options + * @param {Callback} editor.submit Submits the editor + * @param {Callback} editor.close Closes the editor + * @returns {Object} editor object + */ function queryBuilder(editor) { editor.view = "views/common/infiniteeditors/querybuilder/querybuilder.html"; editor.size = "small"; open(editor); } + /** + * @ngdoc method + * @name umbraco.services.editorService#treePicker + * @methodOf umbraco.services.editorService + * + * @description + * Opens the query builder in infinite editing, the submit callback returns the generted query + * @param {Object} editor rendering options + * @param {String} options.section tree section to display + * @param {String} options.treeAlias specific tree to display + * @param {Boolean} options.multiPicker should the tree pick one or multiple items before returning + * @param {Callback} editor.submit Submits the editor + * @param {Callback} editor.close Closes the editor + * @returns {Object} editor object + */ function treePicker(editor) { editor.view = "views/common/infiniteeditors/treepicker/treepicker.html"; editor.size = "small"; open(editor); } + /** + * @ngdoc method + * @name umbraco.services.editorService#nodePermissions + * @methodOf umbraco.services.editorService + * + * @description + * Opens the an editor to set node permissions. + * @param {Object} editor rendering options + * @param {Callback} editor.submit Submits the editor + * @param {Callback} editor.close Closes the editor + * @returns {Object} editor object + */ function nodePermissions(editor) { editor.view = "views/common/infiniteeditors/nodepermissions/nodepermissions.html"; editor.size = "small"; open(editor); } + /** + * @ngdoc method + * @name umbraco.services.editorService#insertCodeSnippet + * @methodOf umbraco.services.editorService + * + * @description + * Open an editor to insert code snippets into the code editor + * @param {Object} editor rendering options + * @param {Callback} editor.submit Submits the editor + * @param {Callback} editor.close Closes the editor + * @returns {Object} editor object + */ function insertCodeSnippet(editor) { editor.view = "views/common/infiniteeditors/insertcodesnippet/insertcodesnippet.html"; editor.size = "small"; @@ -349,6 +491,7 @@ * * @description * Opens the user group picker in infinite editing, the submit callback returns an array of the selected user groups + * @param {Object} editor rendering options * @param {Callback} editor.submit Submits the editor * @param {Callback} editor.close Closes the editor * @returns {Object} editor object @@ -366,6 +509,7 @@ * * @description * Opens the user group picker in infinite editing, the submit callback returns the saved template + * @param {Object} editor rendering options * @param {String} editor.id The template id * @param {Callback} editor.submit Submits the editor * @param {Callback} editor.close Closes the editor @@ -382,7 +526,8 @@ * @methodOf umbraco.services.editorService * * @description - * Opens the section picker in infinite editing, the submit callback returns an array of the selected sections + * Opens the section picker in infinite editing, the submit callback returns an array of the selected sections¨ + * @param {Object} editor rendering options * @param {Callback} editor.submit Submits the editor * @param {Callback} editor.close Closes the editor * @returns {Object} editor object @@ -400,6 +545,7 @@ * * @description * Opens the insert field editor in infinite editing, the submit callback returns the code snippet + * @param {Object} editor rendering options * @param {Callback} editor.submit Submits the editor * @param {Callback} editor.close Closes the editor * @returns {Object} editor object @@ -417,6 +563,7 @@ * * @description * Opens the template sections editor in infinite editing, the submit callback returns the type to insert + * @param {Object} editor rendering options * @param {Callback} editor.submit Submits the editor * @param {Callback} editor.close Closes the editor * @returns {Object} editor object @@ -429,11 +576,12 @@ /** * @ngdoc method - * @name umbraco.services.editorService#sectionPicker + * @name umbraco.services.editorService#userPicker * @methodOf umbraco.services.editorService * * @description * Opens the section picker in infinite editing, the submit callback returns an array of the selected users + * @param {Object} editor rendering options * @param {Callback} editor.submit Submits the editor * @param {Callback} editor.close Closes the editor * @returns {Object} editor object @@ -452,6 +600,7 @@ * @description * Opens the section picker in infinite editing, the submit callback returns an array of the selected items * + * @param {Object} editor rendering options * @param {Array} editor.availableItems Array of available items. * @param {Array} editor.selectedItems Array of selected items. When passed in the selected items will be filtered from the available items. * @param {Boolean} editor.filter Set to false to hide the filter. @@ -485,12 +634,14 @@ /** * @ngdoc method - * @name umbraco.services.editorService#macroPicker + * @name umbraco.services.editorService#memberGroupPicker * @methodOf umbraco.services.editorService * * @description * Opens a member group picker in infinite editing. * + * @param {Object} editor rendering options + * @param {Object} editor.multiPicker Pick one or multiple items. * @param {Callback} editor.submit Submits the editor. * @param {Callback} editor.close Closes the editor. * @returns {Object} editor object diff --git a/src/Umbraco.Web.UI.Client/src/less/dashboards.less b/src/Umbraco.Web.UI.Client/src/less/dashboards.less index 5fd0e25be1..cc13ad31fd 100644 --- a/src/Umbraco.Web.UI.Client/src/less/dashboards.less +++ b/src/Umbraco.Web.UI.Client/src/less/dashboards.less @@ -1,13 +1,14 @@ .umb-dashboards-forms-install { background: url('../img/forms/installer-background.png'); background-repeat: repeat-x; - position: relative; - top: -30px; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; padding-top: 30px; - box-shadow: inset 0px -40px 30px 25px rgba(255,255,255,1); - -moz-border-radius: 0px 0px 200px 200px; - -webkit-border-radius: 0px 0px 200px 200px; - border-radius: 0px 0px 200px 200px; + background-color: @white; + overflow: auto; small { font-size: 14px; diff --git a/src/Umbraco.Web.UI.Client/src/less/typeahead.less b/src/Umbraco.Web.UI.Client/src/less/typeahead.less index da32a85f86..0426c2966a 100644 --- a/src/Umbraco.Web.UI.Client/src/less/typeahead.less +++ b/src/Umbraco.Web.UI.Client/src/less/typeahead.less @@ -33,7 +33,7 @@ color: @gray-8 !important; } -.tt-dropdown-menu { +.tt-menu { width: 422px; margin-top: 12px; padding: 8px 0; @@ -61,4 +61,4 @@ .tt-suggestion p { margin: 0; -} \ No newline at end of file +} diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/insertcodesnippet/insertcodesnippet.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/insertcodesnippet/insertcodesnippet.controller.js index 526d076048..2b40d496f5 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/insertcodesnippet/insertcodesnippet.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/insertcodesnippet/insertcodesnippet.controller.js @@ -77,7 +77,7 @@ var emptyStateMessage = values[2]; var dictionaryItemPicker = { - section: "settings", + section: "translation", treeAlias: "dictionary", entityType: "dictionary", multiPicker: false, diff --git a/src/Umbraco.Web.UI.Client/src/views/components/tags/umb-tags-editor.html b/src/Umbraco.Web.UI.Client/src/views/components/tags/umb-tags-editor.html new file mode 100644 index 0000000000..4f83e05c17 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/tags/umb-tags-editor.html @@ -0,0 +1,34 @@ +
+ +
+ Loading... +
+ +
+ + + + + + + + + + + + + + +
+
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/partialviewmacros/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/partialviewmacros/edit.controller.js index afc00bb7e2..372cecb36c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/partialviewmacros/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/partialviewmacros/edit.controller.js @@ -201,7 +201,7 @@ var emptyStateMessage = values[1]; var dictionaryPicker = { - section: "settings", + section: "translation", treeAlias: "dictionary", entityType: "dictionary", multiPicker: false, diff --git a/src/Umbraco.Web.UI.Client/src/views/partialviews/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/partialviews/edit.controller.js index ff14ea0ebd..292898814d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/partialviews/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/partialviews/edit.controller.js @@ -207,7 +207,7 @@ var emptyStateMessage = values[1]; var dictionaryItem = { - section: "settings", + section: "translation", treeAlias: "dictionary", entityType: "dictionary", multiPicker: false, diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/tags/tags.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/tags/tags.controller.js index bcf27294f3..688ac7693f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/tags/tags.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/tags/tags.controller.js @@ -1,216 +1,10 @@ angular.module("umbraco") .controller("Umbraco.PropertyEditors.TagsController", - function ($rootScope, $scope, $log, assetsService, umbRequestHelper, angularHelper, $timeout, $element) { + function ($scope) { - var $typeahead; - - $scope.isLoading = true; - $scope.tagToAdd = ""; - - function setModelValue(val) { - - $scope.model.value = val || $scope.model.value; - if ($scope.model.value) { - if (!$scope.model.config.storageType || $scope.model.config.storageType !== "Json") { - //it is csv - if (!$scope.model.value) { - $scope.model.value = []; - } - else { - if($scope.model.value.length > 0) { - // split the csv string, and remove any duplicate values - var tempArray = $scope.model.value.split(',').map(function(v) { - return v.trim(); - }); - - $scope.model.value = tempArray.filter(function(v, i, self) { - return self.indexOf(v) === i; - }); - } - } - } - } - else { - $scope.model.value = []; - } - } - - assetsService.loadJs("lib/typeahead.js/typeahead.bundle.min.js", $scope).then(function () { - - $scope.isLoading = false; - - //load current value - setModelValue(); - - // Method required by the valPropertyValidator directive (returns true if the property editor has at least one tag selected) - $scope.validateMandatory = function () { - return { - isValid: !$scope.model.validation.mandatory || ($scope.model.value != null && $scope.model.value.length > 0), - errorMsg: "Value cannot be empty", - errorKey: "required" - }; - } - - //Helper method to add a tag on enter or on typeahead select - function addTag(tagToAdd) { - if (tagToAdd != null && tagToAdd.length > 0) { - if ($scope.model.value.indexOf(tagToAdd) < 0) { - $scope.model.value.push(tagToAdd); - //this is required to re-validate - $scope.propertyForm.tagCount.$setViewValue($scope.model.value.length); - } - } - } - - $scope.addTagOnEnter = function (e) { - var code = e.keyCode || e.which; - if (code == 13) { //Enter keycode - if ($element.find('.tags-' + $scope.model.alias).parent().find(".tt-dropdown-menu .tt-cursor").length === 0) { - //this is required, otherwise the html form will attempt to submit. - e.preventDefault(); - $scope.addTag(); - } - } - }; - - $scope.addTag = function () { - //ensure that we're not pressing the enter key whilst selecting a typeahead value from the drop down - //we need to use jquery because typeahead duplicates the text box - addTag($scope.tagToAdd); - $scope.tagToAdd = ""; - //this clears the value stored in typeahead so it doesn't try to add the text again - // https://issues.umbraco.org/issue/U4-4947 - $typeahead.typeahead('val', ''); - }; - - // Set the visible prompt to -1 to ensure it will not be visible - $scope.promptIsVisible = "-1"; - - $scope.removeTag = function (tag) { - var i = $scope.model.value.indexOf(tag); - - if (i >= 0) { - // Make sure to hide the prompt so it does not stay open because another item gets a new number in the array index - $scope.promptIsVisible = "-1"; - - // Remove the tag from the index - $scope.model.value.splice(i, 1); - - //this is required to re-validate - $scope.propertyForm.tagCount.$setViewValue($scope.model.value.length); - } - }; - - $scope.showPrompt = function (idx, tag){ - - var i = $scope.model.value.indexOf(tag); - - // Make the prompt visible for the clicked tag only - if (i === idx) { - $scope.promptIsVisible = i; - } - } - - $scope.hidePrompt = function(){ - $scope.promptIsVisible = "-1"; - } - - //vice versa - $scope.model.onValueChanged = function (newVal, oldVal) { - //update the display val again if it has changed from the server - setModelValue(newVal); - }; - - //configure the tags data source - - //helper method to format the data for bloodhound - function dataTransform(list) { - //transform the result to what bloodhound wants - var tagList = _.map(list, function (i) { - return { value: i.text }; - }); - // remove current tags from the list - return $.grep(tagList, function (tag) { - return ($.inArray(tag.value, $scope.model.value) === -1); - }); - } - - // helper method to remove current tags - function removeCurrentTagsFromSuggestions(suggestions) { - return $.grep(suggestions, function (suggestion) { - return ($.inArray(suggestion.value, $scope.model.value) === -1); - }); - } - - var tagsHound = new Bloodhound({ - datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'), - queryTokenizer: Bloodhound.tokenizers.whitespace, - dupDetector : function(remoteMatch, localMatch) { - return (remoteMatch["value"] == localMatch["value"]); - }, - //pre-fetch the tags for this category - prefetch: { - url: umbRequestHelper.getApiUrl("tagsDataBaseUrl", "GetTags", [{ tagGroup: $scope.model.config.group }]), - //TTL = 5 minutes - ttl: 300000, - filter: dataTransform - }, - //dynamically get the tags for this category (they may have changed on the server) - remote: { - url: umbRequestHelper.getApiUrl("tagsDataBaseUrl", "GetTags", [{ tagGroup: $scope.model.config.group }]), - filter: dataTransform - } - }); - - tagsHound.initialize(true); - - //configure the type ahead - $timeout(function () { - - $typeahead = $element.find('.tags-' + $scope.model.alias).typeahead( - { - //This causes some strangeness as it duplicates the textbox, best leave off for now. - hint: false, - highlight: true, - cacheKey: new Date(), // Force a cache refresh each time the control is initialized - minLength: 1 - }, { - //see: https://github.com/twitter/typeahead.js/blob/master/doc/jquery_typeahead.md#options - // name = the data set name, we'll make this the tag group name - name: $scope.model.config.group, - displayKey: "value", - source: function (query, cb) { - tagsHound.get(query, function (suggestions) { - cb(removeCurrentTagsFromSuggestions(suggestions)); - }); - } - }).bind("typeahead:selected", function (obj, datum, name) { - angularHelper.safeApply($scope, function () { - addTag(datum["value"]); - $scope.tagToAdd = ""; - // clear the typed text - $typeahead.typeahead('val', ''); - }); - - }).bind("typeahead:autocompleted", function (obj, datum, name) { - angularHelper.safeApply($scope, function () { - addTag(datum["value"]); - $scope.tagToAdd = ""; - }); - - }).bind("typeahead:opened", function (obj) { - //console.log("opened "); - }); - }); - - $scope.$on('$destroy', function () { - tagsHound.clearPrefetchCache(); - tagsHound.clearRemoteCache(); - $element.find('.tags-' + $scope.model.alias).typeahead('destroy'); - tagsHound = null; - }); - - }); + $scope.valueChanged = function(value) { + $scope.model.value = value; + } } ); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/tags/tags.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/tags/tags.html index 6fcdfafdcd..69a4751226 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/tags/tags.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/tags/tags.html @@ -1,35 +1,10 @@
-
- Loading... -
- -
- - - - - - - - - - - - - - -
+ +
diff --git a/src/Umbraco.Web.UI.Client/src/views/templates/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/templates/edit.controller.js index 8ac9dc78e8..1f6fb8863a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/templates/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/templates/edit.controller.js @@ -438,7 +438,7 @@ var emptyStateMessage = values[1]; var dictionaryItem = { - section: "settings", + section: "translation", treeAlias: "dictionary", entityType: "dictionary", multiPicker: false, diff --git a/src/Umbraco.Web.UI/App_Plugins/UmbracoForms/package.manifest b/src/Umbraco.Web.UI/App_Plugins/UmbracoForms/package.manifest deleted file mode 100644 index c7ed4a957a..0000000000 --- a/src/Umbraco.Web.UI/App_Plugins/UmbracoForms/package.manifest +++ /dev/null @@ -1,10 +0,0 @@ -{ - "dashboards": [ - { - "name": "Install Umbraco Forms", - "alias": "installUmbracoForms", - "view": "views/dashboard/forms/formsdashboardintro.html", - "sections": [ "forms" ] - } - ] -} diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 130015ad53..5751e9155c 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -231,7 +231,6 @@ - 404handlers.config diff --git a/src/Umbraco.Web.UI/config/Dashboard.config b/src/Umbraco.Web.UI/config/Dashboard.config index 785fb61681..fec6ab34ae 100644 --- a/src/Umbraco.Web.UI/config/Dashboard.config +++ b/src/Umbraco.Web.UI/config/Dashboard.config @@ -22,6 +22,17 @@ +
+ + forms + + + + views/dashboard/forms/formsdashboardintro.html + + +
+
media diff --git a/src/Umbraco.Web.UI/config/trees.Release.config b/src/Umbraco.Web.UI/config/trees.Release.config index 7b5a9e5e2a..bd75e97c38 100644 --- a/src/Umbraco.Web.UI/config/trees.Release.config +++ b/src/Umbraco.Web.UI/config/trees.Release.config @@ -30,9 +30,5 @@ - - - - - + diff --git a/src/Umbraco.Web.UI/config/trees.config b/src/Umbraco.Web.UI/config/trees.config index 2b99f8751f..7f7aeca8a7 100644 --- a/src/Umbraco.Web.UI/config/trees.config +++ b/src/Umbraco.Web.UI/config/trees.config @@ -30,9 +30,5 @@ - - - - - + diff --git a/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs b/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs index eba7a627ad..c92a88d5b1 100644 --- a/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs +++ b/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs @@ -254,7 +254,7 @@ namespace Umbraco.Web.Editors }, { "tagsDataBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl( - controller => controller.GetTags("")) + controller => controller.GetTags("", "")) }, { "examineMgmtBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl( diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index 832103009a..198787371f 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -1768,7 +1768,8 @@ namespace Umbraco.Web.Editors contentSave, propertyCollection, (save, property) => Varies(property) ? property.GetValue(variant.Culture) : property.GetValue(), //get prop val - (save, property, v) => { if (Varies(property)) property.SetValue(v, variant.Culture); else property.SetValue(v); }); //set prop val + (save, property, v) => { if (Varies(property)) property.SetValue(v, variant.Culture); else property.SetValue(v); }, //set prop val + variant.Culture); variantIndex++; } diff --git a/src/Umbraco.Web/Editors/ContentControllerBase.cs b/src/Umbraco.Web/Editors/ContentControllerBase.cs index 0f174b6bbd..09d91a6436 100644 --- a/src/Umbraco.Web/Editors/ContentControllerBase.cs +++ b/src/Umbraco.Web/Editors/ContentControllerBase.cs @@ -39,17 +39,12 @@ namespace Umbraco.Web.Editors /// /// Maps the dto property values to the persisted model /// - /// - /// - /// - /// - /// - /// internal void MapPropertyValuesForPersistence( TSaved contentItem, ContentPropertyCollectionDto dto, Func getPropertyValue, - Action savePropertyValue) + Action savePropertyValue, + string culture) where TPersisted : IContentBase where TSaved : IContentSave { @@ -70,7 +65,7 @@ namespace Umbraco.Web.Editors // get the property var property = contentItem.PersistedContent.Properties[propertyDto.Alias]; - + // prepare files, if any matching property and culture var files = contentItem.UploadedFiles .Where(x => x.PropertyAlias == propertyDto.Alias && x.Culture == propertyDto.Culture) @@ -96,8 +91,8 @@ namespace Umbraco.Web.Editors { var tagConfiguration = ConfigurationEditor.ConfigurationAs(propertyDto.DataType.Configuration); if (tagConfiguration.Delimiter == default) tagConfiguration.Delimiter = tagAttribute.Delimiter; - //fixme how is this supposed to work with variants? - property.SetTagsValue(value, tagConfiguration); + var tagCulture = property.PropertyType.VariesByCulture() ? culture : null; + property.SetTagsValue(value, tagConfiguration, tagCulture); } else savePropertyValue(contentItem, property, value); diff --git a/src/Umbraco.Web/Editors/MediaController.cs b/src/Umbraco.Web/Editors/MediaController.cs index a15f6e9df7..2cbe32fb89 100644 --- a/src/Umbraco.Web/Editors/MediaController.cs +++ b/src/Umbraco.Web/Editors/MediaController.cs @@ -484,8 +484,9 @@ namespace Umbraco.Web.Editors MapPropertyValuesForPersistence( contentItem, contentItem.PropertyCollectionDto, - (save, property) => property.GetValue(), //get prop val - (save, property, v) => property.SetValue(v)); //set prop val + (save, property) => property.GetValue(), //get prop val + (save, property, v) => property.SetValue(v), //set prop val + null); // media are all invariant //We need to manually check the validation results here because: // * We still need to save the entity even if there are validation value errors diff --git a/src/Umbraco.Web/Editors/MemberController.cs b/src/Umbraco.Web/Editors/MemberController.cs index ae02645afa..36027ee2a5 100644 --- a/src/Umbraco.Web/Editors/MemberController.cs +++ b/src/Umbraco.Web/Editors/MemberController.cs @@ -400,8 +400,9 @@ namespace Umbraco.Web.Editors base.MapPropertyValuesForPersistence( contentItem, contentItem.PropertyCollectionDto, - (save, property) => property.GetValue(), //get prop val - (save, property, v) => property.SetValue(v)); //set prop val + (save, property) => property.GetValue(), //get prop val + (save, property, v) => property.SetValue(v), //set prop val + null); // member are all invariant } /// diff --git a/src/Umbraco.Web/ITagQuery.cs b/src/Umbraco.Web/ITagQuery.cs index 3d0757a695..1b96ea330c 100644 --- a/src/Umbraco.Web/ITagQuery.cs +++ b/src/Umbraco.Web/ITagQuery.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; using Umbraco.Web.Models; @@ -8,76 +7,53 @@ namespace Umbraco.Web public interface ITagQuery { /// - /// Returns all content that is tagged with the specified tag value and optional tag group + /// Gets all documents tagged with the specified tag. /// - /// - /// - /// - IEnumerable GetContentByTag(string tag, string tagGroup = null); + IEnumerable GetContentByTag(string tag, string group = null, string culture = null); /// - /// Returns all content that has been tagged with any tag in the specified group + /// Gets all documents tagged with any tag in the specified group. /// - /// - /// - IEnumerable GetContentByTagGroup(string tagGroup); + IEnumerable GetContentByTagGroup(string group, string culture = null); /// - /// Returns all Media that is tagged with the specified tag value and optional tag group + /// Gets all media tagged with the specified tag. /// - /// - /// - /// - IEnumerable GetMediaByTag(string tag, string tagGroup = null); + IEnumerable GetMediaByTag(string tag, string group = null, string culture = null); /// - /// Returns all Media that has been tagged with any tag in the specified group + /// Gets all media tagged with any tag in the specified group. /// - /// - /// - IEnumerable GetMediaByTagGroup(string tagGroup); + IEnumerable GetMediaByTagGroup(string group, string culture = null); /// - /// Get every tag stored in the database (with optional group) + /// Gets all tags. /// - IEnumerable GetAllTags(string group = null); + IEnumerable GetAllTags(string group = null, string culture = null); /// - /// Get all tags for content items (with optional group) + /// Gets all document tags. /// - /// - /// - IEnumerable GetAllContentTags(string group = null); + IEnumerable GetAllContentTags(string group = null, string culture = null); /// - /// Get all tags for media items (with optional group) + /// Gets all media tags. /// - /// - /// - IEnumerable GetAllMediaTags(string group = null); + IEnumerable GetAllMediaTags(string group = null, string culture = null); /// - /// Get all tags for member items (with optional group) + /// Gets all member tags. /// - /// - /// - IEnumerable GetAllMemberTags(string group = null); + IEnumerable GetAllMemberTags(string group = null, string culture = null); /// - /// Returns all tags attached to a property by entity id + /// Gets all tags attached to an entity via a property. /// - /// - /// - /// - /// - IEnumerable GetTagsForProperty(int contentId, string propertyTypeAlias, string tagGroup = null); + IEnumerable GetTagsForProperty(int contentId, string propertyTypeAlias, string group = null, string culture = null); /// - /// Returns all tags attached to an entity (content, media or member) by entity id + /// Gets all tags attached to an entity. /// - /// - /// - /// - IEnumerable GetTagsForEntity(int contentId, string tagGroup = null); + IEnumerable GetTagsForEntity(int contentId, string group = null, string culture = null); } } diff --git a/src/Umbraco.Web/Install/Controllers/InstallApiController.cs b/src/Umbraco.Web/Install/Controllers/InstallApiController.cs index 0762dfc12f..a63484fb39 100644 --- a/src/Umbraco.Web/Install/Controllers/InstallApiController.cs +++ b/src/Umbraco.Web/Install/Controllers/InstallApiController.cs @@ -40,7 +40,7 @@ namespace Umbraco.Web.Install.Controllers public bool PostValidateDatabaseConnection(DatabaseModel model) { - var canConnect = _databaseBuilder.CheckConnection(model.DatabaseType.ToString(), model.ConnectionString, model.Server, model.DatabaseName, model.Login, model.Password, model.IntegratedAuth); + var canConnect = _databaseBuilder.CanConnect(model.DatabaseType.ToString(), model.ConnectionString, model.Server, model.DatabaseName, model.Login, model.Password, model.IntegratedAuth); return canConnect; } diff --git a/src/Umbraco.Web/Install/InstallSteps/DatabaseConfigureStep.cs b/src/Umbraco.Web/Install/InstallSteps/DatabaseConfigureStep.cs index a54b64733f..2fe6c0ceda 100644 --- a/src/Umbraco.Web/Install/InstallSteps/DatabaseConfigureStep.cs +++ b/src/Umbraco.Web/Install/InstallSteps/DatabaseConfigureStep.cs @@ -1,7 +1,6 @@ using System; using System.Configuration; using Umbraco.Core; -using Umbraco.Core.Configuration; using Umbraco.Core.Logging; using Umbraco.Core.Migrations.Install; using Umbraco.Web.Install.Models; @@ -29,7 +28,7 @@ namespace Umbraco.Web.Install.InstallSteps database = new DatabaseModel(); } - if (_databaseBuilder.CheckConnection(database.DatabaseType.ToString(), database.ConnectionString, database.Server, database.DatabaseName, database.Login, database.Password, database.IntegratedAuth) == false) + if (_databaseBuilder.CanConnect(database.DatabaseType.ToString(), database.ConnectionString, database.Server, database.DatabaseName, database.Login, database.Password, database.IntegratedAuth) == false) { throw new InstallException("Could not connect to the database"); } @@ -79,8 +78,7 @@ namespace Umbraco.Web.Install.InstallSteps try { //Since a connection string was present we verify the db can connect and query - var result = _databaseBuilder.ValidateDatabaseSchema(); - result.DetermineInstalledVersion(); + _ = _databaseBuilder.ValidateSchema(); return false; } catch (Exception ex) diff --git a/src/Umbraco.Web/Install/InstallSteps/DatabaseInstallStep.cs b/src/Umbraco.Web/Install/InstallSteps/DatabaseInstallStep.cs index c4cad38072..a9daee6e95 100644 --- a/src/Umbraco.Web/Install/InstallSteps/DatabaseInstallStep.cs +++ b/src/Umbraco.Web/Install/InstallSteps/DatabaseInstallStep.cs @@ -29,7 +29,7 @@ namespace Umbraco.Web.Install.InstallSteps if (_runtime.Level == RuntimeLevel.Run) throw new Exception("Umbraco is already configured!"); - var result = _databaseBuilder.CreateDatabaseSchemaAndData(); + var result = _databaseBuilder.CreateSchemaAndData(); if (result.Success == false) { diff --git a/src/Umbraco.Web/Install/InstallSteps/DatabaseUpgradeStep.cs b/src/Umbraco.Web/Install/InstallSteps/DatabaseUpgradeStep.cs index c078caf906..8283eb6bef 100644 --- a/src/Umbraco.Web/Install/InstallSteps/DatabaseUpgradeStep.cs +++ b/src/Umbraco.Web/Install/InstallSteps/DatabaseUpgradeStep.cs @@ -63,18 +63,10 @@ namespace Umbraco.Web.Install.InstallSteps if (_databaseBuilder.IsConnectionStringConfigured(databaseSettings)) { - //Since a connection string was present we verify whether this is an upgrade or an empty db - var result = _databaseBuilder.ValidateDatabaseSchema(); - - var determinedVersion = result.DetermineInstalledVersion(); - if (determinedVersion.Equals(new Version(0, 0, 0))) - { - //Fresh install - return false; - } - - //Upgrade - return true; + // a connection string was present, determine whether this is an install/upgrade + // return true (upgrade) if there is an installed version, else false (install) + var result = _databaseBuilder.ValidateSchema(); + return result.DetermineHasInstalledVersion(); } //no connection string configured, probably a fresh install diff --git a/src/Umbraco.Web/Migrations/ClearMediaXmlCacheForDeletedItemsAfterUpgrade.cs b/src/Umbraco.Web/Migrations/ClearMediaXmlCacheForDeletedItemsAfterUpgrade.cs deleted file mode 100644 index 991e77b4d0..0000000000 --- a/src/Umbraco.Web/Migrations/ClearMediaXmlCacheForDeletedItemsAfterUpgrade.cs +++ /dev/null @@ -1,50 +0,0 @@ -using Semver; -using Umbraco.Core; -using Umbraco.Core.Logging; -using Umbraco.Core.Migrations; -using Umbraco.Core.Scoping; - -namespace Umbraco.Web.Migrations -{ - /// - /// This will execute after upgrading to remove any xml cache for media that are currently in the bin - /// - /// - /// This will execute for specific versions - - /// - /// * If current is less than or equal to 7.0.0 - /// - public class ClearMediaXmlCacheForDeletedItemsAfterUpgrade : IPostMigration - { - private readonly ILogger _logger; - - public ClearMediaXmlCacheForDeletedItemsAfterUpgrade(ILogger logger) - { - _logger = logger; - } - - public void Execute(string name, IScope scope, SemVersion originVersion, SemVersion targetVersion, ILogger logger) - { - if (name != Constants.System.UmbracoUpgradePlanName) return; - - var target70 = new SemVersion(7 /*, 0, 0*/); - - if (originVersion <= target70) - { - //This query is structured to work with MySql, SQLCE and SqlServer: - // http://issues.umbraco.org/issue/U4-3876 - - var syntax = scope.SqlContext.SqlSyntax; - - var sql = @"DELETE FROM cmsContentXml WHERE nodeId IN - (SELECT nodeId FROM (SELECT DISTINCT cmsContentXml.nodeId FROM cmsContentXml - INNER JOIN umbracoNode ON cmsContentXml.nodeId = umbracoNode.id - WHERE nodeObjectType = '" + Constants.ObjectTypes.Media + "' AND " + syntax.GetQuotedColumnName("path") + " LIKE '%-21%') x)"; - - var count = scope.Database.Execute(sql); - - _logger.Info("Cleared {Total} items from the media xml cache that were trashed and not meant to be there", count); - } - } - } -} diff --git a/src/Umbraco.Web/Migrations/EnsureListViewDataTypeIsCreated.cs b/src/Umbraco.Web/Migrations/EnsureListViewDataTypeIsCreated.cs deleted file mode 100644 index 2dbe6bd867..0000000000 --- a/src/Umbraco.Web/Migrations/EnsureListViewDataTypeIsCreated.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System; -using NPoco; -using Semver; -using Umbraco.Core; -using Umbraco.Core.Logging; -using Umbraco.Core.Migrations; -using Umbraco.Core.Persistence.Dtos; -using Umbraco.Core.Scoping; - -namespace Umbraco.Web.Migrations -{ - /// - /// Creates the built in list view data types - /// - public class EnsureDefaultListViewDataTypesCreated : IPostMigration - { - public void Execute(string name, IScope scope, SemVersion originVersion, SemVersion targetVersion, ILogger logger) - { - if (name != Constants.System.UmbracoUpgradePlanName) return; - - var target720 = new SemVersion(7, 2, 0); - - if (originVersion > target720) - return; - - var syntax = scope.SqlContext.SqlSyntax; - - try - { - //Turn on identity insert if db provider is not mysql - if (syntax.SupportsIdentityInsert()) - scope.Database.Execute(new Sql(string.Format("SET IDENTITY_INSERT {0} ON ", syntax.GetQuotedTableName("umbracoNode")))); - - if (scope.Database.Exists(Constants.DataTypes.DefaultContentListView)) - { - //If this already exists then just exit - return; - } - - scope.Database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = Constants.DataTypes.DefaultContentListView, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,-95", SortOrder = 2, UniqueId = new Guid("C0808DD3-8133-4E4B-8CE8-E2BEA84A96A4"), Text = Constants.Conventions.DataTypes.ListViewPrefix + "Content", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); - scope.Database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = Constants.DataTypes.DefaultMediaListView, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,-96", SortOrder = 2, UniqueId = new Guid("3A0156C4-3B8C-4803-BDC1-6871FAA83FFF"), Text = Constants.Conventions.DataTypes.ListViewPrefix + "Media", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); - scope.Database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = Constants.DataTypes.DefaultMembersListView, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,-97", SortOrder = 2, UniqueId = new Guid("AA2C52A0-CE87-4E65-A47C-7DF09358585D"), Text = Constants.Conventions.DataTypes.ListViewPrefix + "Members", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); - } - finally - { - //Turn off identity insert if db provider is not mysql - if (syntax.SupportsIdentityInsert()) - scope.Database.Execute(new Sql(string.Format("SET IDENTITY_INSERT {0} OFF;", syntax.GetQuotedTableName("umbracoNode")))); - } - - try - { - //Turn on identity insert if db provider is not mysql - if (syntax.SupportsIdentityInsert()) - scope.Database.Execute(new Sql(string.Format("SET IDENTITY_INSERT {0} ON ", syntax.GetQuotedTableName(Constants.DatabaseSchema.Tables.DataType)))); - - const string memberListViewConfiguration = "{\"pageSize\":10,\"orderBy\":Name,\"orderDirection\":asc,includeProperties:[{\"alias\":\"email\",\"isSystem\":1},{\"alias\":\"username\",\"isSystem\":1},{\"alias\":\"updateDate\",\"header\":\"Last edited\",\"isSystem\":1}]}"; - - scope.Database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = Constants.DataTypes.DefaultContentListView, EditorAlias = Constants.PropertyEditors.Aliases.ListView, DbType = "Nvarchar" }); - scope.Database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = Constants.DataTypes.DefaultMediaListView, EditorAlias = Constants.PropertyEditors.Aliases.ListView, DbType = "Nvarchar" }); - scope.Database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = Constants.DataTypes.DefaultMembersListView, EditorAlias = Constants.PropertyEditors.Aliases.ListView, DbType = "Nvarchar", Configuration = memberListViewConfiguration }); - } - finally - { - //Turn off identity insert if db provider is not mysql - if (syntax.SupportsIdentityInsert()) - scope.Database.Execute(new Sql(string.Format("SET IDENTITY_INSERT {0} OFF;", syntax.GetQuotedTableName(Constants.DatabaseSchema.Tables.DataType)))); - } - } - } -} diff --git a/src/Umbraco.Web/Migrations/OverwriteStylesheetFilesFromTempFiles.cs b/src/Umbraco.Web/Migrations/OverwriteStylesheetFilesFromTempFiles.cs deleted file mode 100644 index 7a752fa562..0000000000 --- a/src/Umbraco.Web/Migrations/OverwriteStylesheetFilesFromTempFiles.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System.IO; -using Semver; -using Umbraco.Core; -using Umbraco.Core.IO; -using Umbraco.Core.Logging; -using Umbraco.Core.Migrations; -using Umbraco.Core.Migrations.Upgrade; -using Umbraco.Core.Scoping; - -namespace Umbraco.Web.Migrations -{ - - /// - /// When upgrading version 7.3 the migration MigrateStylesheetDataToFile will execute but we don't want to overwrite the developers - /// files during the migration since other parts of the migration might fail. So once the migration is complete, we'll then copy over the temp - /// files that this migration created over top of the developer's files. We'll also create a backup of their files. - /// - public sealed class OverwriteStylesheetFilesFromTempFiles : IPostMigration - { - public void Execute(string name, IScope scope, SemVersion originVersion, SemVersion targetVersion, ILogger logger) - { - if (name != Constants.System.UmbracoUpgradePlanName) return; - - var target73 = new SemVersion(7, 3, 0); - - if (originVersion <= target73) - { - var tempCssFolder = IOHelper.MapPath("~/App_Data/TEMP/CssMigration/"); - var cssFolder = IOHelper.MapPath("~/css"); - if (Directory.Exists(tempCssFolder)) - { - var files = Directory.GetFiles(tempCssFolder, "*.css", SearchOption.AllDirectories); - foreach (var file in files) - { - var relativePath = file.TrimStart(tempCssFolder).TrimStart("\\"); - var cssFilePath = Path.Combine(cssFolder, relativePath); - if (File.Exists(cssFilePath)) - { - //backup - var targetPath = Path.Combine(tempCssFolder, relativePath.EnsureEndsWith(".bak")); - logger.Info("CSS file is being backed up from {CssFilePath}, to {TargetPath} before being migrated to new format", cssFilePath, targetPath); - File.Copy(cssFilePath, targetPath, true); - } - - //ensure the sub folder eixts - Directory.CreateDirectory(Path.GetDirectoryName(cssFilePath)); - File.Copy(file, cssFilePath, true); - } - } - } - } - } -} diff --git a/src/Umbraco.Web/Migrations/RebuildXmlCachesAfterUpgrade.cs b/src/Umbraco.Web/Migrations/RebuildXmlCachesAfterUpgrade.cs deleted file mode 100644 index 2181eea367..0000000000 --- a/src/Umbraco.Web/Migrations/RebuildXmlCachesAfterUpgrade.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System; -using Semver; -using Umbraco.Core; -using Umbraco.Core.Logging; -using Umbraco.Core.Migrations; -using Umbraco.Core.Scoping; -using Umbraco.Web.Composing; -using Umbraco.Web.PublishedCache.XmlPublishedCache; - -namespace Umbraco.Web.Migrations -{ - /// - /// Rebuilds the Xml caches after upgrading. - /// This will execute after upgrading to rebuild the xml cache - /// - /// - /// This cannot execute as part of a DB migration since it needs access to services and repositories. - /// Executes for: - /// - Media Xml : if current is less than, or equal to, 7.0.0 (superceeded by the next rule) - /// - Media & Content Xml : if current is less than, or equal to, 7.3.0 - because 7.3.0 adds .Key to cached items - /// - /// - public class RebuildXmlCachesAfterUpgrade : IPostMigration - { - public void Execute(string name, IScope scope, SemVersion originVersion, SemVersion targetVersion, ILogger logger) - { - if (name != Constants.System.UmbracoUpgradePlanName) return; - - var v730 = new SemVersion(new Version(7, 3, 0)); - - var doMedia = originVersion < v730; - var doContent = originVersion < v730; - - if (doMedia) - { - // fixme - maintain - for backward compatibility?! or replace with...?! - //var mediaService = (MediaService) ApplicationContext.Current.Services.MediaService; - //mediaService.RebuildXmlStructures(); - - var svc = Current.PublishedSnapshotService as PublishedSnapshotService; - svc?.RebuildMediaXml(); - - // note: not re-indexing medias? - } - - if (doContent) - { - // fixme - maintain - for backward compatibility?! or replace with...?! - //var contentService = (ContentService) ApplicationContext.Current.Services.ContentService; - //contentService.RebuildXmlStructures(); - - var svc = Current.PublishedSnapshotService as PublishedSnapshotService; - svc?.RebuildContentAndPreviewXml(); - } - } - } -} diff --git a/src/Umbraco.Web/Models/PublishedContentBase.cs b/src/Umbraco.Web/Models/PublishedContentBase.cs index 1b8128b4c0..667cf145bd 100644 --- a/src/Umbraco.Web/Models/PublishedContentBase.cs +++ b/src/Umbraco.Web/Models/PublishedContentBase.cs @@ -142,7 +142,7 @@ namespace Umbraco.Web.Models public abstract PublishedItemType ItemType { get; } /// - public abstract bool IsDraft { get; } + public abstract bool IsDraft(string culture = null); #endregion diff --git a/src/Umbraco.Web/PropertyEditors/TagsDataController.cs b/src/Umbraco.Web/PropertyEditors/TagsDataController.cs index dc0a0787dd..bc0c281f98 100644 --- a/src/Umbraco.Web/PropertyEditors/TagsDataController.cs +++ b/src/Umbraco.Web/PropertyEditors/TagsDataController.cs @@ -15,9 +15,10 @@ namespace Umbraco.Web.PropertyEditors [PluginController("UmbracoApi")] public class TagsDataController : UmbracoAuthorizedApiController { - public IEnumerable GetTags(string tagGroup) + public IEnumerable GetTags(string tagGroup, string culture) { - return Umbraco.TagQuery.GetAllTags(tagGroup); + if (culture == string.Empty) culture = null; + return Umbraco.TagQuery.GetAllTags(tagGroup, culture); } } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/ContentCache.cs b/src/Umbraco.Web/PublishedCache/NuCache/ContentCache.cs index 217efceaf1..817c363fa5 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/ContentCache.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/ContentCache.cs @@ -152,7 +152,7 @@ namespace Umbraco.Web.PublishedCache.NuCache var urlSegment = n.GetUrlSegment(culture); var hasDomains = _domainHelper.NodeHasDomains(n.Id); while (hasDomains == false && n != null) // n is null at root - { + { // no segment indicates this is not published when this is a variant if (urlSegment.IsNullOrWhiteSpace()) return null; @@ -178,7 +178,7 @@ namespace Umbraco.Web.PublishedCache.NuCache var path = "/" + string.Join("/", pathParts); // will be "/" or "/foo" or "/foo/bar" etc //prefix the root node id containing the domain if it exists (this is a standard way of creating route paths) //and is done so that we know the ID of the domain node for the path - var route = (n?.Id.ToString(CultureInfo.InvariantCulture) ?? "") + path; + var route = (n?.Id.ToString(CultureInfo.InvariantCulture) ?? "") + path; return route; } @@ -228,24 +228,14 @@ namespace Umbraco.Web.PublishedCache.NuCache public override IPublishedContent GetById(bool preview, int contentId) { - var n = _snapshot.Get(contentId); - if (n == null) return null; - - // both .Draft and .Published cannot be null at the same time - return preview - ? n.Draft ?? GetPublishedContentAsPreviewing(n.Published) - : n.Published; + var node = _snapshot.Get(contentId); + return GetNodePublishedContent(node, preview); } public override IPublishedContent GetById(bool preview, Guid contentId) { - var n = _snapshot.Get(contentId); - if (n == null) return null; - - // both .Draft and .Published cannot be null at the same time - return preview - ? n.Draft ?? GetPublishedContentAsPreviewing(n.Published) - : n.Published; + var node = _snapshot.Get(contentId); + return GetNodePublishedContent(node, preview); } public override bool HasById(bool preview, int contentId) @@ -279,14 +269,24 @@ namespace Umbraco.Web.PublishedCache.NuCache var c = _snapshot.GetAtRoot(); // both .Draft and .Published cannot be null at the same time - return c.Select(n => preview - ? n.Draft ?? GetPublishedContentAsPreviewing(n.Published) - : n.Published).WhereNotNull().OrderBy(x => x.SortOrder); + return c.Select(n => GetNodePublishedContent(n, preview)).WhereNotNull().OrderBy(x => x.SortOrder); + } + + private static IPublishedContent GetNodePublishedContent(ContentNode node, bool preview) + { + if (node == null) + return null; + + // both .Draft and .Published cannot be null at the same time + + return preview + ? node.Draft ?? GetPublishedContentAsDraft(node.Published) + : node.Published; } // gets a published content as a previewing draft, if preview is true // this is for published content when previewing - internal static IPublishedContent GetPublishedContentAsPreviewing(IPublishedContent content /*, bool preview*/) + private static IPublishedContent GetPublishedContentAsDraft(IPublishedContent content /*, bool preview*/) { if (content == null /*|| preview == false*/) return null; //content; @@ -295,7 +295,7 @@ namespace Umbraco.Web.PublishedCache.NuCache // case we need to unwrap to get to the original IPublishedContentOrMedia. var inner = PublishedContent.UnwrapIPublishedContent(content); - return inner.AsPreviewingModel(); + return inner.AsDraft(); } public override bool HasContent(bool preview) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/ContentNestedData.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/ContentNestedData.cs index be3e813275..0f120024cc 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/ContentNestedData.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/ContentNestedData.cs @@ -1,5 +1,6 @@ using Newtonsoft.Json; using System.Collections.Generic; +using Umbraco.Core.Serialization; namespace Umbraco.Web.PublishedCache.NuCache.DataSource { @@ -9,9 +10,11 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource internal class ContentNestedData { [JsonProperty("properties")] + [JsonConverter(typeof(CaseInsensitiveDictionaryConverter))] public Dictionary PropertyData { get; set; } [JsonProperty("cultureData")] + [JsonConverter(typeof(CaseInsensitiveDictionaryConverter))] public Dictionary CultureData { get; set; } } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/CultureVariation.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/CultureVariation.cs index 50a2adaeb8..c6e603f5a9 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/CultureVariation.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/CultureVariation.cs @@ -13,5 +13,8 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource [JsonProperty("date")] public DateTime Date { get; set; } + + [JsonProperty("isDraft")] + public bool IsDraft { get; set; } } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs index a143997fab..4531d37b2b 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs @@ -182,27 +182,30 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource ContentData d = null; ContentData p = null; - if (dto.EditData == null) + if (dto.Edited) { - if (Debugger.IsAttached) - throw new Exception("Missing cmsContentNu edited content for node " + dto.Id + ", consider rebuilding."); - Current.Logger.Warn("Missing cmsContentNu edited content for node {NodeId}, consider rebuilding.", dto.Id); - } - else - { - var nested = DeserializeNestedData(dto.EditData); - - d = new ContentData + if (dto.EditData == null) { - Name = dto.EditName, - Published = false, - TemplateId = dto.EditTemplateId, - VersionId = dto.VersionId, - VersionDate = dto.EditVersionDate, - WriterId = dto.EditWriterId, - Properties = nested.PropertyData, - CultureInfos = nested.CultureData - }; + if (Debugger.IsAttached) + throw new Exception("Missing cmsContentNu edited content for node " + dto.Id + ", consider rebuilding."); + Current.Logger.Warn("Missing cmsContentNu edited content for node {NodeId}, consider rebuilding.", dto.Id); + } + else + { + var nested = DeserializeNestedData(dto.EditData); + + d = new ContentData + { + Name = dto.EditName, + Published = false, + TemplateId = dto.EditTemplateId, + VersionId = dto.VersionId, + VersionDate = dto.EditVersionDate, + WriterId = dto.EditWriterId, + Properties = nested.PropertyData, + CultureInfos = nested.CultureData + }; + } } if (dto.Published) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/Navigable/NavigableContent.cs b/src/Umbraco.Web/PublishedCache/NuCache/Navigable/NavigableContent.cs index ae34d0cb32..51badc8b9a 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/Navigable/NavigableContent.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/Navigable/NavigableContent.cs @@ -29,7 +29,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.Navigable XmlString(i++, _content.WriterId), XmlString(i++, _content.CreatorId), XmlString(i++, _content.UrlSegment), - XmlString(i, _content.IsDraft) + XmlString(i, _content.IsDraft()) }; } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedContent.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedContent.cs index a4610e82db..36e5698e32 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedContent.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedContent.cs @@ -273,8 +273,27 @@ namespace Umbraco.Web.PublishedCache.NuCache /// public override PublishedItemType ItemType => _contentNode.ContentType.ItemType; + // fixme + // was => _contentData.Published == false; /// - public override bool IsDraft => _contentData.Published == false; + public override bool IsDraft(string culture = null) + { + // if this is the 'published' published content, nothing can be draft + if (_contentData.Published) + return false; + + // not the 'published' published content, and does not vary = must be draft + if (!ContentType.VariesByCulture()) + return true; + + // handle context culture + if (culture == null) + culture = VariationContextAccessor?.VariationContext?.Culture ?? ""; + + // not the 'published' published content, and varies + // = depends on the culture + return _contentData.CultureInfos.TryGetValue(culture, out var cvar) && cvar.IsDraft; + } #endregion @@ -410,7 +429,7 @@ namespace Umbraco.Web.PublishedCache.NuCache private string AsPreviewingCacheKey => _asPreviewingCacheKey ?? (_asPreviewingCacheKey = CacheKeys.PublishedContentAsPreviewing(Key)); // used by ContentCache - internal IPublishedContent AsPreviewingModel() + internal IPublishedContent AsDraft() { if (IsPreviewing) return this; diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs index e97fa16266..3aa33f0e2f 100755 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs @@ -1206,7 +1206,8 @@ namespace Umbraco.Web.PublishedCache.NuCache foreach (var (culture, info) in infos) { - cultureData[culture] = new CultureVariation { Name = info.Name, Date = content.GetUpdateDate(culture) ?? DateTime.MinValue }; + var cultureIsDraft = !published && content is IContent d && d.IsCultureEdited(culture); + cultureData[culture] = new CultureVariation { Name = info.Name, Date = content.GetUpdateDate(culture) ?? DateTime.MinValue, IsDraft = cultureIsDraft }; } } diff --git a/src/Umbraco.Web/PublishedCache/PublishedMember.cs b/src/Umbraco.Web/PublishedCache/PublishedMember.cs index 44ce2328b7..56c8f440d8 100644 --- a/src/Umbraco.Web/PublishedCache/PublishedMember.cs +++ b/src/Umbraco.Web/PublishedCache/PublishedMember.cs @@ -79,7 +79,7 @@ namespace Umbraco.Web.PublishedCache public override PublishedItemType ItemType => PublishedItemType.Member; - public override bool IsDraft => false; + public override bool IsDraft(string culture = null) => false; public override IPublishedContent Parent => null; diff --git a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/DictionaryPublishedContent.cs b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/DictionaryPublishedContent.cs index 7a8ce65ae3..4453fe7321 100644 --- a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/DictionaryPublishedContent.cs +++ b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/DictionaryPublishedContent.cs @@ -176,7 +176,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache public override int Level => _level; - public override bool IsDraft => false; + public override bool IsDraft(string culture = null) => false; public override IEnumerable Properties => _properties; diff --git a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlPublishedContent.cs b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlPublishedContent.cs index 3c143a6066..af867cc089 100644 --- a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlPublishedContent.cs +++ b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlPublishedContent.cs @@ -221,13 +221,10 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache } } - public override bool IsDraft + public override bool IsDraft(string culture = null) { - get - { - EnsureNodeInitialized(); - return _isDraft; - } + EnsureNodeInitialized(); + return _isDraft; // bah } public override IEnumerable Properties diff --git a/src/Umbraco.Web/PublishedContentExtensions.cs b/src/Umbraco.Web/PublishedContentExtensions.cs index 9d9dbaae6d..92edb88297 100644 --- a/src/Umbraco.Web/PublishedContentExtensions.cs +++ b/src/Umbraco.Web/PublishedContentExtensions.cs @@ -141,50 +141,35 @@ namespace Umbraco.Web #endregion - // fixme - .HasValue() and .Value() refactoring - in progress - see exceptions below - - #region HasValue + #region HasValue, Value, Value /// /// Gets a value indicating whether the content has a value for a property identified by its alias. /// /// The content. /// The property alias. - /// A value indicating whether to navigate the tree upwards until a property with a value is found. + /// The variation language. + /// The variation segment. + /// Optional fallback strategy. /// A value indicating whether the content has a value for the property identified by the alias. - /// Returns true if GetProperty(alias, recurse) is not null and GetProperty(alias, recurse).HasValue is true. - public static bool HasValue(this IPublishedContent content, string alias, bool recurse) + /// Returns true if HasValue is true, or a fallback strategy can provide a value. + public static bool HasValue(this IPublishedContent content, string alias, string culture = null, string segment = null, Fallback fallback = default) { - throw new NotImplementedException("WorkInProgress"); + var property = content.GetProperty(alias); - //var prop = content.GetProperty(alias, recurse); - //return prop != null && prop.HasValue(); + // if we have a property, and it has a value, return that value + if (property != null && property.HasValue(culture, segment)) + return true; + + // else let fallback try to get a value + // fixme - really? + if (PublishedValueFallback.TryGetValue(content, alias, culture, segment, fallback, null, out _)) + return true; + + // else... no + return false; } - /// - /// Returns one of two strings depending on whether the content has a value for a property identified by its alias. - /// - /// The content. - /// The property alias. - /// A value indicating whether to navigate the tree upwards until a property with a value is found. - /// The value to return if the content has a value for the property. - /// The value to return if the content has no value for the property. - /// Either or depending on whether the content - /// has a value for the property identified by the alias. - public static IHtmlString HasValue(this IPublishedContent content, string alias, bool recurse, - string valueIfTrue, string valueIfFalse = null) - { - throw new NotImplementedException("WorkInProgress"); - - //return content.HasValue(alias, recurse) - // ? new HtmlString(valueIfTrue) - // : new HtmlString(valueIfFalse ?? string.Empty); - } - - #endregion - - #region Value - /// /// Gets the value of a content's property identified by its alias, if it exists, otherwise a default value. /// @@ -207,15 +192,14 @@ namespace Umbraco.Web if (PublishedValueFallback.TryGetValue(content, alias, culture, segment, fallback, defaultValue, out var value)) return value; + if (property == null) + return null; + // else... if we have a property, at least let the converter return its own - // vision of 'no value' (could be an empty enumerable) - otherwise, default - return property?.GetValue(culture, segment); + // vision of 'no value' (could be an empty enumerable) + return property.GetValue(culture, segment); } - #endregion - - #region Value - /// /// Gets the value of a content's property identified by its alias, converted to a specified type. /// @@ -383,16 +367,6 @@ namespace Umbraco.Web return recursive && content.IsComposedOf(docTypeAlias); } - public static bool IsNull(this IPublishedContent content, string alias, bool recurse) - { - return content.HasValue(alias, recurse) == false; - } - - public static bool IsNull(this IPublishedContent content, string alias) - { - return content.HasValue(alias) == false; - } - #endregion #region IsSomething: equality diff --git a/src/Umbraco.Web/PublishedContentQuery.cs b/src/Umbraco.Web/PublishedContentQuery.cs index c2fb84a3da..04611000b9 100644 --- a/src/Umbraco.Web/PublishedContentQuery.cs +++ b/src/Umbraco.Web/PublishedContentQuery.cs @@ -22,7 +22,6 @@ namespace Umbraco.Web /// public class PublishedContentQuery : IPublishedContentQuery { - private readonly IPublishedContentQuery _query; private readonly IPublishedContentCache _contentCache; private readonly IPublishedMediaCache _mediaCache; @@ -37,79 +36,52 @@ namespace Umbraco.Web _mediaCache = mediaCache ?? throw new ArgumentNullException(nameof(mediaCache)); } - /// - /// Constructor used to wrap the ITypedPublishedContentQuery object passed in - /// - /// - public PublishedContentQuery(IPublishedContentQuery query) - { - _query = query ?? throw new ArgumentNullException(nameof(query)); - } - #region Content public IPublishedContent Content(int id) { - return _query == null - ? ItemById(id, _contentCache) - : _query.Content(id); + return ItemById(id, _contentCache); } public IPublishedContent Content(Guid id) { - return _query == null - ? ItemById(id, _contentCache) - : _query.Content(id); + return ItemById(id, _contentCache); } public IPublishedContent Content(Udi id) { if (!(id is GuidUdi udi)) return null; - return _query == null - ? ItemById(udi.Guid, _contentCache) - : _query.Content(udi.Guid); + return ItemById(udi.Guid, _contentCache); } public IPublishedContent ContentSingleAtXPath(string xpath, params XPathVariable[] vars) { - return _query == null - ? ItemByXPath(xpath, vars, _contentCache) - : _query.ContentSingleAtXPath(xpath, vars); + return ItemByXPath(xpath, vars, _contentCache); } public IEnumerable Content(IEnumerable ids) { - return _query == null - ? ItemsByIds(_contentCache, ids) - : _query.Content(ids); + return ItemsByIds(_contentCache, ids); } public IEnumerable Content(IEnumerable ids) { - return _query == null - ? ItemsByIds(_contentCache, ids) - : _query.Content(ids); + return ItemsByIds(_contentCache, ids); } public IEnumerable ContentAtXPath(string xpath, params XPathVariable[] vars) { - return _query == null - ? ItemsByXPath(xpath, vars, _contentCache) - : _query.ContentAtXPath(xpath, vars); + return ItemsByXPath(xpath, vars, _contentCache); } public IEnumerable ContentAtXPath(XPathExpression xpath, params XPathVariable[] vars) { - return _query == null - ? ItemsByXPath(xpath, vars, _contentCache) - : _query.ContentAtXPath(xpath, vars); + return ItemsByXPath(xpath, vars, _contentCache); } public IEnumerable ContentAtRoot() { - return _query == null - ? ItemsAtRoot(_contentCache) - : _query.ContentAtRoot(); + return ItemsAtRoot(_contentCache); } #endregion @@ -118,45 +90,33 @@ namespace Umbraco.Web public IPublishedContent Media(int id) { - return _query == null - ? ItemById(id, _mediaCache) - : _query.Media(id); + return ItemById(id, _mediaCache); } public IPublishedContent Media(Guid id) { - return _query == null - ? ItemById(id, _mediaCache) - : _query.Media(id); + return ItemById(id, _mediaCache); } public IPublishedContent Media(Udi id) { if (!(id is GuidUdi udi)) return null; - return _query == null - ? ItemById(udi.Guid, _mediaCache) - : _query.Media(udi.Guid); + return ItemById(udi.Guid, _mediaCache); } public IEnumerable Media(IEnumerable ids) { - return _query == null - ? ItemsByIds(_mediaCache, ids) - : _query.Media(ids); + return ItemsByIds(_mediaCache, ids); } public IEnumerable Media(IEnumerable ids) { - return _query == null - ? ItemsByIds(_mediaCache, ids) - : _query.Media(ids); + return ItemsByIds(_mediaCache, ids); } public IEnumerable MediaAtRoot() { - return _query == null - ? ItemsAtRoot(_mediaCache) - : _query.MediaAtRoot(); + return ItemsAtRoot(_mediaCache); } @@ -231,8 +191,6 @@ namespace Umbraco.Web { //fixme: inject IExamineManager - if (_query != null) return _query.Search(skip, take, out totalRecords, term, useWildCards, indexName); - indexName = string.IsNullOrEmpty(indexName) ? Constants.Examine.ExternalIndexer : indexName; @@ -259,8 +217,6 @@ namespace Umbraco.Web /// public IEnumerable Search(int skip, int take, out long totalRecords, ISearchCriteria criteria, ISearcher searcher = null) { - if (_query != null) return _query.Search(skip, take, out totalRecords, criteria, searcher); - //fixme: inject IExamineManager if (searcher == null) { diff --git a/src/Umbraco.Web/Routing/UrlProvider.cs b/src/Umbraco.Web/Routing/UrlProvider.cs index 36c3ba5533..b265d48923 100644 --- a/src/Umbraco.Web/Routing/UrlProvider.cs +++ b/src/Umbraco.Web/Routing/UrlProvider.cs @@ -96,7 +96,7 @@ namespace Umbraco.Web.Routing /// public string GetUrl(IPublishedContent content, bool absolute, string culture = null, Uri current = null) => GetUrl(content, GetMode(absolute), culture, current); - + /// /// Gets the url of a published content. /// @@ -196,10 +196,6 @@ namespace Umbraco.Web.Routing if (culture == null) culture = _variationContextAccessor?.VariationContext?.Culture ?? ""; } - else - { - culture = null; - } if (current == null) current = _umbracoContext.CleanedUmbracoUrl; diff --git a/src/Umbraco.Web/Search/ExamineComponent.cs b/src/Umbraco.Web/Search/ExamineComponent.cs index a2d4bff9da..2faacf828d 100644 --- a/src/Umbraco.Web/Search/ExamineComponent.cs +++ b/src/Umbraco.Web/Search/ExamineComponent.cs @@ -208,23 +208,7 @@ namespace Umbraco.Web.Search if (_isConfigured) return; _isConfigured = true; - - foreach (var luceneIndexer in examineManager.Indexes.OfType()) - { - //We now need to disable waiting for indexing for Examine so that the appdomain is shutdown immediately and doesn't wait for pending - //indexing operations. We used to wait for indexing operations to complete but this can cause more problems than that is worth because - //that could end up halting shutdown for a very long time causing overlapping appdomains and many other problems. - luceneIndexer.WaitForIndexQueueOnShutdown = false; - - //we should check if the index is locked ... it shouldn't be! We are using simple fs lock now and we are also ensuring that - //the indexes are not operational unless MainDom is true - var dir = luceneIndexer.GetLuceneDirectory(); - if (IndexWriter.IsLocked(dir)) - { - logger.Info("Forcing index {IndexerName} to be unlocked since it was left in a locked state", luceneIndexer.Name); - IndexWriter.Unlock(dir); - } - } + examineManager.UnlockLuceneIndexes(logger); } } diff --git a/src/Umbraco.Web/Search/IUmbracoIndexesCreator.cs b/src/Umbraco.Web/Search/IUmbracoIndexesCreator.cs index 58014597d2..d654e4effd 100644 --- a/src/Umbraco.Web/Search/IUmbracoIndexesCreator.cs +++ b/src/Umbraco.Web/Search/IUmbracoIndexesCreator.cs @@ -1,13 +1,14 @@ using System.Collections.Generic; using Examine; +using Umbraco.Examine; namespace Umbraco.Web.Search { + /// /// /// Used to create the Umbraco indexes /// - public interface IUmbracoIndexesCreator + public interface IUmbracoIndexesCreator : IIndexCreator { - IEnumerable Create(); } } diff --git a/src/Umbraco.Web/Search/UmbracoIndexesCreator.cs b/src/Umbraco.Web/Search/UmbracoIndexesCreator.cs index decfb3c738..77a8f3f47c 100644 --- a/src/Umbraco.Web/Search/UmbracoIndexesCreator.cs +++ b/src/Umbraco.Web/Search/UmbracoIndexesCreator.cs @@ -21,7 +21,7 @@ namespace Umbraco.Web.Search /// /// Creates the indexes used by Umbraco /// - public class UmbracoIndexesCreator : IUmbracoIndexesCreator + public class UmbracoIndexesCreator : LuceneIndexCreator, IUmbracoIndexesCreator { //TODO: we should inject the different IValueSetValidator so devs can just register them instead of overriding this class? @@ -45,7 +45,7 @@ namespace Umbraco.Web.Search /// Creates the Umbraco indexes ///
/// - public IEnumerable Create() + public override IEnumerable Create() { return new [] { @@ -61,7 +61,7 @@ namespace Umbraco.Web.Search Constants.UmbracoIndexes.InternalIndexName, //fixme - how to deal with languages like in UmbracoContentIndexer.CreateFieldValueTypes UmbracoExamineIndex.UmbracoIndexFieldDefinitions, - GetFileSystemLuceneDirectory(Constants.UmbracoIndexes.InternalIndexPath), + CreateFileSystemLuceneDirectory(Constants.UmbracoIndexes.InternalIndexPath), new CultureInvariantWhitespaceAnalyzer(), ProfilingLogger, LanguageService, @@ -75,7 +75,7 @@ namespace Umbraco.Web.Search Constants.UmbracoIndexes.ExternalIndexName, //fixme - how to deal with languages like in UmbracoContentIndexer.CreateFieldValueTypes UmbracoExamineIndex.UmbracoIndexFieldDefinitions, - GetFileSystemLuceneDirectory(Constants.UmbracoIndexes.ExternalIndexPath), + CreateFileSystemLuceneDirectory(Constants.UmbracoIndexes.ExternalIndexPath), new StandardAnalyzer(Lucene.Net.Util.Version.LUCENE_30), ProfilingLogger, LanguageService, @@ -89,27 +89,13 @@ namespace Umbraco.Web.Search Constants.UmbracoIndexes.MembersIndexName, //fixme - how to deal with languages like in UmbracoContentIndexer.CreateFieldValueTypes UmbracoExamineIndex.UmbracoIndexFieldDefinitions, - GetFileSystemLuceneDirectory(Constants.UmbracoIndexes.MembersIndexPath), + CreateFileSystemLuceneDirectory(Constants.UmbracoIndexes.MembersIndexPath), new CultureInvariantWhitespaceAnalyzer(), ProfilingLogger, GetMemberValueSetValidator()); return index; } - - public virtual Lucene.Net.Store.Directory GetFileSystemLuceneDirectory(string name) - { - var dirInfo = new DirectoryInfo(Path.Combine(IOHelper.MapPath(SystemDirectories.Data), "TEMP", "ExamineIndexes", name)); - if (!dirInfo.Exists) - System.IO.Directory.CreateDirectory(dirInfo.FullName); - - var luceneDir = new SimpleFSDirectory(dirInfo); - //we want to tell examine to use a different fs lock instead of the default NativeFSFileLock which could cause problems if the appdomain - //terminates and in some rare cases would only allow unlocking of the file if IIS is forcefully terminated. Instead we'll rely on the simplefslock - //which simply checks the existence of the lock file - luceneDir.SetLockFactory(new NoPrefixSimpleFsLockFactory(dirInfo)); - return luceneDir; - } - + public virtual IContentValueSetValidator GetContentValueSetValidator() { return new ContentValueSetValidator(false, true, PublicAccessService); diff --git a/src/Umbraco.Web/TagQuery.cs b/src/Umbraco.Web/TagQuery.cs index 9527e72453..e79640bb4d 100644 --- a/src/Umbraco.Web/TagQuery.cs +++ b/src/Umbraco.Web/TagQuery.cs @@ -9,195 +9,92 @@ using Umbraco.Web.Models; namespace Umbraco.Web { /// - /// A class that exposes methods used to query tag data in views + /// Implements . /// public class TagQuery : ITagQuery { - - //TODO: This class also acts as a wrapper for ITagQuery due to breaking changes, need to fix in - // version 8: http://issues.umbraco.org/issue/U4-6899 - private readonly ITagQuery _wrappedQuery; - private readonly ITagService _tagService; private readonly IPublishedContentQuery _contentQuery; - /// - /// Constructor for wrapping ITagQuery, see http://issues.umbraco.org/issue/U4-6899 + /// Initializes a new instance of the class. /// - /// - internal TagQuery(ITagQuery wrappedQuery) - { - if (wrappedQuery == null) throw new ArgumentNullException("wrappedQuery"); - _wrappedQuery = wrappedQuery; - } - - /// - /// Constructor - /// - /// - /// public TagQuery(ITagService tagService, IPublishedContentQuery contentQuery) { - if (tagService == null) throw new ArgumentNullException("tagService"); - if (contentQuery == null) throw new ArgumentNullException("contentQuery"); - _tagService = tagService; - _contentQuery = contentQuery; + _tagService = tagService ?? throw new ArgumentNullException(nameof(tagService)); + _contentQuery = contentQuery ?? throw new ArgumentNullException(nameof(contentQuery)); } - /// - /// Returns all content that is tagged with the specified tag value and optional tag group - /// - /// - /// - /// - public IEnumerable GetContentByTag(string tag, string tagGroup = null) + /// + public IEnumerable GetContentByTag(string tag, string group = null, string culture = null) { - //TODO: http://issues.umbraco.org/issue/U4-6899 - if (_wrappedQuery != null) return _wrappedQuery.GetContentByTag(tag, tagGroup); - - var ids = _tagService.GetTaggedContentByTag(tag, tagGroup) + var ids = _tagService.GetTaggedContentByTag(tag, group, culture) .Select(x => x.EntityId); return _contentQuery.Content(ids) .Where(x => x != null); } - /// - /// Returns all content that has been tagged with any tag in the specified group - /// - /// - /// - public IEnumerable GetContentByTagGroup(string tagGroup) + /// + public IEnumerable GetContentByTagGroup(string group, string culture = null) { - //TODO: http://issues.umbraco.org/issue/U4-6899 - if (_wrappedQuery != null) return _wrappedQuery.GetContentByTagGroup(tagGroup); - - var ids = _tagService.GetTaggedContentByTagGroup(tagGroup) + var ids = _tagService.GetTaggedContentByTagGroup(group, culture) .Select(x => x.EntityId); return _contentQuery.Content(ids) .Where(x => x != null); } - /// - /// Returns all Media that is tagged with the specified tag value and optional tag group - /// - /// - /// - /// - public IEnumerable GetMediaByTag(string tag, string tagGroup = null) + /// + public IEnumerable GetMediaByTag(string tag, string group = null, string culture = null) { - //TODO: http://issues.umbraco.org/issue/U4-6899 - if (_wrappedQuery != null) return _wrappedQuery.GetMediaByTag(tag, tagGroup); - - var ids = _tagService.GetTaggedMediaByTag(tag, tagGroup) + var ids = _tagService.GetTaggedMediaByTag(tag, group, culture) .Select(x => x.EntityId); return _contentQuery.Media(ids) .Where(x => x != null); } - /// - /// Returns all Media that has been tagged with any tag in the specified group - /// - /// - /// - public IEnumerable GetMediaByTagGroup(string tagGroup) + /// + public IEnumerable GetMediaByTagGroup(string group, string culture = null) { - //TODO: http://issues.umbraco.org/issue/U4-6899 - if (_wrappedQuery != null) return _wrappedQuery.GetMediaByTagGroup(tagGroup); - - var ids = _tagService.GetTaggedMediaByTagGroup(tagGroup) + var ids = _tagService.GetTaggedMediaByTagGroup(group, culture) .Select(x => x.EntityId); return _contentQuery.Media(ids) .Where(x => x != null); } - //TODO: Should prob implement these, requires a bit of work on the member service to do this, - // also not sure if its necessary ? - //public IEnumerable GetMembersByTag(string tag, string tagGroup = null) - //{ - //} - - //public IEnumerable GetMembersByTagGroup(string tagGroup) - //{ - //} - - /// - /// Get every tag stored in the database (with optional group) - /// - public IEnumerable GetAllTags(string group = null) + /// + public IEnumerable GetAllTags(string group = null, string culture = null) { - //TODO: http://issues.umbraco.org/issue/U4-6899 - if (_wrappedQuery != null) return _wrappedQuery.GetAllTags(group); - - return Mapper.Map>(_tagService.GetAllTags(group)); + return Mapper.Map>(_tagService.GetAllTags(group, culture)); } - /// - /// Get all tags for content items (with optional group) - /// - /// - /// - public IEnumerable GetAllContentTags(string group = null) + /// + public IEnumerable GetAllContentTags(string group = null, string culture = null) { - //TODO: http://issues.umbraco.org/issue/U4-6899 - if (_wrappedQuery != null) return _wrappedQuery.GetAllContentTags(group); - - return Mapper.Map>(_tagService.GetAllContentTags(group)); + return Mapper.Map>(_tagService.GetAllContentTags(group, culture)); } - /// - /// Get all tags for media items (with optional group) - /// - /// - /// - public IEnumerable GetAllMediaTags(string group = null) + /// + public IEnumerable GetAllMediaTags(string group = null, string culture = null) { - //TODO: http://issues.umbraco.org/issue/U4-6899 - if (_wrappedQuery != null) return _wrappedQuery.GetAllMediaTags(group); - - return Mapper.Map>(_tagService.GetAllMediaTags(group)); + return Mapper.Map>(_tagService.GetAllMediaTags(group, culture)); } - /// - /// Get all tags for member items (with optional group) - /// - /// - /// - public IEnumerable GetAllMemberTags(string group = null) + /// + public IEnumerable GetAllMemberTags(string group = null, string culture = null) { - //TODO: http://issues.umbraco.org/issue/U4-6899 - if (_wrappedQuery != null) return _wrappedQuery.GetAllMemberTags(group); - - return Mapper.Map>(_tagService.GetAllMemberTags(group)); + return Mapper.Map>(_tagService.GetAllMemberTags(group, culture)); } - /// - /// Returns all tags attached to a property by entity id - /// - /// - /// - /// - /// - public IEnumerable GetTagsForProperty(int contentId, string propertyTypeAlias, string tagGroup = null) + /// + public IEnumerable GetTagsForProperty(int contentId, string propertyTypeAlias, string group = null, string culture = null) { - //TODO: http://issues.umbraco.org/issue/U4-6899 - if (_wrappedQuery != null) return _wrappedQuery.GetTagsForProperty(contentId, propertyTypeAlias, tagGroup); - - return Mapper.Map>(_tagService.GetTagsForProperty(contentId, propertyTypeAlias, tagGroup)); + return Mapper.Map>(_tagService.GetTagsForProperty(contentId, propertyTypeAlias, group, culture)); } - /// - /// Returns all tags attached to an entity (content, media or member) by entity id - /// - /// - /// - /// - public IEnumerable GetTagsForEntity(int contentId, string tagGroup = null) + /// + public IEnumerable GetTagsForEntity(int contentId, string group = null, string culture = null) { - //TODO: http://issues.umbraco.org/issue/U4-6899 - if (_wrappedQuery != null) return _wrappedQuery.GetTagsForEntity(contentId, tagGroup); - - return Mapper.Map>(_tagService.GetTagsForEntity(contentId, tagGroup)); + return Mapper.Map>(_tagService.GetTagsForEntity(contentId, group, culture)); } } } diff --git a/src/Umbraco.Web/Trees/ApplicationTreeController.cs b/src/Umbraco.Web/Trees/ApplicationTreeController.cs index c1192b6909..d824f32f4b 100644 --- a/src/Umbraco.Web/Trees/ApplicationTreeController.cs +++ b/src/Umbraco.Web/Trees/ApplicationTreeController.cs @@ -78,10 +78,20 @@ namespace Umbraco.Web.Trees } } - var multiTree = TreeRootNode.CreateMultiTreeRoot(collection); - multiTree.Name = Services.TextService.Localize("sections/" + application); + if(collection.Count > 0) + { + var multiTree = TreeRootNode.CreateMultiTreeRoot(collection); + multiTree.Name = Services.TextService.Localize("sections/" + application); - return multiTree; + return multiTree; + } + + //Otherwise its a application/section with no trees (aka a full screen app) + //For example we do not have a Forms tree definied in C# & can not attribute with [Tree(isSingleNodeTree:true0] + var rootId = Constants.System.Root.ToString(CultureInfo.InvariantCulture); + var section = Services.TextService.Localize("sections/" + application); + + return TreeRootNode.CreateSingleTreeRoot(rootId, null, null, section, TreeNodeCollection.Empty, true); } var rootNodeGroups = new List(); diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index fb931c2f3a..ac6e40623d 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -840,9 +840,6 @@ - - - @@ -1010,7 +1007,6 @@ Resources.resx - diff --git a/src/Umbraco.Web/UmbracoHelper.cs b/src/Umbraco.Web/UmbracoHelper.cs index d274ca2f44..3c7e0d3738 100644 --- a/src/Umbraco.Web/UmbracoHelper.cs +++ b/src/Umbraco.Web/UmbracoHelper.cs @@ -31,7 +31,7 @@ namespace Umbraco.Web private readonly ServiceContext _services; private IUmbracoComponentRenderer _componentRenderer; - private PublishedContentQuery _query; + private IPublishedContentQuery _query; private MembershipHelper _membershipHelper; private ITagQuery _tag; private ICultureDictionary _cultureDictionary; diff --git a/src/Umbraco.Web/umbraco.presentation/page.cs b/src/Umbraco.Web/umbraco.presentation/page.cs index e8d395881c..219e2101be 100644 --- a/src/Umbraco.Web/umbraco.presentation/page.cs +++ b/src/Umbraco.Web/umbraco.presentation/page.cs @@ -467,9 +467,9 @@ namespace umbraco get { return PublishedItemType.Content; } } - public bool IsDraft + public bool IsDraft(string culture = null) { - get { throw new NotImplementedException(); } + throw new NotImplementedException(); } public IPublishedContent Parent