From 6c91d5437289d4ce6cca6d8d10efa5fb7c91b219 Mon Sep 17 00:00:00 2001 From: Alexander Bryukhov Date: Wed, 26 Jul 2017 10:24:03 +0700 Subject: [PATCH 01/16] U4-10200 Listview layouts - empty state not localized --- .../src/views/propertyeditors/listview/layouts/grid/grid.html | 4 ++-- .../src/views/propertyeditors/listview/layouts/list/list.html | 4 ++-- src/Umbraco.Web.UI/umbraco/config/lang/en.xml | 2 ++ src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml | 2 ++ src/Umbraco.Web.UI/umbraco/config/lang/ru.xml | 2 ++ 5 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/grid/grid.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/grid/grid.html index 949374cbc2..cf1c3f6411 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/grid/grid.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/grid/grid.html @@ -14,8 +14,8 @@ -
No content has been added
-
No members have been added
+
No content has been added
+
No members have been added
-
No content has been added
-
No members have been added
+
No content has been added
+
No members have been added
diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml index 52c0779733..c2be0a2ec0 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml @@ -143,6 +143,8 @@ Last published There are no items to show There are no items to show in the list. + No content has been added + No members have been added Media Type Link to media item(s) Member Group diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml index f2ea5ad011..1950fc9aa2 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml @@ -142,6 +142,8 @@ Last published There are no items to show There are no items to show in the list. + No content has been added + No members have been added Media Type Link to media item(s) Member Group diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/ru.xml b/src/Umbraco.Web.UI/umbraco/config/lang/ru.xml index 3cc1978416..228b549261 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/ru.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/ru.xml @@ -184,6 +184,8 @@ Документ опубликован Здесь еще нет элементов. В этом списке пока нет элементов. + Содержимое пока еще не добавлено + Участники пока еще не добавлены Ссылка на медиа-элементы Тип медиа-контента Группа участников From a851cef70f692a0a9026f0a12702885c1557492c Mon Sep 17 00:00:00 2001 From: Stephan Date: Tue, 8 Aug 2017 18:54:15 +0200 Subject: [PATCH 02/16] Improve EnsureUniqueNodeName perfs? --- .../Repositories/ContentRepository.cs | 27 +-- .../DataTypeDefinitionRepository.cs | 29 +--- .../Repositories/MediaRepository.cs | 27 +-- .../Repositories/SimilarNodeName.cs | 112 +++++++++++++ .../Repositories/SimilarNodeNameComparer.cs | 45 ----- src/Umbraco.Core/Umbraco.Core.csproj | 2 +- .../Repositories/SimilarNodeNameTests.cs | 98 +++++++++++ src/Umbraco.Tests/Umbraco.Tests.csproj | 1 + src/Umbraco.Web.UI/config/Dashboard.config | 10 ++ .../config/splashes/noNodes.aspx | 155 ++++++++++++++---- 10 files changed, 354 insertions(+), 152 deletions(-) create mode 100644 src/Umbraco.Core/Persistence/Repositories/SimilarNodeName.cs delete mode 100644 src/Umbraco.Core/Persistence/Repositories/SimilarNodeNameComparer.cs create mode 100644 src/Umbraco.Tests/Persistence/Repositories/SimilarNodeNameTests.cs diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs index addd0bc925..e8df52e9d3 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs @@ -1126,31 +1126,10 @@ ORDER BY cmsContentVersion.id DESC if (EnsureUniqueNaming == false) return nodeName; - var sql = new Sql(); - sql.Select("*") - .From() - .Where(x => x.NodeObjectType == NodeObjectTypeId && x.ParentId == parentId && x.Text.StartsWith(nodeName)); + var names = Database.Fetch("SELECT id, text AS name FROM umbracoNode WHERE nodeObjectType=@objectType AND parentId=@parentId", + new { objectType = NodeObjectTypeId, parentId }); - int uniqueNumber = 1; - var currentName = nodeName; - - var dtos = Database.Fetch(sql); - if (dtos.Any()) - { - var results = dtos.OrderBy(x => x.Text, new SimilarNodeNameComparer()); - foreach (var dto in results) - { - if (id != 0 && id == dto.NodeId) continue; - - if (dto.Text.ToLowerInvariant().Equals(currentName.ToLowerInvariant())) - { - currentName = nodeName + string.Format(" ({0})", uniqueNumber); - uniqueNumber++; - } - } - } - - return currentName; + return SimilarNodeName.GetUniqueName(names, id, nodeName); } /// diff --git a/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs b/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs index 8c263f4211..44be068b4b 100644 --- a/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs @@ -450,33 +450,10 @@ AND umbracoNode.id <> @id", private string EnsureUniqueNodeName(string nodeName, int id = 0) { + var names = Database.Fetch("SELECT id, text AS name FROM umbracoNode WHERE nodeObjectType=@objectType", + new { objectType = NodeObjectTypeId }); - - var sql = new Sql(); - sql.Select("*") - .From(SqlSyntax) - .Where(x => x.NodeObjectType == NodeObjectTypeId && x.Text.StartsWith(nodeName)); - - int uniqueNumber = 1; - var currentName = nodeName; - - var dtos = Database.Fetch(sql); - if (dtos.Any()) - { - var results = dtos.OrderBy(x => x.Text, new SimilarNodeNameComparer()); - foreach (var dto in results) - { - if (id != 0 && id == dto.NodeId) continue; - - if (dto.Text.ToLowerInvariant().Equals(currentName.ToLowerInvariant())) - { - currentName = nodeName + string.Format(" ({0})", uniqueNumber); - uniqueNumber++; - } - } - } - - return currentName; + return SimilarNodeName.GetUniqueName(names, id, nodeName); } /// diff --git a/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs index ee413c2a59..bcb1d006a8 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs @@ -584,31 +584,10 @@ namespace Umbraco.Core.Persistence.Repositories if (EnsureUniqueNaming == false) return nodeName; - var sql = new Sql(); - sql.Select("*") - .From() - .Where(x => x.NodeObjectType == NodeObjectTypeId && x.ParentId == parentId && x.Text.StartsWith(nodeName)); + var names = Database.Fetch("SELECT id, text AS name FROM umbracoNode WHERE nodeObjectType=@objectType AND parentId=@parentId", + new { objectType = NodeObjectTypeId, parentId }); - int uniqueNumber = 1; - var currentName = nodeName; - - var dtos = Database.Fetch(sql); - if (dtos.Any()) - { - var results = dtos.OrderBy(x => x.Text, new SimilarNodeNameComparer()); - foreach (var dto in results) - { - if (id != 0 && id == dto.NodeId) continue; - - if (dto.Text.ToLowerInvariant().Equals(currentName.ToLowerInvariant())) - { - currentName = nodeName + string.Format(" ({0})", uniqueNumber); - uniqueNumber++; - } - } - } - - return currentName; + return SimilarNodeName.GetUniqueName(names, id, nodeName); } /// diff --git a/src/Umbraco.Core/Persistence/Repositories/SimilarNodeName.cs b/src/Umbraco.Core/Persistence/Repositories/SimilarNodeName.cs new file mode 100644 index 0000000000..371f73b27f --- /dev/null +++ b/src/Umbraco.Core/Persistence/Repositories/SimilarNodeName.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Umbraco.Core.Persistence.Repositories +{ + internal class SimilarNodeName + { + private int _numPos = -2; + + public int Id { get; set; } + public string Name { get; set; } + + // cached - reused + public int NumPos + { + get + { + if (_numPos != -2) return _numPos; + + var name = Name; + + if (name[name.Length - 1] != ')') + return _numPos = -1; + + var pos = name.LastIndexOf('('); + if (pos < 2 || pos == name.Length - 2) // < 2 and not < 0, because we want at least "x (" + return _numPos = -1; + + return _numPos = pos; + } + } + + // not cached - used only once + public int NumVal + { + get + { + if (NumPos < 0) + throw new InvalidOperationException(); + int num; + if (int.TryParse(Name.Substring(NumPos + 1, Name.Length - 2 - NumPos), out num)) + return num; + return 0; + } + } + + // compare without allocating, nor parsing integers + internal class Comparer : IComparer + { + public int Compare(SimilarNodeName x, SimilarNodeName y) + { + if (x == null) throw new ArgumentNullException("x"); + if (y == null) throw new ArgumentNullException("y"); + + var xpos = x.NumPos; + var ypos = y.NumPos; + + var xname = x.Name; + var yname = y.Name; + + if (xpos < 0 || ypos < 0 || xpos != ypos) + return string.Compare(xname, yname, StringComparison.Ordinal); + + // compare the part before (number) + var n = string.Compare(xname, 0, yname, 0, xpos, StringComparison.Ordinal); + if (n != 0) + return n; + + // compare (number) lengths + var diff = xname.Length - yname.Length; + if (diff != 0) return diff < 0 ? -1 : +1; + + // actually compare (number) + var i = xpos; + while (i < xname.Length - 1) + { + if (xname[i] != yname[i]) + return xname[i] < yname[i] ? -1 : +1; + i++; + } + return 0; + } + } + + // gets a unique name + public static string GetUniqueName(IEnumerable names, int nodeId, string nodeName) + { + var uniqueNumber = 1; + var uniqueing = false; + foreach (var name in names.OrderBy(x => x, new Comparer())) + { + // ignore self + if (nodeId != 0 && name.Id == nodeId) continue; + + if (uniqueing) + { + if (name.NumPos > 0 && name.Name.StartsWith(nodeName) && name.NumVal == uniqueNumber) + uniqueNumber++; + else + break; + } + else if (name.Name.InvariantEquals(nodeName)) + { + uniqueing = true; + } + } + + return uniqueing ? string.Concat(nodeName, " (", uniqueNumber.ToString(), ")") : nodeName; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/SimilarNodeNameComparer.cs b/src/Umbraco.Core/Persistence/Repositories/SimilarNodeNameComparer.cs deleted file mode 100644 index ce25486422..0000000000 --- a/src/Umbraco.Core/Persistence/Repositories/SimilarNodeNameComparer.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Umbraco.Core.Persistence.Repositories -{ - /// - /// Comparer that takes into account the duplicate index of a node name - /// This is needed as a normal alphabetic sort would go Page (1), Page (10), Page (2) etc. - /// - internal class SimilarNodeNameComparer : IComparer - { - public int Compare(string x, string y) - { - if (x.LastIndexOf('(') != -1 && x.LastIndexOf(')') == x.Length - 1 && y.LastIndexOf(')') == y.Length - 1) - { - if (x.ToLower().Substring(0, x.LastIndexOf('(')) == y.ToLower().Substring(0, y.LastIndexOf('('))) - { - int xDuplicateIndex = ExtractDuplicateIndex(x); - int yDuplicateIndex = ExtractDuplicateIndex(y); - - if (xDuplicateIndex != 0 && yDuplicateIndex != 0) - { - return xDuplicateIndex.CompareTo(yDuplicateIndex); - } - } - } - return String.Compare(x.ToLower(), y.ToLower(), StringComparison.Ordinal); - } - - private int ExtractDuplicateIndex(string text) - { - int index = 0; - - if (text.LastIndexOf('(') != -1 && text.LastIndexOf('(') < text.Length - 2) - { - int startPos = text.LastIndexOf('(') + 1; - int length = text.Length - 1 - startPos; - - int.TryParse(text.Substring(startPos, length), out index); - } - - return index; - } - } -} \ No newline at end of file diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 96183f61bf..8931fa1b23 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -628,6 +628,7 @@ + @@ -1230,7 +1231,6 @@ - diff --git a/src/Umbraco.Tests/Persistence/Repositories/SimilarNodeNameTests.cs b/src/Umbraco.Tests/Persistence/Repositories/SimilarNodeNameTests.cs new file mode 100644 index 0000000000..72f3e39874 --- /dev/null +++ b/src/Umbraco.Tests/Persistence/Repositories/SimilarNodeNameTests.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using NUnit.Framework; +using Umbraco.Core.Persistence.Repositories; + +namespace Umbraco.Tests.Persistence.Repositories +{ + [TestFixture] + public class SimilarNodeNameTests + { + [TestCase("Alpha", "Alpha", 0)] + [TestCase("Alpha", "ALPHA", +1)] // case is important + [TestCase("Alpha", "Bravo", -1)] + [TestCase("Bravo", "Alpha", +1)] + [TestCase("Alpha (1)", "Alpha (1)", 0)] + [TestCase("Alpha", "Alpha (1)", -1)] + [TestCase("Alpha (1)", "Alpha", +1)] + [TestCase("Alpha (1)", "Alpha (2)", -1)] + [TestCase("Alpha (2)", "Alpha (1)", +1)] + [TestCase("Alpha (2)", "Alpha (10)", -1)] // this is the real stuff + [TestCase("Alpha (10)", "Alpha (2)", +1)] // this is the real stuff + [TestCase("Kilo", "Golf (2)", +1)] + [TestCase("Kilo (1)", "Golf (2)", +1)] + public void ComparerTest(string name1, string name2, int expected) + { + var comparer = new SimilarNodeName.Comparer(); + + var result = comparer.Compare(new SimilarNodeName { Name = name1 }, new SimilarNodeName { Name = name2 }); + if (expected == 0) + Assert.AreEqual(0, result); + else if (expected < 0) + Assert.IsTrue(result < 0, "Expected <0 but was " + result); + else if (expected > 0) + Assert.IsTrue(result > 0, "Expected >0 but was " + result); + } + + [Test] + public void OrderByTest() + { + var names = new[] + { + new SimilarNodeName { Id = 1, Name = "Alpha (2)" }, + new SimilarNodeName { Id = 2, Name = "Alpha" }, + new SimilarNodeName { Id = 3, Name = "Golf" }, + new SimilarNodeName { Id = 4, Name = "Zulu" }, + new SimilarNodeName { Id = 5, Name = "Mike" }, + new SimilarNodeName { Id = 6, Name = "Kilo (1)" }, + new SimilarNodeName { Id = 7, Name = "Yankee" }, + new SimilarNodeName { Id = 8, Name = "Kilo" }, + new SimilarNodeName { Id = 9, Name = "Golf (2)" }, + new SimilarNodeName { Id = 10, Name = "Alpha (1)" }, + }; + + var ordered = names.OrderBy(x => x, new SimilarNodeName.Comparer()).ToArray(); + + var i = 0; + Assert.AreEqual(2, ordered[i++].Id); + Assert.AreEqual(10, ordered[i++].Id); + Assert.AreEqual(1, ordered[i++].Id); + Assert.AreEqual(3, ordered[i++].Id); + Assert.AreEqual(9, ordered[i++].Id); + Assert.AreEqual(8, ordered[i++].Id); + Assert.AreEqual(6, ordered[i++].Id); + Assert.AreEqual(5, ordered[i++].Id); + Assert.AreEqual(7, ordered[i++].Id); + Assert.AreEqual(4, ordered[i++].Id); + } + + [TestCase(0, "Charlie", "Charlie")] + [TestCase(0, "Zulu", "Zulu (1)")] + [TestCase(0, "Golf", "Golf (1)")] + [TestCase(0, "Kilo", "Kilo (2)")] + [TestCase(0, "Alpha", "Alpha (3)")] + [TestCase(0, "Kilo (1)", "Kilo (1) (1)")] // though... we might consider "Kilo (2)" + [TestCase(6, "Kilo (1)", "Kilo (1)")] // because of the id + public void Test(int nodeId, string nodeName, string expected) + { + var names = new[] + { + new SimilarNodeName { Id = 1, Name = "Alpha (2)" }, + new SimilarNodeName { Id = 2, Name = "Alpha" }, + new SimilarNodeName { Id = 3, Name = "Golf" }, + new SimilarNodeName { Id = 4, Name = "Zulu" }, + new SimilarNodeName { Id = 5, Name = "Mike" }, + new SimilarNodeName { Id = 6, Name = "Kilo (1)" }, + new SimilarNodeName { Id = 7, Name = "Yankee" }, + new SimilarNodeName { Id = 8, Name = "Kilo" }, + new SimilarNodeName { Id = 9, Name = "Golf (2)" }, + new SimilarNodeName { Id = 10, Name = "Alpha (1)" }, + }; + + Assert.AreEqual(expected, SimilarNodeName.GetUniqueName(names, nodeId, nodeName)); + } + } +} diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 92d00e08dc..0824e837e1 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -188,6 +188,7 @@ + diff --git a/src/Umbraco.Web.UI/config/Dashboard.config b/src/Umbraco.Web.UI/config/Dashboard.config index 322551545d..c8c7e32f27 100644 --- a/src/Umbraco.Web.UI/config/Dashboard.config +++ b/src/Umbraco.Web.UI/config/Dashboard.config @@ -119,4 +119,14 @@ +
+ + content + + + + /App_Plugins/Deploy/views/dashboards/dashboard.html + + +
\ No newline at end of file diff --git a/src/Umbraco.Web.UI/config/splashes/noNodes.aspx b/src/Umbraco.Web.UI/config/splashes/noNodes.aspx index d46ecd6113..c1ceabf05b 100644 --- a/src/Umbraco.Web.UI/config/splashes/noNodes.aspx +++ b/src/Umbraco.Web.UI/config/splashes/noNodes.aspx @@ -1,7 +1,7 @@ -<%@ Page Language="C#" AutoEventWireup="True" Inherits="Umbraco.Web.UI.Config.Splashes.NoNodes" CodeBehind="NoNodes.aspx.cs" %> -<%@ Import Namespace="Umbraco.Core.Configuration" %> +<%@ Page Language="C#" AutoEventWireup="true"%> <%@ Import Namespace="Umbraco.Core.IO" %> - +<%@ Import Namespace="Umbraco.Deploy.UI" %> +<%@ Import Namespace="Umbraco.Web" %> @@ -11,51 +11,142 @@ - + - + + + + + + + +<% if(HttpContext.Current.Request.IsLocal == false){ %>
-
-
- +
+
+ -

Welcome to your Umbraco installation

-

You're seeing this wonderful page because your website doesn't contain any published content yet.

+

Welcome to your Umbraco installation

+

You're seeing the wonderful page because your website doesn't contain any published content yet.

- + -
-
-

Easy start with Umbraco.tv

-

We have created a bunch of 'how-to' videos, to get you easily started with Umbraco. Learn how to build projects in just a couple of minutes. Easiest CMS in the world.

- - Umbraco.tv → -
+
+
+

Easy start with Umbraco.tv

+

We have created a bunch of 'how-to' videos, to get you easily started with Umbraco. Learn how to build projects in just a couple of minutes. Easiest CMS in the world.

+ + Umbraco.tv → +
-
-

Be a part of the community

-

The Umbraco community is the best of its kind, be sure to visit, and if you have any questions, we're sure that you can get your answers from the community.

- - our.Umbraco → -
-
+
+

Be a part of the community

+

The Umbraco community is the best of its kind, be sure to visit, and if you have any questions, we’re sure that you can get your answers from the community.

+ + our.Umbraco → +
+
-
-
+
+
+
+ +<% }else{ %> + +
+
+ +
+ + +
+ +
+ +

Initializing...

+ Please wait while your site is initialized + +

{{ vm.restore.restoreProgress }}% restored

+ {{ vm.restore.currentActivity }} + +

Initializing your site...

+ Press the button below to begin +
+ +
+ +
+ +
+

Restore from Umbraco Cloud

+ +
+ +
+

Restoring your website...

+

{{ vm.restore.restoreProgress }}% restored

+ {{ vm.restore.currentActivity }} +
+ +
+

Ready to rock n' roll!

+

Everything has been restored and is ready for use, click below to open Umbraco

+ +
+ + + + + <%--
+

An error occurred:

+

{{ vm.restore.error.exceptionMessage }}

+ Show details +
{{ vm.restore.error.log }}
+
--%> + +
+ +
+
- - +<%= NoNodesHelper.ServerVariables(HttpContext.Current.Request.RequestContext, UmbracoContext.Current) %> + + + + + + + + + +<% } %> - + \ No newline at end of file From 3263e496aa3d0ec0ecac0e9c9971ed0321071b80 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 10 Aug 2017 00:02:27 +1000 Subject: [PATCH 03/16] U4-10201 Exception when deleting a user's start node - adds FKs, fixes delete scripts, adds migrations for FKs --- src/Umbraco.Core/Models/Rdbms/UserGroupDto.cs | 2 + .../AddUserGroupTables.cs | 39 +++++++++++++++++++ .../Repositories/ContentRepository.cs | 4 +- .../Repositories/MediaRepository.cs | 2 + .../Repositories/RecycleBinRepository.cs | 20 +++++++++- .../Services/ContentServiceTests.cs | 9 +++++ 6 files changed, 74 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Core/Models/Rdbms/UserGroupDto.cs b/src/Umbraco.Core/Models/Rdbms/UserGroupDto.cs index fdad78bfcd..f6bb09066d 100644 --- a/src/Umbraco.Core/Models/Rdbms/UserGroupDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/UserGroupDto.cs @@ -51,10 +51,12 @@ namespace Umbraco.Core.Models.Rdbms [Column("startContentId")] [NullSetting(NullSetting = NullSettings.Null)] + [ForeignKey(typeof(NodeDto), Name = "FK_startContentId_umbracoNode_id")] public int? StartContentId { get; set; } [Column("startMediaId")] [NullSetting(NullSetting = NullSettings.Null)] + [ForeignKey(typeof(NodeDto), Name = "FK_startMediaId_umbracoNode_id")] public int? StartMediaId { get; set; } [ResultColumn] diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSevenZero/AddUserGroupTables.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSevenZero/AddUserGroupTables.cs index d1cf0a40d9..36c45615a3 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSevenZero/AddUserGroupTables.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSevenZero/AddUserGroupTables.cs @@ -28,6 +28,45 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenSevenZe DeleteOldTables(tables, constraints); SetDefaultIcons(); } + else + { + //if we aren't adding the tables, make sure that the umbracoUserGroup table has the correct FKs - these + //were added after the beta release so we need to do some cleanup + //if the FK doesn't exist + if (constraints.Any(x => x.Item1.InvariantEquals("umbracoUserGroup") + && x.Item2.InvariantEquals("startContentId") + && x.Item3.InvariantEquals("FK_startContentId_umbracoNode_id")) == false) + { + //before we add any foreign key we need to make sure there's no stale data in there which would have happened in the beta + //release if a start node was assigned and then that start node was deleted. + Execute.Sql(@"UPDATE umbracoUserGroup SET startContentId = NULL WHERE startContentId NOT IN (SELECT id FROM umbracoNode)"); + + Create.ForeignKey("FK_startContentId_umbracoNode_id") + .FromTable("umbracoUserGroup") + .ForeignColumn("startContentId") + .ToTable("umbracoNode") + .PrimaryColumn("id") + .OnDelete(Rule.None) + .OnUpdate(Rule.None); + } + + if (constraints.Any(x => x.Item1.InvariantEquals("umbracoUserGroup") + && x.Item2.InvariantEquals("startMediaId") + && x.Item3.InvariantEquals("FK_startMediaId_umbracoNode_id")) == false) + { + //before we add any foreign key we need to make sure there's no stale data in there which would have happened in the beta + //release if a start node was assigned and then that start node was deleted. + Execute.Sql(@"UPDATE umbracoUserGroup SET startMediaId = NULL WHERE startMediaId NOT IN (SELECT id FROM umbracoNode)"); + + Create.ForeignKey("FK_startMediaId_umbracoNode_id") + .FromTable("umbracoUserGroup") + .ForeignColumn("startMediaId") + .ToTable("umbracoNode") + .PrimaryColumn("id") + .OnDelete(Rule.None) + .OnUpdate(Rule.None); + } + } } private void SetDefaultIcons() diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs index addd0bc925..daa8140670 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs @@ -183,6 +183,8 @@ namespace Umbraco.Core.Persistence.Repositories "DELETE FROM cmsTask WHERE nodeId = @Id", "DELETE FROM umbracoUser2NodeNotify WHERE nodeId = @Id", "DELETE FROM umbracoUserGroup2NodePermission WHERE nodeId = @Id", + "DELETE FROM umbracoUserStartNode WHERE startNode = @Id", + "UPDATE umbracoUserGroup SET startContentId = NULL WHERE startContentId = @Id", "DELETE FROM umbracoRelation WHERE parentId = @Id", "DELETE FROM umbracoRelation WHERE childId = @Id", "DELETE FROM cmsTagRelationship WHERE nodeId = @Id", @@ -194,7 +196,7 @@ namespace Umbraco.Core.Persistence.Repositories "DELETE FROM cmsContentXml WHERE nodeId = @Id", "DELETE FROM cmsContent WHERE nodeId = @Id", "DELETE FROM umbracoAccess WHERE nodeId = @Id", - "DELETE FROM umbracoNode WHERE id = @Id" + "DELETE FROM umbracoNode WHERE id = @Id" }; return list; } diff --git a/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs index ee413c2a59..be96d80f5a 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs @@ -118,6 +118,8 @@ namespace Umbraco.Core.Persistence.Repositories "DELETE FROM cmsTask WHERE nodeId = @Id", "DELETE FROM umbracoUser2NodeNotify WHERE nodeId = @Id", "DELETE FROM umbracoUserGroup2NodePermission WHERE nodeId = @Id", + "DELETE FROM umbracoUserStartNode WHERE startNode = @Id", + "UPDATE umbracoUserGroup SET startMediaId = NULL WHERE startMediaId = @Id", "DELETE FROM umbracoRelation WHERE parentId = @Id", "DELETE FROM umbracoRelation WHERE childId = @Id", "DELETE FROM cmsTagRelationship WHERE nodeId = @Id", diff --git a/src/Umbraco.Core/Persistence/Repositories/RecycleBinRepository.cs b/src/Umbraco.Core/Persistence/Repositories/RecycleBinRepository.cs index efb7487fdf..efb93bcff3 100644 --- a/src/Umbraco.Core/Persistence/Repositories/RecycleBinRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/RecycleBinRepository.cs @@ -53,7 +53,10 @@ namespace Umbraco.Core.Persistence.Repositories @"DELETE FROM umbracoRedirectUrl WHERE umbracoRedirectUrl.id IN( SELECT TB1.id FROM umbracoRedirectUrl as TB1 INNER JOIN umbracoNode as TB2 ON TB1.contentKey = TB2.uniqueId - WHERE TB2.trashed = '1' AND TB2.nodeObjectType = @NodeObjectType)", + WHERE TB2.trashed = '1' AND TB2.nodeObjectType = @NodeObjectType)", + FormatDeleteStatement("umbracoUserStartNode", "startNode"), + FormatUpdateStatement("umbracoUserGroup", "startContentId"), + FormatUpdateStatement("umbracoUserGroup", "startMediaId"), FormatDeleteStatement("umbracoRelation", "parentId"), FormatDeleteStatement("umbracoRelation", "childId"), FormatDeleteStatement("cmsTagRelationship", "nodeId"), @@ -108,6 +111,21 @@ namespace Umbraco.Core.Persistence.Repositories tableName, keyName); } + /// + /// An update statement that will update a value to NULL in the table specified where its PK (keyName) is found in the + /// list of umbracoNode.id that have trashed flag set + /// + /// + /// + /// + private string FormatUpdateStatement(string tableName, string keyName) + { + return + string.Format( + "UPDATE {0} SET {0}.{1} = NULL WHERE {0}.{1} IN (SELECT id FROM umbracoNode WHERE trashed = '1' AND nodeObjectType = @NodeObjectType)", + tableName, keyName); + } + /// /// Gets a list of files, which are referenced on items in the Recycle Bin. /// The list is generated by the convention that a file is referenced by diff --git a/src/Umbraco.Tests/Services/ContentServiceTests.cs b/src/Umbraco.Tests/Services/ContentServiceTests.cs index 5a2b9f23e7..c9daeb0022 100644 --- a/src/Umbraco.Tests/Services/ContentServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentServiceTests.cs @@ -1583,6 +1583,14 @@ namespace Umbraco.Tests.Services ServiceContext.ContentService.Save(content2, 0); Assert.IsTrue(ServiceContext.ContentService.PublishWithStatus(content2, 0).Success); + var editorGroup = ServiceContext.UserService.GetUserGroupByAlias("editor"); + editorGroup.StartContentId = content1.Id; + ServiceContext.UserService.Save(editorGroup); + + var admin = ServiceContext.UserService.GetUserById(0); + admin.StartContentIds = new[] {content1.Id}; + ServiceContext.UserService.Save(admin); + ServiceContext.RelationService.Save(new RelationType(Constants.ObjectTypes.DocumentGuid, Constants.ObjectTypes.DocumentGuid, "test")); Assert.IsNotNull(ServiceContext.RelationService.Relate(content1, content2, "test")); @@ -1608,6 +1616,7 @@ namespace Umbraco.Tests.Services }).Success); // Act + ServiceContext.ContentService.MoveToRecycleBin(content1); ServiceContext.ContentService.EmptyRecycleBin(); var contents = ServiceContext.ContentService.GetContentInRecycleBin(); From bdb991f6dc0598e2b33afb2385ad4f1be2c48502 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 18 Aug 2017 14:50:20 +1000 Subject: [PATCH 04/16] U4-10249 FolderAndFilePermissions HealthCheck causes the application to restart --- .../FolderAndFilePermissionsCheck.cs | 23 +++++++- .../Install/FilePermissionHelper.cs | 57 ++++++++++++++++++- 2 files changed, 75 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Web/HealthCheck/Checks/Permissions/FolderAndFilePermissionsCheck.cs b/src/Umbraco.Web/HealthCheck/Checks/Permissions/FolderAndFilePermissionsCheck.cs index 36e774287b..ff8c2646cc 100644 --- a/src/Umbraco.Web/HealthCheck/Checks/Permissions/FolderAndFilePermissionsCheck.cs +++ b/src/Umbraco.Web/HealthCheck/Checks/Permissions/FolderAndFilePermissionsCheck.cs @@ -59,12 +59,10 @@ namespace Umbraco.Web.HealthCheck.Checks.Permissions // in ALL circumstances or just some var pathsToCheck = new Dictionary { - { SystemDirectories.AppCode, PermissionCheckRequirement.Optional }, { SystemDirectories.Data, PermissionCheckRequirement.Required }, { SystemDirectories.Packages, PermissionCheckRequirement.Required}, { SystemDirectories.Preview, PermissionCheckRequirement.Required }, { SystemDirectories.AppPlugins, PermissionCheckRequirement.Required }, - { SystemDirectories.Bin, PermissionCheckRequirement.Optional }, { SystemDirectories.Config, PermissionCheckRequirement.Optional }, { SystemDirectories.Css, PermissionCheckRequirement.Optional }, { SystemDirectories.Masterpages, PermissionCheckRequirement.Optional }, @@ -77,12 +75,33 @@ namespace Umbraco.Web.HealthCheck.Checks.Permissions { SystemDirectories.Xslt, PermissionCheckRequirement.Optional }, }; + //These are special paths to check that will restart an app domain if a file is written to them, + //so these need to be tested differently + var pathsToCheckWithRestarts = new Dictionary + { + { SystemDirectories.AppCode, PermissionCheckRequirement.Optional }, + { SystemDirectories.Bin, PermissionCheckRequirement.Optional } + }; + // Run checks for required and optional paths for modify permission List requiredFailedPaths; List optionalFailedPaths; var requiredPathCheckResult = FilePermissionHelper.TestDirectories(GetPathsToCheck(pathsToCheck, PermissionCheckRequirement.Required), out requiredFailedPaths); var optionalPathCheckResult = FilePermissionHelper.TestDirectories(GetPathsToCheck(pathsToCheck, PermissionCheckRequirement.Optional), out optionalFailedPaths); + //now check the special folders + List requiredFailedPaths2; + List optionalFailedPaths2; + var requiredPathCheckResult2 = FilePermissionHelper.TestDirectories(GetPathsToCheck(pathsToCheckWithRestarts, PermissionCheckRequirement.Required), out requiredFailedPaths2, writeCausesRestart:true); + var optionalPathCheckResult2 = FilePermissionHelper.TestDirectories(GetPathsToCheck(pathsToCheckWithRestarts, PermissionCheckRequirement.Optional), out optionalFailedPaths2, writeCausesRestart: true); + + requiredPathCheckResult = requiredPathCheckResult && requiredPathCheckResult2; + optionalPathCheckResult = optionalPathCheckResult && optionalPathCheckResult2; + + //combine the paths + requiredFailedPaths = requiredFailedPaths.Concat(requiredFailedPaths2).ToList(); + optionalFailedPaths = requiredFailedPaths.Concat(optionalFailedPaths2).ToList(); + return GetStatus(requiredPathCheckResult, requiredFailedPaths, optionalPathCheckResult, optionalFailedPaths, PermissionCheckFor.Folder); } diff --git a/src/Umbraco.Web/Install/FilePermissionHelper.cs b/src/Umbraco.Web/Install/FilePermissionHelper.cs index 87491b93a8..a91bd6d306 100644 --- a/src/Umbraco.Web/Install/FilePermissionHelper.cs +++ b/src/Umbraco.Web/Install/FilePermissionHelper.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Web; using System.IO; +using System.Security.AccessControl; using Umbraco.Core.IO; using umbraco; @@ -37,8 +38,19 @@ namespace Umbraco.Web.Install return errorReport.Any() == false; } - - public static bool TestDirectories(string[] directories, out List errorReport) + + /// + /// This will test the directories for write access + /// + /// + /// + /// + /// If this is false, the easiest way to test for write access is to write a temp file, however some folder will cause + /// an App Domain restart if a file is written to the folder, so in that case we need to use the ACL APIs which aren't as + /// reliable but we cannot write a file since it will cause an app domain restart. + /// + /// + public static bool TestDirectories(string[] directories, out List errorReport, bool writeCausesRestart = false) { errorReport = new List(); bool succes = true; @@ -46,7 +58,11 @@ namespace Umbraco.Web.Install { if (Directory.Exists(dir) == false) continue; - bool result = SaveAndDeleteFile(IOHelper.MapPath(dir + "/configWizardPermissionTest.txt")); + var folder = IOHelper.MapPath(dir); + + var result = writeCausesRestart + ? HasWritePermissionOnDir(folder) + : SaveAndDeleteFile(Path.Combine(folder, "configWizardPermissionTest.txt")); if (result == false) { @@ -131,7 +147,42 @@ namespace Umbraco.Web.Install { return false; } + } + private static bool HasWritePermissionOnDir(string path) + { + var writeAllow = false; + var writeDeny = false; + var accessControlList = Directory.GetAccessControl(path); + if (accessControlList == null) + return false; + AuthorizationRuleCollection accessRules; + try + { + accessRules = accessControlList.GetAccessRules(true, true, typeof(System.Security.Principal.SecurityIdentifier)); + if (accessRules == null) + return false; + } + catch (Exception e) + { + //This is not 100% accurate btw because it could turn out that the current user doesn't + //have access to read the current permissions but does have write access. + //I think this is an edge case however + return false; + } + + foreach (FileSystemAccessRule rule in accessRules) + { + if ((FileSystemRights.Write & rule.FileSystemRights) != FileSystemRights.Write) + continue; + + if (rule.AccessControlType == AccessControlType.Allow) + writeAllow = true; + else if (rule.AccessControlType == AccessControlType.Deny) + writeDeny = true; + } + + return writeAllow && writeDeny == false; } private static bool OpenFileForWrite(string file) From 04d73eedfe8399fd74423fbf45a177b751126e27 Mon Sep 17 00:00:00 2001 From: Zsolt Date: Fri, 18 Aug 2017 10:56:38 +0200 Subject: [PATCH 05/16] documentation for umbUserGoupPreview directive --- .../users/umbusergrouppreview.directive.js | 51 ++++++++++++++++++- .../src/views/users/user.html | 2 +- 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/users/umbusergrouppreview.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/users/umbusergrouppreview.directive.js index 7623dc0ae8..fdbfd088ab 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/users/umbusergrouppreview.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/users/umbusergrouppreview.directive.js @@ -1,3 +1,51 @@ +/** +@ngdoc directive +@name umbraco.directives.directive:umbUserGroupPreview +@restrict E +@scope + +@description +Use this directive to render a user group preview, where you can see the permissions the user or group has in the back office. + +

