Merge branch 'temp8' into temp8-backoffice-search-with-variants

# Conflicts:
#	src/Umbraco.Examine/ExamineExtensions.cs
This commit is contained in:
Shannon
2018-12-14 13:06:54 +11:00
85 changed files with 5423 additions and 5100 deletions
+12 -34
View File
@@ -42,6 +42,17 @@ namespace Umbraco.Core.Composing
private static LocalTempStorage _localTempStorage = LocalTempStorage.Unknown;
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="globalSettings"></param>
/// <param name="logger">A profiling logger.</param>
/// <remarks>Used by LightInject.</remarks>
public TypeLoader(IRuntimeCacheProvider runtimeCache, IGlobalSettings globalSettings, ProfilingLogger logger)
: this(runtimeCache, globalSettings, logger, true)
{ }
/// <summary>
/// Initializes a new instance of the <see cref="TypeLoader"/> class.
/// </summary>
@@ -49,7 +60,7 @@ namespace Umbraco.Core.Composing
/// <param name="globalSettings"></param>
/// <param name="logger">A profiling logger.</param>
/// <param name="detectChanges">Whether to detect changes using hashes.</param>
internal TypeLoader(IRuntimeCacheProvider runtimeCache, IGlobalSettings globalSettings, ProfilingLogger logger, bool detectChanges = true)
internal TypeLoader(IRuntimeCacheProvider runtimeCache, IGlobalSettings globalSettings, ProfilingLogger logger, bool detectChanges)
{
_runtimeCache = runtimeCache ?? throw new ArgumentNullException(nameof(runtimeCache));
_globalSettings = globalSettings ?? throw new ArgumentNullException(nameof(globalSettings));
@@ -401,39 +412,6 @@ namespace Umbraco.Core.Composing
return _fileBasePath;
}
//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()
{
@@ -202,7 +202,6 @@ namespace Umbraco.Core.Logging.Serilog
private static bool IsTimeoutThreadAbortException(Exception exception)
{
if (!(exception is ThreadAbortException abort)) return false;
if (abort.ExceptionState == null) return false;
var stateType = abort.ExceptionState.GetType();
@@ -1,6 +1,7 @@
using NPoco;
using Umbraco.Core.Migrations.Expressions.Common;
using Umbraco.Core.Migrations.Expressions.Execute.Expressions;
using Umbraco.Core.Persistence;
namespace Umbraco.Core.Migrations.Expressions.Execute
{
@@ -12,7 +13,16 @@ namespace Umbraco.Core.Migrations.Expressions.Execute
{ }
/// <inheritdoc />
public void Do() => Expression.Execute();
public void Do()
{
// slightly awkward, but doing it right would mean a *lot*
// of changes for MigrationExpressionBase
if (Expression.SqlObject == null)
Expression.Execute();
else
Expression.ExecuteSqlObject();
}
/// <inheritdoc />
public IExecutableBuilder Sql(string sqlStatement)
@@ -20,5 +30,12 @@ namespace Umbraco.Core.Migrations.Expressions.Execute
Expression.SqlStatement = sqlStatement;
return this;
}
/// <inheritdoc />
public IExecutableBuilder Sql(Sql<ISqlContext> sql)
{
Expression.SqlObject = sql;
return this;
}
}
}
@@ -1,4 +1,5 @@
using NPoco;
using Umbraco.Core.Persistence;
namespace Umbraco.Core.Migrations.Expressions.Execute.Expressions
{
@@ -10,6 +11,13 @@ namespace Umbraco.Core.Migrations.Expressions.Execute.Expressions
public virtual string SqlStatement { get; set; }
public virtual Sql<ISqlContext> SqlObject { get; set; }
public void ExecuteSqlObject()
{
Execute(SqlObject);
}
protected override string GetSql()
{
return SqlStatement;
@@ -1,4 +1,6 @@
using Umbraco.Core.Migrations.Expressions.Common;
using NPoco;
using Umbraco.Core.Migrations.Expressions.Common;
using Umbraco.Core.Persistence;
namespace Umbraco.Core.Migrations.Expressions.Execute
{
@@ -12,5 +14,10 @@ namespace Umbraco.Core.Migrations.Expressions.Execute
/// Specifies the Sql statement to execute.
/// </summary>
IExecutableBuilder Sql(string sqlStatement);
/// <summary>
/// Specifies the Sql statement to execute.
/// </summary>
IExecutableBuilder Sql(Sql<ISqlContext> sql);
}
}
@@ -534,56 +534,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());
//}
// 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>";
@@ -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 });
}
@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using NPoco;
using Umbraco.Core.Logging;
@@ -88,6 +87,31 @@ namespace Umbraco.Core.Migrations
expression.Execute();
}
protected void Execute(Sql<ISqlContext> sql)
{
if (_executed)
throw new InvalidOperationException("This expression has already been executed.");
_executed = true;
if (sql == null)
{
Logger.Info(GetType(), $"SQL [{Context.Index}]: <empty>");
}
else
{
Logger.Info(GetType(), $"SQL [{Context.Index}]: {sql.ToText()}");
Database.Execute(sql);
}
Context.Index++;
if (_expressions == null)
return;
foreach (var expression in _expressions)
expression.Execute();
}
private void ExecuteStatement(StringBuilder stmtBuilder)
{
var stmt = stmtBuilder.ToString();
+73 -83
View File
@@ -4,6 +4,7 @@ using System.Linq;
using Umbraco.Core.Exceptions;
using Umbraco.Core.Logging;
using Umbraco.Core.Scoping;
using Type = System.Type;
namespace Umbraco.Core.Migrations
{
@@ -12,8 +13,6 @@ namespace Umbraco.Core.Migrations
/// </summary>
public class MigrationPlan
{
private readonly IMigrationBuilder _migrationBuilder;
private readonly ILogger _logger;
private readonly Dictionary<string, Transition> _transitions = new Dictionary<string, Transition>();
private string _prevState;
@@ -23,64 +22,24 @@ namespace Umbraco.Core.Migrations
/// Initializes a new instance of the <see cref="MigrationPlan"/> class.
/// </summary>
/// <param name="name">The name of the plan.</param>
/// <remarks>The plan cannot be executed. Use this constructor e.g. when only validating the plan,
/// or trying to get its final state, without actually needing to execute it.</remarks>
public MigrationPlan(string name)
{
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullOrEmptyException(nameof(name));
Name = name;
// ReSharper disable once VirtualMemberCallInConstructor
// (accepted)
DefinePlan();
}
/// <summary>
/// Initializes a new instance of the <see cref="MigrationPlan"/> class.
/// Gets the transitions.
/// </summary>
/// <param name="name">The name of the plan.</param>
/// <param name="migrationBuilder">A migration builder.</param>
/// <param name="logger">A logger.</param>
/// <remarks>The plan can be executed.</remarks>
public MigrationPlan(string name, IMigrationBuilder migrationBuilder, ILogger logger)
{
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullOrEmptyException(nameof(name));
Name = name;
_migrationBuilder = migrationBuilder ?? throw new ArgumentNullException(nameof(migrationBuilder));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
// ReSharper disable once VirtualMemberCallInConstructor
// (accepted)
DefinePlan();
}
/// <summary>
/// Defines the plan.
/// </summary>
protected virtual void DefinePlan() { }
public IReadOnlyDictionary<string, Transition> Transitions => _transitions;
/// <summary>
/// Gets the name of the plan.
/// </summary>
public string Name { get; }
/// <summary>
/// Adds an empty migration from source to target state.
/// </summary>
public MigrationPlan Add(string sourceState, string targetState)
=> Add<NoopMigration>(sourceState, targetState);
/// <summary>
/// Adds a migration from source to target state.
/// </summary>
public MigrationPlan Add<TMigration>(string sourceState, string targetState)
where TMigration : IMigration
=> Add(sourceState, targetState, typeof(TMigration));
/// <summary>
/// Adds a migration from source to target state.
/// </summary>
public MigrationPlan Add(string sourceState, string targetState, Type migration)
// adds a transition
private MigrationPlan Add(string sourceState, string targetState, Type migration)
{
if (sourceState == null) throw new ArgumentNullException(nameof(sourceState));
if (string.IsNullOrWhiteSpace(targetState)) throw new ArgumentNullOrEmptyException(nameof(targetState));
@@ -113,26 +72,26 @@ namespace Umbraco.Core.Migrations
}
/// <summary>
/// Chains an empty migration from chain to target state.
/// </summary>
public MigrationPlan Chain(string targetState)
=> Chain<NoopMigration>(targetState);
/// Adds a transition to a target state through an empty migration.
/// </summary>
public MigrationPlan To(string targetState)
=> To<NoopMigration>(targetState);
/// <summary>
/// Chains a migration from chain to target state.
/// Adds a transition to a target state through a migration.
/// </summary>
public MigrationPlan Chain<TMigration>(string targetState)
public MigrationPlan To<TMigration>(string targetState)
where TMigration : IMigration
=> Chain(targetState, typeof(TMigration));
=> To(targetState, typeof(TMigration));
/// <summary>
/// Chains a migration from chain to target state.
/// Adds a transition to a target state through a migration.
/// </summary>
public MigrationPlan Chain(string targetState, Type migration)
public MigrationPlan To(string targetState, Type migration)
=> Add(_prevState, targetState, migration);
/// <summary>
/// Sets the chain state.
/// Sets the starting state.
/// </summary>
public MigrationPlan From(string sourceState)
{
@@ -141,19 +100,16 @@ namespace Umbraco.Core.Migrations
}
/// <summary>
/// Copies a chain.
/// Adds transitions to a target state by copying transitions from a start state to an end state.
/// </summary>
/// <remarks>Copies the chain going from startState to endState, with new states going from sourceState to targetState.</remarks>
public MigrationPlan CopyChain(string sourceState, string startState, string endState, string targetState)
public MigrationPlan To(string targetState, string startState, string endState)
{
if (sourceState == null) throw new ArgumentNullException(nameof(sourceState));
if (string.IsNullOrWhiteSpace(startState)) throw new ArgumentNullOrEmptyException(nameof(startState));
if (string.IsNullOrWhiteSpace(endState)) throw new ArgumentNullOrEmptyException(nameof(endState));
if (string.IsNullOrWhiteSpace(targetState)) throw new ArgumentNullOrEmptyException(nameof(targetState));
if (sourceState == targetState) throw new ArgumentException("Source and target states cannot be identical.");
if (startState == endState) throw new ArgumentException("Start and end states cannot be identical.");
sourceState = sourceState.Trim();
startState = startState.Trim();
endState = endState.Trim();
targetState = targetState.Trim();
@@ -168,26 +124,18 @@ namespace Umbraco.Core.Migrations
visited.Add(state);
if (!_transitions.TryGetValue(state, out var transition))
throw new InvalidOperationException($"There is no transition from state \"{sourceState}\".");
throw new InvalidOperationException($"There is no transition from state \"{state}\".");
var newTargetState = transition.TargetState == endState
? targetState
: Guid.NewGuid().ToString("B").ToUpper();
Add(sourceState, newTargetState, transition.MigrationType);
sourceState = newTargetState;
To(newTargetState, transition.MigrationType);
state = transition.TargetState;
}
return this;
}
/// <summary>
/// Copies a chain.
/// </summary>
/// <remarks>Copies the chain going from startState to endState, with new states going from chain to targetState.</remarks>
public MigrationPlan CopyChain(string startState, string endState, string targetState)
=> CopyChain(_prevState, startState, endState, targetState);
/// <summary>
/// Gets the initial state.
/// </summary>
@@ -260,50 +208,92 @@ namespace Umbraco.Core.Migrations
/// </summary>
/// <param name="scope">A scope.</param>
/// <param name="fromState">The state to start execution at.</param>
/// <param name="migrationBuilder">A migration builder.</param>
/// <param name="logger">A logger.</param>
/// <returns>The final state.</returns>
/// <remarks>The plan executes within the scope, which must then be completed.</remarks>
public string Execute(IScope scope, string fromState)
public string Execute(IScope scope, string fromState, IMigrationBuilder migrationBuilder, ILogger logger)
{
Validate();
if (_migrationBuilder == null || _logger == null)
throw new InvalidOperationException("Cannot execute a non-executing plan.");
if (migrationBuilder == null) throw new ArgumentNullException(nameof(migrationBuilder));
if (logger == null) throw new ArgumentNullException(nameof(logger));
_logger.Info<MigrationPlan>("Starting '{MigrationName}'...", Name);
logger.Info<MigrationPlan>("Starting '{MigrationName}'...", Name);
var origState = fromState ?? string.Empty;
_logger.Info<MigrationPlan>("At {OrigState}", string.IsNullOrWhiteSpace(origState) ? "origin": origState);
logger.Info<MigrationPlan>("At {OrigState}", string.IsNullOrWhiteSpace(origState) ? "origin": origState);
if (!_transitions.TryGetValue(origState, out var transition))
throw new Exception($"Unknown state \"{origState}\".");
var context = new MigrationContext(scope.Database, _logger);
var context = new MigrationContext(scope.Database, logger);
while (transition != null)
{
var migration = _migrationBuilder.Build(transition.MigrationType, context);
var migration = migrationBuilder.Build(transition.MigrationType, context);
migration.Migrate();
var nextState = transition.TargetState;
origState = nextState;
_logger.Info<MigrationPlan>("At {OrigState}", origState);
logger.Info<MigrationPlan>("At {OrigState}", origState);
if (!_transitions.TryGetValue(origState, out transition))
throw new Exception($"Unknown state \"{origState}\".");
}
_logger.Info<MigrationPlan>("Done (pending scope completion).");
logger.Info<MigrationPlan>("Done (pending scope completion).");
// safety check
if (origState != _finalState)
throw new Exception($"Internal error, reached state {origState} which is not final state {_finalState}");
return origState;
}
/// <summary>
/// Follows a path (for tests and debugging).
/// </summary>
/// <remarks>Does the same thing Execute does, but does not actually execute migrations.</remarks>
internal string FollowPath(string fromState, string toState = null)
{
toState = toState.NullOrWhiteSpaceAsNull();
Validate();
var origState = fromState ?? string.Empty;
if (!_transitions.TryGetValue(origState, out var transition))
throw new Exception($"Unknown state \"{origState}\".");
while (transition != null)
{
var nextState = transition.TargetState;
origState = nextState;
if (nextState == toState)
{
transition = null;
continue;
}
if (!_transitions.TryGetValue(origState, out transition))
throw new Exception($"Unknown state \"{origState}\".");
}
// safety check
if (origState != (toState ?? _finalState))
throw new Exception($"Internal error, reached state {origState} which is not state {toState ?? _finalState}");
// fixme - what about post-migrations?
return origState;
}
/// <summary>
/// Represents a plan transition.
/// </summary>
private class Transition
public class Transition
{
/// <summary>
/// Initializes a new instance of the <see cref="Transition"/> class.
@@ -2,7 +2,6 @@
using System.Configuration;
using Semver;
using Umbraco.Core.Configuration;
using Umbraco.Core.Logging;
using Umbraco.Core.Migrations.Upgrade.V_7_12_0;
using Umbraco.Core.Migrations.Upgrade.V_8_0_0;
@@ -18,14 +17,9 @@ namespace Umbraco.Core.Migrations.Upgrade
/// </summary>
public UmbracoPlan()
: base(Constants.System.UmbracoUpgradePlanName)
{ }
/// <summary>
/// Initializes a new instance of the <see cref="UmbracoPlan"/> class.
/// </summary>
public UmbracoPlan(IMigrationBuilder migrationBuilder, ILogger logger)
: base(Constants.System.UmbracoUpgradePlanName, migrationBuilder, logger)
{ }
{
DefinePlan();
}
/// <inheritdoc />
/// <remarks>
@@ -61,8 +55,8 @@ namespace Umbraco.Core.Migrations.Upgrade
}
}
/// <inheritdoc />
protected override void DefinePlan()
// define the plan
protected void DefinePlan()
{
// MODIFYING THE PLAN
//
@@ -85,65 +79,74 @@ 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}");
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<MakeTagsVariant>("{C39BF2A7-1454-4047-BBFE-89E40F66ED63}");
//FINAL
@@ -152,20 +155,20 @@ namespace Umbraco.Core.Migrations.Upgrade
// and then, need to support upgrading from more recent 7.x
//
From("{init-7.10.1}").Chain("{init-7.10.0}"); // same as 7.10.0
From("{init-7.10.2}").Chain("{init-7.10.0}"); // same as 7.10.0
From("{init-7.10.3}").Chain("{init-7.10.0}"); // same as 7.10.0
From("{init-7.10.4}").Chain("{init-7.10.0}"); // same as 7.10.0
From("{init-7.11.0}").Chain("{init-7.10.0}"); // same as 7.10.0
From("{init-7.11.1}").Chain("{init-7.10.0}"); // same as 7.10.0
From("{init-7.10.1}").To("{init-7.10.0}"); // same as 7.10.0
From("{init-7.10.2}").To("{init-7.10.0}"); // same as 7.10.0
From("{init-7.10.3}").To("{init-7.10.0}"); // same as 7.10.0
From("{init-7.10.4}").To("{init-7.10.0}"); // same as 7.10.0
From("{init-7.11.0}").To("{init-7.10.0}"); // same as 7.10.0
From("{init-7.11.1}").To("{init-7.10.0}"); // same as 7.10.0
// 7.12.0 has migrations, define a custom chain which copies the chain
// going from {init-7.10.0} to former final (1350617A) , and then goes straight to
// main chain, skipping the migrations
//
From("{init-7.12.0}");
// copy from copy to (former final) main chain
CopyChain("{init-7.10.0}", "{1350617A-4930-4D61-852F-E3AA9E692173}", "{BBD99901-1545-40E4-8A5A-D7A675C7D2F2}");
// target copy from copy to (former final)
To("{BBD99901-1545-40E4-8A5A-D7A675C7D2F2}", "{init-7.10.0}", "{1350617A-4930-4D61-852F-E3AA9E692173}");
}
}
}
@@ -8,24 +8,41 @@ using Umbraco.Core.Services;
namespace Umbraco.Core.Migrations.Upgrade
{
/// <summary>
/// Represents the Umbraco upgrader.
/// </summary>
public class UmbracoUpgrader : Upgrader
{
public UmbracoUpgrader(IScopeProvider scopeProvider, IMigrationBuilder migrationBuilder, IKeyValueService keyValueService, PostMigrationCollection postMigrations, ILogger logger)
: base(scopeProvider, migrationBuilder, keyValueService, postMigrations, logger)
private PostMigrationCollection _postMigrations;
/// <summary>
/// Initializes a new instance of the <see ref="UmbracoUpgrader" /> class.
/// </summary>
public UmbracoUpgrader()
: base(new UmbracoPlan())
{ }
protected override MigrationPlan GetPlan()
/// <summary>
/// Executes.
/// </summary>
public void Execute(IScopeProvider scopeProvider, IMigrationBuilder migrationBuilder, IKeyValueService keyValueService, ILogger logger, PostMigrationCollection postMigrations)
{
return new UmbracoPlan(MigrationBuilder, Logger);
_postMigrations = postMigrations;
Execute(scopeProvider, migrationBuilder, keyValueService, logger);
}
protected override (SemVersion, SemVersion) GetVersions()
/// <inheritdoc />
public override void AfterMigrations(IScope scope, ILogger logger)
{
// assume we have something in web.config that makes some sense
if (!SemVersion.TryParse(ConfigurationManager.AppSettings["umbracoConfigurationStatus"], out var currentVersion))
// assume we have something in web.config that makes some sense = the origin version
if (!SemVersion.TryParse(ConfigurationManager.AppSettings["umbracoConfigurationStatus"], out var originVersion))
throw new InvalidOperationException("Could not get current version from web.config umbracoConfigurationStatus appSetting.");
return (currentVersion, UmbracoVersion.SemanticVersion);
// target version is the code version
var targetVersion = UmbracoVersion.SemanticVersion;
foreach (var postMigration in _postMigrations)
postMigration.Execute(Name, scope, originVersion, targetVersion, logger);
}
}
}
+53 -34
View File
@@ -1,49 +1,60 @@
using System;
using Semver;
using Umbraco.Core.Logging;
using Umbraco.Core.Scoping;
using Umbraco.Core.Services;
namespace Umbraco.Core.Migrations.Upgrade
{
public abstract class Upgrader
/// <summary>
/// Represents an upgrader.
/// </summary>
public class Upgrader
{
private readonly IKeyValueService _keyValueService;
private readonly PostMigrationCollection _postMigrations;
private MigrationPlan _plan;
protected Upgrader(IScopeProvider scopeProvider, IMigrationBuilder migrationBuilder, IKeyValueService keyValueService, PostMigrationCollection postMigrations, ILogger logger)
/// <summary>
/// Initializes a new instance of the <see ref="Upgrader"/> class.
/// </summary>
public Upgrader(MigrationPlan plan)
{
ScopeProvider = scopeProvider ?? throw new ArgumentNullException(nameof(scopeProvider));
MigrationBuilder = migrationBuilder ?? throw new ArgumentNullException(nameof(migrationBuilder));
_keyValueService = keyValueService ?? throw new ArgumentNullException(nameof(keyValueService));
_postMigrations = postMigrations ?? throw new ArgumentNullException(nameof(postMigrations));
Logger = logger ?? throw new ArgumentNullException(nameof(logger));
Plan = plan;
}
/// <summary>
/// Gets the name of the migration plan.
/// </summary>
public string Name => Plan.Name;
public string StateValueKey => GetStateValueKey(Plan);
/// <summary>
/// Gets the migration plan.
/// </summary>
public MigrationPlan Plan { get; }
protected IScopeProvider ScopeProvider { get; }
/// <summary>
/// Gets the key for the state value.
/// </summary>
public virtual string StateValueKey => "Umbraco.Core.Upgrader.State+" + Name;
protected IMigrationBuilder MigrationBuilder { get; }
protected ILogger Logger { get; }
protected MigrationPlan Plan => _plan ?? (_plan = GetPlan());
protected abstract MigrationPlan GetPlan();
protected abstract (SemVersion, SemVersion) GetVersions();
public void Execute()
/// <summary>
/// Executes.
/// </summary>
/// <param name="scopeProvider">A scope provider.</param>
/// <param name="migrationBuilder">A migration builder.</param>
/// <param name="keyValueService">A key-value service.</param>
/// <param name="logger">A logger.</param>
public void Execute(IScopeProvider scopeProvider, IMigrationBuilder migrationBuilder, IKeyValueService keyValueService, ILogger logger)
{
if (scopeProvider == null) throw new ArgumentNullException(nameof(scopeProvider));
if (migrationBuilder == null) throw new ArgumentNullException(nameof(migrationBuilder));
if (keyValueService == null) throw new ArgumentNullException(nameof(keyValueService));
if (logger == null) throw new ArgumentNullException(nameof(logger));
var plan = Plan;
using (var scope = ScopeProvider.CreateScope())
using (var scope = scopeProvider.CreateScope())
{
BeforeMigrations(scope, logger);
// read current state
var currentState = _keyValueService.GetValue(StateValueKey);
var currentState = keyValueService.GetValue(StateValueKey);
var forceState = false;
if (currentState == null)
@@ -53,25 +64,33 @@ namespace Umbraco.Core.Migrations.Upgrade
}
// execute plan
var state = plan.Execute(scope, currentState);
var state = plan.Execute(scope, currentState, migrationBuilder, logger);
if (string.IsNullOrWhiteSpace(state))
throw new Exception("Plan execution returned an invalid null or empty state.");
// save new state
if (forceState)
_keyValueService.SetValue(StateValueKey, state);
keyValueService.SetValue(StateValueKey, state);
else if (currentState != state)
_keyValueService.SetValue(StateValueKey, currentState, state);
keyValueService.SetValue(StateValueKey, currentState, state);
// run post-migrations
(var originVersion, var targetVersion) = GetVersions();
foreach (var postMigration in _postMigrations)
postMigration.Execute(Name, scope, originVersion, targetVersion, Logger);
AfterMigrations(scope, logger);
scope.Complete();
}
}
public static string GetStateValueKey(MigrationPlan plan) => "Umbraco.Core.Upgrader.State+" + plan.Name;
/// <summary>
/// Executes as part of the upgrade scope and before all migrations have executed.
/// </summary>
public virtual void BeforeMigrations(IScope scope, ILogger logger)
{ }
/// <summary>
/// Executes as part of the upgrade scope and after all migrations have executed.
/// </summary>
public virtual void AfterMigrations(IScope scope, ILogger logger)
{ }
}
}
@@ -0,0 +1,27 @@
using Umbraco.Core.Persistence.Dtos;
namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0
{
public class MakeTagsVariant : MigrationBase
{
public MakeTagsVariant(IMigrationContext context)
: base(context)
{ }
public override void Migrate()
{
AddColumn<TagDto>("languageId");
Delete.Index($"IX_{Constants.DatabaseSchema.Tables.Tag}").OnTable(Constants.DatabaseSchema.Tables.Tag).Do();
Create.Index($"IX_{Constants.DatabaseSchema.Tables.Tag}").OnTable(Constants.DatabaseSchema.Tables.Tag)
.OnColumn("group")
.Ascending()
.OnColumn("tag")
.Ascending()
.OnColumn("languageId")
.Ascending()
.WithOptions().Unique()
.Do();
}
}
}
@@ -15,10 +15,10 @@ namespace Umbraco.Core.Models
/// <param name="propertyTypeAlias">The property alias.</param>
/// <param name="tags">The tags.</param>
/// <param name="merge">A value indicating whether to merge the tags with existing tags instead of replacing them.</param>
/// <remarks>Tags do not support variants.</remarks>
public static void AssignTags(this IContentBase content, string propertyTypeAlias, IEnumerable<string> tags, bool merge = false)
/// <param name="culture">A culture, for multi-lingual properties.</param>
public static void AssignTags(this IContentBase content, string propertyTypeAlias, IEnumerable<string> tags, bool merge = false, string culture = null)
{
content.GetTagProperty(propertyTypeAlias).AssignTags(tags, merge);
content.GetTagProperty(propertyTypeAlias).AssignTags(tags, merge, culture);
}
/// <summary>
@@ -27,10 +27,10 @@ namespace Umbraco.Core.Models
/// <param name="content">The content item.</param>
/// <param name="propertyTypeAlias">The property alias.</param>
/// <param name="tags">The tags.</param>
/// <remarks>Tags do not support variants.</remarks>
public static void RemoveTags(this IContentBase content, string propertyTypeAlias, IEnumerable<string> tags)
/// <param name="culture">A culture, for multi-lingual properties.</param>
public static void RemoveTags(this IContentBase content, string propertyTypeAlias, IEnumerable<string> tags, string culture = null)
{
content.GetTagProperty(propertyTypeAlias).RemoveTags(tags);
content.GetTagProperty(propertyTypeAlias).RemoveTags(tags, culture);
}
// gets and validates the property
+6
View File
@@ -20,6 +20,12 @@ namespace Umbraco.Core.Models
[DataMember]
string Text { get; set; }
/// <summary>
/// Gets or sets the tag language.
/// </summary>
[DataMember]
int? LanguageId { get; set; }
/// <summary>
/// Gets the number of nodes tagged with this tag.
/// </summary>
@@ -38,13 +38,13 @@ namespace Umbraco.Core.Models
}
/// <summary>
/// Assign default tags.
/// Assign tags.
/// </summary>
/// <param name="property">The property.</param>
/// <param name="tags">The tags.</param>
/// <param name="merge">A value indicating whether to merge the tags with existing tags instead of replacing them.</param>
/// <remarks>Tags do not support variants.</remarks>
public static void AssignTags(this Property property, IEnumerable<string> tags, bool merge = false)
/// <param name="culture">A culture, for multi-lingual properties.</param>
public static void AssignTags(this Property property, IEnumerable<string> tags, bool merge = false, string culture = null)
{
if (property == null) throw new ArgumentNullException(nameof(property));
@@ -52,11 +52,11 @@ namespace Umbraco.Core.Models
if (configuration == null)
throw new NotSupportedException($"Property with alias \"{property.Alias}\" does not support tags.");
property.AssignTags(tags, merge, configuration.StorageType, configuration.Delimiter);
property.AssignTags(tags, merge, configuration.StorageType, configuration.Delimiter, culture);
}
// assumes that parameters are consistent with the datatype configuration
internal static void AssignTags(this Property property, IEnumerable<string> tags, bool merge, TagsStorageType storageType, char delimiter)
private static void AssignTags(this Property property, IEnumerable<string> tags, bool merge, TagsStorageType storageType, char delimiter, string culture)
{
// set the property value
var trimmedTags = tags.Select(x => x.Trim()).ToArray();
@@ -68,11 +68,11 @@ namespace Umbraco.Core.Models
switch (storageType)
{
case TagsStorageType.Csv:
property.SetValue(string.Join(delimiter.ToString(), currentTags.Union(trimmedTags))); // csv string
property.SetValue(string.Join(delimiter.ToString(), currentTags.Union(trimmedTags)), culture); // csv string
break;
case TagsStorageType.Json:
property.SetValue(JsonConvert.SerializeObject(currentTags.Union(trimmedTags).ToArray())); // json array
property.SetValue(JsonConvert.SerializeObject(currentTags.Union(trimmedTags).ToArray()), culture); // json array
break;
}
}
@@ -81,23 +81,23 @@ namespace Umbraco.Core.Models
switch (storageType)
{
case TagsStorageType.Csv:
property.SetValue(string.Join(delimiter.ToString(), trimmedTags)); // csv string
property.SetValue(string.Join(delimiter.ToString(), trimmedTags), culture); // csv string
break;
case TagsStorageType.Json:
property.SetValue(JsonConvert.SerializeObject(trimmedTags)); // json array
property.SetValue(JsonConvert.SerializeObject(trimmedTags), culture); // json array
break;
}
}
}
/// <summary>
/// Removes default tags.
/// Removes tags.
/// </summary>
/// <param name="property">The property.</param>
/// <param name="tags">The tags.</param>
/// <remarks>Tags do not support variants.</remarks>
public static void RemoveTags(this Property property, IEnumerable<string> tags)
/// <param name="culture">A culture, for multi-lingual properties.</param>
public static void RemoveTags(this Property property, IEnumerable<string> tags, string culture = null)
{
if (property == null) throw new ArgumentNullException(nameof(property));
@@ -105,33 +105,33 @@ namespace Umbraco.Core.Models
if (configuration == null)
throw new NotSupportedException($"Property with alias \"{property.Alias}\" does not support tags.");
property.RemoveTags(tags, configuration.StorageType, configuration.Delimiter);
property.RemoveTags(tags, configuration.StorageType, configuration.Delimiter, culture);
}
// assumes that parameters are consistent with the datatype configuration
private static void RemoveTags(this Property property, IEnumerable<string> tags, TagsStorageType storageType, char delimiter)
private static void RemoveTags(this Property property, IEnumerable<string> tags, TagsStorageType storageType, char delimiter, string culture)
{
// already empty = nothing to do
//fixme doesn't take into account variants
var value = property.GetValue()?.ToString();
var value = property.GetValue(culture)?.ToString();
if (string.IsNullOrWhiteSpace(value)) return;
// set the property value
var trimmedTags = tags.Select(x => x.Trim()).ToArray();
var currentTags = property.GetTagsValue(storageType, delimiter);
var currentTags = property.GetTagsValue(storageType, delimiter, culture);
switch (storageType)
{
case TagsStorageType.Csv:
property.SetValue(string.Join(delimiter.ToString(), currentTags.Except(trimmedTags))); // csv string
property.SetValue(string.Join(delimiter.ToString(), currentTags.Except(trimmedTags)), culture); // csv string
break;
case TagsStorageType.Json:
property.SetValue(JsonConvert.SerializeObject(currentTags.Except(trimmedTags).ToArray())); // json array
property.SetValue(JsonConvert.SerializeObject(currentTags.Except(trimmedTags).ToArray()), culture); // json array
break;
}
}
internal static IEnumerable<string> GetTagsValue(this Property property)
// used by ContentRepositoryBase
internal static IEnumerable<string> GetTagsValue(this Property property, string culture = null)
{
if (property == null) throw new ArgumentNullException(nameof(property));
@@ -139,15 +139,14 @@ namespace Umbraco.Core.Models
if (configuration == null)
throw new NotSupportedException($"Property with alias \"{property.Alias}\" does not support tags.");
return property.GetTagsValue(configuration.StorageType, configuration.Delimiter);
return property.GetTagsValue(configuration.StorageType, configuration.Delimiter, culture);
}
internal static IEnumerable<string> GetTagsValue(this Property property, TagsStorageType storageType, char delimiter)
private static IEnumerable<string> GetTagsValue(this Property property, TagsStorageType storageType, char delimiter, string culture = null)
{
if (property == null) throw new ArgumentNullException(nameof(property));
//fixme doesn't take into account variants
var value = property.GetValue()?.ToString();
var value = property.GetValue(culture)?.ToString();
if (string.IsNullOrWhiteSpace(value)) return Enumerable.Empty<string>();
switch (storageType)
@@ -158,7 +157,6 @@ namespace Umbraco.Core.Models
case TagsStorageType.Json:
try
{
//fixme doesn't take into account variants
return JsonConvert.DeserializeObject<JArray>(value).Select(x => x.ToString().Trim());
}
catch (JsonException)
@@ -178,34 +176,33 @@ namespace Umbraco.Core.Models
/// <param name="property">The property.</param>
/// <param name="value">The property value.</param>
/// <param name="tagConfiguration">The datatype configuration.</param>
/// <remarks>
/// <param name="culture">A culture, for multi-lingual properties.</param>
/// <remarks>
/// <para>The value is either a string (delimited string) or an enumeration of strings (tag list).</para>
/// <para>This is used both by the content repositories to initialize a property with some tag values, and by the
/// content controllers to update a property with values received from the property editor.</para>
/// </remarks>
internal static void SetTagsValue(this Property property, object value, TagConfiguration tagConfiguration)
internal static void SetTagsValue(this Property property, object value, TagConfiguration tagConfiguration, string culture)
{
if (property == null) throw new ArgumentNullException(nameof(property));
if (tagConfiguration == null) throw new ArgumentNullException(nameof(tagConfiguration));
var merge = false; // fixme always!
var storageType = tagConfiguration.StorageType;
var delimiter = tagConfiguration.Delimiter;
SetTagsValue(property, value, merge, storageType, delimiter);
SetTagsValue(property, value, storageType, delimiter, culture);
}
// assumes that parameters are consistent with the datatype configuration
// value can be an enumeration of string, or a serialized value using storageType format
// fixme merge always false here?!
private static void SetTagsValue(Property property, object value, bool merge, TagsStorageType storageType, char delimiter)
private static void SetTagsValue(Property property, object value, TagsStorageType storageType, char delimiter, string culture)
{
if (value == null) value = Enumerable.Empty<string>();
// if value is already an enumeration of strings, just use it
if (value is IEnumerable<string> tags1)
{
property.AssignTags(tags1, merge, storageType, delimiter);
property.AssignTags(tags1, false, storageType, delimiter, culture);
return;
}
@@ -214,14 +211,14 @@ namespace Umbraco.Core.Models
{
case TagsStorageType.Csv:
var tags2 = value.ToString().Split(new[] { delimiter }, StringSplitOptions.RemoveEmptyEntries);
property.AssignTags(tags2, merge, storageType, delimiter);
property.AssignTags(tags2, false, storageType, delimiter, culture);
break;
case TagsStorageType.Json:
try
{
var tags3 = JsonConvert.DeserializeObject<IEnumerable<string>>(value.ToString());
property.AssignTags(tags3 ?? Enumerable.Empty<string>(), merge, storageType, delimiter);
property.AssignTags(tags3 ?? Enumerable.Empty<string>(), false, storageType, delimiter, culture);
}
catch (Exception ex)
{
+11 -1
View File
@@ -16,6 +16,7 @@ namespace Umbraco.Core.Models
private string _group;
private string _text;
private int? _languageId;
/// <summary>
/// Initializes a new instance of the <see cref="Tag"/> class.
@@ -26,11 +27,12 @@ namespace Umbraco.Core.Models
/// <summary>
/// Initializes a new instance of the <see cref="Tag"/> class.
/// </summary>
public Tag(int id, string group, string text)
public Tag(int id, string group, string text, int? languageId = null)
{
Id = id;
Text = text;
Group = group;
LanguageId = languageId;
}
private static PropertySelectors Selectors => _selectors ?? (_selectors = new PropertySelectors());
@@ -39,6 +41,7 @@ namespace Umbraco.Core.Models
{
public readonly PropertyInfo Group = ExpressionHelper.GetPropertyInfo<Tag, string>(x => x.Group);
public readonly PropertyInfo Text = ExpressionHelper.GetPropertyInfo<Tag, string>(x => x.Text);
public readonly PropertyInfo LanguageId = ExpressionHelper.GetPropertyInfo<Tag, int?>(x => x.LanguageId);
}
/// <inheritdoc />
@@ -55,6 +58,13 @@ namespace Umbraco.Core.Models
set => SetPropertyValueAndDetectChanges(value, ref _text, Selectors.Text);
}
/// <inheritdoc />
public int? LanguageId
{
get => _languageId;
set => SetPropertyValueAndDetectChanges(value, ref _languageId, Selectors.LanguageId);
}
/// <inheritdoc />
public int NodeCount { get; internal set; }
}
+9 -6
View File
@@ -5,10 +5,13 @@ namespace Umbraco.Core.Models
/// <summary>
/// Represents a tagged entity.
/// </summary>
/// <remarks>Note that it is the properties of an entity (like Content, Media, Members, etc.) that is tagged,
/// which is why this class is composed of a list of tagged properties and an Id reference to the actual entity.</remarks>
/// <remarks>Note that it is the properties of an entity (like Content, Media, Members, etc.) that are tagged,
/// which is why this class is composed of a list of tagged properties and the identifier the actual entity.</remarks>
public class TaggedEntity
{
/// <summary>
/// Initializes a new instance of the <see cref="TaggedEntity"/> class.
/// </summary>
public TaggedEntity(int entityId, IEnumerable<TaggedProperty> taggedProperties)
{
EntityId = entityId;
@@ -16,13 +19,13 @@ namespace Umbraco.Core.Models
}
/// <summary>
/// Id of the entity, which is tagged
/// Gets the identifier of the entity.
/// </summary>
public int EntityId { get; private set; }
public int EntityId { get; }
/// <summary>
/// An enumerable list of tagged properties
/// Gets the tagged properties.
/// </summary>
public IEnumerable<TaggedProperty> TaggedProperties { get; private set; }
public IEnumerable<TaggedProperty> TaggedProperties { get; }
}
}
+9 -6
View File
@@ -7,6 +7,9 @@ namespace Umbraco.Core.Models
/// </summary>
public class TaggedProperty
{
/// <summary>
/// Initializes a new instance of the <see cref="TaggedProperty"/> class.
/// </summary>
public TaggedProperty(int propertyTypeId, string propertyTypeAlias, IEnumerable<ITag> tags)
{
PropertyTypeId = propertyTypeId;
@@ -15,18 +18,18 @@ namespace Umbraco.Core.Models
}
/// <summary>
/// Id of the PropertyType, which this tagged property is based on
/// Gets the identifier of the property type.
/// </summary>
public int PropertyTypeId { get; private set; }
public int PropertyTypeId { get; }
/// <summary>
/// Alias of the PropertyType, which this tagged property is based on
/// Gets the alias of the property type.
/// </summary>
public string PropertyTypeAlias { get; private set; }
public string PropertyTypeAlias { get; }
/// <summary>
/// An enumerable list of Tags for the property
/// Gets the tags.
/// </summary>
public IEnumerable<ITag> Tags { get; private set; }
public IEnumerable<ITag> Tags { get; }
}
}
+2 -1
View File
@@ -595,7 +595,6 @@ namespace Umbraco.Core
return null;
}
/// <summary>
/// Attempts to serialize the value to an XmlString using ToXmlString
/// </summary>
@@ -788,5 +787,7 @@ namespace Umbraco.Core
return BoolConvertCache[type] = false;
}
}
}
+10 -2
View File
@@ -3,11 +3,13 @@ using Umbraco.Core.Persistence.DatabaseAnnotations;
namespace Umbraco.Core.Persistence.Dtos
{
[TableName(Constants.DatabaseSchema.Tables.Tag)]
[TableName(TableName)]
[PrimaryKey("id")]
[ExplicitColumns]
internal class TagDto
{
public const string TableName = Constants.DatabaseSchema.Tables.Tag;
[Column("id")]
[PrimaryKeyColumn]
public int Id { get; set; }
@@ -16,9 +18,15 @@ namespace Umbraco.Core.Persistence.Dtos
[Length(100)]
public string Group { get; set; }
[Column("languageId")]
[ForeignKey(typeof(LanguageDto))]
[Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_LanguageId")]
[NullSetting(NullSetting = NullSettings.Null)]
public int? LanguageId { get;set; }
[Column("tag")]
[Length(200)]
[Index(IndexTypes.UniqueNonClustered, ForColumns = "group,tag", Name = "IX_cmsTags")]
[Index(IndexTypes.UniqueNonClustered, ForColumns = "group,tag,languageId", Name = "IX_cmsTags")]
public string Text { get; set; }
//[Column("key")]
@@ -3,11 +3,13 @@ using Umbraco.Core.Persistence.DatabaseAnnotations;
namespace Umbraco.Core.Persistence.Dtos
{
[TableName(Constants.DatabaseSchema.Tables.TagRelationship)]
[TableName(TableName)]
[PrimaryKey("nodeId", AutoIncrement = false)]
[ExplicitColumns]
internal class TagRelationshipDto
{
public const string TableName = Constants.DatabaseSchema.Tables.TagRelationship;
[Column("nodeId")]
[PrimaryKeyColumn(AutoIncrement = false, Name = "PK_cmsTagRelationship", OnColumns = "nodeId, propertyTypeId, tagId")]
[ForeignKey(typeof(ContentDto), Name = "FK_cmsTagRelationship_cmsContent", Column = "nodeId")]
@@ -7,7 +7,7 @@ namespace Umbraco.Core.Persistence.Factories
{
public static ITag BuildEntity(TagDto dto)
{
var entity = new Tag(dto.Id, dto.Group, dto.Text) { NodeCount = dto.NodeCount };
var entity = new Tag(dto.Id, dto.Group, dto.Text, dto.LanguageId) { NodeCount = dto.NodeCount };
// reset dirty initial properties (U4-1946)
entity.ResetDirtyProperties(false);
return entity;
@@ -20,6 +20,7 @@ namespace Umbraco.Core.Persistence.Factories
Id = entity.Id,
Group = entity.Group,
Text = entity.Text,
LanguageId = entity.LanguageId
//Key = entity.Group + "/" + entity.Text // de-normalize
};
}
@@ -23,6 +23,7 @@ namespace Umbraco.Core.Persistence.Mappers
CacheMap<Tag, TagDto>(src => src.Id, dto => dto.Id);
CacheMap<Tag, TagDto>(src => src.Text, dto => dto.Text);
CacheMap<Tag, TagDto>(src => src.Group, dto => dto.Group);
CacheMap<Tag, TagDto>(src => src.LanguageId, dto => dto.LanguageId);
}
}
}
@@ -76,7 +76,7 @@ namespace Umbraco.Core.Persistence
var (s, a) = sql.SqlContext.VisitDto(predicate, alias);
return sql.Where(s, a);
}
/// <summary>
/// Appends a WHERE clause to the Sql statement.
/// </summary>
@@ -589,11 +589,14 @@ namespace Umbraco.Core.Persistence
/// Creates a SELECT COUNT(*) Sql statement.
/// </summary>
/// <param name="sql">The origin sql.</param>
/// <param name="alias">An optional alias.</param>
/// <returns>The Sql statement.</returns>
public static Sql<ISqlContext> SelectCount(this Sql<ISqlContext> sql)
public static Sql<ISqlContext> SelectCount(this Sql<ISqlContext> sql, string alias = null)
{
if (sql == null) throw new ArgumentNullException(nameof(sql));
return sql.Select("COUNT(*)");
var text = "COUNT(*)";
if (alias != null) text += " AS " + sql.SqlContext.SqlSyntax.GetQuotedColumnName(alias);
return sql.Select(text);
}
/// <summary>
@@ -607,13 +610,29 @@ namespace Umbraco.Core.Persistence
/// <para>If <paramref name="fields"/> is empty, all columns are counted.</para>
/// </remarks>
public static Sql<ISqlContext> SelectCount<TDto>(this Sql<ISqlContext> sql, params Expression<Func<TDto, object>>[] fields)
=> sql.SelectCount(null, fields);
/// <summary>
/// Creates a SELECT COUNT Sql statement.
/// </summary>
/// <typeparam name="TDto">The type of the DTO to count.</typeparam>
/// <param name="sql">The origin sql.</param>
/// <param name="alias">An alias.</param>
/// <param name="fields">Expressions indicating the columns to count.</param>
/// <returns>The Sql statement.</returns>
/// <remarks>
/// <para>If <paramref name="fields"/> is empty, all columns are counted.</para>
/// </remarks>
public static Sql<ISqlContext> SelectCount<TDto>(this Sql<ISqlContext> sql, string alias, params Expression<Func<TDto, object>>[] fields)
{
if (sql == null) throw new ArgumentNullException(nameof(sql));
var sqlSyntax = sql.SqlContext.SqlSyntax;
var columns = fields.Length == 0
? sql.GetColumns<TDto>(withAlias: false)
: fields.Select(x => sqlSyntax.GetFieldName(x)).ToArray();
return sql.Select("COUNT (" + string.Join(", ", columns) + ")");
var text = "COUNT (" + string.Join(", ", columns) + ")";
if (alias != null) text += " AS " + sql.SqlContext.SqlSyntax.GetQuotedColumnName(alias);
return sql.Select(text);
}
/// <summary>
@@ -643,6 +662,26 @@ namespace Umbraco.Core.Persistence
return sql.Select(sql.GetColumns(columnExpressions: fields));
}
/// <summary>
/// Creates a SELECT DISTINCT Sql statement.
/// </summary>
/// <typeparam name="TDto">The type of the DTO to select.</typeparam>
/// <param name="sql">The origin sql.</param>
/// <param name="fields">Expressions indicating the columns to select.</param>
/// <returns>The Sql statement.</returns>
/// <remarks>
/// <para>If <paramref name="fields"/> is empty, all columns are selected.</para>
/// </remarks>
public static Sql<ISqlContext> SelectDistinct<TDto>(this Sql<ISqlContext> sql, params Expression<Func<TDto, object>>[] fields)
{
if (sql == null) throw new ArgumentNullException(nameof(sql));
var columns = sql.GetColumns(columnExpressions: fields);
sql.Append("SELECT DISTINCT " + string.Join(", ", columns));
return sql;
}
//this.Append("SELECT " + string.Join(", ", columns), new object[0]);
/// <summary>
/// Creates a SELECT Sql statement.
/// </summary>
@@ -705,6 +744,56 @@ namespace Umbraco.Core.Persistence
return sql.Append(", " + string.Join(", ", sql.GetColumns(tableAlias: tableAlias, columnExpressions: fields)));
}
/// <summary>
/// Adds a COUNT(*) to a SELECT Sql statement.
/// </summary>
/// <param name="sql">The origin sql.</param>
/// <param name="alias">An optional alias.</param>
/// <returns>The Sql statement.</returns>
public static Sql<ISqlContext> AndSelectCount(this Sql<ISqlContext> sql, string alias = null)
{
if (sql == null) throw new ArgumentNullException(nameof(sql));
var text = ", COUNT(*)";
if (alias != null) text += " AS " + sql.SqlContext.SqlSyntax.GetQuotedColumnName(alias);
return sql.Append(text);
}
/// <summary>
/// Adds a COUNT to a SELECT Sql statement.
/// </summary>
/// <typeparam name="TDto">The type of the DTO to count.</typeparam>
/// <param name="sql">The origin sql.</param>
/// <param name="fields">Expressions indicating the columns to count.</param>
/// <returns>The Sql statement.</returns>
/// <remarks>
/// <para>If <paramref name="fields"/> is empty, all columns are counted.</para>
/// </remarks>
public static Sql<ISqlContext> AndSelectCount<TDto>(this Sql<ISqlContext> sql, params Expression<Func<TDto, object>>[] fields)
=> sql.AndSelectCount(null, fields);
/// <summary>
/// Adds a COUNT to a SELECT Sql statement.
/// </summary>
/// <typeparam name="TDto">The type of the DTO to count.</typeparam>
/// <param name="sql">The origin sql.</param>
/// <param name="alias">An alias.</param>
/// <param name="fields">Expressions indicating the columns to count.</param>
/// <returns>The Sql statement.</returns>
/// <remarks>
/// <para>If <paramref name="fields"/> is empty, all columns are counted.</para>
/// </remarks>
public static Sql<ISqlContext> AndSelectCount<TDto>(this Sql<ISqlContext> sql, string alias = null, params Expression<Func<TDto, object>>[] fields)
{
if (sql == null) throw new ArgumentNullException(nameof(sql));
var sqlSyntax = sql.SqlContext.SqlSyntax;
var columns = fields.Length == 0
? sql.GetColumns<TDto>(withAlias: false)
: fields.Select(x => sqlSyntax.GetFieldName(x)).ToArray();
var text = ", COUNT (" + string.Join(", ", columns) + ")";
if (alias != null) text += " AS " + sql.SqlContext.SqlSyntax.GetQuotedColumnName(alias);
return sql.Append(text);
}
/// <summary>
/// Creates a SELECT Sql statement with a referenced Dto.
/// </summary>
@@ -1115,12 +1204,37 @@ namespace Umbraco.Core.Persistence
return string.IsNullOrWhiteSpace(attr?.Name) ? column.Name : attr.Name;
}
internal static void WriteToConsole(this Sql sql)
internal static string ToText(this Sql sql)
{
Console.WriteLine(sql.SQL);
var text = new StringBuilder();
sql.ToText(text);
return text.ToString();
}
internal static void ToText(this Sql sql, StringBuilder text)
{
ToText(sql.SQL, sql.Arguments, text);
}
internal static void ToText(string sql, object[] arguments, StringBuilder text)
{
text.AppendLine(sql);
if (arguments == null || arguments.Length == 0)
return;
text.Append(" --");
var i = 0;
foreach (var arg in sql.Arguments)
Console.WriteLine($" @{i++}: {arg}");
foreach (var arg in arguments)
{
text.Append(" @");
text.Append(i++);
text.Append(":");
text.Append(arg);
}
text.AppendLine();
}
#endregion
@@ -653,6 +653,23 @@ namespace Umbraco.Core.Persistence.Querying
else
throw new NotSupportedException("Expression is not a proper lambda.");
// c# 'x == null' becomes sql 'x IS NULL' which is fine
// c# 'x == y' becomes sql 'x = @0' which is fine - unless they are nullable types,
// because sql 'x = NULL' is always false and the 'IS NULL' syntax is required,
// so for comparing nullable types, we use x.SqlNullableEquals(y, fb) where fb is a fallback
// value which will be used when values are null - turning the comparison into
// sql 'COALESCE(x,fb) = COALESCE(y,fb)' - of course, fb must be a value outside
// of x and y range - and if that is not possible, then a manual comparison need
// to be written
//TODO support SqlNullableEquals with 0 parameters, using the full syntax below
case "SqlNullableEquals":
var compareTo = Visit(m.Arguments[1]);
var fallback = Visit(m.Arguments[2]);
// that would work without a fallback value but is more cumbersome
//return Visited ? string.Empty : $"((({compareTo} is null) AND ({visitedMethodObject} is null)) OR (({compareTo} is not null) AND ({visitedMethodObject} = {compareTo})))";
// use a fallback value
return Visited ? string.Empty : $"(COALESCE({visitedMethodObject},{fallback}) = COALESCE({compareTo},{fallback}))";
default:
throw new ArgumentOutOfRangeException("No logic supported for " + m.Method.Name);
@@ -9,6 +9,21 @@ namespace Umbraco.Core.Persistence.Querying
/// </summary>
internal static class SqlExpressionExtensions
{
/// <summary>
/// Indicates whether two nullable values are equal, substituting a fallback value for nulls.
/// </summary>
/// <typeparam name="T">The nullable type.</typeparam>
/// <param name="value">The value to compare.</param>
/// <param name="other">The value to compare to.</param>
/// <param name="fallbackValue">The value to use when any value is null.</param>
/// <remarks>Do not use outside of Sql expressions.</remarks>
// see usage in ExpressionVisitorBase
public static bool SqlNullableEquals<T>(this T? value, T? other, T fallbackValue)
where T : struct
{
return (value ?? fallbackValue).Equals(other ?? fallbackValue);
}
public static bool SqlIn<T>(this IEnumerable<T> collection, T item)
{
return collection.Contains(item);
@@ -19,7 +19,8 @@ namespace Umbraco.Core.Persistence.Repositories
/// <para>When <paramref name="replaceTags"/> is false, the tags specified in <paramref name="tags"/> are added to those already assigned.</para>
/// <para>When <paramref name="tags"/> is empty and <paramref name="replaceTags"/> is true, all assigned tags are removed.</para>
/// </remarks>
void Assign(int contentId, int propertyTypeId, IEnumerable<ITag> tags, bool replaceTags);
// TODO: replaceTags is used as 'false' in tests exclusively - should get rid of it
void Assign(int contentId, int propertyTypeId, IEnumerable<ITag> tags, bool replaceTags = true);
/// <summary>
/// Removes assigned tags from a content property.
@@ -46,54 +47,48 @@ namespace Umbraco.Core.Persistence.Repositories
#region Queries
/// <summary>
/// Gets a tagged entity.
/// </summary>
TaggedEntity GetTaggedEntityByKey(Guid key);
/// <summary>
/// Gets a tagged entity.
/// </summary>
TaggedEntity GetTaggedEntityById(int id);
IEnumerable<TaggedEntity> GetTaggedEntitiesByTagGroup(TaggableObjectTypes objectType, string tagGroup);
IEnumerable<TaggedEntity> GetTaggedEntitiesByTag(TaggableObjectTypes objectType, string tag, string tagGroup = null);
/// Gets all entities of a type, tagged with any tag in the specified group.
IEnumerable<TaggedEntity> GetTaggedEntitiesByTagGroup(TaggableObjectTypes objectType, string group, string culture = null);
/// <summary>
/// Returns all tags for an entity type (content/media/member)
/// Gets all entities of a type, tagged with the specified tag.
/// </summary>
/// <param name="objectType">Entity type</param>
/// <param name="group">Optional group</param>
/// <returns></returns>
IEnumerable<ITag> GetTagsForEntityType(TaggableObjectTypes objectType, string group = null);
IEnumerable<TaggedEntity> GetTaggedEntitiesByTag(TaggableObjectTypes objectType, string tag, string group = null, string culture = null);
/// <summary>
/// Returns all tags that exist on the content item - Content/Media/Member
/// Gets all tags for an entity type.
/// </summary>
/// <param name="contentId">The content item id to get tags for</param>
/// <param name="group">Optional group</param>
/// <returns></returns>
IEnumerable<ITag> GetTagsForEntity(int contentId, string group = null);
IEnumerable<ITag> GetTagsForEntityType(TaggableObjectTypes objectType, string group = null, string culture = null);
/// <summary>
/// Returns all tags that exist on the content item - Content/Media/Member
/// Gets all tags attached to an entity.
/// </summary>
/// <param name="contentId">The content item id to get tags for</param>
/// <param name="group">Optional group</param>
/// <returns></returns>
IEnumerable<ITag> GetTagsForEntity(Guid contentId, string group = null);
IEnumerable<ITag> GetTagsForEntity(int contentId, string group = null, string culture = null);
/// <summary>
/// Returns all tags that exist on the content item for the property specified - Content/Media/Member
/// Gets all tags attached to an entity.
/// </summary>
/// <param name="contentId">The content item id to get tags for</param>
/// <param name="propertyTypeAlias">The property alias to get tags for</param>
/// <param name="group">Optional group</param>
/// <returns></returns>
IEnumerable<ITag> GetTagsForProperty(int contentId, string propertyTypeAlias, string group = null);
IEnumerable<ITag> GetTagsForEntity(Guid contentId, string group = null, string culture = null);
/// <summary>
/// Returns all tags that exist on the content item for the property specified - Content/Media/Member
/// Gets all tags attached to an entity via a property.
/// </summary>
/// <param name="contentId">The content item id to get tags for</param>
/// <param name="propertyTypeAlias">The property alias to get tags for</param>
/// <param name="group">Optional group</param>
/// <returns></returns>
IEnumerable<ITag> GetTagsForProperty(Guid contentId, string propertyTypeAlias, string group = null);
IEnumerable<ITag> GetTagsForProperty(int contentId, string propertyTypeAlias, string group = null, string culture = null);
/// <summary>
/// Gets all tags attached to an entity via a property.
/// </summary>
IEnumerable<ITag> GetTagsForProperty(Guid contentId, string propertyTypeAlias, string group = null, string culture = null);
#endregion
}
@@ -217,8 +217,26 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
foreach (var property in entity.Properties)
{
var tagConfiguration = property.GetTagConfiguration();
if (tagConfiguration == null) continue;
tagRepo.Assign(entity.Id, property.PropertyTypeId, property.GetTagsValue().Select(x => new Tag { Group = tagConfiguration.Group, Text = x }), true);
if (tagConfiguration == null) continue; // not a tags property
if (property.PropertyType.VariesByCulture())
{
var tags = new List<ITag>();
foreach (var pvalue in property.Values)
{
var tagsValue = property.GetTagsValue(pvalue.Culture);
var languageId = LanguageRepository.GetIdByIsoCode(pvalue.Culture);
var cultureTags = tagsValue.Select(x => new Tag { Group = tagConfiguration.Group, Text = x, LanguageId = languageId });
tags.AddRange(cultureTags);
}
tagRepo.Assign(entity.Id, property.PropertyTypeId, tags);
}
else
{
var tagsValue = property.GetTagsValue(); // strings
var tags = tagsValue.Select(x => new Tag { Group = tagConfiguration.Group, Text = x });
tagRepo.Assign(entity.Id, property.PropertyTypeId, tags);
}
}
}
@@ -541,16 +559,6 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
propertyDataDtos.AddRange(propertyDataDtos2);
var properties = PropertyFactory.BuildEntities(compositionProperties, propertyDataDtos, temp.PublishedVersionId, LanguageRepository).ToList();
// deal with tags
foreach (var property in properties)
{
if (!tagConfigurations.TryGetValue(property.PropertyType.PropertyEditorAlias, out var tagConfiguration))
continue;
//fixme doesn't take into account variants
property.SetTagsValue(property.GetValue(), tagConfiguration);
}
if (result.ContainsKey(temp.VersionId))
{
if (ContentRepositoryBase.ThrowOnWarning)
@@ -663,9 +663,11 @@ AND umbracoNode.id <> @id",
{
case ContentVariation.Culture:
CopyPropertyData(null, defaultLanguageId, propertyTypeIds, impactedL);
CopyTagData(null, defaultLanguageId, propertyTypeIds, impactedL);
break;
case ContentVariation.Nothing:
CopyPropertyData(defaultLanguageId, null, propertyTypeIds, impactedL);
CopyTagData(defaultLanguageId, null, propertyTypeIds, impactedL);
break;
case ContentVariation.CultureAndSegment:
case ContentVariation.Segment:
@@ -757,6 +759,139 @@ AND umbracoNode.id <> @id",
}
}
///
private void CopyTagData(int? sourceLanguageId, int? targetLanguageId, IReadOnlyCollection<int> propertyTypeIds, IReadOnlyCollection<int> contentTypeIds = null)
{
// note: important to use SqlNullableEquals for nullable types, cannot directly compare language identifiers
// fixme - should we batch then?
var whereInArgsCount = propertyTypeIds.Count + (contentTypeIds?.Count ?? 0);
if (whereInArgsCount > 2000)
throw new NotSupportedException("Too many property/content types.");
// delete existing relations (for target language)
// do *not* delete existing tags
var sqlSelectTagsToDelete = Sql()
.Select<TagDto>(x => x.Id)
.From<TagDto>()
.InnerJoin<TagRelationshipDto>().On<TagDto, TagRelationshipDto>((tag, rel) => tag.Id == rel.TagId);
if (contentTypeIds != null)
sqlSelectTagsToDelete
.InnerJoin<ContentDto>().On<TagRelationshipDto, ContentDto>((rel, content) => rel.NodeId == content.NodeId)
.WhereIn<ContentDto>(x => x.ContentTypeId, contentTypeIds);
sqlSelectTagsToDelete
.WhereIn<TagRelationshipDto>(x => x.PropertyTypeId, propertyTypeIds)
.Where<TagDto>(x => x.LanguageId.SqlNullableEquals(targetLanguageId, -1));
var sqlDeleteRelations = Sql()
.Delete<TagRelationshipDto>()
.WhereIn<TagRelationshipDto>(x => x.TagId, sqlSelectTagsToDelete);
Database.Execute(sqlDeleteRelations);
// do *not* delete the tags - they could be used by other content types / property types
/*
var sqlDeleteTag = Sql()
.Delete<TagDto>()
.WhereIn<TagDto>(x => x.Id, sqlTagToDelete);
Database.Execute(sqlDeleteTag);
*/
// copy tags from source language to target language
// target tags may exist already, so we have to check for existence here
//
// select tags to insert: tags pointed to by a relation ship, for proper property/content types,
// and of source language, and where we cannot left join to an existing tag with same text,
// group and languageId
var targetLanguageIdS = targetLanguageId.HasValue ? targetLanguageId.ToString() : "NULL";
var sqlSelectTagsToInsert = Sql()
.SelectDistinct<TagDto>(x => x.Text, x => x.Group)
.Append(", " + targetLanguageIdS)
.From<TagDto>();
sqlSelectTagsToInsert
.InnerJoin<TagRelationshipDto>().On<TagDto, TagRelationshipDto>((tag, rel) => tag.Id == rel.TagId)
.LeftJoin<TagDto>("xtags").On<TagDto, TagDto>((tag, xtag) => tag.Text == xtag.Text && tag.Group == xtag.Group && xtag.LanguageId.SqlNullableEquals(targetLanguageId, -1), aliasRight: "xtags");
if (contentTypeIds != null)
sqlSelectTagsToInsert
.InnerJoin<ContentDto>().On<TagRelationshipDto, ContentDto>((rel, content) => rel.NodeId == content.NodeId)
.WhereIn<ContentDto>(x => x.ContentTypeId, contentTypeIds);
sqlSelectTagsToInsert
.WhereIn<TagRelationshipDto>(x => x.PropertyTypeId, propertyTypeIds)
.WhereNull<TagDto>(x => x.Id, "xtags") // ie, not exists
.Where<TagDto>(x => x.LanguageId.SqlNullableEquals(sourceLanguageId, -1));
var cols = Sql().Columns<TagDto>(x => x.Text, x => x.Group, x => x.LanguageId);
var sqlInsertTags = Sql($"INSERT INTO {TagDto.TableName} ({cols})").Append(sqlSelectTagsToInsert);
Database.Execute(sqlInsertTags);
// create relations to new tags
// any existing relations have been deleted above, no need to check for existence here
//
// select node id and property type id from existing relations to tags of source language,
// for proper property/content types, and select new tag id from tags, with matching text,
// and group, but for the target language
var sqlSelectRelationsToInsert = Sql()
.SelectDistinct<TagRelationshipDto>(x => x.NodeId, x => x.PropertyTypeId)
.AndSelect<TagDto>("otag", x => x.Id)
.From<TagRelationshipDto>()
.InnerJoin<TagDto>().On<TagRelationshipDto, TagDto>((rel, tag) => rel.TagId == tag.Id)
.InnerJoin<TagDto>("otag").On<TagDto, TagDto>((tag, otag) => tag.Text == otag.Text && tag.Group == otag.Group && otag.LanguageId.SqlNullableEquals(targetLanguageId, -1), aliasRight: "otag");
if (contentTypeIds != null)
sqlSelectRelationsToInsert
.InnerJoin<ContentDto>().On<TagRelationshipDto, ContentDto>((rel, content) => rel.NodeId == content.NodeId)
.WhereIn<ContentDto>(x => x.ContentTypeId, contentTypeIds);
sqlSelectRelationsToInsert
.Where<TagDto>(x => x.LanguageId.SqlNullableEquals(sourceLanguageId, -1))
.WhereIn<TagRelationshipDto>(x => x.PropertyTypeId, propertyTypeIds);
var relationColumnsToInsert = Sql().Columns<TagRelationshipDto>(x => x.NodeId, x => x.PropertyTypeId, x => x.TagId);
var sqlInsertRelations = Sql($"INSERT INTO {TagRelationshipDto.TableName} ({relationColumnsToInsert})").Append(sqlSelectRelationsToInsert);
Database.Execute(sqlInsertRelations);
// delete original relations - *not* the tags - all of them
// cannot really "go back" with relations, would have to do it with property values
sqlSelectTagsToDelete = Sql()
.Select<TagDto>(x => x.Id)
.From<TagDto>()
.InnerJoin<TagRelationshipDto>().On<TagDto, TagRelationshipDto>((tag, rel) => tag.Id == rel.TagId);
if (contentTypeIds != null)
sqlSelectTagsToDelete
.InnerJoin<ContentDto>().On<TagRelationshipDto, ContentDto>((rel, content) => rel.NodeId == content.NodeId)
.WhereIn<ContentDto>(x => x.ContentTypeId, contentTypeIds);
sqlSelectTagsToDelete
.WhereIn<TagRelationshipDto>(x => x.PropertyTypeId, propertyTypeIds)
.Where<TagDto>(x => !x.LanguageId.SqlNullableEquals(targetLanguageId, -1));
sqlDeleteRelations = Sql()
.Delete<TagRelationshipDto>()
.WhereIn<TagRelationshipDto>(x => x.TagId, sqlSelectTagsToDelete);
Database.Execute(sqlDeleteRelations);
// no
/*
var sqlDeleteTag = Sql()
.Delete<TagDto>()
.WhereIn<TagDto>(x => x.Id, sqlTagToDelete);
Database.Execute(sqlDeleteTag);
*/
}
/// <summary>
/// Copies property data from one language to another.
/// </summary>
@@ -766,6 +901,8 @@ AND umbracoNode.id <> @id",
/// <param name="contentTypeIds">The content type identifiers.</param>
private void CopyPropertyData(int? sourceLanguageId, int? targetLanguageId, IReadOnlyCollection<int> propertyTypeIds, IReadOnlyCollection<int> contentTypeIds = null)
{
// note: important to use SqlNullableEquals for nullable types, cannot directly compare language identifiers
//
// fixme - should we batch then?
var whereInArgsCount = propertyTypeIds.Count + (contentTypeIds?.Count ?? 0);
if (whereInArgsCount > 2000)
@@ -793,11 +930,7 @@ AND umbracoNode.id <> @id",
sqlDelete.WhereIn<PropertyDataDto>(x => x.VersionId, inSql);
}
// NPoco cannot turn the clause into IS NULL with a nullable parameter - deal with it
if (targetLanguageId == null)
sqlDelete.Where<PropertyDataDto>(x => x.LanguageId == null);
else
sqlDelete.Where<PropertyDataDto>(x => x.LanguageId == targetLanguageId);
sqlDelete.Where<PropertyDataDto>(x => x.LanguageId.SqlNullableEquals(targetLanguageId, -1));
sqlDelete
.WhereIn<PropertyDataDto>(x => x.PropertyTypeId, propertyTypeIds);
@@ -821,11 +954,7 @@ AND umbracoNode.id <> @id",
.InnerJoin<ContentVersionDto>().On<PropertyDataDto, ContentVersionDto>((pdata, cversion) => pdata.VersionId == cversion.Id)
.InnerJoin<ContentDto>().On<ContentVersionDto, ContentDto>((cversion, c) => cversion.NodeId == c.NodeId);
// NPoco cannot turn the clause into IS NULL with a nullable parameter - deal with it
if (sourceLanguageId == null)
sqlSelectData.Where<PropertyDataDto>(x => x.LanguageId == null);
else
sqlSelectData.Where<PropertyDataDto>(x => x.LanguageId == sourceLanguageId);
sqlSelectData.Where<PropertyDataDto>(x => x.LanguageId.SqlNullableEquals(sourceLanguageId, -1));
sqlSelectData
.WhereIn<PropertyDataDto>(x => x.PropertyTypeId, propertyTypeIds);
@@ -111,7 +111,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
"DELETE FROM umbracoPropertyData WHERE languageId = @id",
"DELETE FROM umbracoContentVersionCultureVariation WHERE languageId = @id",
"DELETE FROM umbracoDocumentCultureVariation WHERE languageId = @id",
"DELETE FROM umbracoLanguage WHERE id = @id"
"DELETE FROM umbracoLanguage WHERE id = @id",
"DELETE FROM " + Constants.DatabaseSchema.Tables.Tag + " WHERE languageId = @id"
};
return list;
}
@@ -11,6 +11,7 @@ using Umbraco.Core.Persistence.Dtos;
using Umbraco.Core.Persistence.Factories;
using Umbraco.Core.Persistence.Querying;
using Umbraco.Core.Scoping;
using static Umbraco.Core.Persistence.NPocoSqlExtensions.Statics;
namespace Umbraco.Core.Persistence.Repositories.Implement
{
@@ -109,74 +110,65 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
#region Assign and Remove Tags
/// <inheritdoc />
public void Assign(int contentId, int propertyTypeId, IEnumerable<ITag> tags, bool replaceTags)
// only invoked from ContentRepositoryBase with all cultures + replaceTags being true
public void Assign(int contentId, int propertyTypeId, IEnumerable<ITag> tags, bool replaceTags = true)
{
// to no-duplicates array
var tagsA = tags.Distinct(new TagComparer()).ToArray();
// no tags?
if (tagsA.Length == 0)
// replacing = clear all
if (replaceTags)
{
// replacing = clear all
if (replaceTags)
{
var sql0 = Sql().Delete<TagRelationshipDto>().Where<TagRelationshipDto>(x => x.NodeId == contentId && x.PropertyTypeId == propertyTypeId);
Database.Execute(sql0);
}
// nothing else to do
return;
var sql0 = Sql().Delete<TagRelationshipDto>().Where<TagRelationshipDto>(x => x.NodeId == contentId && x.PropertyTypeId == propertyTypeId);
Database.Execute(sql0);
}
// no tags? nothing else to do
if (tagsA.Length == 0)
return;
// tags
// using some clever logic (?) to insert tags that don't exist in 1 query
// must coalesce languageId because equality of NULLs does not exist
var tagSetSql = GetTagSet(tagsA);
var group = SqlSyntax.GetQuotedColumnName("group");
// insert tags
var sql1 = $@"INSERT INTO cmsTags (tag, {group})
SELECT tagSet.tag, tagSet.{group}
var sql1 = $@"INSERT INTO cmsTags (tag, {group}, languageId)
SELECT tagSet.tag, tagSet.{group}, tagSet.languageId
FROM {tagSetSql}
LEFT OUTER JOIN cmsTags ON (tagSet.tag = cmsTags.tag AND tagSet.{group} = cmsTags.{group})
LEFT OUTER JOIN cmsTags ON (tagSet.tag = cmsTags.tag AND tagSet.{group} = cmsTags.{group} AND COALESCE(tagSet.languageId, -1) = COALESCE(cmsTags.languageId, -1))
WHERE cmsTags.id IS NULL";
Database.Execute(sql1);
// if replacing, remove everything first
if (replaceTags)
{
var sql2 = Sql().Delete<TagRelationshipDto>().Where<TagRelationshipDto>(x => x.NodeId == contentId && x.PropertyTypeId == propertyTypeId);
Database.Execute(sql2);
}
// insert relations
var sql3 = $@"INSERT INTO cmsTagRelationship (nodeId, propertyTypeId, tagId)
var sql2 = $@"INSERT INTO cmsTagRelationship (nodeId, propertyTypeId, tagId)
SELECT {contentId}, {propertyTypeId}, tagSet2.Id
FROM (
SELECT t.Id
FROM {tagSetSql}
INNER JOIN cmsTags as t ON (tagSet.tag = t.tag AND tagSet.{group} = t.{group})
INNER JOIN cmsTags as t ON (tagSet.tag = t.tag AND tagSet.{group} = t.{group} AND COALESCE(tagSet.languageId, -1) = COALESCE(t.languageId, -1))
) AS tagSet2
LEFT OUTER JOIN cmsTagRelationship r ON (tagSet2.id = r.tagId AND r.nodeId = {contentId} AND r.propertyTypeID = {propertyTypeId})
WHERE r.tagId IS NULL";
Database.Execute(sql3);
Database.Execute(sql2);
}
/// <inheritdoc />
// only invoked from tests
public void Remove(int contentId, int propertyTypeId, IEnumerable<ITag> tags)
{
var tagSetSql = GetTagSet(tags);
var group = SqlSyntax.GetQuotedColumnName("group");
var deleteSql = string.Concat("DELETE FROM cmsTagRelationship WHERE nodeId = ",
contentId,
" AND propertyTypeId = ",
propertyTypeId,
" AND tagId IN ",
"(SELECT id FROM cmsTags INNER JOIN ",
tagSetSql,
" ON (TagSet.Tag = cmsTags.Tag and TagSet." + SqlSyntax.GetQuotedColumnName("group") + @" = cmsTags." + SqlSyntax.GetQuotedColumnName("group") + @"))");
var deleteSql = $@"DELETE FROM cmsTagRelationship WHERE nodeId = {contentId} AND propertyTypeId = {propertyTypeId} AND tagId IN (
SELECT id FROM cmsTags INNER JOIN {tagSetSql} ON (
tagSet.tag = cmsTags.tag AND tagSet.{group} = cmsTags.{group} AND COALESCE(tagSet.languageId, -1) = COALESCE(cmsTags.languageId, -1)
)
)";
Database.Execute(deleteSql);
}
@@ -207,13 +199,6 @@ WHERE r.tagId IS NULL";
//
private string GetTagSet(IEnumerable<ITag> tags)
{
string EscapeSqlString(string s)
{
// why were we escaping @ symbols?
//return NPocoDatabaseExtensions.EscapeAtSymbols(s.Replace("'", "''"));
return s.Replace("'", "''");
}
var sql = new StringBuilder();
var group = SqlSyntax.GetQuotedColumnName("group");
var first = true;
@@ -226,11 +211,17 @@ WHERE r.tagId IS NULL";
else sql.Append(" UNION ");
sql.Append("SELECT N'");
sql.Append(EscapeSqlString(tag.Text));
sql.Append(SqlSyntax.EscapeString(tag.Text));
sql.Append("' AS tag, '");
sql.Append(EscapeSqlString(tag.Group));
sql.Append(SqlSyntax.EscapeString(tag.Group));
sql.Append("' AS ");
sql.Append(group);
sql.Append(" , ");
if (tag.LanguageId.HasValue)
sql.Append(tag.LanguageId);
else
sql.Append("NULL");
sql.Append(" AS languageId");
}
sql.Append(") AS tagSet");
@@ -244,14 +235,17 @@ WHERE r.tagId IS NULL";
public bool Equals(ITag x, ITag y)
{
return ReferenceEquals(x, y) // takes care of both being null
|| x != null && y != null && x.Text == y.Text && x.Group == y.Group;
|| x != null && y != null && x.Text == y.Text && x.Group == y.Group && x.LanguageId == y.LanguageId;
}
public int GetHashCode(ITag obj)
{
unchecked
{
return (obj.Text.GetHashCode() * 397) ^ obj.Group.GetHashCode();
var h = obj.Text.GetHashCode();
h = h * 397 ^ obj.Group.GetHashCode();
h = h * 397 ^ (obj.LanguageId?.GetHashCode() ?? 0);
return h;
}
}
}
@@ -264,118 +258,126 @@ WHERE r.tagId IS NULL";
// consider caching implications
// add lookups for parentId or path (ie get content in tag group, that are descendants of x)
// ReSharper disable once ClassNeverInstantiated.Local
// ReSharper disable UnusedAutoPropertyAccessor.Local
private class TaggedEntityDto
{
public int NodeId { get; set; }
public string PropertyTypeAlias { get; set; }
public int PropertyTypeId { get; set; }
public int TagId { get; set; }
public string TagText { get; set; }
public string TagGroup { get; set; }
public int? TagLanguage { get; set; }
}
// ReSharper restore UnusedAutoPropertyAccessor.Local
/// <inheritdoc />
public TaggedEntity GetTaggedEntityByKey(Guid key)
{
var sql = Sql()
.Select("cmsTagRelationship.nodeId, cmsPropertyType.Alias, cmsPropertyType.id as propertyTypeId, cmsTags.tag, cmsTags.id as tagId, cmsTags." + SqlSyntax.GetQuotedColumnName("group"))
.From<TagDto>()
.InnerJoin<TagRelationshipDto>()
.On<TagRelationshipDto, TagDto>(left => left.TagId, right => right.Id)
.InnerJoin<ContentDto>()
.On<ContentDto, TagRelationshipDto>(left => left.NodeId, right => right.NodeId)
.InnerJoin<PropertyTypeDto>()
.On<PropertyTypeDto, TagRelationshipDto>(left => left.Id, right => right.PropertyTypeId)
.InnerJoin<NodeDto>()
.On<NodeDto, ContentDto>(left => left.NodeId, right => right.NodeId)
var sql = GetTaggedEntitiesSql(TaggableObjectTypes.All, "*");
sql = sql
.Where<NodeDto>(dto => dto.UniqueId == key);
return CreateTaggedEntityCollection(Database.Fetch<dynamic>(sql)).FirstOrDefault();
return Map(Database.Fetch<TaggedEntityDto>(sql)).FirstOrDefault();
}
/// <inheritdoc />
public TaggedEntity GetTaggedEntityById(int id)
{
var sql = Sql()
.Select("cmsTagRelationship.nodeId, cmsPropertyType.Alias, cmsPropertyType.id as propertyTypeId, cmsTags.tag, cmsTags.id as tagId, cmsTags." + SqlSyntax.GetQuotedColumnName("group"))
.From<TagDto>()
.InnerJoin<TagRelationshipDto>()
.On<TagRelationshipDto, TagDto>(left => left.TagId, right => right.Id)
.InnerJoin<ContentDto>()
.On<ContentDto, TagRelationshipDto>(left => left.NodeId, right => right.NodeId)
.InnerJoin<PropertyTypeDto>()
.On<PropertyTypeDto, TagRelationshipDto>(left => left.Id, right => right.PropertyTypeId)
.InnerJoin<NodeDto>()
.On<NodeDto, ContentDto>(left => left.NodeId, right => right.NodeId)
var sql = GetTaggedEntitiesSql(TaggableObjectTypes.All, "*");
sql = sql
.Where<NodeDto>(dto => dto.NodeId == id);
return CreateTaggedEntityCollection(Database.Fetch<dynamic>(sql)).FirstOrDefault();
return Map(Database.Fetch<TaggedEntityDto>(sql)).FirstOrDefault();
}
public IEnumerable<TaggedEntity> GetTaggedEntitiesByTagGroup(TaggableObjectTypes objectType, string tagGroup)
/// <inheritdoc />
public IEnumerable<TaggedEntity> GetTaggedEntitiesByTagGroup(TaggableObjectTypes objectType, string group, string culture = null)
{
var sql = Sql()
.Select("cmsTagRelationship.nodeId, cmsPropertyType.Alias, cmsPropertyType.id as propertyTypeId, cmsTags.tag, cmsTags.id as tagId, cmsTags." + SqlSyntax.GetQuotedColumnName("group"))
.From<TagDto>()
.InnerJoin<TagRelationshipDto>()
.On<TagRelationshipDto, TagDto>(left => left.TagId, right => right.Id)
.InnerJoin<ContentDto>()
.On<ContentDto, TagRelationshipDto>(left => left.NodeId, right => right.NodeId)
.InnerJoin<PropertyTypeDto>()
.On<PropertyTypeDto, TagRelationshipDto>(left => left.Id, right => right.PropertyTypeId)
.InnerJoin<NodeDto>()
.On<NodeDto, ContentDto>(left => left.NodeId, right => right.NodeId)
.Where<TagDto>(dto => dto.Group == tagGroup);
var sql = GetTaggedEntitiesSql(objectType, culture);
if (objectType != TaggableObjectTypes.All)
{
var nodeObjectType = GetNodeObjectType(objectType);
sql = sql
.Where<NodeDto>(dto => dto.NodeObjectType == nodeObjectType);
}
sql = sql
.Where<TagDto>(x => x.Group == group);
return CreateTaggedEntityCollection(
Database.Fetch<dynamic>(sql));
return Map(Database.Fetch<TaggedEntityDto>(sql));
}
public IEnumerable<TaggedEntity> GetTaggedEntitiesByTag(TaggableObjectTypes objectType, string tag, string tagGroup = null)
/// <inheritdoc />
public IEnumerable<TaggedEntity> GetTaggedEntitiesByTag(TaggableObjectTypes objectType, string tag, string group = null, string culture = null)
{
var sql = Sql()
.Select("cmsTagRelationship.nodeId, cmsPropertyType.Alias, cmsPropertyType.id as propertyTypeId, cmsTags.tag, cmsTags.id as tagId, cmsTags." + SqlSyntax.GetQuotedColumnName("group"))
.From<TagDto>()
.InnerJoin<TagRelationshipDto>()
.On<TagRelationshipDto, TagDto>(left => left.TagId, right => right.Id)
.InnerJoin<ContentDto>()
.On<ContentDto, TagRelationshipDto>(left => left.NodeId, right => right.NodeId)
.InnerJoin<PropertyTypeDto>()
.On<PropertyTypeDto, TagRelationshipDto>(left => left.Id, right => right.PropertyTypeId)
.InnerJoin<NodeDto>()
.On<NodeDto, ContentDto>(left => left.NodeId, right => right.NodeId)
var sql = GetTaggedEntitiesSql(objectType, culture);
sql = sql
.Where<TagDto>(dto => dto.Text == tag);
if (group.IsNullOrWhiteSpace() == false)
sql = sql
.Where<TagDto>(dto => dto.Group == group);
return Map(Database.Fetch<TaggedEntityDto>(sql));
}
private Sql<ISqlContext> GetTaggedEntitiesSql(TaggableObjectTypes objectType, string culture)
{
var sql = Sql()
.Select<TagRelationshipDto>(x => Alias(x.NodeId, "NodeId"))
.AndSelect<PropertyTypeDto>(x => Alias(x.Alias, "PropertyTypeAlias"), x => Alias(x.Id, "PropertyTypeId"))
.AndSelect<TagDto>(x => Alias(x.Id, "TagId"), x => Alias(x.Text, "TagText"), x => Alias(x.Group, "TagGroup"), x => Alias(x.LanguageId, "TagLanguage"))
.From<TagDto>()
.InnerJoin<TagRelationshipDto>().On<TagDto, TagRelationshipDto>((tag, rel) => tag.Id == rel.TagId)
.InnerJoin<ContentDto>().On<TagRelationshipDto, ContentDto>((rel, content) => rel.NodeId == content.NodeId)
.InnerJoin<PropertyTypeDto>().On<TagRelationshipDto, PropertyTypeDto>((rel, prop) => rel.PropertyTypeId == prop.Id)
.InnerJoin<NodeDto>().On<ContentDto, NodeDto>((content, node) => content.NodeId == node.NodeId);
if (culture == null)
{
sql = sql
.Where<TagDto>(dto => dto.LanguageId == null);
}
else if (culture != "*")
{
sql = sql
.InnerJoin<LanguageDto>().On<TagDto, LanguageDto>((tag, lang) => tag.LanguageId == lang.Id)
.Where<LanguageDto>(x => x.IsoCode == culture);
}
if (objectType != TaggableObjectTypes.All)
{
var nodeObjectType = GetNodeObjectType(objectType);
sql = sql
.Where<NodeDto>(dto => dto.NodeObjectType == nodeObjectType);
sql = sql.Where<NodeDto>(dto => dto.NodeObjectType == nodeObjectType);
}
if (tagGroup.IsNullOrWhiteSpace() == false)
{
sql = sql.Where<TagDto>(dto => dto.Group == tagGroup);
}
return CreateTaggedEntityCollection(
Database.Fetch<dynamic>(sql));
return sql;
}
private IEnumerable<TaggedEntity> CreateTaggedEntityCollection(IEnumerable<dynamic> dbResult)
private static IEnumerable<TaggedEntity> Map(IEnumerable<TaggedEntityDto> dtos)
{
foreach (var node in dbResult.GroupBy(x => (int)x.nodeId))
return dtos.GroupBy(x => x.NodeId).Select(dtosForNode =>
{
var properties = new List<TaggedProperty>();
foreach (var propertyType in node.GroupBy(x => new { id = (int)x.propertyTypeId, alias = (string)x.Alias }))
var taggedProperties = dtosForNode.GroupBy(x => x.PropertyTypeId).Select(dtosForProperty =>
{
var tags = propertyType.Select(x => new Tag((int)x.tagId, (string)x.group, (string)x.tag));
properties.Add(new TaggedProperty(propertyType.Key.id, propertyType.Key.alias, tags));
}
yield return new TaggedEntity(node.Key, properties);
}
string propertyTypeAlias = null;
var tags = dtosForProperty.Select(dto =>
{
propertyTypeAlias = dto.PropertyTypeAlias;
return new Tag(dto.TagId, dto.TagGroup, dto.TagText, dto.TagLanguage);
}).ToList();
return new TaggedProperty(dtosForProperty.Key, propertyTypeAlias, tags);
}).ToList();
return new TaggedEntity(dtosForNode.Key, taggedProperties);
}).ToList();
}
public IEnumerable<ITag> GetTagsForEntityType(TaggableObjectTypes objectType, string group = null)
/// <inheritdoc />
public IEnumerable<ITag> GetTagsForEntityType(TaggableObjectTypes objectType, string group = null, string culture = null)
{
var sql = GetTagsQuerySelect(true);
var sql = GetTagsSql(culture, true);
sql = ApplyRelationshipJoinToTagsQuery(sql);
AddTagsSqlWhere(sql, culture);
if (objectType != TaggableObjectTypes.All)
{
@@ -384,116 +386,126 @@ WHERE r.tagId IS NULL";
.Where<NodeDto>(dto => dto.NodeObjectType == nodeObjectType);
}
sql = ApplyGroupFilterToTagsQuery(sql, group);
if (group.IsNullOrWhiteSpace() == false)
sql = sql
.Where<TagDto>(dto => dto.Group == group);
sql = ApplyGroupByToTagsQuery(sql);
sql = sql
.GroupBy<TagDto>(x => x.Id, x => x.Text, x => x.Group, x => x.LanguageId);
return ExecuteTagsQuery(sql);
}
public IEnumerable<ITag> GetTagsForEntity(int contentId, string group = null)
/// <inheritdoc />
public IEnumerable<ITag> GetTagsForEntity(int contentId, string group = null, string culture = null)
{
var sql = GetTagsQuerySelect();
var sql = GetTagsSql(culture);
sql = ApplyRelationshipJoinToTagsQuery(sql);
AddTagsSqlWhere(sql, culture);
sql = sql
.Where<NodeDto>(dto => dto.NodeId == contentId);
sql = ApplyGroupFilterToTagsQuery(sql, group);
if (group.IsNullOrWhiteSpace() == false)
sql = sql
.Where<TagDto>(dto => dto.Group == group);
return ExecuteTagsQuery(sql);
}
public IEnumerable<ITag> GetTagsForEntity(Guid contentId, string group = null)
/// <inheritdoc />
public IEnumerable<ITag> GetTagsForEntity(Guid contentId, string group = null, string culture = null)
{
var sql = GetTagsQuerySelect();
var sql = GetTagsSql(culture);
sql = ApplyRelationshipJoinToTagsQuery(sql);
AddTagsSqlWhere(sql, culture);
sql = sql
.Where<NodeDto>(dto => dto.UniqueId == contentId);
sql = ApplyGroupFilterToTagsQuery(sql, group);
if (group.IsNullOrWhiteSpace() == false)
sql = sql
.Where<TagDto>(dto => dto.Group == group);
return ExecuteTagsQuery(sql);
}
public IEnumerable<ITag> GetTagsForProperty(int contentId, string propertyTypeAlias, string group = null)
/// <inheritdoc />
public IEnumerable<ITag> GetTagsForProperty(int contentId, string propertyTypeAlias, string group = null, string culture = null)
{
var sql = GetTagsQuerySelect();
sql = ApplyRelationshipJoinToTagsQuery(sql);
var sql = GetTagsSql(culture);
sql = sql
.InnerJoin<PropertyTypeDto>()
.On<PropertyTypeDto, TagRelationshipDto>(left => left.Id, right => right.PropertyTypeId)
.Where<NodeDto>(dto => dto.NodeId == contentId)
.Where<PropertyTypeDto>(dto => dto.Alias == propertyTypeAlias);
.InnerJoin<PropertyTypeDto>().On<PropertyTypeDto, TagRelationshipDto>((prop, rel) => prop.Id == rel.PropertyTypeId)
.Where<NodeDto>(x => x.NodeId == contentId)
.Where<PropertyTypeDto>(x => x.Alias == propertyTypeAlias);
sql = ApplyGroupFilterToTagsQuery(sql, group);
AddTagsSqlWhere(sql, culture);
if (group.IsNullOrWhiteSpace() == false)
sql = sql
.Where<TagDto>(dto => dto.Group == group);
return ExecuteTagsQuery(sql);
}
public IEnumerable<ITag> GetTagsForProperty(Guid contentId, string propertyTypeAlias, string group = null)
/// <inheritdoc />
public IEnumerable<ITag> GetTagsForProperty(Guid contentId, string propertyTypeAlias, string group = null, string culture = null)
{
var sql = GetTagsQuerySelect();
sql = ApplyRelationshipJoinToTagsQuery(sql);
var sql = GetTagsSql(culture);
sql = sql
.InnerJoin<PropertyTypeDto>()
.On<PropertyTypeDto, TagRelationshipDto>(left => left.Id, right => right.PropertyTypeId)
.InnerJoin<PropertyTypeDto>().On<PropertyTypeDto, TagRelationshipDto>((prop, rel) => prop.Id == rel.PropertyTypeId)
.Where<NodeDto>(dto => dto.UniqueId == contentId)
.Where<PropertyTypeDto>(dto => dto.Alias == propertyTypeAlias);
sql = ApplyGroupFilterToTagsQuery(sql, group);
AddTagsSqlWhere(sql, culture);
if (group.IsNullOrWhiteSpace() == false)
sql = sql
.Where<TagDto>(dto => dto.Group == group);
return ExecuteTagsQuery(sql);
}
private Sql<ISqlContext> GetTagsQuerySelect(bool withGrouping = false)
private Sql<ISqlContext> GetTagsSql(string culture, bool withGrouping = false)
{
var sql = Sql();
var sql = Sql()
.Select<TagDto>();
if (withGrouping)
{
sql = sql.Select("cmsTags.id, cmsTags.tag, cmsTags." + SqlSyntax.GetQuotedColumnName("group") + @", Count(*) NodeCount");
}
else
{
sql = sql.Select("DISTINCT cmsTags.*");
}
sql = sql
.AndSelectCount("NodeCount");
return sql;
}
private Sql<ISqlContext> ApplyRelationshipJoinToTagsQuery(Sql<ISqlContext> sql)
{
return sql
sql = sql
.From<TagDto>()
.InnerJoin<TagRelationshipDto>()
.On<TagRelationshipDto, TagDto>(left => left.TagId, right => right.Id)
.InnerJoin<ContentDto>()
.On<ContentDto, TagRelationshipDto>(left => left.NodeId, right => right.NodeId)
.InnerJoin<NodeDto>()
.On<NodeDto, ContentDto>(left => left.NodeId, right => right.NodeId);
}
.InnerJoin<TagRelationshipDto>().On<TagRelationshipDto, TagDto>((rel, tag) => tag.Id == rel.TagId)
.InnerJoin<ContentDto>().On<ContentDto, TagRelationshipDto>((content, rel) => content.NodeId == rel.NodeId)
.InnerJoin<NodeDto>().On<NodeDto, ContentDto>((node, content) => node.NodeId == content.NodeId);
private Sql<ISqlContext> ApplyGroupFilterToTagsQuery(Sql<ISqlContext> sql, string group)
{
if (group.IsNullOrWhiteSpace() == false)
if (culture != null && culture != "*")
{
sql = sql.Where<TagDto>(dto => dto.Group == group);
sql = sql
.InnerJoin<LanguageDto>().On<TagDto, LanguageDto>((tag, lang) => tag.LanguageId == lang.Id);
}
return sql;
}
private Sql<ISqlContext> ApplyGroupByToTagsQuery(Sql<ISqlContext> sql)
private Sql<ISqlContext> AddTagsSqlWhere(Sql<ISqlContext> sql, string culture)
{
return sql.GroupBy("cmsTags.id", "cmsTags.tag", "cmsTags." + SqlSyntax.GetQuotedColumnName("group") + @"");
if (culture == null)
{
sql = sql
.Where<TagDto>(dto => dto.LanguageId == null);
}
else if (culture != "*")
{
sql = sql
.Where<LanguageDto>(x => x.IsoCode == culture);
}
return sql;
}
private IEnumerable<ITag> ExecuteTagsQuery(Sql sql)
+15 -1
View File
@@ -30,10 +30,24 @@ namespace Umbraco.Core.Persistence
Templates = new SqlTemplates(this);
}
// fixme
/// <summary>
/// Initializes a new instance of the <see cref="SqlContext"/> class.
/// </summary>
/// <remarks>Initializes an empty context which must be fully initialized using the
/// <see cref="Initialize"/> method; this is done in <see cref="UmbracoDatabaseFactory"/>
/// as soon as the factory is fully configured.</remarks>
internal SqlContext()
{ }
/// <summary>
/// Initializes this <see cref="SqlContext"/>.
/// </summary>
/// <param name="sqlSyntax">The sql syntax provider.</param>
/// <param name="pocoDataFactory">The Poco data factory.</param>
/// <param name="databaseType">The database type.</param>
/// <param name="mappers">The mappers.</param>
/// <remarks>Fully initializes an initially empty context; this is done in <see cref="UmbracoDatabaseFactory"/>
/// as soon as the factory is fully configured.</remarks>
internal void Initialize(ISqlSyntaxProvider sqlSyntax, DatabaseType databaseType, IPocoDataFactory pocoDataFactory, IMapperCollection mappers = null)
{
// for tests
+3 -2
View File
@@ -95,9 +95,10 @@ namespace Umbraco.Core.Persistence
return new Sql<ISqlContext>(_sqlContext, isBuilt, _sql, args);
}
internal void WriteToConsole()
internal string ToText()
{
new Sql<ISqlContext>(_sqlContext, _sql, _args.Values.ToArray()).WriteToConsole();
var sql = new Sql<ISqlContext>(_sqlContext, _sql, _args.Values.ToArray());
return sql.ToText();
}
/// <summary>
+23 -16
View File
@@ -67,6 +67,24 @@ namespace Umbraco.Core.Persistence
/// <inheritdoc />
public ISqlContext SqlContext { get; }
#region Temp
// work around NPoco issue https://github.com/schotime/NPoco/issues/517 while we wait for the fix
public override DbCommand CreateCommand(DbConnection connection, CommandType commandType, string sql, params object[] args)
{
var command = base.CreateCommand(connection, commandType, sql, args);
if (!DatabaseType.IsSqlCe()) return command;
foreach (DbParameter parameter in command.Parameters)
if (parameter.Value == DBNull.Value)
parameter.DbType = DbType.String;
return command;
}
#endregion
#region Testing, Debugging and Troubleshooting
private bool _enableCount;
@@ -228,24 +246,13 @@ namespace Umbraco.Core.Persistence
private string CommandToString(string sql, object[] args)
{
var sb = new StringBuilder();
var text = new StringBuilder();
#if DEBUG_DATABASES
sb.Append(InstanceId);
sb.Append(": ");
text.Append(InstanceId);
text.Append(": ");
#endif
sb.Append(sql);
if (args.Length > 0)
sb.Append(" --");
var i = 0;
foreach (var arg in args)
{
sb.Append(" @");
sb.Append(i++);
sb.Append(":");
sb.Append(arg);
}
return sb.ToString();
NPocoSqlExtensions.ToText(sql, args, text);
return text.ToString();
}
protected override void OnExecutedCommand(DbCommand cmd)
@@ -9,7 +9,7 @@
/// Determines whether an editor supports tags.
/// </summary>
public static bool IsTagsEditor(this IDataEditor editor)
=> editor?.GetType().GetCustomAttribute<TagsPropertyEditorAttribute>(false) != null;
=> editor.GetTagAttribute() != null;
/// <summary>
/// Gets the tags configuration attribute of an editor.
+6 -6
View File
@@ -237,10 +237,10 @@ namespace Umbraco.Core.Runtime
// and only these things - the rest should be composed in runtime components
// register basic things
// logging, runtime state, configuration
container.RegisterSingleton<IProfiler, LogProfiler>();
container.RegisterSingleton<ProfilingLogger>();
container.RegisterSingleton<IRuntimeState, RuntimeState>();
container.RegisterFrom<ConfigurationCompositionRoot>();
// register caches
@@ -254,8 +254,8 @@ namespace Umbraco.Core.Runtime
new IsolatedRuntimeCache(type => new DeepCloneRuntimeCacheProvider(new ObjectCacheRuntimeCacheProvider()))));
container.RegisterSingleton(f => f.GetInstance<CacheHelper>().RuntimeCache);
// register the plugin manager
container.RegisterSingleton(f => new TypeLoader(f.GetInstance<IRuntimeCacheProvider>(), f.GetInstance<IGlobalSettings>(), f.GetInstance<ProfilingLogger>()));
// register the type loader
container.RegisterSingleton<TypeLoader>();
// register syntax providers - required by database factory
container.Register<ISqlSyntaxProvider, MySqlSyntaxProvider>("MySqlSyntaxProvider");
@@ -389,14 +389,14 @@ namespace Umbraco.Core.Runtime
protected virtual bool EnsureUmbracoUpgradeState(IUmbracoDatabaseFactory databaseFactory, ILogger logger)
{
var umbracoPlan = new UmbracoPlan();
var stateValueKey = Upgrader.GetStateValueKey(umbracoPlan);
var upgrader = new UmbracoUpgrader();
var stateValueKey = upgrader.StateValueKey;
// no scope, no service - just directly accessing the database
using (var database = databaseFactory.CreateDatabase())
{
_state.CurrentMigrationState = KeyValueService.GetValue(database, stateValueKey);
_state.FinalMigrationState = umbracoPlan.FinalState;
_state.FinalMigrationState = upgrader.Plan.FinalState;
}
logger.Debug<CoreRuntime>("Final upgrade state is {FinalMigrationState}, database contains {DatabaseState}", _state.FinalMigrationState, _state.CurrentMigrationState ?? "<null>");
+36 -87
View File
@@ -5,7 +5,7 @@ using Umbraco.Core.Models;
namespace Umbraco.Core.Services
{
/// <summary>
/// Tag service to query for tags in the tags db table. The tags returned are only relavent for published content & saved media or members
/// Tag service to query for tags in the tags db table. The tags returned are only relevant for published content & saved media or members
/// </summary>
/// <remarks>
/// If there is unpublished content with tags, those tags will not be contained.
@@ -15,135 +15,84 @@ namespace Umbraco.Core.Services
/// </remarks>
public interface ITagService : IService
{
/// <summary>
/// Gets a tagged entity.
/// </summary>
TaggedEntity GetTaggedEntityById(int id);
/// <summary>
/// Gets a tagged entity.
/// </summary>
TaggedEntity GetTaggedEntityByKey(Guid key);
/// <summary>
/// Gets tagged Content by a specific 'Tag Group'.
/// Gets all documents tagged with any tag in the specified group.
/// </summary>
/// <remarks>The <see cref="TaggedEntity"/> contains the Id and Tags of the Content, not the actual Content item.</remarks>
/// <param name="tagGroup">Name of the 'Tag Group'</param>
/// <returns>An enumerable list of <see cref="TaggedEntity"/></returns>
IEnumerable<TaggedEntity> GetTaggedContentByTagGroup(string tagGroup);
IEnumerable<TaggedEntity> GetTaggedContentByTagGroup(string group, string culture = null);
/// <summary>
/// Gets tagged Content by a specific 'Tag' and optional 'Tag Group'.
/// Gets all documents tagged with the specified tag.
/// </summary>
/// <remarks>The <see cref="TaggedEntity"/> contains the Id and Tags of the Content, not the actual Content item.</remarks>
/// <param name="tag">Tag</param>
/// <param name="tagGroup">Optional name of the 'Tag Group'</param>
/// <returns>An enumerable list of <see cref="TaggedEntity"/></returns>
IEnumerable<TaggedEntity> GetTaggedContentByTag(string tag, string tagGroup = null);
IEnumerable<TaggedEntity> GetTaggedContentByTag(string tag, string group = null, string culture = null);
/// <summary>
/// Gets tagged Media by a specific 'Tag Group'.
/// Gets all media tagged with any tag in the specified group.
/// </summary>
/// <remarks>The <see cref="TaggedEntity"/> contains the Id and Tags of the Media, not the actual Media item.</remarks>
/// <param name="tagGroup">Name of the 'Tag Group'</param>
/// <returns>An enumerable list of <see cref="TaggedEntity"/></returns>
IEnumerable<TaggedEntity> GetTaggedMediaByTagGroup(string tagGroup);
IEnumerable<TaggedEntity> GetTaggedMediaByTagGroup(string group, string culture = null);
/// <summary>
/// Gets tagged Media by a specific 'Tag' and optional 'Tag Group'.
/// Gets all media tagged with the specified tag.
/// </summary>
/// <remarks>The <see cref="TaggedEntity"/> contains the Id and Tags of the Media, not the actual Media item.</remarks>
/// <param name="tag">Tag</param>
/// <param name="tagGroup">Optional name of the 'Tag Group'</param>
/// <returns>An enumerable list of <see cref="TaggedEntity"/></returns>
IEnumerable<TaggedEntity> GetTaggedMediaByTag(string tag, string tagGroup = null);
IEnumerable<TaggedEntity> GetTaggedMediaByTag(string tag, string group = null, string culture = null);
/// <summary>
/// Gets tagged Members by a specific 'Tag Group'.
/// Gets all members tagged with any tag in the specified group.
/// </summary>
/// <remarks>The <see cref="TaggedEntity"/> contains the Id and Tags of the Member, not the actual Member item.</remarks>
/// <param name="tagGroup">Name of the 'Tag Group'</param>
/// <returns>An enumerable list of <see cref="TaggedEntity"/></returns>
IEnumerable<TaggedEntity> GetTaggedMembersByTagGroup(string tagGroup);
IEnumerable<TaggedEntity> GetTaggedMembersByTagGroup(string group, string culture = null);
/// <summary>
/// Gets tagged Members by a specific 'Tag' and optional 'Tag Group'.
/// Gets all members tagged with the specified tag.
/// </summary>
/// <remarks>The <see cref="TaggedEntity"/> contains the Id and Tags of the Member, not the actual Member item.</remarks>
/// <param name="tag">Tag</param>
/// <param name="tagGroup">Optional name of the 'Tag Group'</param>
/// <returns>An enumerable list of <see cref="TaggedEntity"/></returns>
IEnumerable<TaggedEntity> GetTaggedMembersByTag(string tag, string tagGroup = null);
IEnumerable<TaggedEntity> GetTaggedMembersByTag(string tag, string group = null, string culture = null);
/// <summary>
/// Gets every tag stored in the database
/// Gets all tags.
/// </summary>
/// <param name="tagGroup">Optional name of the 'Tag Group'</param>
/// <returns>An enumerable list of <see cref="ITag"/></returns>
IEnumerable<ITag> GetAllTags(string tagGroup = null);
IEnumerable<ITag> GetAllTags(string group = null, string culture = null);
/// <summary>
/// Gets all tags for content items
/// Gets all document tags.
/// </summary>
/// <remarks>Use the optional tagGroup parameter to limit the
/// result to a specific 'Tag Group'.</remarks>
/// <param name="tagGroup">Optional name of the 'Tag Group'</param>
/// <returns>An enumerable list of <see cref="ITag"/></returns>
IEnumerable<ITag> GetAllContentTags(string tagGroup = null);
IEnumerable<ITag> GetAllContentTags(string group = null, string culture = null);
/// <summary>
/// Gets all tags for media items
/// Gets all media tags.
/// </summary>
/// <remarks>Use the optional tagGroup parameter to limit the
/// result to a specific 'Tag Group'.</remarks>
/// <param name="tagGroup">Optional name of the 'Tag Group'</param>
/// <returns>An enumerable list of <see cref="ITag"/></returns>
IEnumerable<ITag> GetAllMediaTags(string tagGroup = null);
IEnumerable<ITag> GetAllMediaTags(string group = null, string culture = null);
/// <summary>
/// Gets all tags for member items
/// Gets all member tags.
/// </summary>
/// <remarks>Use the optional tagGroup parameter to limit the
/// result to a specific 'Tag Group'.</remarks>
/// <param name="tagGroup">Optional name of the 'Tag Group'</param>
/// <returns>An enumerable list of <see cref="ITag"/></returns>
IEnumerable<ITag> GetAllMemberTags(string tagGroup = null);
IEnumerable<ITag> GetAllMemberTags(string group = null, string culture = null);
/// <summary>
/// Gets all tags attached to a property by entity id
/// Gets all tags attached to an entity via a property.
/// </summary>
/// <remarks>Use the optional tagGroup parameter to limit the
/// result to a specific 'Tag Group'.</remarks>
/// <param name="contentId">The content item id to get tags for</param>
/// <param name="propertyTypeAlias">Property type alias</param>
/// <param name="tagGroup">Optional name of the 'Tag Group'</param>
/// <returns>An enumerable list of <see cref="ITag"/></returns>
IEnumerable<ITag> GetTagsForProperty(int contentId, string propertyTypeAlias, string tagGroup = null);
IEnumerable<ITag> GetTagsForProperty(int contentId, string propertyTypeAlias, string group = null, string culture = null);
/// <summary>
/// Gets all tags attached to an entity (content, media or member) by entity id
/// Gets all tags attached to an entity.
/// </summary>
/// <remarks>Use the optional tagGroup parameter to limit the
/// result to a specific 'Tag Group'.</remarks>
/// <param name="contentId">The content item id to get tags for</param>
/// <param name="tagGroup">Optional name of the 'Tag Group'</param>
/// <returns>An enumerable list of <see cref="ITag"/></returns>
IEnumerable<ITag> GetTagsForEntity(int contentId, string tagGroup = null);
IEnumerable<ITag> GetTagsForEntity(int contentId, string group = null, string culture = null);
/// <summary>
/// Gets all tags attached to a property by entity id
/// Gets all tags attached to an entity via a property.
/// </summary>
/// <remarks>Use the optional tagGroup parameter to limit the
/// result to a specific 'Tag Group'.</remarks>
/// <param name="contentId">The content item id to get tags for</param>
/// <param name="propertyTypeAlias">Property type alias</param>
/// <param name="tagGroup">Optional name of the 'Tag Group'</param>
/// <returns>An enumerable list of <see cref="ITag"/></returns>
IEnumerable<ITag> GetTagsForProperty(Guid contentId, string propertyTypeAlias, string tagGroup = null);
IEnumerable<ITag> GetTagsForProperty(Guid contentId, string propertyTypeAlias, string group = null, string culture = null);
/// <summary>
/// Gets all tags attached to an entity (content, media or member) by entity id
/// Gets all tags attached to an entity.
/// </summary>
/// <remarks>Use the optional tagGroup parameter to limit the
/// result to a specific 'Tag Group'.</remarks>
/// <param name="contentId">The content item id to get tags for</param>
/// <param name="tagGroup">Optional name of the 'Tag Group'</param>
/// <returns>An enumerable list of <see cref="ITag"/></returns>
IEnumerable<ITag> GetTagsForEntity(Guid contentId, string tagGroup = null);
IEnumerable<ITag> GetTagsForEntity(Guid contentId, string group = null, string culture = null);
}
}
+60 -143
View File
@@ -25,230 +25,147 @@ namespace Umbraco.Core.Services.Implement
_tagRepository = tagRepository;
}
/// <inheritdoc />
public TaggedEntity GetTaggedEntityById(int id)
{
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
using (ScopeProvider.CreateScope(autoComplete: true))
{
return _tagRepository.GetTaggedEntityById(id);
}
}
/// <inheritdoc />
public TaggedEntity GetTaggedEntityByKey(Guid key)
{
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
using (ScopeProvider.CreateScope(autoComplete: true))
{
return _tagRepository.GetTaggedEntityByKey(key);
}
}
/// <summary>
/// Gets tagged Content by a specific 'Tag Group'.
/// </summary>
/// <remarks>The <see cref="TaggedEntity"/> contains the Id and Tags of the Content, not the actual Content item.</remarks>
/// <param name="tagGroup">Name of the 'Tag Group'</param>
/// <returns>An enumerable list of <see cref="TaggedEntity"/></returns>
public IEnumerable<TaggedEntity> GetTaggedContentByTagGroup(string tagGroup)
/// <inheritdoc />
public IEnumerable<TaggedEntity> GetTaggedContentByTagGroup(string group, string culture = null)
{
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
using (ScopeProvider.CreateScope(autoComplete: true))
{
return _tagRepository.GetTaggedEntitiesByTagGroup(TaggableObjectTypes.Content, tagGroup);
return _tagRepository.GetTaggedEntitiesByTagGroup(TaggableObjectTypes.Content, group, culture);
}
}
/// <summary>
/// Gets tagged Content by a specific 'Tag' and optional 'Tag Group'.
/// </summary>
/// <remarks>The <see cref="TaggedEntity"/> contains the Id and Tags of the Content, not the actual Content item.</remarks>
/// <param name="tag">Tag</param>
/// <param name="tagGroup">Optional name of the 'Tag Group'</param>
/// <returns>An enumerable list of <see cref="TaggedEntity"/></returns>
public IEnumerable<TaggedEntity> GetTaggedContentByTag(string tag, string tagGroup = null)
/// <inheritdoc />
public IEnumerable<TaggedEntity> GetTaggedContentByTag(string tag, string group = null, string culture = null)
{
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
using (ScopeProvider.CreateScope(autoComplete: true))
{
return _tagRepository.GetTaggedEntitiesByTag(TaggableObjectTypes.Content, tag, tagGroup);
return _tagRepository.GetTaggedEntitiesByTag(TaggableObjectTypes.Content, tag, group, culture);
}
}
/// <summary>
/// Gets tagged Media by a specific 'Tag Group'.
/// </summary>
/// <remarks>The <see cref="TaggedEntity"/> contains the Id and Tags of the Media, not the actual Media item.</remarks>
/// <param name="tagGroup">Name of the 'Tag Group'</param>
/// <returns>An enumerable list of <see cref="TaggedEntity"/></returns>
public IEnumerable<TaggedEntity> GetTaggedMediaByTagGroup(string tagGroup)
/// <inheritdoc />
public IEnumerable<TaggedEntity> GetTaggedMediaByTagGroup(string group, string culture = null)
{
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
using (ScopeProvider.CreateScope(autoComplete: true))
{
return _tagRepository.GetTaggedEntitiesByTagGroup(TaggableObjectTypes.Media, tagGroup);
return _tagRepository.GetTaggedEntitiesByTagGroup(TaggableObjectTypes.Media, group, culture);
}
}
/// <summary>
/// Gets tagged Media by a specific 'Tag' and optional 'Tag Group'.
/// </summary>
/// <remarks>The <see cref="TaggedEntity"/> contains the Id and Tags of the Media, not the actual Media item.</remarks>
/// <param name="tag">Tag</param>
/// <param name="tagGroup">Optional name of the 'Tag Group'</param>
/// <returns>An enumerable list of <see cref="TaggedEntity"/></returns>
public IEnumerable<TaggedEntity> GetTaggedMediaByTag(string tag, string tagGroup = null)
/// <inheritdoc />
public IEnumerable<TaggedEntity> GetTaggedMediaByTag(string tag, string group = null, string culture = null)
{
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
using (ScopeProvider.CreateScope(autoComplete: true))
{
return _tagRepository.GetTaggedEntitiesByTag(TaggableObjectTypes.Media, tag, tagGroup);
return _tagRepository.GetTaggedEntitiesByTag(TaggableObjectTypes.Media, tag, group, culture);
}
}
/// <summary>
/// Gets tagged Members by a specific 'Tag Group'.
/// </summary>
/// <remarks>The <see cref="TaggedEntity"/> contains the Id and Tags of the Member, not the actual Member item.</remarks>
/// <param name="tagGroup">Name of the 'Tag Group'</param>
/// <returns>An enumerable list of <see cref="TaggedEntity"/></returns>
public IEnumerable<TaggedEntity> GetTaggedMembersByTagGroup(string tagGroup)
/// <inheritdoc />
public IEnumerable<TaggedEntity> GetTaggedMembersByTagGroup(string group, string culture = null)
{
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
using (ScopeProvider.CreateScope(autoComplete: true))
{
return _tagRepository.GetTaggedEntitiesByTagGroup(TaggableObjectTypes.Member, tagGroup);
return _tagRepository.GetTaggedEntitiesByTagGroup(TaggableObjectTypes.Member, group, culture);
}
}
/// <summary>
/// Gets tagged Members by a specific 'Tag' and optional 'Tag Group'.
/// </summary>
/// <remarks>The <see cref="TaggedEntity"/> contains the Id and Tags of the Member, not the actual Member item.</remarks>
/// <param name="tag">Tag</param>
/// <param name="tagGroup">Optional name of the 'Tag Group'</param>
/// <returns>An enumerable list of <see cref="TaggedEntity"/></returns>
public IEnumerable<TaggedEntity> GetTaggedMembersByTag(string tag, string tagGroup = null)
/// <inheritdoc />
public IEnumerable<TaggedEntity> GetTaggedMembersByTag(string tag, string group = null, string culture = null)
{
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
using (ScopeProvider.CreateScope(autoComplete: true))
{
return _tagRepository.GetTaggedEntitiesByTag(TaggableObjectTypes.Member, tag, tagGroup);
return _tagRepository.GetTaggedEntitiesByTag(TaggableObjectTypes.Member, tag, group, culture);
}
}
/// <summary>
/// Gets every tag stored in the database
/// </summary>
/// <param name="tagGroup">Optional name of the 'Tag Group'</param>
/// <returns>An enumerable list of <see cref="ITag"/></returns>
public IEnumerable<ITag> GetAllTags(string tagGroup = null)
/// <inheritdoc />
public IEnumerable<ITag> GetAllTags(string group = null, string culture = null)
{
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
using (ScopeProvider.CreateScope(autoComplete: true))
{
return _tagRepository.GetTagsForEntityType(TaggableObjectTypes.All, tagGroup);
return _tagRepository.GetTagsForEntityType(TaggableObjectTypes.All, group, culture);
}
}
/// <summary>
/// Gets all tags for content items
/// </summary>
/// <remarks>Use the optional tagGroup parameter to limit the
/// result to a specific 'Tag Group'.</remarks>
/// <param name="tagGroup">Optional name of the 'Tag Group'</param>
/// <returns>An enumerable list of <see cref="ITag"/></returns>
public IEnumerable<ITag> GetAllContentTags(string tagGroup = null)
/// <inheritdoc />
public IEnumerable<ITag> GetAllContentTags(string group = null, string culture = null)
{
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
using (ScopeProvider.CreateScope(autoComplete: true))
{
return _tagRepository.GetTagsForEntityType(TaggableObjectTypes.Content, tagGroup);
return _tagRepository.GetTagsForEntityType(TaggableObjectTypes.Content, group, culture);
}
}
/// <summary>
/// Gets all tags for media items
/// </summary>
/// <remarks>Use the optional tagGroup parameter to limit the
/// result to a specific 'Tag Group'.</remarks>
/// <param name="tagGroup">Optional name of the 'Tag Group'</param>
/// <returns>An enumerable list of <see cref="ITag"/></returns>
public IEnumerable<ITag> GetAllMediaTags(string tagGroup = null)
/// <inheritdoc />
public IEnumerable<ITag> GetAllMediaTags(string group = null, string culture = null)
{
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
using (ScopeProvider.CreateScope(autoComplete: true))
{
return _tagRepository.GetTagsForEntityType(TaggableObjectTypes.Media, tagGroup);
return _tagRepository.GetTagsForEntityType(TaggableObjectTypes.Media, group, culture);
}
}
/// <summary>
/// Gets all tags for member items
/// </summary>
/// <remarks>Use the optional tagGroup parameter to limit the
/// result to a specific 'Tag Group'.</remarks>
/// <param name="tagGroup">Optional name of the 'Tag Group'</param>
/// <returns>An enumerable list of <see cref="ITag"/></returns>
public IEnumerable<ITag> GetAllMemberTags(string tagGroup = null)
/// <inheritdoc />
public IEnumerable<ITag> GetAllMemberTags(string group = null, string culture = null)
{
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
using (ScopeProvider.CreateScope(autoComplete: true))
{
return _tagRepository.GetTagsForEntityType(TaggableObjectTypes.Member, tagGroup);
return _tagRepository.GetTagsForEntityType(TaggableObjectTypes.Member, group, culture);
}
}
/// <summary>
/// Gets all tags attached to a property by entity id
/// </summary>
/// <remarks>Use the optional tagGroup parameter to limit the
/// result to a specific 'Tag Group'.</remarks>
/// <param name="contentId">The content item id to get tags for</param>
/// <param name="propertyTypeAlias">Property type alias</param>
/// <param name="tagGroup">Optional name of the 'Tag Group'</param>
/// <returns>An enumerable list of <see cref="ITag"/></returns>
public IEnumerable<ITag> GetTagsForProperty(int contentId, string propertyTypeAlias, string tagGroup = null)
/// <inheritdoc />
public IEnumerable<ITag> GetTagsForProperty(int contentId, string propertyTypeAlias, string group = null, string culture = null)
{
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
using (ScopeProvider.CreateScope(autoComplete: true))
{
return _tagRepository.GetTagsForProperty(contentId, propertyTypeAlias, tagGroup);
return _tagRepository.GetTagsForProperty(contentId, propertyTypeAlias, group, culture);
}
}
/// <summary>
/// Gets all tags attached to an entity (content, media or member) by entity id
/// </summary>
/// <remarks>Use the optional tagGroup parameter to limit the
/// result to a specific 'Tag Group'.</remarks>
/// <param name="contentId">The content item id to get tags for</param>
/// <param name="tagGroup">Optional name of the 'Tag Group'</param>
/// <returns>An enumerable list of <see cref="ITag"/></returns>
public IEnumerable<ITag> GetTagsForEntity(int contentId, string tagGroup = null)
/// <inheritdoc />
public IEnumerable<ITag> GetTagsForEntity(int contentId, string group = null, string culture = null)
{
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
using (ScopeProvider.CreateScope(autoComplete: true))
{
return _tagRepository.GetTagsForEntity(contentId, tagGroup);
return _tagRepository.GetTagsForEntity(contentId, group, culture);
}
}
/// <summary>
/// Gets all tags attached to a property by entity id
/// </summary>
/// <remarks>Use the optional tagGroup parameter to limit the
/// result to a specific 'Tag Group'.</remarks>
/// <param name="contentId">The content item id to get tags for</param>
/// <param name="propertyTypeAlias">Property type alias</param>
/// <param name="tagGroup">Optional name of the 'Tag Group'</param>
/// <returns>An enumerable list of <see cref="ITag"/></returns>
public IEnumerable<ITag> GetTagsForProperty(Guid contentId, string propertyTypeAlias, string tagGroup = null)
/// <inheritdoc />
public IEnumerable<ITag> GetTagsForProperty(Guid contentId, string propertyTypeAlias, string group = null, string culture = null)
{
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
using (ScopeProvider.CreateScope(autoComplete: true))
{
return _tagRepository.GetTagsForProperty(contentId, propertyTypeAlias, tagGroup);
return _tagRepository.GetTagsForProperty(contentId, propertyTypeAlias, group, culture);
}
}
/// <summary>
/// Gets all tags attached to an entity (content, media or member) by entity id
/// </summary>
/// <remarks>Use the optional tagGroup parameter to limit the
/// result to a specific 'Tag Group'.</remarks>
/// <param name="contentId">The content item id to get tags for</param>
/// <param name="tagGroup">Optional name of the 'Tag Group'</param>
/// <returns>An enumerable list of <see cref="ITag"/></returns>
public IEnumerable<ITag> GetTagsForEntity(Guid contentId, string tagGroup = null)
/// <inheritdoc />
public IEnumerable<ITag> GetTagsForEntity(Guid contentId, string group = null, string culture = null)
{
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
using (ScopeProvider.CreateScope(autoComplete: true))
{
return _tagRepository.GetTagsForEntity(contentId, tagGroup);
return _tagRepository.GetTagsForEntity(contentId, group, culture);
}
}
}
+1
View File
@@ -366,6 +366,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" />
+29
View File
@@ -1,4 +1,6 @@
using System;
using System.Linq;
using Examine;
using Examine.LuceneEngine.Providers;
using Lucene.Net.Analysis;
using Lucene.Net.Index;
@@ -6,6 +8,7 @@ using Lucene.Net.QueryParsers;
using Lucene.Net.Search;
using Lucene.Net.Store;
using Version = Lucene.Net.Util.Version;
using Umbraco.Core.Logging;
namespace Umbraco.Examine
{
@@ -37,6 +40,32 @@ namespace Umbraco.Examine
}
}
/// <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>
@@ -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
@@ -25,7 +31,7 @@ namespace Umbraco.Tests.Benchmarks
// see benchmarkdotnet FAQ
Add(Job.Default
.WithLaunchCount(1) // benchmark process will be launched only once
.WithIterationTime(TimeInterval.FromMilliseconds(400))
.WithIterationTime(TimeInterval.FromMilliseconds(400))
.WithWarmupCount(3)
.WithIterationCount(6));
}
@@ -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" />
@@ -22,6 +22,7 @@ namespace Umbraco.Tests.Composing
public class TypeLoaderTests
{
private TypeLoader _typeLoader;
[SetUp]
public void Initialize()
{
@@ -53,6 +54,12 @@ namespace Umbraco.Tests.Composing
public void TearDown()
{
_typeLoader = null;
// cleanup
var assDir = new FileInfo(Assembly.GetExecutingAssembly().Location).Directory;
foreach (var d in Directory.GetDirectories(Path.Combine(assDir.FullName, "TypeLoader")))
Directory.Delete(d, true);
}
private DirectoryInfo PrepareFolder()
@@ -5,6 +5,7 @@ using NUnit.Framework;
using Umbraco.Core.Logging;
using Umbraco.Core.Migrations;
using Umbraco.Core.Migrations.Install;
using Umbraco.Core.Migrations.Upgrade;
using Umbraco.Core.Persistence.DatabaseModelDefinitions;
using Umbraco.Core.Persistence.Dtos;
using Umbraco.Core.Services;
@@ -34,11 +35,12 @@ namespace Umbraco.Tests.Migrations
using (var scope = ScopeProvider.CreateScope())
{
var upgrader = new MigrationTests.TestUpgrader(ScopeProvider, builder, Mock.Of<IKeyValueService>(), new PostMigrationCollection(Enumerable.Empty<IPostMigration>()), logger,
new MigrationPlan("test", builder, logger)
.Add<CreateTableOfTDtoMigration>(string.Empty, "done"));
var upgrader = new Upgrader(
new MigrationPlan("test")
.From(string.Empty)
.To<CreateTableOfTDtoMigration>("done"));
upgrader.Execute();
upgrader.Execute(ScopeProvider, builder, Mock.Of<IKeyValueService>(), logger);
var helper = new DatabaseSchemaCreator(scope.Database, logger);
var exists = helper.TableExists("umbracoUser");
@@ -71,12 +73,13 @@ namespace Umbraco.Tests.Migrations
using (var scope = ScopeProvider.CreateScope())
{
var upgrader = new MigrationTests.TestUpgrader(ScopeProvider, builder, Mock.Of<IKeyValueService>(), new PostMigrationCollection(Enumerable.Empty<IPostMigration>()), logger,
new MigrationPlan("test", builder, logger)
.Add<CreateTableOfTDtoMigration>(string.Empty, "a")
.Add<DeleteKeysAndIndexesMigration>("a", "done"));
var upgrader = new Upgrader(
new MigrationPlan("test")
.From(string.Empty)
.To<CreateTableOfTDtoMigration>("a")
.To<DeleteKeysAndIndexesMigration>("done"));
upgrader.Execute();
upgrader.Execute(ScopeProvider, builder, Mock.Of<IKeyValueService>(), logger);
scope.Complete();
}
}
@@ -106,13 +109,14 @@ namespace Umbraco.Tests.Migrations
using (var scope = ScopeProvider.CreateScope())
{
var upgrader = new MigrationTests.TestUpgrader(ScopeProvider, builder, Mock.Of<IKeyValueService>(), new PostMigrationCollection(Enumerable.Empty<IPostMigration>()), logger,
new MigrationPlan("test", builder, logger)
.Add<CreateTableOfTDtoMigration>(string.Empty, "a")
.Add<DeleteKeysAndIndexesMigration>("a", "b")
.Add<CreateKeysAndIndexesOfTDtoMigration>("b", "done"));
var upgrader = new Upgrader(
new MigrationPlan("test")
.From(string.Empty)
.To<CreateTableOfTDtoMigration>("a")
.To<DeleteKeysAndIndexesMigration>("b")
.To<CreateKeysAndIndexesOfTDtoMigration>("done"));
upgrader.Execute();
upgrader.Execute(ScopeProvider, builder, Mock.Of<IKeyValueService>(), logger);
scope.Complete();
}
}
@@ -142,13 +146,14 @@ namespace Umbraco.Tests.Migrations
using (var scope = ScopeProvider.CreateScope())
{
var upgrader = new MigrationTests.TestUpgrader(ScopeProvider, builder, Mock.Of<IKeyValueService>(), new PostMigrationCollection(Enumerable.Empty<IPostMigration>()), logger,
new MigrationPlan("test", builder, logger)
.Add<CreateTableOfTDtoMigration>(string.Empty, "a")
.Add<DeleteKeysAndIndexesMigration>("a", "b")
.Add<CreateKeysAndIndexesMigration>("b", "done"));
var upgrader = new Upgrader(
new MigrationPlan("test")
.From(string.Empty)
.To<CreateTableOfTDtoMigration>("a")
.To<DeleteKeysAndIndexesMigration>("b")
.To<CreateKeysAndIndexesMigration>("done"));
upgrader.Execute();
upgrader.Execute(ScopeProvider, builder, Mock.Of<IKeyValueService>(), logger);
scope.Complete();
}
}
@@ -176,12 +181,13 @@ namespace Umbraco.Tests.Migrations
using (var scope = ScopeProvider.CreateScope())
{
var upgrader = new MigrationTests.TestUpgrader(ScopeProvider, builder, Mock.Of<IKeyValueService>(), new PostMigrationCollection(Enumerable.Empty<IPostMigration>()), logger,
new MigrationPlan("test", builder, logger)
.Add<CreateTableOfTDtoMigration>(string.Empty, "a")
.Add<CreateColumnMigration>("a", "done"));
var upgrader = new Upgrader(
new MigrationPlan("test")
.From(string.Empty)
.To<CreateTableOfTDtoMigration>("a")
.To<CreateColumnMigration>("done"));
upgrader.Execute();
upgrader.Execute(ScopeProvider, builder, Mock.Of<IKeyValueService>(), logger);
scope.Complete();
}
}
@@ -1,7 +1,9 @@
using System;
using System.Linq;
using Moq;
using NPoco;
using NUnit.Framework;
using Umbraco.Core;
using Umbraco.Core.Logging;
using Umbraco.Core.Migrations;
using Umbraco.Core.Migrations.Upgrade;
@@ -46,13 +48,10 @@ namespace Umbraco.Tests.Migrations
}
});
// fixme - NOT a migration collection builder, just a migration builder
// done, remove everywhere else, and delete migrationCollection stuff entirely
var plan = new MigrationPlan("default", migrationBuilder, logger)
var plan = new MigrationPlan("default")
.From(string.Empty)
.Chain<DeleteRedirectUrlTable>("{4A9A1A8F-0DA1-4BCF-AD06-C19D79152E35}")
.Chain<NoopMigration>("VERSION.33");
.To<DeleteRedirectUrlTable>("{4A9A1A8F-0DA1-4BCF-AD06-C19D79152E35}")
.To<NoopMigration>("VERSION.33");
var kvs = Mock.Of<IKeyValueService>();
Mock.Get(kvs).Setup(x => x.GetValue(It.IsAny<string>())).Returns<string>(k => k == "Umbraco.Tests.MigrationPlan" ? string.Empty : null);
@@ -64,7 +63,7 @@ namespace Umbraco.Tests.Migrations
var sourceState = kvs.GetValue("Umbraco.Tests.MigrationPlan") ?? string.Empty;
// execute plan
state = plan.Execute(s, sourceState);
state = plan.Execute(s, sourceState, migrationBuilder, logger);
// save new state
kvs.SetValue("Umbraco.Tests.MigrationPlan", sourceState, state);
@@ -81,63 +80,103 @@ namespace Umbraco.Tests.Migrations
[Test]
public void CanAddMigrations()
{
var plan = new MigrationPlan("default", Mock.Of<IMigrationBuilder>(), Mock.Of<ILogger>());
plan.Add(string.Empty, "aaa");
plan.Add("aaa", "bbb");
plan.Add("bbb", "ccc");
var plan = new MigrationPlan("default");
plan
.From(string.Empty)
.To("aaa")
.To("bbb")
.To("ccc");
}
[Test]
public void CannotTransitionToSameState()
{
var plan = new MigrationPlan("default", Mock.Of<IMigrationBuilder>(), Mock.Of<ILogger>());
var plan = new MigrationPlan("default");
Assert.Throws<ArgumentException>(() =>
{
plan.Add("aaa", "aaa");
plan.From("aaa").To("aaa");
});
}
[Test]
public void OnlyOneTransitionPerState()
{
var plan = new MigrationPlan("default", Mock.Of<IMigrationBuilder>(), Mock.Of<ILogger>());
plan.Add("aaa", "bbb");
var plan = new MigrationPlan("default");
plan.From("aaa").To("bbb");
Assert.Throws<InvalidOperationException>(() =>
{
plan.Add("aaa", "ccc");
plan.From("aaa").To("ccc");
});
}
[Test]
public void CannotContainTwoMoreHeads()
{
var plan = new MigrationPlan("default", Mock.Of<IMigrationBuilder>(), Mock.Of<ILogger>());
plan.Add(string.Empty, "aaa");
plan.Add("aaa", "bbb");
plan.Add("ccc", "ddd");
var plan = new MigrationPlan("default");
plan
.From(string.Empty)
.To("aaa")
.To("bbb")
.From("ccc")
.To("ddd");
Assert.Throws<Exception>(() => plan.Validate());
}
[Test]
public void CannotContainLoops()
{
var plan = new MigrationPlan("default", Mock.Of<IMigrationBuilder>(), Mock.Of<ILogger>());
plan.Add(string.Empty, "aaa");
plan.Add("aaa", "bbb");
plan.Add("bbb", "ccc");
plan.Add("ccc", "aaa");
var plan = new MigrationPlan("default");
plan
.From("aaa")
.To("bbb")
.To("ccc")
.To("aaa");
Assert.Throws<Exception>(() => plan.Validate());
}
[Test]
public void ValidateUmbracoPlan()
{
var plan = new UmbracoPlan(Mock.Of<IMigrationBuilder>(), Mock.Of<ILogger>());
var plan = new UmbracoPlan();
plan.Validate();
Console.WriteLine(plan.FinalState);
Assert.IsFalse(string.IsNullOrWhiteSpace(plan.FinalState));
}
[Test]
public void CanCopyChain()
{
var plan = new MigrationPlan("default");
plan
.From(string.Empty)
.To("aaa")
.To("bbb")
.To("ccc")
.To("ddd")
.To("eee");
plan
.From("xxx")
.To("yyy", "bbb", "ddd")
.To("eee");
WritePlanToConsole(plan);
plan.Validate();
Assert.AreEqual("eee", plan.FollowPath("xxx"));
Assert.AreEqual("yyy", plan.FollowPath("xxx", "yyy"));
}
private void WritePlanToConsole(MigrationPlan plan)
{
var final = plan.Transitions.First(x => x.Value == null).Key;
Console.WriteLine("plan \"{0}\" to final state \"{1}\":", plan.Name, final);
foreach (var (_, transition) in plan.Transitions)
if (transition != null)
Console.WriteLine(transition);
}
public class DeleteRedirectUrlTable : MigrationBase
{
public DeleteRedirectUrlTable(IMigrationContext context)
+16 -12
View File
@@ -16,28 +16,32 @@ namespace Umbraco.Tests.Migrations
[TestFixture]
public class MigrationTests
{
public class TestUpgrader : Upgrader
public class TestUpgraderWithPostMigrations : Upgrader
{
private readonly MigrationPlan _plan;
private PostMigrationCollection _postMigrations;
public TestUpgrader(IScopeProvider scopeProvider, IMigrationBuilder migrationBuilder, IKeyValueService keyValueService, PostMigrationCollection postMigrations, ILogger logger, MigrationPlan plan)
: base(scopeProvider, migrationBuilder, keyValueService, postMigrations, logger)
public TestUpgraderWithPostMigrations(MigrationPlan plan)
: base(plan)
{ }
public void Execute(IScopeProvider scopeProvider, IMigrationBuilder migrationBuilder, IKeyValueService keyValueService, ILogger logger, PostMigrationCollection postMigrations)
{
_plan = plan;
_postMigrations = postMigrations;
Execute(scopeProvider, migrationBuilder, keyValueService, logger);
}
protected override MigrationPlan GetPlan()
public override void AfterMigrations(IScope scope, ILogger logger)
{
return _plan;
}
// run post-migrations
var originVersion = new SemVersion(0);
var targetVersion = new SemVersion(0);
protected override (SemVersion, SemVersion) GetVersions()
{
return (new SemVersion(0), new SemVersion(0));
// run post-migrations
foreach (var postMigration in _postMigrations)
postMigration.Execute(Name, scope, originVersion, targetVersion, logger);
}
}
public class TestScopeProvider : IScopeProvider
{
private readonly IScope _scope;
@@ -50,10 +50,9 @@ namespace Umbraco.Tests.Migrations
var sqlContext = new SqlContext(new SqlCeSyntaxProvider(), DatabaseType.SQLCe, Mock.Of<IPocoDataFactory>());
var scopeProvider = new MigrationTests.TestScopeProvider(scope) { SqlContext = sqlContext };
var u1 = new MigrationTests.TestUpgrader(scopeProvider, builder, Mock.Of<IKeyValueService>(), posts, logger,
new MigrationPlan("Test", builder, logger)
.Add<NoopMigration>(string.Empty, "done"));
u1.Execute();
var u1 = new MigrationTests.TestUpgraderWithPostMigrations(
new MigrationPlan("Test").From(string.Empty).To("done"));
u1.Execute(scopeProvider, builder, Mock.Of<IKeyValueService>(), logger, posts);
Assert.AreEqual(1, changed1.CountExecuted);
}
@@ -94,18 +93,16 @@ namespace Umbraco.Tests.Migrations
var sqlContext = new SqlContext(new SqlCeSyntaxProvider(), DatabaseType.SQLCe, Mock.Of<IPocoDataFactory>());
var scopeProvider = new MigrationTests.TestScopeProvider(scope) { SqlContext = sqlContext };
var u1 = new MigrationTests.TestUpgrader(scopeProvider, builder, Mock.Of<IKeyValueService>(), posts, logger,
new MigrationPlan("Test1", builder, logger)
.Add<NoopMigration>(string.Empty, "done"));
u1.Execute();
var u1 = new MigrationTests.TestUpgraderWithPostMigrations(
new MigrationPlan("Test1").From(string.Empty).To("done"));
u1.Execute(scopeProvider, builder, Mock.Of<IKeyValueService>(), logger, posts);
Assert.AreEqual(1, changed1.CountExecuted);
Assert.AreEqual(0, changed2.CountExecuted);
var u2 = new MigrationTests.TestUpgrader(scopeProvider, builder, Mock.Of<IKeyValueService>(), posts, logger,
new MigrationPlan("Test2", builder, logger)
.Add<NoopMigration>(string.Empty, "done"));
u2.Execute();
var u2 = new MigrationTests.TestUpgraderWithPostMigrations(
new MigrationPlan("Test2").From(string.Empty).To("done"));
u2.Execute(scopeProvider, builder, Mock.Of<IKeyValueService>(), logger, posts);
Assert.AreEqual(1, changed1.CountExecuted);
Assert.AreEqual(1, changed2.CountExecuted);
@@ -403,7 +403,6 @@ namespace Umbraco.Tests.Persistence.NPocoTests
.From<Thing1Dto>()
.Where<Thing1Dto>(x => x.Id == 1);
sql.WriteToConsole();
var dto = scope.Database.Fetch<Thing1Dto>(sql).FirstOrDefault();
Assert.IsNotNull(dto);
Assert.AreEqual("one", dto.Name);
@@ -415,7 +414,6 @@ namespace Umbraco.Tests.Persistence.NPocoTests
//Assert.AreEqual("one", dto.Name);
var sql3 = new Sql(sql.SQL, 1);
sql.WriteToConsole();
dto = scope.Database.Fetch<Thing1Dto>(sql3).FirstOrDefault();
Assert.IsNotNull(dto);
Assert.AreEqual("one", dto.Name);
@@ -1,6 +1,7 @@
using System.Collections.Generic;
using NPoco;
using NUnit.Framework;
using Umbraco.Core.Persistence.Querying;
using Umbraco.Core.Persistence;
using Umbraco.Core.Persistence.Dtos;
using Umbraco.Tests.TestHelpers;
@@ -11,6 +12,80 @@ namespace Umbraco.Tests.Persistence.NPocoTests
[TestFixture]
public class NPocoSqlExtensionsTests : BaseUsingSqlCeSyntax
{
[Test]
public void WhereTest()
{
var sql = new Sql<ISqlContext>(SqlContext)
.Select("*")
.From<PropertyDataDto>()
.Where<PropertyDataDto>(x => x.LanguageId == null);
Assert.AreEqual("SELECT *\nFROM [umbracoPropertyData]\nWHERE (([umbracoPropertyData].[languageId] is null))", sql.SQL, sql.SQL);
sql = new Sql<ISqlContext>(SqlContext)
.Select("*")
.From<PropertyDataDto>()
.Where<PropertyDataDto>(x => x.LanguageId == 123);
Assert.AreEqual("SELECT *\nFROM [umbracoPropertyData]\nWHERE (([umbracoPropertyData].[languageId] = @0))", sql.SQL, sql.SQL);
var id = 123;
sql = new Sql<ISqlContext>(SqlContext)
.Select("*")
.From<PropertyDataDto>()
.Where<PropertyDataDto>(x => x.LanguageId == id);
Assert.AreEqual("SELECT *\nFROM [umbracoPropertyData]\nWHERE (([umbracoPropertyData].[languageId] = @0))", sql.SQL, sql.SQL);
int? nid = 123;
sql = new Sql<ISqlContext>(SqlContext)
.Select("*")
.From<PropertyDataDto>()
.Where<PropertyDataDto>(x => x.LanguageId == nid);
Assert.AreEqual("SELECT *\nFROM [umbracoPropertyData]\nWHERE (([umbracoPropertyData].[languageId] = @0))", sql.SQL, sql.SQL);
// but the above comparison fails if @0 is null
// what we want is something similar to:
sql = new Sql<ISqlContext>(SqlContext)
.Select("*")
.From<PropertyDataDto>()
.Where<PropertyDataDto>(x => (nid == null && x.LanguageId == null) || (nid != null && x.LanguageId == nid));
Assert.AreEqual("SELECT *\nFROM [umbracoPropertyData]\nWHERE ((((@0 is null) AND ([umbracoPropertyData].[languageId] is null)) OR ((@1 is not null) AND ([umbracoPropertyData].[languageId] = @2))))", sql.SQL, sql.SQL);
// new SqlNullableEquals method does it automatically
// 'course it would be nicer if '==' could do it
// see note in ExpressionVisitorBase for SqlNullableEquals
//sql = new Sql<ISqlContext>(SqlContext)
// .Select("*")
// .From<PropertyDataDto>()
// .Where<PropertyDataDto>(x => x.LanguageId.SqlNullableEquals(nid));
//Assert.AreEqual("SELECT *\nFROM [umbracoPropertyData]\nWHERE ((((@0 is null) AND ([umbracoPropertyData].[languageId] is null)) OR ((@0 is not null) AND ([umbracoPropertyData].[languageId] = @0))))", sql.SQL, sql.SQL);
// but, the expression above fails with SQL CE, 'specified argument for the function is not valid' in 'isnull' function
// so... compare with fallback values
sql = new Sql<ISqlContext>(SqlContext)
.Select("*")
.From<PropertyDataDto>()
.Where<PropertyDataDto>(x => x.LanguageId.SqlNullableEquals(nid, -1));
Assert.AreEqual("SELECT *\nFROM [umbracoPropertyData]\nWHERE ((COALESCE([umbracoPropertyData].[languageId],@0) = COALESCE(@1,@0)))", sql.SQL, sql.SQL);
}
[Test]
public void SqlNullableEqualsTest()
{
int? a, b;
a = b = null;
Assert.IsTrue(a.SqlNullableEquals(b, -1));
b = 2;
Assert.IsFalse(a.SqlNullableEquals(b, -1));
a = 2;
Assert.IsTrue(a.SqlNullableEquals(b, -1));
b = null;
Assert.IsFalse(a.SqlNullableEquals(b, -1));
}
[Test]
public void WhereInValueFieldTest()
{
@@ -117,7 +192,6 @@ INNER JOIN [dto2] ON [dto1].[id] = [dto2].[dto1id]".NoCrLf(), sql.SQL.NoCrLf());
var sql = Sql()
.Update<DataTypeDto>(u => u.Set(x => x.EditorAlias, "Umbraco.ColorPicker"))
.Where<DataTypeDto>(x => x.EditorAlias == "Umbraco.ColorPickerAlias");
sql.WriteToConsole();
}
[TableName("dto1")]
@@ -27,15 +27,9 @@ namespace Umbraco.Tests.Persistence.NPocoTests
.From("zbThing1")
.Where("id=@id", new { id = SqlTemplate.Arg("id") })).Sql(new { id = 1 });
sql.WriteToConsole();
var sql2 = sqlTemplates.Get("xxx", x => throw new InvalidOperationException("Should be cached.")).Sql(1);
sql2.WriteToConsole();
var sql3 = sqlTemplates.Get("xxx", x => throw new InvalidOperationException("Should be cached.")).Sql(new { id = 1 });
sql3.WriteToConsole();
}
[Test]
@@ -75,8 +69,8 @@ namespace Umbraco.Tests.Persistence.NPocoTests
Assert.AreEqual(1, sql.Arguments.Length);
Assert.AreEqual(123, sql.Arguments[0]);
Assert.Throws<InvalidOperationException>(() => template.Sql(new { xvalue = 123 }).WriteToConsole());
Assert.Throws<InvalidOperationException>(() => template.Sql(new { value = 123, xvalue = 456 }).WriteToConsole());
Assert.Throws<InvalidOperationException>(() => template.Sql(new { xvalue = 123 }));
Assert.Throws<InvalidOperationException>(() => template.Sql(new { value = 123, xvalue = 456 }));
var i = 666;
@@ -121,8 +115,8 @@ namespace Umbraco.Tests.Persistence.NPocoTests
Assert.AreEqual(1, sql.Arguments.Length);
Assert.AreEqual(123, sql.Arguments[0]);
Assert.Throws<InvalidOperationException>(() => template.Sql(new { j = 123 }).WriteToConsole());
Assert.Throws<InvalidOperationException>(() => template.Sql(new { i = 123, j = 456 }).WriteToConsole());
Assert.Throws<InvalidOperationException>(() => template.Sql(new { j = 123 }));
Assert.Throws<InvalidOperationException>(() => template.Sql(new { i = 123, j = 456 }));
// now with more arguments
@@ -305,12 +305,10 @@ namespace Umbraco.Tests.Persistence.NPocoTests
.From<UserLoginDto>()
.Where<UserLoginDto>(x => x.SessionId == sessionId);
sql.WriteToConsole();
Assert.AreEqual("SELECT * FROM [umbracoUserLogin] WHERE (([umbracoUserLogin].[sessionId] = @0))", sql.SQL.NoCrLf());
sql = sql.ForUpdate();
sql.WriteToConsole();
Assert.AreEqual("SELECT * FROM [umbracoUserLogin] WITH (UPDLOCK) WHERE (([umbracoUserLogin].[sessionId] = @0))", sql.SQL.NoCrLf());
}
}
@@ -107,8 +107,6 @@ namespace Umbraco.Tests.Persistence.Querying
var translator = new SqlTranslator<IContent>(sql, query);
var result = translator.Translate();
result.WriteToConsole();
Assert.AreEqual("-1,1046,1076,1089%", result.Arguments[0]);
Assert.AreEqual(1046, result.Arguments[1]);
Assert.AreEqual(true, result.Arguments[2]);
@@ -106,12 +106,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(Container.GetInstance<PropertyEditorCollection>(), IndexInitializer.GetMockMediaService());
@@ -119,7 +118,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();
@@ -139,7 +140,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(Container.GetInstance<PropertyEditorCollection>(), IndexInitializer.GetMockMediaService());
@@ -147,9 +147,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();
@@ -187,7 +188,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(Container.GetInstance<PropertyEditorCollection>(), IndexInitializer.GetMockMediaService());
@@ -197,6 +197,7 @@ namespace Umbraco.Tests.PublishedContent
validator: new ContentValueSetValidator(true)))
using (indexer.ProcessNonAsync())
{
rebuilder.RegisterIndex(indexer.Name);
rebuilder.Populate(indexer);
var searcher = indexer.GetSearcher();
@@ -215,7 +216,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(Container.GetInstance<PropertyEditorCollection>(), IndexInitializer.GetMockMediaService());
@@ -225,6 +225,7 @@ namespace Umbraco.Tests.PublishedContent
validator: new ContentValueSetValidator(true)))
using (indexer.ProcessNonAsync())
{
rebuilder.RegisterIndex(indexer.Name);
rebuilder.Populate(indexer);
var searcher = indexer.GetSearcher();
@@ -243,7 +244,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(Container.GetInstance<PropertyEditorCollection>(), IndexInitializer.GetMockMediaService());
@@ -253,6 +253,7 @@ namespace Umbraco.Tests.PublishedContent
validator: new ContentValueSetValidator(true)))
using (indexer.ProcessNonAsync())
{
rebuilder.RegisterIndex(indexer.Name);
rebuilder.Populate(indexer);
var searcher = indexer.GetSearcher();
@@ -271,7 +272,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(Container.GetInstance<PropertyEditorCollection>(), IndexInitializer.GetMockMediaService());
@@ -282,6 +282,7 @@ namespace Umbraco.Tests.PublishedContent
validator: new ContentValueSetValidator(true)))
using (indexer.ProcessNonAsync())
{
rebuilder.RegisterIndex(indexer.Name);
rebuilder.Populate(indexer);
var ctx = GetUmbracoContext("/test");
@@ -297,7 +298,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(Container.GetInstance<PropertyEditorCollection>(), IndexInitializer.GetMockMediaService());
@@ -307,6 +307,7 @@ namespace Umbraco.Tests.PublishedContent
validator: new ContentValueSetValidator(true)))
using (indexer.ProcessNonAsync())
{
rebuilder.RegisterIndex(indexer.Name);
rebuilder.Populate(indexer);
@@ -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()
{
+1
View File
@@ -133,6 +133,7 @@
<Compile Include="PublishedContent\SolidPublishedSnapshot.cs" />
<Compile Include="PublishedContent\NuCacheTests.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" />
+2801 -2813
View File
File diff suppressed because it is too large Load Diff
@@ -12,11 +12,12 @@
//expose the property/methods for other directives to use
this.content = $scope.content;
$scope.activeVariant = _.find(this.content.variants, variant => {
this.activeVariant = _.find(this.content.variants, variant => {
return variant.active;
});
$scope.activeVariant = this.activeVariant;
$scope.defaultVariant = _.find(this.content.variants, variant => {
return variant.language.isDefault;
});
@@ -95,7 +95,7 @@ angular.module('umbraco.directives')
};
})
.directive('onOutsideClick', function ($timeout) {
.directive('onOutsideClick', function ($timeout, angularHelper) {
return function (scope, element, attrs) {
var eventBindings = [];
@@ -136,7 +136,7 @@ angular.module('umbraco.directives')
return;
}
scope.$apply(attrs.onOutsideClick);
angularHelper.safeApply(scope, attrs.onOutsideClick);
}
@@ -274,7 +274,7 @@ angular.module("umbraco.directives")
}
//// INIT /////
$image.load(function(){
$image.on("load", function(){
$timeout(function(){
init($image);
});
@@ -27,34 +27,34 @@ angular.module("umbraco.directives")
var $image = element.find("img");
scope.loaded = false;
$image.load(function(){
$timeout(function(){
$image.width("auto");
$image.height("auto");
$image.on("load", function() {
$timeout(function () {
$image.width("auto");
$image.height("auto");
scope.image = {};
scope.image.width = $image[0].width;
scope.image.height = $image[0].height;
scope.image = {};
scope.image.width = $image[0].width;
scope.image.height = $image[0].height;
//we force a lower thumbnail size to fit the max size
//we do not compare to the image dimensions, but the thumbs
if(scope.maxSize){
var ratioCalculation = cropperHelper.calculateAspectRatioFit(
scope.width,
scope.height,
scope.maxSize,
scope.maxSize,
false);
//we force a lower thumbnail size to fit the max size
//we do not compare to the image dimensions, but the thumbs
if (scope.maxSize) {
var ratioCalculation = cropperHelper.calculateAspectRatioFit(
scope.width,
scope.height,
scope.maxSize,
scope.maxSize,
false);
//so if we have a max size, override the thumb sizes
scope.width = ratioCalculation.width;
scope.height = ratioCalculation.height;
}
//so if we have a max size, override the thumb sizes
scope.width = ratioCalculation.width;
scope.height = ratioCalculation.height;
}
setPreviewStyle();
scope.loaded = true;
});
});
setPreviewStyle();
scope.loaded = true;
});
})
/// WATCHERS ////
scope.$watchCollection('[crop, center]', function(newValues, oldValues){
@@ -0,0 +1,277 @@
/**
@ngdoc directive
@name umbraco.directives.directive:umbTagsEditor
**/
(function () {
'use strict';
angular
.module('umbraco.directives')
.component('umbTagsEditor', {
transclude: true,
templateUrl: 'views/components/tags/umb-tags-editor.html',
controller: umbTagsEditorController,
controllerAs: 'vm',
bindings: {
value: "<",
config: "<",
validation: "<",
culture: "<?",
onValueChanged: "&"
}
});
function umbTagsEditorController($rootScope, assetsService, umbRequestHelper, angularHelper, $timeout, $element) {
var vm = this;
var typeahead;
var tagsHound;
vm.$onInit = onInit;
vm.$onChanges = onChanges;
vm.$onDestroy = onDestroy;
vm.validateMandatory = validateMandatory;
vm.addTagOnEnter = addTagOnEnter;
vm.addTag = addTag;
vm.removeTag = removeTag;
vm.showPrompt = showPrompt;
vm.hidePrompt = hidePrompt;
vm.htmlId = "t" + String.CreateGuid();
vm.isLoading = true;
vm.tagToAdd = "";
vm.promptIsVisible = "-1";
vm.viewModel = [];
function onInit() {
assetsService.loadJs("lib/typeahead.js/typeahead.bundle.min.js").then(function () {
vm.isLoading = false;
configureViewModel();
// Set the visible prompt to -1 to ensure it will not be visible
vm.promptIsVisible = "-1";
tagsHound = new Bloodhound({
datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'),
queryTokenizer: Bloodhound.tokenizers.whitespace,
//pre-fetch the tags for this category
prefetch: {
url: umbRequestHelper.getApiUrl("tagsDataBaseUrl", "GetTags", { tagGroup: vm.config.group, culture: vm.culture }),
//TTL = 5 minutes
ttl: 300000,
transform: dataTransform
},
//dynamically get the tags for this category (they may have changed on the server)
remote: {
url: umbRequestHelper.getApiUrl("tagsDataBaseUrl", "GetTags", { tagGroup: vm.config.group, culture: vm.culture }),
transform: dataTransform
}
});
tagsHound.initialize(true);
//configure the type ahead
$timeout(function () {
var sources = {
//see: https://github.com/twitter/typeahead.js/blob/master/doc/jquery_typeahead.md#options
// name = the data set name, we'll make this the tag group name
name: vm.config.group,
display: "value",
//source: tagsHound
source: function (query, cb) {
tagsHound.search(query,
function(suggestions) {
cb(removeCurrentTagsFromSuggestions(suggestions));
});
}
};
var opts = {
//This causes some strangeness as it duplicates the textbox, best leave off for now.
hint: false,
highlight: true,
cacheKey: new Date(), // Force a cache refresh each time the control is initialized
minLength: 1
};
typeahead = $element.find('.tags-' + vm.htmlId).typeahead(opts, sources)
.bind("typeahead:selected", function (obj, datum, name) {
angularHelper.safeApply($rootScope, function () {
addTagInternal(datum["value"]);
vm.tagToAdd = "";
// clear the typed text
typeahead.typeahead('val', '');
});
}).bind("typeahead:autocompleted", function (obj, datum, name) {
angularHelper.safeApply($rootScope, function () {
addTagInternal(datum["value"]);
vm.tagToAdd = "";
});
}).bind("typeahead:opened", function (obj) {
console.log("opened ");
});
});
});
}
function onChanges(changes) {
// watch for value changes externally
if (changes.value) {
if (!changes.value.isFirstChange() && changes.value.currentValue !== changes.value.previousValue) {
configureViewModel();
//this is required to re-validate
vm.tagEditorForm.tagCount.$setViewValue(vm.viewModel.length);
}
}
}
function onDestroy() {
if (tagsHound) {
tagsHound.clearPrefetchCache();
tagsHound.clearRemoteCache();
tagsHound = null;
}
$element.find('.tags-' + vm.htmlId).typeahead('destroy');
}
function configureViewModel() {
if (vm.value) {
if (angular.isString(vm.value) && vm.value.length > 0) {
if (vm.config.storageType === "Json") {
//json storage
vm.viewModel = JSON.parse(vm.value);
updateModelValue(vm.viewModel);
}
else {
//csv storage
// split the csv string, and remove any duplicate values
let tempArray = vm.value.split(',').map(function (v) {
return v.trim();
});
vm.viewModel = tempArray.filter(function (v, i, self) {
return self.indexOf(v) === i;
});
updateModelValue(vm.viewModel);
}
}
else if (angular.isArray(vm.value)) {
vm.viewModel = vm.value;
}
}
}
function updateModelValue(val) {
if (val) {
vm.onValueChanged({ value: val });
}
else {
vm.onValueChanged({ value: [] });
}
}
/**
* Method required by the valPropertyValidator directive (returns true if the property editor has at least one tag selected)
*/
function validateMandatory() {
return {
isValid: !vm.validation.mandatory || (vm.viewModel != null && vm.viewModel.length > 0),
errorMsg: "Value cannot be empty",
errorKey: "required"
};
}
function addTagInternal(tagToAdd) {
if (tagToAdd != null && tagToAdd.length > 0) {
if (vm.viewModel.indexOf(tagToAdd) < 0) {
vm.viewModel.push(tagToAdd);
updateModelValue(vm.viewModel);
}
}
}
function addTagOnEnter(e) {
var code = e.keyCode || e.which;
if (code == 13) { //Enter keycode
if ($element.find('.tags-' + vm.htmlId).parent().find(".tt-menu .tt-cursor").length === 0) {
//this is required, otherwise the html form will attempt to submit.
e.preventDefault();
addTag();
}
}
}
function addTag() {
//ensure that we're not pressing the enter key whilst selecting a typeahead value from the drop down
//we need to use jquery because typeahead duplicates the text box
addTagInternal(vm.tagToAdd);
vm.tagToAdd = "";
//this clears the value stored in typeahead so it doesn't try to add the text again
// https://issues.umbraco.org/issue/U4-4947
typeahead.typeahead('val', '');
}
function removeTag(tag) {
var i = vm.viewModel.indexOf(tag);
if (i >= 0) {
// Make sure to hide the prompt so it does not stay open because another item gets a new number in the array index
vm.promptIsVisible = "-1";
// Remove the tag from the index
vm.viewModel.splice(i, 1);
updateModelValue(vm.viewModel);
}
}
function showPrompt(idx, tag) {
var i = vm.viewModel.indexOf(tag);
// Make the prompt visible for the clicked tag only
if (i === idx) {
vm.promptIsVisible = i;
}
}
function hidePrompt() {
vm.promptIsVisible = "-1";
}
//helper method to format the data for bloodhound
function dataTransform(list) {
//transform the result to what bloodhound wants
var tagList = _.map(list, function (i) {
return { value: i.text };
});
// remove current tags from the list
return $.grep(tagList, function (tag) {
return ($.inArray(tag.value, vm.viewModel) === -1);
});
}
// helper method to remove current tags
function removeCurrentTagsFromSuggestions(suggestions) {
return $.grep(suggestions, function (suggestion) {
return ($.inArray(suggestion.value, vm.viewModel) === -1);
});
}
}
})();
@@ -36,7 +36,7 @@ function valPropertyValidator(serverValidationManager) {
// Validation method
function validate (viewValue) {
// Calls the validition method
// Calls the validation method
var result = scope.valPropertyValidator();
if (!result.errorKey || result.isValid === undefined || !result.errorMsg) {
throw "The result object from valPropertyValidator does not contain required properties: isValid, errorKey, errorMsg";
@@ -55,6 +55,9 @@ function valPropertyValidator(serverValidationManager) {
propCtrl.setPropertyError(result.errorMsg);
}
}
// parsers are expected to return a value
return (result.isValid) ? viewValue : undefined;
};
// Parsers are called as soon as the value in the form input is modified
@@ -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;
}
}
@@ -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>
@@ -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>
@@ -254,7 +254,7 @@ namespace Umbraco.Web.Editors
},
{
"tagsDataBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl<TagsDataController>(
controller => controller.GetTags(""))
controller => controller.GetTags("", ""))
},
{
"examineMgmtBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl<ExamineManagementController>(
+2 -1
View File
@@ -1768,7 +1768,8 @@ namespace Umbraco.Web.Editors
contentSave,
propertyCollection,
(save, property) => Varies(property) ? property.GetValue(variant.Culture) : property.GetValue(), //get prop val
(save, property, v) => { if (Varies(property)) property.SetValue(v, variant.Culture); else property.SetValue(v); }); //set prop val
(save, property, v) => { if (Varies(property)) property.SetValue(v, variant.Culture); else property.SetValue(v); }, //set prop val
variant.Culture);
variantIndex++;
}
@@ -39,17 +39,12 @@ namespace Umbraco.Web.Editors
/// <summary>
/// Maps the dto property values to the persisted model
/// </summary>
/// <typeparam name="TPersisted"></typeparam>
/// <typeparam name="TSaved"></typeparam>
/// <param name="contentItem"></param>
/// <param name="dto"></param>
/// <param name="getPropertyValue"></param>
/// <param name="savePropertyValue"></param>
internal void MapPropertyValuesForPersistence<TPersisted, TSaved>(
TSaved contentItem,
ContentPropertyCollectionDto dto,
Func<TSaved, Property, object> getPropertyValue,
Action<TSaved, Property, object> savePropertyValue)
Action<TSaved, Property, object> savePropertyValue,
string culture)
where TPersisted : IContentBase
where TSaved : IContentSave<TPersisted>
{
@@ -70,7 +65,7 @@ namespace Umbraco.Web.Editors
// get the property
var property = contentItem.PersistedContent.Properties[propertyDto.Alias];
// prepare files, if any matching property and culture
var files = contentItem.UploadedFiles
.Where(x => x.PropertyAlias == propertyDto.Alias && x.Culture == propertyDto.Culture)
@@ -96,8 +91,8 @@ namespace Umbraco.Web.Editors
{
var tagConfiguration = ConfigurationEditor.ConfigurationAs<TagConfiguration>(propertyDto.DataType.Configuration);
if (tagConfiguration.Delimiter == default) tagConfiguration.Delimiter = tagAttribute.Delimiter;
//fixme how is this supposed to work with variants?
property.SetTagsValue(value, tagConfiguration);
var tagCulture = property.PropertyType.VariesByCulture() ? culture : null;
property.SetTagsValue(value, tagConfiguration, tagCulture);
}
else
savePropertyValue(contentItem, property, value);
+3 -2
View File
@@ -483,8 +483,9 @@ namespace Umbraco.Web.Editors
MapPropertyValuesForPersistence<IMedia, MediaItemSave>(
contentItem,
contentItem.PropertyCollectionDto,
(save, property) => property.GetValue(), //get prop val
(save, property, v) => property.SetValue(v)); //set prop val
(save, property) => property.GetValue(), //get prop val
(save, property, v) => property.SetValue(v), //set prop val
null); // media are all invariant
//We need to manually check the validation results here because:
// * We still need to save the entity even if there are validation value errors
+3 -2
View File
@@ -400,8 +400,9 @@ namespace Umbraco.Web.Editors
base.MapPropertyValuesForPersistence<IMember, MemberSave>(
contentItem,
contentItem.PropertyCollectionDto,
(save, property) => property.GetValue(), //get prop val
(save, property, v) => property.SetValue(v)); //set prop val
(save, property) => property.GetValue(), //get prop val
(save, property, v) => property.SetValue(v), //set prop val
null); // member are all invariant
}
/// <summary>
+20 -44
View File
@@ -1,5 +1,4 @@
using System.Collections.Generic;
using Umbraco.Core.Models;
using Umbraco.Core.Models.PublishedContent;
using Umbraco.Web.Models;
@@ -8,76 +7,53 @@ namespace Umbraco.Web
public interface ITagQuery
{
/// <summary>
/// Returns all content that is tagged with the specified tag value and optional tag group
/// Gets all documents tagged with the specified tag.
/// </summary>
/// <param name="tag"></param>
/// <param name="tagGroup"></param>
/// <returns></returns>
IEnumerable<IPublishedContent> GetContentByTag(string tag, string tagGroup = null);
IEnumerable<IPublishedContent> GetContentByTag(string tag, string group = null, string culture = null);
/// <summary>
/// Returns all content that has been tagged with any tag in the specified group
/// Gets all documents tagged with any tag in the specified group.
/// </summary>
/// <param name="tagGroup"></param>
/// <returns></returns>
IEnumerable<IPublishedContent> GetContentByTagGroup(string tagGroup);
IEnumerable<IPublishedContent> GetContentByTagGroup(string group, string culture = null);
/// <summary>
/// Returns all Media that is tagged with the specified tag value and optional tag group
/// Gets all media tagged with the specified tag.
/// </summary>
/// <param name="tag"></param>
/// <param name="tagGroup"></param>
/// <returns></returns>
IEnumerable<IPublishedContent> GetMediaByTag(string tag, string tagGroup = null);
IEnumerable<IPublishedContent> GetMediaByTag(string tag, string group = null, string culture = null);
/// <summary>
/// Returns all Media that has been tagged with any tag in the specified group
/// Gets all media tagged with any tag in the specified group.
/// </summary>
/// <param name="tagGroup"></param>
/// <returns></returns>
IEnumerable<IPublishedContent> GetMediaByTagGroup(string tagGroup);
IEnumerable<IPublishedContent> GetMediaByTagGroup(string group, string culture = null);
/// <summary>
/// Get every tag stored in the database (with optional group)
/// Gets all tags.
/// </summary>
IEnumerable<TagModel> GetAllTags(string group = null);
IEnumerable<TagModel> GetAllTags(string group = null, string culture = null);
/// <summary>
/// Get all tags for content items (with optional group)
/// Gets all document tags.
/// </summary>
/// <param name="group"></param>
/// <returns></returns>
IEnumerable<TagModel> GetAllContentTags(string group = null);
IEnumerable<TagModel> GetAllContentTags(string group = null, string culture = null);
/// <summary>
/// Get all tags for media items (with optional group)
/// Gets all media tags.
/// </summary>
/// <param name="group"></param>
/// <returns></returns>
IEnumerable<TagModel> GetAllMediaTags(string group = null);
IEnumerable<TagModel> GetAllMediaTags(string group = null, string culture = null);
/// <summary>
/// Get all tags for member items (with optional group)
/// Gets all member tags.
/// </summary>
/// <param name="group"></param>
/// <returns></returns>
IEnumerable<TagModel> GetAllMemberTags(string group = null);
IEnumerable<TagModel> GetAllMemberTags(string group = null, string culture = null);
/// <summary>
/// Returns all tags attached to a property by entity id
/// Gets all tags attached to an entity via a property.
/// </summary>
/// <param name="contentId"></param>
/// <param name="propertyTypeAlias"></param>
/// <param name="tagGroup"></param>
/// <returns></returns>
IEnumerable<TagModel> GetTagsForProperty(int contentId, string propertyTypeAlias, string tagGroup = null);
IEnumerable<TagModel> GetTagsForProperty(int contentId, string propertyTypeAlias, string group = null, string culture = null);
/// <summary>
/// Returns all tags attached to an entity (content, media or member) by entity id
/// Gets all tags attached to an entity.
/// </summary>
/// <param name="contentId"></param>
/// <param name="tagGroup"></param>
/// <returns></returns>
IEnumerable<TagModel> GetTagsForEntity(int contentId, string tagGroup = null);
IEnumerable<TagModel> GetTagsForEntity(int contentId, string group = null, string culture = null);
}
}
@@ -1,50 +0,0 @@
using Semver;
using Umbraco.Core;
using Umbraco.Core.Logging;
using Umbraco.Core.Migrations;
using Umbraco.Core.Scoping;
namespace Umbraco.Web.Migrations
{
/// <summary>
/// This will execute after upgrading to remove any xml cache for media that are currently in the bin
/// </summary>
/// <remarks>
/// This will execute for specific versions -
///
/// * If current is less than or equal to 7.0.0
/// </remarks>
public class ClearMediaXmlCacheForDeletedItemsAfterUpgrade : IPostMigration
{
private readonly ILogger _logger;
public ClearMediaXmlCacheForDeletedItemsAfterUpgrade(ILogger logger)
{
_logger = logger;
}
public void Execute(string name, IScope scope, SemVersion originVersion, SemVersion targetVersion, ILogger logger)
{
if (name != Constants.System.UmbracoUpgradePlanName) return;
var target70 = new SemVersion(7 /*, 0, 0*/);
if (originVersion <= target70)
{
//This query is structured to work with MySql, SQLCE and SqlServer:
// http://issues.umbraco.org/issue/U4-3876
var syntax = scope.SqlContext.SqlSyntax;
var sql = @"DELETE FROM cmsContentXml WHERE nodeId IN
(SELECT nodeId FROM (SELECT DISTINCT cmsContentXml.nodeId FROM cmsContentXml
INNER JOIN umbracoNode ON cmsContentXml.nodeId = umbracoNode.id
WHERE nodeObjectType = '" + Constants.ObjectTypes.Media + "' AND " + syntax.GetQuotedColumnName("path") + " LIKE '%-21%') x)";
var count = scope.Database.Execute(sql);
_logger.Info<ClearMediaXmlCacheForDeletedItemsAfterUpgrade>("Cleared {Total} items from the media xml cache that were trashed and not meant to be there", count);
}
}
}
}
@@ -1,71 +0,0 @@
using System;
using NPoco;
using Semver;
using Umbraco.Core;
using Umbraco.Core.Logging;
using Umbraco.Core.Migrations;
using Umbraco.Core.Persistence.Dtos;
using Umbraco.Core.Scoping;
namespace Umbraco.Web.Migrations
{
/// <summary>
/// Creates the built in list view data types
/// </summary>
public class EnsureDefaultListViewDataTypesCreated : IPostMigration
{
public void Execute(string name, IScope scope, SemVersion originVersion, SemVersion targetVersion, ILogger logger)
{
if (name != Constants.System.UmbracoUpgradePlanName) return;
var target720 = new SemVersion(7, 2, 0);
if (originVersion > target720)
return;
var syntax = scope.SqlContext.SqlSyntax;
try
{
//Turn on identity insert if db provider is not mysql
if (syntax.SupportsIdentityInsert())
scope.Database.Execute(new Sql(string.Format("SET IDENTITY_INSERT {0} ON ", syntax.GetQuotedTableName("umbracoNode"))));
if (scope.Database.Exists<NodeDto>(Constants.DataTypes.DefaultContentListView))
{
//If this already exists then just exit
return;
}
scope.Database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = Constants.DataTypes.DefaultContentListView, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,-95", SortOrder = 2, UniqueId = new Guid("C0808DD3-8133-4E4B-8CE8-E2BEA84A96A4"), Text = Constants.Conventions.DataTypes.ListViewPrefix + "Content", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now });
scope.Database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = Constants.DataTypes.DefaultMediaListView, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,-96", SortOrder = 2, UniqueId = new Guid("3A0156C4-3B8C-4803-BDC1-6871FAA83FFF"), Text = Constants.Conventions.DataTypes.ListViewPrefix + "Media", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now });
scope.Database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = Constants.DataTypes.DefaultMembersListView, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,-97", SortOrder = 2, UniqueId = new Guid("AA2C52A0-CE87-4E65-A47C-7DF09358585D"), Text = Constants.Conventions.DataTypes.ListViewPrefix + "Members", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now });
}
finally
{
//Turn off identity insert if db provider is not mysql
if (syntax.SupportsIdentityInsert())
scope.Database.Execute(new Sql(string.Format("SET IDENTITY_INSERT {0} OFF;", syntax.GetQuotedTableName("umbracoNode"))));
}
try
{
//Turn on identity insert if db provider is not mysql
if (syntax.SupportsIdentityInsert())
scope.Database.Execute(new Sql(string.Format("SET IDENTITY_INSERT {0} ON ", syntax.GetQuotedTableName(Constants.DatabaseSchema.Tables.DataType))));
const string memberListViewConfiguration = "{\"pageSize\":10,\"orderBy\":Name,\"orderDirection\":asc,includeProperties:[{\"alias\":\"email\",\"isSystem\":1},{\"alias\":\"username\",\"isSystem\":1},{\"alias\":\"updateDate\",\"header\":\"Last edited\",\"isSystem\":1}]}";
scope.Database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = Constants.DataTypes.DefaultContentListView, EditorAlias = Constants.PropertyEditors.Aliases.ListView, DbType = "Nvarchar" });
scope.Database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = Constants.DataTypes.DefaultMediaListView, EditorAlias = Constants.PropertyEditors.Aliases.ListView, DbType = "Nvarchar" });
scope.Database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = Constants.DataTypes.DefaultMembersListView, EditorAlias = Constants.PropertyEditors.Aliases.ListView, DbType = "Nvarchar", Configuration = memberListViewConfiguration });
}
finally
{
//Turn off identity insert if db provider is not mysql
if (syntax.SupportsIdentityInsert())
scope.Database.Execute(new Sql(string.Format("SET IDENTITY_INSERT {0} OFF;", syntax.GetQuotedTableName(Constants.DatabaseSchema.Tables.DataType))));
}
}
}
}
@@ -1,53 +0,0 @@
using System.IO;
using Semver;
using Umbraco.Core;
using Umbraco.Core.IO;
using Umbraco.Core.Logging;
using Umbraco.Core.Migrations;
using Umbraco.Core.Migrations.Upgrade;
using Umbraco.Core.Scoping;
namespace Umbraco.Web.Migrations
{
/// <summary>
/// When upgrading version 7.3 the migration MigrateStylesheetDataToFile will execute but we don't want to overwrite the developers
/// files during the migration since other parts of the migration might fail. So once the migration is complete, we'll then copy over the temp
/// files that this migration created over top of the developer's files. We'll also create a backup of their files.
/// </summary>
public sealed class OverwriteStylesheetFilesFromTempFiles : IPostMigration
{
public void Execute(string name, IScope scope, SemVersion originVersion, SemVersion targetVersion, ILogger logger)
{
if (name != Constants.System.UmbracoUpgradePlanName) return;
var target73 = new SemVersion(7, 3, 0);
if (originVersion <= target73)
{
var tempCssFolder = IOHelper.MapPath("~/App_Data/TEMP/CssMigration/");
var cssFolder = IOHelper.MapPath("~/css");
if (Directory.Exists(tempCssFolder))
{
var files = Directory.GetFiles(tempCssFolder, "*.css", SearchOption.AllDirectories);
foreach (var file in files)
{
var relativePath = file.TrimStart(tempCssFolder).TrimStart("\\");
var cssFilePath = Path.Combine(cssFolder, relativePath);
if (File.Exists(cssFilePath))
{
//backup
var targetPath = Path.Combine(tempCssFolder, relativePath.EnsureEndsWith(".bak"));
logger.Info<OverwriteStylesheetFilesFromTempFiles>("CSS file is being backed up from {CssFilePath}, to {TargetPath} before being migrated to new format", cssFilePath, targetPath);
File.Copy(cssFilePath, targetPath, true);
}
//ensure the sub folder eixts
Directory.CreateDirectory(Path.GetDirectoryName(cssFilePath));
File.Copy(file, cssFilePath, true);
}
}
}
}
}
}
@@ -1,57 +0,0 @@
using System;
using Semver;
using Umbraco.Core;
using Umbraco.Core.Logging;
using Umbraco.Core.Migrations;
using Umbraco.Core.Scoping;
using Umbraco.Web.Composing;
using Umbraco.Web.PublishedCache.XmlPublishedCache;
namespace Umbraco.Web.Migrations
{
/// <summary>
/// Rebuilds the Xml caches after upgrading.
/// This will execute after upgrading to rebuild the xml cache
/// </summary>
/// <remarks>
/// <para>This cannot execute as part of a DB migration since it needs access to services and repositories.</para>
/// <para>Executes for:
/// - Media Xml : if current is less than, or equal to, 7.0.0 (superceeded by the next rule)
/// - Media & Content Xml : if current is less than, or equal to, 7.3.0 - because 7.3.0 adds .Key to cached items
/// </para>
/// </remarks>
public class RebuildXmlCachesAfterUpgrade : IPostMigration
{
public void Execute(string name, IScope scope, SemVersion originVersion, SemVersion targetVersion, ILogger logger)
{
if (name != Constants.System.UmbracoUpgradePlanName) return;
var v730 = new SemVersion(new Version(7, 3, 0));
var doMedia = originVersion < v730;
var doContent = originVersion < v730;
if (doMedia)
{
// fixme - maintain - for backward compatibility?! or replace with...?!
//var mediaService = (MediaService) ApplicationContext.Current.Services.MediaService;
//mediaService.RebuildXmlStructures();
var svc = Current.PublishedSnapshotService as PublishedSnapshotService;
svc?.RebuildMediaXml();
// note: not re-indexing medias?
}
if (doContent)
{
// fixme - maintain - for backward compatibility?! or replace with...?!
//var contentService = (ContentService) ApplicationContext.Current.Services.ContentService;
//contentService.RebuildXmlStructures();
var svc = Current.PublishedSnapshotService as PublishedSnapshotService;
svc?.RebuildContentAndPreviewXml();
}
}
}
}
@@ -15,9 +15,10 @@ namespace Umbraco.Web.PropertyEditors
[PluginController("UmbracoApi")]
public class TagsDataController : UmbracoAuthorizedApiController
{
public IEnumerable<TagModel> GetTags(string tagGroup)
public IEnumerable<TagModel> GetTags(string tagGroup, string culture)
{
return Umbraco.TagQuery.GetAllTags(tagGroup);
if (culture == string.Empty) culture = null;
return Umbraco.TagQuery.GetAllTags(tagGroup, culture);
}
}
}
+15 -59
View File
@@ -22,7 +22,6 @@ namespace Umbraco.Web
/// </summary>
public class PublishedContentQuery : IPublishedContentQuery
{
private readonly IPublishedContentQuery _query;
private readonly IPublishedContentCache _contentCache;
private readonly IPublishedMediaCache _mediaCache;
@@ -37,79 +36,52 @@ namespace Umbraco.Web
_mediaCache = mediaCache ?? throw new ArgumentNullException(nameof(mediaCache));
}
/// <summary>
/// Constructor used to wrap the ITypedPublishedContentQuery object passed in
/// </summary>
/// <param name="query"></param>
public PublishedContentQuery(IPublishedContentQuery query)
{
_query = query ?? throw new ArgumentNullException(nameof(query));
}
#region Content
public IPublishedContent Content(int id)
{
return _query == null
? ItemById(id, _contentCache)
: _query.Content(id);
return ItemById(id, _contentCache);
}
public IPublishedContent Content(Guid id)
{
return _query == null
? ItemById(id, _contentCache)
: _query.Content(id);
return ItemById(id, _contentCache);
}
public IPublishedContent Content(Udi id)
{
if (!(id is GuidUdi udi)) return null;
return _query == null
? ItemById(udi.Guid, _contentCache)
: _query.Content(udi.Guid);
return ItemById(udi.Guid, _contentCache);
}
public IPublishedContent ContentSingleAtXPath(string xpath, params XPathVariable[] vars)
{
return _query == null
? ItemByXPath(xpath, vars, _contentCache)
: _query.ContentSingleAtXPath(xpath, vars);
return ItemByXPath(xpath, vars, _contentCache);
}
public IEnumerable<IPublishedContent> Content(IEnumerable<int> ids)
{
return _query == null
? ItemsByIds(_contentCache, ids)
: _query.Content(ids);
return ItemsByIds(_contentCache, ids);
}
public IEnumerable<IPublishedContent> Content(IEnumerable<Guid> ids)
{
return _query == null
? ItemsByIds(_contentCache, ids)
: _query.Content(ids);
return ItemsByIds(_contentCache, ids);
}
public IEnumerable<IPublishedContent> ContentAtXPath(string xpath, params XPathVariable[] vars)
{
return _query == null
? ItemsByXPath(xpath, vars, _contentCache)
: _query.ContentAtXPath(xpath, vars);
return ItemsByXPath(xpath, vars, _contentCache);
}
public IEnumerable<IPublishedContent> ContentAtXPath(XPathExpression xpath, params XPathVariable[] vars)
{
return _query == null
? ItemsByXPath(xpath, vars, _contentCache)
: _query.ContentAtXPath(xpath, vars);
return ItemsByXPath(xpath, vars, _contentCache);
}
public IEnumerable<IPublishedContent> ContentAtRoot()
{
return _query == null
? ItemsAtRoot(_contentCache)
: _query.ContentAtRoot();
return ItemsAtRoot(_contentCache);
}
#endregion
@@ -118,45 +90,33 @@ namespace Umbraco.Web
public IPublishedContent Media(int id)
{
return _query == null
? ItemById(id, _mediaCache)
: _query.Media(id);
return ItemById(id, _mediaCache);
}
public IPublishedContent Media(Guid id)
{
return _query == null
? ItemById(id, _mediaCache)
: _query.Media(id);
return ItemById(id, _mediaCache);
}
public IPublishedContent Media(Udi id)
{
if (!(id is GuidUdi udi)) return null;
return _query == null
? ItemById(udi.Guid, _mediaCache)
: _query.Media(udi.Guid);
return ItemById(udi.Guid, _mediaCache);
}
public IEnumerable<IPublishedContent> Media(IEnumerable<int> ids)
{
return _query == null
? ItemsByIds(_mediaCache, ids)
: _query.Media(ids);
return ItemsByIds(_mediaCache, ids);
}
public IEnumerable<IPublishedContent> Media(IEnumerable<Guid> ids)
{
return _query == null
? ItemsByIds(_mediaCache, ids)
: _query.Media(ids);
return ItemsByIds(_mediaCache, ids);
}
public IEnumerable<IPublishedContent> MediaAtRoot()
{
return _query == null
? ItemsAtRoot(_mediaCache)
: _query.MediaAtRoot();
return ItemsAtRoot(_mediaCache);
}
@@ -231,8 +191,6 @@ namespace Umbraco.Web
{
//fixme: inject IExamineManager
if (_query != null) return _query.Search(skip, take, out totalRecords, term, useWildCards, indexName);
indexName = string.IsNullOrEmpty(indexName)
? Constants.UmbracoIndexes.ExternalIndexName
: indexName;
@@ -259,8 +217,6 @@ namespace Umbraco.Web
/// <inheritdoc />
public IEnumerable<PublishedSearchResult> Search(int skip, int take, out long totalRecords, ISearchCriteria criteria, ISearcher searcher = null)
{
if (_query != null) return _query.Search(skip, take, out totalRecords, criteria, searcher);
//fixme: inject IExamineManager
if (searcher == null)
{
+1 -5
View File
@@ -96,7 +96,7 @@ namespace Umbraco.Web.Routing
/// </remarks>
public string GetUrl(IPublishedContent content, bool absolute, string culture = null, Uri current = null)
=> GetUrl(content, GetMode(absolute), culture, current);
/// <summary>
/// Gets the url of a published content.
/// </summary>
@@ -196,10 +196,6 @@ namespace Umbraco.Web.Routing
if (culture == null)
culture = _variationContextAccessor?.VariationContext?.Culture ?? "";
}
else
{
culture = null;
}
if (current == null)
current = _umbracoContext.CleanedUmbracoUrl;
+1 -17
View File
@@ -222,23 +222,7 @@ namespace Umbraco.Web.Search
if (_isConfigured) return;
_isConfigured = true;
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<ExamineComponent>("Forcing index {IndexerName} to be unlocked since it was left in a locked state", luceneIndexer.Name);
IndexWriter.Unlock(dir);
}
}
examineManager.UnlockLuceneIndexes(logger);
}
}
+34 -137
View File
@@ -9,195 +9,92 @@ using Umbraco.Web.Models;
namespace Umbraco.Web
{
/// <summary>
/// A class that exposes methods used to query tag data in views
/// Implements <see cref="ITagQuery"/>.
/// </summary>
public class TagQuery : ITagQuery
{
//TODO: This class also acts as a wrapper for ITagQuery due to breaking changes, need to fix in
// version 8: http://issues.umbraco.org/issue/U4-6899
private readonly ITagQuery _wrappedQuery;
private readonly ITagService _tagService;
private readonly IPublishedContentQuery _contentQuery;
/// <summary>
/// Constructor for wrapping ITagQuery, see http://issues.umbraco.org/issue/U4-6899
/// Initializes a new instance of the <see cref="TagQuery"/> class.
/// </summary>
/// <param name="wrappedQuery"></param>
internal TagQuery(ITagQuery wrappedQuery)
{
if (wrappedQuery == null) throw new ArgumentNullException("wrappedQuery");
_wrappedQuery = wrappedQuery;
}
/// <summary>
/// Constructor
/// </summary>
/// <param name="tagService"></param>
/// <param name="contentQuery"></param>
public TagQuery(ITagService tagService, IPublishedContentQuery contentQuery)
{
if (tagService == null) throw new ArgumentNullException("tagService");
if (contentQuery == null) throw new ArgumentNullException("contentQuery");
_tagService = tagService;
_contentQuery = contentQuery;
_tagService = tagService ?? throw new ArgumentNullException(nameof(tagService));
_contentQuery = contentQuery ?? throw new ArgumentNullException(nameof(contentQuery));
}
/// <summary>
/// Returns all content that is tagged with the specified tag value and optional tag group
/// </summary>
/// <param name="tag"></param>
/// <param name="tagGroup"></param>
/// <returns></returns>
public IEnumerable<IPublishedContent> GetContentByTag(string tag, string tagGroup = null)
/// <inheritdoc />
public IEnumerable<IPublishedContent> GetContentByTag(string tag, string group = null, string culture = null)
{
//TODO: http://issues.umbraco.org/issue/U4-6899
if (_wrappedQuery != null) return _wrappedQuery.GetContentByTag(tag, tagGroup);
var ids = _tagService.GetTaggedContentByTag(tag, tagGroup)
var ids = _tagService.GetTaggedContentByTag(tag, group, culture)
.Select(x => x.EntityId);
return _contentQuery.Content(ids)
.Where(x => x != null);
}
/// <summary>
/// Returns all content that has been tagged with any tag in the specified group
/// </summary>
/// <param name="tagGroup"></param>
/// <returns></returns>
public IEnumerable<IPublishedContent> GetContentByTagGroup(string tagGroup)
/// <inheritdoc />
public IEnumerable<IPublishedContent> GetContentByTagGroup(string group, string culture = null)
{
//TODO: http://issues.umbraco.org/issue/U4-6899
if (_wrappedQuery != null) return _wrappedQuery.GetContentByTagGroup(tagGroup);
var ids = _tagService.GetTaggedContentByTagGroup(tagGroup)
var ids = _tagService.GetTaggedContentByTagGroup(group, culture)
.Select(x => x.EntityId);
return _contentQuery.Content(ids)
.Where(x => x != null);
}
/// <summary>
/// Returns all Media that is tagged with the specified tag value and optional tag group
/// </summary>
/// <param name="tag"></param>
/// <param name="tagGroup"></param>
/// <returns></returns>
public IEnumerable<IPublishedContent> GetMediaByTag(string tag, string tagGroup = null)
/// <inheritdoc />
public IEnumerable<IPublishedContent> GetMediaByTag(string tag, string group = null, string culture = null)
{
//TODO: http://issues.umbraco.org/issue/U4-6899
if (_wrappedQuery != null) return _wrappedQuery.GetMediaByTag(tag, tagGroup);
var ids = _tagService.GetTaggedMediaByTag(tag, tagGroup)
var ids = _tagService.GetTaggedMediaByTag(tag, group, culture)
.Select(x => x.EntityId);
return _contentQuery.Media(ids)
.Where(x => x != null);
}
/// <summary>
/// Returns all Media that has been tagged with any tag in the specified group
/// </summary>
/// <param name="tagGroup"></param>
/// <returns></returns>
public IEnumerable<IPublishedContent> GetMediaByTagGroup(string tagGroup)
/// <inheritdoc />
public IEnumerable<IPublishedContent> GetMediaByTagGroup(string group, string culture = null)
{
//TODO: http://issues.umbraco.org/issue/U4-6899
if (_wrappedQuery != null) return _wrappedQuery.GetMediaByTagGroup(tagGroup);
var ids = _tagService.GetTaggedMediaByTagGroup(tagGroup)
var ids = _tagService.GetTaggedMediaByTagGroup(group, culture)
.Select(x => x.EntityId);
return _contentQuery.Media(ids)
.Where(x => x != null);
}
//TODO: Should prob implement these, requires a bit of work on the member service to do this,
// also not sure if its necessary ?
//public IEnumerable<IPublishedContent> GetMembersByTag(string tag, string tagGroup = null)
//{
//}
//public IEnumerable<IPublishedContent> GetMembersByTagGroup(string tagGroup)
//{
//}
/// <summary>
/// Get every tag stored in the database (with optional group)
/// </summary>
public IEnumerable<TagModel> GetAllTags(string group = null)
/// <inheritdoc />
public IEnumerable<TagModel> GetAllTags(string group = null, string culture = null)
{
//TODO: http://issues.umbraco.org/issue/U4-6899
if (_wrappedQuery != null) return _wrappedQuery.GetAllTags(group);
return Mapper.Map<IEnumerable<TagModel>>(_tagService.GetAllTags(group));
return Mapper.Map<IEnumerable<TagModel>>(_tagService.GetAllTags(group, culture));
}
/// <summary>
/// Get all tags for content items (with optional group)
/// </summary>
/// <param name="group"></param>
/// <returns></returns>
public IEnumerable<TagModel> GetAllContentTags(string group = null)
/// <inheritdoc />
public IEnumerable<TagModel> GetAllContentTags(string group = null, string culture = null)
{
//TODO: http://issues.umbraco.org/issue/U4-6899
if (_wrappedQuery != null) return _wrappedQuery.GetAllContentTags(group);
return Mapper.Map<IEnumerable<TagModel>>(_tagService.GetAllContentTags(group));
return Mapper.Map<IEnumerable<TagModel>>(_tagService.GetAllContentTags(group, culture));
}
/// <summary>
/// Get all tags for media items (with optional group)
/// </summary>
/// <param name="group"></param>
/// <returns></returns>
public IEnumerable<TagModel> GetAllMediaTags(string group = null)
/// <inheritdoc />
public IEnumerable<TagModel> GetAllMediaTags(string group = null, string culture = null)
{
//TODO: http://issues.umbraco.org/issue/U4-6899
if (_wrappedQuery != null) return _wrappedQuery.GetAllMediaTags(group);
return Mapper.Map<IEnumerable<TagModel>>(_tagService.GetAllMediaTags(group));
return Mapper.Map<IEnumerable<TagModel>>(_tagService.GetAllMediaTags(group, culture));
}
/// <summary>
/// Get all tags for member items (with optional group)
/// </summary>
/// <param name="group"></param>
/// <returns></returns>
public IEnumerable<TagModel> GetAllMemberTags(string group = null)
/// <inheritdoc />
public IEnumerable<TagModel> GetAllMemberTags(string group = null, string culture = null)
{
//TODO: http://issues.umbraco.org/issue/U4-6899
if (_wrappedQuery != null) return _wrappedQuery.GetAllMemberTags(group);
return Mapper.Map<IEnumerable<TagModel>>(_tagService.GetAllMemberTags(group));
return Mapper.Map<IEnumerable<TagModel>>(_tagService.GetAllMemberTags(group, culture));
}
/// <summary>
/// Returns all tags attached to a property by entity id
/// </summary>
/// <param name="contentId"></param>
/// <param name="propertyTypeAlias"></param>
/// <param name="tagGroup"></param>
/// <returns></returns>
public IEnumerable<TagModel> GetTagsForProperty(int contentId, string propertyTypeAlias, string tagGroup = null)
/// <inheritdoc />
public IEnumerable<TagModel> GetTagsForProperty(int contentId, string propertyTypeAlias, string group = null, string culture = null)
{
//TODO: http://issues.umbraco.org/issue/U4-6899
if (_wrappedQuery != null) return _wrappedQuery.GetTagsForProperty(contentId, propertyTypeAlias, tagGroup);
return Mapper.Map<IEnumerable<TagModel>>(_tagService.GetTagsForProperty(contentId, propertyTypeAlias, tagGroup));
return Mapper.Map<IEnumerable<TagModel>>(_tagService.GetTagsForProperty(contentId, propertyTypeAlias, group, culture));
}
/// <summary>
/// Returns all tags attached to an entity (content, media or member) by entity id
/// </summary>
/// <param name="contentId"></param>
/// <param name="tagGroup"></param>
/// <returns></returns>
public IEnumerable<TagModel> GetTagsForEntity(int contentId, string tagGroup = null)
/// <inheritdoc />
public IEnumerable<TagModel> GetTagsForEntity(int contentId, string group = null, string culture = null)
{
//TODO: http://issues.umbraco.org/issue/U4-6899
if (_wrappedQuery != null) return _wrappedQuery.GetTagsForEntity(contentId, tagGroup);
return Mapper.Map<IEnumerable<TagModel>>(_tagService.GetTagsForEntity(contentId, tagGroup));
return Mapper.Map<IEnumerable<TagModel>>(_tagService.GetTagsForEntity(contentId, group, culture));
}
}
}
-4
View File
@@ -835,9 +835,6 @@
<Compile Include="Security\UmbracoBackOfficeCookieAuthOptions.cs" />
<Compile Include="Scheduling\TaskAndFactoryExtensions.cs" />
<Compile Include="Migrations\ClearCsrfCookiesAfterUpgrade.cs" />
<Compile Include="Migrations\ClearMediaXmlCacheForDeletedItemsAfterUpgrade.cs" />
<Compile Include="Migrations\EnsureListViewDataTypeIsCreated.cs" />
<Compile Include="Migrations\OverwriteStylesheetFilesFromTempFiles.cs" />
<Compile Include="Components\NotificationsComponent.cs" />
<Compile Include="TagQuery.cs" />
<Compile Include="Trees\CoreTreeAttribute.cs" />
@@ -1005,7 +1002,6 @@
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
<Compile Include="UI\JavaScript\ServerVariablesParser.cs" />
<Compile Include="Migrations\RebuildXmlCachesAfterUpgrade.cs" />
<Compile Include="Components\PublicAccessComponent.cs" />
<Compile Include="UI\Bundles\JsJQueryCore.cs" />
<Compile Include="UI\Bundles\JsUmbracoApplicationUI.cs" />
+1 -1
View File
@@ -30,7 +30,7 @@ namespace Umbraco.Web
private readonly ServiceContext _services;
private IUmbracoComponentRenderer _componentRenderer;
private PublishedContentQuery _query;
private IPublishedContentQuery _query;
private MembershipHelper _membershipHelper;
private ITagQuery _tag;
private ICultureDictionary _cultureDictionary;