Merge branch temp8 into temp8-di2690
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+8
@@ -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> </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> </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> </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> </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> </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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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,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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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,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; }
|
||||
|
||||
|
||||
@@ -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
@@ -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": {
|
||||
|
||||
+1
-1
@@ -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 ....
|
||||
|
||||
+3
-2
@@ -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;
|
||||
});
|
||||
|
||||
+2
-2
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
+1
-1
@@ -274,7 +274,7 @@ angular.module("umbraco.directives")
|
||||
}
|
||||
|
||||
//// INIT /////
|
||||
$image.load(function(){
|
||||
$image.on("load", function(){
|
||||
$timeout(function(){
|
||||
init($image);
|
||||
});
|
||||
|
||||
+24
-24
@@ -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){
|
||||
|
||||
-216
@@ -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
|
||||
|
||||
+277
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
})();
|
||||
+4
-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;
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -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" ]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user