diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RedirectUrlRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RedirectUrlRepository.cs index 246adf7415..84e5cb864f 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RedirectUrlRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RedirectUrlRepository.cs @@ -167,6 +167,23 @@ JOIN umbracoNode ON umbracoRedirectUrl.contentKey=umbracoNode.uniqueID"); return dto == null ? null : Map(dto); } + public IRedirectUrl GetMostRecentUrl(string url, string culture) + { + if (string.IsNullOrWhiteSpace(culture)) return GetMostRecentUrl(url); + var urlHash = url.GenerateHash(); + var sql = GetBaseQuery(false) + .Where(x => x.Url == url && x.UrlHash == urlHash && + (x.Culture == culture.ToLower() || x.Culture == string.Empty)) + .OrderByDescending(x => x.CreateDateUtc); + var dtos = Database.Fetch(sql); + var dto = dtos.FirstOrDefault(f => f.Culture == culture.ToLower()); + + if (dto == null) + dto = dtos.FirstOrDefault(f => f.Culture == string.Empty); + + return dto == null ? null : Map(dto); + } + public IEnumerable GetContentUrls(Guid contentKey) { var sql = GetBaseQuery(false) @@ -208,17 +225,5 @@ JOIN umbracoNode ON umbracoRedirectUrl.contentKey=umbracoNode.uniqueID"); var rules = result.Items.Select(Map); return rules; } - - public IRedirectUrl GetMostRecentUrl(string url, string culture) - { - if (string.IsNullOrWhiteSpace(culture)) return GetMostRecentUrl(url); - var urlHash = url.GenerateHash(); - var sql = GetBaseQuery(false) - .Where(x => x.Url == url && x.UrlHash == urlHash && x.Culture == culture.ToLower()) - .OrderByDescending(x => x.CreateDateUtc); - var dtos = Database.Fetch(sql); - var dto = dtos.FirstOrDefault(); - return dto == null ? null : Map(dto); - } } } diff --git a/src/Umbraco.Infrastructure/Services/Implement/UserService.cs b/src/Umbraco.Infrastructure/Services/Implement/UserService.cs index a6b55a1dc3..cae3c95b92 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/UserService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/UserService.cs @@ -260,7 +260,7 @@ namespace Umbraco.Core.Services.Implement // this method must exist in this service as an implementation (legacy) void IMembershipMemberService.SetLastLogin(string username, DateTime date) { - throw new NotSupportedException("This method is not implemented or supported for users"); + _logger.LogWarning("This method is not implemented. Using membership providers users is not advised, use ASP.NET Identity instead. See issue #9224 for more information."); } /// diff --git a/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTestWithContent.cs b/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTestWithContent.cs index af8822ad19..47159c419e 100644 --- a/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTestWithContent.cs +++ b/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTestWithContent.cs @@ -1,4 +1,4 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. using System; @@ -46,6 +46,10 @@ namespace Umbraco.Tests.Integration.Testing Subpage2 = ContentBuilder.CreateSimpleContent(ContentType, "Text Page 2", Textpage.Id); ContentService.Save(Subpage2, 0); + + Subpage3 = ContentBuilder.CreateSimpleContent(ContentType, "Text Page 3", Textpage.Id); + ContentService.Save(Subpage3, 0); + // Create and Save Content "Text Page Deleted" based on "umbTextpage" -> 1056 Trashed = ContentBuilder.CreateSimpleContent(ContentType, "Text Page Deleted", -20); Trashed.Trashed = true; @@ -55,6 +59,7 @@ namespace Umbraco.Tests.Integration.Testing protected Content Trashed { get; private set; } protected Content Subpage2 { get; private set; } + protected Content Subpage3 { get; private set; } protected Content Subpage { get; private set; } diff --git a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEventsTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEventsTests.cs index aaaa73416f..1a2f39f51f 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEventsTests.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEventsTests.cs @@ -2009,7 +2009,7 @@ namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Services } [Test] - public void HasInitialContent() => Assert.AreEqual(4, ContentService.Count()); + public void HasInitialContent() => Assert.AreEqual(5, ContentService.Count()); #endregion diff --git a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceTests.cs index f809bd3d7e..bc8e68cb5d 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceTests.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceTests.cs @@ -363,7 +363,7 @@ namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Services } // Assert - Assert.AreEqual(24, ContentService.Count()); + Assert.AreEqual(25, ContentService.Count()); } [Test] @@ -1634,7 +1634,7 @@ namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Services Assert.AreNotEqual(-20, content.ParentId); Assert.IsFalse(content.Trashed); - Assert.AreEqual(3, descendants.Count); + Assert.AreEqual(4, descendants.Count); Assert.IsFalse(descendants.Any(x => x.Path.StartsWith("-1,-20,"))); Assert.IsFalse(descendants.Any(x => x.Trashed)); @@ -1649,7 +1649,7 @@ namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Services Assert.AreEqual(-20, content.ParentId); Assert.IsTrue(content.Trashed); - Assert.AreEqual(3, descendants.Count); + Assert.AreEqual(4, descendants.Count); Assert.IsTrue(descendants.All(x => x.Path.StartsWith("-1,-20,"))); Assert.True(descendants.All(x => x.Trashed)); @@ -1942,7 +1942,7 @@ namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Services // Arrange IContent temp = ContentService.GetById(Textpage.Id); Assert.AreEqual("Home", temp.Name); - Assert.AreEqual(2, ContentService.CountChildren(temp.Id)); + Assert.AreEqual(3, ContentService.CountChildren(temp.Id)); // Act IContent copy = ContentService.Copy(temp, temp.ParentId, false, true, Constants.Security.SuperUserId); @@ -1952,7 +1952,7 @@ namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Services Assert.That(copy, Is.Not.Null); Assert.That(copy.Id, Is.Not.EqualTo(content.Id)); Assert.AreNotSame(content, copy); - Assert.AreEqual(2, ContentService.CountChildren(copy.Id)); + Assert.AreEqual(3, ContentService.CountChildren(copy.Id)); IContent child = ContentService.GetById(Subpage.Id); IContent childCopy = ContentService.GetPagedChildren(copy.Id, 0, 500, out long total).First(); @@ -1967,7 +1967,7 @@ namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Services // Arrange IContent temp = ContentService.GetById(Textpage.Id); Assert.AreEqual("Home", temp.Name); - Assert.AreEqual(2, ContentService.CountChildren(temp.Id)); + Assert.AreEqual(3, ContentService.CountChildren(temp.Id)); // Act IContent copy = ContentService.Copy(temp, temp.ParentId, false, false, Constants.Security.SuperUserId); diff --git a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/EntityServiceTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/EntityServiceTests.cs index a221575f7c..7267f34e51 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/EntityServiceTests.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/EntityServiceTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. using System; diff --git a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/RedirectUrlServiceTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/RedirectUrlServiceTests.cs index 72a5eb4302..6c67797421 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/RedirectUrlServiceTests.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/RedirectUrlServiceTests.cs @@ -6,6 +6,8 @@ using System.Threading; using Microsoft.Extensions.Logging; using Moq; using NUnit.Framework; +using System.Linq; +using System.Threading; using Umbraco.Core.Cache; using Umbraco.Core.Models; using Umbraco.Core.Persistence.Repositories.Implement; @@ -21,11 +23,14 @@ namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Services [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] public class RedirectUrlServiceTests : UmbracoIntegrationTestWithContent { - private IContent _testPage; - private IContent _altTestPage; + private IContent _firstSubPage; + private IContent _secondSubPage; + private IContent _thirdSubPage; private const string Url = "blah"; - private const string CultureA = "en"; - private const string CultureB = "de"; + private const string UrlAlt = "alternativeUrl"; + private const string CultureEnglish = "en"; + private const string CultureGerman = "de"; + private const string UnusedCulture = "es"; private IRedirectUrlService RedirectUrlService => GetRequiredService(); @@ -37,22 +42,33 @@ namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Services { var repository = new RedirectUrlRepository((IScopeAccessor)ScopeProvider, AppCaches.Disabled, Mock.Of>()); IContent rootContent = ContentService.GetRootContent().First(); - var subPages = ContentService.GetPagedChildren(rootContent.Id, 0, 2, out _).ToList(); - _testPage = subPages[0]; - _altTestPage = subPages[1]; + var subPages = ContentService.GetPagedChildren(rootContent.Id, 0, 3, out _).ToList(); + _firstSubPage = subPages[0]; + _secondSubPage = subPages[1]; + _thirdSubPage = subPages[2]; + repository.Save(new RedirectUrl { - ContentKey = _testPage.Key, + ContentKey = _firstSubPage.Key, Url = Url, - Culture = CultureA + Culture = CultureEnglish }); + Thread.Sleep(1000); //Added delay to ensure timestamp difference as sometimes they seem to have the same timestamp repository.Save(new RedirectUrl { - ContentKey = _altTestPage.Key, + ContentKey = _secondSubPage.Key, Url = Url, - Culture = CultureB + Culture = CultureGerman }); + Thread.Sleep(1000); + repository.Save(new RedirectUrl + { + ContentKey = _thirdSubPage.Key, + Url = UrlAlt, + Culture = string.Empty + }); + scope.Complete(); } } @@ -61,14 +77,22 @@ namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Services public void Can_Get_Most_Recent_RedirectUrl() { IRedirectUrl redirect = RedirectUrlService.GetMostRecentRedirectUrl(Url); - Assert.AreEqual(redirect.ContentId, _altTestPage.Id); + Assert.AreEqual(redirect.ContentId, _secondSubPage.Id); } [Test] public void Can_Get_Most_Recent_RedirectUrl_With_Culture() { - IRedirectUrl redirect = RedirectUrlService.GetMostRecentRedirectUrl(Url, CultureA); - Assert.AreEqual(redirect.ContentId, _testPage.Id); + var redirect = RedirectUrlService.GetMostRecentRedirectUrl(Url, CultureEnglish); + Assert.AreEqual(redirect.ContentId, _firstSubPage.Id); + } + + [Test] + public void Can_Get_Most_Recent_RedirectUrl_With_Culture_When_No_CultureVariant_Exists() + { + var redirect = RedirectUrlService.GetMostRecentRedirectUrl(UrlAlt, UnusedCulture); + Assert.AreEqual(redirect.ContentId, _thirdSubPage.Id); + } } } diff --git a/src/Umbraco.Tests/Services/TestWithSomeContentBase.cs b/src/Umbraco.Tests/Services/TestWithSomeContentBase.cs index 2b313afc5c..6daa16470b 100644 --- a/src/Umbraco.Tests/Services/TestWithSomeContentBase.cs +++ b/src/Umbraco.Tests/Services/TestWithSomeContentBase.cs @@ -41,6 +41,9 @@ namespace Umbraco.Tests.Services Content subpage2 = MockedContent.CreateSimpleContent(contentType, "Text Page 2", textpage.Id); ServiceContext.ContentService.Save(subpage2, 0); + Content subpage3 = MockedContent.CreateSimpleContent(contentType, "Text Page 3", textpage.Id); + ServiceContext.ContentService.Save(subpage3, 0); + //Create and Save Content "Text Page Deleted" based on "umbTextpage" -> 1064 Content trashed = MockedContent.CreateSimpleContent(contentType, "Text Page Deleted", -20); trashed.Trashed = true; diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbautofocus.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbautofocus.directive.js index 47ef460c53..98c4ef691e 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbautofocus.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbautofocus.directive.js @@ -10,18 +10,14 @@ angular.module("umbraco.directives") } }; - //check if there's a value for the attribute, if there is and it's false then we conditionally don't - //use auto focus. - if (attrs.umbAutoFocus) { - attrs.$observe("umbAutoFocus", function (newVal) { - var enabled = (newVal === "false" || newVal === 0 || newVal === false) ? false : true; - if (enabled) { - $timeout(function() { - update(); - }); - } - }); - } + attrs.$observe("umbAutoFocus", function (newVal) { + var enabled = (newVal === "false" || newVal === 0 || newVal === false) ? false : true; + if (enabled) { + $timeout(function() { + update(); + }); + } + }); }; }); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbradiobutton.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbradiobutton.directive.js index 3232ed7f34..8c7157c414 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbradiobutton.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbradiobutton.directive.js @@ -40,7 +40,7 @@ (function () { 'use strict'; - function UmbRadiobuttonController($timeout) { + function UmbRadiobuttonController($timeout, localizationService) { var vm = this; diff --git a/src/Umbraco.Web.UI.Client/src/common/services/angularhelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/angularhelper.service.js index 7e7f804656..36ce4541f1 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/angularhelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/angularhelper.service.js @@ -11,6 +11,11 @@ function angularHelper($q) { var requiredFormProps = ["$error", "$name", "$dirty", "$pristine", "$valid", "$submitted", "$pending"]; function collectAllFormErrorsRecursively(formCtrl, allErrors) { + // skip if the control is already added to the array + if (allErrors.indexOf(formCtrl) !== -1) { + return; + } + // loop over the error dictionary (see https://docs.angularjs.org/api/ng/type/form.FormController#$error) var keys = Object.keys(formCtrl.$error); if (keys.length === 0) { @@ -31,6 +36,7 @@ function angularHelper($q) { allErrors.push(ctrl); // add the error return; } + // recurse with the sub form collectAllFormErrorsRecursively(ctrl, allErrors); } @@ -43,6 +49,7 @@ function angularHelper($q) { } function isForm(obj) { + // a method to check that the collection of object prop names contains the property name expected function allPropertiesExist(objectPropNames) { //ensure that every required property name exists on the current object @@ -89,9 +96,9 @@ function angularHelper($q) { /** * Method used to re-run the $parsers for a given ngModel - * @param {} scope - * @param {} ngModel - * @returns {} + * @param {} scope + * @param {} ngModel + * @returns {} */ revalidateNgModel: function (scope, ngModel) { this.safeApply(scope, function() { @@ -103,8 +110,8 @@ function angularHelper($q) { /** * Execute a list of promises sequentially. Unlike $q.all which executes all promises at once, this will execute them in sequence. - * @param {} promises - * @returns {} + * @param {} promises + * @returns {} */ executeSequentialPromises: function (promises) { @@ -178,7 +185,7 @@ function angularHelper($q) { //NOTE: There isn't a way in angular to get a reference to the current form object since the form object // is just defined as a property of the scope when it is named but you'll always need to know the name which // isn't very convenient. If we want to watch for validation changes we need to get a form reference. - // The way that we detect the form object is a bit hackerific in that we detect all of the required properties + // The way that we detect the form object is a bit hackerific in that we detect all of the required properties // that exist on a form object. // //The other way to do it in a directive is to require "^form", but in a controller the only other way to do it @@ -239,7 +246,7 @@ function angularHelper($q) { $submitted: false, $pending: undefined, $addControl: Utilities.noop, - $removeControl: Utilities.noop, + $removeControl: Utilities.noop, $setValidity: Utilities.noop, $setDirty: Utilities.noop, $setPristine: Utilities.noop, diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-node-preview.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-node-preview.html index a7daa57775..34fb4d7dfd 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-node-preview.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-node-preview.html @@ -1,6 +1,6 @@
- +