Merge branch 'temp8-I3089' of https://github.com/umbraco/Umbraco-CMS into temp8-I3089

This commit is contained in:
elitsa
2018-10-08 10:23:53 +02:00
201 changed files with 9580 additions and 4378 deletions
+2 -1
View File
@@ -48,4 +48,5 @@ Umbraco is contribution focused and community driven. If you want to contribute
Another way you can contribute to Umbraco is by providing issue reports. For information on how to submit an issue report refer to our [online guide for reporting issues](CONTRIBUTING_DETAILED.md#reporting-bugs).
To view existing issues, please visit [http://issues.umbraco.org](http://issues.umbraco.org).
You can comment and report issues on the [github issue tracker](https://github.com/umbraco/Umbraco-CMS/issues).
Since [September 2018](https://umbraco.com/blog/a-second-take-on-umbraco-issue-tracker-hello-github-issues/) the old issue tracker is in read only mode, but can still be found at [http://issues.umbraco.org](http://issues.umbraco.org).
+1 -1
View File
@@ -21,6 +21,6 @@ The following items will now be automatically included when creating a deploy pa
system: umbraco, config\splashes and global.asax.
Please read the release notes on our.umbraco.com:
http://our.umbraco.com/contribute/releases
https://our.umbraco.com/download/releases
- Umbraco
+3
View File
@@ -159,6 +159,9 @@ namespace Umbraco.Core.Composing
public static IPublishedValueFallback PublishedValueFallback
=> _publishedValueFallback ?? Container.GetInstance<IPublishedValueFallback>() ?? new NoopPublishedValueFallback();
public static IVariationContextAccessor VariationContextAccessor
=> Container.GetInstance<IVariationContextAccessor>();
#endregion
}
}
@@ -81,6 +81,12 @@ namespace Umbraco.Core.Configuration.UmbracoSettings
[ConfigurationProperty("loginBackgroundImage")]
internal InnerTextConfigurationElement<string> LoginBackgroundImage => GetOptionalTextElement("loginBackgroundImage", string.Empty);
[ConfigurationProperty("StripUdiAttributes")]
internal InnerTextConfigurationElement<bool> StripUdiAttributes
{
get { return GetOptionalTextElement("StripUdiAttributes", true); }
}
string IContentSection.NotificationEmailAddress => Notifications.NotificationEmailAddress;
@@ -136,6 +142,8 @@ namespace Umbraco.Core.Configuration.UmbracoSettings
bool IContentSection.EnableInheritedMediaTypes => EnableInheritedMediaTypes;
bool IContentSection.StripUdiAttributes => StripUdiAttributes;
string IContentSection.LoginBackgroundImage => LoginBackgroundImage;
}
}
@@ -14,7 +14,7 @@ namespace Umbraco.Core.Configuration.UmbracoSettings
IEnumerable<string> ImageTagAllowedAttributes { get; }
IEnumerable<IImagingAutoFillUploadField> ImageAutoFillProperties { get; }
string ScriptFolderPath { get; }
IEnumerable<string> ScriptFileTypes { get; }
@@ -66,5 +66,7 @@ namespace Umbraco.Core.Configuration.UmbracoSettings
bool EnableInheritedMediaTypes { get; }
string LoginBackgroundImage { get; }
bool StripUdiAttributes { get; }
}
}
@@ -91,6 +91,11 @@ namespace Umbraco.Core
/// Property alias for the Media's file extension.
/// </summary>
public const string Extension = "umbracoExtension";
/// <summary>
/// The default height/width of an image file if the size can't be determined from the metadata
/// </summary>
public const int DefaultSize = 200;
}
/// <summary>
+1
View File
@@ -58,6 +58,7 @@
/// </remarks>
public const string RecycleBinMediaPathPrefix = "-1,-21,";
public const int DefaultLabelDataTypeId = -92;
public const string UmbracoConnectionName = "umbracoDbDSN";
public const string UmbracoUpgradePlanName = "Umbraco.Core";
}
@@ -307,7 +307,7 @@ namespace Umbraco.Core.Events
// fixme see notes above
// delete event args does NOT superceedes 'unpublished' event
if (argType.IsGenericType && argType.GetGenericTypeDefinition() == typeof(PublishEventArgs<>) && infos.EventDefinition.EventName == "UnPublished")
if (argType.IsGenericType && argType.GetGenericTypeDefinition() == typeof(PublishEventArgs<>) && infos.EventDefinition.EventName == "Unpublished")
return false;
// found occurences, need to determine if this event args is superceded
@@ -230,7 +230,7 @@ namespace Umbraco.Core.Migrations.Install
private void CreateLanguageData()
{
_database.Insert(Constants.DatabaseSchema.Tables.Language, "id", false, new LanguageDto { Id = 1, IsoCode = "en-US", CultureName = "English (United States)", IsDefaultVariantLanguage = true });
_database.Insert(Constants.DatabaseSchema.Tables.Language, "id", false, new LanguageDto { Id = 1, IsoCode = "en-US", CultureName = "English (United States)", IsDefault = true });
}
private void CreateContentChildTypeData()
@@ -107,18 +107,35 @@ namespace Umbraco.Core.Migrations.Upgrade
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 it after shannon's, with a new target state,
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
// 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}");
Chain<RefactorVariantsModel>("{B19BF0F2-E1C6-4AEB-A146-BC559D97A2C6}");
//Chain<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
// 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
// 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}");
// resume at {290C18EE-B3DE-4769-84F1-1F467F3F76DA}...
//FINAL
@@ -0,0 +1,24 @@
using System.Linq;
using Umbraco.Core.Persistence.Dtos;
namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0
{
/// <summary>
/// Adds a new, self-joined field to umbracoLanguages to hold the fall-back language for
/// a given language.
/// </summary>
public class FallbackLanguage : MigrationBase
{
public FallbackLanguage(IMigrationContext context)
: base(context)
{ }
public override void Migrate()
{
var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToArray();
if (columns.Any(x => x.TableName.InvariantEquals(Constants.DatabaseSchema.Tables.Language) && x.ColumnName.InvariantEquals("fallbackLanguageId")) == false)
AddColumn<LanguageDto>("fallbackLanguageId");
}
}
}
@@ -26,7 +26,7 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0
var defaultId = int.MaxValue;
foreach (var dto in dtos)
{
if (dto.IsDefaultVariantLanguage)
if (dto.IsDefault)
{
defaultId = dto.Id;
break;
@@ -38,8 +38,8 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0
// update, so that language with that id is now default and mandatory
var updateDefault = Sql()
.Update<LanguageDto>(u => u
.Set(x => x.IsDefaultVariantLanguage, true)
.Set(x => x.Mandatory, true))
.Set(x => x.IsDefault, true)
.Set(x => x.IsMandatory, true))
.Where<LanguageDto>(x => x.Id == defaultId);
Database.Execute(updateDefault);
+1 -1
View File
@@ -32,7 +32,7 @@
/// <summary>
/// Used when nodes are unpublished
/// </summary>
UnPublish,
Unpublish,
/// <summary>
/// Used when nodes are moved
/// </summary>
+27 -7
View File
@@ -4,34 +4,54 @@ using Umbraco.Core.Models.Entities;
namespace Umbraco.Core.Models
{
/// <summary>
/// Represents a language.
/// </summary>
public interface ILanguage : IEntity, IRememberBeingDirty
{
/// <summary>
/// Gets or sets the Iso Code for the Language
/// Gets or sets the ISO code of the language.
/// </summary>
[DataMember]
string IsoCode { get; set; }
/// <summary>
/// Gets or sets the Culture Name for the Language
/// Gets or sets the culture name of the language.
/// </summary>
[DataMember]
string CultureName { get; set; }
/// <summary>
/// Returns a <see cref="CultureInfo"/> object for the current Language
/// Gets the <see cref="CultureInfo"/> object for the language.
/// </summary>
[IgnoreDataMember]
CultureInfo CultureInfo { get; }
/// <summary>
/// Defines if this language is the default variant language when language variants are in use
/// Gets or sets a value indicating whether the language is the default language.
/// </summary>
bool IsDefaultVariantLanguage { get; set; }
[DataMember]
bool IsDefault { get; set; }
/// <summary>
/// If true, a variant node cannot be published unless this language variant is created
/// Gets or sets a value indicating whether the language is mandatory.
/// </summary>
bool Mandatory { get; set; }
/// <remarks>
/// <para>When a language is mandatory, a multi-lingual document cannot be published
/// without that language being published, and unpublishing that language unpublishes
/// the entire document.</para>
/// </remarks>
[DataMember]
bool IsMandatory { get; set; }
/// <summary>
/// Gets or sets the identifier of a fallback language.
/// </summary>
/// <remarks>
/// <para>The fallback language can be used in multi-lingual scenarios, to help
/// define fallback strategies when a value does not exist for a requested language.</para>
/// </remarks>
[DataMember]
int? FallbackLanguageId { get; set; }
}
}
+18 -13
View File
@@ -19,6 +19,7 @@ namespace Umbraco.Core.Models
private string _cultureName;
private bool _isDefaultVariantLanguage;
private bool _mandatory;
private int? _fallbackLanguageId;
public Language(string isoCode)
{
@@ -30,13 +31,12 @@ namespace Umbraco.Core.Models
{
public readonly PropertyInfo IsoCodeSelector = ExpressionHelper.GetPropertyInfo<Language, string>(x => x.IsoCode);
public readonly PropertyInfo CultureNameSelector = ExpressionHelper.GetPropertyInfo<Language, string>(x => x.CultureName);
public readonly PropertyInfo IsDefaultVariantLanguageSelector = ExpressionHelper.GetPropertyInfo<Language, bool>(x => x.IsDefaultVariantLanguage);
public readonly PropertyInfo MandatorySelector = ExpressionHelper.GetPropertyInfo<Language, bool>(x => x.Mandatory);
public readonly PropertyInfo IsDefaultVariantLanguageSelector = ExpressionHelper.GetPropertyInfo<Language, bool>(x => x.IsDefault);
public readonly PropertyInfo MandatorySelector = ExpressionHelper.GetPropertyInfo<Language, bool>(x => x.IsMandatory);
public readonly PropertyInfo FallbackLanguageSelector = ExpressionHelper.GetPropertyInfo<Language, int?>(x => x.FallbackLanguageId);
}
/// <summary>
/// Gets or sets the Iso Code for the Language
/// </summary>
/// <inheritdoc />
[DataMember]
public string IsoCode
{
@@ -44,9 +44,7 @@ namespace Umbraco.Core.Models
set => SetPropertyValueAndDetectChanges(value, ref _isoCode, Ps.Value.IsoCodeSelector);
}
/// <summary>
/// Gets or sets the Culture Name for the Language
/// </summary>
/// <inheritdoc />
[DataMember]
public string CultureName
{
@@ -54,22 +52,29 @@ namespace Umbraco.Core.Models
set => SetPropertyValueAndDetectChanges(value, ref _cultureName, Ps.Value.CultureNameSelector);
}
/// <summary>
/// Returns a <see cref="CultureInfo"/> object for the current Language
/// </summary>
/// <inheritdoc />
[IgnoreDataMember]
public CultureInfo CultureInfo => CultureInfo.GetCultureInfo(IsoCode);
public bool IsDefaultVariantLanguage
/// <inheritdoc />
public bool IsDefault
{
get => _isDefaultVariantLanguage;
set => SetPropertyValueAndDetectChanges(value, ref _isDefaultVariantLanguage, Ps.Value.IsDefaultVariantLanguageSelector);
}
public bool Mandatory
/// <inheritdoc />
public bool IsMandatory
{
get => _mandatory;
set => SetPropertyValueAndDetectChanges(value, ref _mandatory, Ps.Value.MandatorySelector);
}
/// <inheritdoc />
public int? FallbackLanguageId
{
get => _fallbackLanguageId;
set => SetPropertyValueAndDetectChanges(value, ref _fallbackLanguageId, Ps.Value.FallbackLanguageSelector);
}
}
}
@@ -1,22 +0,0 @@
using System;
using System.Collections.Generic;
namespace Umbraco.Core.Models
{
/// <summary>
/// A set of tag changes.
/// </summary>
internal class PropertyTagChange
{
public ChangeType Type { get; set; }
public IEnumerable<(string Type, string Tags)> Tags { get; set; }
public enum ChangeType
{
Replace,
Remove,
Merge
}
}
}
@@ -0,0 +1,75 @@
using System;
using System.Collections;
using System.Collections.Generic;
namespace Umbraco.Core.Models.PublishedContent
{
/// <summary>
/// Manages the built-in fallback policies.
/// </summary>
public struct Fallback : IEnumerable<int>
{
private readonly int[] _values;
/// <summary>
/// Initializes a new instance of the <see cref="Fallback"/> struct with values.
/// </summary>
private Fallback(int[] values)
{
_values = values;
}
/// <summary>
/// Gets an ordered set of fallback policies.
/// </summary>
/// <param name="values"></param>
public static Fallback To(params int[] values) => new Fallback(values);
/// <summary>
/// Do not fallback.
/// </summary>
public const int None = 0;
/// <summary>
/// Fallback to default value.
/// </summary>
public const int DefaultValue = 1;
/// <summary>
/// Gets the fallback to default value policy.
/// </summary>
public static Fallback ToDefaultValue => new Fallback(new[] { DefaultValue });
/// <summary>
/// Fallback to other languages.
/// </summary>
public const int Language = 2;
/// <summary>
/// Gets the fallback to language policy.
/// </summary>
public static Fallback ToLanguage => new Fallback(new[] { Language });
/// <summary>
/// Fallback to tree ancestors.
/// </summary>
public const int Ancestors = 3;
/// <summary>
/// Gets the fallback to tree ancestors policy.
/// </summary>
public static Fallback ToAncestors => new Fallback(new[] { Ancestors });
/// <inheritdoc />
public IEnumerator<int> GetEnumerator()
{
return ((IEnumerable<int>)_values ?? Array.Empty<int>()).GetEnumerator();
}
/// <inheritdoc />
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
}
@@ -1,37 +1,122 @@
using Umbraco.Core.Composing;
namespace Umbraco.Core.Models.PublishedContent
namespace Umbraco.Core.Models.PublishedContent
{
/// <summary>
/// Provides a fallback strategy for getting <see cref="IPublishedElement"/> values.
/// </summary>
// fixme - IPublishedValueFallback is still WorkInProgress
// todo - properly document methods, etc
// todo - understand caching vs fallback (recurse etc)
public interface IPublishedValueFallback
{
// note that at property level, property.GetValue() does NOT implement fallback, and one has
// to get property.Value() or property.Value<T>() to trigger fallback
/// <summary>
/// Tries to get a fallback value for a property.
/// </summary>
/// <param name="property">The property.</param>
/// <param name="culture">The requested culture.</param>
/// <param name="segment">The requested segment.</param>
/// <param name="fallback">A fallback strategy.</param>
/// <param name="defaultValue">An optional default value.</param>
/// <param name="value">The fallback value.</param>
/// <returns>A value indicating whether a fallback value could be provided.</returns>
/// <remarks>
/// <para>This method is called whenever property.Value(culture, segment, defaultValue) is called, and
/// property.HasValue(culture, segment) is false.</para>
/// <para>It can only fallback at property level (no recurse).</para>
/// <para>At property level, property.GetValue() does *not* implement fallback, and one has to
/// get property.Value() or property.Value{T}() to trigger fallback.</para>
/// <para>Note that <paramref name="culture"/> and <paramref name="segment"/> may not be contextualized,
/// so the variant context should be used to contextualize them (see our default implementation in
/// the web project.</para>
/// </remarks>
bool TryGetValue(IPublishedProperty property, string culture, string segment, Fallback fallback, object defaultValue, out object value);
// this method is called whenever property.Value(culture, segment, defaultValue) is called, and
// property.HasValue(culture, segment) is false. it can only fallback at property level (no recurse).
/// <summary>
/// Tries to get a fallback value for a property.
/// </summary>
/// <typeparam name="T">The type of the value.</typeparam>
/// <param name="property">The property.</param>
/// <param name="culture">The requested culture.</param>
/// <param name="segment">The requested segment.</param>
/// <param name="fallback">A fallback strategy.</param>
/// <param name="defaultValue">An optional default value.</param>
/// <param name="value">The fallback value.</param>
/// <returns>A value indicating whether a fallback value could be provided.</returns>
/// <remarks>
/// <para>This method is called whenever property.Value{T}(culture, segment, defaultValue) is called, and
/// property.HasValue(culture, segment) is false.</para>
/// <para>It can only fallback at property level (no recurse).</para>
/// <para>At property level, property.GetValue() does *not* implement fallback, and one has to
/// get property.Value() or property.Value{T}() to trigger fallback.</para>
/// </remarks>
bool TryGetValue<T>(IPublishedProperty property, string culture, string segment, Fallback fallback, T defaultValue, out T value);
object GetValue(IPublishedProperty property, string culture, string segment, object defaultValue);
/// <summary>
/// Tries to get a fallback value for a published element property.
/// </summary>
/// <param name="content">The published element.</param>
/// <param name="alias">The property alias.</param>
/// <param name="culture">The requested culture.</param>
/// <param name="segment">The requested segment.</param>
/// <param name="fallback">A fallback strategy.</param>
/// <param name="defaultValue">An optional default value.</param>
/// <param name="value">The fallback value.</param>
/// <returns>A value indicating whether a fallback value could be provided.</returns>
/// <remarks>
/// <para>This method is called whenever getting the property value for the specified alias, culture and
/// segment, either returned no property at all, or a property with HasValue(culture, segment) being false.</para>
/// <para>It can only fallback at element level (no recurse).</para>
/// </remarks>
bool TryGetValue(IPublishedElement content, string alias, string culture, string segment, Fallback fallback, object defaultValue, out object value);
// this method is called whenever property.Value<T>(culture, segment, defaultValue) is called, and
// property.HasValue(culture, segment) is false. it can only fallback at property level (no recurse).
/// <summary>
/// Tries to get a fallback value for a published element property.
/// </summary>
/// <typeparam name="T">The type of the value.</typeparam>
/// <param name="content">The published element.</param>
/// <param name="alias">The property alias.</param>
/// <param name="culture">The requested culture.</param>
/// <param name="segment">The requested segment.</param>
/// <param name="fallback">A fallback strategy.</param>
/// <param name="defaultValue">An optional default value.</param>
/// <param name="value">The fallback value.</param>
/// <returns>A value indicating whether a fallback value could be provided.</returns>
/// <remarks>
/// <para>This method is called whenever getting the property value for the specified alias, culture and
/// segment, either returned no property at all, or a property with HasValue(culture, segment) being false.</para>
/// <para>It can only fallback at element level (no recurse).</para>
/// </remarks>
bool TryGetValue<T>(IPublishedElement content, string alias, string culture, string segment, Fallback fallback, T defaultValue, out T value);
T GetValue<T>(IPublishedProperty property, string culture, string segment, T defaultValue);
/// <summary>
/// Tries to get a fallback value for a published content property.
/// </summary>
/// <param name="content">The published element.</param>
/// <param name="alias">The property alias.</param>
/// <param name="culture">The requested culture.</param>
/// <param name="segment">The requested segment.</param>
/// <param name="fallback">A fallback strategy.</param>
/// <param name="defaultValue">An optional default value.</param>
/// <param name="value">The fallback value.</param>
/// <returns>A value indicating whether a fallback value could be provided.</returns>
/// <remarks>
/// <para>This method is called whenever getting the property value for the specified alias, culture and
/// segment, either returned no property at all, or a property with HasValue(culture, segment) being false.</para>
/// </remarks>
bool TryGetValue(IPublishedContent content, string alias, string culture, string segment, Fallback fallback, object defaultValue, out object value);
// these methods to be called whenever getting the property value for the specified alias, culture and segment,
// either returned no property at all, or a property that does not HasValue for the specified culture and segment.
object GetValue(IPublishedElement content, string alias, string culture, string segment, object defaultValue);
T GetValue<T>(IPublishedElement content, string alias, string culture, string segment, T defaultValue);
object GetValue(IPublishedContent content, string alias, string culture, string segment, object defaultValue, bool recurse);
T GetValue<T>(IPublishedContent content, string alias, string culture, string segment, T defaultValue, bool recurse);
/// <summary>
/// Tries to get a fallback value for a published content property.
/// </summary>
/// <typeparam name="T">The type of the value.</typeparam>
/// <param name="content">The published element.</param>
/// <param name="alias">The property alias.</param>
/// <param name="culture">The requested culture.</param>
/// <param name="segment">The requested segment.</param>
/// <param name="fallback">A fallback strategy.</param>
/// <param name="defaultValue">An optional default value.</param>
/// <param name="value">The fallback value.</param>
/// <returns>A value indicating whether a fallback value could be provided.</returns>
/// <remarks>
/// <para>This method is called whenever getting the property value for the specified alias, culture and
/// segment, either returned no property at all, or a property with HasValue(culture, segment) being false.</para>
/// </remarks>
bool TryGetValue<T>(IPublishedContent content, string alias, string culture, string segment, Fallback fallback, T defaultValue, out T value);
}
}
@@ -49,17 +49,24 @@ namespace Umbraco.Core.Models.PublishedContent
/// <param name="modelTypes">The model types map.</param>
/// <returns>The actual Clr type.</returns>
public static Type Map(Type type, Dictionary<string, Type> modelTypes)
=> Map(type, modelTypes, false);
public static Type Map(Type type, Dictionary<string, Type> modelTypes, bool dictionaryIsInvariant)
{
// it may be that senders forgot to send an invariant dictionary (garbage-in)
if (!dictionaryIsInvariant)
modelTypes = new Dictionary<string, Type>(modelTypes, StringComparer.InvariantCultureIgnoreCase);
if (type is ModelType modelType)
{
if (modelTypes.TryGetValue(modelType.ContentTypeAlias, out Type actualType))
if (modelTypes.TryGetValue(modelType.ContentTypeAlias, out var actualType))
return actualType;
throw new InvalidOperationException($"Don't know how to map ModelType with content type alias \"{modelType.ContentTypeAlias}\".");
}
if (type is ModelTypeArrayType arrayType)
{
if (modelTypes.TryGetValue(arrayType.ContentTypeAlias, out Type actualType))
if (modelTypes.TryGetValue(arrayType.ContentTypeAlias, out var actualType))
return actualType.MakeArrayType();
throw new InvalidOperationException($"Don't know how to map ModelType with content type alias \"{arrayType.ContentTypeAlias}\".");
}
@@ -70,7 +77,7 @@ namespace Umbraco.Core.Models.PublishedContent
if (def == null)
throw new InvalidOperationException("panic");
var args = type.GetGenericArguments().Select(x => Map(x, modelTypes)).ToArray();
var args = type.GetGenericArguments().Select(x => Map(x, modelTypes, true)).ToArray();
return def.MakeGenericType(args);
}
@@ -81,7 +88,14 @@ namespace Umbraco.Core.Models.PublishedContent
/// <param name="map">The model types map.</param>
/// <returns>The actual Clr type name.</returns>
public static string MapToName(Type type, Dictionary<string, string> map)
=> MapToName(type, map, false);
private static string MapToName(Type type, Dictionary<string, string> map, bool dictionaryIsInvariant)
{
// it may be that senders forgot to send an invariant dictionary (garbage-in)
if (!dictionaryIsInvariant)
map = new Dictionary<string, string>(map, StringComparer.InvariantCultureIgnoreCase);
if (type is ModelType modelType)
{
if (map.TryGetValue(modelType.ContentTypeAlias, out var actualTypeName))
@@ -102,7 +116,7 @@ namespace Umbraco.Core.Models.PublishedContent
if (def == null)
throw new InvalidOperationException("panic");
var args = type.GetGenericArguments().Select(x => MapToName(x, map)).ToArray();
var args = type.GetGenericArguments().Select(x => MapToName(x, map, true)).ToArray();
var defFullName = def.FullName.Substring(0, def.FullName.IndexOf('`'));
return defFullName + "<" + string.Join(", ", args) + ">";
}
@@ -9,21 +9,45 @@
public class NoopPublishedValueFallback : IPublishedValueFallback
{
/// <inheritdoc />
public object GetValue(IPublishedProperty property, string culture, string segment, object defaultValue) => defaultValue;
public bool TryGetValue(IPublishedProperty property, string culture, string segment, Fallback fallback, object defaultValue, out object value)
{
value = default;
return false;
}
/// <inheritdoc />
public T GetValue<T>(IPublishedProperty property, string culture, string segment, T defaultValue) => defaultValue;
public bool TryGetValue<T>(IPublishedProperty property, string culture, string segment, Fallback fallback, T defaultValue, out T value)
{
value = default;
return false;
}
/// <inheritdoc />
public object GetValue(IPublishedElement content, string alias, string culture, string segment, object defaultValue) => defaultValue;
public bool TryGetValue(IPublishedElement content, string alias, string culture, string segment, Fallback fallback, object defaultValue, out object value)
{
value = default;
return false;
}
/// <inheritdoc />
public T GetValue<T>(IPublishedElement content, string alias, string culture, string segment, T defaultValue) => defaultValue;
public bool TryGetValue<T>(IPublishedElement content, string alias, string culture, string segment, Fallback fallback, T defaultValue, out T value)
{
value = default;
return false;
}
/// <inheritdoc />
public object GetValue(IPublishedContent content, string alias, string culture, string segment, object defaultValue, bool recurse) => defaultValue;
public bool TryGetValue(IPublishedContent content, string alias, string culture, string segment, Fallback fallback, object defaultValue, out object value)
{
value = default;
return false;
}
/// <inheritdoc />
public T GetValue<T>(IPublishedContent content, string alias, string culture, string segment, T defaultValue, bool recurse) => defaultValue;
public bool TryGetValue<T>(IPublishedContent content, string alias, string culture, string segment, Fallback fallback, T defaultValue, out T value)
{
value = default;
return false;
}
}
}
}
@@ -0,0 +1,15 @@
namespace Umbraco.Core.Models.PublishedContent
{
public static class VariationContextAccessorExtensions
{
public static void ContextualizeVariation(this IVariationContextAccessor variationContextAccessor, ContentVariation variations, ref string culture, ref string segment)
{
if (culture != null && segment != null) return;
// use context values
var publishedVariationContext = variationContextAccessor?.VariationContext;
if (culture == null) culture = variations.VariesByCulture() ? publishedVariationContext?.Culture : "";
if (segment == null) segment = variations.VariesBySegment() ? publishedVariationContext?.Segment : "";
}
}
}
@@ -10,33 +10,51 @@ namespace Umbraco.Core.Persistence.Dtos
{
public const string TableName = Constants.DatabaseSchema.Tables.Language;
/// <summary>
/// Gets or sets the identifier of the language.
/// </summary>
[Column("id")]
[PrimaryKeyColumn(IdentitySeed = 2)]
public short Id { get; set; }
/// <summary>
/// Gets or sets the ISO code of the language.
/// </summary>
[Column("languageISOCode")]
[Index(IndexTypes.UniqueNonClustered)]
[NullSetting(NullSetting = NullSettings.Null)]
[Length(14)]
public string IsoCode { get; set; }
/// <summary>
/// Gets or sets the culture name of the language.
/// </summary>
[Column("languageCultureName")]
[NullSetting(NullSetting = NullSettings.Null)]
[Length(100)]
public string CultureName { get; set; }
/// <summary>
/// Defines if this language is the default variant language when language variants are in use
/// Gets or sets a value indicating whether the language is the default language.
/// </summary>
[Column("isDefaultVariantLang")]
[Constraint(Default = "0")]
public bool IsDefaultVariantLanguage { get; set; }
public bool IsDefault { get; set; }
/// <summary>
/// If true, a variant node cannot be published unless this language variant is created
/// Gets or sets a value indicating whether the language is mandatory.
/// </summary>
[Column("mandatory")]
[Constraint(Default = "0")]
public bool Mandatory { get; set; }
public bool IsMandatory { get; set; }
/// <summary>
/// Gets or sets the identifier of a fallback language.
/// </summary>
[Column("fallbackLanguageId")]
[ForeignKey(typeof(LanguageDto), Column = "id")]
[Index(IndexTypes.NonClustered)]
[NullSetting(NullSetting = NullSettings.Null)]
public int? FallbackLanguageId { get; set; }
}
}
@@ -12,8 +12,9 @@ namespace Umbraco.Core.Persistence.Factories
{
CultureName = dto.CultureName,
Id = dto.Id,
IsDefaultVariantLanguage = dto.IsDefaultVariantLanguage,
Mandatory = dto.Mandatory
IsDefault = dto.IsDefault,
IsMandatory = dto.IsMandatory,
FallbackLanguageId = dto.FallbackLanguageId
};
// reset dirty initial properties (U4-1946)
@@ -27,12 +28,15 @@ namespace Umbraco.Core.Persistence.Factories
{
CultureName = entity.CultureName,
IsoCode = entity.IsoCode,
IsDefaultVariantLanguage = entity.IsDefaultVariantLanguage,
Mandatory = entity.Mandatory
IsDefault = entity.IsDefault,
IsMandatory = entity.IsMandatory,
FallbackLanguageId = entity.FallbackLanguageId
};
if (entity.HasIdentity)
{
dto.Id = short.Parse(entity.Id.ToString(CultureInfo.InvariantCulture));
}
return dto;
}
@@ -74,7 +74,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
var sqlClause = GetBaseQuery(false);
var translator = new SqlTranslator<ILanguage>(sqlClause, query);
var sql = translator.Translate();
return Database.Fetch<LanguageDto>(sql).Select(ConvertFromDto);
var dtos = Database.Fetch<LanguageDto>(sql);
return dtos.Select(ConvertFromDto).ToList();
}
#endregion
@@ -115,10 +116,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
return list;
}
protected override Guid NodeObjectTypeId
{
get { throw new NotImplementedException(); }
}
protected override Guid NodeObjectTypeId => throw new NotImplementedException();
#endregion
@@ -133,15 +131,17 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
((EntityBase) entity).AddingEntity();
// deal with entity becoming the new default entity
if (entity.IsDefaultVariantLanguage)
if (entity.IsDefault)
{
// set all other entities to non-default
// safe (no race cond) because the service locks languages
var setAllDefaultToFalse = Sql()
.Update<LanguageDto>(u => u.Set(x => x.IsDefaultVariantLanguage, false));
.Update<LanguageDto>(u => u.Set(x => x.IsDefault, false));
Database.Execute(setAllDefaultToFalse);
}
;
// fallback cycles are detected at service level
// insert
var dto = LanguageFactory.BuildDto(entity);
var id = Convert.ToInt32(Database.Insert(dto));
@@ -157,14 +157,14 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
((EntityBase) entity).UpdatingEntity();
if (entity.IsDefaultVariantLanguage)
if (entity.IsDefault)
{
// deal with entity becoming the new default entity
// set all other entities to non-default
// safe (no race cond) because the service locks languages
var setAllDefaultToFalse = Sql()
.Update<LanguageDto>(u => u.Set(x => x.IsDefaultVariantLanguage, false));
.Update<LanguageDto>(u => u.Set(x => x.IsDefault, false));
Database.Execute(setAllDefaultToFalse);
}
else
@@ -174,13 +174,15 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
var selectDefaultId = Sql()
.Select<LanguageDto>(x => x.Id)
.From<LanguageDto>()
.Where<LanguageDto>(x => x.IsDefaultVariantLanguage);
.Where<LanguageDto>(x => x.IsDefault);
var defaultId = Database.ExecuteScalar<int>(selectDefaultId);
if (entity.Id == defaultId)
throw new InvalidOperationException($"Cannot save the default language ({entity.IsoCode}) as non-default. Make another language the default language instead.");
}
// fallback cycles are detected at service level
// update
var dto = LanguageFactory.BuildDto(entity);
Database.Update(dto);
@@ -195,12 +197,20 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
var selectDefaultId = Sql()
.Select<LanguageDto>(x => x.Id)
.From<LanguageDto>()
.Where<LanguageDto>(x => x.IsDefaultVariantLanguage);
.Where<LanguageDto>(x => x.IsDefault);
var defaultId = Database.ExecuteScalar<int>(selectDefaultId);
if (entity.Id == defaultId)
throw new InvalidOperationException($"Cannot delete the default language ({entity.IsoCode}).");
// We need to remove any references to the language if it's being used as a fall-back from other ones
var clearFallbackLanguage = Sql()
.Update<LanguageDto>(u => u
.Set(x => x.FallbackLanguageId, null))
.Where<LanguageDto>(x => x.FallbackLanguageId == entity.Id);
Database.Execute(clearFallbackLanguage);
// delete
base.PersistDeletedItem(entity);
}
@@ -212,7 +222,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
var entity = LanguageFactory.BuildEntity(dto);
return entity;
}
public ILanguage GetByIsoCode(string isoCode)
{
TypedCachePolicy.GetAllCached(PerformGetAll); // ensure cache is populated, in a non-expensive way
@@ -271,7 +281,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
{
// get all cached, non-cloned
var languages = TypedCachePolicy.GetAllCached(PerformGetAll).ToList();
var language = languages.FirstOrDefault(x => x.IsDefaultVariantLanguage);
var language = languages.FirstOrDefault(x => x.IsDefault);
if (language != null) return language;
// this is an anomaly, the service/repo should ensure it cannot happen
@@ -748,7 +748,7 @@ ORDER BY colName";
if (excludeUserGroups != null && excludeUserGroups.Length > 0)
{
var subQuery = @"AND (umbracoUser.id NOT IN (SELECT DISTINCT umbracoUser.id
const string subQuery = @"AND (umbracoUser.id NOT IN (SELECT DISTINCT umbracoUser.id
FROM umbracoUser
INNER JOIN umbracoUser2UserGroup ON umbracoUser2UserGroup.userId = umbracoUser.id
INNER JOIN umbracoUserGroup ON umbracoUserGroup.id = umbracoUser2UserGroup.userGroupId
@@ -809,7 +809,7 @@ ORDER BY colName";
sql = new SqlTranslator<IUser>(sql, query).Translate();
// get sorted and filtered sql
var sqlNodeIdsWithSort = ApplySort(ApplyFilter(sql, filterSql), orderDirection, orderBy);
var sqlNodeIdsWithSort = ApplySort(ApplyFilter(sql, filterSql, query != null), orderDirection, orderBy);
// get a page of results and total count
var pagedResult = Database.Page<UserDto>(pageIndex + 1, pageSize, sqlNodeIdsWithSort);
@@ -820,11 +820,17 @@ ORDER BY colName";
return pagedResult.Items.Select(UserFactory.BuildEntity);
}
private Sql<ISqlContext> ApplyFilter(Sql<ISqlContext> sql, Sql<ISqlContext> filterSql)
private Sql<ISqlContext> ApplyFilter(Sql<ISqlContext> sql, Sql<ISqlContext> filterSql, bool hasWhereClause)
{
if (filterSql == null) return sql;
sql.Append(SqlContext.Sql(" WHERE " + filterSql.SQL.TrimStart("AND "), filterSql.Arguments));
//ensure we don't append a WHERE if there is already one
var args = filterSql.Arguments;
var sqlFilter = hasWhereClause
? filterSql.SQL
: " WHERE " + filterSql.SQL.TrimStart("AND ");
sql.Append(SqlContext.Sql(sqlFilter, args));
return sql;
}
@@ -1,7 +1,9 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.Globalization;
using System.Linq;
using System.Security.Claims;
using System.Security.Principal;
@@ -63,5 +65,6 @@ namespace Umbraco.Core.Security
/// Used so that we aren't creating a new CultureInfo object for every single request
/// </summary>
private static readonly ConcurrentDictionary<string, CultureInfo> UserCultures = new ConcurrentDictionary<string, CultureInfo>();
}
}
@@ -635,7 +635,9 @@ namespace Umbraco.Core.Security
|| identityUser.LastLoginDateUtc.HasValue && user.LastLoginDate.ToUniversalTime() != identityUser.LastLoginDateUtc.Value)
{
anythingChanged = true;
user.LastLoginDate = identityUser.LastLoginDateUtc.Value.ToLocalTime();
//if the LastLoginDate is being set to MinValue, don't convert it ToLocalTime
var dt = identityUser.LastLoginDateUtc == DateTime.MinValue ? DateTime.MinValue : identityUser.LastLoginDateUtc.Value.ToLocalTime();
user.LastLoginDate = dt;
}
if (identityUser.IsPropertyDirty("LastPasswordChangeDateUtc")
|| (user.LastPasswordChangeDate != default(DateTime) && identityUser.LastPasswordChangeDateUtc.HasValue == false)
@@ -988,14 +988,14 @@ namespace Umbraco.Core.Services.Implement
UnpublishResultType result;
if (culture == "*" || culture == null)
{
Audit(AuditType.UnPublish, "Unpublished by user", userId, content.Id);
Audit(AuditType.Unpublish, "Unpublished by user", userId, content.Id);
result = UnpublishResultType.Success;
}
else
{
Audit(AuditType.UnPublish, $"Culture \"{culture}\" unpublished by user", userId, content.Id);
Audit(AuditType.Unpublish, $"Culture \"{culture}\" unpublished by user", userId, content.Id);
if (!content.Published)
Audit(AuditType.UnPublish, $"Unpublished (culture \"{culture}\" is mandatory) by user", userId, content.Id);
Audit(AuditType.Unpublish, $"Unpublished (culture \"{culture}\" is mandatory) by user", userId, content.Id);
result = content.Published ? UnpublishResultType.SuccessCulture : UnpublishResultType.SuccessMandatoryCulture;
}
scope.Complete();
@@ -1034,7 +1034,7 @@ namespace Umbraco.Core.Services.Implement
var cannotBePublished = publishedCultures.Count == 0; // no published cultures = cannot be published
if (!cannotBePublished)
{
var mandatoryCultures = _languageRepository.GetMany().Where(x => x.Mandatory).Select(x => x.IsoCode);
var mandatoryCultures = _languageRepository.GetMany().Where(x => x.IsMandatory).Select(x => x.IsoCode);
cannotBePublished = mandatoryCultures.Any(x => !publishedCultures.Contains(x, StringComparer.OrdinalIgnoreCase)); // missing mandatory culture = cannot be published
}
@@ -1120,9 +1120,9 @@ namespace Umbraco.Core.Services.Implement
if (unpublishResult.Success) // and succeeded, trigger events
{
// events and audit
scope.Events.Dispatch(UnPublished, this, new PublishEventArgs<IContent>(content, false, false), "UnPublished");
scope.Events.Dispatch(Unpublished, this, new PublishEventArgs<IContent>(content, false, false), "Unpublished");
scope.Events.Dispatch(TreeChanged, this, new TreeChange<IContent>(content, TreeChangeTypes.RefreshBranch).ToEventArgs());
Audit(AuditType.UnPublish, "Unpublished by user", userId, content.Id);
Audit(AuditType.Unpublish, "Unpublished by user", userId, content.Id);
scope.Complete();
return new PublishResult(PublishResultType.Success, evtMsgs, content);
}
@@ -1348,10 +1348,10 @@ namespace Umbraco.Core.Services.Implement
scope.WriteLock(Constants.Locks.ContentTree);
// if it's not trashed yet, and published, we should unpublish
// but... UnPublishing event makes no sense (not going to cancel?) and no need to save
// but... Unpublishing event makes no sense (not going to cancel?) and no need to save
// just raise the event
if (content.Trashed == false && content.Published)
scope.Events.Dispatch(UnPublished, this, new PublishEventArgs<IContent>(content, false, false), nameof(UnPublished));
scope.Events.Dispatch(Unpublished, this, new PublishEventArgs<IContent>(content, false, false), nameof(Unpublished));
DeleteLocked(scope, content);
@@ -2098,12 +2098,12 @@ namespace Umbraco.Core.Services.Implement
/// <summary>
/// Occurs before unpublish
/// </summary>
public static event TypedEventHandler<IContentService, PublishEventArgs<IContent>> UnPublishing;
public static event TypedEventHandler<IContentService, PublishEventArgs<IContent>> Unpublishing;
/// <summary>
/// Occurs after unpublish
/// </summary>
public static event TypedEventHandler<IContentService, PublishEventArgs<IContent>> UnPublished;
public static event TypedEventHandler<IContentService, PublishEventArgs<IContent>> Unpublished;
/// <summary>
/// Occurs after change.
@@ -2197,8 +2197,8 @@ namespace Umbraco.Core.Services.Implement
// ensures that a document can be unpublished
internal UnpublishResult StrategyCanUnpublish(IScope scope, IContent content, int userId, EventMessages evtMsgs)
{
// raise UnPublishing event
if (scope.Events.DispatchCancelable(UnPublishing, this, new PublishEventArgs<IContent>(content, evtMsgs)))
// raise Unpublishing event
if (scope.Events.DispatchCancelable(Unpublishing, this, new PublishEventArgs<IContent>(content, evtMsgs)))
{
Logger.Info<ContentService>("Document {ContentName} (id={ContentId}) cannot be unpublished: unpublishing was cancelled.", content.Name, content.Id);
return new UnpublishResult(UnpublishResultType.FailedCancelledByEvent, evtMsgs, content);
@@ -2282,10 +2282,10 @@ namespace Umbraco.Core.Services.Implement
foreach (var content in contents.OrderByDescending(x => x.ParentId))
{
// if it's not trashed yet, and published, we should unpublish
// but... UnPublishing event makes no sense (not going to cancel?) and no need to save
// but... Unpublishing event makes no sense (not going to cancel?) and no need to save
// just raise the event
if (content.Trashed == false && content.Published)
scope.Events.Dispatch(UnPublished, this, new PublishEventArgs<IContent>(content, false, false), nameof(UnPublished));
scope.Events.Dispatch(Unpublished, this, new PublishEventArgs<IContent>(content, false, false), nameof(Unpublished));
// if current content has children, move them to trash
var c = content;
@@ -363,6 +363,16 @@ namespace Umbraco.Core.Services.Implement
// write-lock languages to guard against race conds when dealing with default language
scope.WriteLock(Constants.Locks.Languages);
// look for cycles - within write-lock
if (language.FallbackLanguageId.HasValue)
{
var languages = _languageRepository.GetMany().ToDictionary(x => x.Id, x => x);
if (!languages.ContainsKey(language.FallbackLanguageId.Value))
throw new InvalidOperationException($"Cannot save language {language.IsoCode} with fallback id={language.FallbackLanguageId.Value} which is not a valid language id.");
if (CreatesCycle(language, languages))
throw new InvalidOperationException($"Cannot save language {language.IsoCode} with fallback {languages[language.FallbackLanguageId.Value].IsoCode} as it would create a fallback cycle.");
}
var saveEventArgs = new SaveEventArgs<ILanguage>(language);
if (scope.Events.DispatchCancelable(SavingLanguage, this, saveEventArgs))
{
@@ -380,6 +390,20 @@ namespace Umbraco.Core.Services.Implement
}
}
private bool CreatesCycle(ILanguage language, IDictionary<int, ILanguage> languages)
{
// a new language is not referenced yet, so cannot be part of a cycle
if (!language.HasIdentity) return false;
var id = language.FallbackLanguageId;
while (true) // assuming languages does not already contains a cycle, this must end
{
if (!id.HasValue) return false; // no fallback means no cycle
if (id.Value == language.Id) return true; // back to language = cycle!
id = languages[id.Value].FallbackLanguageId; // else keep chaining
}
}
/// <summary>
/// Deletes a <see cref="ILanguage"/> by removing it (but not its usages) from the db
/// </summary>
@@ -399,8 +423,7 @@ namespace Umbraco.Core.Services.Implement
return;
}
//NOTE: There isn't any constraints in the db, so possible references aren't deleted
// NOTE: Other than the fall-back language, there aren't any other constraints in the db, so possible references aren't deleted
_languageRepository.Delete(language);
deleteEventArgs.CanCancel = false;
@@ -52,6 +52,7 @@ namespace Umbraco.Core.Services.Implement
private readonly IAuditRepository _auditRepository;
private readonly IContentTypeRepository _contentTypeRepository;
private readonly PropertyEditorCollection _propertyEditors;
private static HttpClient _httpClient;
public PackagingService(
ILogger logger,
@@ -1441,7 +1442,6 @@ namespace Umbraco.Core.Services.Implement
/// <returns></returns>
public string FetchPackageFile(Guid packageId, Version umbracoVersion, int userId)
{
using (var httpClient = new HttpClient())
using (var scope = _scopeProvider.CreateScope())
{
//includeHidden = true because we don't care if it's hidden we want to get the file regardless
@@ -1449,7 +1449,11 @@ namespace Umbraco.Core.Services.Implement
byte[] bytes;
try
{
bytes = httpClient.GetByteArrayAsync(url).GetAwaiter().GetResult();
if (_httpClient == null)
{
_httpClient = new HttpClient();
}
bytes = _httpClient.GetByteArrayAsync(url).GetAwaiter().GetResult();
}
catch (HttpRequestException ex)
{
@@ -76,11 +76,11 @@ namespace Umbraco.Core.Services.Implement
// reload - cheap, cached
// default role is single server, but if registrations contain more
// than one active server, then role is master or slave
// than one active server, then role is master or replica
regs = _serverRegistrationRepository.GetMany().ToArray();
// default role is single server, but if registrations contain more
// than one active server, then role is master or slave
// than one active server, then role is master or replica
_currentServerRole = regs.Count(x => x.IsActive) > 1
? (server.IsMaster ? ServerRole.Master : ServerRole.Replica)
: ServerRole.Single;
+24
View File
@@ -1384,6 +1384,30 @@ namespace Umbraco.Core
return idCheckList.Contains(value);
}
/// <summary>
/// Converts a file name to a friendly name for a content item
/// </summary>
/// <param name="fileName"></param>
/// <returns></returns>
public static string ToFriendlyName(this string fileName)
{
// strip the file extension
fileName = fileName.StripFileExtension();
// underscores and dashes to spaces
fileName = fileName.ReplaceMany(new[] { '_', '-' }, ' ');
// any other conversions ?
// Pascalcase (to be done last)
fileName = CultureInfo.InvariantCulture.TextInfo.ToTitleCase(fileName);
// Replace multiple consecutive spaces with a single space
fileName = string.Join(" ", fileName.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries));
return fileName;
}
// From: http://stackoverflow.com/a/961504/5018
// filters control characters but allows only properly-formed surrogate sequences
private static readonly Lazy<Regex> InvalidXmlChars = new Lazy<Regex>(() =>
+1 -1
View File
@@ -16,7 +16,7 @@
Single = 1,
/// <summary>
/// In a multi-servers environment, the server is a slave server.
/// In a multi-servers environment, the server is a replica server.
/// </summary>
Replica = 2,
+3 -1
View File
@@ -362,6 +362,7 @@
<Compile Include="Migrations\Upgrade\V_8_0_0\RefactorVariantsModel.cs" />
<Compile Include="Migrations\Upgrade\V_8_0_0\SuperZero.cs" />
<Compile Include="Migrations\Upgrade\V_8_0_0\TagsMigration.cs" />
<Compile Include="Migrations\Upgrade\V_8_0_0\FallbackLanguage.cs" />
<Compile Include="Migrations\Upgrade\V_8_0_0\UpdateDefaultMandatoryLanguage.cs" />
<Compile Include="Migrations\Upgrade\V_8_0_0\UserForeignKeys.cs" />
<Compile Include="Models\AuditEntry.cs" />
@@ -391,12 +392,14 @@
<Compile Include="Models\PathValidationExtensions.cs" />
<Compile Include="Models\Entities\TreeEntityBase.cs" />
<Compile Include="Models\PropertyTagsExtensions.cs" />
<Compile Include="Models\PublishedContent\Fallback.cs" />
<Compile Include="Models\PublishedContent\NoopPublishedValueFallback.cs" />
<Compile Include="Models\PublishedContent\PublishedCultureInfos.cs" />
<Compile Include="Models\PublishedContent\IVariationContextAccessor.cs" />
<Compile Include="Models\PublishedContent\IPublishedValueFallback.cs" />
<Compile Include="Models\PublishedContent\VariationContext.cs" />
<Compile Include="Models\PublishedContent\ThreadCultureVariationContextAccessor.cs" />
<Compile Include="Models\PublishedContent\VariationContextAccessorExtensions.cs" />
<Compile Include="Persistence\Dtos\AuditEntryDto.cs" />
<Compile Include="Persistence\Dtos\ConsentDto.cs" />
<Compile Include="Persistence\Dtos\ContentVersionCultureVariationDto.cs" />
@@ -727,7 +730,6 @@
<Compile Include="Models\PropertyCollection.cs" />
<Compile Include="Models\PropertyGroup.cs" />
<Compile Include="Models\PropertyGroupCollection.cs" />
<Compile Include="Models\PropertyTagChange.cs" />
<Compile Include="Models\PropertyType.cs" />
<Compile Include="Models\PropertyTypeCollection.cs" />
<Compile Include="Models\PublicAccessEntry.cs" />
@@ -99,7 +99,7 @@ namespace Umbraco.Tests.Cache
new EventDefinition<IContentService, MoveEventArgs<IContent>>(null, serviceContext.ContentService, new MoveEventArgs<IContent>(new MoveEventInfo<IContent>(null, "", -1)), "Trashed"),
new EventDefinition<IContentService, RecycleBinEventArgs>(null, serviceContext.ContentService, new RecycleBinEventArgs(Guid.NewGuid())),
new EventDefinition<IContentService, PublishEventArgs<IContent>>(null, serviceContext.ContentService, new PublishEventArgs<IContent>(Enumerable.Empty<IContent>()), "Published"),
new EventDefinition<IContentService, PublishEventArgs<IContent>>(null, serviceContext.ContentService, new PublishEventArgs<IContent>(Enumerable.Empty<IContent>()), "UnPublished"),
new EventDefinition<IContentService, PublishEventArgs<IContent>>(null, serviceContext.ContentService, new PublishEventArgs<IContent>(Enumerable.Empty<IContent>()), "Unpublished"),
new EventDefinition<IPublicAccessService, SaveEventArgs<PublicAccessEntry>>(null, serviceContext.PublicAccessService, new SaveEventArgs<PublicAccessEntry>(Enumerable.Empty<PublicAccessEntry>())),
new EventDefinition<IPublicAccessService, DeleteEventArgs<PublicAccessEntry>>(null, serviceContext.PublicAccessService, new DeleteEventArgs<PublicAccessEntry>(Enumerable.Empty<PublicAccessEntry>())),
@@ -66,10 +66,21 @@ namespace Umbraco.Tests.Configurations.DashboardSettings
[Test]
public void Test_Section_Tabs()
{
Assert.AreEqual(1, SettingsSection.Sections.ElementAt(0).Tabs.Count());
Assert.AreEqual(2, SettingsSection.Sections.ElementAt(1).Tabs.Count());
//Element 0 Alias "StartupSettingsDashboardSection"
Assert.AreEqual(2, SettingsSection.Sections.ElementAt(0).Tabs.Count());
//Element 1 Alias "StartupDeveloperDashboardSection"
Assert.AreEqual(1, SettingsSection.Sections.ElementAt(1).Tabs.Count());
//Element 2 Alias "StartupMediaDashboardSection"
Assert.AreEqual(2, SettingsSection.Sections.ElementAt(2).Tabs.Count());
//Element 3 Alias "StartupDashboardSection"
Assert.AreEqual(3, SettingsSection.Sections.ElementAt(3).Tabs.Count());
//Element 4 Alias "StartupMemberDashboardSection"
Assert.AreEqual(1, SettingsSection.Sections.ElementAt(4).Tabs.Count());
}
[Test]
@@ -463,7 +463,7 @@ namespace Umbraco.Tests.Integration
#endregion
#region Save, Publish & UnPublish single content
#region Save, Publish & Unpublish single content
[Test]
public void SaveUnpublishedContent()
@@ -721,7 +721,7 @@ namespace Umbraco.Tests.Integration
#endregion
#region Publish & UnPublish branch
#region Publish & Unpublish branch
[Test]
public void UnpublishContentBranch()
@@ -1,21 +1,25 @@
using System.Linq;
using NUnit.Framework;
using Umbraco.Core.Composing;
using Umbraco.Core.Models;
using Umbraco.Core.Persistence;
using Umbraco.Core.Persistence.DatabaseModelDefinitions;
using Umbraco.Core.Persistence.Dtos;
using Umbraco.Core.Persistence.Querying;
using Umbraco.Core.Persistence.Repositories;
using Umbraco.Core.Persistence.Repositories.Implement;
using Umbraco.Core.Scoping;
using Umbraco.Core.Logging;
using Umbraco.Tests.TestHelpers;
using Umbraco.Tests.Testing;
namespace Umbraco.Tests.Persistence.Repositories
{
[TestFixture]
[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)]
[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest, Logger = UmbracoTestOptions.Logger.Console)]
public class AuditRepositoryTest : TestWithDatabaseBase
{
[Test]
public void Can_Add_Audit_Entry()
{
@@ -23,7 +27,7 @@ namespace Umbraco.Tests.Persistence.Repositories
using (var scope = sp.CreateScope())
{
var repo = new AuditRepository((IScopeAccessor) sp, CacheHelper, Logger);
repo.Save(new AuditItem(-1, "This is a System audit trail", AuditType.System, 0));
repo.Save(new AuditItem(-1, "This is a System audit trail", AuditType.System, -1));
var dtos = scope.Database.Fetch<LogDto>("WHERE id > -1");
@@ -42,8 +46,8 @@ namespace Umbraco.Tests.Persistence.Repositories
for (var i = 0; i < 100; i++)
{
repo.Save(new AuditItem(i, $"Content {i} created", AuditType.New, 0));
repo.Save(new AuditItem(i, $"Content {i} published", AuditType.Publish, 0));
repo.Save(new AuditItem(i, $"Content {i} created", AuditType.New, -1));
repo.Save(new AuditItem(i, $"Content {i} published", AuditType.Publish, -1));
}
scope.Complete();
@@ -60,6 +64,49 @@ namespace Umbraco.Tests.Persistence.Repositories
}
}
[Test]
public void Get_Paged_Items_By_User_Id_With_Query_And_Filter()
{
var sp = TestObjects.GetScopeProvider(Logger);
using (var scope = sp.CreateScope())
{
var repo = new AuditRepository((IScopeAccessor)sp, CacheHelper, Logger);
for (var i = 0; i < 100; i++)
{
repo.Save(new AuditItem(i, $"Content {i} created", AuditType.New, -1));
repo.Save(new AuditItem(i, $"Content {i} published", AuditType.Publish, -1));
}
scope.Complete();
}
using (var scope = sp.CreateScope())
{
var repo = new AuditRepository((IScopeAccessor)sp, CacheHelper, Logger);
var query = sp.SqlContext.Query<IAuditItem>().Where(x => x.UserId == -1);
try
{
scope.Database.AsUmbracoDatabase().EnableSqlTrace = true;
scope.Database.AsUmbracoDatabase().EnableSqlCount = true;
var page = repo.GetPagedResultsByQuery(query, 0, 10, out var total, Direction.Descending,
new[] { AuditType.Publish },
sp.SqlContext.Query<IAuditItem>().Where(x => x.UserId > -2));
Assert.AreEqual(10, page.Count());
Assert.AreEqual(100, total);
}
finally
{
scope.Database.AsUmbracoDatabase().EnableSqlTrace = false;
scope.Database.AsUmbracoDatabase().EnableSqlCount = false;
}
}
}
[Test]
public void Get_Paged_Items_With_AuditType_Filter()
{
@@ -70,8 +117,8 @@ namespace Umbraco.Tests.Persistence.Repositories
for (var i = 0; i < 100; i++)
{
repo.Save(new AuditItem(i, $"Content {i} created", AuditType.New, 0));
repo.Save(new AuditItem(i, $"Content {i} published", AuditType.Publish, 0));
repo.Save(new AuditItem(i, $"Content {i} created", AuditType.New, -1));
repo.Save(new AuditItem(i, $"Content {i} published", AuditType.Publish, -1));
}
scope.Complete();
@@ -101,8 +148,8 @@ namespace Umbraco.Tests.Persistence.Repositories
for (var i = 0; i < 100; i++)
{
repo.Save(new AuditItem(i, "Content created", AuditType.New, 0));
repo.Save(new AuditItem(i, "Content published", AuditType.Publish, 0));
repo.Save(new AuditItem(i, "Content created", AuditType.New, -1));
repo.Save(new AuditItem(i, "Content published", AuditType.Publish, -1));
}
scope.Complete();
@@ -47,6 +47,7 @@ namespace Umbraco.Tests.Persistence.Repositories
Assert.That(language.HasIdentity, Is.True);
Assert.That(language.CultureName, Is.EqualTo("English (United States)"));
Assert.That(language.IsoCode, Is.EqualTo("en-US"));
Assert.That(language.FallbackLanguageId, Is.Null);
}
}
@@ -61,7 +62,8 @@ namespace Umbraco.Tests.Persistence.Repositories
var au = CultureInfo.GetCultureInfo("en-AU");
var language = (ILanguage)new Language(au.Name)
{
CultureName = au.DisplayName
CultureName = au.DisplayName,
FallbackLanguageId = 1
};
repository.Save(language);
@@ -73,6 +75,7 @@ namespace Umbraco.Tests.Persistence.Repositories
Assert.That(language.HasIdentity, Is.True);
Assert.That(language.CultureName, Is.EqualTo(au.DisplayName));
Assert.That(language.IsoCode, Is.EqualTo(au.Name));
Assert.That(language.FallbackLanguageId, Is.EqualTo(1));
}
}
@@ -182,14 +185,15 @@ namespace Umbraco.Tests.Persistence.Repositories
var repository = CreateRepository(provider);
// Act
var languageBR = new Language("pt-BR") {CultureName = "pt-BR"};
var languageBR = new Language("pt-BR") { CultureName = "pt-BR" };
repository.Save(languageBR);
// Assert
Assert.That(languageBR.HasIdentity, Is.True);
Assert.That(languageBR.Id, Is.EqualTo(6)); //With 5 existing entries the Id should be 6
Assert.IsFalse(languageBR.IsDefaultVariantLanguage);
Assert.IsFalse(languageBR.Mandatory);
Assert.IsFalse(languageBR.IsDefault);
Assert.IsFalse(languageBR.IsMandatory);
Assert.IsNull(languageBR.FallbackLanguageId);
}
}
@@ -203,14 +207,39 @@ namespace Umbraco.Tests.Persistence.Repositories
var repository = CreateRepository(provider);
// Act
var languageBR = new Language("pt-BR") { CultureName = "pt-BR", IsDefaultVariantLanguage = true, Mandatory = true };
var languageBR = new Language("pt-BR") { CultureName = "pt-BR", IsDefault = true, IsMandatory = true };
repository.Save(languageBR);
// Assert
Assert.That(languageBR.HasIdentity, Is.True);
Assert.That(languageBR.Id, Is.EqualTo(6)); //With 5 existing entries the Id should be 6
Assert.IsTrue(languageBR.IsDefaultVariantLanguage);
Assert.IsTrue(languageBR.Mandatory);
Assert.IsTrue(languageBR.IsDefault);
Assert.IsTrue(languageBR.IsMandatory);
Assert.IsNull(languageBR.FallbackLanguageId);
}
}
[Test]
public void Can_Perform_Add_On_LanguageRepository_With_Fallback_Language()
{
// Arrange
var provider = TestObjects.GetScopeProvider(Logger);
using (var scope = provider.CreateScope())
{
var repository = CreateRepository(provider);
// Act
var languageBR = new Language("pt-BR")
{
CultureName = "pt-BR",
FallbackLanguageId = 1
};
repository.Save(languageBR);
// Assert
Assert.That(languageBR.HasIdentity, Is.True);
Assert.That(languageBR.Id, Is.EqualTo(6)); //With 5 existing entries the Id should be 6
Assert.That(languageBR.FallbackLanguageId, Is.EqualTo(1));
}
}
@@ -223,24 +252,22 @@ namespace Umbraco.Tests.Persistence.Repositories
{
var repository = CreateRepository(provider);
var languageBR = (ILanguage)new Language("pt-BR") { CultureName = "pt-BR", IsDefaultVariantLanguage = true, Mandatory = true };
var languageBR = (ILanguage)new Language("pt-BR") { CultureName = "pt-BR", IsDefault = true, IsMandatory = true };
repository.Save(languageBR);
var languageEN = new Language("en-AU") { CultureName = "en-AU" };
repository.Save(languageEN);
Assert.IsTrue(languageBR.IsDefaultVariantLanguage);
Assert.IsTrue(languageBR.Mandatory);
Assert.IsTrue(languageBR.IsDefault);
Assert.IsTrue(languageBR.IsMandatory);
// Act
var languageNZ = new Language("en-NZ") { CultureName = "en-NZ", IsDefaultVariantLanguage = true, Mandatory = true };
var languageNZ = new Language("en-NZ") { CultureName = "en-NZ", IsDefault = true, IsMandatory = true };
repository.Save(languageNZ);
languageBR = repository.Get(languageBR.Id);
// Assert
Assert.IsFalse(languageBR.IsDefaultVariantLanguage);
Assert.IsTrue(languageNZ.IsDefaultVariantLanguage);
Assert.IsFalse(languageBR.IsDefault);
Assert.IsTrue(languageNZ.IsDefault);
}
}
@@ -257,6 +284,7 @@ namespace Umbraco.Tests.Persistence.Repositories
var language = repository.Get(5);
language.IsoCode = "pt-BR";
language.CultureName = "pt-BR";
language.FallbackLanguageId = 1;
repository.Save(language);
@@ -266,6 +294,7 @@ namespace Umbraco.Tests.Persistence.Repositories
Assert.That(languageUpdated, Is.Not.Null);
Assert.That(languageUpdated.IsoCode, Is.EqualTo("pt-BR"));
Assert.That(languageUpdated.CultureName, Is.EqualTo("pt-BR"));
Assert.That(languageUpdated.FallbackLanguageId, Is.EqualTo(1));
}
}
@@ -289,6 +318,30 @@ namespace Umbraco.Tests.Persistence.Repositories
}
}
[Test]
public void Can_Perform_Delete_On_LanguageRepository_With_Language_Used_As_Fallback()
{
// Arrange
var provider = TestObjects.GetScopeProvider(Logger);
using (var scope = provider.CreateScope())
{
// Add language to delete as a fall-back language to another one
var repository = CreateRepository(provider);
var languageToFallbackFrom = repository.Get(5);
languageToFallbackFrom.FallbackLanguageId = 2; // fall back to #2 (something we can delete)
repository.Save(languageToFallbackFrom);
// delete #2
var languageToDelete = repository.Get(2);
repository.Delete(languageToDelete);
var exists = repository.Exists(2);
// has been deleted
Assert.That(exists, Is.False);
}
}
[Test]
public void Can_Perform_Exists_On_LanguageRepository()
{
@@ -314,8 +367,10 @@ namespace Umbraco.Tests.Persistence.Repositories
base.TearDown();
}
public void CreateTestData()
private void CreateTestData()
{
//Id 1 is en-US - when Umbraco is installed
var languageDK = new Language("da-DK") { CultureName = "da-DK" };
ServiceContext.LocalizationService.Save(languageDK);//Id 2
@@ -8,17 +8,19 @@ using Umbraco.Core.IO;
using Umbraco.Core.Logging;
using Umbraco.Core.Models.Membership;
using Umbraco.Core.Persistence.Mappers;
using Umbraco.Core.Persistence.DatabaseModelDefinitions;
using Umbraco.Core.Persistence.Repositories;
using Umbraco.Core.Persistence.Repositories.Implement;
using Umbraco.Core.Scoping;
using Umbraco.Tests.TestHelpers;
using Umbraco.Tests.TestHelpers.Entities;
using Umbraco.Tests.Testing;
using Umbraco.Core.Persistence;
namespace Umbraco.Tests.Persistence.Repositories
{
[TestFixture]
[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest, WithApplication = true)]
[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest, WithApplication = true, Logger = UmbracoTestOptions.Logger.Console)]
public class UserRepositoryTest : TestWithDatabaseBase
{
private MediaRepository CreateMediaRepository(IScopeProvider provider, out IMediaTypeRepository mediaTypeRepository)
@@ -50,14 +52,14 @@ namespace Umbraco.Tests.Persistence.Repositories
private UserRepository CreateRepository(IScopeProvider provider)
{
var accessor = (IScopeAccessor) provider;
var repository = new UserRepository(accessor, CacheHelper.CreateDisabledCacheHelper(), Mock.Of<ILogger>(), Mock.Of<IMapperCollection>(), TestObjects.GetGlobalSettings());
var repository = new UserRepository(accessor, CacheHelper.CreateDisabledCacheHelper(), Logger, Mappers, TestObjects.GetGlobalSettings());
return repository;
}
private UserGroupRepository CreateUserGroupRepository(IScopeProvider provider)
{
var accessor = (IScopeAccessor) provider;
return new UserGroupRepository(accessor, CacheHelper.CreateDisabledCacheHelper(), Mock.Of<ILogger>());
return new UserGroupRepository(accessor, CacheHelper.CreateDisabledCacheHelper(), Logger);
}
[Test]
@@ -338,7 +340,72 @@ namespace Umbraco.Tests.Persistence.Repositories
var result = repository.Count(query);
// Assert
Assert.That(result, Is.GreaterThanOrEqualTo(2));
Assert.AreEqual(2, result);
}
}
[Test]
public void Can_Get_Paged_Results_By_Query_And_Filter_And_Groups()
{
var provider = TestObjects.GetScopeProvider(Logger);
using (var scope = provider.CreateScope())
{
var repository = CreateRepository(provider);
var users = CreateAndCommitMultipleUsers(repository);
var query = provider.SqlContext.Query<IUser>().Where(x => x.Username == "TestUser1" || x.Username == "TestUser2");
try
{
scope.Database.AsUmbracoDatabase().EnableSqlTrace = true;
scope.Database.AsUmbracoDatabase().EnableSqlCount = true;
// Act
var result = repository.GetPagedResultsByQuery(query, 0, 10, out var totalRecs, user => user.Id, Direction.Ascending,
excludeUserGroups: new[] { Constants.Security.TranslatorGroupAlias },
filter: provider.SqlContext.Query<IUser>().Where(x => x.Id > -1));
// Assert
Assert.AreEqual(2, totalRecs);
}
finally
{
scope.Database.AsUmbracoDatabase().EnableSqlTrace = false;
scope.Database.AsUmbracoDatabase().EnableSqlCount = false;
}
}
}
[Test]
public void Can_Get_Paged_Results_With_Filter_And_Groups()
{
var provider = TestObjects.GetScopeProvider(Logger);
using (var scope = provider.CreateScope())
{
var repository = CreateRepository(provider);
var users = CreateAndCommitMultipleUsers(repository);
try
{
scope.Database.AsUmbracoDatabase().EnableSqlTrace = true;
scope.Database.AsUmbracoDatabase().EnableSqlCount = true;
// Act
var result = repository.GetPagedResultsByQuery(null, 0, 10, out var totalRecs, user => user.Id, Direction.Ascending,
includeUserGroups: new[] { Constants.Security.AdminGroupAlias, Constants.Security.SensitiveDataGroupAlias },
excludeUserGroups: new[] { Constants.Security.TranslatorGroupAlias },
filter: provider.SqlContext.Query<IUser>().Where(x => x.Id == -1));
// Assert
Assert.AreEqual(1, totalRecs);
}
finally
{
scope.Database.AsUmbracoDatabase().EnableSqlTrace = false;
scope.Database.AsUmbracoDatabase().EnableSqlCount = false;
}
}
}
@@ -0,0 +1,279 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reflection;
using Moq;
using NUnit.Framework;
using Umbraco.Core.Composing;
using Umbraco.Core.Models;
using Umbraco.Core.Models.PublishedContent;
using Umbraco.Core.PropertyEditors;
using Umbraco.Core.Services;
using Umbraco.Tests.Testing;
using Umbraco.Web;
namespace Umbraco.Tests.PublishedContent
{
[TestFixture]
[UmbracoTest(PluginManager = UmbracoTestOptions.PluginManager.PerFixture)]
public class PublishedContentLanguageVariantTests : PublishedContentSnapshotTestBase
{
protected override void Compose()
{
base.Compose();
Container.RegisterSingleton(_ => GetServiceContext());
}
protected ServiceContext GetServiceContext()
{
var serviceContext = TestObjects.GetServiceContextMock(Container);
MockLocalizationService(serviceContext);
return serviceContext;
}
private static void MockLocalizationService(ServiceContext serviceContext)
{
// Set up languages.
// Spanish falls back to English and Italian to Spanish (and then to English).
// French has no fall back.
// Danish, Swedish and Norweigan create an invalid loop.
var languages = new List<Language>
{
new Language("en-US") { Id = 1, CultureName = "English", IsDefault = true },
new Language("fr") { Id = 2, CultureName = "French" },
new Language("es") { Id = 3, CultureName = "Spanish", FallbackLanguageId = 1 },
new Language("it") { Id = 4, CultureName = "Italian", FallbackLanguageId = 3 },
new Language("de") { Id = 5, CultureName = "German" },
new Language("da") { Id = 6, CultureName = "Danish", FallbackLanguageId = 8 },
new Language("sv") { Id = 7, CultureName = "Swedish", FallbackLanguageId = 6 },
new Language("no") { Id = 8, CultureName = "Norweigan", FallbackLanguageId = 7 },
new Language("nl") { Id = 9, CultureName = "Dutch", FallbackLanguageId = 1 }
};
var localizationService = Mock.Get(serviceContext.LocalizationService);
localizationService.Setup(x => x.GetAllLanguages()).Returns(languages);
localizationService.Setup(x => x.GetLanguageById(It.IsAny<int>()))
.Returns((int id) => languages.SingleOrDefault(y => y.Id == id));
localizationService.Setup(x => x.GetLanguageByIsoCode(It.IsAny<string>()))
.Returns((string c) => languages.SingleOrDefault(y => y.IsoCode == c));
}
internal override void PopulateCache(PublishedContentTypeFactory factory, SolidPublishedContentCache cache)
{
var props = new[]
{
factory.CreatePropertyType("prop1", 1),
factory.CreatePropertyType("welcomeText", 1),
factory.CreatePropertyType("welcomeText2", 1),
};
var contentType1 = factory.CreateContentType(1, "ContentType1", Enumerable.Empty<string>(), props);
var prop1 = new SolidPublishedPropertyWithLanguageVariants
{
Alias = "welcomeText",
};
prop1.SetSourceValue("en-US", "Welcome", true);
prop1.SetValue("en-US", "Welcome", true);
prop1.SetSourceValue("de", "Willkommen");
prop1.SetValue("de", "Willkommen");
prop1.SetSourceValue("nl", "Welkom");
prop1.SetValue("nl", "Welkom");
var prop2 = new SolidPublishedPropertyWithLanguageVariants
{
Alias = "welcomeText2",
};
prop2.SetSourceValue("en-US", "Welcome", true);
prop2.SetValue("en-US", "Welcome", true);
var prop3 = new SolidPublishedPropertyWithLanguageVariants
{
Alias = "welcomeText",
};
prop3.SetSourceValue("en-US", "Welcome", true);
prop3.SetValue("en-US", "Welcome", true);
var item1 = new SolidPublishedContent(contentType1)
{
Id = 1,
SortOrder = 0,
Name = "Content 1",
UrlSegment = "content-1",
Path = "/1",
Level = 1,
Url = "/content-1",
ParentId = -1,
ChildIds = new[] { 2 },
Properties = new Collection<IPublishedProperty>
{
prop1, prop2
}
};
var item2 = new SolidPublishedContent(contentType1)
{
Id = 2,
SortOrder = 0,
Name = "Content 2",
UrlSegment = "content-2",
Path = "/1/2",
Level = 2,
Url = "/content-1/content-2",
ParentId = 1,
ChildIds = new int[] { },
Properties = new Collection<IPublishedProperty>
{
prop3
}
};
item1.Children = new List<IPublishedContent> { item2 };
item2.Parent = item1;
cache.Add(item1);
cache.Add(item2);
}
[Test]
public void Can_Get_Content_For_Populated_Requested_Language()
{
var content = UmbracoContext.Current.ContentCache.GetAtRoot().First();
var value = content.Value("welcomeText", "en-US");
Assert.AreEqual("Welcome", value);
}
[Test]
public void Can_Get_Content_For_Populated_Requested_Non_Default_Language()
{
var content = UmbracoContext.Current.ContentCache.GetAtRoot().First();
var value = content.Value("welcomeText", "de");
Assert.AreEqual("Willkommen", value);
}
[Test]
public void Do_Not_Get_Content_For_Unpopulated_Requested_Language_Without_Fallback()
{
var content = UmbracoContext.Current.ContentCache.GetAtRoot().First();
var value = content.Value("welcomeText", "fr");
Assert.IsNull(value);
}
[Test]
public void Do_Not_Get_Content_For_Unpopulated_Requested_Language_With_Fallback_Unless_Requested()
{
var content = UmbracoContext.Current.ContentCache.GetAtRoot().First();
var value = content.Value("welcomeText", "es");
Assert.IsNull(value);
}
[Test]
public void Can_Get_Content_For_Unpopulated_Requested_Language_With_Fallback()
{
var content = UmbracoContext.Current.ContentCache.GetAtRoot().First();
var value = content.Value("welcomeText", "es", fallback: Fallback.ToLanguage);
Assert.AreEqual("Welcome", value);
}
[Test]
public void Can_Get_Content_For_Unpopulated_Requested_Language_With_Fallback_Over_Two_Levels()
{
var content = UmbracoContext.Current.ContentCache.GetAtRoot().First();
var value = content.Value("welcomeText", "it", fallback: Fallback.To(Fallback.Language, Fallback.Ancestors));
Assert.AreEqual("Welcome", value);
}
[Test]
public void Do_Not_GetContent_For_Unpopulated_Requested_Language_With_Fallback_Over_That_Loops()
{
var content = UmbracoContext.Current.ContentCache.GetAtRoot().First();
var value = content.Value("welcomeText", "no", fallback: Fallback.ToLanguage);
Assert.IsNull(value);
}
[Test]
public void Do_Not_Get_Content_Recursively_Unless_Requested()
{
var content = UmbracoContext.Current.ContentCache.GetAtRoot().First().Children.First();
var value = content.Value("welcomeText2");
Assert.IsNull(value);
}
[Test]
public void Can_Get_Content_Recursively()
{
var content = UmbracoContext.Current.ContentCache.GetAtRoot().First().Children.First();
var value = content.Value("welcomeText2", fallback: Fallback.ToAncestors);
Assert.AreEqual("Welcome", value);
}
[Test]
public void Can_Get_Content_With_Recursive_Priority()
{
var content = UmbracoContext.Current.ContentCache.GetAtRoot().First().Children.First();
var value = content.Value("welcomeText", "nl", fallback: Fallback.To(Fallback.Ancestors, Fallback.Language));
// No Dutch value is directly assigned. Check has fallen back to Dutch value from parent.
Assert.AreEqual("Welkom", value);
}
[Test]
public void Can_Get_Content_With_Fallback_Language_Priority()
{
var content = UmbracoContext.Current.ContentCache.GetAtRoot().First().Children.First();
var value = content.Value("welcomeText", "nl", fallback: Fallback.ToLanguage);
// No Dutch value is directly assigned. Check has fallen back to English value from language variant.
Assert.AreEqual("Welcome", value);
}
[Test]
public void Throws_For_Non_Supported_Fallback()
{
var content = UmbracoContext.Current.ContentCache.GetAtRoot().First().Children.First();
Assert.Throws<NotSupportedException>(() => content.Value("welcomeText", "nl", fallback: Fallback.To(999)));
}
[Test]
public void Can_Fallback_To_Default_Value()
{
var content = UmbracoContext.Current.ContentCache.GetAtRoot().First().Children.First();
// no Dutch value is assigned, so getting null
var value = content.Value("welcomeText", "nl");
Assert.IsNull(value);
// even if we 'just' provide a default value
value = content.Value("welcomeText", "nl", defaultValue: "woop");
Assert.IsNull(value);
// but it works with proper fallback settings
value = content.Value("welcomeText", "nl", fallback: Fallback.ToDefaultValue, defaultValue: "woop");
Assert.AreEqual("woop", value);
}
[Test]
public void Can_Have_Custom_Default_Value()
{
var content = UmbracoContext.Current.ContentCache.GetAtRoot().First().Children.First();
// hack the value, pretend the converter would return something
var prop = content.GetProperty("welcomeText") as SolidPublishedPropertyWithLanguageVariants;
Assert.IsNotNull(prop);
prop.SetValue("nl", "nope"); // HasValue false but getting value returns this
// there is an EN value
var value = content.Value("welcomeText", "en-US");
Assert.AreEqual("Welcome", value);
// there is no NL value and we get the 'converted' value
value = content.Value("welcomeText", "nl");
Assert.AreEqual("nope", value);
// but it works with proper fallback settings
value = content.Value("welcomeText", "nl", fallback: Fallback.ToDefaultValue, defaultValue: "woop");
Assert.AreEqual("woop", value);
}
}
}
@@ -1,91 +1,94 @@
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Collections.ObjectModel;
using System.Web.Routing;
using Moq;
using NUnit.Framework;
using Umbraco.Core.Models.PublishedContent;
using Umbraco.Web;
using Umbraco.Web.PublishedCache;
using Umbraco.Web.Routing;
using Umbraco.Web.Security;
using Umbraco.Core.Composing;
using Current = Umbraco.Core.Composing.Current;
using LightInject;
using Umbraco.Core.Logging;
using Umbraco.Core.Models;
using Umbraco.Core.PropertyEditors;
using Umbraco.Core.Services;
using Umbraco.Tests.TestHelpers;
using Umbraco.Tests.Testing;
using Umbraco.Tests.Testing.Objects.Accessors;
namespace Umbraco.Tests.PublishedContent
{
[TestFixture]
[UmbracoTest(PluginManager = UmbracoTestOptions.PluginManager.PerFixture)]
public class PublishedContentMoreTests : PublishedContentTestBase
public class PublishedContentMoreTests : PublishedContentSnapshotTestBase
{
// read http://stackoverflow.com/questions/7713326/extension-method-that-works-on-ienumerablet-and-iqueryablet
// and http://msmvps.com/blogs/jon_skeet/archive/2010/10/28/overloading-and-generic-constraints.aspx
// and http://blogs.msdn.com/b/ericlippert/archive/2009/12/10/constraints-are-not-part-of-the-signature.aspx
public override void SetUp()
internal override void PopulateCache(PublishedContentTypeFactory factory, SolidPublishedContentCache cache)
{
base.SetUp();
var props = new[]
{
factory.CreatePropertyType("prop1", 1),
};
var contentType1 = factory.CreateContentType(1, "ContentType1", Enumerable.Empty<string>(), props);
var contentType2 = factory.CreateContentType(2, "ContentType2", Enumerable.Empty<string>(), props);
var contentType2Sub = factory.CreateContentType(3, "ContentType2Sub", Enumerable.Empty<string>(), props);
var umbracoContext = GetUmbracoContext();
Umbraco.Web.Composing.Current.UmbracoContextAccessor.UmbracoContext = umbracoContext;
}
cache.Add(new SolidPublishedContent(contentType1)
{
Id = 1,
SortOrder = 0,
Name = "Content 1",
UrlSegment = "content-1",
Path = "/1",
Level = 1,
Url = "/content-1",
ParentId = -1,
ChildIds = new int[] { },
Properties = new Collection<IPublishedProperty>
{
new SolidPublishedProperty
{
Alias = "prop1",
SolidHasValue = true,
SolidValue = 1234,
SolidSourceValue = "1234"
}
}
});
protected override void Compose()
{
base.Compose();
cache.Add(new SolidPublishedContent(contentType2)
{
Id = 2,
SortOrder = 1,
Name = "Content 2",
UrlSegment = "content-2",
Path = "/2",
Level = 1,
Url = "/content-2",
ParentId = -1,
ChildIds = new int[] { },
Properties = new Collection<IPublishedProperty>
{
new SolidPublishedProperty
{
Alias = "prop1",
SolidHasValue = true,
SolidValue = 1234,
SolidSourceValue = "1234"
}
}
});
Container.RegisterSingleton<IPublishedModelFactory>(f => new PublishedModelFactory(f.GetInstance<TypeLoader>().GetTypes<PublishedContentModel>()));
}
protected override TypeLoader CreatePluginManager(IServiceFactory f)
{
var pluginManager = base.CreatePluginManager(f);
// this is so the model factory looks into the test assembly
pluginManager.AssembliesToScan = pluginManager.AssembliesToScan
.Union(new[] { typeof (PublishedContentMoreTests).Assembly })
.ToList();
return pluginManager;
}
private UmbracoContext GetUmbracoContext()
{
RouteData routeData = null;
var publishedSnapshot = CreatePublishedSnapshot();
var publishedSnapshotService = new Mock<IPublishedSnapshotService>();
publishedSnapshotService.Setup(x => x.CreatePublishedSnapshot(It.IsAny<string>())).Returns(publishedSnapshot);
var globalSettings = TestObjects.GetGlobalSettings();
var httpContext = GetHttpContextFactory("http://umbraco.local/", routeData).HttpContext;
var umbracoContext = new UmbracoContext(
httpContext,
publishedSnapshotService.Object,
new WebSecurity(httpContext, Current.Services.UserService, globalSettings),
TestObjects.GetUmbracoSettings(),
Enumerable.Empty<IUrlProvider>(),
globalSettings,
new TestVariationContextAccessor());
return umbracoContext;
}
public override void TearDown()
{
base.TearDown();
Current.Reset();
cache.Add(new SolidPublishedContent(contentType2Sub)
{
Id = 3,
SortOrder = 2,
Name = "Content 2Sub",
UrlSegment = "content-2sub",
Path = "/3",
Level = 1,
Url = "/content-2sub",
ParentId = -1,
ChildIds = new int[] { },
Properties = new Collection<IPublishedProperty>
{
new SolidPublishedProperty
{
Alias = "prop1",
SolidHasValue = true,
SolidValue = 1234,
SolidSourceValue = "1234"
}
}
});
}
[Test]
@@ -197,95 +200,5 @@ namespace Umbraco.Tests.PublishedContent
Assert.AreEqual(1, result[0].Id);
Assert.AreEqual(2, result[1].Id);
}
private static SolidPublishedSnapshot CreatePublishedSnapshot()
{
var dataTypeService = new TestObjects.TestDataTypeService(
new DataType(new VoidEditor(Mock.Of<ILogger>())) { Id = 1 });
var factory = new PublishedContentTypeFactory(Mock.Of<IPublishedModelFactory>(), new PropertyValueConverterCollection(Array.Empty<IPropertyValueConverter>()), dataTypeService);
var caches = new SolidPublishedSnapshot();
var cache = caches.InnerContentCache;
var props = new[]
{
factory.CreatePropertyType("prop1", 1),
};
var contentType1 = factory.CreateContentType(1, "ContentType1", Enumerable.Empty<string>(), props);
var contentType2 = factory.CreateContentType(2, "ContentType2", Enumerable.Empty<string>(), props);
var contentType2Sub = factory.CreateContentType(3, "ContentType2Sub", Enumerable.Empty<string>(), props);
cache.Add(new SolidPublishedContent(contentType1)
{
Id = 1,
SortOrder = 0,
Name = "Content 1",
UrlSegment = "content-1",
Path = "/1",
Level = 1,
Url = "/content-1",
ParentId = -1,
ChildIds = new int[] {},
Properties = new Collection<IPublishedProperty>
{
new SolidPublishedProperty
{
Alias = "prop1",
SolidHasValue = true,
SolidValue = 1234,
SolidSourceValue = "1234"
}
}
});
cache.Add(new SolidPublishedContent(contentType2)
{
Id = 2,
SortOrder = 1,
Name = "Content 2",
UrlSegment = "content-2",
Path = "/2",
Level = 1,
Url = "/content-2",
ParentId = -1,
ChildIds = new int[] { },
Properties = new Collection<IPublishedProperty>
{
new SolidPublishedProperty
{
Alias = "prop1",
SolidHasValue = true,
SolidValue = 1234,
SolidSourceValue = "1234"
}
}
});
cache.Add(new SolidPublishedContent(contentType2Sub)
{
Id = 3,
SortOrder = 2,
Name = "Content 2Sub",
UrlSegment = "content-2sub",
Path = "/3",
Level = 1,
Url = "/content-2sub",
ParentId = -1,
ChildIds = new int[] { },
Properties = new Collection<IPublishedProperty>
{
new SolidPublishedProperty
{
Alias = "prop1",
SolidHasValue = true,
SolidValue = 1234,
SolidSourceValue = "1234"
}
}
});
return caches;
}
}
}
@@ -0,0 +1,100 @@
using System;
using System.Linq;
using System.Collections.ObjectModel;
using System.Web.Routing;
using Moq;
using Umbraco.Core.Models.PublishedContent;
using Umbraco.Web;
using Umbraco.Web.PublishedCache;
using Umbraco.Web.Routing;
using Umbraco.Web.Security;
using Umbraco.Core.Composing;
using Current = Umbraco.Core.Composing.Current;
using LightInject;
using Umbraco.Core.Logging;
using Umbraco.Core.Models;
using Umbraco.Core.PropertyEditors;
using Umbraco.Tests.TestHelpers;
using Umbraco.Tests.Testing.Objects.Accessors;
namespace Umbraco.Tests.PublishedContent
{
public abstract class PublishedContentSnapshotTestBase : PublishedContentTestBase
{
// read http://stackoverflow.com/questions/7713326/extension-method-that-works-on-ienumerablet-and-iqueryablet
// and http://msmvps.com/blogs/jon_skeet/archive/2010/10/28/overloading-and-generic-constraints.aspx
// and http://blogs.msdn.com/b/ericlippert/archive/2009/12/10/constraints-are-not-part-of-the-signature.aspx
public override void SetUp()
{
base.SetUp();
var umbracoContext = GetUmbracoContext();
Umbraco.Web.Composing.Current.UmbracoContextAccessor.UmbracoContext = umbracoContext;
}
protected override void Compose()
{
base.Compose();
Container.RegisterSingleton<IPublishedModelFactory>(f => new PublishedModelFactory(f.GetInstance<TypeLoader>().GetTypes<PublishedContentModel>()));
}
protected override TypeLoader CreatePluginManager(IServiceFactory f)
{
var pluginManager = base.CreatePluginManager(f);
// this is so the model factory looks into the test assembly
pluginManager.AssembliesToScan = pluginManager.AssembliesToScan
.Union(new[] { typeof (PublishedContentMoreTests).Assembly })
.ToList();
return pluginManager;
}
private UmbracoContext GetUmbracoContext()
{
RouteData routeData = null;
var publishedSnapshot = CreatePublishedSnapshot();
var publishedSnapshotService = new Mock<IPublishedSnapshotService>();
publishedSnapshotService.Setup(x => x.CreatePublishedSnapshot(It.IsAny<string>())).Returns(publishedSnapshot);
var globalSettings = TestObjects.GetGlobalSettings();
var httpContext = GetHttpContextFactory("http://umbraco.local/", routeData).HttpContext;
var umbracoContext = new UmbracoContext(
httpContext,
publishedSnapshotService.Object,
new WebSecurity(httpContext, Current.Services.UserService, globalSettings),
TestObjects.GetUmbracoSettings(),
Enumerable.Empty<IUrlProvider>(),
globalSettings,
new TestVariationContextAccessor());
return umbracoContext;
}
public override void TearDown()
{
base.TearDown();
Current.Reset();
}
private SolidPublishedSnapshot CreatePublishedSnapshot()
{
var dataTypeService = new TestObjects.TestDataTypeService(
new DataType(new VoidEditor(Mock.Of<ILogger>())) { Id = 1 });
var factory = new PublishedContentTypeFactory(Mock.Of<IPublishedModelFactory>(), new PropertyValueConverterCollection(Array.Empty<IPropertyValueConverter>()), dataTypeService);
var caches = new SolidPublishedSnapshot();
var cache = caches.InnerContentCache;
PopulateCache(factory, cache);
return caches;
}
internal abstract void PopulateCache(PublishedContentTypeFactory factory, SolidPublishedContentCache cache);
}
}
@@ -38,7 +38,7 @@ namespace Umbraco.Tests.PublishedContent
var logger = Mock.Of<ILogger>();
var dataTypeService = new TestObjects.TestDataTypeService(
new DataType(new VoidEditor(logger)) { Id = 1},
new DataType(new VoidEditor(logger)) { Id = 1 },
new DataType(new TrueFalsePropertyEditor(logger)) { Id = 1001 },
new DataType(new RichTextPropertyEditor(logger)) { Id = 1002 },
new DataType(new IntegerPropertyEditor(logger)) { Id = 1003 },
@@ -338,11 +338,11 @@ namespace Umbraco.Tests.PublishedContent
}
[Test]
public void GetPropertyValueRecursiveTest()
public void Get_Property_Value_Recursive()
{
var doc = GetNode(1174);
var rVal = doc.Value("testRecursive", recurse: true);
var nullVal = doc.Value("DoNotFindThis", recurse: true);
var rVal = doc.Value("testRecursive", fallback: Fallback.ToAncestors);
var nullVal = doc.Value("DoNotFindThis", fallback: Fallback.ToAncestors);
Assert.AreEqual("This is the recursive val", rVal);
Assert.AreEqual(null, nullVal);
}
@@ -248,7 +248,7 @@ namespace Umbraco.Tests.PublishedContent
#endregion
}
class SolidPublishedProperty : IPublishedProperty
internal class SolidPublishedProperty : IPublishedProperty
{
public PublishedPropertyType PropertyType { get; set; }
public string Alias { get; set; }
@@ -257,10 +257,86 @@ namespace Umbraco.Tests.PublishedContent
public bool SolidHasValue { get; set; }
public object SolidXPathValue { get; set; }
public object GetSourceValue(string culture = null, string segment = null) => SolidSourceValue;
public object GetValue(string culture = null, string segment = null) => SolidValue;
public object GetXPathValue(string culture = null, string segment = null) => SolidXPathValue;
public bool HasValue(string culture = null, string segment = null) => SolidHasValue;
public virtual object GetSourceValue(string culture = null, string segment = null) => SolidSourceValue;
public virtual object GetValue(string culture = null, string segment = null) => SolidValue;
public virtual object GetXPathValue(string culture = null, string segment = null) => SolidXPathValue;
public virtual bool HasValue(string culture = null, string segment = null) => SolidHasValue;
}
internal class SolidPublishedPropertyWithLanguageVariants : SolidPublishedProperty
{
private readonly IDictionary<string, object> _solidSourceValues = new Dictionary<string, object>();
private readonly IDictionary<string, object> _solidValues = new Dictionary<string, object>();
private readonly IDictionary<string, object> _solidXPathValues = new Dictionary<string, object>();
public override object GetSourceValue(string culture = null, string segment = null)
{
if (string.IsNullOrEmpty(culture))
{
return base.GetSourceValue(culture, segment);
}
return _solidSourceValues.ContainsKey(culture) ? _solidSourceValues[culture] : null;
}
public override object GetValue(string culture = null, string segment = null)
{
if (string.IsNullOrEmpty(culture))
{
return base.GetValue(culture, segment);
}
return _solidValues.ContainsKey(culture) ? _solidValues[culture] : null;
}
public override object GetXPathValue(string culture = null, string segment = null)
{
if (string.IsNullOrEmpty(culture))
{
return base.GetXPathValue(culture, segment);
}
return _solidXPathValues.ContainsKey(culture) ? _solidXPathValues[culture] : null;
}
public override bool HasValue(string culture = null, string segment = null)
{
if (string.IsNullOrEmpty(culture))
{
return base.HasValue(culture, segment);
}
return _solidSourceValues.ContainsKey(culture);
}
public void SetSourceValue(string culture, object value, bool defaultValue = false)
{
_solidSourceValues.Add(culture, value);
if (defaultValue)
{
SolidSourceValue = value;
SolidHasValue = true;
}
}
public void SetValue(string culture, object value, bool defaultValue = false)
{
_solidValues.Add(culture, value);
if (defaultValue)
{
SolidValue = value;
SolidHasValue = true;
}
}
public void SetXPathValue(string culture, object value, bool defaultValue = false)
{
_solidXPathValues.Add(culture, value);
if (defaultValue)
{
SolidXPathValue = value;
}
}
}
[PublishedModel("ContentType2")]
@@ -169,7 +169,7 @@ namespace Umbraco.Tests.Scoping
[Test]
public void SupersededEvents2()
{
Test_UnPublished += OnDoThingFail;
Test_Unpublished += OnDoThingFail;
Test_Deleted += OnDoThingFail;
var contentService = Mock.Of<IContentService>();
@@ -178,7 +178,7 @@ namespace Umbraco.Tests.Scoping
var scopeProvider = _testObjects.GetScopeProvider(Mock.Of<ILogger>());
using (var scope = scopeProvider.CreateScope(eventDispatcher: new PassiveEventDispatcher()))
{
scope.Events.Dispatch(Test_UnPublished, contentService, new PublishEventArgs<IContent>(new [] { content }), "UnPublished");
scope.Events.Dispatch(Test_Unpublished, contentService, new PublishEventArgs<IContent>(new [] { content }), "Unpublished");
scope.Events.Dispatch(Test_Deleted, contentService, new DeleteEventArgs<IContent>(new [] { content }), "Deleted");
// see U4-10764
@@ -395,7 +395,7 @@ namespace Umbraco.Tests.Scoping
public static event TypedEventHandler<ScopeEventDispatcherTests, SaveEventArgs<decimal>> DoThing3;
public static event TypedEventHandler<IContentService, PublishEventArgs<IContent>> Test_UnPublished;
public static event TypedEventHandler<IContentService, PublishEventArgs<IContent>> Test_Unpublished;
public static event TypedEventHandler<IContentService, DeleteEventArgs<IContent>> Test_Deleted;
public class TestEventArgs : CancellableObjectEventArgs
@@ -1208,7 +1208,7 @@ namespace Umbraco.Tests.Services
}
[Test]
public void Can_UnPublish_Content()
public void Can_Unpublish_Content()
{
// Arrange
var contentService = ServiceContext.ContentService;
@@ -2489,7 +2489,7 @@ namespace Umbraco.Tests.Services
{
var languageService = ServiceContext.LocalizationService;
var langUk = new Language("en-UK") { IsDefaultVariantLanguage = true };
var langUk = new Language("en-UK") { IsDefault = true };
var langFr = new Language("fr-FR");
languageService.Save(langFr);
@@ -2524,7 +2524,7 @@ namespace Umbraco.Tests.Services
{
var languageService = ServiceContext.LocalizationService;
var langUk = new Language("en-UK") { IsDefaultVariantLanguage = true };
var langUk = new Language("en-UK") { IsDefault = true };
var langFr = new Language("fr-FR");
languageService.Save(langFr);
@@ -2562,7 +2562,7 @@ namespace Umbraco.Tests.Services
var languageService = ServiceContext.LocalizationService;
//var langFr = new Language("fr-FR") { IsDefaultVariantLanguage = true };
var langXx = new Language("pt-PT") { IsDefaultVariantLanguage = true };
var langXx = new Language("pt-PT") { IsDefault = true };
var langFr = new Language("fr-FR");
var langUk = new Language("en-UK");
var langDe = new Language("de-DE");
@@ -192,6 +192,20 @@ namespace Umbraco.Tests.Services
Assert.Null(language);
}
[Test]
public void Can_Delete_Language_Used_As_Fallback()
{
var danish = ServiceContext.LocalizationService.GetLanguageByIsoCode("da-DK");
var norwegian = new Language("nb-NO") { CultureName = "Norwegian", FallbackLanguageId = danish.Id };
ServiceContext.LocalizationService.Save(norwegian, 0);
var languageId = danish.Id;
ServiceContext.LocalizationService.Delete(danish);
var language = ServiceContext.LocalizationService.GetLanguageById(languageId);
Assert.Null(language);
}
[Test]
public void Can_Create_DictionaryItem_At_Root()
{
@@ -362,21 +376,21 @@ namespace Umbraco.Tests.Services
{
var localizationService = ServiceContext.LocalizationService;
var language = new Core.Models.Language("en-AU");
language.IsDefaultVariantLanguage = true;
language.IsDefault = true;
localizationService.Save(language);
var result = localizationService.GetLanguageById(language.Id);
Assert.IsTrue(result.IsDefaultVariantLanguage);
Assert.IsTrue(result.IsDefault);
var language2 = new Core.Models.Language("en-NZ");
language2.IsDefaultVariantLanguage = true;
language2.IsDefault = true;
localizationService.Save(language2);
var result2 = localizationService.GetLanguageById(language2.Id);
//re-get
result = localizationService.GetLanguageById(language.Id);
Assert.IsTrue(result2.IsDefaultVariantLanguage);
Assert.IsFalse(result.IsDefaultVariantLanguage);
Assert.IsTrue(result2.IsDefault);
Assert.IsFalse(result.IsDefault);
}
[Test]
@@ -3,9 +3,11 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using Moq;
using NUnit.Framework;
using Umbraco.Core;
using Umbraco.Core.Composing;
using Umbraco.Core.Configuration.UmbracoSettings;
using Umbraco.Core.Strings;
using Umbraco.Tests.TestHelpers;
using Umbraco.Tests.Testing;
@@ -30,6 +32,14 @@ namespace Umbraco.Tests.Strings
Assert.IsInstanceOf<MockShortStringHelper>(helper);
}
[TestCase("hello-world.png", "Hello World")]
[TestCase("hello-world .png", "Hello World")]
[TestCase("_hello-world __1.png", "Hello World 1")]
public void To_Friendly_Name(string first, string second)
{
Assert.AreEqual(first.ToFriendlyName(), second);
}
[TestCase("hello", "world", false)]
[TestCase("hello", "hello", true)]
[TestCase("hellohellohellohellohellohellohello", "hellohellohellohellohellohellohelloo", false)]
@@ -172,6 +172,11 @@ namespace Umbraco.Tests.Testing
Container.RegisterSingleton<ILogger>(f => new SerilogLogger(new FileInfo(TestHelper.MapPathForTest("~/unit-test.config"))));
Container.RegisterSingleton<IProfiler>(f => new LogProfiler(f.GetInstance<ILogger>()));
}
else if (option == UmbracoTestOptions.Logger.Console)
{
Container.RegisterSingleton<ILogger>(f => new ConsoleLogger());
Container.RegisterSingleton<IProfiler>(f => new LogProfiler(f.GetInstance<ILogger>()));
}
Container.RegisterSingleton(f => new ProfilingLogger(f.GetInstance<ILogger>(), f.GetInstance<IProfiler>()));
}
@@ -7,7 +7,9 @@
// pure mocks
Mock,
// Serilog for tests
Serilog
Serilog,
// console logger
Console
}
public enum Database
+5
View File
@@ -78,6 +78,9 @@
<PackageReference Include="AutoMapper" Version="7.0.1" />
<PackageReference Include="Castle.Core" Version="4.2.1" />
<PackageReference Include="Examine" Version="1.0.0-beta025" />
<PackageReference Include="HtmlAgilityPack">
<Version>1.8.9</Version>
</PackageReference>
<PackageReference Include="LightInject" Version="5.1.2" />
<PackageReference Include="LightInject.Annotation" Version="1.1.0" />
<PackageReference Include="Lucene.Net" Version="3.0.3" />
@@ -120,6 +123,8 @@
<Compile Include="Migrations\MigrationTests.cs" />
<Compile Include="Models\PathValidationTests.cs" />
<Compile Include="Models\VariationTests.cs" />
<Compile Include="PublishedContent\PublishedContentLanguageVariantTests.cs" />
<Compile Include="PublishedContent\PublishedContentSnapshotTestBase.cs" />
<Compile Include="PublishedContent\SolidPublishedSnapshot.cs" />
<Compile Include="PublishedContent\NuCacheTests.cs" />
<Compile Include="Testing\Objects\TestDataSource.cs" />
@@ -1,8 +1,8 @@
using System;
using System.Globalization;
using System.Linq;
using System.Web;
using LightInject;
using HtmlAgilityPack;
using Moq;
using NUnit.Framework;
using Umbraco.Core;
@@ -12,7 +12,6 @@ using Umbraco.Core.Configuration.UmbracoSettings;
using Umbraco.Core.Logging;
using Umbraco.Core.Models;
using Umbraco.Core.Models.PublishedContent;
using Umbraco.Core.Services;
using Umbraco.Tests.TestHelpers;
using Umbraco.Tests.TestHelpers.Stubs;
using Umbraco.Tests.Testing.Objects.Accessors;
@@ -21,6 +20,8 @@ using Umbraco.Web.PublishedCache;
using Umbraco.Web.Routing;
using Umbraco.Web.Security;
using Umbraco.Web.Templates;
using System.Linq;
using Umbraco.Core.Services;
namespace Umbraco.Tests.Web
{
@@ -62,6 +63,14 @@ namespace Umbraco.Tests.Web
[TestCase("hello href=\"{localLink:umb://document-type/9931BDE0AAC34BABB838909A7B47570E}\" world ", "hello href=\"/my-test-url\" world ")]
//this one has an invalid char so won't match
[TestCase("hello href=\"{localLink:umb^://document-type/9931BDE0-AAC3-4BAB-B838-909A7B47570E}\" world ", "hello href=\"{localLink:umb^://document-type/9931BDE0-AAC3-4BAB-B838-909A7B47570E}\" world ")]
// with a-tag with data-udi attribute, that needs to be stripped
[TestCase("hello <a data-udi=\"umb://document-type/9931BDE0-AAC3-4BAB-B838-909A7B47570\" href=\"{localLink:umb://document-type/9931BDE0-AAC3-4BAB-B838-909A7B47570E}\"> world</a> ", "hello <a href=\"/my-test-url\"> world</a> ")]
// with a-tag with data-udi attribute spelled wrong, so don't need stripping
[TestCase("hello <a data-uid=\"umb://document-type/9931BDE0-AAC3-4BAB-B838-909A7B47570\" href=\"{localLink:umb://document-type/9931BDE0-AAC3-4BAB-B838-909A7B47570E}\"> world</a> ", "hello <a data-uid=\"umb://document-type/9931BDE0-AAC3-4BAB-B838-909A7B47570\" href=\"/my-test-url\"> world</a> ")]
// with a img-tag with data-udi id, that needs to be strippde
[TestCase("hello <img data-udi=\"umb://document-type/9931BDE0-AAC3-4BAB-B838-909A7B47570\" src=\"imageofcats.jpg\"> world ", "hello <img src=\"imageofcats.jpg\"> world ")]
// with a img-tag with data-udi id spelled wrong, so don't need stripping
[TestCase("hello <img data-uid=\"umb://document-type/9931BDE0-AAC3-4BAB-B838-909A7B47570\" src=\"imageofcats.jpg\"> world ", "hello <img data-uid=\"umb://document-type/9931BDE0-AAC3-4BAB-B838-909A7B47570\" src=\"imageofcats.jpg\"> world ")]
public void ParseLocalLinks(string input, string result)
{
var serviceCtxMock = new TestObjects(null).GetServiceContextMock();
@@ -102,7 +111,7 @@ namespace Umbraco.Tests.Web
//setup a quick mock of the WebRouting section
Mock.Of<IUmbracoSettingsSection>(section => section.WebRouting == Mock.Of<IWebRoutingSection>(routingSection => routingSection.UrlProviderMode == "AutoLegacy")),
//pass in the custom url provider
new[]{ testUrlProvider.Object },
new[] { testUrlProvider.Object },
globalSettings,
new TestVariationContextAccessor(),
true))
@@ -112,5 +121,27 @@ namespace Umbraco.Tests.Web
Assert.AreEqual(result, output);
}
}
[Test]
public void StripDataUdiAttributesUsingSrtringOnLinks()
{
var input = "hello <a data-udi=\"umb://document-type/9931BDE0-AAC3-4BAB-B838-909A7B47570\" href=\"/my-test-url\"> world</a> ";
var expected = "hello <a href=\"/my-test-url\"> world</a> ";
var result = TemplateUtilities.StripUdiDataAttributes(input);
Assert.AreEqual(expected, result);
}
[Test]
public void StripDataUdiAttributesUsingStringOnImages()
{
var input = "hello <img data-udi=\"umb://document-type/9931BDE0-AAC3-4BAB-B838-909A7B47570\" src=\"imageofcats.jpg\"> world ";
var expected = "hello <img src=\"imageofcats.jpg\"> world ";
var result = TemplateUtilities.StripUdiDataAttributes(input);
Assert.AreEqual(expected, result);
}
}
}
+7 -7
View File
@@ -16,13 +16,13 @@
"tests"
],
"dependencies": {
"angular": "~1.7.2",
"angular-cookies": "~1.7.2",
"angular-sanitize": "~1.7.2",
"angular-touch": "~1.7.2",
"angular-route": "~1.7.2",
"angular-animate": "~1.7.2",
"angular-i18n": "~1.7.2",
"angular": "~1.7.4",
"angular-cookies": "~1.7.4",
"angular-sanitize": "~1.7.4",
"angular-touch": "~1.7.4",
"angular-route": "~1.7.4",
"angular-animate": "~1.7.4",
"angular-i18n": "~1.7.4",
"signalr": "^2.2.1",
"typeahead.js": "~0.10.5",
"underscore": "~1.9.1",
+6012 -3181
View File
File diff suppressed because it is too large Load Diff
+3 -16
View File
@@ -1,25 +1,12 @@
{
"author": "Umbraco HQ",
"name": "umbraco",
"homepage": "https://github.com/umbraco/umbraco-cms/",
"version": "0.0.0",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/umbraco/Umbraco-CMS.git"
},
"bugs": {
"url": "https://issues.umbraco.org"
},
"engines": {
"node": ">= 0.8.4"
},
"scripts": {
"install": "bower-installer",
"test": "karma start test/config/karma.conf.js --singlerun",
"build": "gulp"
},
"dependencies": {},
"dependencies": {
"npm": "^6.4.1"
},
"devDependencies": {
"autoprefixer": "^6.5.0",
"bower-installer": "^1.2.0",
@@ -3,7 +3,7 @@
function ContentEditController($rootScope, $scope, $routeParams, $q, $window,
appState, contentResource, entityResource, navigationService, notificationsService,
serverValidationManager, contentEditingHelper, treeService, formHelper, umbRequestHelper,
serverValidationManager, contentEditingHelper, treeService, formHelper, umbRequestHelper,
editorState, $http, eventsService, relationResource, overlayService) {
var evts = [];
@@ -56,11 +56,11 @@
}
bindEvents();
resetVariantFlags();
resetVariantFlags();
}
/**
* This will reset isDirty flags if save is true.
* When working with multiple variants, this will set the save/publish flags of each one to false.
@@ -85,7 +85,7 @@
$scope.content.variants[0].publish = false;
}
}
/** Returns true if the save/publish dialog should be shown when pressing the button */
function showSaveOrPublishDialog() {
return $scope.content.variants.length > 1;
@@ -152,15 +152,24 @@
*/
function createButtons(content, app) {
// only create the save/publish/preview buttons if the
// only create the save/publish/preview buttons if the
// content app is "Conent"
if(app && app.alias !== "umbContent" && app.alias !== "umbInfo") {
$scope.defaultButton = null;
$scope.subButtons = null;
$scope.page.showSaveButton = false;
$scope.page.showPreviewButton = false;
return;
}
// create the save button
if(_.contains($scope.content.allowedActions, "A")) {
$scope.page.showSaveButton = true;
// add ellipsis to the save button if it opens the variant overlay
$scope.page.saveButtonEllipsis = content.variants && content.variants.length > 1 ? "true" : "false";
}
// create the pubish combo button
$scope.page.buttonGroupState = "init";
var buttons = contentEditingHelper.configureContentEditorButtons({
create: $scope.page.isNew,
@@ -168,11 +177,10 @@
methods: {
saveAndPublish: $scope.saveAndPublish,
sendToPublish: $scope.sendToPublish,
save: $scope.save,
unPublish: $scope.unPublish
unpublish: $scope.unpublish
}
});
$scope.defaultButton = buttons.defaultButton;
$scope.subButtons = buttons.subButtons;
$scope.page.showPreviewButton = true;
@@ -202,7 +210,7 @@
if (infiniteMode || !path) {
return;
}
if (!$scope.content.isChildOfListView) {
navigationService.syncTree({ tree: $scope.treeAlias, path: path.split(","), forceReload: initialLoad !== true }).then(function (syncArgs) {
$scope.page.menu.currentNode = syncArgs.node;
@@ -213,7 +221,7 @@
//it's a child item, just sync the ui node to the parent
navigationService.syncTree({ tree: $scope.treeAlias, path: path.substring(0, path.lastIndexOf(",")).split(","), forceReload: initialLoad !== true });
//if this is a child of a list view and it's the initial load of the editor, we need to get the tree node
//if this is a child of a list view and it's the initial load of the editor, we need to get the tree node
// from the server so that we can load in the actions menu.
umbRequestHelper.resourcePromise(
$http.get(content.treeNodeUrl),
@@ -223,11 +231,89 @@
}
}
function checkValidility(){
//Get all controls from the 'contentForm'
var allControls = $scope.contentForm.$getControls();
//An array to store items in when we find child form fields (no matter how many deep nested forms)
var childFieldsToMarkAsValid = [];
//Exclude known formControls 'contentHeaderForm' and 'tabbedContentForm'
//Check property - $name === "contentHeaderForm"
allControls = _.filter(allControls, function(obj){
return obj.$name !== 'contentHeaderForm' && obj.$name !== 'tabbedContentForm' && obj.hasOwnProperty('$submitted');
});
for (var i = 0; i < allControls.length; i++) {
var nestedForm = allControls[i];
//Get Nested Controls of this form in the loop
var nestedFormControls = nestedForm.$getControls();
//Need to recurse through controls (could be more nested forms)
childFieldsToMarkAsValid = recurseFormControls(nestedFormControls, childFieldsToMarkAsValid);
}
return childFieldsToMarkAsValid;
}
//Controls is the
function recurseFormControls(controls, array){
//Loop over the controls
for (var i = 0; i < controls.length; i++) {
var controlItem = controls[i];
//Check if the controlItem has a property ''
if(controlItem.hasOwnProperty('$submitted')){
//This item is a form - so lets get the child controls of it & recurse again
var childFormControls = controlItem.$getControls();
recurseFormControls(childFormControls, array);
}
else {
//We can assume its a field on a form
if(controlItem.hasOwnProperty('$error')){
//Set the validlity of the error/s to be valid
//String of keys of error invalid messages
var errorKeys = [];
for(var key in controlItem.$error){
errorKeys.push(key);
controlItem.$setValidity(key, true);
}
//Create a basic obj - storing the control item & the error keys
var obj = { 'control': controlItem, 'errorKeys': errorKeys };
//Push the updated control into the array - so we can set them back
array.push(obj);
}
}
}
return array;
}
function resetNestedFieldValiation(array){
for (var i = 0; i < array.length; i++) {
var item = array[i];
//Item is an object containing two props
//'control' (obj) & 'errorKeys' (string array)
var fieldControl = item.control;
var fieldErrorKeys = item.errorKeys;
for(var i = 0; i < fieldErrorKeys.length; i++) {
fieldControl.$setValidity(fieldErrorKeys[i], false);
}
}
}
// This is a helper method to reduce the amount of code repitition for actions: Save, Publish, SendToPublish
function performSave(args) {
$scope.page.buttonGroupState = "busy";
//Used to check validility of nested form - coming from Content Apps mostly
//Set them all to be invalid
var fieldsToRollback = checkValidility();
eventsService.emit("content.saving", { content: $scope.content, action: args.action });
return contentEditingHelper.contentEditorPerformSave({
@@ -237,19 +323,17 @@
action: args.action,
showNotifications: args.showNotifications
}).then(function (data) {
//success
//success
init($scope.content);
syncTreeNode($scope.content, data.path);
$scope.page.buttonGroupState = "success";
eventsService.emit("content.saved", { content: $scope.content, action: args.action });
resetNestedFieldValiation(fieldsToRollback);
return $q.when(data);
},
function (err) {
syncTreeNode($scope.content, $scope.content.path);
//error
@@ -257,7 +341,7 @@
editorState.set($scope.content);
}
$scope.page.buttonGroupState = "error";
resetNestedFieldValiation(fieldsToRollback);
return $q.reject(err);
});
@@ -332,52 +416,45 @@
});
}
$scope.unPublish = function () {
$scope.unpublish = function() {
clearNotifications($scope.content);
if (formHelper.submitForm({ scope: $scope, action: "unpublish", skipValidation: true })) {
var dialog = {
parentScope: $scope,
view: "views/content/overlays/unpublish.html",
variants: $scope.content.variants, //set a model property for the dialog
skipFormValidation: true, //when submitting the overlay form, skip any client side validation
submitButtonLabelKey: "content_unpublish",
submit: function (model) {
//if there's any variants than we need to set the language and include the variants to publish
var culture = null;
if ($scope.content.variants.length > 0) {
_.each($scope.content.variants,
function (d) {
//set the culture if this is active
if (d.active === true) {
culture = d.language.culture;
}
});
model.submitButtonState = "busy";
var selectedVariants = _.filter(model.variants, function(variant) { return variant.save; });
var culturesForUnpublishing = _.map(selectedVariants, function(variant) { return variant.language.culture; });
contentResource.unpublish($scope.content.id, culturesForUnpublishing)
.then(function (data) {
formHelper.resetForm({ scope: $scope });
contentEditingHelper.reBindChangedProperties($scope.content, data);
init($scope.content);
syncTreeNode($scope.content, data.path);
$scope.page.buttonGroupState = "success";
eventsService.emit("content.unpublished", { content: $scope.content });
overlayService.close();
}, function (err) {
$scope.page.buttonGroupState = 'error';
});
},
close: function () {
overlayService.close();
}
};
overlayService.open(dialog);
}
if (formHelper.submitForm({ scope: $scope, skipValidation: true })) {
$scope.page.buttonGroupState = "busy";
eventsService.emit("content.unpublishing", { content: $scope.content });
contentResource.unPublish($scope.content.id, culture)
.then(function (data) {
formHelper.resetForm({ scope: $scope });
contentEditingHelper.handleSuccessfulSave({
scope: $scope,
savedContent: data,
rebindCallback: contentEditingHelper.reBindChangedProperties($scope.content, data)
});
init($scope.content);
syncTreeNode($scope.content, data.path);
$scope.page.buttonGroupState = "success";
eventsService.emit("content.unpublished", { content: $scope.content });
}, function (err) {
$scope.page.buttonGroupState = 'error';
});
}
};
$scope.sendToPublish = function () {
clearNotifications($scope.content);
if (showSaveOrPublishDialog()) {
@@ -400,18 +477,25 @@
overlayService.close();
}
};
overlayService.open(dialog);
}
}
else {
return performSave({ saveMethod: contentResource.sendToPublish, action: "sendToPublish" });
$scope.page.buttonGroupState = "busy";
return performSave({
saveMethod: contentResource.sendToPublish,
action: "sendToPublish"
}).then(function(){
$scope.page.buttonGroupState = "success";
}, function () {
$scope.page.buttonGroupState = "error";
});;
}
};
$scope.saveAndPublish = function () {
clearNotifications($scope.content);
// TODO: Add "..." to publish button label if there are more than one variant to publish - currently it just adds the elipses if there's more than 1 variant
if (showSaveOrPublishDialog()) {
//before we launch the dialog we want to execute all client side validations first
if (formHelper.submitForm({ scope: $scope, action: "publish" })) {
@@ -457,7 +541,15 @@
else {
//ensure the publish flag is set
$scope.content.variants[0].publish = true;
return performSave({ saveMethod: contentResource.publish, action: "publish" });
$scope.page.buttonGroupState = "busy";
return performSave({
saveMethod: contentResource.publish,
action: "publish"
}).then(function(){
$scope.page.buttonGroupState = "success";
}, function () {
$scope.page.buttonGroupState = "error";
});;
}
};
@@ -488,15 +580,14 @@
clearNotifications($scope.content);
overlayService.close();
return $q.when(data);
},
function (err) {
clearDirtyState($scope.content.variants);
model.submitButtonState = "error";
//re-map the dialog model since we've re-bound the properties
dialog.variants = $scope.content.variants;
//don't reject, we've handled the error
return $q.when(err);
});
}, function (err) {
clearDirtyState($scope.content.variants);
model.submitButtonState = "error";
//re-map the dialog model since we've re-bound the properties
dialog.variants = $scope.content.variants;
//don't reject, we've handled the error
return $q.when(err);
});
},
close: function (oldModel) {
overlayService.close();
@@ -507,7 +598,15 @@
}
}
else {
return performSave({ saveMethod: $scope.saveMethod(), action: "save" });
$scope.page.saveButtonState = "busy";
return performSave({
saveMethod: $scope.saveMethod(),
action: "save"
}).then(function(){
$scope.page.saveButtonState = "success";
}, function () {
$scope.page.saveButtonState = "error";
});
}
};
@@ -517,9 +616,9 @@
if (!$scope.busy) {
// Chromes popup blocker will kick in if a window is opened
// Chromes popup blocker will kick in if a window is opened
// without the initial scoped request. This trick will fix that.
//
//
var previewWindow = $window.open('preview/?init=true', 'umbpreview');
// Build the correct path so both /#/ and #/ work.
@@ -1,7 +1,7 @@
(function () {
'use strict';
function ContentNodeInfoDirective($timeout, $location, logResource, eventsService, userService, localizationService, dateHelper, editorService) {
function ContentNodeInfoDirective($timeout, $location, logResource, eventsService, userService, localizationService, dateHelper, editorService, redirectUrlsResource) {
function link(scope, element, attrs, ctrl) {
@@ -72,6 +72,9 @@
// make sure dates are formatted to the user's locale
formatDatesToLocal();
//default setting for redirect url management
scope.urlTrackerDisabled = false;
// Declare a fallback URL for the <umb-node-preview/> directive
if (scope.documentType !== null) {
scope.previewOpenUrl = '#/settings/documenttypes/edit/' + scope.documentType.id;
@@ -139,7 +142,7 @@
// get current backoffice user and format dates
userService.getCurrentUser().then(function (currentUser) {
angular.forEach(data.items, function(item) {
angular.forEach(data.items, function (item) {
item.timestampFormatted = dateHelper.getLocalDate(item.timestamp, currentUser.locale, 'LLL');
});
});
@@ -156,6 +159,25 @@
});
}
function loadRedirectUrls() {
scope.loadingRedirectUrls = true;
//check if Redirect Url Management is enabled
redirectUrlsResource.getEnableState().then(function (response) {
scope.urlTrackerDisabled = response.enabled !== true;
if (scope.urlTrackerDisabled === false) {
redirectUrlsResource.getRedirectsForContentItem(scope.node.udi)
.then(function (data) {
scope.redirectUrls = data.searchResults;
scope.hasRedirects = (typeof data.searchResults !== 'undefined' && data.searchResults.length > 0);
scope.loadingRedirectUrls = false;
});
}
else {
scope.loadingRedirectUrls = false;
}
});
}
function setAuditTrailLogTypeColor(auditTrail) {
angular.forEach(auditTrail, function (item) {
@@ -164,7 +186,7 @@
case "Publish":
item.logTypeColor = "success";
break;
case "UnPublish":
case "Unpublish":
case "Delete":
item.logTypeColor = "danger";
break;
@@ -312,12 +334,13 @@
});
}
// load audit trail when on the info tab
// load audit trail and redirects when on the info tab
evts.push(eventsService.on("app.tabChange", function (event, args) {
$timeout(function(){
$timeout(function () {
if (args.alias === "umbInfo") {
isInfoTab = true;
loadAuditTrail();
loadRedirectUrls();
} else {
isInfoTab = false;
}
@@ -325,13 +348,14 @@
}));
// watch for content state updates
scope.$watch('node.updateDate', function(newValue, oldValue){
scope.$watch('node.updateDate', function (newValue, oldValue) {
if(!newValue) { return; }
if(newValue === oldValue) { return; }
if (!newValue) { return; }
if (newValue === oldValue) { return; }
if(isInfoTab) {
loadAuditTrail();
loadRedirectUrls();
formatDatesToLocal();
setNodePublishStatus(scope.node);
}
@@ -186,14 +186,15 @@
} else {
vm.openVariants[editorIndex] = variant.language.culture;
}
}
//then assign the variant to a view model to the content app
var contentApp = _.find(variant.apps, function (a) {
return a.alias === "umbContent";
});
contentApp.viewModel = variant;
contentApp.viewModel = _.omit(variant, 'apps');
// make sure the same app it set to active in the new variant
if(activeAppAlias) {
@@ -11,32 +11,36 @@ function hexBgColor() {
restrict: "A",
link: function (scope, element, attr, formCtrl) {
var origColor = null;
if (attr.hexBgOrig) {
//set the orig based on the attribute if there is one
origColor = attr.hexBgOrig;
}
attr.$observe("hexBgColor", function (newVal) {
if (newVal) {
if (!origColor) {
//get the orig color before changing it
origColor = element.css("border-color");
}
//validate it - test with and without the leading hash.
if (/^([0-9a-f]{3}|[0-9a-f]{6})$/i.test(newVal)) {
element.css("background-color", "#" + newVal);
return;
}
if (/^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(newVal)) {
element.css("background-color", newVal);
return;
}
}
element.css("background-color", origColor);
});
// Only add inline hex background color if defined and not "true".
if (attr.hexBgInline === undefined || (attr.hexBgInline !== undefined && attr.hexBgInline === "true")) {
var origColor = null;
if (attr.hexBgOrig) {
// Set the orig based on the attribute if there is one.
origColor = attr.hexBgOrig;
}
attr.$observe("hexBgColor", function (newVal) {
if (newVal) {
if (!origColor) {
// Get the orig color before changing it.
origColor = element.css("border-color");
}
// Validate it - test with and without the leading hash.
if (/^([0-9a-f]{3}|[0-9a-f]{6})$/i.test(newVal)) {
element.css("background-color", "#" + newVal);
return;
}
if (/^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(newVal)) {
element.css("background-color", newVal);
return;
}
}
element.css("background-color", origColor);
});
}
}
};
}
angular.module('umbraco.directives').directive("hexBgColor", hexBgColor);
angular.module('umbraco.directives').directive("hexBgColor", hexBgColor);
@@ -41,7 +41,6 @@
};
function setFocalPoint (event) {
$scope.$emit("imageFocalPointStart");
var offsetX = event.offsetX - 10;
@@ -50,7 +49,6 @@
calculateGravity(offsetX, offsetY);
lazyEndEvent();
};
/** Initializes the component */
@@ -148,12 +146,26 @@
/** Sets the width/height/left/top dimentions based on the image size and the "center" value */
function setDimensions() {
if (htmlImage && vm.center) {
if (vm.src.endsWith(".svg")) {
// svg files don't automatically get a size by
// loading them set a default size for now
vm.dimensions.width = 200;
vm.dimensions.height = 200;
vm.dimensions.left = vm.center.left * vm.dimensions.width - 10;
vm.dimensions.top = vm.center.top * vm.dimensions.height - 10;
// can't crop an svg file, don't show the focal point
if (htmlOverlay) {
htmlOverlay.remove();
}
}
else if (htmlImage && vm.center) {
vm.dimensions.width = htmlImage.width();
vm.dimensions.height = htmlImage.height();
vm.dimensions.left = vm.center.left * vm.dimensions.width - 10;
vm.dimensions.top = vm.center.top * vm.dimensions.height - 10;
}
return vm.dimensions.width;
};
@@ -73,7 +73,15 @@ angular.module("umbraco.directives")
if (node.selected) {
css.push("umb-tree-node-checked");
}
//is this the current action node (this is not the same as the current selected node!)
var actionNode = appState.getMenuState("currentNode");
if (actionNode) {
if (actionNode.id === node.id) {
css.push("active");
}
}
return css.join(" ");
};
@@ -1,5 +1,4 @@
/**
/**
@ngdoc directive
@name umbraco.directives.directive:umbColorSwatches
@restrict E
@@ -15,9 +14,10 @@ Use this directive to generate color swatches to pick from.
</umb-color-swatches>
</pre>
@param {array} colors (<code>attribute</code>): The array of colors.
@param {string} colors (<code>attribute</code>): The array of colors.
@param {string} selectedColor (<code>attribute</code>): The selected color.
@param {string} size (<code>attribute</code>): The size (s, m).
@param {string} useLabel (<code>attribute</code>): Specify if labels should be used.
@param {string} useColorClass (<code>attribute</code>): Specify if color values are css classes.
@param {function} onSelect (<code>expression</code>): Callback function when the item is selected.
**/
@@ -28,6 +28,11 @@ Use this directive to generate color swatches to pick from.
function link(scope, el, attr, ctrl) {
// Set default to true if not defined
if (angular.isUndefined(scope.useColorClass)) {
scope.useColorClass = false;
}
scope.setColor = function (color) {
scope.selectedColor = color;
if (scope.onSelect) {
@@ -45,7 +50,9 @@ Use this directive to generate color swatches to pick from.
colors: '=?',
size: '@',
selectedColor: '=',
onSelect: '&'
onSelect: '&',
useLabel: '=',
useColorClass: '=?'
},
link: link
};
@@ -125,6 +125,14 @@ Use this directive to generate a thumbnail grid of media items.
i--;
}
if (scope.includeSubFolders !== 'true') {
if (item.parentId !== parseInt(scope.currentFolderId)) {
scope.items.splice(i, 1);
i--;
}
}
}
if (scope.items.length > 0) {
@@ -316,7 +324,9 @@ Use this directive to generate a thumbnail grid of media items.
itemMaxHeight: "@",
itemMinWidth: "@",
itemMinHeight: "@",
onlyImages: "@"
onlyImages: "@",
includeSubFolders: "@",
currentFolderId: "@"
},
link: link
};
@@ -221,7 +221,8 @@
isClientSide: true
});
newVal += files[i].name + ",";
//special check for a comma in the name
newVal += files[i].name.replace(',', '-') + ",";
if (isImage) {
@@ -3,7 +3,7 @@
* @name umbraco.directives.directive:valFormManager
* @restrict A
* @require formController
* @description Used to broadcast an event to all elements inside this one to notify that form validation has
* @description Used to broadcast an event to all elements inside this one to notify that form validation has
* changed. If we don't use this that means you have to put a watch for each directive on a form's validation
* changing which would result in much higher processing. We need to actually watch the whole $error collection of a form
* because just watching $valid or $invalid doesn't acurrately trigger form validation changing.
@@ -19,7 +19,7 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location
var SAVED_EVENT_NAME = "formSubmitted";
return {
require: ["form", "^^?valFormManager"],
require: ["form", "^^?valFormManager", "^^?valSubView"],
restrict: "A",
controller: function($scope) {
//This exposes an API for direct use with this directive
@@ -46,9 +46,15 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location
},
link: function (scope, element, attr, ctrls) {
function notifySubView() {
if (subView){
subView.valStatusChanged({ form: formCtrl, showValidation: scope.showValidation });
}
}
var formCtrl = ctrls[0];
var parentFormMgr = ctrls.length > 0 ? ctrls[1] : null;
var subView = ctrls.length > 1 ? ctrls[2] : null;
var labels = {};
var labelKeys = [
@@ -83,7 +89,9 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location
return sum;
}, function (e) {
scope.$broadcast("valStatusChanged", { form: formCtrl });
notifySubView();
//find all invalid elements' .control-group's and apply the error class
var inError = element.find(".control-group .ng-invalid").closest(".control-group");
inError.addClass("error");
@@ -94,7 +102,7 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location
});
//This tracks if the user is currently saving a new item, we use this to determine
//This tracks if the user is currently saving a new item, we use this to determine
// if we should display the warning dialog that they are leaving the page - if a new item
// is being saved we never want to display that dialog, this will also cause problems when there
// are server side validation issues.
@@ -104,6 +112,7 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location
if (serverValidationManager.items.length > 0 || (parentFormMgr && parentFormMgr.showValidation)) {
element.addClass(SHOW_VALIDATION_CLASS_NAME);
scope.showValidation = true;
notifySubView();
}
var unsubscribe = [];
@@ -112,6 +121,7 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location
unsubscribe.push(scope.$on(SAVING_EVENT_NAME, function(ev, args) {
element.addClass(SHOW_VALIDATION_CLASS_NAME);
scope.showValidation = true;
notifySubView();
//set the flag so we can check to see if we should display the error.
isSavingNewItem = $routeParams.create;
}));
@@ -121,8 +131,9 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location
//remove validation class
element.removeClass(SHOW_VALIDATION_CLASS_NAME);
scope.showValidation = false;
notifySubView();
//clear form state as at this point we retrieve new data from the server
//and all validation will have cleared at this point
//and all validation will have cleared at this point
formCtrl.$setPristine();
}));
@@ -203,7 +214,7 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location
$timeout(function(){
formCtrl.$setPristine();
}, 1000);
}
};
}
@@ -10,6 +10,29 @@
function valSubViewDirective() {
function controller($scope, $element) {
//expose api
return {
valStatusChanged: function(args) {
if (!args.form.$valid) {
var subViewContent = $element.find(".ng-invalid");
if (subViewContent.length > 0) {
$scope.model.hasError = true;
$scope.model.errorClass = args.showValidation ? 'show-validation' : null;
} else {
$scope.model.hasError = false;
$scope.model.errorClass = null;
}
}
else {
$scope.model.hasError = false;
$scope.model.errorClass = null;
}
}
}
}
function link(scope, el, attr, ctrl) {
//if there are no containing form or valFormManager controllers, then we do nothing
@@ -43,7 +66,8 @@
var directive = {
require: ['?^^form', '?^^valFormManager'],
restrict: "A",
link: link
link: link,
controller: controller
};
return directive;
@@ -145,7 +145,7 @@ angular.module('umbraco.mocks').
"content_statistics": "Statistics",
"content_titleOptional": "Title (optional)",
"content_type": "Type",
"content_unPublish": "Unpublish",
"content_unpublish": "Unpublish",
"content_updateDate": "Last edited",
"content_updateDateDesc": "Date/time this document was created",
"content_uploadClear": "Remove file",
@@ -206,7 +206,7 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) {
/**
* @ngdoc method
* @name umbraco.resources.contentResource#unPublish
* @name umbraco.resources.contentResource#unpublish
* @methodOf umbraco.resources.contentResource
*
* @description
@@ -214,7 +214,7 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) {
*
* ##usage
* <pre>
* contentResource.unPublish(1234)
* contentResource.unpublish(1234)
* .then(function() {
* alert("node was unpulished");
* }, function(err){
@@ -225,21 +225,20 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) {
* @returns {Promise} resourcePromise object.
*
*/
unPublish: function (id, culture) {
unpublish: function (id, cultures) {
if (!id) {
throw "id cannot be null";
}
if (!culture) {
culture = null;
if (!cultures) {
cultures = [];
}
return umbRequestHelper.resourcePromise(
$http.post(
umbRequestHelper.getApiUrl(
"contentApiBaseUrl",
"PostUnPublish",
{ id: id, culture: culture })),
"PostUnpublish"), { id: id, cultures: cultures }),
'Failed to publish content with id ' + id);
},
/**
@@ -7,10 +7,60 @@
**/
function logResource($q, $http, umbRequestHelper) {
function isValidDate(input) {
if (input) {
if (Object.prototype.toString.call(input) === "[object Date]" && !isNaN(input.getTime())) {
return true;
}
}
return false;
};
function dateToValidIsoString(input) {
if (isValidDate(input)) {
return input.toISOString();
}
return '';
};
//the factory object returned
return {
getPagedEntityLog: function (options) {
/**
* @ngdoc method
* @name umbraco.resources.logResource#getPagedEntityLog
* @methodOf umbraco.resources.logResource
*
* @description
* Gets a paginated log history for a entity
*
* ##usage
* <pre>
* var options = {
* id : 1234
* pageSize : 10,
* pageNumber : 1,
* orderDirection : "Descending",
* sinceDate : new Date(2018,0,1)
* };
* logResource.getPagedEntityLog(options)
* .then(function(log) {
* alert('its here!');
* });
* </pre>
*
* @param {Object} options options object
* @param {Int} options.id the id of the entity
* @param {Int} options.pageSize if paging data, number of nodes per page, default = 10, set to 0 to disable paging
* @param {Int} options.pageNumber if paging data, current page index, default = 1
* @param {String} options.orderDirection can be `Ascending` or `Descending` - Default: `Descending`
* @param {Date} options.sinceDate if provided this will only get log entries going back to this date
* @returns {Promise} resourcePromise object containing the log.
*
*/
getPagedEntityLog: function(options) {
var defaults = {
pageSize: 10,
@@ -24,11 +74,15 @@ function logResource($q, $http, umbRequestHelper) {
angular.extend(defaults, options);
//now copy back to the options we will use
options = defaults;
if (options.hasOwnProperty('sinceDate')) {
options.sinceDate = dateToValidIsoString(options.sinceDate);
}
//change asc/desct
if (options.orderDirection === "asc") {
options.orderDirection = "Ascending";
}
else if (options.orderDirection === "desc") {
} else if (options.orderDirection === "desc") {
options.orderDirection = "Descending";
}
@@ -45,7 +99,37 @@ function logResource($q, $http, umbRequestHelper) {
'Failed to retrieve log data for id');
},
getPagedUserLog: function (options) {
/**
* @ngdoc method
* @name umbraco.resources.logResource#getPagedUserLog
* @methodOf umbraco.resources.logResource
*
* @description
* Gets a paginated log history for the current user
*
* ##usage
* <pre>
* var options = {
* pageSize : 10,
* pageNumber : 1,
* orderDirection : "Descending",
* sinceDate : new Date(2018,0,1)
* };
* logResource.getPagedUserLog(options)
* .then(function(log) {
* alert('its here!');
* });
* </pre>
*
* @param {Object} options options object
* @param {Int} options.pageSize if paging data, number of nodes per page, default = 10, set to 0 to disable paging
* @param {Int} options.pageNumber if paging data, current page index, default = 1
* @param {String} options.orderDirection can be `Ascending` or `Descending` - Default: `Descending`
* @param {Date} options.sinceDate if provided this will only get log entries going back to this date
* @returns {Promise} resourcePromise object containing the log.
*
*/
getPagedUserLog: function(options) {
var defaults = {
pageSize: 10,
@@ -59,11 +143,15 @@ function logResource($q, $http, umbRequestHelper) {
angular.extend(defaults, options);
//now copy back to the options we will use
options = defaults;
if (options.hasOwnProperty('sinceDate')) {
options.sinceDate = dateToValidIsoString(options.sinceDate);
}
//change asc/desct
if (options.orderDirection === "asc") {
options.orderDirection = "Ascending";
}
else if (options.orderDirection === "desc") {
} else if (options.orderDirection === "desc") {
options.orderDirection = "Descending";
}
@@ -71,7 +159,7 @@ function logResource($q, $http, umbRequestHelper) {
$http.get(
umbRequestHelper.getApiUrl(
"logApiBaseUrl",
"GetPagedEntityLog",
"GetPagedCurrentUserLog",
options)),
'Failed to retrieve log data for id');
},
@@ -82,6 +170,7 @@ function logResource($q, $http, umbRequestHelper) {
* @methodOf umbraco.resources.logResource
*
* @description
* <strong>[OBSOLETE] use getPagedEntityLog instead</strong><br />
* Gets the log history for a give entity id
*
* ##usage
@@ -96,23 +185,24 @@ function logResource($q, $http, umbRequestHelper) {
* @returns {Promise} resourcePromise object containing the log.
*
*/
getEntityLog: function (id) {
getEntityLog: function(id) {
return umbRequestHelper.resourcePromise(
$http.get(
umbRequestHelper.getApiUrl(
"logApiBaseUrl",
"GetEntityLog",
[{ id: id }])),
'Failed to retrieve user data for id ' + id);
$http.get(
umbRequestHelper.getApiUrl(
"logApiBaseUrl",
"GetEntityLog",
[{ id: id }])),
'Failed to retrieve user data for id ' + id);
},
/**
* @ngdoc method
* @name umbraco.resources.logResource#getUserLog
* @methodOf umbraco.resources.logResource
*
* @description
* Gets the current users' log history for a given type of log entry
* <strong>[OBSOLETE] use getPagedUserLog instead</strong><br />
* Gets the current user's log history for a given type of log entry
*
* ##usage
* <pre>
@@ -127,14 +217,14 @@ function logResource($q, $http, umbRequestHelper) {
* @returns {Promise} resourcePromise object containing the log.
*
*/
getUserLog: function (type, since) {
getUserLog: function(type, since) {
return umbRequestHelper.resourcePromise(
$http.get(
umbRequestHelper.getApiUrl(
"logApiBaseUrl",
"GetCurrentUserLog",
[{ logtype: type}, {sinceDate: since }])),
'Failed to retrieve log data for current user of type ' + type + ' since ' + since);
$http.get(
umbRequestHelper.getApiUrl(
"logApiBaseUrl",
"GetCurrentUserLog",
[{ logtype: type }, { sinceDate: dateToValidIsoString(since) }])),
'Failed to retrieve log data for current user of type ' + type + ' since ' + since);
},
/**
@@ -158,16 +248,16 @@ function logResource($q, $http, umbRequestHelper) {
* @returns {Promise} resourcePromise object containing the log.
*
*/
getLog: function (type, since) {
getLog: function(type, since) {
return umbRequestHelper.resourcePromise(
$http.get(
umbRequestHelper.getApiUrl(
"logApiBaseUrl",
"GetLog",
[{ logtype: type}, {sinceDate: since }])),
'Failed to retrieve log data of type ' + type + ' since ' + since);
}
};
$http.get(
umbRequestHelper.getApiUrl(
"logApiBaseUrl",
"GetLog",
[{ logtype: type }, { sinceDate: dateToValidIsoString(since) }])),
'Failed to retrieve log data of type ' + type + ' since ' + since);
}
};
}
angular.module('umbraco.resources').factory('logResource', logResource);
@@ -40,6 +40,32 @@
{ searchTerm: searchTerm, page: pageIndex, pageSize: pageSize })),
'Failed to retrieve data for searching redirect urls');
}
/**
* @ngdoc function
* @name umbraco.resources.redirectUrlResource#getRedirectsForContentItem
* @methodOf umbraco.resources.redirectUrlResource
* @function
*
* @description
* Used to retrieve RedirectUrls for a specific item of content for Information tab
* ##usage
* <pre>
* redirectUrlsResource.getRedirectsForContentItem("udi:123456")
* .then(function(response) {
*
* });
* </pre>
* @param {String} contentUdi identifier for the content item to retrieve redirects for
*/
function getRedirectsForContentItem(contentUdi) {
return umbRequestHelper.resourcePromise(
$http.get(
umbRequestHelper.getApiUrl(
"redirectUrlManagementApiBaseUrl",
"RedirectUrlsForContentItem",
{ contentUdi: contentUdi })),
'Failed to retrieve redirects for content: ' + contentUdi);
}
function getEnableState() {
@@ -50,7 +76,7 @@
"GetEnableState")),
'Failed to retrieve data to check if the 301 redirect is enabled');
}
/**
* @ngdoc function
* @name umbraco.resources.redirectUrlResource#deleteRedirectUrl
@@ -107,7 +133,8 @@
searchRedirectUrls: searchRedirectUrls,
deleteRedirectUrl: deleteRedirectUrl,
toggleUrlTracker: toggleUrlTracker,
getEnableState: getEnableState
getEnableState: getEnableState,
getRedirectsForContentItem: getRedirectsForContentItem
};
return resource;
@@ -65,17 +65,14 @@ angular.module('umbraco.services')
return path;
}
/**
* Loads in moment.js requirements during the _loadInitAssets call
*/
function loadMomentLocaleForCurrentUser() {
function getMomentLocales(locales, supportedLocales) {
var self = this;
function loadLocales(currentUser, supportedLocales) {
var locale = currentUser.locale.toLowerCase();
var localeUrls = [];
var locales = locales.split(',');
for (var i = 0; i < locales.length; i++) {
var locale = locales[i].toString().toLowerCase();
if (locale !== 'en-us') {
var localeUrls = [];
if (supportedLocales.indexOf(locale + '.js') > -1) {
localeUrls.push('lib/moment/' + locale + '.js');
}
@@ -85,16 +82,35 @@ angular.module('umbraco.services')
localeUrls.push('lib/moment/' + majorLocale);
}
}
return self.load(localeUrls, $rootScope);
}
else {
$q.when(true);
}
}
return localeUrls;
}
/**
* Loads specific Moment.js Locales.
* @param {any} locales
* @param {any} supportedLocales
*/
function loadLocales(locales, supportedLocales) {
var localeUrls = getMomentLocales(locales, supportedLocales);
if (localeUrls.length >= 1) {
return assetsService.load(localeUrls, $rootScope);
}
else {
$q.when(true);
}
}
/**
* Loads in moment.js requirements during the _loadInitAssets call
*/
function loadMomentLocaleForCurrentUser() {
userService.getCurrentUser().then(function (currentUser) {
return javascriptLibraryResource.getSupportedLocalesForMoment().then(function (supportedLocales) {
return loadLocales(currentUser, supportedLocales);
return loadLocales(currentUser.locale, supportedLocales);
});
});
@@ -136,7 +152,9 @@ angular.module('umbraco.services')
return $q.when(true);
}
},
loadLocales: loadLocales,
/**
* @ngdoc method
* @name umbraco.services.assetsService#loadCss
@@ -146,7 +146,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica
if (!args.methods) {
throw "args.methods is not defined";
}
if (!args.methods.saveAndPublish || !args.methods.sendToPublish || !args.methods.save || !args.methods.unPublish) {
if (!args.methods.saveAndPublish || !args.methods.sendToPublish || !args.methods.unpublish) {
throw "args.methods does not contain all required defined methods";
}
@@ -179,26 +179,16 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica
alias: "sendToPublish",
addEllipsis: args.content.variants && args.content.variants.length > 1 ? "true" : "false"
};
case "A":
//save
return {
letter: ch,
labelKey: "buttons_save",
handler: args.methods.save,
hotKey: "ctrl+s",
hotKeyWhenHidden: true,
alias: "save",
addEllipsis: args.content.variants && args.content.variants.length > 1 ? "true" : "false"
};
case "Z":
//unpublish
return {
letter: ch,
labelKey: "content_unPublish",
handler: args.methods.unPublish,
labelKey: "content_unpublish",
handler: args.methods.unpublish,
hotKey: "ctrl+u",
hotKeyWhenHidden: true,
alias: "unpublish"
alias: "unpublish",
addEllipsis: args.content.variants && args.content.variants.length > 1 ? "true" : "false"
};
default:
return null;
@@ -209,8 +199,8 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica
buttons.subButtons = [];
//This is the ideal button order but depends on circumstance, we'll use this array to create the button list
// Publish, SendToPublish, Save
var buttonOrder = ["U", "H", "A"];
// Publish, SendToPublish
var buttonOrder = ["U", "H"];
//Create the first button (primary button)
//We cannot have the Save or SaveAndPublish buttons if they don't have create permissions when we are creating a new item.
@@ -252,7 +242,8 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica
// so long as it's already published and if the user has access to publish
// and the user has access to unpublish (may have been removed via Event)
if (!args.create) {
if (args.content.publishDate && _.contains(args.content.allowedActions, "U") && _.contains(args.content.allowedActions, "Z")) {
var hasPublishedVariant = args.content.variants.filter(function(variant) { return (variant.state === "Published" || variant.state === "PublishedPendingChanges"); }).length > 0;
if (hasPublishedVariant && _.contains(args.content.allowedActions, "U") && _.contains(args.content.allowedActions, "Z")) {
buttons.subButtons.push(createButtonDefinition("Z"));
}
}
@@ -412,8 +403,8 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica
case "Z":
return {
letter: ch,
labelKey: "content_unPublish",
handler: "unPublish"
labelKey: "content_unpublish",
handler: "unpublish"
};
default:
@@ -85,6 +85,7 @@ function navigationService($rootScope, $route, $routeParams, $log, $location, $q
appState.setSectionState("showSearchResults", false);
appState.setGlobalState("stickyNavigation", false);
appState.setGlobalState("showTray", false);
appState.setMenuState("currentNode", null);
if (appState.getGlobalState("isTablet") === true) {
appState.setGlobalState("showNavigation", false);
@@ -365,7 +366,8 @@ function navigationService($rootScope, $route, $routeParams, $log, $location, $q
if (appState.getGlobalState("isTablet") === true && !appState.getGlobalState("stickyNavigation")) {
//reset it to whatever is in the url
appState.setSectionState("currentSection", $routeParams.section);
appState.setSectionState("currentSection", $routeParams.section);
setMode("default-hidesectiontree");
}
@@ -143,7 +143,6 @@ angular.module('umbraco.services')
/** Called to update the current user's timeout */
function setUserTimeoutInternal(newTimeout) {
var asNumber = parseFloat(newTimeout);
if (!isNaN(asNumber) && currentUser && angular.isNumber(asNumber)) {
currentUser.remainingAuthSeconds = newTimeout;
@@ -138,6 +138,7 @@
@import "components/umb-querybuilder.less";
@import "components/umb-pagination.less";
@import "components/umb-mini-list-view.less";
@import "components/umb-multiple-textbox.less";
@import "components/umb-badge.less";
@import "components/umb-nested-content.less";
@import "components/umb-checkmark.less";
@@ -227,13 +227,29 @@ input[type="button"] {
font-size: 16px;
border: none;
background: @green;
color: white;
color: @white;
font-weight: bold;
&:hover {
background: @green-d1;
}
}
// outlined
.btn-outline {
border: 1px solid @gray-8;
background: @white;
color: @black;
padding: 5px 13px;
}
.btn-outline:hover,
.btn-outline:focus,
.btn-outline:active {
border-color: @gray-7;
background: transparent;
color: @black;
}
// Cross-browser Jank
// --------------------------------------------------
@@ -790,13 +790,13 @@ h4.panel-title {
}
.smartphone-portrait {
width: 320px;
height: 504px;
width: 360px;
height: 640px;
}
.smartphone-landscape {
width: 480px;
height: 256px;
width: 640px;
height: 360px;
}
.border {
@@ -20,7 +20,7 @@
}
.umb-dashboard__content {
padding: 30px 20px;
padding: 20px;
overflow: auto;
}
@@ -16,12 +16,15 @@
.umb-button-group {
.umb-button__button {
border-radius: 3px 0px 0px 3px;
border-radius: @baseBorderRadius;
}
.umb-button-group__toggle {
border-radius: 0px 3px 3px 0;
border-radius: 0px @baseBorderRadius @baseBorderRadius 0;
border-left: 1px solid rgba(0,0,0,0.09);
margin-left: -2px;
padding-left: 10px;
padding-right: 10px;
}
}
@@ -96,7 +96,6 @@
font-size: 12px;
text-align: center;
width: 100px;
height: 105px;
box-sizing: border-box;
position: relative;
}
@@ -114,6 +113,7 @@
width: 100%;
height: 100%;
border-radius: 3px;
padding-bottom: 5px;
}
@@ -4,7 +4,7 @@
background: @gray-10;
display: flex;
justify-content: space-between;
margin-top: -20px;
margin-top: -10px;
position: relative;
top: 0;
@@ -10,6 +10,7 @@
.umb-prevalues-multivalues__left {
display: flex;
flex: 1 1 auto;
overflow: hidden;
}
.umb-prevalues-multivalues__right {
@@ -94,6 +94,10 @@ body.touch .umb-tree {
flex-wrap: nowrap;
align-items: center;
&.active {
background: @gray-10;
}
&:hover {
background: @gray-10;
@@ -1,18 +1,22 @@
.umb-color-swatches {
display: flex;
flex-flow: row wrap;
.umb-color-box {
border: none;
color: white;
cursor: pointer;
padding: 5px;
padding: 1px;
text-align: center;
text-decoration: none;
display: inline-block;
margin: 5px;
border-radius: 3px;
width: 30px;
height: 30px;
transition: box-shadow .3s;
display: flex;
align-items: center;
justify-content: center;
&:hover, &:focus {
box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24);
@@ -28,4 +32,55 @@
}
}
}
&.with-labels {
.umb-color-box {
width: 120px;
height: 100%;
display: flex;
flex-flow: row wrap;
.umb-color-box-inner {
display: flex;
flex-flow: column wrap;
flex: 0 0 100%;
max-width: 100%;
min-height: 80px;
padding-top: 10px;
.umb-color-box__label {
background: #fff;
font-size: 14px;
display: flex;
flex-flow: column wrap;
flex: 0 0 100%;
padding: 1px 5px;
max-width: 100%;
margin-top: auto;
margin-bottom: -3px;
margin-left: -1px;
margin-right: -1px;
text-indent: 0;
text-align: left;
border: 1px solid @gray-8;
border-bottom-left-radius: 3px;
border-bottom-right-radius: 3px;
overflow: hidden;
.umb-color-box__name {
color: @black;
font-weight: bold;
margin-top: 3px;
}
.umb-color-box__description {
font-size: 12px;
line-height: 1.5em;
color: @gray-3;
}
}
}
}
}
}
@@ -33,6 +33,18 @@
background-color: @white;
}
.umb-media-grid__item-file-icon > span {
color: @white;
background: @gray-4;
padding: 1px 3px;
font-size: 10px;
line-height: 130%;
display: block;
margin-top: -30px;
margin-left: -10px;
position: relative;
}
.umb-media-grid__item.-selected {
box-shadow: 0 2px 8px 0 rgba(0,0,0,.35);
}
@@ -0,0 +1,34 @@
.umb-multiple-textbox .textbox-wrapper {
align-items: center;
margin-bottom: 15px;
}
.umb-multiple-textbox .textbox-wrapper .umb-editor {
margin-bottom: 0;
}
.umb-multiple-textbox .textbox-wrapper i {
margin-right: 5px;
}
.umb-multiple-textbox .textbox-wrapper i.handle {
margin-left: 5px;
}
.umb-multiple-textbox .textbox-wrapper a.remove {
margin-left: 5px;
text-decoration: none;
}
.umb-multiple-textbox .add-link {
&:extend(.umb-node-preview-add);
}
.umb-editor-wrapper .umb-multiple-textbox .add-link {
&:extend(.umb-editor-wrapper .umb-node-preview);
}
.umb-modal .umb-multiple-textbox .textbox-wrapper .umb-editor {
flex: 1 1 auto;
width: auto;
}
@@ -166,7 +166,7 @@ input.umb-table__input {
}
.-content .-unpublished {
.-content :not(.with-unpublished-version).-unpublished {
.umb-table__name > * {
opacity: .4;
}
@@ -27,5 +27,5 @@
.umb-permission__description {
font-size: 13px;
color: @gray-5;
color: @gray-4;
}
@@ -133,27 +133,6 @@ div.umb-codeeditor .umb-btn-toolbar {
//
// Color picker
// --------------------------------------------------
ul.color-picker li {
padding: 2px;
margin: 3px;
border: 2px solid transparent;
width: 60px;
.thumbnail{
min-width: auto;
width: inherit;
padding: 0;
}
a {
height: 50px;
display:flex;
align-items: center;
justify-content: center;
cursor:pointer;
margin: 0 0 5px;
}
}
/* pre-value editor */
.control-group.color-picker-preval {
@@ -174,7 +153,7 @@ ul.color-picker li {
div.color-picker-prediv {
display: inline-flex;
align-items: center;
max-width: 80%;
max-width: 85%;
pre {
display: inline-flex;
@@ -357,13 +336,30 @@ ul.color-picker li {
text-align: center;
}
.umb-sortable-thumbnails .umb-icon-holder .icon{
.umb-sortable-thumbnails .umb-icon-holder .icon {
font-size: 40px;
line-height: 50px;
color: @gray-3;
display: block;
}
.umb-sortable-thumbnails .umb-icon-holder .file-icon > span {
color: @white;
background: @gray-4;
padding: 1px 3px;
font-size: 10px;
line-height: 130%;
display: block;
margin-top: -30px;
width: 2em;
}
.umb-sortable-thumbnails .umb-icon-holder .file-icon + small {
display: block;
margin-top: 1em;
}
.umb-sortable-thumbnails .umb-sortable-thumbnails__wrapper {
width: 124px;
height: 124px;
@@ -29,6 +29,7 @@
function init() {
// Check if it is a new user
var inviteVal = $location.search().invite;
//1 = enter password, 2 = password set, 3 = invalid token
if (inviteVal && (inviteVal === "1" || inviteVal === "2")) {
$q.all([
@@ -58,6 +59,8 @@
$scope.inviteStep = Number(inviteVal);
});
} else if (inviteVal && inviteVal === "3") {
$scope.inviteStep = Number(inviteVal);
}
}
@@ -104,10 +104,18 @@
</umb-button>
</div>
</div>
</div>
<div ng-show="invitedUser == null && inviteStep === 3" ng-if="inviteStep === 3" class="umb-login-container">
<div class="form">
<h1 style="margin-bottom: 10px; text-align: left;">Hi there</h1>
<p style="line-height: 1.6; margin-bottom: 25px;">
<localize key="user_userinviteExpiredMessage">Welcome to Umbraco! Unfortunately your invite has expired. Please contact your administrator and ask them to resend it.</localize>
</p>
<div ng-show="invitedUser == null" class="umb-login-container">
</div>
</div>
<div ng-show="invitedUser == null && !inviteStep" class="umb-login-container">
<div class="form">
<h1>{{greeting}}</h1>
@@ -313,7 +313,7 @@ angular.module("umbraco")
function searchMedia() {
$scope.loading = true;
entityResource.getPagedDescendants($scope.startNodeId, "Media", $scope.searchOptions)
entityResource.getPagedDescendants($scope.currentFolder.id, "Media", $scope.searchOptions)
.then(function(data) {
// update image data to work with image grid
angular.forEach(data.items, function(mediaItem) {
@@ -23,13 +23,19 @@
<div class="form-search">
<i class="icon-search"></i>
<input class="umb-search-field search-query -full-width-input"
ng-model="searchOptions.filter"
localize="placeholder"
placeholder="@placeholders_search"
ng-change="changeSearch()"
type="text"
no-dirty-check />
<input class="umb-search-field search-query -full-width-input"
ng-model="searchOptions.filter"
localize="placeholder"
placeholder="@placeholders_search"
ng-change="changeSearch()"
type="text"
no-dirty-check />
<br />
<label>
<localize key="general_includeFromsubFolders">Include subfolders in search</localize>
<input type="checkbox" ng-model="showChilds"
ng-change="changeSearch()">
</label>
</div>
<div class="upload-button">
@@ -81,17 +87,19 @@
</umb-file-dropzone>
<umb-media-grid
ng-if="!loading"
items="images"
on-click="clickHandler"
on-click-name="clickItemName"
on-click-edit="editMediaItem(item)"
allow-on-click-edit="{{allowMediaEdit}}"
item-max-width="150"
item-max-height="150"
item-min-width="100"
item-min-height="100"
only-images={{onlyImages}}>
ng-if="!loading"
items="images"
on-click="clickHandler"
on-click-name="clickItemName"
on-click-edit="editMediaItem(item)"
allow-on-click-edit="{{allowMediaEdit}}"
item-max-width="150"
item-max-height="150"
item-min-width="100"
item-min-height="100"
only-images={{onlyImages}}
include-sub-folders={{showChilds}}
current-Folder-id="{{currentFolder.id}}">
</umb-media-grid>
<div class="flex justify-center">
@@ -39,12 +39,23 @@
<umb-button
alias="preview"
ng-if="!page.isNew && content.allowPreview && page.showPreviewButton"
type="button"
button-style="info"
type="button"
button-style="outline"
action="preview(content)"
label="Preview page"
label-key="buttons_showPage">
</umb-button>
<umb-button
ng-if="page.showSaveButton"
alias="save"
type="button"
button-style="info"
state="page.saveButtonState"
action="save(content)"
label-key="buttons_save"
shortcut="ctrl+s"
add-ellipsis="{{page.saveButtonEllipsis}}">
</umb-button>
<umb-button-group
ng-if="defaultButton && !content.trashed && !infiniteModel.infiniteMode"
@@ -22,6 +22,23 @@
</ul>
</umb-box-content>
</umb-box>
<umb-box data-element="node-info-redirects" style="display:none;" ng-cloak ng-show="!urlTrackerDisabled && hasRedirects">
<umb-box-header title-key="redirectUrls_redirectUrlManagement"></umb-box-header>
<umb-box-content class="block-form">
<div style="position: relative;">
<div ng-if="loadingRedirectUrls" style="background: rgba(255, 255, 255, 0.8); position: absolute; top: 0; left: 0; right: 0; bottom: 0;"></div>
<umb-load-indicator ng-if="loadingRedirectUrls"></umb-load-indicator>
<div ng-show="hasRedirects">
<p><localize key="redirectUrls_panelInformation" class="ng-isolate-scope ng-scope">The following URLs redirect to this content item:</localize></p>
<ul class="nav nav-stacked" style="margin-bottom: 0;">
<li ng-repeat="redirectUrl in redirectUrls">
<a href="{{redirectUrl.originalUrl}}" target="_blank"><i ng-class="value.icon" class="icon-out"></i> {{redirectUrl.originalUrl}}</a>
</li>
</ul>
</div>
</div>
</umb-box-content>
</umb-box>
<umb-box data-element="node-info-history">
<umb-box-header title-key="general_history"></umb-box-header>
@@ -67,6 +84,7 @@
class="history-item__badge"
size="xs"
color="{{item.logTypeColor}}">
<localize key="auditTrails_small{{ item.logType }}">{{ item.logType }}</localize>
</umb-badge>
<span>
@@ -179,7 +197,7 @@
</div>
</umb-control-group>
<umb-control-group data-element="node-info-create-date" label="@template_createdDate">
<umb-control-group ng-if="node.id !== 0" data-element="node-info-create-date" label="@template_createdDate">
{{node.createDateFormatted}} <localize key="general_by">by</localize> {{ node.owner.name }}
</umb-control-group>
@@ -209,7 +227,7 @@
</div>
</umb-control-group>
<umb-control-group data-element="node-info-id" label="Id">
<umb-control-group ng-if="node.id !== 0" data-element="node-info-id" label="Id">
<div>{{ node.id }}</div>
<small>{{ node.key }}</small>
</umb-control-group>
@@ -1,7 +1,8 @@
<ul class="umb-sub-views-nav" ng-show="showNavigation">
<li ng-repeat="item in navigation | limitTo: itemsLimit ">
<div ng-show="item.alias !== 'more'">
<div ng-show="item.alias !== 'more'"
ng-class="item.errorClass">
<a data-element="sub-view-{{item.alias}}"
tabindex="-1"
class="umb-sub-views-nav-item js-umb-sub-views-nav-item"
@@ -1,10 +1,11 @@
<div class="umb-editor-sub-view"
ng-class="'sub-view-' + model.name"
val-sub-view>
ng-class="'sub-view-' + model.name"
val-sub-view>
<div class="umb-editor-sub-view__content"
ng-show="model.active === true"
ng-include="model.view">
<div
class="umb-editor-sub-view__content"
ng-show="model.active === true"
ng-include="model.view">
</div>
</div>
@@ -9,12 +9,15 @@
<div ng-if="notification.view">
<div ng-include="notification.view"></div>
</div>
<div ng-if="notification.headline">
<a ng-href="{{notification.url}}" target="_blank">
<div ng-if="notification.headline" ng-switch on="{{notification}}">
<a ng-href="{{notification.url}}" ng-switch-when="{{notification.url && notification.url.trim() != ''}}" target="_blank">
<strong>{{notification.headline}}</strong>
<span ng-bind-html="notification.message"></span>
</a>
<div ng-switch-default>
<strong>{{notification.headline}}</strong>
<span ng-bind-html="notification.message"></span>
</div>
</div>
</li>
</ul>
@@ -1,9 +1,16 @@
<div class="umb-color-swatches">
<div class="umb-color-swatches" ng-class="{ 'with-labels': useLabel }">
<button class="umb-color-box umb-color-box--{{size}} btn-{{color.value}}" ng-repeat="color in colors" title="{{color.name}}" ng-click="setColor(color.value)">
<div class="check_circle" ng-if="color.value === selectedColor">
<i class="icon icon-check small"></i>
<button type="button" class="umb-color-box umb-color-box--{{size}} btn-{{color.value}}" ng-repeat="color in colors" title="{{color.label}}" hex-bg-inline="{{useColorClass === false}}" hex-bg-color="{{color.value}}" ng-class="{ 'active': color.value === selectedColor }" ng-click="setColor(color.value)">
<div class="umb-color-box-inner">
<div class="check_circle" ng-if="color.value === selectedColor">
<i class="icon icon-check small"></i>
</div>
<div class="umb-color-box__label" ng-if="useLabel">
<div class="umb-color-box__name truncate">{{ color.label }}</div>
<div class="umb-color-box__description">#{{ color.value }}</div>
</div>
</div>
</button>
</div>

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