Markup example

+
+    
+ + +
+
+ +@param {string} icon (binding): The user group icon. +@param {string} name (binding): The user group name. +@param {array} sections (binding) Lists out the sections where the user has authority to edit. +@param {string} contentStartNode (binding) +
    +
  • The starting point in the tree of the content section.
  • +
  • So the user has no authority to work on other branches, only on this branch in the content section.
  • +
+@param {boolean} hideContentStartNode (binding) Hides the contentStartNode. +@param {string} mediaStartNode (binding) +
    +
  • The starting point in the tree of the media section.
  • +
  • So the user has no authority to work on other branches, only on this branch in the media section.
  • +
+@param {boolean} hideMediaStartNode (binding) Hides the mediaStartNode. +@param {array} permissions (binding) A list of permissions, the user can have. +@param {boolean} allowRemove (binding): Shows or Hides the remove button. +@param {function} onRemove (expression): Callback function when the remove button is clicked. +@param {boolean} allowEdit (binding): Shows or Hides the edit button. +@param {function} onEdit (expression): Callback function when the edit button is clicked. +**/ + + (function () { 'use strict'; @@ -14,9 +62,8 @@ scope: { icon: "=?", name: "=", - description: "=?", sections: "=?", - contentStartNode: "=?", + contentStartNode: "=?", hideContentStartNode: "@?", mediaStartNode: "=?", hideMediaStartNode: "@?", diff --git a/src/Umbraco.Web.UI.Client/src/views/users/user.html b/src/Umbraco.Web.UI.Client/src/views/users/user.html index 886054c3e8..ea94687584 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/user.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/user.html @@ -93,7 +93,7 @@ media-start-node="userGroup.mediaStartNode" allow-remove="!vm.user.isCurrentUser" on-remove="vm.removeSelectedItem($index, vm.user.userGroups)"> - + Date: Mon, 21 Aug 2017 13:33:20 +0200 Subject: [PATCH 06/16] testfzfzfg --- src/Umbraco.Web.UI.Client/src/views/users/user.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/users/user.html b/src/Umbraco.Web.UI.Client/src/views/users/user.html index ea94687584..9d64315a78 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/user.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/user.html @@ -13,7 +13,7 @@ hide-icon="true" hide-description="true" hide-alias="true"> - + From 9fabfa92eab11b7ae153e2cba2eb70cd4491398c Mon Sep 17 00:00:00 2001 From: Zsolt Date: Mon, 21 Aug 2017 13:48:35 +0200 Subject: [PATCH 07/16] changing back the previous test --- src/Umbraco.Web.UI.Client/src/views/users/user.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/users/user.html b/src/Umbraco.Web.UI.Client/src/views/users/user.html index 9d64315a78..ea94687584 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/user.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/user.html @@ -13,7 +13,7 @@ hide-icon="true" hide-description="true" hide-alias="true"> - + From 86bc701511608894336d48d6bbb627791e6ffb4c Mon Sep 17 00:00:00 2001 From: Anders Bjerner Date: Mon, 21 Aug 2017 14:16:01 +0200 Subject: [PATCH 08/16] Typo Very minor typo, but still ;) --- src/Umbraco.Core/StringExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Core/StringExtensions.cs b/src/Umbraco.Core/StringExtensions.cs index 87b4c676b5..d6abc597e3 100644 --- a/src/Umbraco.Core/StringExtensions.cs +++ b/src/Umbraco.Core/StringExtensions.cs @@ -633,7 +633,7 @@ namespace Umbraco.Core } /// - /// Ensures that the folder path endds with a DirectorySeperatorChar + /// Ensures that the folder path ends with a DirectorySeperatorChar /// /// /// From c5e1fc5a2062985d8aed788a8f212ac48a50ac9e Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 23 Aug 2017 15:45:42 +1000 Subject: [PATCH 09/16] U4-10348 Show readonly user profile information on user editor --- .../Models/ContentEditing/UserDisplay.cs | 39 +++++++- .../Models/Mapping/UserModelMapper.cs | 93 +++++++++++-------- 2 files changed, 92 insertions(+), 40 deletions(-) diff --git a/src/Umbraco.Web/Models/ContentEditing/UserDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/UserDisplay.cs index 4cff43e3b8..8a79344c8e 100644 --- a/src/Umbraco.Web/Models/ContentEditing/UserDisplay.cs +++ b/src/Umbraco.Web/Models/ContentEditing/UserDisplay.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.ComponentModel; using System.Globalization; using System.Runtime.Serialization; @@ -38,6 +39,40 @@ namespace Umbraco.Web.Models.ContentEditing [DataMember(Name = "resetPasswordValue")] [ReadOnly(true)] public string ResetPasswordValue { get; set; } - + + /// + /// A readonly value showing the user's current calculated start content ids + /// + [DataMember(Name = "calculatedStartContentIds")] + [ReadOnly(true)] + public IEnumerable CalculatedStartContentIds { get; set; } + + /// + /// A readonly value showing the user's current calculated start media ids + /// + [DataMember(Name = "calculatedStartMediaIds")] + [ReadOnly(true)] + public IEnumerable CalculatedStartMediaIds { get; set; } + + [DataMember(Name = "failedPasswordAttempts")] + [ReadOnly(true)] + public int FailedPasswordAttempts { get; set; } + + [DataMember(Name = "lastLockoutDate")] + [ReadOnly(true)] + public DateTime LastLockoutDate { get; set; } + + [DataMember(Name = "lastPasswordChangeDate")] + [ReadOnly(true)] + public DateTime LastPasswordChangeDate { get; set; } + + [DataMember(Name = "createDate")] + [ReadOnly(true)] + public DateTime CreateDate { get; set; } + + [DataMember(Name = "updateDate")] + [ReadOnly(true)] + public DateTime UpdateDate { get; set; } + } } \ No newline at end of file diff --git a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs index 02f400cdf9..bd25c5da07 100644 --- a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs @@ -228,13 +228,45 @@ namespace Umbraco.Web.Models.Mapping display.AssignedPermissions = allAssignedPermissions; }); + //Important! Currently we are never mapping to multiple UserDisplay objects but if we start doing that + // this will cause an N+1 and we'll need to change how this works. config.CreateMap() .ForMember(detail => detail.Avatars, opt => opt.MapFrom(user => user.GetCurrentUserAvatarUrls(applicationContext.Services.UserService, applicationContext.ApplicationCache.RuntimeCache))) .ForMember(detail => detail.Username, opt => opt.MapFrom(user => user.Username)) .ForMember(detail => detail.LastLoginDate, opt => opt.MapFrom(user => user.LastLoginDate == default(DateTime) ? null : (DateTime?) user.LastLoginDate)) .ForMember(detail => detail.UserGroups, opt => opt.MapFrom(user => user.Groups)) - .ForMember(detail => detail.StartContentIds, opt => opt.UseValue(Enumerable.Empty())) - .ForMember(detail => detail.StartMediaIds, opt => opt.UseValue(Enumerable.Empty())) + .ForMember( + detail => detail.CalculatedStartContentIds, + opt => opt.MapFrom(user => GetStartNodeValues( + user.CalculateContentStartNodeIds(applicationContext.Services.EntityService), + applicationContext.Services.TextService, + applicationContext.Services.EntityService, + UmbracoObjectTypes.Document, + "content/contentRoot"))) + .ForMember( + detail => detail.CalculatedStartMediaIds, + opt => opt.MapFrom(user => GetStartNodeValues( + user.CalculateMediaStartNodeIds(applicationContext.Services.EntityService), + applicationContext.Services.TextService, + applicationContext.Services.EntityService, + UmbracoObjectTypes.Document, + "media/mediaRoot"))) + .ForMember( + detail => detail.StartContentIds, + opt => opt.MapFrom(user => GetStartNodeValues( + user.StartContentIds.ToArray(), + applicationContext.Services.TextService, + applicationContext.Services.EntityService, + UmbracoObjectTypes.Document, + "content/contentRoot"))) + .ForMember( + detail => detail.StartMediaIds, + opt => opt.MapFrom(user => GetStartNodeValues( + user.StartMediaIds.ToArray(), + applicationContext.Services.TextService, + applicationContext.Services.EntityService, + UmbracoObjectTypes.Media, + "media/mediaRoot"))) .ForMember(detail => detail.Culture, opt => opt.MapFrom(user => user.GetUserCulture(applicationContext.Services.TextService))) .ForMember( detail => detail.AvailableCultures, @@ -252,40 +284,7 @@ namespace Umbraco.Web.Models.Mapping .ForMember(detail => detail.ResetPasswordValue, opt => opt.Ignore()) .ForMember(detail => detail.Alias, opt => opt.Ignore()) .ForMember(detail => detail.Trashed, opt => opt.Ignore()) - .ForMember(detail => detail.AdditionalData, opt => opt.Ignore()) - .AfterMap((user, display) => - { - //Important! Currently we are never mapping to multiple UserDisplay objects but if we start doing that - // this will cause an N+1 and we'll need to change how this works. - - var startContentIds = user.StartContentIds.ToArray(); - if (startContentIds.Length > 0) - { - //TODO: Update GetAll to be able to pass in a parameter like on the normal Get to NOT load in the entire object! - var startNodes = new List(); - if (startContentIds.Contains(-1)) - { - startNodes.Add(RootNode(applicationContext.Services.TextService.Localize("content/contentRoot"))); - } - var contentItems = applicationContext.Services.EntityService.GetAll(UmbracoObjectTypes.Document, startContentIds); - startNodes.AddRange(Mapper.Map, IEnumerable>(contentItems)); - display.StartContentIds = startNodes; - - - } - var startMediaIds = user.StartMediaIds.ToArray(); - if (startMediaIds.Length > 0) - { - var startNodes = new List(); - if (startContentIds.Contains(-1)) - { - startNodes.Add(RootNode(applicationContext.Services.TextService.Localize("media/mediaRoot"))); - } - var mediaItems = applicationContext.Services.EntityService.GetAll(UmbracoObjectTypes.Media, startMediaIds); - startNodes.AddRange(Mapper.Map, IEnumerable>(mediaItems)); - display.StartMediaIds = startNodes; - } - }); + .ForMember(detail => detail.AdditionalData, opt => opt.Ignore()); config.CreateMap() //Loading in the user avatar's requires an external request if they don't have a local file avatar, this means that initial load of paging may incur a cost @@ -323,7 +322,7 @@ namespace Umbraco.Web.Models.Mapping opt => opt.MapFrom(user => user.Email.ToLowerInvariant().Trim().GenerateHash())) .ForMember(detail => detail.SecondsUntilTimeout, opt => opt.Ignore()) .AfterMap((user, detail) => - { + { //we need to map the legacy UserType //the best we can do here is to return the user's first user group as a IUserType object //but we should attempt to return any group that is the built in ones first @@ -342,7 +341,7 @@ namespace Umbraco.Web.Models.Mapping detail.UserType = foundBuiltIn.Alias; } else - { + { //otherwise return the first detail.UserType = groups[0].Alias; } @@ -367,6 +366,24 @@ namespace Umbraco.Web.Models.Mapping } + private IEnumerable GetStartNodeValues(int[] startNodeIds, + ILocalizedTextService textService, IEntityService entityService, UmbracoObjectTypes objectType, + string localizedKey) + { + if (startNodeIds.Length > 0) + { + var startNodes = new List(); + if (startNodeIds.Contains(-1)) + { + startNodes.Add(RootNode(textService.Localize(localizedKey))); + } + var mediaItems = entityService.GetAll(objectType, startNodeIds); + startNodes.AddRange(Mapper.Map, IEnumerable>(mediaItems)); + return startNodes; + } + return Enumerable.Empty(); + } + private void MapUserGroupBasic(ServiceContext services, dynamic group, UserGroupBasic display) { var allSections = services.SectionService.GetSections(); From f48ffd28c0fd6fdc03e576d81598079b3facdae6 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 23 Aug 2017 15:59:47 +1000 Subject: [PATCH 10/16] reverts files that shouldn't have been committed --- src/Umbraco.Web.UI/config/Dashboard.config | 10 -- .../config/splashes/noNodes.aspx | 155 ++++-------------- 2 files changed, 32 insertions(+), 133 deletions(-) diff --git a/src/Umbraco.Web.UI/config/Dashboard.config b/src/Umbraco.Web.UI/config/Dashboard.config index c8c7e32f27..322551545d 100644 --- a/src/Umbraco.Web.UI/config/Dashboard.config +++ b/src/Umbraco.Web.UI/config/Dashboard.config @@ -119,14 +119,4 @@ -
- - content - - - - /App_Plugins/Deploy/views/dashboards/dashboard.html - - -
\ No newline at end of file diff --git a/src/Umbraco.Web.UI/config/splashes/noNodes.aspx b/src/Umbraco.Web.UI/config/splashes/noNodes.aspx index c1ceabf05b..d46ecd6113 100644 --- a/src/Umbraco.Web.UI/config/splashes/noNodes.aspx +++ b/src/Umbraco.Web.UI/config/splashes/noNodes.aspx @@ -1,7 +1,7 @@ -<%@ Page Language="C#" AutoEventWireup="true"%> +<%@ Page Language="C#" AutoEventWireup="True" Inherits="Umbraco.Web.UI.Config.Splashes.NoNodes" CodeBehind="NoNodes.aspx.cs" %> +<%@ Import Namespace="Umbraco.Core.Configuration" %> <%@ Import Namespace="Umbraco.Core.IO" %> -<%@ Import Namespace="Umbraco.Deploy.UI" %> -<%@ Import Namespace="Umbraco.Web" %> + @@ -11,142 +11,51 @@ - + - - - - - - - + -<% if(HttpContext.Current.Request.IsLocal == false){ %>
-
-
- +
+
+ -

Welcome to your Umbraco installation

-

You're seeing the wonderful page because your website doesn't contain any published content yet.

+

Welcome to your Umbraco installation

+

You're seeing this wonderful page because your website doesn't contain any published content yet.

-
+ -
-
-

Easy start with Umbraco.tv

-

We have created a bunch of 'how-to' videos, to get you easily started with Umbraco. Learn how to build projects in just a couple of minutes. Easiest CMS in the world.

- - Umbraco.tv → -
+
+
+

Easy start with Umbraco.tv

+

We have created a bunch of 'how-to' videos, to get you easily started with Umbraco. Learn how to build projects in just a couple of minutes. Easiest CMS in the world.

+ + Umbraco.tv → +
-
-

Be a part of the community

-

The Umbraco community is the best of its kind, be sure to visit, and if you have any questions, we’re sure that you can get your answers from the community.

- - our.Umbraco → -
-
+
+

Be a part of the community

+

The Umbraco community is the best of its kind, be sure to visit, and if you have any questions, we're sure that you can get your answers from the community.

+ + our.Umbraco → +
+
-
-
-
- -<% }else{ %> - -
-
- -
- - -
- -
- -

Initializing...

- Please wait while your site is initialized - -

{{ vm.restore.restoreProgress }}% restored

- {{ vm.restore.currentActivity }} - -

Initializing your site...

- Press the button below to begin -
- -
- -
- -
-

Restore from Umbraco Cloud

- -
- -
-

Restoring your website...

-

{{ vm.restore.restoreProgress }}% restored

- {{ vm.restore.currentActivity }} -
- -
-

Ready to rock n' roll!

-

Everything has been restored and is ready for use, click below to open Umbraco

- -
- - - - - <%--
-

An error occurred:

-

{{ vm.restore.error.exceptionMessage }}

- Show details -
{{ vm.restore.error.log }}
-
--%> - -
- -
-
+ +
-<%= NoNodesHelper.ServerVariables(HttpContext.Current.Request.RequestContext, UmbracoContext.Current) %> - - - - - - - - - -<% } %> + + - \ No newline at end of file + From a0785f2e0d8bd7e5486d1e155b8f69d76d42c57f Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 23 Aug 2017 16:22:01 +1000 Subject: [PATCH 11/16] backports U4-10349 Optimize EnsureUniqueNodeName --- .../Repositories/ContentRepository.cs | 27 +---- .../DataTypeDefinitionRepository.cs | 29 +---- .../Repositories/MediaRepository.cs | 27 +---- .../Repositories/SimilarNodeName.cs | 112 ++++++++++++++++++ .../Repositories/SimilarNodeNameComparer.cs | 45 ------- src/Umbraco.Core/Umbraco.Core.csproj | 2 +- .../Repositories/SimilarNodeNameTests.cs | 98 +++++++++++++++ src/Umbraco.Tests/Umbraco.Tests.csproj | 1 + 8 files changed, 221 insertions(+), 120 deletions(-) create mode 100644 src/Umbraco.Core/Persistence/Repositories/SimilarNodeName.cs delete mode 100644 src/Umbraco.Core/Persistence/Repositories/SimilarNodeNameComparer.cs create mode 100644 src/Umbraco.Tests/Persistence/Repositories/SimilarNodeNameTests.cs diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs index f3d199e175..eac9d197be 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs @@ -1122,31 +1122,10 @@ ORDER BY cmsContentVersion.id DESC if (EnsureUniqueNaming == false) return nodeName; - var sql = new Sql(); - sql.Select("*") - .From() - .Where(x => x.NodeObjectType == NodeObjectTypeId && x.ParentId == parentId && x.Text.StartsWith(nodeName)); + var names = Database.Fetch("SELECT id, text AS name FROM umbracoNode WHERE nodeObjectType=@objectType AND parentId=@parentId", + new { objectType = NodeObjectTypeId, parentId }); - int uniqueNumber = 1; - var currentName = nodeName; - - var dtos = Database.Fetch(sql); - if (dtos.Any()) - { - var results = dtos.OrderBy(x => x.Text, new SimilarNodeNameComparer()); - foreach (var dto in results) - { - if (id != 0 && id == dto.NodeId) continue; - - if (dto.Text.ToLowerInvariant().Equals(currentName.ToLowerInvariant())) - { - currentName = nodeName + string.Format(" ({0})", uniqueNumber); - uniqueNumber++; - } - } - } - - return currentName; + return SimilarNodeName.GetUniqueName(names, id, nodeName); } /// diff --git a/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs b/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs index 1690b36148..eb64ea190b 100644 --- a/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs @@ -450,33 +450,10 @@ AND umbracoNode.id <> @id", private string EnsureUniqueNodeName(string nodeName, int id = 0) { + var names = Database.Fetch("SELECT id, text AS name FROM umbracoNode WHERE nodeObjectType=@objectType", + new { objectType = NodeObjectTypeId }); - - var sql = new Sql(); - sql.Select("*") - .From(SqlSyntax) - .Where(x => x.NodeObjectType == NodeObjectTypeId && x.Text.StartsWith(nodeName)); - - int uniqueNumber = 1; - var currentName = nodeName; - - var dtos = Database.Fetch(sql); - if (dtos.Any()) - { - var results = dtos.OrderBy(x => x.Text, new SimilarNodeNameComparer()); - foreach (var dto in results) - { - if (id != 0 && id == dto.NodeId) continue; - - if (dto.Text.ToLowerInvariant().Equals(currentName.ToLowerInvariant())) - { - currentName = nodeName + string.Format(" ({0})", uniqueNumber); - uniqueNumber++; - } - } - } - - return currentName; + return SimilarNodeName.GetUniqueName(names, id, nodeName); } /// diff --git a/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs index c9f06c6c75..598b16d78d 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs @@ -584,31 +584,10 @@ namespace Umbraco.Core.Persistence.Repositories if (EnsureUniqueNaming == false) return nodeName; - var sql = new Sql(); - sql.Select("*") - .From() - .Where(x => x.NodeObjectType == NodeObjectTypeId && x.ParentId == parentId && x.Text.StartsWith(nodeName)); + var names = Database.Fetch("SELECT id, text AS name FROM umbracoNode WHERE nodeObjectType=@objectType AND parentId=@parentId", + new { objectType = NodeObjectTypeId, parentId }); - int uniqueNumber = 1; - var currentName = nodeName; - - var dtos = Database.Fetch(sql); - if (dtos.Any()) - { - var results = dtos.OrderBy(x => x.Text, new SimilarNodeNameComparer()); - foreach (var dto in results) - { - if (id != 0 && id == dto.NodeId) continue; - - if (dto.Text.ToLowerInvariant().Equals(currentName.ToLowerInvariant())) - { - currentName = nodeName + string.Format(" ({0})", uniqueNumber); - uniqueNumber++; - } - } - } - - return currentName; + return SimilarNodeName.GetUniqueName(names, id, nodeName); } /// diff --git a/src/Umbraco.Core/Persistence/Repositories/SimilarNodeName.cs b/src/Umbraco.Core/Persistence/Repositories/SimilarNodeName.cs new file mode 100644 index 0000000000..371f73b27f --- /dev/null +++ b/src/Umbraco.Core/Persistence/Repositories/SimilarNodeName.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Umbraco.Core.Persistence.Repositories +{ + internal class SimilarNodeName + { + private int _numPos = -2; + + public int Id { get; set; } + public string Name { get; set; } + + // cached - reused + public int NumPos + { + get + { + if (_numPos != -2) return _numPos; + + var name = Name; + + if (name[name.Length - 1] != ')') + return _numPos = -1; + + var pos = name.LastIndexOf('('); + if (pos < 2 || pos == name.Length - 2) // < 2 and not < 0, because we want at least "x (" + return _numPos = -1; + + return _numPos = pos; + } + } + + // not cached - used only once + public int NumVal + { + get + { + if (NumPos < 0) + throw new InvalidOperationException(); + int num; + if (int.TryParse(Name.Substring(NumPos + 1, Name.Length - 2 - NumPos), out num)) + return num; + return 0; + } + } + + // compare without allocating, nor parsing integers + internal class Comparer : IComparer + { + public int Compare(SimilarNodeName x, SimilarNodeName y) + { + if (x == null) throw new ArgumentNullException("x"); + if (y == null) throw new ArgumentNullException("y"); + + var xpos = x.NumPos; + var ypos = y.NumPos; + + var xname = x.Name; + var yname = y.Name; + + if (xpos < 0 || ypos < 0 || xpos != ypos) + return string.Compare(xname, yname, StringComparison.Ordinal); + + // compare the part before (number) + var n = string.Compare(xname, 0, yname, 0, xpos, StringComparison.Ordinal); + if (n != 0) + return n; + + // compare (number) lengths + var diff = xname.Length - yname.Length; + if (diff != 0) return diff < 0 ? -1 : +1; + + // actually compare (number) + var i = xpos; + while (i < xname.Length - 1) + { + if (xname[i] != yname[i]) + return xname[i] < yname[i] ? -1 : +1; + i++; + } + return 0; + } + } + + // gets a unique name + public static string GetUniqueName(IEnumerable names, int nodeId, string nodeName) + { + var uniqueNumber = 1; + var uniqueing = false; + foreach (var name in names.OrderBy(x => x, new Comparer())) + { + // ignore self + if (nodeId != 0 && name.Id == nodeId) continue; + + if (uniqueing) + { + if (name.NumPos > 0 && name.Name.StartsWith(nodeName) && name.NumVal == uniqueNumber) + uniqueNumber++; + else + break; + } + else if (name.Name.InvariantEquals(nodeName)) + { + uniqueing = true; + } + } + + return uniqueing ? string.Concat(nodeName, " (", uniqueNumber.ToString(), ")") : nodeName; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/SimilarNodeNameComparer.cs b/src/Umbraco.Core/Persistence/Repositories/SimilarNodeNameComparer.cs deleted file mode 100644 index ce25486422..0000000000 --- a/src/Umbraco.Core/Persistence/Repositories/SimilarNodeNameComparer.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Umbraco.Core.Persistence.Repositories -{ - /// - /// Comparer that takes into account the duplicate index of a node name - /// This is needed as a normal alphabetic sort would go Page (1), Page (10), Page (2) etc. - /// - internal class SimilarNodeNameComparer : IComparer - { - public int Compare(string x, string y) - { - if (x.LastIndexOf('(') != -1 && x.LastIndexOf(')') == x.Length - 1 && y.LastIndexOf(')') == y.Length - 1) - { - if (x.ToLower().Substring(0, x.LastIndexOf('(')) == y.ToLower().Substring(0, y.LastIndexOf('('))) - { - int xDuplicateIndex = ExtractDuplicateIndex(x); - int yDuplicateIndex = ExtractDuplicateIndex(y); - - if (xDuplicateIndex != 0 && yDuplicateIndex != 0) - { - return xDuplicateIndex.CompareTo(yDuplicateIndex); - } - } - } - return String.Compare(x.ToLower(), y.ToLower(), StringComparison.Ordinal); - } - - private int ExtractDuplicateIndex(string text) - { - int index = 0; - - if (text.LastIndexOf('(') != -1 && text.LastIndexOf('(') < text.Length - 2) - { - int startPos = text.LastIndexOf('(') + 1; - int length = text.Length - 1 - startPos; - - int.TryParse(text.Substring(startPos, length), out index); - } - - return index; - } - } -} \ No newline at end of file diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 24fdf32150..228905226f 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -586,6 +586,7 @@ + @@ -1188,7 +1189,6 @@ - diff --git a/src/Umbraco.Tests/Persistence/Repositories/SimilarNodeNameTests.cs b/src/Umbraco.Tests/Persistence/Repositories/SimilarNodeNameTests.cs new file mode 100644 index 0000000000..72f3e39874 --- /dev/null +++ b/src/Umbraco.Tests/Persistence/Repositories/SimilarNodeNameTests.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using NUnit.Framework; +using Umbraco.Core.Persistence.Repositories; + +namespace Umbraco.Tests.Persistence.Repositories +{ + [TestFixture] + public class SimilarNodeNameTests + { + [TestCase("Alpha", "Alpha", 0)] + [TestCase("Alpha", "ALPHA", +1)] // case is important + [TestCase("Alpha", "Bravo", -1)] + [TestCase("Bravo", "Alpha", +1)] + [TestCase("Alpha (1)", "Alpha (1)", 0)] + [TestCase("Alpha", "Alpha (1)", -1)] + [TestCase("Alpha (1)", "Alpha", +1)] + [TestCase("Alpha (1)", "Alpha (2)", -1)] + [TestCase("Alpha (2)", "Alpha (1)", +1)] + [TestCase("Alpha (2)", "Alpha (10)", -1)] // this is the real stuff + [TestCase("Alpha (10)", "Alpha (2)", +1)] // this is the real stuff + [TestCase("Kilo", "Golf (2)", +1)] + [TestCase("Kilo (1)", "Golf (2)", +1)] + public void ComparerTest(string name1, string name2, int expected) + { + var comparer = new SimilarNodeName.Comparer(); + + var result = comparer.Compare(new SimilarNodeName { Name = name1 }, new SimilarNodeName { Name = name2 }); + if (expected == 0) + Assert.AreEqual(0, result); + else if (expected < 0) + Assert.IsTrue(result < 0, "Expected <0 but was " + result); + else if (expected > 0) + Assert.IsTrue(result > 0, "Expected >0 but was " + result); + } + + [Test] + public void OrderByTest() + { + var names = new[] + { + new SimilarNodeName { Id = 1, Name = "Alpha (2)" }, + new SimilarNodeName { Id = 2, Name = "Alpha" }, + new SimilarNodeName { Id = 3, Name = "Golf" }, + new SimilarNodeName { Id = 4, Name = "Zulu" }, + new SimilarNodeName { Id = 5, Name = "Mike" }, + new SimilarNodeName { Id = 6, Name = "Kilo (1)" }, + new SimilarNodeName { Id = 7, Name = "Yankee" }, + new SimilarNodeName { Id = 8, Name = "Kilo" }, + new SimilarNodeName { Id = 9, Name = "Golf (2)" }, + new SimilarNodeName { Id = 10, Name = "Alpha (1)" }, + }; + + var ordered = names.OrderBy(x => x, new SimilarNodeName.Comparer()).ToArray(); + + var i = 0; + Assert.AreEqual(2, ordered[i++].Id); + Assert.AreEqual(10, ordered[i++].Id); + Assert.AreEqual(1, ordered[i++].Id); + Assert.AreEqual(3, ordered[i++].Id); + Assert.AreEqual(9, ordered[i++].Id); + Assert.AreEqual(8, ordered[i++].Id); + Assert.AreEqual(6, ordered[i++].Id); + Assert.AreEqual(5, ordered[i++].Id); + Assert.AreEqual(7, ordered[i++].Id); + Assert.AreEqual(4, ordered[i++].Id); + } + + [TestCase(0, "Charlie", "Charlie")] + [TestCase(0, "Zulu", "Zulu (1)")] + [TestCase(0, "Golf", "Golf (1)")] + [TestCase(0, "Kilo", "Kilo (2)")] + [TestCase(0, "Alpha", "Alpha (3)")] + [TestCase(0, "Kilo (1)", "Kilo (1) (1)")] // though... we might consider "Kilo (2)" + [TestCase(6, "Kilo (1)", "Kilo (1)")] // because of the id + public void Test(int nodeId, string nodeName, string expected) + { + var names = new[] + { + new SimilarNodeName { Id = 1, Name = "Alpha (2)" }, + new SimilarNodeName { Id = 2, Name = "Alpha" }, + new SimilarNodeName { Id = 3, Name = "Golf" }, + new SimilarNodeName { Id = 4, Name = "Zulu" }, + new SimilarNodeName { Id = 5, Name = "Mike" }, + new SimilarNodeName { Id = 6, Name = "Kilo (1)" }, + new SimilarNodeName { Id = 7, Name = "Yankee" }, + new SimilarNodeName { Id = 8, Name = "Kilo" }, + new SimilarNodeName { Id = 9, Name = "Golf (2)" }, + new SimilarNodeName { Id = 10, Name = "Alpha (1)" }, + }; + + Assert.AreEqual(expected, SimilarNodeName.GetUniqueName(names, nodeId, nodeName)); + } + } +} diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 067109d8bf..6859cb899f 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -163,6 +163,7 @@ + From 6796f4d4f5d040bef399cc9a8e421c8235faa657 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Wed, 23 Aug 2017 13:08:25 +0200 Subject: [PATCH 12/16] U4-10348 Show readonly user profile information on user editor --- .../src/views/users/user.controller.js | 37 +++--- .../src/views/users/user.html | 111 +++++++++++++----- .../umbraco/config/lang/en_us.xml | 7 ++ 3 files changed, 113 insertions(+), 42 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js index a9b17998d5..89b1334baf 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js @@ -66,11 +66,7 @@ vm.user = user; makeBreadcrumbs(vm.user); setUserDisplayState(); - - // format dates to local - if(vm.user.lastLoginDate) { - vm.user.formattedLastLogin = getLocalDate(vm.user.lastLoginDate, "MMMM Do YYYY, HH:mm"); - } + formatDatesToLocal(vm.user); vm.emailIsUsername = user.email === user.username; @@ -89,18 +85,20 @@ } function getLocalDate(date, format) { - var dateVal; - var serverOffset = Umbraco.Sys.ServerVariables.application.serverTimeOffset; - var localOffset = new Date().getTimezoneOffset(); - var serverTimeNeedsOffsetting = (-serverOffset !== localOffset); + if(date) { + var dateVal; + var serverOffset = Umbraco.Sys.ServerVariables.application.serverTimeOffset; + var localOffset = new Date().getTimezoneOffset(); + var serverTimeNeedsOffsetting = (-serverOffset !== localOffset); - if(serverTimeNeedsOffsetting) { - dateVal = dateHelper.convertToLocalMomentTime(date, serverOffset); - } else { - dateVal = moment(date, "YYYY-MM-DD HH:mm:ss"); + if(serverTimeNeedsOffsetting) { + dateVal = dateHelper.convertToLocalMomentTime(date, serverOffset); + } else { + dateVal = moment(date, "YYYY-MM-DD HH:mm:ss"); + } + + return dateVal.format(format); } - - return dateVal.format(format); } function toggleChangePassword() { @@ -128,6 +126,7 @@ vm.user = saved; setUserDisplayState(); + formatDatesToLocal(vm.user); vm.changePasswordModel.isChanging = false; vm.page.saveButtonState = "success"; @@ -380,6 +379,14 @@ vm.user.userDisplayState = usersHelper.getUserStateFromValue(vm.user.userState); } + function formatDatesToLocal(user) { + user.formattedLastLogin = getLocalDate(user.lastLoginDate, "MMMM Do YYYY, HH:mm"); + user.formattedLastLockoutDate = getLocalDate(user.lastLockoutDate, "MMMM Do YYYY, HH:mm"); + user.formattedCreateDate = getLocalDate(user.createDate, "MMMM Do YYYY, HH:mm"); + user.formattedUpdateDate = getLocalDate(user.updateDate, "MMMM Do YYYY, HH:mm"); + user.formattedLastPasswordChangeDate = getLocalDate(user.lastPasswordChangeDate, "MMMM Do YYYY, HH:mm"); + } + init(); } angular.module("umbraco").controller("Umbraco.Editors.Users.UserController", UserEditController); diff --git a/src/Umbraco.Web.UI.Client/src/views/users/user.html b/src/Umbraco.Web.UI.Client/src/views/users/user.html index 88ae4bf46e..99cfcf348f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/user.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/user.html @@ -1,9 +1,5 @@
- - -
@@ -17,6 +13,10 @@ + + +
@@ -164,6 +164,7 @@
+
@@ -209,27 +210,8 @@
-
-
- Status:
-
- - {{vm.user.userDisplayState.name}} - -
-
- -
-
- Last login: -
-
- {{ vm.user.formattedLastLogin }} - {{ vm.user.name }} has not logged in yet -
-
- -
+ +
+ +
+
+ Status: +
+
+ + {{vm.user.userDisplayState.name}} + +
+
+ +
+
+ Last login: +
+
+ {{ vm.user.formattedLastLogin }} + {{ vm.user.name | umbWordLimit:1 }} has not logged in yet +
+
+ +
+
+ Failed login attempts: +
+
+ {{ vm.user.failedPasswordAttempts }} +
+
+ +
+
+ Last lockout date: +
+
+ + {{ vm.user.name | umbWordLimit:1 }} hasn't been locked out + + {{ vm.user.formattedLastLockoutDate }} +
+
+ +
+
+ Password is last changed: +
+
+ + The password hasn't been changed + + {{ vm.user.formattedLastPasswordChangeDate }} +
+
+ +
+
+ User is created: +
+
+ {{ vm.user.formattedCreateDate }} +
+
+ +
+
+ User is last updated: +
+
+ {{ vm.user.formattedUpdateDate }} +
+
+
@@ -332,7 +387,8 @@ type="button" action="vm.goToPage(vm.breadcrumbs[0])" label="Return to list" - label-key="buttons_returnToList"> + label-key="buttons_returnToList" + disabled="vm.loading"> + label-key="buttons_save" + disabled="vm.loading"> diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml index e312368270..91d2e42574 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml @@ -1588,9 +1588,12 @@ To manage your website, simply open the Umbraco back office and start adding con Access Administrator Category field + User is created Change Your Password Change photo New password + hasn't been locked out + The password hasn't been changed Confirm new password You can change your password for accessing the Umbraco Back Office by filling out the form below and click the 'Change Password' button Content Channel @@ -1601,13 +1604,16 @@ To manage your website, simply open the Umbraco back office and start adding con Document Type Editor Excerpt field + Failed login attempts Go to user profile Add groups to assign access and permissions Invite another user Invite new users to give them access to Umbraco. An invite email will be sent to the user with information on how to log in to Umbraco. Language Set the language you will see in menus and dialogs + Last lockout date Last login + Password is last changed Login Media start node Limit the media library to a specific start node @@ -1643,6 +1649,7 @@ To manage your website, simply open the Umbraco back office and start adding con Limit the content tree to a specific start node Content start nodes Limit the content tree to specific start nodes + User is last updated has been created The new user has successfully been created. To log in to Umbraco use the password below. Name From f50898ee6dafe5e63fbbe226e4a6672f3c8f6bd5 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Wed, 23 Aug 2017 15:20:23 +0200 Subject: [PATCH 13/16] add description to umb-box component --- .../html/umbbox/umbboxheader.directive.js | 14 +++++++++----- .../src/less/components/umb-box.less | 7 +++++++ .../components/html/umb-box/umb-box-header.html | 6 +++++- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/html/umbbox/umbboxheader.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/html/umbbox/umbboxheader.directive.js index 4ed1d68e59..bb16edf761 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/html/umbbox/umbboxheader.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/html/umbbox/umbboxheader.directive.js @@ -10,7 +10,7 @@ Use this directive to construct a title. Recommended to use it inside an {@link

Markup example

     
-        
+        
         
             // Content here
         
@@ -21,7 +21,7 @@ Use this directive to construct a title. Recommended to use it inside an {@link
 
     
         // the title-key property needs an areaAlias_keyAlias from the language files
-        
+        
         
             // Content here
         
@@ -35,8 +35,10 @@ Use this directive to construct a title. Recommended to use it inside an {@link
     
  • {@link umbraco.directives.directive:umbBoxContent umbBoxContent}
  • -@param {string} title (attrbute): Custom title text. -@param {string} title-key (attrbute): the key alias of the language xml files. +@param {string=} title (attrbute): Custom title text. +@param {string=} titleKey (attrbute): The translation key from the language xml files. +@param {string=} description (attrbute): Custom description text. +@param {string=} descriptionKey (attrbute): The translation key from the language xml files. **/ @@ -52,7 +54,9 @@ Use this directive to construct a title. Recommended to use it inside an {@link templateUrl: 'views/components/html/umb-box/umb-box-header.html', scope: { titleKey: "@?", - title: "@?" + title: "@?", + descriptionKey: "@?", + description: "@?" } }; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-box.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-box.less index cfbd929a3a..492a257b38 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-box.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-box.less @@ -15,6 +15,13 @@ font-weight: bold; } +.umb-box-header-description { + font-size: 13px; + color: @gray-3; + line-height: 1.6em; + margin-top: 1px; +} + .umb-box-content { padding: 20px; } \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/components/html/umb-box/umb-box-header.html b/src/Umbraco.Web.UI.Client/src/views/components/html/umb-box/umb-box-header.html index 6feffe9d3d..8c59061788 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/html/umb-box/umb-box-header.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/html/umb-box/umb-box-header.html @@ -1,6 +1,10 @@
    -
    +
    {{title}}
    +
    + + {{description}} +
    \ No newline at end of file From ff45f1f9cc69e89eb4d93ca96c7b596aa50b922d Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Wed, 23 Aug 2017 15:21:19 +0200 Subject: [PATCH 14/16] show calculated start nodes on the user profile --- .../src/views/users/group.html | 2 +- .../src/views/users/user.html | 39 ++++++++++++++++++- .../umbraco/config/lang/en_us.xml | 2 + 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/users/group.html b/src/Umbraco.Web.UI.Client/src/views/users/group.html index f1427e3266..455f2177c6 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/group.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/group.html @@ -23,7 +23,7 @@ - + diff --git a/src/Umbraco.Web.UI.Client/src/views/users/user.html b/src/Umbraco.Web.UI.Client/src/views/users/user.html index 99cfcf348f..6fb9806fb4 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/user.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/user.html @@ -73,7 +73,7 @@ - + @@ -158,6 +158,41 @@ + + + + + + + + + + + + + + + + + + + + +
    @@ -367,6 +402,8 @@
    +
    {{ vm.user | json }}
    + diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml index 91d2e42574..25d762c87c 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml @@ -1586,6 +1586,8 @@ To manage your website, simply open the Umbraco back office and start adding con Access + Based on the assigned groups and start nodes, the user has access to the following nodes + Assign access Administrator Category field User is created From 1b369b5ba9ecf9d67d2bc3e3da33526e6c0f22b4 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 24 Aug 2017 19:45:48 +1000 Subject: [PATCH 15/16] Adds back a backwards compatibility API --- src/Umbraco.Core/Security/BackOfficeUserManager.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/Umbraco.Core/Security/BackOfficeUserManager.cs b/src/Umbraco.Core/Security/BackOfficeUserManager.cs index 76cf2971ea..5e04659b43 100644 --- a/src/Umbraco.Core/Security/BackOfficeUserManager.cs +++ b/src/Umbraco.Core/Security/BackOfficeUserManager.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -35,6 +36,19 @@ namespace Umbraco.Core.Security #region Static Create methods + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Use the overload specifying all dependencies instead")] + public static BackOfficeUserManager Create( + IdentityFactoryOptions options, + IUserService userService, + IExternalLoginService externalLoginService, + MembershipProviderBase membershipProvider) + { + return Create(options, userService, + ApplicationContext.Current.Services.EntityService, + externalLoginService, membershipProvider); + } + /// /// Creates a BackOfficeUserManager instance with all default options and the default BackOfficeUserManager /// From ee6dbcef175a19d2afdb5546c538fb061abefbf3 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 24 Aug 2017 20:02:41 +1000 Subject: [PATCH 16/16] minor copy updates, removes test json --- src/Umbraco.Web.UI.Client/src/views/users/user.html | 2 -- src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml | 6 +++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/users/user.html b/src/Umbraco.Web.UI.Client/src/views/users/user.html index 6fb9806fb4..a55b1bf83d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/user.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/user.html @@ -402,8 +402,6 @@
    -
    {{ vm.user | json }}
    - diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml index 25d762c87c..ed0edf8b6d 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml @@ -1590,7 +1590,7 @@ To manage your website, simply open the Umbraco back office and start adding con Assign access Administrator Category field - User is created + User created Change Your Password Change photo New password @@ -1615,7 +1615,7 @@ To manage your website, simply open the Umbraco back office and start adding con Set the language you will see in menus and dialogs Last lockout date Last login - Password is last changed + Password last changed Login Media start node Limit the media library to a specific start node @@ -1651,7 +1651,7 @@ To manage your website, simply open the Umbraco back office and start adding con Limit the content tree to a specific start node Content start nodes Limit the content tree to specific start nodes - User is last updated + User last updated has been created The new user has successfully been created. To log in to Umbraco use the password below. Name