Merge branch temp8 into temp8-di2690

This commit is contained in:
Stephan
2018-12-18 15:42:45 +01:00
136 changed files with 3339 additions and 3147 deletions
@@ -65,37 +65,6 @@ namespace Umbraco.Core.Composing
return This;
}
/// <summary>
/// Removes a type from the collection.
/// </summary>
/// <typeparam name="T">The type to remove.</typeparam>
/// <returns>The builder.</returns>
public TBuilder Remove<T>()
where T : TItem
{
Configure(types =>
{
var type = typeof(T);
if (types.Contains(type)) types.Remove(type);
});
return This;
}
/// <summary>
/// Removes a type from the collection.
/// </summary>
/// <param name="type">The type to remove.</param>
/// <returns>The builder.</returns>
public TBuilder Remove(Type type)
{
Configure(types =>
{
EnsureType(type, "remove");
if (types.Contains(type)) types.Remove(type);
});
return This;
}
/// <summary>
/// Adds a types producer to the collection.
/// </summary>
+11 -34
View File
@@ -41,6 +41,16 @@ namespace Umbraco.Core.Composing
private static LocalTempStorage _localTempStorage;
private static string _fileBasePath;
/// <summary>
/// Initializes a new instance of the <see cref="TypeLoader"/> class.
/// </summary>
/// <param name="runtimeCache">The application runtime cache.</param>
/// <param name="localTempStorage">Files storage mode.</param>
/// <param name="logger">A profiling logger.</param>
public TypeLoader(IRuntimeCacheProvider runtimeCache, LocalTempStorage localTempStorage, ProfilingLogger logger)
: this(runtimeCache, localTempStorage, logger, true)
{ }
/// <summary>
/// Initializes a new instance of the <see cref="TypeLoader"/> class.
/// </summary>
@@ -48,7 +58,7 @@ namespace Umbraco.Core.Composing
/// <param name="localTempStorage">Files storage mode.</param>
/// <param name="logger">A profiling logger.</param>
/// <param name="detectChanges">Whether to detect changes using hashes.</param>
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()
{
+3 -2
View File
@@ -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;
@@ -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();
@@ -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
{ }
/// <inheritdoc />
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();
}
/// <inheritdoc />
public IExecutableBuilder Sql(string sqlStatement)
@@ -20,5 +30,12 @@ namespace Umbraco.Core.Migrations.Expressions.Execute
Expression.SqlStatement = sqlStatement;
return this;
}
/// <inheritdoc />
public IExecutableBuilder Sql(Sql<ISqlContext> sql)
{
Expression.SqlObject = sql;
return this;
}
}
}
@@ -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<ISqlContext> SqlObject { get; set; }
public void ExecuteSqlObject()
{
Execute(SqlObject);
}
protected override string GetSql()
{
return SqlStatement;
@@ -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.
/// </summary>
IExecutableBuilder Sql(string sqlStatement);
/// <summary>
/// Specifies the Sql statement to execute.
/// </summary>
IExecutableBuilder Sql(Sql<ISqlContext> sql);
}
}
@@ -33,6 +33,9 @@ namespace Umbraco.Core.Migrations.Install
private DatabaseSchemaResult _databaseSchemaValidationResult;
/// <summary>
/// Initializes a new instance of the <see cref="DatabaseBuilder"/> class.
/// </summary>
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
/// <summary>
/// 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.
/// </summary>
public bool IsDatabaseConfigured => _databaseFactory.Configured;
/// <summary>
/// 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.
/// </summary>
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)
/// <summary>
/// Verifies whether a it is possible to connect to a database.
/// </summary>
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()
/// <summary>
/// Validates the database schema.
/// </summary>
/// <remarks>
/// <para>This assumes that the database exists and the connection string is
/// configured and it is possible to connect to the database.</para>
/// </remarks>
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()
/// <summary>
/// Creates the database schema and inserts initial data.
/// </summary>
/// <remarks>
/// <para>This assumes that the database exists and the connection string is
/// configured and it is possible to connect to the database.</para>
/// </remarks>
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 = "<p>&nbsp;</p><p>The database you're trying to use does not support case insensitive queries. <br />We currently do not support these types of databases.</p>" +
"<p>You can fix this by changing the following setting in your my.ini file in your MySQL installation directory:</p>" +
"<pre>lower_case_table_names=1</pre><br />" +
"<p>Note: Make sure to check with your hosting provider if they support case insensitive queries as well.</p>" +
"<p>For more technical information on case sensitivity in MySQL, have a look at " +
"<a href='http://dev.mysql.com/doc/refman/5.0/en/identifier-case-sensitivity.html'>the documentation on the subject</a></p>";
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()
/// <summary>
/// Upgrades the database schema and data by running migrations.
/// </summary>
/// <remarks>
/// <para>This assumes that the database exists and the connection string is
/// configured and it is possible to connect to the database.</para>
/// <para>Runs whichever migrations need to run.</para>
/// </remarks>
public Result UpgradeSchemaAndData()
{
try
{
@@ -488,56 +495,11 @@ namespace Umbraco.Core.Migrations.Install
_logger.Info<DatabaseBuilder>("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 + "<p>Upgrade completed!</p>";
@@ -553,47 +515,14 @@ namespace Umbraco.Core.Migrations.Install
}
}
private string GetResultMessageForMySql()
{
if (_databaseFactory.GetType() == typeof(MySqlSyntaxProvider))
{
return "<p>&nbsp;</p><p>Congratulations, the database step ran successfully!</p>" +
"<p>Note: You're using MySQL and the database instance you're connecting to seems to support case insensitive queries.</p>" +
"<p>However, your hosting provider may not support this option. Umbraco does not currently support MySQL installs that do not support case insensitive queries</p>" +
"<p>Make sure to check with your hosting provider if they support case insensitive queries as well.</p>" +
"<p>They can check this by looking for the following setting in the my.ini file in their MySQL installation directory:</p>" +
"<pre>lower_case_table_names=1</pre><br />" +
"<p>For more technical information on case sensitivity in MySQL, have a look at " +
"<a href='http://dev.mysql.com/doc/refman/5.0/en/identifier-case-sensitivity.html'>the documentation on the subject</a></p>";
}
return string.Empty;
}
/*
private string GetResultMessageForMySql(bool? supportsCaseInsensitiveQueries)
{
if (supportsCaseInsensitiveQueries == null)
{
return "<p>&nbsp;</p><p>Warning! Could not check if your database type supports case insensitive queries. <br />We currently do not support these databases that do not support case insensitive queries.</p>" +
"<p>You can check this by looking for the following setting in your my.ini file in your MySQL installation directory:</p>" +
"<pre>lower_case_table_names=1</pre><br />" +
"<p>Note: Make sure to check with your hosting provider if they support case insensitive queries as well.</p>" +
"<p>For more technical information on case sensitivity in MySQL, have a look at " +
"<a href='http://dev.mysql.com/doc/refman/5.0/en/identifier-case-sensitivity.html'>the documentation on the subject</a></p>";
}
if (SqlSyntax.GetType() == typeof(MySqlSyntaxProvider))
{
return "<p>&nbsp;</p><p>Congratulations, the database step ran successfully!</p>" +
"<p>Note: You're using MySQL and the database instance you're connecting to seems to support case insensitive queries.</p>" +
"<p>However, your hosting provider may not support this option. Umbraco does not currently support MySQL installs that do not support case insensitive queries</p>" +
"<p>Make sure to check with your hosting provider if they support case insensitive queries as well.</p>" +
"<p>They can check this by looking for the following setting in the my.ini file in their MySQL installation directory:</p>" +
"<pre>lower_case_table_names=1</pre><br />" +
"<p>For more technical information on case sensitivity in MySQL, have a look at " +
"<a href='http://dev.mysql.com/doc/refman/5.0/en/identifier-case-sensitivity.html'>the documentation on the subject</a></p>";
}
return string.Empty;
}*/
private const string ResultMessageForMySql = "<p>&nbsp;</p><p>Congratulations, the database step ran successfully!</p>" +
"<p>Note: You're using MySQL and the database instance you're connecting to seems to support case insensitive queries.</p>" +
"<p>However, your hosting provider may not support this option. Umbraco does not currently support MySQL installs that do not support case insensitive queries</p>" +
"<p>Make sure to check with your hosting provider if they support case insensitive queries as well.</p>" +
"<p>They can check this by looking for the following setting in the my.ini file in their MySQL installation directory:</p>" +
"<pre>lower_case_table_names=1</pre><br />" +
"<p>For more technical information on case sensitivity in MySQL, have a look at " +
"<a href='http://dev.mysql.com/doc/refman/5.0/en/identifier-case-sensitivity.html'>the documentation on the subject</a></p>";
private Attempt<Result> CheckReadyForInstall()
{
@@ -629,11 +558,29 @@ namespace Umbraco.Core.Migrations.Install
};
}
internal class Result
/// <summary>
/// Represents the result of a database creation or upgrade.
/// </summary>
public class Result
{
/// <summary>
/// Gets or sets a value indicating whether an upgrade is required.
/// </summary>
public bool RequiresUpgrade { get; set; }
/// <summary>
/// Gets or sets the message returned by the operation.
/// </summary>
public string Message { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the operation succeeded.
/// </summary>
public bool Success { get; set; }
/// <summary>
/// Gets or sets an install progress pseudo-percentage.
/// </summary>
public string Percentage { get; set; }
}
@@ -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 });
}
@@ -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
@@ -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
/// <summary>
/// Represents ...
/// </summary>
internal class DatabaseSchemaResult
{
private readonly ISqlSyntaxProvider _sqlSyntax;
private readonly bool _isMySql;
public DatabaseSchemaResult(ISqlSyntaxProvider sqlSyntax)
{
_sqlSyntax = sqlSyntax;
_isMySql = sqlSyntax is MySqlSyntaxProvider;
Errors = new List<Tuple<string, string>>();
TableDefinitions = new List<TableDefinition>();
ValidTables = new List<string>();
ValidColumns = new List<string>();
ValidConstraints = new List<string>();
ValidIndexes = new List<string>();
IndexDefinitions = new List<DbIndexDefinition>();
}
public List<Tuple<string, string>> Errors { get; set; }
public List<Tuple<string, string>> Errors { get; }
public List<TableDefinition> TableDefinitions { get; set; }
public List<TableDefinition> TableDefinitions { get; }
public List<string> ValidTables { get; set; }
// fixme TableDefinitions are those that should be there, IndexDefinitions are those that... are in DB?
internal List<DbIndexDefinition> IndexDefinitions { get; }
public List<string> ValidColumns { get; set; }
public List<string> ValidTables { get; }
public List<string> ValidConstraints { get; set; }
public List<string> ValidColumns { get; }
public List<string> ValidIndexes { get; set; }
public List<string> ValidConstraints { get; }
internal IEnumerable<DbIndexDefinition> DbIndexDefinitions { get; set; }
public List<string> ValidIndexes { get; }
/// <summary>
/// Determines the version of the currently installed database by detecting the current database structure
/// Determines whether the database contains an installed version.
/// </summary>
/// <returns>
/// A <see cref="Version"/> with Major and Minor values for
/// non-empty database, otherwise "0.0.0" for empty databases.
/// </returns>
public Version DetermineInstalledVersion()
/// <remarks>
/// <para>A database contains an installed version when it contains at least one valid table.</para>
/// </remarks>
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;
}
/// <summary>
@@ -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();
@@ -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<ISqlContext> sql)
{
if (_executed)
throw new InvalidOperationException("This expression has already been executed.");
_executed = true;
if (sql == null)
{
Logger.Info(GetType(), $"SQL [{Context.Index}]: <empty>");
}
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();
+73 -83
View File
@@ -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
/// </summary>
public class MigrationPlan
{
private readonly IMigrationBuilder _migrationBuilder;
private readonly ILogger _logger;
private readonly Dictionary<string, Transition> _transitions = new Dictionary<string, Transition>();
private string _prevState;
@@ -23,64 +22,24 @@ namespace Umbraco.Core.Migrations
/// Initializes a new instance of the <see cref="MigrationPlan"/> class.
/// </summary>
/// <param name="name">The name of the plan.</param>
/// <remarks>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.</remarks>
public MigrationPlan(string name)
{
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullOrEmptyException(nameof(name));
Name = name;
// ReSharper disable once VirtualMemberCallInConstructor
// (accepted)
DefinePlan();
}
/// <summary>
/// Initializes a new instance of the <see cref="MigrationPlan"/> class.
/// Gets the transitions.
/// </summary>
/// <param name="name">The name of the plan.</param>
/// <param name="migrationBuilder">A migration builder.</param>
/// <param name="logger">A logger.</param>
/// <remarks>The plan can be executed.</remarks>
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();
}
/// <summary>
/// Defines the plan.
/// </summary>
protected virtual void DefinePlan() { }
public IReadOnlyDictionary<string, Transition> Transitions => _transitions;
/// <summary>
/// Gets the name of the plan.
/// </summary>
public string Name { get; }
/// <summary>
/// Adds an empty migration from source to target state.
/// </summary>
public MigrationPlan Add(string sourceState, string targetState)
=> Add<NoopMigration>(sourceState, targetState);
/// <summary>
/// Adds a migration from source to target state.
/// </summary>
public MigrationPlan Add<TMigration>(string sourceState, string targetState)
where TMigration : IMigration
=> Add(sourceState, targetState, typeof(TMigration));
/// <summary>
/// Adds a migration from source to target state.
/// </summary>
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
}
/// <summary>
/// Chains an empty migration from chain to target state.
/// </summary>
public MigrationPlan Chain(string targetState)
=> Chain<NoopMigration>(targetState);
/// Adds a transition to a target state through an empty migration.
/// </summary>
public MigrationPlan To(string targetState)
=> To<NoopMigration>(targetState);
/// <summary>
/// Chains a migration from chain to target state.
/// Adds a transition to a target state through a migration.
/// </summary>
public MigrationPlan Chain<TMigration>(string targetState)
public MigrationPlan To<TMigration>(string targetState)
where TMigration : IMigration
=> Chain(targetState, typeof(TMigration));
=> To(targetState, typeof(TMigration));
/// <summary>
/// Chains a migration from chain to target state.
/// Adds a transition to a target state through a migration.
/// </summary>
public MigrationPlan Chain(string targetState, Type migration)
public MigrationPlan To(string targetState, Type migration)
=> Add(_prevState, targetState, migration);
/// <summary>
/// Sets the chain state.
/// Sets the starting state.
/// </summary>
public MigrationPlan From(string sourceState)
{
@@ -141,19 +100,16 @@ namespace Umbraco.Core.Migrations
}
/// <summary>
/// Copies a chain.
/// Adds transitions to a target state by copying transitions from a start state to an end state.
/// </summary>
/// <remarks>Copies the chain going from startState to endState, with new states going from sourceState to targetState.</remarks>
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;
}
/// <summary>
/// Copies a chain.
/// </summary>
/// <remarks>Copies the chain going from startState to endState, with new states going from chain to targetState.</remarks>
public MigrationPlan CopyChain(string startState, string endState, string targetState)
=> CopyChain(_prevState, startState, endState, targetState);
/// <summary>
/// Gets the initial state.
/// </summary>
@@ -260,50 +208,92 @@ namespace Umbraco.Core.Migrations
/// </summary>
/// <param name="scope">A scope.</param>
/// <param name="fromState">The state to start execution at.</param>
/// <param name="migrationBuilder">A migration builder.</param>
/// <param name="logger">A logger.</param>
/// <returns>The final state.</returns>
/// <remarks>The plan executes within the scope, which must then be completed.</remarks>
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<MigrationPlan>("Starting '{MigrationName}'...", Name);
logger.Info<MigrationPlan>("Starting '{MigrationName}'...", Name);
var origState = fromState ?? string.Empty;
_logger.Info<MigrationPlan>("At {OrigState}", string.IsNullOrWhiteSpace(origState) ? "origin": origState);
logger.Info<MigrationPlan>("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<MigrationPlan>("At {OrigState}", origState);
logger.Info<MigrationPlan>("At {OrigState}", origState);
if (!_transitions.TryGetValue(origState, out transition))
throw new Exception($"Unknown state \"{origState}\".");
}
_logger.Info<MigrationPlan>("Done (pending scope completion).");
logger.Info<MigrationPlan>("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;
}
/// <summary>
/// Follows a path (for tests and debugging).
/// </summary>
/// <remarks>Does the same thing Execute does, but does not actually execute migrations.</remarks>
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;
}
/// <summary>
/// Represents a plan transition.
/// </summary>
private class Transition
public class Transition
{
/// <summary>
/// Initializes a new instance of the <see cref="Transition"/> class.
@@ -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
/// </summary>
public UmbracoPlan()
: base(Constants.System.UmbracoUpgradePlanName)
{ }
/// <summary>
/// Initializes a new instance of the <see cref="UmbracoPlan"/> class.
/// </summary>
public UmbracoPlan(IMigrationBuilder migrationBuilder, ILogger logger)
: base(Constants.System.UmbracoUpgradePlanName, migrationBuilder, logger)
{ }
{
DefinePlan();
}
/// <inheritdoc />
/// <remarks>
@@ -61,8 +55,8 @@ namespace Umbraco.Core.Migrations.Upgrade
}
}
/// <inheritdoc />
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<AddLockObjects>("{7C447271-CA3F-4A6A-A913-5D77015655CB}");
Chain<AddContentNuTable>("{CBFF58A2-7B50-4F75-8E98-249920DB0F37}");
Chain<RefactorXmlColumns>("{3D18920C-E84D-405C-A06A-B7CEE52FE5DD}");
Chain<VariantsMigration>("{FB0A5429-587E-4BD0-8A67-20F0E7E62FF7}");
Chain<DropMigrationsTable>("{F0C42457-6A3B-4912-A7EA-F27ED85A2092}");
Chain<DataTypeMigration>("{8640C9E4-A1C0-4C59-99BB-609B4E604981}");
Chain<TagsMigration>("{DD1B99AF-8106-4E00-BAC7-A43003EA07F8}");
Chain<SuperZero>("{9DF05B77-11D1-475C-A00A-B656AF7E0908}");
Chain<PropertyEditorsMigration>("{6FE3EF34-44A0-4992-B379-B40BC4EF1C4D}");
Chain<LanguageColumns>("{7F59355A-0EC9-4438-8157-EB517E6D2727}");
To<AddLockObjects>("{7C447271-CA3F-4A6A-A913-5D77015655CB}");
To<AddContentNuTable>("{CBFF58A2-7B50-4F75-8E98-249920DB0F37}");
To<RefactorXmlColumns>("{3D18920C-E84D-405C-A06A-B7CEE52FE5DD}");
To<VariantsMigration>("{FB0A5429-587E-4BD0-8A67-20F0E7E62FF7}");
To<DropMigrationsTable>("{F0C42457-6A3B-4912-A7EA-F27ED85A2092}");
To<DataTypeMigration>("{8640C9E4-A1C0-4C59-99BB-609B4E604981}");
To<TagsMigration>("{DD1B99AF-8106-4E00-BAC7-A43003EA07F8}");
To<SuperZero>("{9DF05B77-11D1-475C-A00A-B656AF7E0908}");
To<PropertyEditorsMigration>("{6FE3EF34-44A0-4992-B379-B40BC4EF1C4D}");
To<LanguageColumns>("{7F59355A-0EC9-4438-8157-EB517E6D2727}");
//To<AddVariationTables1>("{941B2ABA-2D06-4E04-81F5-74224F1DB037}"); // AddVariationTables1 has been superseded by AddVariationTables2
To<AddVariationTables2>("{76DF5CD7-A884-41A5-8DC6-7860D95B1DF5}");
// AddVariationTables1 has been superceeded by AddVariationTables2
//Chain<AddVariationTables1>("{941B2ABA-2D06-4E04-81F5-74224F1DB037}");
Chain<AddVariationTables2>("{76DF5CD7-A884-41A5-8DC6-7860D95B1DF5}");
// however, provide a path out of the old state
Add<AddVariationTables1A>("{941B2ABA-2D06-4E04-81F5-74224F1DB037}", "{76DF5CD7-A884-41A5-8DC6-7860D95B1DF5}");
// way out of the commented state
From("{941B2ABA-2D06-4E04-81F5-74224F1DB037}");
To<AddVariationTables1A>("{76DF5CD7-A884-41A5-8DC6-7860D95B1DF5}");
// resume at {76DF5CD7-A884-41A5-8DC6-7860D95B1DF5} ...
Chain<RefactorMacroColumns>("{A7540C58-171D-462A-91C5-7A9AA5CB8BFD}");
To<RefactorMacroColumns>("{A7540C58-171D-462A-91C5-7A9AA5CB8BFD}");
To<UserForeignKeys>("{3E44F712-E2E3-473A-AE49-5D7F8E67CE3F}"); // shannon added that one - let's keep it as the default path
//To<AddTypedLabels>("{65D6B71C-BDD5-4A2E-8D35-8896325E9151}"); // stephan added that one = merge conflict, remove
To<AddTypedLabels>("{4CACE351-C6B9-4F0C-A6BA-85A02BBD39E4}"); // but add it after shannon's, with a new target state
Chain<UserForeignKeys>("{3E44F712-E2E3-473A-AE49-5D7F8E67CE3F}"); // shannon added that one - let's keep it as the default path
//Chain<AddTypedLabels>("{65D6B71C-BDD5-4A2E-8D35-8896325E9151}"); // stephan added that one = merge conflict, remove,
Chain<AddTypedLabels>("{4CACE351-C6B9-4F0C-A6BA-85A02BBD39E4}"); // but add it after shannon's, with a new target state,
Add<UserForeignKeys>("{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<UserForeignKeys>("{4CACE351-C6B9-4F0C-A6BA-85A02BBD39E4}");
// resume at {4CACE351-C6B9-4F0C-A6BA-85A02BBD39E4} ...
Chain<ContentVariationMigration>("{1350617A-4930-4D61-852F-E3AA9E692173}");
Chain<UpdateUmbracoConsent>("{39E5B1F7-A50B-437E-B768-1723AEC45B65}"); // from 7.12.0
//Chain<FallbackLanguage>("{CF51B39B-9B9A-4740-BB7C-EAF606A7BFBF}"); // andy added that one = merge conflict, remove
Chain<AddRelationTypeForMediaFolderOnDelete>("{0541A62B-EF87-4CA2-8225-F0EB98ECCC9F}"); // from 7.12.0
Chain<IncreaseLanguageIsoCodeColumnLength>("{EB34B5DC-BB87-4005-985E-D983EA496C38}"); // from 7.12.0
Chain<RenameTrueFalseField>("{517CE9EA-36D7-472A-BF4B-A0D6FB1B8F89}"); // from 7.12.0
Chain<SetDefaultTagsStorageType>("{BBD99901-1545-40E4-8A5A-D7A675C7D2F2}"); // from 7.12.0
//Chain<UpdateDefaultMandatoryLanguage>("{2C87AA47-D1BC-4ECB-8A73-2D8D1046C27F}"); // stephan added that one = merge conflict, remove
To<ContentVariationMigration>("{1350617A-4930-4D61-852F-E3AA9E692173}");
To<UpdateUmbracoConsent>("{39E5B1F7-A50B-437E-B768-1723AEC45B65}"); // from 7.12.0
//To<FallbackLanguage>("{CF51B39B-9B9A-4740-BB7C-EAF606A7BFBF}"); // andy added that one = merge conflict, remove
To<AddRelationTypeForMediaFolderOnDelete>("{0541A62B-EF87-4CA2-8225-F0EB98ECCC9F}"); // from 7.12.0
To<IncreaseLanguageIsoCodeColumnLength>("{EB34B5DC-BB87-4005-985E-D983EA496C38}"); // from 7.12.0
To<RenameTrueFalseField>("{517CE9EA-36D7-472A-BF4B-A0D6FB1B8F89}"); // from 7.12.0
To<SetDefaultTagsStorageType>("{BBD99901-1545-40E4-8A5A-D7A675C7D2F2}"); // from 7.12.0
//To<UpdateDefaultMandatoryLanguage>("{2C87AA47-D1BC-4ECB-8A73-2D8D1046C27F}"); // stephan added that one = merge conflict, remove
Chain<FallbackLanguage>("{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<FallbackLanguage>("{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<UpdateDefaultMandatoryLanguage>("{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<FallbackLanguage>("{5F4597F4-A4E0-4AFE-90B5-6D2F896830EB}"); // to next
To<UpdateDefaultMandatoryLanguage>("{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<FallbackLanguage>("{5F4597F4-A4E0-4AFE-90B5-6D2F896830EB}"); // to next
// resume at {5F4597F4-A4E0-4AFE-90B5-6D2F896830EB} ...
//Chain<RefactorVariantsModel>("{B19BF0F2-E1C6-4AEB-A146-BC559D97A2C6}");
Chain<RefactorVariantsModel>("{290C18EE-B3DE-4769-84F1-1F467F3F76DA}");
From("{B19BF0F2-E1C6-4AEB-A146-BC559D97A2C6}")
.Chain<FallbackLanguage>("{290C18EE-B3DE-4769-84F1-1F467F3F76DA}");
//To<RefactorVariantsModel>("{B19BF0F2-E1C6-4AEB-A146-BC559D97A2C6}");
To<RefactorVariantsModel>("{290C18EE-B3DE-4769-84F1-1F467F3F76DA}");
// way out of the commented state
From("{B19BF0F2-E1C6-4AEB-A146-BC559D97A2C6}");
To<FallbackLanguage>("{290C18EE-B3DE-4769-84F1-1F467F3F76DA}");
// resume at {290C18EE-B3DE-4769-84F1-1F467F3F76DA}...
Chain<DropTaskTables>("{6A2C7C1B-A9DB-4EA9-B6AB-78E7D5B722A7}");
Chain<FixLockTablePrimaryKey>("{77874C77-93E5-4488-A404-A630907CEEF0}");
Chain<AddLogTableColumns>("{8804D8E8-FE62-4E3A-B8A2-C047C2118C38}");
Chain<DropPreValueTable>("{23275462-446E-44C7-8C2C-3B8C1127B07D}");
Chain<DropDownPropertyEditorsMigration>("{6B251841-3069-4AD5-8AE9-861F9523E8DA}");
Chain<TagsMigrationFix>("{EE429F1B-9B26-43CA-89F8-A86017C809A3}");
Chain<DropTemplateDesignColumn>("{08919C4B-B431-449C-90EC-2B8445B5C6B1}");
Chain<TablesForScheduledPublishing>("{7EB0254C-CB8B-4C75-B15B-D48C55B449EB}");
Chain<DropTaskTables>("{648A2D5F-7467-48F8-B309-E99CEEE00E2A}"); // fixed version
To<DropTaskTables>("{6A2C7C1B-A9DB-4EA9-B6AB-78E7D5B722A7}");
To<FixLockTablePrimaryKey>("{77874C77-93E5-4488-A404-A630907CEEF0}");
To<AddLogTableColumns>("{8804D8E8-FE62-4E3A-B8A2-C047C2118C38}");
To<DropPreValueTable>("{23275462-446E-44C7-8C2C-3B8C1127B07D}");
To<DropDownPropertyEditorsMigration>("{6B251841-3069-4AD5-8AE9-861F9523E8DA}");
To<TagsMigrationFix>("{EE429F1B-9B26-43CA-89F8-A86017C809A3}");
To<DropTemplateDesignColumn>("{08919C4B-B431-449C-90EC-2B8445B5C6B1}");
To<TablesForScheduledPublishing>("{7EB0254C-CB8B-4C75-B15B-D48C55B449EB}");
To<DropTaskTables>("{648A2D5F-7467-48F8-B309-E99CEEE00E2A}"); // fixed version
To<MakeTagsVariant>("{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}");
}
}
}
@@ -8,24 +8,41 @@ using Umbraco.Core.Services;
namespace Umbraco.Core.Migrations.Upgrade
{
/// <summary>
/// Represents the Umbraco upgrader.
/// </summary>
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;
/// <summary>
/// Initializes a new instance of the <see ref="UmbracoUpgrader" /> class.
/// </summary>
public UmbracoUpgrader()
: base(new UmbracoPlan())
{ }
protected override MigrationPlan GetPlan()
/// <summary>
/// Executes.
/// </summary>
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()
/// <inheritdoc />
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);
}
}
}
+53 -34
View File
@@ -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
/// <summary>
/// Represents an upgrader.
/// </summary>
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)
/// <summary>
/// Initializes a new instance of the <see ref="Upgrader"/> class.
/// </summary>
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;
}
/// <summary>
/// Gets the name of the migration plan.
/// </summary>
public string Name => Plan.Name;
public string StateValueKey => GetStateValueKey(Plan);
/// <summary>
/// Gets the migration plan.
/// </summary>
public MigrationPlan Plan { get; }
protected IScopeProvider ScopeProvider { get; }
/// <summary>
/// Gets the key for the state value.
/// </summary>
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()
/// <summary>
/// Executes.
/// </summary>
/// <param name="scopeProvider">A scope provider.</param>
/// <param name="migrationBuilder">A migration builder.</param>
/// <param name="keyValueService">A key-value service.</param>
/// <param name="logger">A logger.</param>
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;
/// <summary>
/// Executes as part of the upgrade scope and before all migrations have executed.
/// </summary>
public virtual void BeforeMigrations(IScope scope, ILogger logger)
{ }
/// <summary>
/// Executes as part of the upgrade scope and after all migrations have executed.
/// </summary>
public virtual void AfterMigrations(IScope scope, ILogger logger)
{ }
}
}
@@ -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<TagDto>("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();
}
}
}
@@ -15,10 +15,10 @@ namespace Umbraco.Core.Models
/// <param name="propertyTypeAlias">The property alias.</param>
/// <param name="tags">The tags.</param>
/// <param name="merge">A value indicating whether to merge the tags with existing tags instead of replacing them.</param>
/// <remarks>Tags do not support variants.</remarks>
public static void AssignTags(this IContentBase content, string propertyTypeAlias, IEnumerable<string> tags, bool merge = false)
/// <param name="culture">A culture, for multi-lingual properties.</param>
public static void AssignTags(this IContentBase content, string propertyTypeAlias, IEnumerable<string> tags, bool merge = false, string culture = null)
{
content.GetTagProperty(propertyTypeAlias).AssignTags(tags, merge);
content.GetTagProperty(propertyTypeAlias).AssignTags(tags, merge, culture);
}
/// <summary>
@@ -27,10 +27,10 @@ namespace Umbraco.Core.Models
/// <param name="content">The content item.</param>
/// <param name="propertyTypeAlias">The property alias.</param>
/// <param name="tags">The tags.</param>
/// <remarks>Tags do not support variants.</remarks>
public static void RemoveTags(this IContentBase content, string propertyTypeAlias, IEnumerable<string> tags)
/// <param name="culture">A culture, for multi-lingual properties.</param>
public static void RemoveTags(this IContentBase content, string propertyTypeAlias, IEnumerable<string> tags, string culture = null)
{
content.GetTagProperty(propertyTypeAlias).RemoveTags(tags);
content.GetTagProperty(propertyTypeAlias).RemoveTags(tags, culture);
}
// gets and validates the property
+6
View File
@@ -20,6 +20,12 @@ namespace Umbraco.Core.Models
[DataMember]
string Text { get; set; }
/// <summary>
/// Gets or sets the tag language.
/// </summary>
[DataMember]
int? LanguageId { get; set; }
/// <summary>
/// Gets the number of nodes tagged with this tag.
/// </summary>
@@ -38,13 +38,13 @@ namespace Umbraco.Core.Models
}
/// <summary>
/// Assign default tags.
/// Assign tags.
/// </summary>
/// <param name="property">The property.</param>
/// <param name="tags">The tags.</param>
/// <param name="merge">A value indicating whether to merge the tags with existing tags instead of replacing them.</param>
/// <remarks>Tags do not support variants.</remarks>
public static void AssignTags(this Property property, IEnumerable<string> tags, bool merge = false)
/// <param name="culture">A culture, for multi-lingual properties.</param>
public static void AssignTags(this Property property, IEnumerable<string> 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<string> tags, bool merge, TagsStorageType storageType, char delimiter)
private static void AssignTags(this Property property, IEnumerable<string> 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;
}
}
}
/// <summary>
/// Removes default tags.
/// Removes tags.
/// </summary>
/// <param name="property">The property.</param>
/// <param name="tags">The tags.</param>
/// <remarks>Tags do not support variants.</remarks>
public static void RemoveTags(this Property property, IEnumerable<string> tags)
/// <param name="culture">A culture, for multi-lingual properties.</param>
public static void RemoveTags(this Property property, IEnumerable<string> 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<string> tags, TagsStorageType storageType, char delimiter)
private static void RemoveTags(this Property property, IEnumerable<string> 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<string> GetTagsValue(this Property property)
// used by ContentRepositoryBase
internal static IEnumerable<string> 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<string> GetTagsValue(this Property property, TagsStorageType storageType, char delimiter)
private static IEnumerable<string> 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<string>();
switch (storageType)
@@ -158,7 +157,6 @@ namespace Umbraco.Core.Models
case TagsStorageType.Json:
try
{
//fixme doesn't take into account variants
return JsonConvert.DeserializeObject<JArray>(value).Select(x => x.ToString().Trim());
}
catch (JsonException)
@@ -178,34 +176,33 @@ namespace Umbraco.Core.Models
/// <param name="property">The property.</param>
/// <param name="value">The property value.</param>
/// <param name="tagConfiguration">The datatype configuration.</param>
/// <remarks>
/// <param name="culture">A culture, for multi-lingual properties.</param>
/// <remarks>
/// <para>The value is either a string (delimited string) or an enumeration of strings (tag list).</para>
/// <para>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.</para>
/// </remarks>
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<string>();
// if value is already an enumeration of strings, just use it
if (value is IEnumerable<string> 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<IEnumerable<string>>(value.ToString());
property.AssignTags(tags3 ?? Enumerable.Empty<string>(), merge, storageType, delimiter);
property.AssignTags(tags3 ?? Enumerable.Empty<string>(), false, storageType, delimiter, culture);
}
catch (Exception ex)
{
@@ -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).</para>
/// </remarks>
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?
@@ -109,7 +109,7 @@ namespace Umbraco.Core.Models.PublishedContent
public virtual PublishedItemType ItemType => _content.ItemType;
/// <inheritdoc />
public virtual bool IsDraft => _content.IsDraft;
public virtual bool IsDraft(string culture = null) => _content.IsDraft(culture);
#endregion
+11 -1
View File
@@ -16,6 +16,7 @@ namespace Umbraco.Core.Models
private string _group;
private string _text;
private int? _languageId;
/// <summary>
/// Initializes a new instance of the <see cref="Tag"/> class.
@@ -26,11 +27,12 @@ namespace Umbraco.Core.Models
/// <summary>
/// Initializes a new instance of the <see cref="Tag"/> class.
/// </summary>
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<Tag, string>(x => x.Group);
public readonly PropertyInfo Text = ExpressionHelper.GetPropertyInfo<Tag, string>(x => x.Text);
public readonly PropertyInfo LanguageId = ExpressionHelper.GetPropertyInfo<Tag, int?>(x => x.LanguageId);
}
/// <inheritdoc />
@@ -55,6 +58,13 @@ namespace Umbraco.Core.Models
set => SetPropertyValueAndDetectChanges(value, ref _text, Selectors.Text);
}
/// <inheritdoc />
public int? LanguageId
{
get => _languageId;
set => SetPropertyValueAndDetectChanges(value, ref _languageId, Selectors.LanguageId);
}
/// <inheritdoc />
public int NodeCount { get; internal set; }
}
+9 -6
View File
@@ -5,10 +5,13 @@ namespace Umbraco.Core.Models
/// <summary>
/// Represents a tagged entity.
/// </summary>
/// <remarks>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.</remarks>
/// <remarks>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.</remarks>
public class TaggedEntity
{
/// <summary>
/// Initializes a new instance of the <see cref="TaggedEntity"/> class.
/// </summary>
public TaggedEntity(int entityId, IEnumerable<TaggedProperty> taggedProperties)
{
EntityId = entityId;
@@ -16,13 +19,13 @@ namespace Umbraco.Core.Models
}
/// <summary>
/// Id of the entity, which is tagged
/// Gets the identifier of the entity.
/// </summary>
public int EntityId { get; private set; }
public int EntityId { get; }
/// <summary>
/// An enumerable list of tagged properties
/// Gets the tagged properties.
/// </summary>
public IEnumerable<TaggedProperty> TaggedProperties { get; private set; }
public IEnumerable<TaggedProperty> TaggedProperties { get; }
}
}
+9 -6
View File
@@ -7,6 +7,9 @@ namespace Umbraco.Core.Models
/// </summary>
public class TaggedProperty
{
/// <summary>
/// Initializes a new instance of the <see cref="TaggedProperty"/> class.
/// </summary>
public TaggedProperty(int propertyTypeId, string propertyTypeAlias, IEnumerable<ITag> tags)
{
PropertyTypeId = propertyTypeId;
@@ -15,18 +18,18 @@ namespace Umbraco.Core.Models
}
/// <summary>
/// Id of the PropertyType, which this tagged property is based on
/// Gets the identifier of the property type.
/// </summary>
public int PropertyTypeId { get; private set; }
public int PropertyTypeId { get; }
/// <summary>
/// Alias of the PropertyType, which this tagged property is based on
/// Gets the alias of the property type.
/// </summary>
public string PropertyTypeAlias { get; private set; }
public string PropertyTypeAlias { get; }
/// <summary>
/// An enumerable list of Tags for the property
/// Gets the tags.
/// </summary>
public IEnumerable<ITag> Tags { get; private set; }
public IEnumerable<ITag> Tags { get; }
}
}
+2 -1
View File
@@ -595,7 +595,6 @@ namespace Umbraco.Core
return null;
}
/// <summary>
/// Attempts to serialize the value to an XmlString using ToXmlString
/// </summary>
@@ -788,5 +787,7 @@ namespace Umbraco.Core
return BoolConvertCache[type] = false;
}
}
}
+10 -2
View File
@@ -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")]
@@ -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")]
@@ -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
};
}
@@ -23,6 +23,7 @@ namespace Umbraco.Core.Persistence.Mappers
CacheMap<Tag, TagDto>(src => src.Id, dto => dto.Id);
CacheMap<Tag, TagDto>(src => src.Text, dto => dto.Text);
CacheMap<Tag, TagDto>(src => src.Group, dto => dto.Group);
CacheMap<Tag, TagDto>(src => src.LanguageId, dto => dto.LanguageId);
}
}
}
@@ -76,7 +76,7 @@ namespace Umbraco.Core.Persistence
var (s, a) = sql.SqlContext.VisitDto(predicate, alias);
return sql.Where(s, a);
}
/// <summary>
/// Appends a WHERE clause to the Sql statement.
/// </summary>
@@ -589,11 +589,14 @@ namespace Umbraco.Core.Persistence
/// Creates a SELECT COUNT(*) Sql statement.
/// </summary>
/// <param name="sql">The origin sql.</param>
/// <param name="alias">An optional alias.</param>
/// <returns>The Sql statement.</returns>
public static Sql<ISqlContext> SelectCount(this Sql<ISqlContext> sql)
public static Sql<ISqlContext> SelectCount(this Sql<ISqlContext> 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);
}
/// <summary>
@@ -607,13 +610,29 @@ namespace Umbraco.Core.Persistence
/// <para>If <paramref name="fields"/> is empty, all columns are counted.</para>
/// </remarks>
public static Sql<ISqlContext> SelectCount<TDto>(this Sql<ISqlContext> sql, params Expression<Func<TDto, object>>[] fields)
=> sql.SelectCount(null, fields);
/// <summary>
/// Creates a SELECT COUNT Sql statement.
/// </summary>
/// <typeparam name="TDto">The type of the DTO to count.</typeparam>
/// <param name="sql">The origin sql.</param>
/// <param name="alias">An alias.</param>
/// <param name="fields">Expressions indicating the columns to count.</param>
/// <returns>The Sql statement.</returns>
/// <remarks>
/// <para>If <paramref name="fields"/> is empty, all columns are counted.</para>
/// </remarks>
public static Sql<ISqlContext> SelectCount<TDto>(this Sql<ISqlContext> sql, string alias, params Expression<Func<TDto, object>>[] fields)
{
if (sql == null) throw new ArgumentNullException(nameof(sql));
var sqlSyntax = sql.SqlContext.SqlSyntax;
var columns = fields.Length == 0
? sql.GetColumns<TDto>(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);
}
/// <summary>
@@ -643,6 +662,26 @@ namespace Umbraco.Core.Persistence
return sql.Select(sql.GetColumns(columnExpressions: fields));
}
/// <summary>
/// Creates a SELECT DISTINCT Sql statement.
/// </summary>
/// <typeparam name="TDto">The type of the DTO to select.</typeparam>
/// <param name="sql">The origin sql.</param>
/// <param name="fields">Expressions indicating the columns to select.</param>
/// <returns>The Sql statement.</returns>
/// <remarks>
/// <para>If <paramref name="fields"/> is empty, all columns are selected.</para>
/// </remarks>
public static Sql<ISqlContext> SelectDistinct<TDto>(this Sql<ISqlContext> sql, params Expression<Func<TDto, object>>[] 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]);
/// <summary>
/// Creates a SELECT Sql statement.
/// </summary>
@@ -705,6 +744,56 @@ namespace Umbraco.Core.Persistence
return sql.Append(", " + string.Join(", ", sql.GetColumns(tableAlias: tableAlias, columnExpressions: fields)));
}
/// <summary>
/// Adds a COUNT(*) to a SELECT Sql statement.
/// </summary>
/// <param name="sql">The origin sql.</param>
/// <param name="alias">An optional alias.</param>
/// <returns>The Sql statement.</returns>
public static Sql<ISqlContext> AndSelectCount(this Sql<ISqlContext> 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);
}
/// <summary>
/// Adds a COUNT to a SELECT Sql statement.
/// </summary>
/// <typeparam name="TDto">The type of the DTO to count.</typeparam>
/// <param name="sql">The origin sql.</param>
/// <param name="fields">Expressions indicating the columns to count.</param>
/// <returns>The Sql statement.</returns>
/// <remarks>
/// <para>If <paramref name="fields"/> is empty, all columns are counted.</para>
/// </remarks>
public static Sql<ISqlContext> AndSelectCount<TDto>(this Sql<ISqlContext> sql, params Expression<Func<TDto, object>>[] fields)
=> sql.AndSelectCount(null, fields);
/// <summary>
/// Adds a COUNT to a SELECT Sql statement.
/// </summary>
/// <typeparam name="TDto">The type of the DTO to count.</typeparam>
/// <param name="sql">The origin sql.</param>
/// <param name="alias">An alias.</param>
/// <param name="fields">Expressions indicating the columns to count.</param>
/// <returns>The Sql statement.</returns>
/// <remarks>
/// <para>If <paramref name="fields"/> is empty, all columns are counted.</para>
/// </remarks>
public static Sql<ISqlContext> AndSelectCount<TDto>(this Sql<ISqlContext> sql, string alias = null, params Expression<Func<TDto, object>>[] fields)
{
if (sql == null) throw new ArgumentNullException(nameof(sql));
var sqlSyntax = sql.SqlContext.SqlSyntax;
var columns = fields.Length == 0
? sql.GetColumns<TDto>(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);
}
/// <summary>
/// Creates a SELECT Sql statement with a referenced Dto.
/// </summary>
@@ -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
@@ -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);
@@ -9,6 +9,21 @@ namespace Umbraco.Core.Persistence.Querying
/// </summary>
internal static class SqlExpressionExtensions
{
/// <summary>
/// Indicates whether two nullable values are equal, substituting a fallback value for nulls.
/// </summary>
/// <typeparam name="T">The nullable type.</typeparam>
/// <param name="value">The value to compare.</param>
/// <param name="other">The value to compare to.</param>
/// <param name="fallbackValue">The value to use when any value is null.</param>
/// <remarks>Do not use outside of Sql expressions.</remarks>
// see usage in ExpressionVisitorBase
public static bool SqlNullableEquals<T>(this T? value, T? other, T fallbackValue)
where T : struct
{
return (value ?? fallbackValue).Equals(other ?? fallbackValue);
}
public static bool SqlIn<T>(this IEnumerable<T> collection, T item)
{
return collection.Contains(item);
@@ -19,7 +19,8 @@ namespace Umbraco.Core.Persistence.Repositories
/// <para>When <paramref name="replaceTags"/> is false, the tags specified in <paramref name="tags"/> are added to those already assigned.</para>
/// <para>When <paramref name="tags"/> is empty and <paramref name="replaceTags"/> is true, all assigned tags are removed.</para>
/// </remarks>
void Assign(int contentId, int propertyTypeId, IEnumerable<ITag> tags, bool replaceTags);
// TODO: replaceTags is used as 'false' in tests exclusively - should get rid of it
void Assign(int contentId, int propertyTypeId, IEnumerable<ITag> tags, bool replaceTags = true);
/// <summary>
/// Removes assigned tags from a content property.
@@ -46,54 +47,48 @@ namespace Umbraco.Core.Persistence.Repositories
#region Queries
/// <summary>
/// Gets a tagged entity.
/// </summary>
TaggedEntity GetTaggedEntityByKey(Guid key);
/// <summary>
/// Gets a tagged entity.
/// </summary>
TaggedEntity GetTaggedEntityById(int id);
IEnumerable<TaggedEntity> GetTaggedEntitiesByTagGroup(TaggableObjectTypes objectType, string tagGroup);
IEnumerable<TaggedEntity> GetTaggedEntitiesByTag(TaggableObjectTypes objectType, string tag, string tagGroup = null);
/// Gets all entities of a type, tagged with any tag in the specified group.
IEnumerable<TaggedEntity> GetTaggedEntitiesByTagGroup(TaggableObjectTypes objectType, string group, string culture = null);
/// <summary>
/// Returns all tags for an entity type (content/media/member)
/// Gets all entities of a type, tagged with the specified tag.
/// </summary>
/// <param name="objectType">Entity type</param>
/// <param name="group">Optional group</param>
/// <returns></returns>
IEnumerable<ITag> GetTagsForEntityType(TaggableObjectTypes objectType, string group = null);
IEnumerable<TaggedEntity> GetTaggedEntitiesByTag(TaggableObjectTypes objectType, string tag, string group = null, string culture = null);
/// <summary>
/// Returns all tags that exist on the content item - Content/Media/Member
/// Gets all tags for an entity type.
/// </summary>
/// <param name="contentId">The content item id to get tags for</param>
/// <param name="group">Optional group</param>
/// <returns></returns>
IEnumerable<ITag> GetTagsForEntity(int contentId, string group = null);
IEnumerable<ITag> GetTagsForEntityType(TaggableObjectTypes objectType, string group = null, string culture = null);
/// <summary>
/// Returns all tags that exist on the content item - Content/Media/Member
/// Gets all tags attached to an entity.
/// </summary>
/// <param name="contentId">The content item id to get tags for</param>
/// <param name="group">Optional group</param>
/// <returns></returns>
IEnumerable<ITag> GetTagsForEntity(Guid contentId, string group = null);
IEnumerable<ITag> GetTagsForEntity(int contentId, string group = null, string culture = null);
/// <summary>
/// Returns all tags that exist on the content item for the property specified - Content/Media/Member
/// Gets all tags attached to an entity.
/// </summary>
/// <param name="contentId">The content item id to get tags for</param>
/// <param name="propertyTypeAlias">The property alias to get tags for</param>
/// <param name="group">Optional group</param>
/// <returns></returns>
IEnumerable<ITag> GetTagsForProperty(int contentId, string propertyTypeAlias, string group = null);
IEnumerable<ITag> GetTagsForEntity(Guid contentId, string group = null, string culture = null);
/// <summary>
/// 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.
/// </summary>
/// <param name="contentId">The content item id to get tags for</param>
/// <param name="propertyTypeAlias">The property alias to get tags for</param>
/// <param name="group">Optional group</param>
/// <returns></returns>
IEnumerable<ITag> GetTagsForProperty(Guid contentId, string propertyTypeAlias, string group = null);
IEnumerable<ITag> GetTagsForProperty(int contentId, string propertyTypeAlias, string group = null, string culture = null);
/// <summary>
/// Gets all tags attached to an entity via a property.
/// </summary>
IEnumerable<ITag> GetTagsForProperty(Guid contentId, string propertyTypeAlias, string group = null, string culture = null);
#endregion
}
@@ -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<ITag>();
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)
@@ -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<int> propertyTypeIds, IReadOnlyCollection<int> 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<TagDto>(x => x.Id)
.From<TagDto>()
.InnerJoin<TagRelationshipDto>().On<TagDto, TagRelationshipDto>((tag, rel) => tag.Id == rel.TagId);
if (contentTypeIds != null)
sqlSelectTagsToDelete
.InnerJoin<ContentDto>().On<TagRelationshipDto, ContentDto>((rel, content) => rel.NodeId == content.NodeId)
.WhereIn<ContentDto>(x => x.ContentTypeId, contentTypeIds);
sqlSelectTagsToDelete
.WhereIn<TagRelationshipDto>(x => x.PropertyTypeId, propertyTypeIds)
.Where<TagDto>(x => x.LanguageId.SqlNullableEquals(targetLanguageId, -1));
var sqlDeleteRelations = Sql()
.Delete<TagRelationshipDto>()
.WhereIn<TagRelationshipDto>(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<TagDto>()
.WhereIn<TagDto>(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<TagDto>(x => x.Text, x => x.Group)
.Append(", " + targetLanguageIdS)
.From<TagDto>();
sqlSelectTagsToInsert
.InnerJoin<TagRelationshipDto>().On<TagDto, TagRelationshipDto>((tag, rel) => tag.Id == rel.TagId)
.LeftJoin<TagDto>("xtags").On<TagDto, TagDto>((tag, xtag) => tag.Text == xtag.Text && tag.Group == xtag.Group && xtag.LanguageId.SqlNullableEquals(targetLanguageId, -1), aliasRight: "xtags");
if (contentTypeIds != null)
sqlSelectTagsToInsert
.InnerJoin<ContentDto>().On<TagRelationshipDto, ContentDto>((rel, content) => rel.NodeId == content.NodeId)
.WhereIn<ContentDto>(x => x.ContentTypeId, contentTypeIds);
sqlSelectTagsToInsert
.WhereIn<TagRelationshipDto>(x => x.PropertyTypeId, propertyTypeIds)
.WhereNull<TagDto>(x => x.Id, "xtags") // ie, not exists
.Where<TagDto>(x => x.LanguageId.SqlNullableEquals(sourceLanguageId, -1));
var cols = Sql().Columns<TagDto>(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<TagRelationshipDto>(x => x.NodeId, x => x.PropertyTypeId)
.AndSelect<TagDto>("otag", x => x.Id)
.From<TagRelationshipDto>()
.InnerJoin<TagDto>().On<TagRelationshipDto, TagDto>((rel, tag) => rel.TagId == tag.Id)
.InnerJoin<TagDto>("otag").On<TagDto, TagDto>((tag, otag) => tag.Text == otag.Text && tag.Group == otag.Group && otag.LanguageId.SqlNullableEquals(targetLanguageId, -1), aliasRight: "otag");
if (contentTypeIds != null)
sqlSelectRelationsToInsert
.InnerJoin<ContentDto>().On<TagRelationshipDto, ContentDto>((rel, content) => rel.NodeId == content.NodeId)
.WhereIn<ContentDto>(x => x.ContentTypeId, contentTypeIds);
sqlSelectRelationsToInsert
.Where<TagDto>(x => x.LanguageId.SqlNullableEquals(sourceLanguageId, -1))
.WhereIn<TagRelationshipDto>(x => x.PropertyTypeId, propertyTypeIds);
var relationColumnsToInsert = Sql().Columns<TagRelationshipDto>(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<TagDto>(x => x.Id)
.From<TagDto>()
.InnerJoin<TagRelationshipDto>().On<TagDto, TagRelationshipDto>((tag, rel) => tag.Id == rel.TagId);
if (contentTypeIds != null)
sqlSelectTagsToDelete
.InnerJoin<ContentDto>().On<TagRelationshipDto, ContentDto>((rel, content) => rel.NodeId == content.NodeId)
.WhereIn<ContentDto>(x => x.ContentTypeId, contentTypeIds);
sqlSelectTagsToDelete
.WhereIn<TagRelationshipDto>(x => x.PropertyTypeId, propertyTypeIds)
.Where<TagDto>(x => !x.LanguageId.SqlNullableEquals(targetLanguageId, -1));
sqlDeleteRelations = Sql()
.Delete<TagRelationshipDto>()
.WhereIn<TagRelationshipDto>(x => x.TagId, sqlSelectTagsToDelete);
Database.Execute(sqlDeleteRelations);
// no
/*
var sqlDeleteTag = Sql()
.Delete<TagDto>()
.WhereIn<TagDto>(x => x.Id, sqlTagToDelete);
Database.Execute(sqlDeleteTag);
*/
}
/// <summary>
/// Copies property data from one language to another.
/// </summary>
@@ -766,6 +901,8 @@ AND umbracoNode.id <> @id",
/// <param name="contentTypeIds">The content type identifiers.</param>
private void CopyPropertyData(int? sourceLanguageId, int? targetLanguageId, IReadOnlyCollection<int> propertyTypeIds, IReadOnlyCollection<int> 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<PropertyDataDto>(x => x.VersionId, inSql);
}
// NPoco cannot turn the clause into IS NULL with a nullable parameter - deal with it
if (targetLanguageId == null)
sqlDelete.Where<PropertyDataDto>(x => x.LanguageId == null);
else
sqlDelete.Where<PropertyDataDto>(x => x.LanguageId == targetLanguageId);
sqlDelete.Where<PropertyDataDto>(x => x.LanguageId.SqlNullableEquals(targetLanguageId, -1));
sqlDelete
.WhereIn<PropertyDataDto>(x => x.PropertyTypeId, propertyTypeIds);
@@ -821,11 +954,7 @@ AND umbracoNode.id <> @id",
.InnerJoin<ContentVersionDto>().On<PropertyDataDto, ContentVersionDto>((pdata, cversion) => pdata.VersionId == cversion.Id)
.InnerJoin<ContentDto>().On<ContentVersionDto, ContentDto>((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<PropertyDataDto>(x => x.LanguageId == null);
else
sqlSelectData.Where<PropertyDataDto>(x => x.LanguageId == sourceLanguageId);
sqlSelectData.Where<PropertyDataDto>(x => x.LanguageId.SqlNullableEquals(sourceLanguageId, -1));
sqlSelectData
.WhereIn<PropertyDataDto>(x => x.PropertyTypeId, propertyTypeIds);
@@ -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;
}
@@ -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
/// <inheritdoc />
public void Assign(int contentId, int propertyTypeId, IEnumerable<ITag> tags, bool replaceTags)
// only invoked from ContentRepositoryBase with all cultures + replaceTags being true
public void Assign(int contentId, int propertyTypeId, IEnumerable<ITag> 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<TagRelationshipDto>().Where<TagRelationshipDto>(x => x.NodeId == contentId && x.PropertyTypeId == propertyTypeId);
Database.Execute(sql0);
}
// nothing else to do
return;
var sql0 = Sql().Delete<TagRelationshipDto>().Where<TagRelationshipDto>(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<TagRelationshipDto>().Where<TagRelationshipDto>(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);
}
/// <inheritdoc />
// only invoked from tests
public void Remove(int contentId, int propertyTypeId, IEnumerable<ITag> 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<ITag> 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
/// <inheritdoc />
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<TagDto>()
.InnerJoin<TagRelationshipDto>()
.On<TagRelationshipDto, TagDto>(left => left.TagId, right => right.Id)
.InnerJoin<ContentDto>()
.On<ContentDto, TagRelationshipDto>(left => left.NodeId, right => right.NodeId)
.InnerJoin<PropertyTypeDto>()
.On<PropertyTypeDto, TagRelationshipDto>(left => left.Id, right => right.PropertyTypeId)
.InnerJoin<NodeDto>()
.On<NodeDto, ContentDto>(left => left.NodeId, right => right.NodeId)
var sql = GetTaggedEntitiesSql(TaggableObjectTypes.All, "*");
sql = sql
.Where<NodeDto>(dto => dto.UniqueId == key);
return CreateTaggedEntityCollection(Database.Fetch<dynamic>(sql)).FirstOrDefault();
return Map(Database.Fetch<TaggedEntityDto>(sql)).FirstOrDefault();
}
/// <inheritdoc />
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<TagDto>()
.InnerJoin<TagRelationshipDto>()
.On<TagRelationshipDto, TagDto>(left => left.TagId, right => right.Id)
.InnerJoin<ContentDto>()
.On<ContentDto, TagRelationshipDto>(left => left.NodeId, right => right.NodeId)
.InnerJoin<PropertyTypeDto>()
.On<PropertyTypeDto, TagRelationshipDto>(left => left.Id, right => right.PropertyTypeId)
.InnerJoin<NodeDto>()
.On<NodeDto, ContentDto>(left => left.NodeId, right => right.NodeId)
var sql = GetTaggedEntitiesSql(TaggableObjectTypes.All, "*");
sql = sql
.Where<NodeDto>(dto => dto.NodeId == id);
return CreateTaggedEntityCollection(Database.Fetch<dynamic>(sql)).FirstOrDefault();
return Map(Database.Fetch<TaggedEntityDto>(sql)).FirstOrDefault();
}
public IEnumerable<TaggedEntity> GetTaggedEntitiesByTagGroup(TaggableObjectTypes objectType, string tagGroup)
/// <inheritdoc />
public IEnumerable<TaggedEntity> 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<TagDto>()
.InnerJoin<TagRelationshipDto>()
.On<TagRelationshipDto, TagDto>(left => left.TagId, right => right.Id)
.InnerJoin<ContentDto>()
.On<ContentDto, TagRelationshipDto>(left => left.NodeId, right => right.NodeId)
.InnerJoin<PropertyTypeDto>()
.On<PropertyTypeDto, TagRelationshipDto>(left => left.Id, right => right.PropertyTypeId)
.InnerJoin<NodeDto>()
.On<NodeDto, ContentDto>(left => left.NodeId, right => right.NodeId)
.Where<TagDto>(dto => dto.Group == tagGroup);
var sql = GetTaggedEntitiesSql(objectType, culture);
if (objectType != TaggableObjectTypes.All)
{
var nodeObjectType = GetNodeObjectType(objectType);
sql = sql
.Where<NodeDto>(dto => dto.NodeObjectType == nodeObjectType);
}
sql = sql
.Where<TagDto>(x => x.Group == group);
return CreateTaggedEntityCollection(
Database.Fetch<dynamic>(sql));
return Map(Database.Fetch<TaggedEntityDto>(sql));
}
public IEnumerable<TaggedEntity> GetTaggedEntitiesByTag(TaggableObjectTypes objectType, string tag, string tagGroup = null)
/// <inheritdoc />
public IEnumerable<TaggedEntity> 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<TagDto>()
.InnerJoin<TagRelationshipDto>()
.On<TagRelationshipDto, TagDto>(left => left.TagId, right => right.Id)
.InnerJoin<ContentDto>()
.On<ContentDto, TagRelationshipDto>(left => left.NodeId, right => right.NodeId)
.InnerJoin<PropertyTypeDto>()
.On<PropertyTypeDto, TagRelationshipDto>(left => left.Id, right => right.PropertyTypeId)
.InnerJoin<NodeDto>()
.On<NodeDto, ContentDto>(left => left.NodeId, right => right.NodeId)
var sql = GetTaggedEntitiesSql(objectType, culture);
sql = sql
.Where<TagDto>(dto => dto.Text == tag);
if (group.IsNullOrWhiteSpace() == false)
sql = sql
.Where<TagDto>(dto => dto.Group == group);
return Map(Database.Fetch<TaggedEntityDto>(sql));
}
private Sql<ISqlContext> GetTaggedEntitiesSql(TaggableObjectTypes objectType, string culture)
{
var sql = Sql()
.Select<TagRelationshipDto>(x => Alias(x.NodeId, "NodeId"))
.AndSelect<PropertyTypeDto>(x => Alias(x.Alias, "PropertyTypeAlias"), x => Alias(x.Id, "PropertyTypeId"))
.AndSelect<TagDto>(x => Alias(x.Id, "TagId"), x => Alias(x.Text, "TagText"), x => Alias(x.Group, "TagGroup"), x => Alias(x.LanguageId, "TagLanguage"))
.From<TagDto>()
.InnerJoin<TagRelationshipDto>().On<TagDto, TagRelationshipDto>((tag, rel) => tag.Id == rel.TagId)
.InnerJoin<ContentDto>().On<TagRelationshipDto, ContentDto>((rel, content) => rel.NodeId == content.NodeId)
.InnerJoin<PropertyTypeDto>().On<TagRelationshipDto, PropertyTypeDto>((rel, prop) => rel.PropertyTypeId == prop.Id)
.InnerJoin<NodeDto>().On<ContentDto, NodeDto>((content, node) => content.NodeId == node.NodeId);
if (culture == null)
{
sql = sql
.Where<TagDto>(dto => dto.LanguageId == null);
}
else if (culture != "*")
{
sql = sql
.InnerJoin<LanguageDto>().On<TagDto, LanguageDto>((tag, lang) => tag.LanguageId == lang.Id)
.Where<LanguageDto>(x => x.IsoCode == culture);
}
if (objectType != TaggableObjectTypes.All)
{
var nodeObjectType = GetNodeObjectType(objectType);
sql = sql
.Where<NodeDto>(dto => dto.NodeObjectType == nodeObjectType);
sql = sql.Where<NodeDto>(dto => dto.NodeObjectType == nodeObjectType);
}
if (tagGroup.IsNullOrWhiteSpace() == false)
{
sql = sql.Where<TagDto>(dto => dto.Group == tagGroup);
}
return CreateTaggedEntityCollection(
Database.Fetch<dynamic>(sql));
return sql;
}
private IEnumerable<TaggedEntity> CreateTaggedEntityCollection(IEnumerable<dynamic> dbResult)
private static IEnumerable<TaggedEntity> Map(IEnumerable<TaggedEntityDto> dtos)
{
foreach (var node in dbResult.GroupBy(x => (int)x.nodeId))
return dtos.GroupBy(x => x.NodeId).Select(dtosForNode =>
{
var properties = new List<TaggedProperty>();
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<ITag> GetTagsForEntityType(TaggableObjectTypes objectType, string group = null)
/// <inheritdoc />
public IEnumerable<ITag> 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<NodeDto>(dto => dto.NodeObjectType == nodeObjectType);
}
sql = ApplyGroupFilterToTagsQuery(sql, group);
if (group.IsNullOrWhiteSpace() == false)
sql = sql
.Where<TagDto>(dto => dto.Group == group);
sql = ApplyGroupByToTagsQuery(sql);
sql = sql
.GroupBy<TagDto>(x => x.Id, x => x.Text, x => x.Group, x => x.LanguageId);
return ExecuteTagsQuery(sql);
}
public IEnumerable<ITag> GetTagsForEntity(int contentId, string group = null)
/// <inheritdoc />
public IEnumerable<ITag> 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<NodeDto>(dto => dto.NodeId == contentId);
sql = ApplyGroupFilterToTagsQuery(sql, group);
if (group.IsNullOrWhiteSpace() == false)
sql = sql
.Where<TagDto>(dto => dto.Group == group);
return ExecuteTagsQuery(sql);
}
public IEnumerable<ITag> GetTagsForEntity(Guid contentId, string group = null)
/// <inheritdoc />
public IEnumerable<ITag> 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<NodeDto>(dto => dto.UniqueId == contentId);
sql = ApplyGroupFilterToTagsQuery(sql, group);
if (group.IsNullOrWhiteSpace() == false)
sql = sql
.Where<TagDto>(dto => dto.Group == group);
return ExecuteTagsQuery(sql);
}
public IEnumerable<ITag> GetTagsForProperty(int contentId, string propertyTypeAlias, string group = null)
/// <inheritdoc />
public IEnumerable<ITag> GetTagsForProperty(int contentId, string propertyTypeAlias, string group = null, string culture = null)
{
var sql = GetTagsQuerySelect();
sql = ApplyRelationshipJoinToTagsQuery(sql);
var sql = GetTagsSql(culture);
sql = sql
.InnerJoin<PropertyTypeDto>()
.On<PropertyTypeDto, TagRelationshipDto>(left => left.Id, right => right.PropertyTypeId)
.Where<NodeDto>(dto => dto.NodeId == contentId)
.Where<PropertyTypeDto>(dto => dto.Alias == propertyTypeAlias);
.InnerJoin<PropertyTypeDto>().On<PropertyTypeDto, TagRelationshipDto>((prop, rel) => prop.Id == rel.PropertyTypeId)
.Where<NodeDto>(x => x.NodeId == contentId)
.Where<PropertyTypeDto>(x => x.Alias == propertyTypeAlias);
sql = ApplyGroupFilterToTagsQuery(sql, group);
AddTagsSqlWhere(sql, culture);
if (group.IsNullOrWhiteSpace() == false)
sql = sql
.Where<TagDto>(dto => dto.Group == group);
return ExecuteTagsQuery(sql);
}
public IEnumerable<ITag> GetTagsForProperty(Guid contentId, string propertyTypeAlias, string group = null)
/// <inheritdoc />
public IEnumerable<ITag> GetTagsForProperty(Guid contentId, string propertyTypeAlias, string group = null, string culture = null)
{
var sql = GetTagsQuerySelect();
sql = ApplyRelationshipJoinToTagsQuery(sql);
var sql = GetTagsSql(culture);
sql = sql
.InnerJoin<PropertyTypeDto>()
.On<PropertyTypeDto, TagRelationshipDto>(left => left.Id, right => right.PropertyTypeId)
.InnerJoin<PropertyTypeDto>().On<PropertyTypeDto, TagRelationshipDto>((prop, rel) => prop.Id == rel.PropertyTypeId)
.Where<NodeDto>(dto => dto.UniqueId == contentId)
.Where<PropertyTypeDto>(dto => dto.Alias == propertyTypeAlias);
sql = ApplyGroupFilterToTagsQuery(sql, group);
AddTagsSqlWhere(sql, culture);
if (group.IsNullOrWhiteSpace() == false)
sql = sql
.Where<TagDto>(dto => dto.Group == group);
return ExecuteTagsQuery(sql);
}
private Sql<ISqlContext> GetTagsQuerySelect(bool withGrouping = false)
private Sql<ISqlContext> GetTagsSql(string culture, bool withGrouping = false)
{
var sql = Sql();
var sql = Sql()
.Select<TagDto>();
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<ISqlContext> ApplyRelationshipJoinToTagsQuery(Sql<ISqlContext> sql)
{
return sql
sql = sql
.From<TagDto>()
.InnerJoin<TagRelationshipDto>()
.On<TagRelationshipDto, TagDto>(left => left.TagId, right => right.Id)
.InnerJoin<ContentDto>()
.On<ContentDto, TagRelationshipDto>(left => left.NodeId, right => right.NodeId)
.InnerJoin<NodeDto>()
.On<NodeDto, ContentDto>(left => left.NodeId, right => right.NodeId);
}
.InnerJoin<TagRelationshipDto>().On<TagRelationshipDto, TagDto>((rel, tag) => tag.Id == rel.TagId)
.InnerJoin<ContentDto>().On<ContentDto, TagRelationshipDto>((content, rel) => content.NodeId == rel.NodeId)
.InnerJoin<NodeDto>().On<NodeDto, ContentDto>((node, content) => node.NodeId == content.NodeId);
private Sql<ISqlContext> ApplyGroupFilterToTagsQuery(Sql<ISqlContext> sql, string group)
{
if (group.IsNullOrWhiteSpace() == false)
if (culture != null && culture != "*")
{
sql = sql.Where<TagDto>(dto => dto.Group == group);
sql = sql
.InnerJoin<LanguageDto>().On<TagDto, LanguageDto>((tag, lang) => tag.LanguageId == lang.Id);
}
return sql;
}
private Sql<ISqlContext> ApplyGroupByToTagsQuery(Sql<ISqlContext> sql)
private Sql<ISqlContext> AddTagsSqlWhere(Sql<ISqlContext> sql, string culture)
{
return sql.GroupBy("cmsTags.id", "cmsTags.tag", "cmsTags." + SqlSyntax.GetQuotedColumnName("group") + @"");
if (culture == null)
{
sql = sql
.Where<TagDto>(dto => dto.LanguageId == null);
}
else if (culture != "*")
{
sql = sql
.Where<LanguageDto>(x => x.IsoCode == culture);
}
return sql;
}
private IEnumerable<ITag> ExecuteTagsQuery(Sql sql)
+3 -2
View File
@@ -95,9 +95,10 @@ namespace Umbraco.Core.Persistence
return new Sql<ISqlContext>(_sqlContext, isBuilt, _sql, args);
}
internal void WriteToConsole()
internal string ToText()
{
new Sql<ISqlContext>(_sqlContext, _sql, _args.Values.ToArray()).WriteToConsole();
var sql = new Sql<ISqlContext>(_sqlContext, _sql, _args.Values.ToArray());
return sql.ToText();
}
/// <summary>
+23 -16
View File
@@ -67,6 +67,24 @@ namespace Umbraco.Core.Persistence
/// <inheritdoc />
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)
@@ -9,7 +9,7 @@
/// Determines whether an editor supports tags.
/// </summary>
public static bool IsTagsEditor(this IDataEditor editor)
=> editor?.GetType().GetCustomAttribute<TagsPropertyEditorAttribute>(false) != null;
=> editor.GetTagAttribute() != null;
/// <summary>
/// Gets the tags configuration attribute of an editor.
@@ -0,0 +1,25 @@
using System;
using System.Collections;
using System.Collections.Generic;
using Newtonsoft.Json.Converters;
namespace Umbraco.Core.Serialization
{
/// <summary>
/// Marks dictionaries so they are deserialized as case-insensitive.
/// </summary>
/// <example>
/// [JsonConverter(typeof(CaseInsensitiveDictionaryConverter{PropertyData[]}))]
/// public Dictionary{string, PropertyData[]} PropertyData {{ get; set; }}
/// </example>
public class CaseInsensitiveDictionaryConverter<T> : CustomCreationConverter<IDictionary>
{
public override bool CanWrite => false;
public override bool CanRead => true;
public override bool CanConvert(Type objectType) => typeof(IDictionary<string,T>).IsAssignableFrom(objectType);
public override IDictionary Create(Type objectType) => new Dictionary<string, T>(StringComparer.OrdinalIgnoreCase);
}
}
+36 -87
View File
@@ -5,7 +5,7 @@ using Umbraco.Core.Models;
namespace Umbraco.Core.Services
{
/// <summary>
/// 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
/// </summary>
/// <remarks>
/// If there is unpublished content with tags, those tags will not be contained.
@@ -15,135 +15,84 @@ namespace Umbraco.Core.Services
/// </remarks>
public interface ITagService : IService
{
/// <summary>
/// Gets a tagged entity.
/// </summary>
TaggedEntity GetTaggedEntityById(int id);
/// <summary>
/// Gets a tagged entity.
/// </summary>
TaggedEntity GetTaggedEntityByKey(Guid key);
/// <summary>
/// Gets tagged Content by a specific 'Tag Group'.
/// Gets all documents tagged with any tag in the specified group.
/// </summary>
/// <remarks>The <see cref="TaggedEntity"/> contains the Id and Tags of the Content, not the actual Content item.</remarks>
/// <param name="tagGroup">Name of the 'Tag Group'</param>
/// <returns>An enumerable list of <see cref="TaggedEntity"/></returns>
IEnumerable<TaggedEntity> GetTaggedContentByTagGroup(string tagGroup);
IEnumerable<TaggedEntity> GetTaggedContentByTagGroup(string group, string culture = null);
/// <summary>
/// Gets tagged Content by a specific 'Tag' and optional 'Tag Group'.
/// Gets all documents tagged with the specified tag.
/// </summary>
/// <remarks>The <see cref="TaggedEntity"/> contains the Id and Tags of the Content, not the actual Content item.</remarks>
/// <param name="tag">Tag</param>
/// <param name="tagGroup">Optional name of the 'Tag Group'</param>
/// <returns>An enumerable list of <see cref="TaggedEntity"/></returns>
IEnumerable<TaggedEntity> GetTaggedContentByTag(string tag, string tagGroup = null);
IEnumerable<TaggedEntity> GetTaggedContentByTag(string tag, string group = null, string culture = null);
/// <summary>
/// Gets tagged Media by a specific 'Tag Group'.
/// Gets all media tagged with any tag in the specified group.
/// </summary>
/// <remarks>The <see cref="TaggedEntity"/> contains the Id and Tags of the Media, not the actual Media item.</remarks>
/// <param name="tagGroup">Name of the 'Tag Group'</param>
/// <returns>An enumerable list of <see cref="TaggedEntity"/></returns>
IEnumerable<TaggedEntity> GetTaggedMediaByTagGroup(string tagGroup);
IEnumerable<TaggedEntity> GetTaggedMediaByTagGroup(string group, string culture = null);
/// <summary>
/// Gets tagged Media by a specific 'Tag' and optional 'Tag Group'.
/// Gets all media tagged with the specified tag.
/// </summary>
/// <remarks>The <see cref="TaggedEntity"/> contains the Id and Tags of the Media, not the actual Media item.</remarks>
/// <param name="tag">Tag</param>
/// <param name="tagGroup">Optional name of the 'Tag Group'</param>
/// <returns>An enumerable list of <see cref="TaggedEntity"/></returns>
IEnumerable<TaggedEntity> GetTaggedMediaByTag(string tag, string tagGroup = null);
IEnumerable<TaggedEntity> GetTaggedMediaByTag(string tag, string group = null, string culture = null);
/// <summary>
/// Gets tagged Members by a specific 'Tag Group'.
/// Gets all members tagged with any tag in the specified group.
/// </summary>
/// <remarks>The <see cref="TaggedEntity"/> contains the Id and Tags of the Member, not the actual Member item.</remarks>
/// <param name="tagGroup">Name of the 'Tag Group'</param>
/// <returns>An enumerable list of <see cref="TaggedEntity"/></returns>
IEnumerable<TaggedEntity> GetTaggedMembersByTagGroup(string tagGroup);
IEnumerable<TaggedEntity> GetTaggedMembersByTagGroup(string group, string culture = null);
/// <summary>
/// Gets tagged Members by a specific 'Tag' and optional 'Tag Group'.
/// Gets all members tagged with the specified tag.
/// </summary>
/// <remarks>The <see cref="TaggedEntity"/> contains the Id and Tags of the Member, not the actual Member item.</remarks>
/// <param name="tag">Tag</param>
/// <param name="tagGroup">Optional name of the 'Tag Group'</param>
/// <returns>An enumerable list of <see cref="TaggedEntity"/></returns>
IEnumerable<TaggedEntity> GetTaggedMembersByTag(string tag, string tagGroup = null);
IEnumerable<TaggedEntity> GetTaggedMembersByTag(string tag, string group = null, string culture = null);
/// <summary>
/// Gets every tag stored in the database
/// Gets all tags.
/// </summary>
/// <param name="tagGroup">Optional name of the 'Tag Group'</param>
/// <returns>An enumerable list of <see cref="ITag"/></returns>
IEnumerable<ITag> GetAllTags(string tagGroup = null);
IEnumerable<ITag> GetAllTags(string group = null, string culture = null);
/// <summary>
/// Gets all tags for content items
/// Gets all document tags.
/// </summary>
/// <remarks>Use the optional tagGroup parameter to limit the
/// result to a specific 'Tag Group'.</remarks>
/// <param name="tagGroup">Optional name of the 'Tag Group'</param>
/// <returns>An enumerable list of <see cref="ITag"/></returns>
IEnumerable<ITag> GetAllContentTags(string tagGroup = null);
IEnumerable<ITag> GetAllContentTags(string group = null, string culture = null);
/// <summary>
/// Gets all tags for media items
/// Gets all media tags.
/// </summary>
/// <remarks>Use the optional tagGroup parameter to limit the
/// result to a specific 'Tag Group'.</remarks>
/// <param name="tagGroup">Optional name of the 'Tag Group'</param>
/// <returns>An enumerable list of <see cref="ITag"/></returns>
IEnumerable<ITag> GetAllMediaTags(string tagGroup = null);
IEnumerable<ITag> GetAllMediaTags(string group = null, string culture = null);
/// <summary>
/// Gets all tags for member items
/// Gets all member tags.
/// </summary>
/// <remarks>Use the optional tagGroup parameter to limit the
/// result to a specific 'Tag Group'.</remarks>
/// <param name="tagGroup">Optional name of the 'Tag Group'</param>
/// <returns>An enumerable list of <see cref="ITag"/></returns>
IEnumerable<ITag> GetAllMemberTags(string tagGroup = null);
IEnumerable<ITag> GetAllMemberTags(string group = null, string culture = null);
/// <summary>
/// Gets all tags attached to a property by entity id
/// Gets all tags attached to an entity via a property.
/// </summary>
/// <remarks>Use the optional tagGroup parameter to limit the
/// result to a specific 'Tag Group'.</remarks>
/// <param name="contentId">The content item id to get tags for</param>
/// <param name="propertyTypeAlias">Property type alias</param>
/// <param name="tagGroup">Optional name of the 'Tag Group'</param>
/// <returns>An enumerable list of <see cref="ITag"/></returns>
IEnumerable<ITag> GetTagsForProperty(int contentId, string propertyTypeAlias, string tagGroup = null);
IEnumerable<ITag> GetTagsForProperty(int contentId, string propertyTypeAlias, string group = null, string culture = null);
/// <summary>
/// Gets all tags attached to an entity (content, media or member) by entity id
/// Gets all tags attached to an entity.
/// </summary>
/// <remarks>Use the optional tagGroup parameter to limit the
/// result to a specific 'Tag Group'.</remarks>
/// <param name="contentId">The content item id to get tags for</param>
/// <param name="tagGroup">Optional name of the 'Tag Group'</param>
/// <returns>An enumerable list of <see cref="ITag"/></returns>
IEnumerable<ITag> GetTagsForEntity(int contentId, string tagGroup = null);
IEnumerable<ITag> GetTagsForEntity(int contentId, string group = null, string culture = null);
/// <summary>
/// Gets all tags attached to a property by entity id
/// Gets all tags attached to an entity via a property.
/// </summary>
/// <remarks>Use the optional tagGroup parameter to limit the
/// result to a specific 'Tag Group'.</remarks>
/// <param name="contentId">The content item id to get tags for</param>
/// <param name="propertyTypeAlias">Property type alias</param>
/// <param name="tagGroup">Optional name of the 'Tag Group'</param>
/// <returns>An enumerable list of <see cref="ITag"/></returns>
IEnumerable<ITag> GetTagsForProperty(Guid contentId, string propertyTypeAlias, string tagGroup = null);
IEnumerable<ITag> GetTagsForProperty(Guid contentId, string propertyTypeAlias, string group = null, string culture = null);
/// <summary>
/// Gets all tags attached to an entity (content, media or member) by entity id
/// Gets all tags attached to an entity.
/// </summary>
/// <remarks>Use the optional tagGroup parameter to limit the
/// result to a specific 'Tag Group'.</remarks>
/// <param name="contentId">The content item id to get tags for</param>
/// <param name="tagGroup">Optional name of the 'Tag Group'</param>
/// <returns>An enumerable list of <see cref="ITag"/></returns>
IEnumerable<ITag> GetTagsForEntity(Guid contentId, string tagGroup = null);
IEnumerable<ITag> GetTagsForEntity(Guid contentId, string group = null, string culture = null);
}
}
+60 -143
View File
@@ -25,230 +25,147 @@ namespace Umbraco.Core.Services.Implement
_tagRepository = tagRepository;
}
/// <inheritdoc />
public TaggedEntity GetTaggedEntityById(int id)
{
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
using (ScopeProvider.CreateScope(autoComplete: true))
{
return _tagRepository.GetTaggedEntityById(id);
}
}
/// <inheritdoc />
public TaggedEntity GetTaggedEntityByKey(Guid key)
{
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
using (ScopeProvider.CreateScope(autoComplete: true))
{
return _tagRepository.GetTaggedEntityByKey(key);
}
}
/// <summary>
/// Gets tagged Content by a specific 'Tag Group'.
/// </summary>
/// <remarks>The <see cref="TaggedEntity"/> contains the Id and Tags of the Content, not the actual Content item.</remarks>
/// <param name="tagGroup">Name of the 'Tag Group'</param>
/// <returns>An enumerable list of <see cref="TaggedEntity"/></returns>
public IEnumerable<TaggedEntity> GetTaggedContentByTagGroup(string tagGroup)
/// <inheritdoc />
public IEnumerable<TaggedEntity> 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);
}
}
/// <summary>
/// Gets tagged Content by a specific 'Tag' and optional 'Tag Group'.
/// </summary>
/// <remarks>The <see cref="TaggedEntity"/> contains the Id and Tags of the Content, not the actual Content item.</remarks>
/// <param name="tag">Tag</param>
/// <param name="tagGroup">Optional name of the 'Tag Group'</param>
/// <returns>An enumerable list of <see cref="TaggedEntity"/></returns>
public IEnumerable<TaggedEntity> GetTaggedContentByTag(string tag, string tagGroup = null)
/// <inheritdoc />
public IEnumerable<TaggedEntity> 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);
}
}
/// <summary>
/// Gets tagged Media by a specific 'Tag Group'.
/// </summary>
/// <remarks>The <see cref="TaggedEntity"/> contains the Id and Tags of the Media, not the actual Media item.</remarks>
/// <param name="tagGroup">Name of the 'Tag Group'</param>
/// <returns>An enumerable list of <see cref="TaggedEntity"/></returns>
public IEnumerable<TaggedEntity> GetTaggedMediaByTagGroup(string tagGroup)
/// <inheritdoc />
public IEnumerable<TaggedEntity> 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);
}
}
/// <summary>
/// Gets tagged Media by a specific 'Tag' and optional 'Tag Group'.
/// </summary>
/// <remarks>The <see cref="TaggedEntity"/> contains the Id and Tags of the Media, not the actual Media item.</remarks>
/// <param name="tag">Tag</param>
/// <param name="tagGroup">Optional name of the 'Tag Group'</param>
/// <returns>An enumerable list of <see cref="TaggedEntity"/></returns>
public IEnumerable<TaggedEntity> GetTaggedMediaByTag(string tag, string tagGroup = null)
/// <inheritdoc />
public IEnumerable<TaggedEntity> 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);
}
}
/// <summary>
/// Gets tagged Members by a specific 'Tag Group'.
/// </summary>
/// <remarks>The <see cref="TaggedEntity"/> contains the Id and Tags of the Member, not the actual Member item.</remarks>
/// <param name="tagGroup">Name of the 'Tag Group'</param>
/// <returns>An enumerable list of <see cref="TaggedEntity"/></returns>
public IEnumerable<TaggedEntity> GetTaggedMembersByTagGroup(string tagGroup)
/// <inheritdoc />
public IEnumerable<TaggedEntity> 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);
}
}
/// <summary>
/// Gets tagged Members by a specific 'Tag' and optional 'Tag Group'.
/// </summary>
/// <remarks>The <see cref="TaggedEntity"/> contains the Id and Tags of the Member, not the actual Member item.</remarks>
/// <param name="tag">Tag</param>
/// <param name="tagGroup">Optional name of the 'Tag Group'</param>
/// <returns>An enumerable list of <see cref="TaggedEntity"/></returns>
public IEnumerable<TaggedEntity> GetTaggedMembersByTag(string tag, string tagGroup = null)
/// <inheritdoc />
public IEnumerable<TaggedEntity> 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);
}
}
/// <summary>
/// Gets every tag stored in the database
/// </summary>
/// <param name="tagGroup">Optional name of the 'Tag Group'</param>
/// <returns>An enumerable list of <see cref="ITag"/></returns>
public IEnumerable<ITag> GetAllTags(string tagGroup = null)
/// <inheritdoc />
public IEnumerable<ITag> 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);
}
}
/// <summary>
/// Gets all tags for content items
/// </summary>
/// <remarks>Use the optional tagGroup parameter to limit the
/// result to a specific 'Tag Group'.</remarks>
/// <param name="tagGroup">Optional name of the 'Tag Group'</param>
/// <returns>An enumerable list of <see cref="ITag"/></returns>
public IEnumerable<ITag> GetAllContentTags(string tagGroup = null)
/// <inheritdoc />
public IEnumerable<ITag> 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);
}
}
/// <summary>
/// Gets all tags for media items
/// </summary>
/// <remarks>Use the optional tagGroup parameter to limit the
/// result to a specific 'Tag Group'.</remarks>
/// <param name="tagGroup">Optional name of the 'Tag Group'</param>
/// <returns>An enumerable list of <see cref="ITag"/></returns>
public IEnumerable<ITag> GetAllMediaTags(string tagGroup = null)
/// <inheritdoc />
public IEnumerable<ITag> 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);
}
}
/// <summary>
/// Gets all tags for member items
/// </summary>
/// <remarks>Use the optional tagGroup parameter to limit the
/// result to a specific 'Tag Group'.</remarks>
/// <param name="tagGroup">Optional name of the 'Tag Group'</param>
/// <returns>An enumerable list of <see cref="ITag"/></returns>
public IEnumerable<ITag> GetAllMemberTags(string tagGroup = null)
/// <inheritdoc />
public IEnumerable<ITag> 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);
}
}
/// <summary>
/// Gets all tags attached to a property by entity id
/// </summary>
/// <remarks>Use the optional tagGroup parameter to limit the
/// result to a specific 'Tag Group'.</remarks>
/// <param name="contentId">The content item id to get tags for</param>
/// <param name="propertyTypeAlias">Property type alias</param>
/// <param name="tagGroup">Optional name of the 'Tag Group'</param>
/// <returns>An enumerable list of <see cref="ITag"/></returns>
public IEnumerable<ITag> GetTagsForProperty(int contentId, string propertyTypeAlias, string tagGroup = null)
/// <inheritdoc />
public IEnumerable<ITag> 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);
}
}
/// <summary>
/// Gets all tags attached to an entity (content, media or member) by entity id
/// </summary>
/// <remarks>Use the optional tagGroup parameter to limit the
/// result to a specific 'Tag Group'.</remarks>
/// <param name="contentId">The content item id to get tags for</param>
/// <param name="tagGroup">Optional name of the 'Tag Group'</param>
/// <returns>An enumerable list of <see cref="ITag"/></returns>
public IEnumerable<ITag> GetTagsForEntity(int contentId, string tagGroup = null)
/// <inheritdoc />
public IEnumerable<ITag> 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);
}
}
/// <summary>
/// Gets all tags attached to a property by entity id
/// </summary>
/// <remarks>Use the optional tagGroup parameter to limit the
/// result to a specific 'Tag Group'.</remarks>
/// <param name="contentId">The content item id to get tags for</param>
/// <param name="propertyTypeAlias">Property type alias</param>
/// <param name="tagGroup">Optional name of the 'Tag Group'</param>
/// <returns>An enumerable list of <see cref="ITag"/></returns>
public IEnumerable<ITag> GetTagsForProperty(Guid contentId, string propertyTypeAlias, string tagGroup = null)
/// <inheritdoc />
public IEnumerable<ITag> 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);
}
}
/// <summary>
/// Gets all tags attached to an entity (content, media or member) by entity id
/// </summary>
/// <remarks>Use the optional tagGroup parameter to limit the
/// result to a specific 'Tag Group'.</remarks>
/// <param name="contentId">The content item id to get tags for</param>
/// <param name="tagGroup">Optional name of the 'Tag Group'</param>
/// <returns>An enumerable list of <see cref="ITag"/></returns>
public IEnumerable<ITag> GetTagsForEntity(Guid contentId, string tagGroup = null)
/// <inheritdoc />
public IEnumerable<ITag> 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);
}
}
}
+2
View File
@@ -380,6 +380,7 @@
<Compile Include="Migrations\Upgrade\V_8_0_0\DropTemplateDesignColumn.cs" />
<Compile Include="Migrations\Upgrade\V_8_0_0\FixLockTablePrimaryKey.cs" />
<Compile Include="Migrations\Upgrade\V_8_0_0\LanguageColumns.cs" />
<Compile Include="Migrations\Upgrade\V_8_0_0\MakeTagsVariant.cs" />
<Compile Include="Migrations\Upgrade\V_8_0_0\PropertyEditorsMigration.cs" />
<Compile Include="Migrations\Upgrade\V_8_0_0\RefactorMacroColumns.cs" />
<Compile Include="Migrations\Upgrade\V_8_0_0\RefactorVariantsModel.cs" />
@@ -1322,6 +1323,7 @@
<Compile Include="Security\UserAwareMembershipProviderPasswordHasher.cs" />
<Compile Include="SemVersionExtensions.cs" />
<Compile Include="Serialization\AbstractSerializationService.cs" />
<Compile Include="Serialization\CaseInsensitiveDictionaryConverter.cs" />
<Compile Include="Serialization\ForceInt32Converter.cs" />
<Compile Include="Serialization\Formatter.cs" />
<Compile Include="Serialization\IFormatter.cs" />
+29
View File
@@ -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
/// </summary>
internal static class ExamineExtensions
{
/// <summary>
/// Forcibly unlocks all lucene based indexes
/// </summary>
/// <remarks>
/// This is not thread safe, use with care
/// </remarks>
internal static void UnlockLuceneIndexes(this IExamineManager examineManager, ILogger logger)
{
foreach (var luceneIndexer in examineManager.Indexes.OfType<LuceneIndex>())
{
//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);
}
}
}
/// <summary>
/// Checks if the index can be read/opened
/// </summary>
+13
View File
@@ -0,0 +1,13 @@
using System.Collections.Generic;
using Examine;
namespace Umbraco.Examine
{
/// <summary>
/// Creates <see cref="IIndex"/>'s
/// </summary>
public interface IIndexCreator
{
IEnumerable<IIndex> Create();
}
}
-3
View File
@@ -1,12 +1,9 @@
using System;
using System.Collections.Generic;
using Examine;
using Umbraco.Core;
namespace Umbraco.Examine
{
/// <summary>
/// Exposes diagnostic information about an index
/// </summary>
+5 -6
View File
@@ -5,18 +5,17 @@ using Umbraco.Core.Models;
namespace Umbraco.Examine
{
/// <summary>
/// Creates a collection of <see cref="ValueSet"/> to be indexed based on a collection of <see cref="TContent"/>
/// Creates a collection of <see cref="ValueSet"/> to be indexed based on a collection of <see cref="T"/>
/// </summary>
/// <typeparam name="TContent"></typeparam>
public interface IValueSetBuilder<in TContent>
where TContent : IContentBase
/// <typeparam name="T"></typeparam>
public interface IValueSetBuilder<in T>
{
/// <summary>
/// Creates a collection of <see cref="ValueSet"/> to be indexed based on a collection of <see cref="TContent"/>
/// Creates a collection of <see cref="ValueSet"/> to be indexed based on a collection of <see cref="T"/>
/// </summary>
/// <param name="content"></param>
/// <returns></returns>
IEnumerable<ValueSet> GetValueSets(params TContent[] content);
IEnumerable<ValueSet> GetValueSets(params T[] content);
}
}
+43
View File
@@ -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
{
/// <inheritdoc />
/// <summary>
/// Abstract class for creating Lucene based Indexes
/// </summary>
public abstract class LuceneIndexCreator : IIndexCreator
{
public abstract IEnumerable<IIndex> Create();
/// <summary>
/// Creates a file system based Lucene <see cref="Lucene.Net.Store.Directory"/> with the correct locking guidelines for Umbraco
/// </summary>
/// <param name="name"></param>
/// <returns></returns>
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;
}
}
}
@@ -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
@@ -66,6 +66,7 @@
<Compile Include="ExamineExtensions.cs" />
<Compile Include="IContentValueSetBuilder.cs" />
<Compile Include="IContentValueSetValidator.cs" />
<Compile Include="IIndexCreator.cs" />
<Compile Include="IIndexDiagnostics.cs" />
<Compile Include="IIndexPopulator.cs" />
<Compile Include="IndexPopulator.cs" />
@@ -88,6 +89,7 @@
<Compile Include="UmbracoExamineIndexDiagnostics.cs" />
<Compile Include="UmbracoExamineIndex.cs" />
<Compile Include="UmbracoExamineSearcher.cs" />
<Compile Include="LuceneIndexCreator.cs" />
<Compile Include="UmbracoMemberIndex.cs" />
<Compile Include="..\SolutionInfo.cs">
<Link>Properties\SolutionInfo.cs</Link>
@@ -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<Func<IFoo, Foo>>();
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()
{
@@ -90,7 +90,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="BenchmarkDotNet">
<Version>0.11.2</Version>
<Version>0.11.3</Version>
</PackageReference>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
@@ -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()
@@ -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<IKeyValueService>(), new PostMigrationCollection(Enumerable.Empty<IPostMigration>()), logger,
new MigrationPlan("test", builder, logger)
.Add<CreateTableOfTDtoMigration>(string.Empty, "done"));
var upgrader = new Upgrader(
new MigrationPlan("test")
.From(string.Empty)
.To<CreateTableOfTDtoMigration>("done"));
upgrader.Execute();
upgrader.Execute(ScopeProvider, builder, Mock.Of<IKeyValueService>(), 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<IKeyValueService>(), new PostMigrationCollection(Enumerable.Empty<IPostMigration>()), logger,
new MigrationPlan("test", builder, logger)
.Add<CreateTableOfTDtoMigration>(string.Empty, "a")
.Add<DeleteKeysAndIndexesMigration>("a", "done"));
var upgrader = new Upgrader(
new MigrationPlan("test")
.From(string.Empty)
.To<CreateTableOfTDtoMigration>("a")
.To<DeleteKeysAndIndexesMigration>("done"));
upgrader.Execute();
upgrader.Execute(ScopeProvider, builder, Mock.Of<IKeyValueService>(), 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<IKeyValueService>(), new PostMigrationCollection(Enumerable.Empty<IPostMigration>()), logger,
new MigrationPlan("test", builder, logger)
.Add<CreateTableOfTDtoMigration>(string.Empty, "a")
.Add<DeleteKeysAndIndexesMigration>("a", "b")
.Add<CreateKeysAndIndexesOfTDtoMigration>("b", "done"));
var upgrader = new Upgrader(
new MigrationPlan("test")
.From(string.Empty)
.To<CreateTableOfTDtoMigration>("a")
.To<DeleteKeysAndIndexesMigration>("b")
.To<CreateKeysAndIndexesOfTDtoMigration>("done"));
upgrader.Execute();
upgrader.Execute(ScopeProvider, builder, Mock.Of<IKeyValueService>(), 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<IKeyValueService>(), new PostMigrationCollection(Enumerable.Empty<IPostMigration>()), logger,
new MigrationPlan("test", builder, logger)
.Add<CreateTableOfTDtoMigration>(string.Empty, "a")
.Add<DeleteKeysAndIndexesMigration>("a", "b")
.Add<CreateKeysAndIndexesMigration>("b", "done"));
var upgrader = new Upgrader(
new MigrationPlan("test")
.From(string.Empty)
.To<CreateTableOfTDtoMigration>("a")
.To<DeleteKeysAndIndexesMigration>("b")
.To<CreateKeysAndIndexesMigration>("done"));
upgrader.Execute();
upgrader.Execute(ScopeProvider, builder, Mock.Of<IKeyValueService>(), 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<IKeyValueService>(), new PostMigrationCollection(Enumerable.Empty<IPostMigration>()), logger,
new MigrationPlan("test", builder, logger)
.Add<CreateTableOfTDtoMigration>(string.Empty, "a")
.Add<CreateColumnMigration>("a", "done"));
var upgrader = new Upgrader(
new MigrationPlan("test")
.From(string.Empty)
.To<CreateTableOfTDtoMigration>("a")
.To<CreateColumnMigration>("done"));
upgrader.Execute();
upgrader.Execute(ScopeProvider, builder, Mock.Of<IKeyValueService>(), logger);
scope.Complete();
}
}
@@ -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<DeleteRedirectUrlTable>("{4A9A1A8F-0DA1-4BCF-AD06-C19D79152E35}")
.Chain<NoopMigration>("VERSION.33");
.To<DeleteRedirectUrlTable>("{4A9A1A8F-0DA1-4BCF-AD06-C19D79152E35}")
.To<NoopMigration>("VERSION.33");
var kvs = Mock.Of<IKeyValueService>();
Mock.Get(kvs).Setup(x => x.GetValue(It.IsAny<string>())).Returns<string>(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<IMigrationBuilder>(), Mock.Of<ILogger>());
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<IMigrationBuilder>(), Mock.Of<ILogger>());
var plan = new MigrationPlan("default");
Assert.Throws<ArgumentException>(() =>
{
plan.Add("aaa", "aaa");
plan.From("aaa").To("aaa");
});
}
[Test]
public void OnlyOneTransitionPerState()
{
var plan = new MigrationPlan("default", Mock.Of<IMigrationBuilder>(), Mock.Of<ILogger>());
plan.Add("aaa", "bbb");
var plan = new MigrationPlan("default");
plan.From("aaa").To("bbb");
Assert.Throws<InvalidOperationException>(() =>
{
plan.Add("aaa", "ccc");
plan.From("aaa").To("ccc");
});
}
[Test]
public void CannotContainTwoMoreHeads()
{
var plan = new MigrationPlan("default", Mock.Of<IMigrationBuilder>(), Mock.Of<ILogger>());
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<Exception>(() => plan.Validate());
}
[Test]
public void CannotContainLoops()
{
var plan = new MigrationPlan("default", Mock.Of<IMigrationBuilder>(), Mock.Of<ILogger>());
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<Exception>(() => plan.Validate());
}
[Test]
public void ValidateUmbracoPlan()
{
var plan = new UmbracoPlan(Mock.Of<IMigrationBuilder>(), Mock.Of<ILogger>());
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)
+16 -12
View File
@@ -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;
@@ -50,10 +50,9 @@ namespace Umbraco.Tests.Migrations
var sqlContext = new SqlContext(new SqlCeSyntaxProvider(), DatabaseType.SQLCe, Mock.Of<IPocoDataFactory>());
var scopeProvider = new MigrationTests.TestScopeProvider(scope) { SqlContext = sqlContext };
var u1 = new MigrationTests.TestUpgrader(scopeProvider, builder, Mock.Of<IKeyValueService>(), posts, logger,
new MigrationPlan("Test", builder, logger)
.Add<NoopMigration>(string.Empty, "done"));
u1.Execute();
var u1 = new MigrationTests.TestUpgraderWithPostMigrations(
new MigrationPlan("Test").From(string.Empty).To("done"));
u1.Execute(scopeProvider, builder, Mock.Of<IKeyValueService>(), 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<IPocoDataFactory>());
var scopeProvider = new MigrationTests.TestScopeProvider(scope) { SqlContext = sqlContext };
var u1 = new MigrationTests.TestUpgrader(scopeProvider, builder, Mock.Of<IKeyValueService>(), posts, logger,
new MigrationPlan("Test1", builder, logger)
.Add<NoopMigration>(string.Empty, "done"));
u1.Execute();
var u1 = new MigrationTests.TestUpgraderWithPostMigrations(
new MigrationPlan("Test1").From(string.Empty).To("done"));
u1.Execute(scopeProvider, builder, Mock.Of<IKeyValueService>(), logger, posts);
Assert.AreEqual(1, changed1.CountExecuted);
Assert.AreEqual(0, changed2.CountExecuted);
var u2 = new MigrationTests.TestUpgrader(scopeProvider, builder, Mock.Of<IKeyValueService>(), posts, logger,
new MigrationPlan("Test2", builder, logger)
.Add<NoopMigration>(string.Empty, "done"));
u2.Execute();
var u2 = new MigrationTests.TestUpgraderWithPostMigrations(
new MigrationPlan("Test2").From(string.Empty).To("done"));
u2.Execute(scopeProvider, builder, Mock.Of<IKeyValueService>(), logger, posts);
Assert.AreEqual(1, changed1.CountExecuted);
Assert.AreEqual(1, changed2.CountExecuted);
@@ -403,7 +403,6 @@ namespace Umbraco.Tests.Persistence.NPocoTests
.From<Thing1Dto>()
.Where<Thing1Dto>(x => x.Id == 1);
sql.WriteToConsole();
var dto = scope.Database.Fetch<Thing1Dto>(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<Thing1Dto>(sql3).FirstOrDefault();
Assert.IsNotNull(dto);
Assert.AreEqual("one", dto.Name);
@@ -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<ISqlContext>(SqlContext)
.Select("*")
.From<PropertyDataDto>()
.Where<PropertyDataDto>(x => x.LanguageId == null);
Assert.AreEqual("SELECT *\nFROM [umbracoPropertyData]\nWHERE (([umbracoPropertyData].[languageId] is null))", sql.SQL, sql.SQL);
sql = new Sql<ISqlContext>(SqlContext)
.Select("*")
.From<PropertyDataDto>()
.Where<PropertyDataDto>(x => x.LanguageId == 123);
Assert.AreEqual("SELECT *\nFROM [umbracoPropertyData]\nWHERE (([umbracoPropertyData].[languageId] = @0))", sql.SQL, sql.SQL);
var id = 123;
sql = new Sql<ISqlContext>(SqlContext)
.Select("*")
.From<PropertyDataDto>()
.Where<PropertyDataDto>(x => x.LanguageId == id);
Assert.AreEqual("SELECT *\nFROM [umbracoPropertyData]\nWHERE (([umbracoPropertyData].[languageId] = @0))", sql.SQL, sql.SQL);
int? nid = 123;
sql = new Sql<ISqlContext>(SqlContext)
.Select("*")
.From<PropertyDataDto>()
.Where<PropertyDataDto>(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<ISqlContext>(SqlContext)
.Select("*")
.From<PropertyDataDto>()
.Where<PropertyDataDto>(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<ISqlContext>(SqlContext)
// .Select("*")
// .From<PropertyDataDto>()
// .Where<PropertyDataDto>(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<ISqlContext>(SqlContext)
.Select("*")
.From<PropertyDataDto>()
.Where<PropertyDataDto>(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<DataTypeDto>(u => u.Set(x => x.EditorAlias, "Umbraco.ColorPicker"))
.Where<DataTypeDto>(x => x.EditorAlias == "Umbraco.ColorPickerAlias");
sql.WriteToConsole();
}
[TableName("dto1")]
@@ -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<InvalidOperationException>(() => template.Sql(new { xvalue = 123 }).WriteToConsole());
Assert.Throws<InvalidOperationException>(() => template.Sql(new { value = 123, xvalue = 456 }).WriteToConsole());
Assert.Throws<InvalidOperationException>(() => template.Sql(new { xvalue = 123 }));
Assert.Throws<InvalidOperationException>(() => 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<InvalidOperationException>(() => template.Sql(new { j = 123 }).WriteToConsole());
Assert.Throws<InvalidOperationException>(() => template.Sql(new { i = 123, j = 456 }).WriteToConsole());
Assert.Throws<InvalidOperationException>(() => template.Sql(new { j = 123 }));
Assert.Throws<InvalidOperationException>(() => template.Sql(new { i = 123, j = 456 }));
// now with more arguments
@@ -305,12 +305,10 @@ namespace Umbraco.Tests.Persistence.NPocoTests
.From<UserLoginDto>()
.Where<UserLoginDto>(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());
}
}
@@ -107,8 +107,6 @@ namespace Umbraco.Tests.Persistence.Querying
var translator = new SqlTranslator<IContent>(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]);
@@ -26,7 +26,6 @@ namespace Umbraco.Tests.Persistence
// Assert
Assert.That(result.Errors.Count, Is.EqualTo(0));
Assert.AreEqual(result.DetermineInstalledVersion(), UmbracoVersion.Current);
}
}
}
@@ -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<IPublishedContent> Children { get; }
public override PublishedContentType ContentType { get; }
@@ -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<IPublishedProperty> Properties { get; set; }
@@ -104,12 +104,11 @@ namespace Umbraco.Tests.PublishedContent
Assert.AreEqual("<div>This is some content</div>", propVal2.ToString());
var propVal3 = publishedMedia.Value("Content");
Assert.IsInstanceOf<IHtmlString>(propVal3);
Assert.IsInstanceOf<IHtmlString>(propVal3);
Assert.AreEqual("<div>This is some content</div>", 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<PropertyEditorCollection>(), 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<PropertyEditorCollection>(), 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<PropertyEditorCollection>(), 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<PropertyEditorCollection>(), 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<PropertyEditorCollection>(), 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<PropertyEditorCollection>(), 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<PropertyEditorCollection>(), IndexInitializer.GetMockMediaService());
@@ -305,6 +305,7 @@ namespace Umbraco.Tests.PublishedContent
validator: new ContentValueSetValidator(true)))
using (indexer.ProcessNonAsync())
{
rebuilder.RegisterIndex(indexer.Name);
rebuilder.Populate(indexer);
@@ -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
@@ -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<ServiceContext>().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<int>(() => 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<int>(
"SELECT COUNT(*) FROM cmsTagRelationship WHERE nodeId=@nodeId AND propertyTypeId=@propTypeId",
new { nodeId = content.Id, propTypeId = propertyTypeId }));
Assert.AreEqual(3, scope.Database.ExecuteScalar<int>(
"SELECT COUNT(*) FROM cmsTagRelationship WHERE nodeId=@nodeId AND propertyTypeId=@propTypeId",
new { nodeId = child1.Id, propTypeId = propertyTypeId }));
Assert.AreEqual(2, scope.Database.ExecuteScalar<int>(
"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<int>(
"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<int>(
"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<int>(
"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<int>(
"SELECT COUNT(*) FROM cmsTagRelationship WHERE nodeId=@nodeId AND propertyTypeId=@propTypeId",
new { nodeId = content.Id, propTypeId = propertyTypeId }));
scope.Complete();
}
}
}
}
@@ -193,63 +193,6 @@ namespace Umbraco.Tests.Services
Assert.AreEqual(5, found.Length);
}
/// <summary>
/// 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
/// </summary>
[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<int>(() => 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<int>(
"SELECT COUNT(*) FROM cmsTagRelationship WHERE nodeId=@nodeId AND propertyTypeId=@propTypeId",
new { nodeId = content.Id, propTypeId = propertyTypeId }));
Assert.AreEqual(3, scope.Database.ExecuteScalar<int>(
"SELECT COUNT(*) FROM cmsTagRelationship WHERE nodeId=@nodeId AND propertyTypeId=@propTypeId",
new { nodeId = child1.Id, propTypeId = propertyTypeId }));
Assert.AreEqual(2, scope.Database.ExecuteScalar<int>(
"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<int>(
"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<int>(
"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<int>(
"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<int>(
"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()
{
@@ -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));
}
@@ -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<IPublishedContent> Children { get; set; }
+1
View File
@@ -136,6 +136,7 @@
<Compile Include="PublishedContent\NuCacheTests.cs" />
<Compile Include="Runtimes\StandaloneTests.cs" />
<Compile Include="Services\ContentServicePublishBranchTests.cs" />
<Compile Include="Services\ContentServiceTagsTests.cs" />
<Compile Include="Services\ContentTypeServiceVariantsTests.cs" />
<Compile Include="Testing\Objects\TestDataSource.cs" />
<Compile Include="Published\PublishedSnapshotTestObjects.cs" />
+177 -177
View File
@@ -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": {
@@ -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 ....
@@ -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;
});
@@ -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);
}
@@ -274,7 +274,7 @@ angular.module("umbraco.directives")
}
//// INIT /////
$image.load(function(){
$image.on("load", function(){
$timeout(function(){
init($image);
});
@@ -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){
@@ -124,58 +124,6 @@
</tr>
</table>
<h1>Content Picker</h1>
Opens a content picker.</br>
<strong>view: </strong>contentpicker
<table>
<thead>
<tr>
<th>Param</th>
<th>Type</th>
<th>Details</th>
</tr>
</thead>
<tr>
<td>model.multiPicker</td>
<td>Boolean</td>
<td>Pick one or multiple items</td>
</tr>
</table>
<table>
<thead>
<tr>
<th>Returns</th>
<th>Type</th>
<th>Details</th>
</tr>
</thead>
<tr>
<td>model.selection</td>
<td>Array</td>
<td>Array of content objects</td>
</tr>
</table>
<h1>Icon Picker</h1>
Opens an icon picker.</br>
<strong>view: </strong>iconpicker
<table>
<thead>
<tr>
<th>Returns</th>
<th>Type</th>
<th>Details</th>
</tr>
</thead>
<tr>
<td>model.icon</td>
<td>String</td>
<td>The icon class</td>
</tr>
</table>
<h1>Item Picker</h1>
Opens an item picker.</br>
<strong>view: </strong>itempicker
@@ -220,170 +168,6 @@ Opens an item picker.</br>
</tr>
</table>
<h1>Macro Picker</h1>
Opens a media picker.</br>
<strong>view: </strong>macropicker
<table>
<thead>
<tr>
<th>Param</th>
<th>Type</th>
<th>Details</th>
</tr>
</thead>
<tbody>
<tr>
<td>model.dialogData</td>
<td>Object</td>
<td>Object which contains array of allowedMacros. Set to <code>null</code> to allow all.</td>
</tr>
</tbody>
</table>
<table>
<thead>
<tr>
<th>Returns</th>
<th>Type</th>
<th>Details</th>
</tr>
</thead>
<tbody>
<tr>
<td>model.macroParams</td>
<td>Array</td>
<td>Array of macro params</td>
</tr>
<tr>
<td>model.selectedMacro</td>
<td>Object</td>
<td>The selected macro</td>
</tr>
</tbody>
</table>
<h1>Media Picker</h1>
Opens a media picker.</br>
<strong>view: </strong>mediapicker
<table>
<thead>
<tr>
<th>Param</th>
<th>Type</th>
<th>Details</th>
</tr>
</thead>
<tbody>
<tr>
<td>model.multiPicker</td>
<td>Boolean</td>
<td>Pick one or multiple items</td>
</tr>
<tr>
<td>model.onlyImages</td>
<td>Boolean</td>
<td>Only display files that have an image file-extension</td>
</tr>
<tr>
<td>model.disableFolderSelect</td>
<td>Boolean</td>
<td>Disable folder selection</td>
</tr>
</tbody>
</table>
<table>
<thead>
<tr>
<th>Returns</th>
<th>Type</th>
<th>Details</th>
</tr>
</thead>
<tbody>
<tr>
<td>model.selectedImages</td>
<td>Array</td>
<td>Array of selected images</td>
</tr>
</tbody>
</table>
<h1>Member Group Picker</h1>
Opens a member group picker.</br>
<strong>view: </strong>membergrouppicker
<table>
<thead>
<tr>
<th>Param</th>
<th>Type</th>
<th>Details</th>
</tr>
</thead>
<tbody>
<tr>
<td>model.multiPicker</td>
<td>Boolean</td>
<td>Pick one or multiple items</td>
</tr>
</tbody>
</table>
<table>
<thead>
<tr>
<th>Returns</th>
<th>Type</th>
<th>Details</th>
</tr>
</thead>
<tbody>
<tr>
<td>model.selectedMemberGroup</td>
<td>String</td>
<td>The selected member group</td>
</tr>
<tr>
<td>model.selectedMemberGroups (multiPicker)</td>
<td>Array</td>
<td>The selected member groups</td>
</tr>
</tbody>
</table>
<h1>Member Picker</h1>
Opens a member picker. </br>
<strong>view: </strong>memberpicker
<table>
<thead>
<tr>
<th>Param</th>
<th>Type</th>
<th>Details</th>
</tr>
</thead>
<tbody>
<tr>
<td>model.multiPicker</td>
<td>Boolean</td>
<td>Pick one or multiple items</td>
</tr>
</tbody>
</table>
<table>
<thead>
<tr>
<th>Returns</th>
<th>Type</th>
<th>Details</th>
</tr>
</thead>
<tbody>
<tr>
<td>model.selection</td>
<td>Array</td>
<td>Array of selected members/td>
</tr>
</tbody>
</table>
<h1>YSOD</h1>
Opens an overlay to show a custom YSOD. </br>
<strong>view: </strong>ysod
@@ -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: "<?",
onValueChanged: "&"
}
});
function umbTagsEditorController($rootScope, assetsService, umbRequestHelper, angularHelper, $timeout, $element) {
var vm = this;
var typeahead;
var tagsHound;
vm.$onInit = onInit;
vm.$onChanges = onChanges;
vm.$onDestroy = onDestroy;
vm.validateMandatory = validateMandatory;
vm.addTagOnEnter = addTagOnEnter;
vm.addTag = addTag;
vm.removeTag = removeTag;
vm.showPrompt = showPrompt;
vm.hidePrompt = hidePrompt;
vm.htmlId = "t" + String.CreateGuid();
vm.isLoading = true;
vm.tagToAdd = "";
vm.promptIsVisible = "-1";
vm.viewModel = [];
function onInit() {
assetsService.loadJs("lib/typeahead.js/typeahead.bundle.min.js").then(function () {
vm.isLoading = false;
configureViewModel();
// Set the visible prompt to -1 to ensure it will not be visible
vm.promptIsVisible = "-1";
tagsHound = new Bloodhound({
datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'),
queryTokenizer: Bloodhound.tokenizers.whitespace,
//pre-fetch the tags for this category
prefetch: {
url: umbRequestHelper.getApiUrl("tagsDataBaseUrl", "GetTags", { tagGroup: vm.config.group, culture: vm.culture }),
//TTL = 5 minutes
ttl: 300000,
transform: dataTransform
},
//dynamically get the tags for this category (they may have changed on the server)
remote: {
url: umbRequestHelper.getApiUrl("tagsDataBaseUrl", "GetTags", { tagGroup: vm.config.group, culture: vm.culture }),
transform: dataTransform
}
});
tagsHound.initialize(true);
//configure the type ahead
$timeout(function () {
var sources = {
//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: vm.config.group,
display: "value",
//source: tagsHound
source: function (query, cb) {
tagsHound.search(query,
function(suggestions) {
cb(removeCurrentTagsFromSuggestions(suggestions));
});
}
};
var opts = {
//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
};
typeahead = $element.find('.tags-' + vm.htmlId).typeahead(opts, sources)
.bind("typeahead:selected", function (obj, datum, name) {
angularHelper.safeApply($rootScope, function () {
addTagInternal(datum["value"]);
vm.tagToAdd = "";
// clear the typed text
typeahead.typeahead('val', '');
});
}).bind("typeahead:autocompleted", function (obj, datum, name) {
angularHelper.safeApply($rootScope, function () {
addTagInternal(datum["value"]);
vm.tagToAdd = "";
});
}).bind("typeahead:opened", function (obj) {
console.log("opened ");
});
});
});
}
function onChanges(changes) {
// watch for value changes externally
if (changes.value) {
if (!changes.value.isFirstChange() && changes.value.currentValue !== changes.value.previousValue) {
configureViewModel();
//this is required to re-validate
vm.tagEditorForm.tagCount.$setViewValue(vm.viewModel.length);
}
}
}
function onDestroy() {
if (tagsHound) {
tagsHound.clearPrefetchCache();
tagsHound.clearRemoteCache();
tagsHound = null;
}
$element.find('.tags-' + vm.htmlId).typeahead('destroy');
}
function configureViewModel() {
if (vm.value) {
if (angular.isString(vm.value) && vm.value.length > 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);
});
}
}
})();
@@ -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
@@ -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.
@@ -4,6 +4,76 @@
*
* @description
* Added in Umbraco 8.0. Application-wide service for handling infinite editing.
*
*
<h3>Markup example</h3>
<pre>
<div ng-controller="My.Controller as vm">
<button type="button" ng-click="vm.open()">Open</button>
</div>
</pre>
<h3>Controller example</h3>
<pre>
(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);
})();
</pre>
<h3>Custom view example</h3>
<pre>
(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);
})();
</pre>
*/
(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
@@ -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;
@@ -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;
}
}
@@ -77,7 +77,7 @@
var emptyStateMessage = values[2];
var dictionaryItemPicker = {
section: "settings",
section: "translation",
treeAlias: "dictionary",
entityType: "dictionary",
multiPicker: false,
@@ -0,0 +1,34 @@
<div>
<ng-form name="vm.tagEditorForm">
<div ng-if="vm.isLoading">
<localize key="loading">Loading</localize>...
</div>
<div ng-if="!isLoading">
<input type="hidden" name="tagCount" ng-model="vm.viewModel.length" val-property-validator="vm.validateMandatory" />
<span ng-repeat="tag in vm.viewModel track by $index" class="label label-primary tag">
<span ng-bind-html="tag"></span>
<i class="icon-trash" ng-click="vm.showPrompt($index, tag)" localize="title" title="@buttons_deleteTag"></i>
<umb-confirm-action ng-if="vm.promptIsVisible === $index"
direction="left"
on-confirm="vm.removeTag(tag)"
on-cancel="vm.hidePrompt()">
</umb-confirm-action>
</span>
<input type="text"
id="{{vm.htmlId}}"
class="typeahead tags-{{vm.htmlId}}"
ng-model="vm.tagToAdd"
ng-keydown="vm.addTagOnEnter($event)"
on-blur="vm.addTag()"
localize="placeholder"
placeholder="@placeholders_enterTags" />
</div>
</ng-form>
</div>
@@ -201,7 +201,7 @@
var emptyStateMessage = values[1];
var dictionaryPicker = {
section: "settings",
section: "translation",
treeAlias: "dictionary",
entityType: "dictionary",
multiPicker: false,
@@ -207,7 +207,7 @@
var emptyStateMessage = values[1];
var dictionaryItem = {
section: "settings",
section: "translation",
treeAlias: "dictionary",
entityType: "dictionary",
multiPicker: false,
@@ -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;
}
}
);
@@ -1,35 +1,10 @@
<div ng-controller="Umbraco.PropertyEditors.TagsController" class="umb-property-editor umb-tags">
<div ng-if="isLoading">
<localize key="loading">Loading</localize>...
</div>
<div ng-if="!isLoading">
<input type="hidden" name="tagCount" ng-model="model.value.length" val-property-validator="validateMandatory" />
<span ng-repeat="tag in model.value track by $index" class="label label-primary tag">
<span ng-bind-html="tag"></span>
<i class="icon-trash" ng-click="$parent.showPrompt($index, tag)" localize="title" title="@buttons_deleteTag"></i>
<umb-confirm-action
ng-if="$parent.promptIsVisible === $index"
direction="left"
on-confirm="$parent.removeTag(tag)"
on-cancel="$parent.hidePrompt()">
</umb-confirm-action>
</span>
<input type="text"
id="{{model.alias}}"
class="typeahead tags-{{model.alias}}"
ng-model="$parent.tagToAdd"
ng-keydown="$parent.addTagOnEnter($event)"
ng-blur="$parent.addTag()"
localize="placeholder"
placeholder="@placeholders_enterTags" />
</div>
<umb-tags-editor value="model.value"
config="model.config"
validation="model.validation"
on-value-changed="valueChanged(value)"
culture="model.culture">
</umb-tags-editor>
</div>
@@ -438,7 +438,7 @@
var emptyStateMessage = values[1];
var dictionaryItem = {
section: "settings",
section: "translation",
treeAlias: "dictionary",
entityType: "dictionary",
multiPicker: false,
@@ -1,10 +0,0 @@
{
"dashboards": [
{
"name": "Install Umbraco Forms",
"alias": "installUmbracoForms",
"view": "views/dashboard/forms/formsdashboardintro.html",
"sections": [ "forms" ]
}
]
}
-1
View File
@@ -231,7 +231,6 @@
<Content Include="Umbraco\Install\Views\Web.config" />
<Content Include="App_Plugins\ModelsBuilder\package.manifest" />
<Content Include=".eslintignore" />
<Content Include="App_Plugins\UmbracoForms\package.manifest" />
<None Include="Config\404handlers.Release.config">
<DependentUpon>404handlers.config</DependentUpon>
</None>
@@ -22,6 +22,17 @@
</tab>
</section>
<section alias="StartupFormsDashboardSection">
<areas>
<area>forms</area>
</areas>
<tab caption="Install Umbraco Forms">
<control panelCaption="">
views/dashboard/forms/formsdashboardintro.html
</control>
</tab>
</section>
<section alias="StartupMediaDashboardSection">
<areas>
<area>media</area>
@@ -30,9 +30,5 @@
<add initialize="true" sortOrder="1" alias="memberGroups" application="member" title="Member Groups" iconClosed="icon-folder" iconOpen="icon-folder-open" type="Umbraco.Web.Trees.MemberGroupTreeController, Umbraco.Web" />
<!--Translation-->
<add initialize="true" application="translation" alias="dictionary" title="Dictionary" type="Umbraco.Web.Trees.DictionaryTreeController, Umbraco.Web" iconClosed="icon-folder" iconOpen="icon-folder" sortOrder="0" />
<!-- Custom -->
<add initialize="true" sortOrder="2" alias="datasource" application="forms" title="Datasources" iconClosed="icon-folder" iconOpen="icon-folder-open" type="Umbraco.Forms.Web.Trees.DataSourceTreeController, Umbraco.Forms.Web" />
<add initialize="true" sortOrder="0" alias="form" application="forms" title="Forms" iconClosed="icon-folder" iconOpen="icon-folder-open" type="Umbraco.Forms.Web.Trees.FormTreeController, Umbraco.Forms.Web" />
<add initialize="true" sortOrder="3" alias="prevaluesource" application="forms" title="Prevalue sources" iconClosed="icon-folder" iconOpen="icon-folder-open" type="Umbraco.Forms.Web.Trees.PreValueSourceTreeController, Umbraco.Forms.Web" />
<add initialize="true" sortOrder="3" alias="formsecurity" application="users" title="Forms Security" iconClosed="icon-folder" iconOpen="icon-folder-open" type="Umbraco.Forms.Web.Trees.FormSecurityTreeController, Umbraco.Forms.Web" />
</trees>
+1 -5
View File
@@ -30,9 +30,5 @@
<add initialize="true" sortOrder="1" alias="memberGroups" application="member" title="Member Groups" iconClosed="icon-folder" iconOpen="icon-folder-open" type="Umbraco.Web.Trees.MemberGroupTreeController, Umbraco.Web" />
<!--Translation-->
<add initialize="true" application="translation" alias="dictionary" title="Dictionary" type="Umbraco.Web.Trees.DictionaryTreeController, Umbraco.Web" iconClosed="icon-folder" iconOpen="icon-folder" sortOrder="0" />
<!-- Custom -->
<add initialize="true" sortOrder="2" alias="datasource" application="forms" title="Datasources" iconClosed="icon-folder" iconOpen="icon-folder-open" type="Umbraco.Forms.Web.Trees.DataSourceTreeController, Umbraco.Forms.Web" />
<add initialize="true" sortOrder="0" alias="form" application="forms" title="Forms" iconClosed="icon-folder" iconOpen="icon-folder-open" type="Umbraco.Forms.Web.Trees.FormTreeController, Umbraco.Forms.Web" />
<add initialize="true" sortOrder="3" alias="prevaluesource" application="forms" title="Prevalue sources" iconClosed="icon-folder" iconOpen="icon-folder-open" type="Umbraco.Forms.Web.Trees.PreValueSourceTreeController, Umbraco.Forms.Web" />
<add initialize="true" sortOrder="3" alias="formsecurity" application="users" title="Forms Security" iconClosed="icon-folder" iconOpen="icon-folder-open" type="Umbraco.Forms.Web.Trees.FormSecurityTreeController, Umbraco.Forms.Web" />
</trees>
@@ -254,7 +254,7 @@ namespace Umbraco.Web.Editors
},
{
"tagsDataBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl<TagsDataController>(
controller => controller.GetTags(""))
controller => controller.GetTags("", ""))
},
{
"examineMgmtBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl<ExamineManagementController>(

Some files were not shown because too many files have changed in this diff Show More