diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md index d21ee5ea02..0e79851c0b 100644 --- a/.github/CODE_OF_CONDUCT.md +++ b/.github/CODE_OF_CONDUCT.md @@ -17,7 +17,7 @@ Assume positive intent and try to understand before being understood. Treat others as you would like to be treated. -This also goes for treating the HQ with respect. For example: don’t promote products on [our.umbraco.org](https://our.umbraco.org) that directly compete with our commercial offerings which enables us to work for a sustainable Umbraco. +This also goes for treating the HQ with respect. For example: don’t promote products on [our.umbraco.com](https://our.umbraco.com) that directly compete with our commercial offerings which enables us to work for a sustainable Umbraco. ## Open diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index c257600769..96014f65b7 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -1,4 +1,4 @@ -_Looking for Umbraco version 8? [Click here](https://github.com/umbraco/Umbraco-CMS/blob/temp8/docs/CONTRIBUTING.md) to go to the v8 branch_ +_Looking for Umbraco version 8? [Click here](https://github.com/umbraco/Umbraco-CMS/blob/temp8/.github/V8_GETTING_STARTED.md) to go to the v8 branch_ # Contributing to Umbraco CMS 👍🎉 First off, thanks for taking the time to contribute! 🎉👍 @@ -16,9 +16,9 @@ This document gives you a quick overview on how to get started, we will link to ## Guidelines for contributions we welcome -Not all changes are wanted so on occassion we might close a PR without merging it. We will give you feedback why we can't accept your changes and we'll be nice about it, thanking you for spending your valueable time. +Not all changes are wanted, so on occassion we might close a PR without merging it. We will give you feedback why we can't accept your changes and we'll be nice about it, thanking you for spending your valueable time. -We have [documented what we consider small and large changes](CONTRIBUTION_GUIDELINES.md), make sure to talk to us before making large changes. +We have [documented what we consider small and large changes](CONTRIBUTION_GUIDELINES.md). Make sure to talk to us before making large changes. Remember, if an issue is in the `Up for grabs` list or you've asked for some feedback before you sent us a PR, your PR will not be closed as unwanted. @@ -36,7 +36,7 @@ Great question! The short version goes like this: * **Build** - build your fork of Umbraco locally as described in [building Umbraco from source code](BUILD.md) * **Change** - make your changes, experiment, have fun, explore and learn, and don't be afraid. We welcome all contributions and will [happily give feedback](#questions) - * **Commit** - done? Yay! 🎉 It is recommended to create a new branch now and name it after the issue you're fixing, we usually follow the format: `temp-U4-12345`. This means it's a temporary branch for the particular issue you're working on, in this case `U4-12345` + * **Commit** - done? Yay! 🎉 It is recommended to create a new branch now and name it after the issue you're fixing, we usually follow the format: `temp-12345`. This means it's a temporary branch for the particular issue you're working on, in this case `12345` * **Push** - great, now you can push the changes up to your fork on GitHub * **Create pull request** - exciting! You're ready to show us your changes (or not quite ready, you just need some feedback to progress). GitHub has picked up on the new branch you've pushed and will offer to create a Pull Request. Click that green button and away you go. @@ -72,7 +72,6 @@ The pull request team consists of a member of Umbraco HQ, [Sebastiaan](https://g - [Anders Bjerner](https://github.com/abjerner) - [Dave Woestenborghs](https://github.com/dawoe) - [Emma Burstow](https://github.com/emmaburstow) -- [Kyle Weems](https://github.com/cssquirrel) - [Poornima Nayar](https://github.com/poornimanayar) These wonderful volunteers will provide you with a first reply to your PR, review and test out your changes and might ask more questions. After that they'll let Umbraco HQ know if everything seems okay. @@ -82,7 +81,7 @@ These wonderful volunteers will provide you with a first reply to your PR, revie You can get in touch with [the PR team](#the-pr-team) in multiple ways, we love open conversations and we are a friendly bunch. No question you have is stupid. Any questions you have usually helps out multiple people with the same question. Ask away: - If there's an existing issue on the issue tracker then that's a good place to leave questions and discuss how to start or move forward -- Unsure where to start? Did something not work as expected? Try leaving a note in the ["Contributing to Umbraco"](https://our.umbraco.org/forum/contributing-to-umbraco-cms/) forum, the team monitors that one closely +- Unsure where to start? Did something not work as expected? Try leaving a note in the ["Contributing to Umbraco"](https://our.umbraco.com/forum/contributing-to-umbraco-cms/) forum, the team monitors that one closely - We're also [active in the Gitter chatroom](https://gitter.im/umbraco/Umbraco-CMS) ## Code of Conduct diff --git a/.github/CONTRIBUTING_DETAILED.md b/.github/CONTRIBUTING_DETAILED.md index 020346dc5e..b3e34ef55d 100644 --- a/.github/CONTRIBUTING_DETAILED.md +++ b/.github/CONTRIBUTING_DETAILED.md @@ -25,13 +25,13 @@ When contributing code to Umbraco there's plenty of things you'll want to know, ### Reporting Bugs This section guides you through submitting a bug report for Umbraco CMS. Following these guidelines helps maintainers and the community understand your report 📝, reproduce the behavior 💻 💻, and find related reports 🔎. -Before creating bug reports, please check [this list](#before-submitting-a-bug-report) as you might find out that you don't need to create one. When you are creating a bug report, please [include as many details as possible](#how-do-i-submit-a-good-bug-report). Fill out [the required template](http://issues.umbraco.org/issues#newissue=61-30118), the information it asks for helps us resolve issues faster. +Before creating bug reports, please check [this list](#before-submitting-a-bug-report) as you might find out that you don't need to create one. When you are creating a bug report, please [include as many details as possible](#how-do-i-submit-a-good-bug-report). Fill out [the required template](https://github.com/umbraco/Umbraco-CMS/issues/new/choose), the information it asks for helps us resolve issues faster. > **Note:** If you find a **Closed** issue that seems like it is the same thing that you're experiencing, open a new issue and include a link to the original issue in the body of your new one. ##### Before Submitting A Bug Report - * Most importantly, check **if you can reproduce the problem** in the [latest version of Umbraco](https://our.umbraco.org/download/). We might have already fixed your particular problem. + * Most importantly, check **if you can reproduce the problem** in the [latest version of Umbraco](https://our.umbraco.com/download/). We might have already fixed your particular problem. * It also helps tremendously to check if the issue you're experiencing is present in **a clean install** of the Umbraco version you're currently using. Custom code can have side-effects that don't occur in a clean install. * **Use the Google**. Whatever you're experiencing, Google it plus "Umbraco" - usually you can get some pretty good hints from the search results, including open issues and further troubleshooting hints. * If you do find and existing issue has **and the issue is still open**, add a comment to the existing issue if you have additional information. If you have the same problem and no new info to add, just "star" the issue. @@ -65,13 +65,11 @@ Most of the suggestions in the [reporting bugs](#reporting-bugs) section also co Some additional hints that may be helpful: * **Include screenshots and animated GIFs** which help you demonstrate the steps or point out the part of Umbraco which the suggestion is related to. - * **Explain why this enhancement would be useful to most Umbraco users** and isn't something that can or should be implemented as a [community package](https://our.umbraco.org/projects/). + * **Explain why this enhancement would be useful to most Umbraco users** and isn't something that can or should be implemented as a [community package](https://our.umbraco.com/projects/). ### Your First Code Contribution -Unsure where to begin contributing to Umbraco? You can start by looking through [these `Up for grabs` and issues](http://issues.umbraco.org/issues/U4?q=%28project%3A+%7BU4%7D+Difficulty%3A+%7BVery+Easy%7D+%23Easy+%23Unresolved+Priority%3A+Normal+%23Major+%23Show-stopper+State%3A+-%7BIn+Progress%7D+sort+by%3A+votes+Affected+versions%3A+-6.*+Affected+versions%3A+-4.*%29+OR+%28tag%3A+%7BUp+For+Grabs%7D+%23Unresolved+%29). - -The issue list is sorted by total number of upvotes. While not perfect, number of upvotes is a reasonable proxy for impact a given change will have. +Unsure where to begin contributing to Umbraco? You can start by looking through [these `Up for grabs` and issues](https://issues.umbraco.org/issues?q=&project=U4&tagValue=upforgrabs&release=&issueType=&search=search) or on the [new issue tracker](https://github.com/umbraco/Umbraco-CMS/issues?q=is%3Aopen+is%3Aissue+label%3Acommunity%2Fup-for-grabs). ### Pull Requests @@ -80,7 +78,7 @@ The most successful pull requests usually look a like this: * Fill in the required template * Include screenshots and animated GIFs in your pull request whenever possible. * Unit tests, while optional are awesome, thank you! - * New code is commented with documentation from which [the reference documentation](https://our.umbraco.org/documentation/Reference/) is generated + * New code is commented with documentation from which [the reference documentation](https://our.umbraco.com/documentation/Reference/) is generated Again, these are guidelines, not strict requirements. @@ -116,8 +114,8 @@ There's two big areas that you should know about: To find the general areas of something you're looking to fix or improve, have a look at the following two parts of the API documentation. - * [The AngularJS based backoffice files](https://our.umbraco.org/apidocs/ui/#/api) (to be found in `src\Umbraco.Web.UI.Client\src`) - * [The rest](https://our.umbraco.org/apidocs/csharp/) + * [The AngularJS based backoffice files](https://our.umbraco.com/apidocs/ui/#/api) (to be found in `src\Umbraco.Web.UI.Client\src`) + * [The rest](https://our.umbraco.com/apidocs/csharp/) ### What branch should I target for my contributions? diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 6275d161dc..8cb9017518 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,11 +1,12 @@ ### Prerequisites -- [ ] I have [created an issue](https://github.com/umbraco/Umbraco-CMS/issues) for the proposed changes in this PR, the link is: - [ ] I have added steps to test this contribution in the description below +If there's an existing issue for this PR then this fixes: + ### Description - - + + diff --git a/.github/README.md b/.github/README.md index cf29f4e527..5a1340006e 100644 --- a/.github/README.md +++ b/.github/README.md @@ -15,7 +15,7 @@ Once a track is done, we start releasing previews where we ask people to test th Umbraco CMS =========== -The friendliest, most flexible and fastest growing ASP.NET CMS used by more than 443,000 websites worldwide: [https://umbraco.com](https://umbraco.com) +The friendliest, most flexible and fastest growing ASP.NET CMS, and used by more than 443,000 websites worldwide: [https://umbraco.com](https://umbraco.com) [![ScreenShot](img/vimeo.png)](https://vimeo.com/172382998/) @@ -28,34 +28,34 @@ Umbraco is a free open source Content Management System built on the ASP.NET pla ## Umbraco - The Friendly CMS -For the first time on the Microsoft platform, there is a free user and developer friendly CMS that makes it quick and easy to create websites - or a breeze to build complex web applications. Umbraco has award-winning integration capabilities and supports ASP.NET MVC or Web Forms, including User and Custom Controls, out of the box. +For the first time on the Microsoft platform, there is a free user- and developer-friendly CMS that makes it quick and easy to create websites - and a breeze to build complex web applications. Umbraco has award-winning integration capabilities and supports ASP.NET MVC or Web Forms, including User and Custom Controls, right out of the box. -Umbraco is not only loved by developers, but is a content editors dream. Enjoy intuitive editing tools, media management, responsive views and approval workflows to send your content live. +Umbraco is not only loved by developers, but is a content editor's dream. Enjoy intuitive editing tools, media management, responsive views, and approval workflows to send your content live. -Used by more than 443,000 active websites including Carlsberg, Segway, Amazon and Heinz and **The Official ASP.NET and IIS.NET website from Microsoft** ([https://asp.net](https://asp.net) / [https://iis.net](https://iis.net)), you can be sure that the technology is proven, stable and scales. Backed by the team at Umbraco HQ, and supported by a dedicated community of over 220,000 craftspeople globally, you can trust that Umbraco is a safe choice and is here to stay. +Used by more than 443,000 active websites including Carlsberg, Segway, Amazon and Heinz and **The Official ASP.NET and IIS.NET website from Microsoft** ([https://asp.net](https://asp.net) / [https://iis.net](https://iis.net)), you can be sure that the technology is proven, stable and scalable. Backed by the team at Umbraco HQ, and supported by a dedicated community of over 220,000 craftspeople globally, you can trust that Umbraco is a safe choice and is here to stay. To view more examples, please visit [https://umbraco.com/case-studies-testimonials/](https://umbraco.com/case-studies-testimonials/) ## Why Open Source? -As an Open Source platform, Umbraco is more than just a CMS. We are transparent with our roadmap for future versions, our incremental sprint planning notes are publicly accessible and community contributions and packages are available for all to use. +As an Open Source platform, Umbraco is more than just a CMS. We are transparent with our roadmap for future versions, our incremental sprint planning notes are publicly accessible, and community contributions and packages are available for all to use. ## Trying out Umbraco CMS -[Umbraco Cloud](https://umbraco.com/cloud) is the easiest and fastest way to use Umbraco yet with full support for all your custom .NET code and intergrations. You're up and running in less than a minute and your life will be made easier with automated upgrades and a built-in deployment engine. We offer a free 14 day trial, no credit card needed. +[Umbraco Cloud](https://umbraco.com/cloud) is the easiest and fastest way to use Umbraco yet, with full support for all your custom .NET code and integrations. You're up and running in less than a minute, and your life will be made easier with automated upgrades and a built-in deployment engine. We offer a free 14-day trial, no credit card needed. -If you want to DIY you can [download Umbraco](https://our.umbraco.com/download) either as a ZIP file or via NuGet. It's the same version of Umbraco CMS that powers Umbraco Cloud, but you'll need to find a place to host yourself and handling deployments and upgrades is all down to you. +If you want to DIY, you can [download Umbraco](https://our.umbraco.com/download) either as a ZIP file or via NuGet. It's the same version of Umbraco CMS that powers Umbraco Cloud, but you'll need to find a place to host it yourself, and handling deployments and upgrades will be all up to you. ## Community -Our friendly community is available 24/7 at the community hub we call ["Our Umbraco"](https://our.umbraco.com). Our Umbraco feature forums for questions and answers, documentation, downloadable plugins for Umbraco and a rich collection of community resources. +Our friendly community is available 24/7 at the community hub we call ["Our Umbraco"](https://our.umbraco.com). Our Umbraco features forums for questions and answers, documentation, downloadable plugins for Umbraco, and a rich collection of community resources. ## Contribute to Umbraco -Umbraco is contribution focused and community driven. If you want to contribute back to Umbraco please check out our [guide to contributing](CONTRIBUTING.md). +Umbraco is contribution-focused and community-driven. If you want to contribute back to Umbraco, please check out our [guide to contributing](CONTRIBUTING.md). ## Found a bug? Another way you can contribute to Umbraco is by providing issue reports. For information on how to submit an issue report refer to our [online guide for reporting issues](CONTRIBUTING_DETAILED.md#reporting-bugs). You can comment and report issues on the [github issue tracker](https://github.com/umbraco/Umbraco-CMS/issues). -Since [September 2018](https://umbraco.com/blog/a-second-take-on-umbraco-issue-tracker-hello-github-issues/) the old issue tracker is in read only mode, but can still be found at [http://issues.umbraco.org](http://issues.umbraco.org). +Since [September 2018](https://umbraco.com/blog/a-second-take-on-umbraco-issue-tracker-hello-github-issues/), the old issue tracker is in read-only mode, but can still be found at [http://issues.umbraco.org](http://issues.umbraco.org). diff --git a/.github/V8_GETTING_STARTED.md b/.github/V8_GETTING_STARTED.md index 8cd792aa71..62b376b0e7 100644 --- a/.github/V8_GETTING_STARTED.md +++ b/.github/V8_GETTING_STARTED.md @@ -23,7 +23,7 @@ We recommend running the site with the Visual Studio since you'll be able to rem ### Making code changes -* _[The process for making code changes in v8 is the same as v7](https://github.com/umbraco/Umbraco-CMS/blob/dev-v7/docs/CONTRIBUTING.md)_ +* _[The process for making code changes in v8 is the same as v7](https://github.com/umbraco/Umbraco-CMS/blob/dev-v7/.github/CONTRIBUTING.md)_ * Any .NET changes you make you just need to compile * Any Angular/JS changes you make you will need to make sure you are running the Gulp build. Easiest way to do this is from within Visual Studio in the `Task Runner Explorer`. You can find this window by pressing `ctrl + q` and typing in `Task Runner Explorer`. In this window you'll see all Gulp tasks, double click on the `dev` task, this will compile the angular solution and start a file watcher, then any html/js changes you make are automatically built. * When making js changes, you should have the chrome developer tools open to ensure that cache is disabled @@ -33,5 +33,5 @@ We recommend running the site with the Visual Studio since you'll be able to rem We are keeping track of [known issues and limitations here](http://issues.umbraco.org/issue/U4-11279). These line items will eventually be turned into actual tasks to be worked on. Feel free to help us keep this list updated if you find issues and even help fix some of these items. If there is a particular item you'd like to help fix please mention this on the task and we'll create a sub task for the item to continue discussion there. -There's [a list of tasks for v8 that haven't been completed](http://issues.umbraco.org/issues/U4?q=Due+in+version%3A+8.0.0+%23Unresolved+). If you are interested in helping out with any of these please mention this on the task. This list will be constantly updated as we begin to document and design some of the other tasks that still need to get done. +There's [a list of tasks for v8 that haven't been completed](https://issues.umbraco.org/issues?q=&project=U4&tagValue=&release=8.0.0&issueType=&resolvedState=open&search=search). If you are interested in helping out with any of these please mention this on the task. This list will be constantly updated as we begin to document and design some of the other tasks that still need to get done. diff --git a/LICENSE.md b/LICENSE.md index c5560c3ce1..fa83dba963 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ # The MIT License (MIT) # -Copyright (c) 2013 Umbraco +Copyright (c) 2013-present Umbraco Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/build/build.ps1 b/build/build.ps1 index 8548cbb1ac..1066c62876 100644 --- a/build/build.ps1 +++ b/build/build.ps1 @@ -43,14 +43,6 @@ $release = "" + $semver.Major + "." + $semver.Minor + "." + $semver.Patch - Write-Host "Update UmbracoVersion.cs" - $this.ReplaceFileText("$($this.SolutionRoot)\src\Umbraco.Core\Configuration\UmbracoVersion.cs", ` - "(\d+)\.(\d+)\.(\d+)(.(\d+))?", ` - "$release") - $this.ReplaceFileText("$($this.SolutionRoot)\src\Umbraco.Core\Configuration\UmbracoVersion.cs", ` - "CurrentComment => `"(.+)`"", ` - "CurrentComment => `"$($semver.PreRelease)`"") - Write-Host "Update IIS Express port in csproj" $updater = New-Object "Umbraco.Build.ExpressPortUpdater" $csproj = "$($this.SolutionRoot)\src\Umbraco.Web.UI\Umbraco.Web.UI.csproj" @@ -69,7 +61,7 @@ $global:node_nodepath = $this.ClearEnvVar("NODEPATH") $global:node_npmcache = $this.ClearEnvVar("NPM_CONFIG_CACHE") $global:node_npmprefix = $this.ClearEnvVar("NPM_CONFIG_PREFIX") - + # https://github.com/gruntjs/grunt-contrib-connect/issues/235 $this.SetEnvVar("NODE_NO_HTTP2", "1") }) @@ -81,7 +73,7 @@ $this.SetEnvVar("NODEPATH", $node_nodepath) $this.SetEnvVar("NPM_CONFIG_CACHE", $node_npmcache) $this.SetEnvVar("NPM_CONFIG_PREFIX", $node_npmprefix) - + $ignore = $this.ClearEnvVar("NODE_NO_HTTP2") }) @@ -434,7 +426,7 @@ Write-Host "Prepare Azure Gallery" $this.CopyFile("$($this.SolutionRoot)\build\Azure\azuregalleryrelease.ps1", $this.BuildOutput) }) - + $ubuild.DefineMethod("Build", { $error.Clear() diff --git a/src/ApiDocs/umbracotemplate/partials/footer.tmpl.partial b/src/ApiDocs/umbracotemplate/partials/footer.tmpl.partial index 69f51a101f..7aac413bfd 100644 --- a/src/ApiDocs/umbracotemplate/partials/footer.tmpl.partial +++ b/src/ApiDocs/umbracotemplate/partials/footer.tmpl.partial @@ -7,7 +7,7 @@ Back to top - Copyright © 2016 Umbraco
Generated by DocFX
+ Copyright © 2016-present Umbraco
Generated by DocFX
diff --git a/src/ApiDocs/umbracotemplate/partials/head.tmpl.partial b/src/ApiDocs/umbracotemplate/partials/head.tmpl.partial index 591e1c1885..ccc4d50229 100644 --- a/src/ApiDocs/umbracotemplate/partials/head.tmpl.partial +++ b/src/ApiDocs/umbracotemplate/partials/head.tmpl.partial @@ -8,7 +8,7 @@ {{#_description}}{{/_description}} - + diff --git a/src/ApiDocs/umbracotemplate/styles/main.css b/src/ApiDocs/umbracotemplate/styles/main.css index 7756b2f7d4..d74d51b150 100644 --- a/src/ApiDocs/umbracotemplate/styles/main.css +++ b/src/ApiDocs/umbracotemplate/styles/main.css @@ -63,7 +63,7 @@ a:focus { } .navbar-header .navbar-brand { - background: url(https://our.umbraco.org/assets/images/logo.svg) left center no-repeat; + background: url(https://our.umbraco.com/assets/images/logo.svg) left center no-repeat; background-size: 40px auto; width:50px; } diff --git a/src/SolutionInfo.cs b/src/SolutionInfo.cs index d7f81c1bb1..b5af335791 100644 --- a/src/SolutionInfo.cs +++ b/src/SolutionInfo.cs @@ -2,7 +2,7 @@ using System.Resources; [assembly: AssemblyCompany("Umbraco")] -[assembly: AssemblyCopyright("Copyright © Umbraco 2017")] +[assembly: AssemblyCopyright("Copyright © Umbraco 2018")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] diff --git a/src/Umbraco.Core/CodeAnnotations/ActionMetadataAttribute.cs b/src/Umbraco.Core/CodeAnnotations/ActionMetadataAttribute.cs deleted file mode 100644 index 9ef87e9a5f..0000000000 --- a/src/Umbraco.Core/CodeAnnotations/ActionMetadataAttribute.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; -using Umbraco.Core.Exceptions; - -namespace Umbraco.Core.CodeAnnotations -{ - [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] - internal class ActionMetadataAttribute : Attribute - { - public string Category { get; } - public string Name { get; } - - /// - /// Constructor used to assign a Category, since no name is assigned it will try to be translated from the language files based on the action's alias - /// - /// - public ActionMetadataAttribute(string category) - { - if (string.IsNullOrWhiteSpace(category)) throw new ArgumentNullOrEmptyException(nameof(category)); - Category = category; - } - - /// - /// Constructor used to assign an explicit name and category - /// - /// - /// - public ActionMetadataAttribute(string category, string name) - { - if (string.IsNullOrWhiteSpace(category)) throw new ArgumentNullOrEmptyException(nameof(category)); - if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullOrEmptyException(nameof(name)); - Category = category; - Name = name; - } - } -} diff --git a/src/Umbraco.Core/Collections/CompositeIntStringKey.cs b/src/Umbraco.Core/Collections/CompositeIntStringKey.cs index eb9db80990..cafc209e08 100644 --- a/src/Umbraco.Core/Collections/CompositeIntStringKey.cs +++ b/src/Umbraco.Core/Collections/CompositeIntStringKey.cs @@ -40,4 +40,4 @@ namespace Umbraco.Core.Collections public static bool operator !=(CompositeIntStringKey key1, CompositeIntStringKey key2) => key1._key2 != key2._key2 || key1._key1 != key2._key1; } -} \ No newline at end of file +} diff --git a/src/Umbraco.Core/Collections/ObservableDictionary.cs b/src/Umbraco.Core/Collections/ObservableDictionary.cs index caa2be92a8..6518533476 100644 --- a/src/Umbraco.Core/Collections/ObservableDictionary.cs +++ b/src/Umbraco.Core/Collections/ObservableDictionary.cs @@ -14,30 +14,31 @@ namespace Umbraco.Core.Collections /// /// The type of elements contained in the BindableCollection /// The type of the indexing key - public class ObservableDictionary : ObservableCollection + public class ObservableDictionary : ObservableCollection, IReadOnlyDictionary, IDictionary { - protected Dictionary Indecies = new Dictionary(); - protected Func KeySelector; + protected Dictionary Indecies { get; } + protected Func KeySelector { get; } /// /// Create new ObservableDictionary /// /// Selector function to create key from value - public ObservableDictionary(Func keySelector) - : base() + /// The equality comparer to use when comparing keys, or null to use the default comparer. + public ObservableDictionary(Func keySelector, IEqualityComparer equalityComparer = null) { - if (keySelector == null) throw new ArgumentException("keySelector"); - KeySelector = keySelector; + KeySelector = keySelector ?? throw new ArgumentException("keySelector"); + Indecies = new Dictionary(equalityComparer); } #region Protected Methods + protected override void InsertItem(int index, TValue item) { var key = KeySelector(item); if (Indecies.ContainsKey(key)) throw new DuplicateKeyException(key.ToString()); - if (index != this.Count) + if (index != Count) { foreach (var k in Indecies.Keys.Where(k => Indecies[k] >= index).ToList()) { @@ -47,7 +48,6 @@ namespace Umbraco.Core.Collections base.InsertItem(index, item); Indecies[key] = index; - } protected override void ClearItems() @@ -56,7 +56,6 @@ namespace Umbraco.Core.Collections Indecies.Clear(); } - protected override void RemoveItem(int index) { var item = this[index]; @@ -71,9 +70,10 @@ namespace Umbraco.Core.Collections Indecies[k]--; } } + #endregion - public virtual bool ContainsKey(TKey key) + public bool ContainsKey(TKey key) { return Indecies.ContainsKey(key); } @@ -83,10 +83,10 @@ namespace Umbraco.Core.Collections /// /// Key of element to replace /// - public virtual TValue this[TKey key] + public TValue this[TKey key] { - get { return this[Indecies[key]]; } + get => this[Indecies[key]]; set { //confirm key matches @@ -95,7 +95,7 @@ namespace Umbraco.Core.Collections if (!Indecies.ContainsKey(key)) { - this.Add(value); + Add(value); } else { @@ -112,9 +112,10 @@ namespace Umbraco.Core.Collections /// /// /// False if key not found - public virtual bool Replace(TKey key, TValue value) + public bool Replace(TKey key, TValue value) { if (!Indecies.ContainsKey(key)) return false; + //confirm key matches if (!KeySelector(value).Equals(key)) throw new InvalidOperationException("Key of new value does not match"); @@ -124,11 +125,11 @@ namespace Umbraco.Core.Collections } - public virtual bool Remove(TKey key) + public bool Remove(TKey key) { if (!Indecies.ContainsKey(key)) return false; - this.RemoveAt(Indecies[key]); + RemoveAt(Indecies[key]); return true; } @@ -138,12 +139,13 @@ namespace Umbraco.Core.Collections /// /// /// - public virtual void ChangeKey(TKey currentKey, TKey newKey) + public void ChangeKey(TKey currentKey, TKey newKey) { if (!Indecies.ContainsKey(currentKey)) { throw new InvalidOperationException("No item with the key " + currentKey + "was found in the collection"); } + if (ContainsKey(newKey)) { throw new DuplicateKeyException(newKey.ToString()); @@ -155,16 +157,81 @@ namespace Umbraco.Core.Collections Indecies.Add(newKey, currentIndex); } - internal class DuplicateKeyException : Exception - { + #region IDictionary and IReadOnlyDictionary implementation - public string Key { get; private set; } - public DuplicateKeyException(string key) - : base("Attempted to insert duplicate key " + key + " in collection") + public bool TryGetValue(TKey key, out TValue val) + { + if (Indecies.TryGetValue(key, out var index)) { - Key = key; + val = this[index]; + return true; + } + val = default; + return false; + } + + /// + /// Returns all keys + /// + public IEnumerable Keys => Indecies.Keys; + + /// + /// Returns all values + /// + public IEnumerable Values => base.Items; + + ICollection IDictionary.Keys => Indecies.Keys; + + //this will never be used + ICollection IDictionary.Values => Values.ToList(); + + bool ICollection>.IsReadOnly => false; + + IEnumerator> IEnumerable>.GetEnumerator() + { + foreach (var i in Values) + { + var key = KeySelector(i); + yield return new KeyValuePair(key, i); } } + void IDictionary.Add(TKey key, TValue value) + { + Add(value); + } + + void ICollection>.Add(KeyValuePair item) + { + Add(item.Value); + } + + bool ICollection>.Contains(KeyValuePair item) + { + return ContainsKey(item.Key); + } + + void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) + { + throw new NotImplementedException(); + } + + bool ICollection>.Remove(KeyValuePair item) + { + return Remove(item.Key); + } + + #endregion + + internal class DuplicateKeyException : Exception + { + public DuplicateKeyException(string key) + : base("Attempted to insert duplicate key \"" + key + "\" in collection.") + { + Key = key; + } + + public string Key { get; } + } } } diff --git a/src/Umbraco.Core/Components/RelateOnCopyComponent.cs b/src/Umbraco.Core/Components/RelateOnCopyComponent.cs index 5356fa6e30..bc66dccd31 100644 --- a/src/Umbraco.Core/Components/RelateOnCopyComponent.cs +++ b/src/Umbraco.Core/Components/RelateOnCopyComponent.cs @@ -1,12 +1,10 @@ using Umbraco.Core.Composing; using Umbraco.Core.Models; -using Umbraco.Core.Persistence.Repositories.Implement; using Umbraco.Core.Services; using Umbraco.Core.Services.Implement; namespace Umbraco.Core.Components { - //TODO: This should just exist in the content service/repo! [RuntimeLevel(MinLevel = RuntimeLevel.Run)] public sealed class RelateOnCopyComponent : UmbracoComponentBase, IUmbracoCoreComponent @@ -39,8 +37,9 @@ namespace Umbraco.Core.Components Current.Services.AuditService.Add( AuditType.Copy, - $"Copied content with Id: '{e.Copy.Id}' related to original content with Id: '{e.Original.Id}'", - e.Copy.WriterId, e.Copy.Id); + e.Copy.WriterId, + e.Copy.Id, ObjectTypes.GetName(UmbracoObjectTypes.Document), + $"Copied content with Id: '{e.Copy.Id}' related to original content with Id: '{e.Original.Id}'"); } } } diff --git a/src/Umbraco.Core/Components/RelateOnTrashComponent.cs b/src/Umbraco.Core/Components/RelateOnTrashComponent.cs index fffae85501..8bcce50c68 100644 --- a/src/Umbraco.Core/Components/RelateOnTrashComponent.cs +++ b/src/Umbraco.Core/Components/RelateOnTrashComponent.cs @@ -82,11 +82,12 @@ namespace Umbraco.Core.Components relationService.Save(relation); Current.Services.AuditService.Add(AuditType.Delete, + item.Entity.WriterId, + item.Entity.Id, + ObjectTypes.GetName(UmbracoObjectTypes.Document), string.Format(textService.Localize( "recycleBin/contentTrashed"), - item.Entity.Id, originalParentId), - item.Entity.WriterId, - item.Entity.Id); + item.Entity.Id, originalParentId)); } } } @@ -120,11 +121,12 @@ namespace Umbraco.Core.Components var relation = new Relation(originalParentId, item.Entity.Id, relationType); relationService.Save(relation); Current.Services.AuditService.Add(AuditType.Delete, - string.Format(textService.Localize( + item.Entity.CreatorId, + item.Entity.Id, + ObjectTypes.GetName(UmbracoObjectTypes.Media), + string.Format(textService.Localize( "recycleBin/mediaTrashed"), - item.Entity.Id, originalParentId), - item.Entity.CreatorId, - item.Entity.Id); + item.Entity.Id, originalParentId)); } } } diff --git a/src/Umbraco.Core/Configuration/GlobalSettings.cs b/src/Umbraco.Core/Configuration/GlobalSettings.cs index 7c274089f7..c00ab795d2 100644 --- a/src/Umbraco.Core/Configuration/GlobalSettings.cs +++ b/src/Umbraco.Core/Configuration/GlobalSettings.cs @@ -61,7 +61,8 @@ namespace Umbraco.Core.Configuration var config = WebConfigurationManager.OpenWebConfiguration(appPath); var settings = (MailSettingsSectionGroup)config.GetSectionGroup("system.net/mailSettings"); - if (settings == null || settings.Smtp == null) return false; + // note: "noreply@example.com" is/was the sample SMTP from email - we'll regard that as "not configured" + if (settings == null || settings.Smtp == null || "noreply@example.com".Equals(settings.Smtp.From, StringComparison.OrdinalIgnoreCase)) return false; if (settings.Smtp.SpecifiedPickupDirectory != null && string.IsNullOrEmpty(settings.Smtp.SpecifiedPickupDirectory.PickupDirectoryLocation) == false) return true; if (settings.Smtp.Network != null && string.IsNullOrEmpty(settings.Smtp.Network.Host) == false) diff --git a/src/Umbraco.Core/Configuration/IGlobalSettings.cs b/src/Umbraco.Core/Configuration/IGlobalSettings.cs index cf9478d30a..a043f608f4 100644 --- a/src/Umbraco.Core/Configuration/IGlobalSettings.cs +++ b/src/Umbraco.Core/Configuration/IGlobalSettings.cs @@ -1,4 +1,6 @@ -namespace Umbraco.Core.Configuration +using System; + +namespace Umbraco.Core.Configuration { /// /// Contains general settings information for the entire Umbraco instance based on information from web.config appsettings @@ -24,6 +26,8 @@ /// /// Defaults to ~/App_Data/umbraco.config /// + //fixme - remove + [Obsolete("This should not be used, need to remove the content xml cache")] string ContentXmlFile { get; } /// diff --git a/src/Umbraco.Core/Configuration/UmbracoConfig.cs b/src/Umbraco.Core/Configuration/UmbracoConfig.cs index 6dd5617992..6a1203313e 100644 --- a/src/Umbraco.Core/Configuration/UmbracoConfig.cs +++ b/src/Umbraco.Core/Configuration/UmbracoConfig.cs @@ -193,4 +193,4 @@ namespace Umbraco.Core.Configuration //TODO: Add other configurations here ! } -} +} \ No newline at end of file diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs index 39861ac4e9..d2236bab70 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs @@ -81,12 +81,6 @@ namespace Umbraco.Core.Configuration.UmbracoSettings [ConfigurationProperty("loginBackgroundImage")] internal InnerTextConfigurationElement LoginBackgroundImage => GetOptionalTextElement("loginBackgroundImage", string.Empty); - [ConfigurationProperty("StripUdiAttributes")] - internal InnerTextConfigurationElement StripUdiAttributes - { - get { return GetOptionalTextElement("StripUdiAttributes", true); } - } - string IContentSection.NotificationEmailAddress => Notifications.NotificationEmailAddress; @@ -142,7 +136,6 @@ namespace Umbraco.Core.Configuration.UmbracoSettings bool IContentSection.EnableInheritedMediaTypes => EnableInheritedMediaTypes; - bool IContentSection.StripUdiAttributes => StripUdiAttributes; string IContentSection.LoginBackgroundImage => LoginBackgroundImage; } diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/IContentSection.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/IContentSection.cs index ef9ffeb014..fe2eea5d91 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/IContentSection.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/IContentSection.cs @@ -66,7 +66,5 @@ namespace Umbraco.Core.Configuration.UmbracoSettings bool EnableInheritedMediaTypes { get; } string LoginBackgroundImage { get; } - bool StripUdiAttributes { get; } - } -} +} \ No newline at end of file diff --git a/src/Umbraco.Core/Configuration/UmbracoVersion.cs b/src/Umbraco.Core/Configuration/UmbracoVersion.cs index 46ad221837..73df566a0f 100644 --- a/src/Umbraco.Core/Configuration/UmbracoVersion.cs +++ b/src/Umbraco.Core/Configuration/UmbracoVersion.cs @@ -10,37 +10,61 @@ namespace Umbraco.Core.Configuration /// public static class UmbracoVersion { - // BEWARE! - // This class is parsed and updated by the build scripts. - // Do NOT modify it unless you understand what you are doing. + static UmbracoVersion() + { + var umbracoCoreAssembly = typeof(UmbracoVersion).Assembly; + + // gets the value indicated by the AssemblyVersion attribute + AssemblyVersion = umbracoCoreAssembly.GetName().Version; + + // gets the value indicated by the AssemblyFileVersion attribute + AssemblyFileVersion = System.Version.Parse(umbracoCoreAssembly.GetCustomAttribute().Version); + + // gets the value indicated by the AssemblyInformationalVersion attribute + // this is the true semantic version of the Umbraco Cms + SemanticVersion = SemVersion.Parse(umbracoCoreAssembly.GetCustomAttribute().InformationalVersion); + + // gets the non-semantic version + Current = SemanticVersion.GetVersion(3); + } /// - /// Gets the version of the executing code. + /// Gets the non-semantic version of the Umbraco code. /// - public static Version Current { get; } = new Version("8.0.0"); + // TODO rename to Version + public static Version Current { get; } /// - /// Gets the version comment of the executing code (eg "beta"). + /// Gets the semantic version comments of the Umbraco code. /// - public static string CurrentComment => "alpha.52"; + public static string Comment => SemanticVersion.Prerelease; /// - /// Gets the assembly version of Umbraco.Code.dll. + /// Gets the assembly version of the Umbraco code. /// - /// Get it by looking at a class in that dll, due to medium trust issues, - /// see http://haacked.com/archive/2010/11/04/assembly-location-and-medium-trust.aspx, - /// however fixme we don't support medium trust anymore? - public static string AssemblyVersion => new AssemblyName(typeof(UmbracoVersion).Assembly.FullName).Version.ToString(); + /// + /// The assembly version is the value of the . + /// Is is the one that the CLR checks for compatibility. Therefore, it changes only on + /// hard-breaking changes (for instance, on new major versions). + /// + public static Version AssemblyVersion {get; } /// - /// Gets the semantic version of the executing code. + /// Gets the assembly file version of the Umbraco code. /// - public static SemVersion SemanticVersion { get; } = new SemVersion( - Current.Major, - Current.Minor, - Current.Build, - CurrentComment.IsNullOrWhiteSpace() ? null : CurrentComment, - Current.Revision > 0 ? Current.Revision.ToInvariantString() : null); + /// + /// The assembly version is the value of the . + /// + public static Version AssemblyFileVersion { get; } + + /// + /// Gets the semantic version of the Umbraco code. + /// + /// + /// The semantic version is the value of the . + /// It is the full version of Umbraco, including comments. + /// + public static SemVersion SemanticVersion { get; } /// /// Gets the "local" version of the site. @@ -51,7 +75,7 @@ namespace Umbraco.Core.Configuration /// and changes during an upgrade. The executing code version changes when new code is /// deployed. The site/files version changes during an upgrade. /// - public static SemVersion Local + public static SemVersion LocalVersion { get { diff --git a/src/Umbraco.Core/Constants-Applications.cs b/src/Umbraco.Core/Constants-Applications.cs index ac42274a71..4aa1760a45 100644 --- a/src/Umbraco.Core/Constants-Applications.cs +++ b/src/Umbraco.Core/Constants-Applications.cs @@ -145,6 +145,15 @@ public const string PartialViewMacros = "partialViewMacros"; + public static class Groups + { + public const string Settings = "settingsGroup"; + + public const string Templating = "templatingGroup"; + + public const string ThirdParty = "thirdPartyGroup"; + } + //TODO: Fill in the rest! } } diff --git a/src/Umbraco.Core/Constants-DataTypes.cs b/src/Umbraco.Core/Constants-DataTypes.cs index c9a33ba04d..f2b31be28f 100644 --- a/src/Umbraco.Core/Constants-DataTypes.cs +++ b/src/Umbraco.Core/Constants-DataTypes.cs @@ -13,7 +13,9 @@ public const int Textbox = -88; public const int Boolean = -49; - public const int Datetime = -36; + public const int DateTime = -36; + public const int DropDownSingle = -39; + public const int DropDownMultiple = -42; public const int DefaultContentListView = -95; public const int DefaultMediaListView = -96; diff --git a/src/Umbraco.Core/Constants-PropertyEditors.cs b/src/Umbraco.Core/Constants-PropertyEditors.cs index 126613cdb3..b09987ad90 100644 --- a/src/Umbraco.Core/Constants-PropertyEditors.cs +++ b/src/Umbraco.Core/Constants-PropertyEditors.cs @@ -44,26 +44,6 @@ namespace Umbraco.Core /// public const string DateTime = "Umbraco.DateTime"; - /// - /// DropDown List. - /// - public const string DropDownList = "Umbraco.DropDown"; - - /// - /// DropDown List, Publish Keys. - /// - public const string DropdownlistPublishKeys = "Umbraco.DropdownlistPublishingKeys"; - - /// - /// DropDown List Multiple. - /// - public const string DropDownListMultiple = "Umbraco.DropDownMultiple"; - - /// - /// DropDown List Multiple, Publish Keys. - /// - public const string DropdownlistMultiplePublishKeys = "Umbraco.DropdownlistMultiplePublishKeys"; - /// /// DropDown List. /// diff --git a/src/Umbraco.Core/ContentExtensions.cs b/src/Umbraco.Core/ContentExtensions.cs index e36731a8cb..b15e371e87 100644 --- a/src/Umbraco.Core/ContentExtensions.cs +++ b/src/Umbraco.Core/ContentExtensions.cs @@ -23,6 +23,49 @@ namespace Umbraco.Core #region IContent + /// + /// Gets the current status of the Content + /// + public static ContentStatus GetStatus(this IContent content, string culture = null) + { + if (content.Trashed) + return ContentStatus.Trashed; + + if (!content.ContentType.VariesByCulture()) + culture = string.Empty; + else if (culture.IsNullOrWhiteSpace()) + throw new ArgumentNullException($"{nameof(culture)} cannot be null or empty"); + + var expires = content.ContentSchedule.GetSchedule(culture, ContentScheduleAction.Expire); + if (expires != null && expires.Any(x => x.Date > DateTime.MinValue && DateTime.Now > x.Date)) + return ContentStatus.Expired; + + var release = content.ContentSchedule.GetSchedule(culture, ContentScheduleAction.Release); + if (release != null && release.Any(x => x.Date > DateTime.MinValue && x.Date > DateTime.Now)) + return ContentStatus.AwaitingRelease; + + if (content.Published) + return ContentStatus.Published; + + return ContentStatus.Unpublished; + } + + /// + /// Gets the cultures that have been flagged for unpublishing. + /// + /// Gets cultures for which content.UnpublishCulture() has been invoked. + internal static IReadOnlyList GetCulturesUnpublishing(this IContent content) + { + if (!content.Published || !content.ContentType.VariesByCulture() || !content.IsPropertyDirty("PublishCultureInfos")) + return Array.Empty(); + + var culturesChanging = content.CultureInfos.Where(x => x.Value.IsDirty()).Select(x => x.Key); + return culturesChanging + .Where(x => !content.IsCulturePublished(x) && // is not published anymore + content.WasCulturePublished(x)) // but was published before + .ToList(); + } + /// /// Returns true if this entity was just published as part of a recent save operation (i.e. it wasn't previously published) /// @@ -37,103 +80,8 @@ namespace Umbraco.Core return dirty.WasPropertyDirty("Published") && entity.Published; } - /// - /// Returns a list of the current contents ancestors, not including the content itself. - /// - /// Current content - /// - /// An enumerable list of objects - public static IEnumerable Ancestors(this IContent content, IContentService contentService) - { - return contentService.GetAncestors(content); - } - - /// - /// Returns a list of the current contents children. - /// - /// Current content - /// - /// An enumerable list of objects - public static IEnumerable Children(this IContent content, IContentService contentService) - { - return contentService.GetChildren(content.Id); - } - - /// - /// Returns a list of the current contents descendants, not including the content itself. - /// - /// Current content - /// - /// An enumerable list of objects - public static IEnumerable Descendants(this IContent content, IContentService contentService) - { - return contentService.GetDescendants(content); - } - - /// - /// Returns the parent of the current content. - /// - /// Current content - /// - /// An object - public static IContent Parent(this IContent content, IContentService contentService) - { - return contentService.GetById(content.ParentId); - } - #endregion - #region IMedia - - /// - /// Returns a list of the current medias ancestors, not including the media itself. - /// - /// Current media - /// - /// An enumerable list of objects - public static IEnumerable Ancestors(this IMedia media, IMediaService mediaService) - { - return mediaService.GetAncestors(media); - } - - - /// - /// Returns a list of the current medias children. - /// - /// Current media - /// - /// An enumerable list of objects - public static IEnumerable Children(this IMedia media, IMediaService mediaService) - { - return mediaService.GetChildren(media.Id); - } - - - /// - /// Returns a list of the current medias descendants, not including the media itself. - /// - /// Current media - /// - /// An enumerable list of objects - public static IEnumerable Descendants(this IMedia media, IMediaService mediaService) - { - return mediaService.GetDescendants(media); - } - - - /// - /// Returns the parent of the current media. - /// - /// Current media - /// - /// An object - public static IMedia Parent(this IMedia media, IMediaService mediaService) - { - return mediaService.GetById(media.ParentId); - } - - #endregion - /// /// Removes characters that are not valide XML characters from all entity properties /// of type string. See: http://stackoverflow.com/a/961504/5018 @@ -179,29 +127,7 @@ namespace Umbraco.Core } return false; } - - /// - /// Returns the children for the content base item - /// - /// - /// - /// - /// - /// This is a bit of a hack because we need to type check! - /// - internal static IEnumerable Children(IContentBase content, ServiceContext services) - { - if (content is IContent) - { - return services.ContentService.GetChildren(content.Id); - } - if (content is IMedia) - { - return services.MediaService.GetChildren(content.Id); - } - return null; - } - + /// /// Returns properties that do not belong to a group /// diff --git a/src/Umbraco.Core/ContentVariationExtensions.cs b/src/Umbraco.Core/ContentVariationExtensions.cs index d18fb4b091..516192b905 100644 --- a/src/Umbraco.Core/ContentVariationExtensions.cs +++ b/src/Umbraco.Core/ContentVariationExtensions.cs @@ -115,7 +115,7 @@ namespace Umbraco.Core /// /// Determines whether a variation varies by culture and segment. /// - public static bool VariesByCultureAndSegment(this ContentVariation variation) => (variation & ContentVariation.CultureAndSegment) > 0; + public static bool VariesByCultureAndSegment(this ContentVariation variation) => (variation & ContentVariation.CultureAndSegment) == ContentVariation.CultureAndSegment; /// /// Validates that a combination of culture and segment is valid for the variation. diff --git a/src/Umbraco.Core/IO/FileSystems.cs b/src/Umbraco.Core/IO/FileSystems.cs index 5d7088b0e1..62ce25dff0 100644 --- a/src/Umbraco.Core/IO/FileSystems.cs +++ b/src/Umbraco.Core/IO/FileSystems.cs @@ -35,11 +35,7 @@ namespace Umbraco.Core.IO private object _wkfsObject; private MediaFileSystem _mediaFileSystem; - - //fixme - is this needed to be a managed file system? seems irrelevant since it won't ever be moved and is only used in one place in code - private IFileSystem _javascriptLibraryFileSystem; - #region Constructor // DI wants a public ctor @@ -129,16 +125,6 @@ namespace Umbraco.Core.IO } } - //fixme - is this needed to be a managed file system? seems irrelevant since it won't ever be moved and is only used in one place in code - internal IFileSystem JavaScriptLibraryFileSystem - { - get - { - if (Volatile.Read(ref _wkfsInitialized) == false) EnsureWellKnownFileSystems(); - return _javascriptLibraryFileSystem; - } - } - private void EnsureWellKnownFileSystems() { LazyInitializer.EnsureInitialized(ref _wkfsObject, ref _wkfsInitialized, ref _wkfsLock, CreateWellKnownFileSystems); @@ -154,7 +140,6 @@ namespace Umbraco.Core.IO var scriptsFileSystem = new PhysicalFileSystem(SystemDirectories.Scripts); var masterPagesFileSystem = new PhysicalFileSystem(SystemDirectories.Masterpages); var mvcViewsFileSystem = new PhysicalFileSystem(SystemDirectories.MvcViews); - var javaScriptLibraryFileSystem = new PhysicalFileSystem(SystemDirectories.JavaScriptLibrary); _macroPartialFileSystem = new ShadowWrapper(macroPartialFileSystem, "Views/MacroPartials", () => IsScoped()); _partialViewsFileSystem = new ShadowWrapper(partialViewsFileSystem, "Views/Partials", () => IsScoped()); @@ -162,7 +147,6 @@ namespace Umbraco.Core.IO _scriptsFileSystem = new ShadowWrapper(scriptsFileSystem, "scripts", () => IsScoped()); _masterPagesFileSystem = new ShadowWrapper(masterPagesFileSystem, "masterpages", () => IsScoped()); _mvcViewsFileSystem = new ShadowWrapper(mvcViewsFileSystem, "Views", () => IsScoped()); - _javascriptLibraryFileSystem = new ShadowWrapper(javaScriptLibraryFileSystem, "Lib", () => IsScoped()); // filesystems obtained from GetFileSystemProvider are already wrapped and do not need to be wrapped again _mediaFileSystem = GetFileSystemProvider(); diff --git a/src/Umbraco.Core/IO/SystemDirectories.cs b/src/Umbraco.Core/IO/SystemDirectories.cs index c8eedb1614..183d48e3d9 100644 --- a/src/Umbraco.Core/IO/SystemDirectories.cs +++ b/src/Umbraco.Core/IO/SystemDirectories.cs @@ -6,28 +6,22 @@ namespace Umbraco.Core.IO //all paths has a starting but no trailing / public class SystemDirectories { - //TODO: Why on earth is this even configurable? You cannot change the /Bin folder in ASP.Net - public static string Bin => IOHelper.ReturnPath("umbracoBinDirectory", "~/bin"); + public static string Bin => "~/bin"; - public static string Base => IOHelper.ReturnPath("umbracoBaseDirectory", "~/base"); + public static string Config => "~/config"; - public static string Config => IOHelper.ReturnPath("umbracoConfigDirectory", "~/config"); + public static string Data => "~/App_Data"; - public static string Css => IOHelper.ReturnPath("umbracoCssDirectory", "~/css"); + public static string Install => "~/install"; - public static string Data => IOHelper.ReturnPath("umbracoStorageDirectory", "~/App_Data"); + //fixme: remove this + [Obsolete("Master pages are obsolete and code should be removed")] + public static string Masterpages => "~/masterpages"; - public static string Install => IOHelper.ReturnPath("umbracoInstallPath", "~/install"); - - public static string Masterpages => IOHelper.ReturnPath("umbracoMasterPagesPath", "~/masterpages"); - - //NOTE: this is not configurable and shouldn't need to be public static string AppCode => "~/App_Code"; - //NOTE: this is not configurable and shouldn't need to be public static string AppPlugins => "~/App_Plugins"; - //NOTE: this is not configurable and shouldn't need to be public static string MvcViews => "~/Views"; public static string PartialViews => MvcViews + "/Partials/"; @@ -38,21 +32,20 @@ namespace Umbraco.Core.IO public static string Scripts => IOHelper.ReturnPath("umbracoScriptsPath", "~/scripts"); + public static string Css => IOHelper.ReturnPath("umbracoCssPath", "~/css"); + public static string Umbraco => IOHelper.ReturnPath("umbracoPath", "~/umbraco"); - [Obsolete("This will be removed, there is no more umbraco_client folder")] - public static string UmbracoClient => IOHelper.ReturnPath("umbracoClientPath", "~/umbraco_client"); - - public static string UserControls => IOHelper.ReturnPath("umbracoUsercontrolsPath", "~/usercontrols"); + //fixme: remove this + [Obsolete("Usercontrols are obsolete and code should be removed")] + public static string UserControls => "~/usercontrols"; + [Obsolete("Only used by legacy load balancing which is obsolete and should be removed")] public static string WebServices => IOHelper.ReturnPath("umbracoWebservicesPath", Umbraco.EnsureEndsWith("/") + "webservices"); - //by default the packages folder should exist in the data folder - public static string Packages => IOHelper.ReturnPath("umbracoPackagesPath", Data + IOHelper.DirSepChar + "packages"); + public static string Packages => Data + IOHelper.DirSepChar + "packages"; - public static string Preview => IOHelper.ReturnPath("umbracoPreviewPath", Data + IOHelper.DirSepChar + "preview"); - - public static string JavaScriptLibrary => IOHelper.ReturnPath("umbracoJavaScriptLibraryPath", Umbraco + IOHelper.DirSepChar + "lib"); + public static string Preview => Data + IOHelper.DirSepChar + "preview"; private static string _root; diff --git a/src/Umbraco.Core/Manifest/ManifestContentAppDefinition.cs b/src/Umbraco.Core/Manifest/ManifestContentAppDefinition.cs index 6b8534a88f..d5f6c2b8c4 100644 --- a/src/Umbraco.Core/Manifest/ManifestContentAppDefinition.cs +++ b/src/Umbraco.Core/Manifest/ManifestContentAppDefinition.cs @@ -6,6 +6,7 @@ using System.Text.RegularExpressions; using Umbraco.Core.IO; using Umbraco.Core.Models; using Umbraco.Core.Models.ContentEditing; +using Umbraco.Core.Models.Membership; namespace Umbraco.Core.Manifest { @@ -19,7 +20,8 @@ namespace Umbraco.Core.Manifest // show: [ // optional, default is always show // '-content/foo', // hide for content type 'foo' // '+content/*', // show for all other content types - // '+media/*' // show for all media types + // '+media/*', // show for all media types + // '+role/admin' // show for admin users. Role based permissions will override others. // ] // }, // ... @@ -82,7 +84,7 @@ namespace Umbraco.Core.Manifest public string[] Show { get; set; } = Array.Empty(); /// - public ContentApp GetContentAppFor(object o) + public ContentApp GetContentAppFor(object o, IEnumerable userGroups) { string partA, partB; @@ -103,15 +105,49 @@ namespace Umbraco.Core.Manifest } var rules = _showRules ?? (_showRules = ShowRule.Parse(Show).ToArray()); + var userGroupsList = userGroups.ToList(); - // if no 'show' is specified, then always display the content app - if (rules.Length > 0) + var okRole = false; + var hasRole = false; + var okType = false; + var hasType = false; + + foreach (var rule in rules) { - var ok = false; - - // else iterate over each entry - foreach (var rule in rules) + if (rule.PartA.InvariantEquals("role")) { + // if roles have been ok-ed already, skip the rule + if (okRole) + continue; + + // remember we have role rules + hasRole = true; + + foreach (var group in userGroupsList) + { + // if the entry does not apply, skip + if (!rule.Matches("role", group.Alias)) + continue; + + // if the entry applies, + // if it's an exclude entry, exit, do not display the content app + if (!rule.Show) + return null; + + // else ok to display, remember roles are ok, break from userGroupsList + okRole = rule.Show; + break; + } + } + else // it is a type rule + { + // if type has been ok-ed already, skip the rule + if (okType) + continue; + + // remember we have type rules + hasType = true; + // if the entry does not apply, skip it if (!rule.Matches(partA, partB)) continue; @@ -121,16 +157,18 @@ namespace Umbraco.Core.Manifest if (!rule.Show) return null; - // else break - ok to display - ok = true; - break; + // else ok to display, remember type rules are ok + okType = true; } - - // when 'show' is specified, default is to *not* show the content app - if (!ok) - return null; } + // if roles rules are specified but not ok, + // or if type roles are specified but not ok, + // cannot display the content app + if ((hasRole && !okRole) || (hasType && !okType)) + return null; + + // else // content app can be displayed return _app ?? (_app = new ContentApp { diff --git a/src/Umbraco.Core/Migrations/Expressions/Alter/Expressions/AlterColumnExpression.cs b/src/Umbraco.Core/Migrations/Expressions/Alter/Expressions/AlterColumnExpression.cs index f19fc94c97..1b00b03ca2 100644 --- a/src/Umbraco.Core/Migrations/Expressions/Alter/Expressions/AlterColumnExpression.cs +++ b/src/Umbraco.Core/Migrations/Expressions/Alter/Expressions/AlterColumnExpression.cs @@ -18,7 +18,6 @@ namespace Umbraco.Core.Migrations.Expressions.Alter.Expressions protected override string GetSql() { - return string.Format(SqlSyntax.AlterColumn, SqlSyntax.GetQuotedTableName(TableName), SqlSyntax.Format(Column)); diff --git a/src/Umbraco.Core/Migrations/Expressions/Alter/Table/AlterTableBuilder.cs b/src/Umbraco.Core/Migrations/Expressions/Alter/Table/AlterTableBuilder.cs index 989ce95002..eee9826a85 100644 --- a/src/Umbraco.Core/Migrations/Expressions/Alter/Table/AlterTableBuilder.cs +++ b/src/Umbraco.Core/Migrations/Expressions/Alter/Table/AlterTableBuilder.cs @@ -2,6 +2,9 @@ using NPoco; using Umbraco.Core.Migrations.Expressions.Alter.Expressions; using Umbraco.Core.Migrations.Expressions.Common.Expressions; +using Umbraco.Core.Migrations.Expressions.Create.Expressions; +using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.DatabaseAnnotations; using Umbraco.Core.Persistence.DatabaseModelDefinitions; namespace Umbraco.Core.Migrations.Expressions.Alter.Table @@ -87,6 +90,21 @@ namespace Umbraco.Core.Migrations.Expressions.Alter.Table public IAlterTableColumnOptionBuilder PrimaryKey() { CurrentColumn.IsPrimaryKey = true; + + // see notes in CreateTableBuilder + if (Expression.DatabaseType.IsMySql() == false) + { + var expression = new CreateConstraintExpression(_context, ConstraintType.PrimaryKey) + { + Constraint = + { + TableName = Expression.TableName, + Columns = new[] { CurrentColumn.Name } + } + }; + Expression.Expressions.Add(expression); + } + return this; } @@ -94,6 +112,22 @@ namespace Umbraco.Core.Migrations.Expressions.Alter.Table { CurrentColumn.IsPrimaryKey = true; CurrentColumn.PrimaryKeyName = primaryKeyName; + + // see notes in CreateTableBuilder + if (Expression.DatabaseType.IsMySql() == false) + { + var expression = new CreateConstraintExpression(_context, ConstraintType.PrimaryKey) + { + Constraint = + { + ConstraintName = primaryKeyName, + TableName = Expression.TableName, + Columns = new[] { CurrentColumn.Name } + } + }; + Expression.Expressions.Add(expression); + } + return this; } @@ -121,8 +155,8 @@ namespace Umbraco.Core.Migrations.Expressions.Alter.Table var index = new CreateIndexExpression(_context, new IndexDefinition { Name = indexName, - TableName = Expression.TableName, - IsUnique = true + TableName = Expression.TableName, + IndexType = IndexTypes.UniqueNonClustered }); index.Index.Columns.Add(new IndexColumnDefinition diff --git a/src/Umbraco.Core/Migrations/Expressions/Create/Column/CreateColumnBuilder.cs b/src/Umbraco.Core/Migrations/Expressions/Create/Column/CreateColumnBuilder.cs index 28de5aef14..656aedcea0 100644 --- a/src/Umbraco.Core/Migrations/Expressions/Create/Column/CreateColumnBuilder.cs +++ b/src/Umbraco.Core/Migrations/Expressions/Create/Column/CreateColumnBuilder.cs @@ -1,6 +1,7 @@ using System.Data; using NPoco; using Umbraco.Core.Migrations.Expressions.Common.Expressions; +using Umbraco.Core.Persistence.DatabaseAnnotations; using Umbraco.Core.Persistence.DatabaseModelDefinitions; namespace Umbraco.Core.Migrations.Expressions.Create.Column @@ -112,8 +113,8 @@ namespace Umbraco.Core.Migrations.Expressions.Create.Column var index = new CreateIndexExpression(_context, new IndexDefinition { Name = indexName, - TableName = Expression.TableName, - IsUnique = true + TableName = Expression.TableName, + IndexType = IndexTypes.UniqueNonClustered }); index.Index.Columns.Add(new IndexColumnDefinition diff --git a/src/Umbraco.Core/Migrations/Expressions/Create/Index/CreateIndexBuilder.cs b/src/Umbraco.Core/Migrations/Expressions/Create/Index/CreateIndexBuilder.cs index ad8ac7f22d..1f2cb93f95 100644 --- a/src/Umbraco.Core/Migrations/Expressions/Create/Index/CreateIndexBuilder.cs +++ b/src/Umbraco.Core/Migrations/Expressions/Create/Index/CreateIndexBuilder.cs @@ -56,41 +56,29 @@ namespace Umbraco.Core.Migrations.Expressions.Create.Index /// ICreateIndexOnColumnBuilder ICreateIndexColumnOptionsBuilder.Unique() - { - Expression.Index.IsUnique = true; - //if it is Unique then it must be unique nonclustered and set the other flags - Expression.Index.IndexType = IndexTypes.UniqueNonClustered; - Expression.Index.IsClustered = false; + { + Expression.Index.IndexType = IndexTypes.UniqueNonClustered; return this; } /// public ICreateIndexOnColumnBuilder NonClustered() { - Expression.Index.IndexType = IndexTypes.NonClustered; - Expression.Index.IsClustered = false; - Expression.Index.IndexType = IndexTypes.NonClustered; - Expression.Index.IsUnique = false; + Expression.Index.IndexType = IndexTypes.NonClustered; return this; } /// public ICreateIndexOnColumnBuilder Clustered() - { - Expression.Index.IndexType = IndexTypes.Clustered; - Expression.Index.IsClustered = true; - //if it is clustered then we have to change the index type set the other flags - Expression.Index.IndexType = IndexTypes.Clustered; - Expression.Index.IsClustered = true; - Expression.Index.IsUnique = false; - return this; + { + Expression.Index.IndexType = IndexTypes.Clustered; + return this; } /// ICreateIndexOnColumnBuilder ICreateIndexOptionsBuilder.Unique() { - Expression.Index.IndexType = IndexTypes.UniqueNonClustered; - Expression.Index.IsUnique = true; + Expression.Index.IndexType = IndexTypes.UniqueNonClustered; return this; } } diff --git a/src/Umbraco.Core/Migrations/Expressions/Create/Table/CreateTableBuilder.cs b/src/Umbraco.Core/Migrations/Expressions/Create/Table/CreateTableBuilder.cs index 10836fd228..f765a58169 100644 --- a/src/Umbraco.Core/Migrations/Expressions/Create/Table/CreateTableBuilder.cs +++ b/src/Umbraco.Core/Migrations/Expressions/Create/Table/CreateTableBuilder.cs @@ -3,6 +3,7 @@ using NPoco; using Umbraco.Core.Migrations.Expressions.Common.Expressions; using Umbraco.Core.Migrations.Expressions.Create.Expressions; using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.DatabaseAnnotations; using Umbraco.Core.Persistence.DatabaseModelDefinitions; namespace Umbraco.Core.Migrations.Expressions.Create.Table @@ -176,8 +177,8 @@ namespace Umbraco.Core.Migrations.Expressions.Create.Table { Name = indexName, SchemaName = Expression.SchemaName, - TableName = Expression.TableName, - IsUnique = true + TableName = Expression.TableName, + IndexType = IndexTypes.UniqueNonClustered }); index.Index.Columns.Add(new IndexColumnDefinition diff --git a/src/Umbraco.Core/Migrations/IMigration.cs b/src/Umbraco.Core/Migrations/IMigration.cs index 53b7874b3a..c929234f77 100644 --- a/src/Umbraco.Core/Migrations/IMigration.cs +++ b/src/Umbraco.Core/Migrations/IMigration.cs @@ -7,6 +7,9 @@ namespace Umbraco.Core.Migrations /// public interface IMigration : IDiscoverable { + /// + /// Executes the migration. + /// void Migrate(); } } diff --git a/src/Umbraco.Core/Migrations/IMigrationContext.cs b/src/Umbraco.Core/Migrations/IMigrationContext.cs index 4db1b07b63..80ba78b6de 100644 --- a/src/Umbraco.Core/Migrations/IMigrationContext.cs +++ b/src/Umbraco.Core/Migrations/IMigrationContext.cs @@ -24,8 +24,13 @@ namespace Umbraco.Core.Migrations ISqlContext SqlContext { get; } /// - /// Gets the expression index. + /// Gets or sets the expression index. /// int Index { get; set; } + + /// + /// Gets or sets a value indicating whether an expression is being built. + /// + bool BuildingExpression { get; set; } } } diff --git a/src/Umbraco.Core/Migrations/IncompleteMigrationExpressionException.cs b/src/Umbraco.Core/Migrations/IncompleteMigrationExpressionException.cs new file mode 100644 index 0000000000..91d1838d6f --- /dev/null +++ b/src/Umbraco.Core/Migrations/IncompleteMigrationExpressionException.cs @@ -0,0 +1,28 @@ +using System; + +namespace Umbraco.Core.Migrations +{ + /// + /// Represents errors that occurs when a migration exception is not executed. + /// + /// + /// Migration expression such as Alter.Table(...).Do() *must* end with Do() else they are + /// not executed. When a non-executed expression is detected, an IncompleteMigrationExpressionException + /// is thrown. + /// + public class IncompleteMigrationExpressionException : Exception + { + /// + /// Initializes a new instance of the class. + /// + public IncompleteMigrationExpressionException() + { } + + /// + /// Initializes a new instance of the class with a message. + /// + public IncompleteMigrationExpressionException(string message) + : base(message) + { } + } +} diff --git a/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs b/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs index ba491fb5e1..eb7cafcb01 100644 --- a/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs +++ b/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs @@ -114,15 +114,15 @@ namespace Umbraco.Core.Migrations.Install _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = -51, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,-51", SortOrder = 2, UniqueId = new Guid("2e6d3631-066e-44b8-aec4-96f09099b2b5"), Text = "Numeric", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = -49, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,-49", SortOrder = 2, UniqueId = new Guid("92897bc6-a5f3-4ffe-ae27-f2e7e33dda49"), Text = "True/false", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = -43, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,-43", SortOrder = 2, UniqueId = new Guid("fbaf13a8-4036-41f2-93a3-974f678c312a"), Text = "Checkbox list", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); - _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = -42, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,-42", SortOrder = 2, UniqueId = new Guid("0b6a45e7-44ba-430d-9da5-4e46060b9e03"), Text = "Dropdown", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.DropDownMultiple, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Constants.DataTypes.DropDownMultiple}", SortOrder = 2, UniqueId = new Guid("0b6a45e7-44ba-430d-9da5-4e46060b9e03"), Text = "Dropdown", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = -41, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,-41", SortOrder = 2, UniqueId = new Guid("5046194e-4237-453c-a547-15db3a07c4e1"), Text = "Date Picker", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = -40, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,-40", SortOrder = 2, UniqueId = new Guid("bb5f57c9-ce2b-4bb9-b697-4caca783a805"), Text = "Radiobox", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); - _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = -39, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,-39", SortOrder = 2, UniqueId = new Guid("f38f0ac7-1d27-439c-9f3f-089cd8825a53"), Text = "Dropdown multiple", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.DropDownSingle, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Constants.DataTypes.DropDownSingle}", SortOrder = 2, UniqueId = new Guid("f38f0ac7-1d27-439c-9f3f-089cd8825a53"), Text = "Dropdown multiple", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = -37, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,-37", SortOrder = 2, UniqueId = new Guid("0225af17-b302-49cb-9176-b9f35cab9c17"), Text = "Approved Color", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = -36, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,-36", SortOrder = 2, UniqueId = new Guid("e4d66c0f-b935-4200-81f0-025f7256b89a"), Text = "Date Picker with time", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); - _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.DefaultContentListView, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,-95", SortOrder = 2, UniqueId = new Guid("C0808DD3-8133-4E4B-8CE8-E2BEA84A96A4"), Text = Constants.Conventions.DataTypes.ListViewPrefix + "Content", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); - _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.DefaultMediaListView, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,-96", SortOrder = 2, UniqueId = new Guid("3A0156C4-3B8C-4803-BDC1-6871FAA83FFF"), Text = Constants.Conventions.DataTypes.ListViewPrefix + "Media", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); - _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.DefaultMembersListView, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,-97", SortOrder = 2, UniqueId = new Guid("AA2C52A0-CE87-4E65-A47C-7DF09358585D"), Text = Constants.Conventions.DataTypes.ListViewPrefix + "Members", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.DefaultContentListView, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Constants.DataTypes.DefaultContentListView}", SortOrder = 2, UniqueId = new Guid("C0808DD3-8133-4E4B-8CE8-E2BEA84A96A4"), Text = Constants.Conventions.DataTypes.ListViewPrefix + "Content", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.DefaultMediaListView, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Constants.DataTypes.DefaultMediaListView}", SortOrder = 2, UniqueId = new Guid("3A0156C4-3B8C-4803-BDC1-6871FAA83FFF"), Text = Constants.Conventions.DataTypes.ListViewPrefix + "Media", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.DefaultMembersListView, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Constants.DataTypes.DefaultMembersListView}", SortOrder = 2, UniqueId = new Guid("AA2C52A0-CE87-4E65-A47C-7DF09358585D"), Text = Constants.Conventions.DataTypes.ListViewPrefix + "Members", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1031, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1031", SortOrder = 2, UniqueId = new Guid("f38bd2d7-65d0-48e6-95dc-87ce06ec2d3d"), Text = Constants.Conventions.MediaTypes.Folder, NodeObjectType = Constants.ObjectTypes.MediaType, CreateDate = DateTime.Now }); _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1032, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1032", SortOrder = 2, UniqueId = new Guid("cc07b313-0843-4aa8-bbda-871c8da728c8"), Text = Constants.Conventions.MediaTypes.Image, NodeObjectType = Constants.ObjectTypes.MediaType, CreateDate = DateTime.Now }); _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1033, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1033", SortOrder = 2, UniqueId = new Guid("4c52d8ab-54e6-40cd-999c-7a5f24903e4d"), Text = Constants.Conventions.MediaTypes.File, NodeObjectType = Constants.ObjectTypes.MediaType, CreateDate = DateTime.Now }); @@ -149,6 +149,7 @@ namespace Umbraco.Core.Migrations.Install _database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Constants.Locks.MemberTypes, Name = "MemberTypes" }); _database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Constants.Locks.MemberTree, Name = "MemberTree" }); _database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Constants.Locks.Domains, Name = "Domains" }); + _database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Constants.Locks.KeyValues, Name = "KeyValues" }); _database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Constants.Locks.Languages, Name = "Languages" }); } @@ -242,12 +243,12 @@ namespace Umbraco.Core.Migrations.Install private void CreateDataTypeData() { - void InsertDataTypeDto(int id, string dbType, string configuration = null) + void InsertDataTypeDto(int id, string editorAlias, string dbType, string configuration = null) { var dataTypeDto = new DataTypeDto { NodeId = id, - EditorAlias = Constants.PropertyEditors.Aliases.NoEdit, + EditorAlias = editorAlias, DbType = dbType }; @@ -270,18 +271,18 @@ namespace Umbraco.Core.Migrations.Install _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = -88, EditorAlias = Constants.PropertyEditors.Aliases.TextBox, DbType = "Nvarchar" }); _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = -89, EditorAlias = Constants.PropertyEditors.Aliases.TextArea, DbType = "Ntext" }); _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = -90, EditorAlias = Constants.PropertyEditors.Aliases.UploadField, DbType = "Nvarchar" }); - InsertDataTypeDto(Constants.DataTypes.LabelString, "Nvarchar", "{\"umbracoDataValueType\":\"STRING\"}"); - InsertDataTypeDto(Constants.DataTypes.LabelInt,"Integer", "{\"umbracoDataValueType\":\"INT\"}"); - InsertDataTypeDto(Constants.DataTypes.LabelBigint, "Nvarchar", "{\"umbracoDataValueType\":\"BIGINT\"}"); - InsertDataTypeDto(Constants.DataTypes.LabelDateTime, "Date", "{\"umbracoDataValueType\":\"DATETIME\"}"); - InsertDataTypeDto(Constants.DataTypes.LabelDecimal, "Decimal", "{\"umbracoDataValueType\":\"DECIMAL\"}"); - InsertDataTypeDto(Constants.DataTypes.LabelTime, "Date", "{\"umbracoDataValueType\":\"TIME\"}"); + InsertDataTypeDto(Constants.DataTypes.LabelString, Constants.PropertyEditors.Aliases.NoEdit, "Nvarchar", "{\"umbracoDataValueType\":\"STRING\"}"); + InsertDataTypeDto(Constants.DataTypes.LabelInt, Constants.PropertyEditors.Aliases.NoEdit, "Integer", "{\"umbracoDataValueType\":\"INT\"}"); + InsertDataTypeDto(Constants.DataTypes.LabelBigint, Constants.PropertyEditors.Aliases.NoEdit, "Nvarchar", "{\"umbracoDataValueType\":\"BIGINT\"}"); + InsertDataTypeDto(Constants.DataTypes.LabelDateTime, Constants.PropertyEditors.Aliases.NoEdit, "Date", "{\"umbracoDataValueType\":\"DATETIME\"}"); + InsertDataTypeDto(Constants.DataTypes.LabelDecimal, Constants.PropertyEditors.Aliases.NoEdit, "Decimal", "{\"umbracoDataValueType\":\"DECIMAL\"}"); + InsertDataTypeDto(Constants.DataTypes.LabelTime, Constants.PropertyEditors.Aliases.NoEdit, "Date", "{\"umbracoDataValueType\":\"TIME\"}"); _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = -36, EditorAlias = Constants.PropertyEditors.Aliases.DateTime, DbType = "Date" }); _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = -37, EditorAlias = Constants.PropertyEditors.Aliases.ColorPicker, DbType = "Nvarchar" }); - _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = -39, EditorAlias = Constants.PropertyEditors.Aliases.DropDownListMultiple, DbType = "Nvarchar" }); + InsertDataTypeDto(Constants.DataTypes.DropDownSingle, Constants.PropertyEditors.Aliases.DropDownListFlexible, "Nvarchar", "{\"multiple\":false}"); _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = -40, EditorAlias = Constants.PropertyEditors.Aliases.RadioButtonList, DbType = "Nvarchar" }); _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = -41, EditorAlias = Constants.PropertyEditors.Aliases.Date, DbType = "Date" }); - _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = -42, EditorAlias = Constants.PropertyEditors.Aliases.DropDownList, DbType = "Integer" }); + InsertDataTypeDto(Constants.DataTypes.DropDownMultiple, Constants.PropertyEditors.Aliases.DropDownListFlexible, "Nvarchar", "{\"multiple\":true}"); _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = -43, EditorAlias = Constants.PropertyEditors.Aliases.CheckBoxList, DbType = "Nvarchar" }); _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = 1041, EditorAlias = Constants.PropertyEditors.Aliases.Tags, DbType = "Ntext", Configuration = "{\"group\":\"default\", \"storageType\":\"Json\"}" diff --git a/src/Umbraco.Core/Migrations/Install/DatabaseSchemaCreator.cs b/src/Umbraco.Core/Migrations/Install/DatabaseSchemaCreator.cs index d45126f07f..64be8161f2 100644 --- a/src/Umbraco.Core/Migrations/Install/DatabaseSchemaCreator.cs +++ b/src/Umbraco.Core/Migrations/Install/DatabaseSchemaCreator.cs @@ -81,7 +81,8 @@ namespace Umbraco.Core.Migrations.Install typeof (ConsentDto), typeof (AuditEntryDto), typeof (ContentVersionCultureVariationDto), - typeof (DocumentCultureVariationDto) + typeof (DocumentCultureVariationDto), + typeof (ContentScheduleDto) }; /// @@ -334,16 +335,60 @@ namespace Umbraco.Core.Migrations.Install #region Utilities + /// + /// Returns whether a table with the specified exists in the database. + /// + /// The name of the table. + /// true if the table exists; otherwise false. + /// + /// + /// if (schemaHelper.TableExist("MyTable")) + /// { + /// // do something when the table exists + /// } + /// + /// public bool TableExists(string tableName) { return SqlSyntax.DoesTableExist(_database, tableName); } + /// + /// Returns whether the table for the specified exists in the database. + /// + /// The type representing the DTO/table. + /// true if the table exists; otherwise false. + /// + /// + /// if (schemaHelper.TableExist<MyDto>) + /// { + /// // do something when the table exists + /// } + /// + /// + /// + /// If has been decorated with an , the name from that + /// attribute will be used for the table name. If the attribute is not present, the name + /// will be used instead. + /// public bool TableExists() { var table = DefinitionFactory.GetTableDefinition(typeof(T), SqlSyntax); return table != null && TableExists(table.Name); } - // this is used in tests + /// + /// Creates a new table in the database based on the type of . + /// + /// The type representing the DTO/table. + /// Whether the table should be overwritten if it already exists. + /// + /// If has been decorated with an , the name from that + /// attribute will be used for the table name. If the attribute is not present, the name + /// will be used instead. + /// + /// If a table with the same name already exists, the parameter will determine + /// whether the table is overwritten. If true, the table will be overwritten, whereas this method will + /// not do anything if the parameter is false. + /// internal void CreateTable(bool overwrite = false) where T : new() { @@ -351,6 +396,21 @@ namespace Umbraco.Core.Migrations.Install CreateTable(overwrite, tableType, new DatabaseDataCreator(_database, _logger)); } + /// + /// Creates a new table in the database for the specified . + /// + /// Whether the table should be overwritten if it already exists. + /// The the representing the table. + /// + /// + /// If has been decorated with an , the name from + /// that attribute will be used for the table name. If the attribute is not present, the name + /// will be used instead. + /// + /// If a table with the same name already exists, the parameter will determine + /// whether the table is overwritten. If true, the table will be overwritten, whereas this method will + /// not do anything if the parameter is false. + /// public void CreateTable(bool overwrite, Type modelType, DatabaseDataCreator dataCreation) { var tableDefinition = DefinitionFactory.GetTableDefinition(modelType, SqlSyntax); @@ -364,6 +424,8 @@ namespace Umbraco.Core.Migrations.Install var tableExist = TableExists(tableName); if (overwrite && tableExist) { + _logger.Info("Table '{TableName}' already exists, but will be recreated", tableName); + DropTable(tableName); tableExist = false; } @@ -417,12 +479,38 @@ namespace Umbraco.Core.Migrations.Install } transaction.Complete(); + + if (overwrite) + { + _logger.Info("Table '{TableName}' was recreated", tableName); + } + else + { + _logger.Info("New table '{TableName}' was created", tableName); + } } } - - _logger.Info("Created table '{TableName}'", tableName); + else + { + // The table exists and was not recreated/overwritten. + _logger.Info("Table '{TableName}' already exists - no changes were made", tableName); + } } + /// + /// Drops the table for the specified . + /// + /// The type representing the DTO/table. + /// + /// + /// schemaHelper.DropTable<MyDto>); + /// + /// + /// + /// If has been decorated with an , the name from that + /// attribute will be used for the table name. If the attribute is not present, the name + /// will be used instead. + /// public void DropTable(string tableName) { var sql = new Sql(string.Format(SqlSyntax.DropTable, SqlSyntax.GetQuotedTableName(tableName))); diff --git a/src/Umbraco.Core/Migrations/MigrationBase.cs b/src/Umbraco.Core/Migrations/MigrationBase.cs index 9fbee0ed92..58edcae80a 100644 --- a/src/Umbraco.Core/Migrations/MigrationBase.cs +++ b/src/Umbraco.Core/Migrations/MigrationBase.cs @@ -62,42 +62,65 @@ namespace Umbraco.Core.Migrations /// protected Sql Sql(string sql, params object[] args) => Context.SqlContext.Sql(sql, args); - /// + /// + /// Executes the migration. + /// public abstract void Migrate(); + /// + void IMigration.Migrate() + { + Migrate(); + + // ensure there is no building expression + // ie we did not forget to .Do() an expression + if (Context.BuildingExpression) + throw new IncompleteMigrationExpressionException("The migration has run, but leaves an expression that has not run."); + } + + // ensures we are not already building, + // ie we did not forget to .Do() an expression + private T BeginBuild(T builder) + { + if (Context.BuildingExpression) + throw new IncompleteMigrationExpressionException("Cannot create a new expression: the previous expression has not run."); + Context.BuildingExpression = true; + return builder; + } + /// /// Builds an Alter expression. /// - public IAlterBuilder Alter => new AlterBuilder(Context); + public IAlterBuilder Alter => BeginBuild(new AlterBuilder(Context)); /// /// Builds a Create expression. /// - public ICreateBuilder Create => new CreateBuilder(Context); + public ICreateBuilder Create => BeginBuild(new CreateBuilder(Context)); /// /// Builds a Delete expression. /// - public IDeleteBuilder Delete => new DeleteBuilder(Context); + public IDeleteBuilder Delete => BeginBuild(new DeleteBuilder(Context)); /// /// Builds an Execute expression. /// - public IExecuteBuilder Execute => new ExecuteBuilder(Context); + public IExecuteBuilder Execute => BeginBuild(new ExecuteBuilder(Context)); /// /// Builds an Insert expression. /// - public IInsertBuilder Insert => new InsertBuilder(Context); + public IInsertBuilder Insert => BeginBuild(new InsertBuilder(Context)); /// /// Builds a Rename expression. /// - public IRenameBuilder Rename => new RenameBuilder(Context); + public IRenameBuilder Rename => BeginBuild(new RenameBuilder(Context)); /// /// Builds an Update expression. /// - public IUpdateBuilder Update => new UpdateBuilder(Context); + public IUpdateBuilder Update => BeginBuild(new UpdateBuilder(Context)); } } diff --git a/src/Umbraco.Core/Migrations/MigrationBase_Extra.cs b/src/Umbraco.Core/Migrations/MigrationBase_Extra.cs index b1b405bcf4..9e13badacf 100644 --- a/src/Umbraco.Core/Migrations/MigrationBase_Extra.cs +++ b/src/Umbraco.Core/Migrations/MigrationBase_Extra.cs @@ -18,12 +18,26 @@ namespace Umbraco.Core.Migrations AddColumn(table, table.Name, columnName); } + protected void AddColumnIfNotExists(IEnumerable columns, string columnName) + { + var table = DefinitionFactory.GetTableDefinition(typeof(T), SqlSyntax); + if (columns.Any(x => x.TableName.InvariantEquals(table.Name) && !x.ColumnName.InvariantEquals(columnName))) + AddColumn(table, table.Name, columnName); + } + protected void AddColumn(string tableName, string columnName) { var table = DefinitionFactory.GetTableDefinition(typeof(T), SqlSyntax); AddColumn(table, tableName, columnName); } + protected void AddColumnIfNotExists(IEnumerable columns, string tableName, string columnName) + { + var table = DefinitionFactory.GetTableDefinition(typeof(T), SqlSyntax); + if (columns.Any(x => x.TableName.InvariantEquals(tableName) && !x.ColumnName.InvariantEquals(columnName))) + AddColumn(table, tableName, columnName); + } + private void AddColumn(TableDefinition table, string tableName, string columnName) { if (ColumnExists(tableName, columnName)) return; diff --git a/src/Umbraco.Core/Migrations/MigrationContext.cs b/src/Umbraco.Core/Migrations/MigrationContext.cs index d0802c813d..da454fab03 100644 --- a/src/Umbraco.Core/Migrations/MigrationContext.cs +++ b/src/Umbraco.Core/Migrations/MigrationContext.cs @@ -4,20 +4,33 @@ using Umbraco.Core.Persistence; namespace Umbraco.Core.Migrations { + /// + /// Represents a migration context. + /// internal class MigrationContext : IMigrationContext { + /// + /// Initializes a new instance of the class. + /// public MigrationContext(IUmbracoDatabase database, ILogger logger) { Database = database ?? throw new ArgumentNullException(nameof(database)); Logger = logger ?? throw new ArgumentNullException(nameof(logger)); } + /// public ILogger Logger { get; } + /// public IUmbracoDatabase Database { get; } + /// public ISqlContext SqlContext => Database.SqlContext; + /// public int Index { get; set; } + + /// + public bool BuildingExpression { get; set; } } } diff --git a/src/Umbraco.Core/Migrations/MigrationExpressionBase.cs b/src/Umbraco.Core/Migrations/MigrationExpressionBase.cs index f1c535b466..6ac92a07aa 100644 --- a/src/Umbraco.Core/Migrations/MigrationExpressionBase.cs +++ b/src/Umbraco.Core/Migrations/MigrationExpressionBase.cs @@ -50,12 +50,13 @@ namespace Umbraco.Core.Migrations if (_executed) throw new InvalidOperationException("This expression has already been executed."); _executed = true; + Context.BuildingExpression = false; var sql = GetSql(); if (string.IsNullOrWhiteSpace(sql)) { - Logger.Info(GetType(), "SQL [{ContextIndex}: ", Context.Index); + Logger.Info(GetType(), "SQL [{ContextIndex}]: ", Context.Index); } else { diff --git a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs index eeaf7533a9..ec49544976 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs @@ -119,7 +119,7 @@ namespace Umbraco.Core.Migrations.Upgrade Chain("{517CE9EA-36D7-472A-BF4B-A0D6FB1B8F89}"); // from 7.12.0 Chain("{BBD99901-1545-40E4-8A5A-D7A675C7D2F2}"); // from 7.12.0 //Chain("{2C87AA47-D1BC-4ECB-8A73-2D8D1046C27F}"); // stephan added that one = merge conflict, remove - + Chain("{8B14CEBD-EE47-4AAD-A841-93551D917F11}"); // add andy's after others, with a new target state From("{CF51B39B-9B9A-4740-BB7C-EAF606A7BFBF}") // and provide a path out of andy's .CopyChain("{39E5B1F7-A50B-437E-B768-1723AEC45B65}", "{BBD99901-1545-40E4-8A5A-D7A675C7D2F2}", "{8B14CEBD-EE47-4AAD-A841-93551D917F11}"); // to next @@ -137,6 +137,13 @@ namespace Umbraco.Core.Migrations.Upgrade // resume at {290C18EE-B3DE-4769-84F1-1F467F3F76DA}... Chain("{6A2C7C1B-A9DB-4EA9-B6AB-78E7D5B722A7}"); + Chain("{77874C77-93E5-4488-A404-A630907CEEF0}"); + Chain("{8804D8E8-FE62-4E3A-B8A2-C047C2118C38}"); + Chain("{23275462-446E-44C7-8C2C-3B8C1127B07D}"); + Chain("{6B251841-3069-4AD5-8AE9-861F9523E8DA}"); + Chain("{EE429F1B-9B26-43CA-89F8-A86017C809A3}"); + Chain("{08919C4B-B431-449C-90EC-2B8445B5C6B1}"); + Chain("{7EB0254C-CB8B-4C75-B15B-D48C55B449EB}"); //FINAL diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_7_12_0/SetDefaultTagsStorageType.cs b/src/Umbraco.Core/Migrations/Upgrade/V_7_12_0/SetDefaultTagsStorageType.cs index d8f2d37067..c8d65961f4 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_7_12_0/SetDefaultTagsStorageType.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_7_12_0/SetDefaultTagsStorageType.cs @@ -1,47 +1,51 @@ -using System; -using System.Linq; -using Newtonsoft.Json; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using Umbraco.Core.Logging; +using Umbraco.Core.Models; +using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Dtos; -using Umbraco.Core.Persistence.SqlSyntax; +using Umbraco.Core.PropertyEditors; namespace Umbraco.Core.Migrations.Upgrade.V_7_12_0 { /// - /// Set the default storageType for the tags datatype to "CSV" to ensure backwards compatibilty since the default is going to be JSON in new versions - /// + /// Set the default storageType for the tags datatype to "CSV" to ensure backwards compatibility since the default is going to be JSON in new versions. + /// public class SetDefaultTagsStorageType : MigrationBase { - public SetDefaultTagsStorageType(IMigrationContext context) : base(context) - { - } + public SetDefaultTagsStorageType(IMigrationContext context) + : base(context) + { } + + // dummy editor for deserialization + private class TagConfigurationEditor : ConfigurationEditor + { } public override void Migrate() { - if (Context?.Database == null) return; + // get all Umbraco.Tags datatypes + var dataTypeDtos = Database.Fetch(Context.SqlContext.Sql() + .Select() + .From() + .Where(x => x.EditorAlias == Constants.PropertyEditors.Aliases.Tags)); - // We need to get all datatypes with an alias of "umbraco.tags" so we can loop over them and set the missing values if needed - var datatypes = Context.Database.Fetch(); - var tagsDataTypes = datatypes.Where(x => string.Equals(x.EditorAlias, Constants.PropertyEditors.Aliases.Tags, StringComparison.InvariantCultureIgnoreCase)); + // get a dummy editor for deserialization + var editor = new TagConfigurationEditor(); - foreach (var datatype in tagsDataTypes) + foreach (var dataTypeDto in dataTypeDtos) { - var dataTypePreValues = JsonConvert.DeserializeObject(datatype.Configuration); + // need to check storageType on raw dictionary, as TagConfiguration would have a default value + var dictionary = JsonConvert.DeserializeObject(dataTypeDto.Configuration); - // We need to check if the node has a "storageType" set - if (!dataTypePreValues.ContainsKey("storageType")) + // if missing, use TagConfiguration to properly update the configuration + // due to ... reasons ... the key can start with a lower or upper 'S' + if (!dictionary.ContainsKey("storageType") && !dictionary.ContainsKey("StorageType")) { - dataTypePreValues["storageType"] = "Csv"; + var configuration = (TagConfiguration)editor.FromDatabase(dataTypeDto.Configuration); + configuration.StorageType = TagsStorageType.Csv; + dataTypeDto.Configuration = ConfigurationEditor.ToDatabase(configuration); + Database.Update(dataTypeDto); } - - Update.Table(Constants.DatabaseSchema.Tables.DataType) - .Set(new { config = JsonConvert.SerializeObject(dataTypePreValues) }) - .Where(new { nodeId = datatype.NodeId }) - .Do(); } } - - } } diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/AddLogTableColumns.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/AddLogTableColumns.cs new file mode 100644 index 0000000000..c8a6e38dad --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/AddLogTableColumns.cs @@ -0,0 +1,20 @@ +using System.Linq; +using Umbraco.Core.Persistence.Dtos; + +namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 +{ + public class AddLogTableColumns : MigrationBase + { + public AddLogTableColumns(IMigrationContext context) + : base(context) + { } + + public override void Migrate() + { + var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToList(); + + AddColumnIfNotExists(columns, "entityType"); + AddColumnIfNotExists(columns, "parameters"); + } + } +} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypeMigration.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypeMigration.cs index eb39f37112..d7180385f0 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypeMigration.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypeMigration.cs @@ -7,10 +7,11 @@ using NPoco; using Umbraco.Core.Migrations.Install; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Dtos; -using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Persistence.Querying; namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 { + public class DataTypeMigration : MigrationBase { public DataTypeMigration(IMigrationContext context) @@ -79,10 +80,6 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 Database.Update(dataType); } - - // drop preValues table - // FIXME keep it around for now - //Delete.Table("cmsDataTypePreValues"); } [TableName("cmsDataTypePreValues")] diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DropDownPropertyEditorsMigration.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DropDownPropertyEditorsMigration.cs new file mode 100644 index 0000000000..ed2990aff7 --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DropDownPropertyEditorsMigration.cs @@ -0,0 +1,185 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core.Cache; +using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.Dtos; +using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Logging; +using Umbraco.Core.Models; +using Umbraco.Core.Sync; + +namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 +{ + public class DropDownPropertyEditorsMigration : MigrationBase + { + private readonly CacheRefresherCollection _cacheRefreshers; + private readonly IServerMessenger _serverMessenger; + + public DropDownPropertyEditorsMigration(IMigrationContext context, CacheRefresherCollection cacheRefreshers, IServerMessenger serverMessenger) + : base(context) + { + _cacheRefreshers = cacheRefreshers; + _serverMessenger = serverMessenger; + } + + // dummy editor for deserialization + private class ValueListConfigurationEditor : ConfigurationEditor + { } + + public override void Migrate() + { + //need to convert the old drop down data types to use the new one + var dataTypes = Database.Fetch(Sql() + .Select() + .From() + .Where(x => x.EditorAlias.Contains(".DropDown"))); + + foreach (var dataType in dataTypes) + { + ValueListConfiguration config; + + if (!dataType.Configuration.IsNullOrWhiteSpace()) + { + // parse configuration, and update everything accordingly + try + { + config = (ValueListConfiguration) new ValueListConfigurationEditor().FromDatabase(dataType.Configuration); + } + catch (Exception ex) + { + Logger.Error( + ex, "Invalid drop down configuration detected: \"{Configuration}\", cannot convert editor, values will be cleared", + dataType.Configuration); + + // reset + config = new ValueListConfiguration(); + } + + // get property data dtos + var propertyDataDtos = Database.Fetch(Sql() + .Select() + .From() + .InnerJoin().On((pt, pd) => pt.Id == pd.PropertyTypeId) + .InnerJoin().On((dt, pt) => dt.NodeId == pt.DataTypeId) + .Where(x => x.DataTypeId == dataType.NodeId)); + + // update dtos + var updatedDtos = propertyDataDtos.Where(x => UpdatePropertyDataDto(x, config)); + + // persist changes + foreach (var propertyDataDto in updatedDtos) + Database.Update(propertyDataDto); + } + else + { + // default configuration + config = new ValueListConfiguration(); + } + + var requiresCacheRebuild = false; + switch (dataType.EditorAlias) + { + case string ea when ea.InvariantEquals("Umbraco.DropDown"): + UpdateDataType(dataType, config, false); + break; + case string ea when ea.InvariantEquals("Umbraco.DropdownlistPublishingKeys"): + UpdateDataType(dataType, config, false); + requiresCacheRebuild = true; + break; + case string ea when ea.InvariantEquals("Umbraco.DropDownMultiple"): + UpdateDataType(dataType, config, true); + break; + case string ea when ea.InvariantEquals("Umbraco.DropdownlistMultiplePublishKeys"): + UpdateDataType(dataType, config, true); + requiresCacheRebuild = true; + break; + } + + if (requiresCacheRebuild) + { + var dataTypeCacheRefresher = _cacheRefreshers[Guid.Parse("35B16C25-A17E-45D7-BC8F-EDAB1DCC28D2")]; + _serverMessenger.PerformRefreshAll(dataTypeCacheRefresher); + } + } + } + + private void UpdateDataType(DataTypeDto dataType, ValueListConfiguration config, bool isMultiple) + { + dataType.EditorAlias = Constants.PropertyEditors.Aliases.DropDownListFlexible; + dataType.DbType = ValueStorageType.Nvarchar.ToString(); + + var flexConfig = new DropDownFlexibleConfiguration + { + Items = config.Items, + Multiple = isMultiple + }; + dataType.Configuration = ConfigurationEditor.ToDatabase(flexConfig); + + Database.Update(dataType); + } + + private bool UpdatePropertyDataDto(PropertyDataDto propData, ValueListConfiguration config) + { + //Get the INT ids stored for this property/drop down + int[] ids = null; + if (!propData.VarcharValue.IsNullOrWhiteSpace()) + { + ids = ConvertStringValues(propData.VarcharValue); + } + else if (!propData.TextValue.IsNullOrWhiteSpace()) + { + ids = ConvertStringValues(propData.TextValue); + } + else if (propData.IntegerValue.HasValue) + { + ids = new[] { propData.IntegerValue.Value }; + } + + //if there are INT ids, convert them to values based on the configured pre-values + if (ids != null && ids.Length > 0) + { + //map the ids to values + var vals = new List(); + var canConvert = true; + foreach (var id in ids) + { + var val = config.Items.FirstOrDefault(x => x.Id == id); + if (val != null) + vals.Add(val.Value); + else + { + Logger.Warn( + "Could not find associated data type configuration for stored Id {DataTypeId}", id); + canConvert = false; + } + } + if (canConvert) + { + propData.VarcharValue = string.Join(",", vals); + propData.TextValue = null; + propData.IntegerValue = null; + return true; + } + } + + return false; + } + + private int[] ConvertStringValues(string val) + { + var splitVals = val.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + + var intVals = splitVals + .Select(x => int.TryParse(x, out var i) ? i : int.MinValue) + .Where(x => x != int.MinValue) + .ToArray(); + + //only return if the number of values are the same (i.e. All INTs) + if (splitVals.Length == intVals.Length) + return intVals; + + return null; + } + } +} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DropPreValueTable.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DropPreValueTable.cs new file mode 100644 index 0000000000..fa6e47fac7 --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DropPreValueTable.cs @@ -0,0 +1,16 @@ +namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 +{ + public class DropPreValueTable : MigrationBase + { + public DropPreValueTable(IMigrationContext context) : base(context) + { + } + + public override void Migrate() + { + // drop preValues table + if (TableExists("cmsDataTypePreValues")) + Delete.Table("cmsDataTypePreValues").Do(); + } + } +} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DropTaskTables.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DropTaskTables.cs index f706671022..e8fd4f409e 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DropTaskTables.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DropTaskTables.cs @@ -1,5 +1,6 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 { + public class DropTaskTables : MigrationBase { public DropTaskTables(IMigrationContext context) @@ -8,8 +9,10 @@ public override void Migrate() { - Delete.Table("cmsTaskType"); - Delete.Table("cmsTask"); + if (TableExists("cmsTaskType")) + Delete.Table("cmsTaskType"); + if (TableExists("cmsTask")) + Delete.Table("cmsTask"); } } } diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DropTemplateDesignColumn.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DropTemplateDesignColumn.cs new file mode 100644 index 0000000000..f1b25403d4 --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DropTemplateDesignColumn.cs @@ -0,0 +1,15 @@ +namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 +{ + public class DropTemplateDesignColumn : MigrationBase + { + public DropTemplateDesignColumn(IMigrationContext context) + : base(context) + { } + + public override void Migrate() + { + if(ColumnExists("cmsTemplate", "design")) + Delete.Column("design").FromTable("cmsTemplate").Do(); + } + } +} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/FixLockTablePrimaryKey.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/FixLockTablePrimaryKey.cs new file mode 100644 index 0000000000..fbb233927b --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/FixLockTablePrimaryKey.cs @@ -0,0 +1,23 @@ +using System.Linq; + +namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 +{ + public class FixLockTablePrimaryKey : MigrationBase + { + public FixLockTablePrimaryKey(IMigrationContext context) + : base(context) + { } + + public override void Migrate() + { + // at some point, the KeyValueService dropped the PK and failed to re-create it, + // so the PK is gone - make sure we have one, and create if needed + + var constraints = SqlSyntax.GetConstraintsPerTable(Database); + var exists = constraints.Any(x => x.Item2 == "PK_umbracoLock"); + + if (!exists) + Create.PrimaryKey("PK_umbracoLock").OnTable(Constants.DatabaseSchema.Tables.Lock).Column("id").Do(); + } + } +} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/TablesForScheduledPublishing.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/TablesForScheduledPublishing.cs new file mode 100644 index 0000000000..cd4de179bd --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/TablesForScheduledPublishing.cs @@ -0,0 +1,46 @@ +using NPoco; +using System; +using Umbraco.Core.Models; +using Umbraco.Core.Persistence.Dtos; + +namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 +{ + public class TablesForScheduledPublishing : MigrationBase + { + public TablesForScheduledPublishing(IMigrationContext context) + : base(context) + { } + + public override void Migrate() + { + //Get anything currently scheduled + var scheduleSql = new Sql() + .Select("nodeId", "releaseDate", "expireDate") + .From("umbracoDocument") + .Where("releaseDate IS NOT NULL OR expireDate IS NOT NULL"); + var schedules = Database.Dictionary (scheduleSql); + + //drop old cols + Delete.Column("releaseDate").FromTable("umbracoDocument").Do(); + Delete.Column("expireDate").FromTable("umbracoDocument").Do(); + //add new table + Create.Table().Do(); + + //migrate the schedule + foreach(var s in schedules) + { + var date = s.Value.releaseDate; + var action = ContentScheduleAction.Release.ToString(); + if (!date.HasValue) + { + date = s.Value.expireDate; + action = ContentScheduleAction.Expire.ToString(); + } + + Insert.IntoTable(ContentScheduleDto.TableName) + .Row(new { nodeId = s.Key, date = date.Value, action = action }) + .Do(); + } + } + } +} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/TagsMigration.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/TagsMigration.cs index 89cb7e74ac..5dc5e0b6fe 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/TagsMigration.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/TagsMigration.cs @@ -18,7 +18,9 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 // kill unused parentId column Delete.ForeignKey("FK_cmsTags_cmsTags").OnTable(Constants.DatabaseSchema.Tables.Tag).Do(); - Delete.Column("ParentId").FromTable(Constants.DatabaseSchema.Tables.Tag); + Delete.Column("ParentId").FromTable(Constants.DatabaseSchema.Tables.Tag).Do(); } } + + // fixes TagsMigration that... originally failed to properly drop the ParentId column } diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/TagsMigrationFix.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/TagsMigrationFix.cs new file mode 100644 index 0000000000..4ee95c9f58 --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/TagsMigrationFix.cs @@ -0,0 +1,16 @@ +namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 +{ + public class TagsMigrationFix : MigrationBase + { + public TagsMigrationFix(IMigrationContext context) + : base(context) + { } + + public override void Migrate() + { + // kill unused parentId column, if it still exists + if (ColumnExists(Constants.DatabaseSchema.Tables.Tag, "ParentId")) + Delete.Column("ParentId").FromTable(Constants.DatabaseSchema.Tables.Tag).Do(); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/ApplicationTree.cs b/src/Umbraco.Core/Models/ApplicationTree.cs index 8b0bbc29c4..ccdebea724 100644 --- a/src/Umbraco.Core/Models/ApplicationTree.cs +++ b/src/Umbraco.Core/Models/ApplicationTree.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Concurrent; using System.Diagnostics; +using Umbraco.Core.Services; namespace Umbraco.Core.Models { @@ -35,6 +36,7 @@ namespace Umbraco.Core.Models IconClosed = iconClosed; IconOpened = iconOpened; Type = type; + } /// @@ -85,6 +87,33 @@ namespace Umbraco.Core.Models /// The type. public string Type { get; set; } + /// + /// Returns the localized root node display name + /// + /// + /// + public string GetRootNodeDisplayName(ILocalizedTextService textService) + { + var label = $"[{Alias}]"; + + // try to look up a the localized tree header matching the tree alias + var localizedLabel = textService.Localize("treeHeaders/" + Alias); + + // if the localizedLabel returns [alias] then return the title attribute from the trees.config file, if it's defined + if (localizedLabel != null && localizedLabel.Equals(label, StringComparison.InvariantCultureIgnoreCase)) + { + if (string.IsNullOrEmpty(Title) == false) + label = Title; + } + else + { + // the localizedLabel translated into something that's not just [alias], so use the translation + label = localizedLabel; + } + + return label; + } + private Type _runtimeType; /// diff --git a/src/Umbraco.Core/Models/AuditItem.cs b/src/Umbraco.Core/Models/AuditItem.cs index 6bfe32bd77..5fbde7f362 100644 --- a/src/Umbraco.Core/Models/AuditItem.cs +++ b/src/Umbraco.Core/Models/AuditItem.cs @@ -5,13 +5,9 @@ namespace Umbraco.Core.Models public sealed class AuditItem : EntityBase, IAuditItem { /// - /// Constructor for creating an item to be created + /// Initializes a new instance of the class. /// - /// - /// - /// - /// - public AuditItem(int objectId, string comment, AuditType type, int userId) + public AuditItem(int objectId, AuditType type, int userId, string entityType, string comment = null, string parameters = null) { DisableChangeTracking(); @@ -19,12 +15,25 @@ namespace Umbraco.Core.Models Comment = comment; AuditType = type; UserId = userId; + EntityType = entityType; + Parameters = parameters; EnableChangeTracking(); } - public string Comment { get; } + /// public AuditType AuditType { get; } - public int UserId { get; } + + /// + public string EntityType { get; } + + /// + public int UserId { get; } + + /// + public string Comment { get; } + + /// + public string Parameters { get; } } } diff --git a/src/Umbraco.Core/Models/AuditType.cs b/src/Umbraco.Core/Models/AuditType.cs index a5ae34a89d..8a57948805 100644 --- a/src/Umbraco.Core/Models/AuditType.cs +++ b/src/Umbraco.Core/Models/AuditType.cs @@ -1,84 +1,117 @@ namespace Umbraco.Core.Models { /// - /// Enums for vailable types of auditing + /// Defines audit types. /// public enum AuditType { /// - /// Used when new nodes are added + /// New node(s) being added. /// New, + /// - /// Used when nodes are saved + /// Node(s) being saved. /// Save, + /// - /// Used when nodes are opened + /// Variant(s) being saved. + /// + SaveVariant, + + /// + /// Node(s) being opened. /// Open, + /// - /// Used when nodes are deleted + /// Node(s) being deleted. /// Delete, + /// - /// Used when nodes are published + /// Node(s) being published. /// Publish, + /// - /// Used when nodes are send to publishing + /// Variant(s) being published. + /// + PublishVariant, + + /// + /// Node(s) being sent to publishing. /// SendToPublish, + /// - /// Used when nodes are unpublished + /// Variant(s) being sent to publishing. + /// + SendToPublishVariant, + + /// + /// Node(s) being unpublished. /// Unpublish, + /// - /// Used when nodes are moved + /// Variant(s) being unpublished. + /// + UnpublishVariant, + + /// + /// Node(s) being moved. /// Move, + /// - /// Used when nodes are copied + /// Node(s) being copied. /// Copy, + /// - /// Used when nodes are assígned a domain + /// Node(s) being assigned domains. /// AssignDomain, + /// - /// Used when public access are changed for a node + /// Node(s) public access changing. /// PublicAccess, + /// - /// Used when nodes are sorted + /// Node(s) being sorted. /// Sort, + /// - /// Used when a notification are send to a user + /// Notification(s) being sent to user. /// Notify, + /// - /// General system notification + /// General system audit message. /// System, + /// - /// Used when a node's content is rolled back to a previous version + /// Node's content being rolled back to a previous version. /// RollBack, + /// - /// Used when a package is installed + /// Package being installed. /// PackagerInstall, + /// - /// Used when a package is uninstalled + /// Package being uninstalled. /// PackagerUninstall, + /// - /// Used when a node is send to translation - /// - SendToTranslate, - /// - /// Use this log action for custom log messages that should be shown in the audit trail + /// Custom audit message. /// Custom } diff --git a/src/Umbraco.Core/Models/Content.cs b/src/Umbraco.Core/Models/Content.cs index 238d87b186..3f6e387dec 100644 --- a/src/Umbraco.Core/Models/Content.cs +++ b/src/Umbraco.Core/Models/Content.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.Specialized; using System.Linq; using System.Reflection; using System.Runtime.Serialization; @@ -16,12 +17,11 @@ namespace Umbraco.Core.Models { private IContentType _contentType; private ITemplate _template; + private ContentScheduleCollection _schedule; private bool _published; private PublishedState _publishedState; - private DateTime? _releaseDate; - private DateTime? _expireDate; - private Dictionary _publishInfos; - private Dictionary _publishInfosOrig; + private ContentCultureInfosCollection _publishInfos; + private ContentCultureInfosCollection _publishInfosOrig; private HashSet _editedCultures; private static readonly Lazy Ps = new Lazy(); @@ -85,8 +85,41 @@ namespace Umbraco.Core.Models { public readonly PropertyInfo TemplateSelector = ExpressionHelper.GetPropertyInfo(x => x.Template); public readonly PropertyInfo PublishedSelector = ExpressionHelper.GetPropertyInfo(x => x.Published); - public readonly PropertyInfo ReleaseDateSelector = ExpressionHelper.GetPropertyInfo(x => x.ReleaseDate); - public readonly PropertyInfo ExpireDateSelector = ExpressionHelper.GetPropertyInfo(x => x.ExpireDate); + public readonly PropertyInfo ContentScheduleSelector = ExpressionHelper.GetPropertyInfo(x => x.ContentSchedule); + public readonly PropertyInfo PublishCultureInfosSelector = ExpressionHelper.GetPropertyInfo>(x => x.PublishCultureInfos); + } + + /// + [DoNotClone] + public ContentScheduleCollection ContentSchedule + { + get + { + if (_schedule == null) + { + _schedule = new ContentScheduleCollection(); + _schedule.CollectionChanged += ScheduleCollectionChanged; + } + return _schedule; + } + set + { + if(_schedule != null) + _schedule.CollectionChanged -= ScheduleCollectionChanged; + SetPropertyValueAndDetectChanges(value, ref _schedule, Ps.Value.ContentScheduleSelector); + if (_schedule != null) + _schedule.CollectionChanged += ScheduleCollectionChanged; + } + } + + /// + /// Collection changed event handler to ensure the schedule field is set to dirty when the schedule changes + /// + /// + /// + private void ScheduleCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + OnPropertyChanged(Ps.Value.ContentScheduleSelector); } /// @@ -98,35 +131,12 @@ namespace Umbraco.Core.Models /// the Default template from the ContentType will be returned. /// [DataMember] - public virtual ITemplate Template + public ITemplate Template { get => _template ?? _contentType.DefaultTemplate; set => SetPropertyValueAndDetectChanges(value, ref _template, Ps.Value.TemplateSelector); } - /// - /// Gets the current status of the Content - /// - [IgnoreDataMember] - public ContentStatus Status - { - get - { - if(Trashed) - return ContentStatus.Trashed; - - if(ExpireDate.HasValue && ExpireDate.Value > DateTime.MinValue && DateTime.Now > ExpireDate.Value) - return ContentStatus.Expired; - - if(ReleaseDate.HasValue && ReleaseDate.Value > DateTime.MinValue && ReleaseDate.Value > DateTime.Now) - return ContentStatus.AwaitingRelease; - - if(Published) - return ContentStatus.Published; - - return ContentStatus.Unpublished; - } - } /// /// Gets or sets a value indicating whether this content item is published or not. @@ -167,26 +177,6 @@ namespace Umbraco.Core.Models [IgnoreDataMember] public bool Edited { get; internal set; } - /// - /// The date this Content should be released and thus be published - /// - [DataMember] - public DateTime? ReleaseDate - { - get => _releaseDate; - set => SetPropertyValueAndDetectChanges(value, ref _releaseDate, Ps.Value.ReleaseDateSelector); - } - - /// - /// The date this Content should expire and thus be unpublished - /// - [DataMember] - public DateTime? ExpireDate - { - get => _expireDate; - set => SetPropertyValueAndDetectChanges(value, ref _expireDate, Ps.Value.ExpireDateSelector); - } - /// /// Gets the ContentType used by this content object /// @@ -211,7 +201,7 @@ namespace Umbraco.Core.Models /// [IgnoreDataMember] - public IEnumerable EditedCultures => CultureNames.Keys.Where(IsCultureEdited); + public IEnumerable EditedCultures => CultureInfos.Keys.Where(IsCultureEdited); /// [IgnoreDataMember] @@ -221,13 +211,33 @@ namespace Umbraco.Core.Models public bool IsCulturePublished(string culture) // just check _publishInfos // a non-available culture could not become published anyways - => _publishInfos != null && _publishInfos.ContainsKey(culture); + => _publishInfos != null && _publishInfos.ContainsKey(culture); /// public bool WasCulturePublished(string culture) // just check _publishInfosOrig - a copy of _publishInfos // a non-available culture could not become published anyways - => _publishInfosOrig != null && _publishInfosOrig.ContainsKey(culture); + => _publishInfosOrig != null && _publishInfosOrig.ContainsKey(culture); + + // adjust dates to sync between version, cultures etc + // used by the repo when persisting + internal void AdjustDates(DateTime date) + { + foreach (var culture in PublishedCultures.ToList()) + { + if (_publishInfos == null || !_publishInfos.TryGetValue(culture, out var publishInfos)) + continue; + + if (_publishInfosOrig != null && _publishInfosOrig.TryGetValue(culture, out var publishInfosOrig) + && publishInfosOrig.Date == publishInfos.Date) + continue; + + _publishInfos.AddOrUpdate(culture, publishInfos.Name, date); + + if (CultureInfos.TryGetValue(culture, out var infos)) + SetCultureInfo(culture, infos.Name, date); + } + } /// public bool IsCultureEdited(string culture) @@ -237,7 +247,7 @@ namespace Umbraco.Core.Models /// [IgnoreDataMember] - public IReadOnlyDictionary PublishNames => _publishInfos?.ToDictionary(x => x.Key, x => x.Value.Name, StringComparer.OrdinalIgnoreCase) ?? NoNames; + public IReadOnlyDictionary PublishCultureInfos => _publishInfos ?? NoInfos; /// public string GetPublishName(string culture) @@ -267,9 +277,12 @@ namespace Umbraco.Core.Models throw new ArgumentNullOrEmptyException(nameof(culture)); if (_publishInfos == null) - _publishInfos = new Dictionary(StringComparer.OrdinalIgnoreCase); + { + _publishInfos = new ContentCultureInfosCollection(); + _publishInfos.CollectionChanged += PublishNamesCollectionChanged; + } - _publishInfos[culture.ToLowerInvariant()] = (name, date); + _publishInfos.AddOrUpdate(culture, name, date); } private void ClearPublishInfos() @@ -285,6 +298,9 @@ namespace Umbraco.Core.Models if (_publishInfos == null) return; _publishInfos.Remove(culture); if (_publishInfos.Count == 0) _publishInfos = null; + + // set the culture to be dirty - it's been modified + TouchCultureInfo(culture); } // sets a publish edited @@ -311,6 +327,14 @@ namespace Umbraco.Core.Models } } + /// + /// Handles culture infos collection changes. + /// + private void PublishNamesCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + OnPropertyChanged(Ps.Value.PublishCultureInfosSelector); + } + [IgnoreDataMember] public int PublishedVersionId { get; internal set; } @@ -318,7 +342,7 @@ namespace Umbraco.Core.Models public bool Blueprint { get; internal set; } /// - public virtual bool PublishCulture(string culture = "*") + public bool PublishCulture(string culture = "*") { culture = culture.NullOrWhiteSpaceAsNull(); @@ -343,6 +367,12 @@ namespace Umbraco.Core.Models SetPublishInfo(c, name, DateTime.Now); } } + else if (culture == null) // invariant culture + { + if (string.IsNullOrWhiteSpace(Name)) + return false; + // PublishName set by repository - nothing to do here + } else // one single culture { var name = GetCultureName(culture); @@ -365,7 +395,7 @@ namespace Umbraco.Core.Models } /// - public virtual void UnpublishCulture(string culture = "*") + public void UnpublishCulture(string culture = "*") { culture = culture.NullOrWhiteSpaceAsNull(); @@ -396,6 +426,8 @@ namespace Umbraco.Core.Models _contentType = contentType; ContentTypeBase = contentType; Properties.EnsurePropertyTypes(PropertyTypes); + + Properties.CollectionChanged -= PropertiesChanged; // be sure not to double add Properties.CollectionChanged += PropertiesChanged; } @@ -413,6 +445,8 @@ namespace Umbraco.Core.Models _contentType = contentType; ContentTypeBase = contentType; Properties.EnsureCleanPropertyTypes(PropertyTypes); + + Properties.CollectionChanged -= PropertiesChanged; // be sure not to double add Properties.CollectionChanged += PropertiesChanged; return; } @@ -424,13 +458,24 @@ namespace Umbraco.Core.Models { base.ResetDirtyProperties(rememberDirty); + if (Template != null) + Template.ResetDirtyProperties(rememberDirty); + if (ContentType != null) + ContentType.ResetDirtyProperties(rememberDirty); + // take care of the published state _publishedState = _published ? PublishedState.Published : PublishedState.Unpublished; - // take care of publish infos + // Make a copy of the _publishInfos, this is purely so that we can detect + // if this entity's previous culture publish state (regardless of the rememberDirty flag) _publishInfosOrig = _publishInfos == null ? null - : new Dictionary(_publishInfos, StringComparer.OrdinalIgnoreCase); + : new ContentCultureInfosCollection(_publishInfos); + + if (_publishInfos == null) return; + + foreach (var infos in _publishInfos) + infos.ResetDirtyProperties(rememberDirty); } /// @@ -450,19 +495,30 @@ namespace Umbraco.Core.Models return clone; } - public override object DeepClone() + protected override void PerformDeepClone(object clone) { - var clone = (Content) base.DeepClone(); - //turn off change tracking - clone.DisableChangeTracking(); - //need to manually clone this since it's not settable - clone._contentType = (IContentType)ContentType.DeepClone(); - //this shouldn't really be needed since we're not tracking - clone.ResetDirtyProperties(false); - //re-enable tracking - clone.EnableChangeTracking(); + base.PerformDeepClone(clone); - return clone; + var clonedContent = (Content)clone; + + //need to manually clone this since it's not settable + clonedContent._contentType = (IContentType) ContentType.DeepClone(); + + //if culture infos exist then deal with event bindings + if (clonedContent._publishInfos != null) + { + clonedContent._publishInfos.CollectionChanged -= PublishNamesCollectionChanged; //clear this event handler if any + clonedContent._publishInfos = (ContentCultureInfosCollection) _publishInfos.DeepClone(); //manually deep clone + clonedContent._publishInfos.CollectionChanged += clonedContent.PublishNamesCollectionChanged; //re-assign correct event handler + } + + //if properties exist then deal with event bindings + if (clonedContent._schedule != null) + { + clonedContent._schedule.CollectionChanged -= ScheduleCollectionChanged; //clear this event handler if any + clonedContent._schedule = (ContentScheduleCollection)_schedule.DeepClone(); //manually deep clone + clonedContent._schedule.CollectionChanged += clonedContent.ScheduleCollectionChanged; //re-assign correct event handler + } } } } diff --git a/src/Umbraco.Core/Models/ContentBase.cs b/src/Umbraco.Core/Models/ContentBase.cs index bf2fd580d9..b0c786d4b0 100644 --- a/src/Umbraco.Core/Models/ContentBase.cs +++ b/src/Umbraco.Core/Models/ContentBase.cs @@ -5,7 +5,6 @@ using System.Diagnostics; using System.Linq; using System.Reflection; using System.Runtime.Serialization; -using System.Web; using Umbraco.Core.Exceptions; using Umbraco.Core.Models.Entities; @@ -19,14 +18,14 @@ namespace Umbraco.Core.Models [DebuggerDisplay("Id: {Id}, Name: {Name}, ContentType: {ContentTypeBase.Alias}")] public abstract class ContentBase : TreeEntityBase, IContentBase { - protected static readonly Dictionary NoNames = new Dictionary(); + protected static readonly ContentCultureInfosCollection NoInfos = new ContentCultureInfosCollection(); private static readonly Lazy Ps = new Lazy(); private int _contentTypeId; protected IContentTypeComposition ContentTypeBase; private int _writerId; private PropertyCollection _properties; - private Dictionary _cultureInfos; + private ContentCultureInfosCollection _cultureInfos; /// /// Initializes a new instance of the class. @@ -69,7 +68,7 @@ namespace Umbraco.Core.Models public readonly PropertyInfo DefaultContentTypeIdSelector = ExpressionHelper.GetPropertyInfo(x => x.ContentTypeId); public readonly PropertyInfo PropertyCollectionSelector = ExpressionHelper.GetPropertyInfo(x => x.Properties); public readonly PropertyInfo WriterSelector = ExpressionHelper.GetPropertyInfo(x => x.WriterId); - public readonly PropertyInfo NamesSelector = ExpressionHelper.GetPropertyInfo>(x => x.CultureNames); + public readonly PropertyInfo CultureInfosSelector = ExpressionHelper.GetPropertyInfo>(x => x.CultureInfos); } protected void PropertiesChanged(object sender, NotifyCollectionChangedEventArgs e) @@ -112,7 +111,11 @@ namespace Umbraco.Core.Models /// /// Gets or sets the collection of properties for the entity. /// + /// + /// Marked DoNotClone since we'll manually clone the underlying field to deal with the event handling + /// [DataMember] + [DoNotClone] public virtual PropertyCollection Properties { get => _properties; @@ -147,7 +150,7 @@ namespace Umbraco.Core.Models /// public IEnumerable AvailableCultures - => _cultureInfos?.Select(x => x.Key) ?? Enumerable.Empty(); + => _cultureInfos?.Keys ?? Enumerable.Empty(); /// public bool IsCultureAvailable(string culture) @@ -155,10 +158,10 @@ namespace Umbraco.Core.Models /// [DataMember] - public virtual IReadOnlyDictionary CultureNames => _cultureInfos?.ToDictionary(x => x.Key, x => x.Value.Name, StringComparer.OrdinalIgnoreCase) ?? NoNames; + public virtual IReadOnlyDictionary CultureInfos => _cultureInfos ?? NoInfos; /// - public virtual string GetCultureName(string culture) + public string GetCultureName(string culture) { if (culture.IsNullOrWhiteSpace()) return Name; if (!ContentTypeBase.VariesByCulture()) return null; @@ -176,7 +179,7 @@ namespace Umbraco.Core.Models } /// - public virtual void SetCultureName(string name, string culture) + public void SetCultureName(string name, string culture) { if (ContentTypeBase.VariesByCulture()) // set on variant content type { @@ -202,16 +205,10 @@ namespace Umbraco.Core.Models } } - internal void TouchCulture(string culture) - { - if (ContentTypeBase.VariesByCulture() && _cultureInfos != null && _cultureInfos.TryGetValue(culture, out var infos)) - _cultureInfos[culture] = (infos.Name, DateTime.Now); - } - protected void ClearCultureInfos() { + _cultureInfos?.Clear(); _cultureInfos = null; - OnPropertyChanged(Ps.Value.NamesSelector); } protected void ClearCultureInfo(string culture) @@ -223,7 +220,12 @@ namespace Umbraco.Core.Models _cultureInfos.Remove(culture); if (_cultureInfos.Count == 0) _cultureInfos = null; - OnPropertyChanged(Ps.Value.NamesSelector); + } + + protected void TouchCultureInfo(string culture) + { + if (_cultureInfos == null || !_cultureInfos.TryGetValue(culture, out var infos)) return; + _cultureInfos.AddOrUpdate(culture, infos.Name, DateTime.Now); } // internal for repository @@ -236,10 +238,20 @@ namespace Umbraco.Core.Models throw new ArgumentNullOrEmptyException(nameof(culture)); if (_cultureInfos == null) - _cultureInfos = new Dictionary(StringComparer.OrdinalIgnoreCase); + { + _cultureInfos = new ContentCultureInfosCollection(); + _cultureInfos.CollectionChanged += CultureInfosCollectionChanged; + } - _cultureInfos[culture.ToLowerInvariant()] = (name, date); - OnPropertyChanged(Ps.Value.NamesSelector); + _cultureInfos.AddOrUpdate(culture, name, date); + } + + /// + /// Handles culture infos collection changes. + /// + private void CultureInfosCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + OnPropertyChanged(Ps.Value.CultureInfosSelector); } #endregion @@ -352,17 +364,17 @@ namespace Umbraco.Core.Models if (culture == null || culture == "*") Name = other.Name; - foreach (var (otherCulture, otherName) in other.CultureNames) + foreach (var (otherCulture, otherInfos) in other.CultureInfos) { if (culture == "*" || culture == otherCulture) - SetCultureName(otherName, otherCulture); + SetCultureName(otherInfos.Name, otherCulture); } } #endregion #region Validation - + /// public virtual Property[] ValidateProperties(string culture = "*") { @@ -387,6 +399,12 @@ namespace Umbraco.Core.Models // also reset dirty changes made to user's properties foreach (var prop in Properties) prop.ResetDirtyProperties(rememberDirty); + + // take care of culture infos + if (_cultureInfos == null) return; + + foreach (var cultureInfo in _cultureInfos) + cultureInfo.ResetDirtyProperties(rememberDirty); } /// @@ -458,5 +476,32 @@ namespace Umbraco.Core.Models } #endregion + + /// + /// + /// Overriden to deal with specific object instances + /// + protected override void PerformDeepClone(object clone) + { + base.PerformDeepClone(clone); + + var clonedContent = (ContentBase)clone; + + //if culture infos exist then deal with event bindings + if (clonedContent._cultureInfos != null) + { + clonedContent._cultureInfos.CollectionChanged -= CultureInfosCollectionChanged; //clear this event handler if any + clonedContent._cultureInfos = (ContentCultureInfosCollection) _cultureInfos.DeepClone(); //manually deep clone + clonedContent._cultureInfos.CollectionChanged += clonedContent.CultureInfosCollectionChanged; //re-assign correct event handler + } + + //if properties exist then deal with event bindings + if (clonedContent._properties != null) + { + clonedContent._properties.CollectionChanged -= PropertiesChanged; //clear this event handler if any + clonedContent._properties = (PropertyCollection) _properties.DeepClone(); //manually deep clone + clonedContent._properties.CollectionChanged += clonedContent.PropertiesChanged; //re-assign correct event handler + } + } } } diff --git a/src/Umbraco.Core/Models/ContentCultureInfos.cs b/src/Umbraco.Core/Models/ContentCultureInfos.cs new file mode 100644 index 0000000000..f51e3a275a --- /dev/null +++ b/src/Umbraco.Core/Models/ContentCultureInfos.cs @@ -0,0 +1,113 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using Umbraco.Core.Exceptions; +using Umbraco.Core.Models.Entities; + +namespace Umbraco.Core.Models +{ + /// + /// The name of a content variant for a given culture + /// + public class ContentCultureInfos : BeingDirtyBase, IDeepCloneable, IEquatable + { + private DateTime _date; + private string _name; + private static readonly Lazy Ps = new Lazy(); + + /// + /// Initializes a new instance of the class. + /// + public ContentCultureInfos(string culture) + { + if (culture.IsNullOrWhiteSpace()) throw new ArgumentNullOrEmptyException(nameof(culture)); + Culture = culture; + } + + /// + /// Initializes a new instance of the class. + /// + /// Used for cloning, without change tracking. + internal ContentCultureInfos(ContentCultureInfos other) + : this(other.Culture) + { + _name = other.Name; + _date = other.Date; + } + + /// + /// Gets the culture. + /// + public string Culture { get; } + + /// + /// Gets the name. + /// + public string Name + { + get => _name; + set => SetPropertyValueAndDetectChanges(value, ref _name, Ps.Value.NameSelector); + } + + /// + /// Gets the date. + /// + public DateTime Date + { + get => _date; + set => SetPropertyValueAndDetectChanges(value, ref _date, Ps.Value.DateSelector); + } + + /// + public object DeepClone() + { + return new ContentCultureInfos(this); + } + + /// + public override bool Equals(object obj) + { + return obj is ContentCultureInfos other && Equals(other); + } + + /// + public bool Equals(ContentCultureInfos other) + { + return other != null && Culture == other.Culture && Name == other.Name; + } + + /// + public override int GetHashCode() + { + var hashCode = 479558943; + hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(Culture); + hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(Name); + return hashCode; + } + + /// + /// Deconstructs into culture and name. + /// + public void Deconstruct(out string culture, out string name) + { + culture = Culture; + name = Name; + } + + /// + /// Deconstructs into culture, name and date. + /// + public void Deconstruct(out string culture, out string name, out DateTime date) + { + Deconstruct(out culture, out name); + date = Date; + } + + // ReSharper disable once ClassNeverInstantiated.Local + private class PropertySelectors + { + public readonly PropertyInfo NameSelector = ExpressionHelper.GetPropertyInfo(x => x.Name); + public readonly PropertyInfo DateSelector = ExpressionHelper.GetPropertyInfo(x => x.Date); + } + } +} diff --git a/src/Umbraco.Core/Models/ContentCultureInfosCollection.cs b/src/Umbraco.Core/Models/ContentCultureInfosCollection.cs new file mode 100644 index 0000000000..82b0ba6475 --- /dev/null +++ b/src/Umbraco.Core/Models/ContentCultureInfosCollection.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using Umbraco.Core.Collections; +using Umbraco.Core.Exceptions; + +namespace Umbraco.Core.Models +{ + /// + /// The culture names of a content's variants + /// + public class ContentCultureInfosCollection : ObservableDictionary, IDeepCloneable + { + /// + /// Initializes a new instance of the class. + /// + public ContentCultureInfosCollection() + : base(x => x.Culture, StringComparer.InvariantCultureIgnoreCase) + { } + + /// + /// Initializes a new instance of the class with items. + /// + public ContentCultureInfosCollection(IEnumerable items) + : base(x => x.Culture, StringComparer.InvariantCultureIgnoreCase) + { + // make sure to add *copies* and not the original items, + // as items can be modified by AddOrUpdate, and therefore + // the new collection would be impacted by changes made + // to the old collection + foreach (var item in items) + Add(new ContentCultureInfos(item)); + } + + /// + /// Adds or updates a instance. + /// + public void AddOrUpdate(string culture, string name, DateTime date) + { + if (culture.IsNullOrWhiteSpace()) throw new ArgumentNullOrEmptyException(nameof(culture)); + culture = culture.ToLowerInvariant(); + + if (TryGetValue(culture, out var item)) + { + item.Name = name; + item.Date = date; + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, item, item)); + } + else + { + Add(new ContentCultureInfos(culture) + { + Name = name, + Date = date + }); + } + } + + /// + public object DeepClone() + { + var clone = new ContentCultureInfosCollection(); + + foreach (var item in this) + { + var itemClone = (ContentCultureInfos) item.DeepClone(); + itemClone.ResetDirtyProperties(false); + clone.Add(itemClone); + } + + return clone; + } + } +} diff --git a/src/Umbraco.Core/Models/ContentEditing/IContentAppDefinition.cs b/src/Umbraco.Core/Models/ContentEditing/IContentAppDefinition.cs index 5e0c421742..2d30fc6ba9 100644 --- a/src/Umbraco.Core/Models/ContentEditing/IContentAppDefinition.cs +++ b/src/Umbraco.Core/Models/ContentEditing/IContentAppDefinition.cs @@ -1,4 +1,7 @@ -namespace Umbraco.Core.Models.ContentEditing +using System.Collections.Generic; +using Umbraco.Core.Models.Membership; + +namespace Umbraco.Core.Models.ContentEditing { /// /// Represents a content app definition. @@ -15,6 +18,6 @@ /// the content app should be displayed or not, and return either a /// instance, or null. /// - ContentApp GetContentAppFor(object source); + ContentApp GetContentAppFor(object source, IEnumerable userGroups); } } diff --git a/src/Umbraco.Core/Models/ContentSchedule.cs b/src/Umbraco.Core/Models/ContentSchedule.cs new file mode 100644 index 0000000000..cac4a0fd1c --- /dev/null +++ b/src/Umbraco.Core/Models/ContentSchedule.cs @@ -0,0 +1,77 @@ +using System; +using System.Runtime.Serialization; + +namespace Umbraco.Core.Models +{ + /// + /// Represents a scheduled action for a document. + /// + [Serializable] + [DataContract(IsReference = true)] + public class ContentSchedule : IDeepCloneable + { + /// + /// Initializes a new instance of the class. + /// + public ContentSchedule(string culture, DateTime date, ContentScheduleAction action) + { + Id = Guid.Empty; // will be assigned by document repository + Culture = culture; + Date = date; + Action = action; + } + + /// + /// Initializes a new instance of the class. + /// + public ContentSchedule(Guid id, string culture, DateTime date, ContentScheduleAction action) + { + Id = id; + Culture = culture; + Date = date; + Action = action; + } + + /// + /// Gets the unique identifier of the document targeted by the scheduled action. + /// + [DataMember] + public Guid Id { get; internal set; } + + /// + /// Gets the culture of the scheduled action. + /// + /// + /// string.Empty represents the invariant culture. + /// + [DataMember] + public string Culture { get; } + + /// + /// Gets the date of the scheduled action. + /// + [DataMember] + public DateTime Date { get; } + + /// + /// Gets the action to take. + /// + [DataMember] + public ContentScheduleAction Action { get; } + + public override bool Equals(object obj) + => obj is ContentSchedule other && Equals(other); + + public bool Equals(ContentSchedule other) + { + // don't compare Ids, two ContentSchedule are equal if they are for the same change + // for the same culture, on the same date - and the collection deals w/duplicates + return Culture.InvariantEquals(other.Culture) && Date == other.Date && Action == other.Action; + } + + public object DeepClone() + { + return new ContentSchedule(Id, Culture, Date, Action); + } + } +} diff --git a/src/Umbraco.Core/Models/ContentScheduleAction.cs b/src/Umbraco.Core/Models/ContentScheduleAction.cs new file mode 100644 index 0000000000..0816f17731 --- /dev/null +++ b/src/Umbraco.Core/Models/ContentScheduleAction.cs @@ -0,0 +1,18 @@ +namespace Umbraco.Core.Models +{ + /// + /// Defines scheduled actions for documents. + /// + public enum ContentScheduleAction + { + /// + /// Release the document. + /// + Release, + + /// + /// Expire the document. + /// + Expire + } +} diff --git a/src/Umbraco.Core/Models/ContentScheduleCollection.cs b/src/Umbraco.Core/Models/ContentScheduleCollection.cs new file mode 100644 index 0000000000..46813bdb45 --- /dev/null +++ b/src/Umbraco.Core/Models/ContentScheduleCollection.cs @@ -0,0 +1,222 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; + +namespace Umbraco.Core.Models +{ + public class ContentScheduleCollection : INotifyCollectionChanged, IDeepCloneable, IEquatable + { + //underlying storage for the collection backed by a sorted list so that the schedule is always in order of date and that duplicate dates per culture are not allowed + private readonly Dictionary> _schedule + = new Dictionary>(StringComparer.InvariantCultureIgnoreCase); + + public event NotifyCollectionChangedEventHandler CollectionChanged; + + private void OnCollectionChanged(NotifyCollectionChangedEventArgs args) + { + CollectionChanged?.Invoke(this, args); + } + + /// + /// Add an existing schedule + /// + /// + public void Add(ContentSchedule schedule) + { + if (!_schedule.TryGetValue(schedule.Culture, out var changes)) + { + changes = new SortedList(); + _schedule[schedule.Culture] = changes; + } + + //TODO: Below will throw if there are duplicate dates added, validate/return bool? + changes.Add(schedule.Date, schedule); + + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, schedule)); + } + + /// + /// Adds a new schedule for invariant content + /// + /// + /// + public bool Add(DateTime? releaseDate, DateTime? expireDate) + { + return Add(string.Empty, releaseDate, expireDate); + } + + /// + /// Adds a new schedule for a culture + /// + /// + /// + /// + /// true if successfully added, false if validation fails + public bool Add(string culture, DateTime? releaseDate, DateTime? expireDate) + { + if (culture == null) throw new ArgumentNullException(nameof(culture)); + if (releaseDate.HasValue && expireDate.HasValue && releaseDate >= expireDate) + return false; + + if (!releaseDate.HasValue && !expireDate.HasValue) return false; + + //TODO: Do we allow passing in a release or expiry date that is before now? + + if (!_schedule.TryGetValue(culture, out var changes)) + { + changes = new SortedList(); + _schedule[culture] = changes; + } + + //TODO: Below will throw if there are duplicate dates added, should validate/return bool? + // but the bool won't indicate which date was in error, maybe have 2 diff methods to schedule start/end? + + if (releaseDate.HasValue) + { + var entry = new ContentSchedule(culture, releaseDate.Value, ContentScheduleAction.Release); + changes.Add(releaseDate.Value, entry); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, entry)); + } + + if (expireDate.HasValue) + { + var entry = new ContentSchedule(culture, expireDate.Value, ContentScheduleAction.Expire); + changes.Add(expireDate.Value, entry); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, entry)); + } + + return true; + } + + /// + /// Remove a scheduled change + /// + /// + public void Remove(ContentSchedule change) + { + if (_schedule.TryGetValue(change.Culture, out var s)) + { + var removed = s.Remove(change.Date); + if (removed) + { + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, change)); + if (s.Count == 0) + _schedule.Remove(change.Culture); + } + + } + } + + /// + /// Clear all of the scheduled change type for invariant content + /// + /// + /// If specified, will clear all entries with dates less than or equal to the value + public void Clear(ContentScheduleAction action, DateTime? changeDate = null) + { + Clear(string.Empty, action, changeDate); + } + + /// + /// Clear all of the scheduled change type for the culture + /// + /// + /// + /// If specified, will clear all entries with dates less than or equal to the value + public void Clear(string culture, ContentScheduleAction action, DateTime? date = null) + { + if (!_schedule.TryGetValue(culture, out var schedules)) + return; + + var removes = schedules.Where(x => x.Value.Action == action && (!date.HasValue || x.Value.Date <= date.Value)).ToList(); + + foreach (var remove in removes) + { + var removed = schedules.Remove(remove.Value.Date); + if (!removed) + continue; + + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, remove.Value)); + } + + if (schedules.Count == 0) + _schedule.Remove(culture); + } + + /// + /// Returns all pending schedules based on the date and type provided + /// + /// + /// + /// + public IReadOnlyList GetPending(ContentScheduleAction action, DateTime date) + { + return _schedule.Values.SelectMany(x => x.Values).Where(x => x.Date <= date).ToList(); + } + + /// + /// Gets the schedule for invariant content + /// + /// + public IEnumerable GetSchedule(ContentScheduleAction? action = null) + { + return GetSchedule(string.Empty, action); + } + + /// + /// Gets the schedule for a culture + /// + /// + /// + public IEnumerable GetSchedule(string culture, ContentScheduleAction? action = null) + { + if (_schedule.TryGetValue(culture, out var changes)) + return action == null ? changes.Values : changes.Values.Where(x => x.Action == action.Value); + return Enumerable.Empty(); + } + + /// + /// Returns all schedules registered + /// + /// + public IReadOnlyList FullSchedule => _schedule.SelectMany(x => x.Value.Values).ToList(); + + public object DeepClone() + { + var clone = new ContentScheduleCollection(); + foreach(var cultureSched in _schedule) + { + var list = new SortedList(); + foreach (var schedEntry in cultureSched.Value) + list.Add(schedEntry.Key, (ContentSchedule)schedEntry.Value.DeepClone()); + clone._schedule[cultureSched.Key] = list; + } + return clone; + } + + public override bool Equals(object obj) + => obj is ContentScheduleCollection other && Equals(other); + + public bool Equals(ContentScheduleCollection other) + { + if (other == null) return false; + + var thisSched = _schedule; + var thatSched = other._schedule; + + if (thisSched.Count != thatSched.Count) + return false; + + foreach (var (culture, thisList) in thisSched) + { + // if culture is missing, or actions differ, false + if (!thatSched.TryGetValue(culture, out var thatList) || !thatList.SequenceEqual(thisList)) + return false; + } + + return true; + } + } +} diff --git a/src/Umbraco.Core/Models/ContentStatus.cs b/src/Umbraco.Core/Models/ContentStatus.cs index 4caf214399..1d35844874 100644 --- a/src/Umbraco.Core/Models/ContentStatus.cs +++ b/src/Umbraco.Core/Models/ContentStatus.cs @@ -4,20 +4,42 @@ using System.Runtime.Serialization; namespace Umbraco.Core.Models { /// - /// Enum for the various statuses a Content object can have + /// Describes the states of a document, with regard to (schedule) publishing. /// [Serializable] [DataContract] public enum ContentStatus { + // typical flow: + // Unpublished (add release date)-> AwaitingRelease (release)-> Published (expire)-> Expired + + /// + /// The document is not trashed, and not published. + /// [EnumMember] Unpublished, + + /// + /// The document is published. + /// [EnumMember] Published, + + /// + /// The document is not trashed, not published, after being unpublished by a scheduled action. + /// [EnumMember] Expired, + + /// + /// The document is trashed. + /// [EnumMember] Trashed, + + /// + /// The document is not trashed, not published, and pending publication by a scheduled action. + /// [EnumMember] AwaitingRelease } diff --git a/src/Umbraco.Core/Models/ContentTypeBase.cs b/src/Umbraco.Core/Models/ContentTypeBase.cs index 9e73205c36..caa63d7526 100644 --- a/src/Umbraco.Core/Models/ContentTypeBase.cs +++ b/src/Umbraco.Core/Models/ContentTypeBase.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Collections.Specialized; -using System.ComponentModel; using System.Diagnostics; using System.Linq; using System.Reflection; @@ -28,7 +27,7 @@ namespace Umbraco.Core.Models private bool _allowedAsRoot; // note: only one that's not 'pure element type' private bool _isContainer; private PropertyGroupCollection _propertyGroups; - private PropertyTypeCollection _propertyTypes; + private PropertyTypeCollection _noGroupPropertyTypes; private IEnumerable _allowedContentTypes; private bool _hasPropertyTypeBeenRemoved; private ContentVariation _variations; @@ -43,8 +42,8 @@ namespace Umbraco.Core.Models // actually OK as IsPublishing is constant // ReSharper disable once VirtualMemberCallInConstructor - _propertyTypes = new PropertyTypeCollection(IsPublishing); - _propertyTypes.CollectionChanged += PropertyTypesChanged; + _noGroupPropertyTypes = new PropertyTypeCollection(IsPublishing); + _noGroupPropertyTypes.CollectionChanged += PropertyTypesChanged; _variations = ContentVariation.Nothing; } @@ -64,8 +63,8 @@ namespace Umbraco.Core.Models // actually OK as IsPublishing is constant // ReSharper disable once VirtualMemberCallInConstructor - _propertyTypes = new PropertyTypeCollection(IsPublishing); - _propertyTypes.CollectionChanged += PropertyTypesChanged; + _noGroupPropertyTypes = new PropertyTypeCollection(IsPublishing); + _noGroupPropertyTypes.CollectionChanged += PropertyTypesChanged; _variations = ContentVariation.Nothing; } @@ -132,7 +131,7 @@ namespace Umbraco.Core.Models /// Description for the ContentType /// [DataMember] - public virtual string Description + public string Description { get => _description; set => SetPropertyValueAndDetectChanges(value, ref _description, Ps.Value.DescriptionSelector); @@ -142,7 +141,7 @@ namespace Umbraco.Core.Models /// Name of the icon (sprite class) used to identify the ContentType /// [DataMember] - public virtual string Icon + public string Icon { get => _icon; set => SetPropertyValueAndDetectChanges(value, ref _icon, Ps.Value.IconSelector); @@ -152,7 +151,7 @@ namespace Umbraco.Core.Models /// Name of the thumbnail used to identify the ContentType /// [DataMember] - public virtual string Thumbnail + public string Thumbnail { get => _thumbnail; set => SetPropertyValueAndDetectChanges(value, ref _thumbnail, Ps.Value.ThumbnailSelector); @@ -162,7 +161,7 @@ namespace Umbraco.Core.Models /// Gets or Sets a boolean indicating whether this ContentType is allowed at the root /// [DataMember] - public virtual bool AllowedAsRoot + public bool AllowedAsRoot { get => _allowedAsRoot; set => SetPropertyValueAndDetectChanges(value, ref _allowedAsRoot, Ps.Value.AllowedAsRootSelector); @@ -175,7 +174,7 @@ namespace Umbraco.Core.Models /// ContentType Containers doesn't show children in the tree, but rather in grid-type view. /// [DataMember] - public virtual bool IsContainer + public bool IsContainer { get => _isContainer; set => SetPropertyValueAndDetectChanges(value, ref _isContainer, Ps.Value.IsContainerSelector); @@ -185,7 +184,7 @@ namespace Umbraco.Core.Models /// Gets or sets a list of integer Ids for allowed ContentTypes /// [DataMember] - public virtual IEnumerable AllowedContentTypes + public IEnumerable AllowedContentTypes { get => _allowedContentTypes; set => SetPropertyValueAndDetectChanges(value, ref _allowedContentTypes, Ps.Value.AllowedContentTypesSelector, @@ -223,10 +222,12 @@ namespace Umbraco.Core.Models /// List of PropertyGroups available on this ContentType /// /// - /// A PropertyGroup corresponds to a Tab in the UI + /// A PropertyGroup corresponds to a Tab in the UI + /// Marked DoNotClone because we will manually deal with cloning and the event handlers /// [DataMember] - public virtual PropertyGroupCollection PropertyGroups + [DoNotClone] + public PropertyGroupCollection PropertyGroups { get => _propertyGroups; set @@ -242,25 +243,29 @@ namespace Umbraco.Core.Models /// [IgnoreDataMember] [DoNotClone] - public virtual IEnumerable PropertyTypes + public IEnumerable PropertyTypes { get { - return _propertyTypes.Union(PropertyGroups.SelectMany(x => x.PropertyTypes)); + return _noGroupPropertyTypes.Union(PropertyGroups.SelectMany(x => x.PropertyTypes)); } } /// /// Gets or sets the property types that are not in a group. /// + /// + /// Marked DoNotClone because we will manually deal with cloning and the event handlers + /// + [DoNotClone] public IEnumerable NoGroupPropertyTypes { - get => _propertyTypes; + get => _noGroupPropertyTypes; set { - _propertyTypes = new PropertyTypeCollection(IsPublishing, value); - _propertyTypes.CollectionChanged += PropertyTypesChanged; - PropertyTypesChanged(_propertyTypes, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + _noGroupPropertyTypes = new PropertyTypeCollection(IsPublishing, value); + _noGroupPropertyTypes.CollectionChanged += PropertyTypesChanged; + PropertyTypesChanged(_noGroupPropertyTypes, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); } } @@ -314,7 +319,7 @@ namespace Umbraco.Core.Models { if (PropertyTypeExists(propertyType.Alias) == false) { - _propertyTypes.Add(propertyType); + _noGroupPropertyTypes.Add(propertyType); return true; } @@ -378,7 +383,7 @@ namespace Umbraco.Core.Models } //check through each local property type collection (not assigned to a tab) - if (_propertyTypes.RemoveItem(propertyTypeAlias)) + if (_noGroupPropertyTypes.RemoveItem(propertyTypeAlias)) { if (!HasPropertyTypeBeenRemoved) { @@ -402,7 +407,7 @@ namespace Umbraco.Core.Models foreach (var property in group.PropertyTypes) { property.PropertyGroupId = null; - _propertyTypes.Add(property); + _noGroupPropertyTypes.Add(property); } // actually remove the group @@ -415,7 +420,7 @@ namespace Umbraco.Core.Models /// [IgnoreDataMember] //fixme should we mark this as EditorBrowsable hidden since it really isn't ever used? - internal PropertyTypeCollection PropertyTypeCollection => _propertyTypes; + internal PropertyTypeCollection PropertyTypeCollection => _noGroupPropertyTypes; /// /// Indicates whether the current entity is dirty. @@ -462,23 +467,29 @@ namespace Umbraco.Core.Models } } - public override object DeepClone() + protected override void PerformDeepClone(object clone) { - var clone = (ContentTypeBase)base.DeepClone(); - //turn off change tracking - clone.DisableChangeTracking(); - //need to manually wire up the event handlers for the property type collections - we've ensured - // its ignored from the auto-clone process because its return values are unions, not raw and - // we end up with duplicates, see: http://issues.umbraco.org/issue/U4-4842 + base.PerformDeepClone(clone); - clone._propertyTypes = (PropertyTypeCollection)_propertyTypes.DeepClone(); - clone._propertyTypes.CollectionChanged += clone.PropertyTypesChanged; - //this shouldn't really be needed since we're not tracking - clone.ResetDirtyProperties(false); - //re-enable tracking - clone.EnableChangeTracking(); + var clonedEntity = (ContentTypeBase) clone; - return clone; + if (clonedEntity._noGroupPropertyTypes != null) + { + //need to manually wire up the event handlers for the property type collections - we've ensured + // its ignored from the auto-clone process because its return values are unions, not raw and + // we end up with duplicates, see: http://issues.umbraco.org/issue/U4-4842 + + clonedEntity._noGroupPropertyTypes.CollectionChanged -= PropertyTypesChanged; //clear this event handler if any + clonedEntity._noGroupPropertyTypes = (PropertyTypeCollection) _noGroupPropertyTypes.DeepClone(); //manually deep clone + clonedEntity._noGroupPropertyTypes.CollectionChanged += clonedEntity.PropertyTypesChanged; //re-assign correct event handler + } + + if (clonedEntity._propertyGroups != null) + { + clonedEntity._propertyGroups.CollectionChanged -= PropertyGroupsChanged; //clear this event handler if any + clonedEntity._propertyGroups = (PropertyGroupCollection) _propertyGroups.DeepClone(); //manually deep clone + clonedEntity._propertyGroups.CollectionChanged += clonedEntity.PropertyGroupsChanged; //re-assign correct event handler + } } public IContentType DeepCloneWithResetIdentities(string alias) diff --git a/src/Umbraco.Core/Models/ContentTypeBaseExtensions.cs b/src/Umbraco.Core/Models/ContentTypeBaseExtensions.cs index 0d2f817660..8af48bb881 100644 --- a/src/Umbraco.Core/Models/ContentTypeBaseExtensions.cs +++ b/src/Umbraco.Core/Models/ContentTypeBaseExtensions.cs @@ -63,20 +63,5 @@ namespace Umbraco.Core.Models aliases = a; return hasAnyPropertyVariationChanged; } - - /// - /// Returns the list of content types the composition is used in - /// - /// - /// - /// - internal static IEnumerable GetWhereCompositionIsUsedInContentTypes(this IContentTypeComposition source, - IContentTypeComposition[] allContentTypes) - { - var sourceId = source != null ? source.Id : 0; - - // find which content types are using this composition - return allContentTypes.Where(x => x.ContentTypeComposition.Any(y => y.Id == sourceId)).ToArray(); - } } } diff --git a/src/Umbraco.Core/Models/ContentTypeCompositionBase.cs b/src/Umbraco.Core/Models/ContentTypeCompositionBase.cs index 838a75b98b..08b9f74802 100644 --- a/src/Umbraco.Core/Models/ContentTypeCompositionBase.cs +++ b/src/Umbraco.Core/Models/ContentTypeCompositionBase.cs @@ -114,6 +114,27 @@ namespace Umbraco.Core.Models } } + /// + /// Gets the property types obtained via composition. + /// + /// + /// Gets them raw, ie with their original variation. + /// + [IgnoreDataMember] + internal IEnumerable RawComposedPropertyTypes => GetRawComposedPropertyTypes(); + + private IEnumerable GetRawComposedPropertyTypes(bool start = true) + { + var propertyTypes = ContentTypeComposition + .Cast() + .SelectMany(x => start ? x.GetRawComposedPropertyTypes(false) : x.CompositionPropertyTypes); + + if (!start) + propertyTypes = propertyTypes.Union(PropertyTypes); + + return propertyTypes; + } + /// /// Adds a content type to the composition. /// @@ -290,20 +311,15 @@ namespace Umbraco.Core.Models .Union(ContentTypeComposition.SelectMany(x => x.CompositionIds())); } - public override object DeepClone() + protected override void PerformDeepClone(object clone) { - var clone = (ContentTypeCompositionBase)base.DeepClone(); - //turn off change tracking - clone.DisableChangeTracking(); - //need to manually assign since this is an internal field and will not be automatically mapped - clone.RemovedContentTypeKeyTracker = new List(); - clone._contentTypeComposition = ContentTypeComposition.Select(x => (IContentTypeComposition)x.DeepClone()).ToList(); - //this shouldn't really be needed since we're not tracking - clone.ResetDirtyProperties(false); - //re-enable tracking - clone.EnableChangeTracking(); + base.PerformDeepClone(clone); - return clone; + var clonedEntity = (ContentTypeCompositionBase)clone; + + //need to manually assign since this is an internal field and will not be automatically mapped + clonedEntity.RemovedContentTypeKeyTracker = new List(); + clonedEntity._contentTypeComposition = ContentTypeComposition.Select(x => (IContentTypeComposition)x.DeepClone()).ToList(); } } } diff --git a/src/Umbraco.Core/Models/DictionaryTranslation.cs b/src/Umbraco.Core/Models/DictionaryTranslation.cs index 2105e8057c..c3b5a8a3b2 100644 --- a/src/Umbraco.Core/Models/DictionaryTranslation.cs +++ b/src/Umbraco.Core/Models/DictionaryTranslation.cs @@ -104,23 +104,14 @@ namespace Umbraco.Core.Models set { SetPropertyValueAndDetectChanges(value, ref _value, Ps.Value.ValueSelector); } } - public override object DeepClone() + protected override void PerformDeepClone(object clone) { - var clone = (DictionaryTranslation)base.DeepClone(); + base.PerformDeepClone(clone); + + var clonedEntity = (DictionaryTranslation)clone; // clear fields that were memberwise-cloned and that we don't want to clone - clone._language = null; - - // turn off change tracking - clone.DisableChangeTracking(); - - // this shouldn't really be needed since we're not tracking - clone.ResetDirtyProperties(false); - - // re-enable tracking - clone.EnableChangeTracking(); - - return clone; + clonedEntity._language = null; } } } diff --git a/src/Umbraco.Core/Models/Entities/EntityBase.cs b/src/Umbraco.Core/Models/Entities/EntityBase.cs index ab57d57ab6..5c6f943c60 100644 --- a/src/Umbraco.Core/Models/Entities/EntityBase.cs +++ b/src/Umbraco.Core/Models/Entities/EntityBase.cs @@ -13,6 +13,10 @@ namespace Umbraco.Core.Models.Entities [DebuggerDisplay("Id: {" + nameof(Id) + "}")] public abstract class EntityBase : BeingDirtyBase, IEntity { +#if DEBUG_MODEL + public Guid InstanceId = Guid.NewGuid(); +#endif + private static readonly Lazy Ps = new Lazy(); private bool _hasIdentity; @@ -155,26 +159,39 @@ namespace Umbraco.Core.Models.Entities } } - public virtual object DeepClone() + public object DeepClone() { // memberwise-clone (ie shallow clone) the entity var unused = Key; // ensure that 'this' has a key, before cloning var clone = (EntityBase) MemberwiseClone(); - // clear changes (ensures the clone has its own dictionaries) - // then disable change tracking - clone.ResetDirtyProperties(false); +#if DEBUG_MODEL + clone.InstanceId = Guid.NewGuid(); +#endif + + //disable change tracking while we deep clone IDeepCloneable properties clone.DisableChangeTracking(); // deep clone ref properties that are IDeepCloneable DeepCloneHelper.DeepCloneRefProperties(this, clone); - // clear changes again (just to be sure, because we were not tracking) - // then enable change tracking + PerformDeepClone(clone); + + // clear changes (ensures the clone has its own dictionaries) clone.ResetDirtyProperties(false); + + //re-enable change tracking clone.EnableChangeTracking(); return clone; } + + /// + /// Used by inheritors to modify the DeepCloning logic + /// + /// + protected virtual void PerformDeepClone(object clone) + { + } } } diff --git a/src/Umbraco.Core/Models/File.cs b/src/Umbraco.Core/Models/File.cs index 2e85b13261..2f8e021f4c 100644 --- a/src/Umbraco.Core/Models/File.cs +++ b/src/Umbraco.Core/Models/File.cs @@ -156,26 +156,17 @@ namespace Umbraco.Core.Models clone._alias = Alias; } - public override object DeepClone() + protected override void PerformDeepClone(object clone) { - var clone = (File) base.DeepClone(); + base.PerformDeepClone(clone); + + var clonedFile = (File)clone; // clear fields that were memberwise-cloned and that we don't want to clone - clone._content = null; - - // turn off change tracking - clone.DisableChangeTracking(); + clonedFile._content = null; // ... - DeepCloneNameAndAlias(clone); - - // this shouldn't really be needed since we're not tracking - clone.ResetDirtyProperties(false); - - // re-enable tracking - clone.EnableChangeTracking(); - - return clone; + DeepCloneNameAndAlias(clonedFile); } } } diff --git a/src/Umbraco.Core/Models/IAuditItem.cs b/src/Umbraco.Core/Models/IAuditItem.cs index 9416e2a055..ed70ada8ad 100644 --- a/src/Umbraco.Core/Models/IAuditItem.cs +++ b/src/Umbraco.Core/Models/IAuditItem.cs @@ -1,12 +1,35 @@ -using System; -using Umbraco.Core.Models.Entities; +using Umbraco.Core.Models.Entities; namespace Umbraco.Core.Models { + /// + /// Represents an audit item. + /// public interface IAuditItem : IEntity { - string Comment { get; } + /// + /// Gets the audit type. + /// AuditType AuditType { get; } + + /// + /// Gets the audited entity type. + /// + string EntityType { get; } + + /// + /// Gets the audit user identifier. + /// int UserId { get; } + + /// + /// Gets the audit comments. + /// + string Comment { get; } + + /// + /// Gets optional additional data parameters. + /// + string Parameters { get; } } } diff --git a/src/Umbraco.Core/Models/IContent.cs b/src/Umbraco.Core/Models/IContent.cs index d9bc32aaf0..a414a03d2f 100644 --- a/src/Umbraco.Core/Models/IContent.cs +++ b/src/Umbraco.Core/Models/IContent.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; namespace Umbraco.Core.Models { + /// /// Represents a document. /// @@ -11,6 +12,11 @@ namespace Umbraco.Core.Models /// public interface IContent : IContentBase { + /// + /// Gets or sets the content schedule + /// + ContentScheduleCollection ContentSchedule { get; set; } + /// /// Gets or sets the template used to render the content. /// @@ -60,26 +66,11 @@ namespace Umbraco.Core.Models /// DateTime? PublishDate { get; } - /// - /// Gets or sets the date and time the content item should be published. - /// - DateTime? ReleaseDate { get; set; } - - /// - /// Gets or sets the date and time the content should be unpublished. - /// - DateTime? ExpireDate { get; set; } - /// /// Gets the content type of this content. /// IContentType ContentType { get; } - /// - /// Gets the current status of the content. - /// - ContentStatus Status { get; } - /// /// Gets a value indicating whether a culture is published. /// @@ -89,6 +80,7 @@ namespace Umbraco.Core.Models /// whenever values for this culture are unpublished. /// A culture becomes published as soon as PublishCulture has been invoked, /// even though the document might now have been saved yet (and can have no identity). + /// Does not support the '*' wildcard (returns false). /// bool IsCulturePublished(string culture); @@ -112,6 +104,7 @@ namespace Umbraco.Core.Models /// A culture is edited when it is available, and not published or published but /// with changes. /// A culture can be edited even though the document might now have been saved yet (and can have no identity). + /// Does not support the '*' wildcard (returns false). /// bool IsCultureEdited(string culture); @@ -126,13 +119,13 @@ namespace Umbraco.Core.Models string GetPublishName(string culture); /// - /// Gets the published names of the content. + /// Gets the published culture infos of the content. /// /// /// Because a dictionary key cannot be null this cannot get the invariant /// name, which must be get via the property. /// - IReadOnlyDictionary PublishNames { get; } + IReadOnlyDictionary PublishCultureInfos { get; } /// /// Gets the published cultures. @@ -172,7 +165,7 @@ namespace Umbraco.Core.Models /// /// A value indicating whether the culture can be published. /// - /// Fails if properties don't pass variant validtion rules. + /// Fails if properties don't pass variant validation rules. /// Publishing must be finalized via the content service SavePublishing method. /// bool PublishCulture(string culture = "*"); diff --git a/src/Umbraco.Core/Models/IContentBase.cs b/src/Umbraco.Core/Models/IContentBase.cs index 460bd521d4..fb3714cfc0 100644 --- a/src/Umbraco.Core/Models/IContentBase.cs +++ b/src/Umbraco.Core/Models/IContentBase.cs @@ -51,13 +51,13 @@ namespace Umbraco.Core.Models string GetCultureName(string culture); /// - /// Gets the names of the content item. + /// Gets culture infos of the content item. /// /// /// Because a dictionary key cannot be null this cannot contain the invariant /// culture name, which must be get or set via the property. /// - IReadOnlyDictionary CultureNames { get; } + IReadOnlyDictionary CultureInfos { get; } /// /// Gets the available cultures. @@ -76,6 +76,7 @@ namespace Umbraco.Core.Models /// Returns false for the invariant culture, in order to be consistent /// with , even though the invariant culture is /// always available. + /// Does not support the '*' wildcard (returns false). /// bool IsCultureAvailable(string culture); diff --git a/src/Umbraco.Core/Models/IContentTypeBase.cs b/src/Umbraco.Core/Models/IContentTypeBase.cs index ef5988344e..a1d4aee02f 100644 --- a/src/Umbraco.Core/Models/IContentTypeBase.cs +++ b/src/Umbraco.Core/Models/IContentTypeBase.cs @@ -21,7 +21,12 @@ namespace Umbraco.Core.Models string Description { get; set; } /// - /// Gets or Sets the Icon for the ContentType + /// Gets or sets the icon for the content type. The value is a CSS class name representing + /// the icon (eg. icon-home) along with an optional CSS class name representing the + /// color (eg. icon-blue). Put together, the value for this scenario would be + /// icon-home color-blue. + /// + /// If a class name for the color isn't specified, the icon color will default to black. /// string Icon { get; set; } diff --git a/src/Umbraco.Core/Models/Language.cs b/src/Umbraco.Core/Models/Language.cs index 940648c4b9..e190c8ad3b 100644 --- a/src/Umbraco.Core/Models/Language.cs +++ b/src/Umbraco.Core/Models/Language.cs @@ -1,7 +1,10 @@ using System; using System.Globalization; +using System.Linq; using System.Reflection; using System.Runtime.Serialization; +using System.Threading; +using Umbraco.Core.Configuration; using Umbraco.Core.Models.Entities; namespace Umbraco.Core.Models @@ -48,7 +51,57 @@ namespace Umbraco.Core.Models [DataMember] public string CultureName { - get => _cultureName ?? CultureInfo.GetCultureInfo(IsoCode).DisplayName; + // CultureInfo.DisplayName is the name in the installed .NET language + // .NativeName is the name in culture info's language + // .EnglishName is the name in English + // + // there is no easy way to get the name in a specified culture (which would need to be installed on the server) + // this works: + // var rm = new ResourceManager("mscorlib", typeof(int).Assembly); + // var name = rm.GetString("Globalization.ci_" + culture.Name, displayCulture); + // but can we rely on it? + // + // and... DisplayName is captured and cached in culture infos returned by GetCultureInfo(), using + // the value for the current thread culture at the moment it is first retrieved - whereas creating + // a new CultureInfo() creates a new instance, which _then_ can get DisplayName again in a different + // culture + // + // I assume that, on a site, all language names should be in the SAME language, in DB, + // and that would be the umbracoDefaultUILanguage (app setting) - BUT if by accident + // ANY culture has been retrieved with another current thread culture - it's now corrupt + // + // so, the logic below ensures that the name always end up being the correct name + // see also LanguageController.GetAllCultures which is doing the same + // + // all this, including the ugly settings injection, because se store language names in db, + // otherwise it would be ok to simply return new CultureInfo(IsoCode).DisplayName to get the name + // in whatever culture is current - we should not do it, see task #3623 + // + // but then, some tests that compare audit strings (for culture names) would need to be fixed + + get + { + if (_cultureName != null) return _cultureName; + + // capture + var threadUiCulture = Thread.CurrentThread.CurrentUICulture; + + try + { + var globalSettings = (IGlobalSettings) Composing.Current.Container.GetInstance(typeof(IGlobalSettings)); + var defaultUiCulture = CultureInfo.GetCultureInfo(globalSettings.DefaultUILanguage); + Thread.CurrentThread.CurrentUICulture = defaultUiCulture; + + // get name - new-ing an instance to get proper display name + return new CultureInfo(IsoCode).DisplayName; + } + finally + { + // restore + Thread.CurrentThread.CurrentUICulture = threadUiCulture; + } + } + set => SetPropertyValueAndDetectChanges(value, ref _cultureName, Ps.Value.CultureNameSelector); } diff --git a/src/Umbraco.Core/Models/Macro.cs b/src/Umbraco.Core/Models/Macro.cs index 6e68bda439..5ef49305ac 100644 --- a/src/Umbraco.Core/Models/Macro.cs +++ b/src/Umbraco.Core/Models/Macro.cs @@ -284,22 +284,18 @@ namespace Umbraco.Core.Models get { return _properties; } } - public override object DeepClone() + protected override void PerformDeepClone(object clone) { - var clone = (Macro)base.DeepClone(); - //turn off change tracking - clone.DisableChangeTracking(); - clone._addedProperties = new List(); - clone._removedProperties = new List(); - clone._properties = (MacroPropertyCollection)Properties.DeepClone(); - //re-assign the event handler - clone._properties.CollectionChanged += clone.PropertiesChanged; - //this shouldn't really be needed since we're not tracking - clone.ResetDirtyProperties(false); - //re-enable tracking - clone.EnableChangeTracking(); + base.PerformDeepClone(clone); - return clone; + var clonedEntity = (Macro)clone; + + clonedEntity._addedProperties = new List(); + clonedEntity._removedProperties = new List(); + clonedEntity._properties = (MacroPropertyCollection)Properties.DeepClone(); + //re-assign the event handler + clonedEntity._properties.CollectionChanged += clonedEntity.PropertiesChanged; + } } } diff --git a/src/Umbraco.Core/Models/Media.cs b/src/Umbraco.Core/Models/Media.cs index c3f7cb6dd5..9c13a22caa 100644 --- a/src/Umbraco.Core/Models/Media.cs +++ b/src/Umbraco.Core/Models/Media.cs @@ -1,6 +1,5 @@ using System; using System.Runtime.Serialization; -using Umbraco.Core.Persistence.Mappers; namespace Umbraco.Core.Models { @@ -21,8 +20,7 @@ namespace Umbraco.Core.Models /// MediaType for the current Media object public Media(string name, IMedia parent, IMediaType contentType) : this(name, parent, contentType, new PropertyCollection()) - { - } + { } /// /// Constructor for creating a Media object @@ -45,8 +43,7 @@ namespace Umbraco.Core.Models /// MediaType for the current Media object public Media(string name, int parentId, IMediaType contentType) : this(name, parentId, contentType, new PropertyCollection()) - { - } + { } /// /// Constructor for creating a Media object @@ -78,6 +75,8 @@ namespace Umbraco.Core.Models _contentType = contentType; ContentTypeBase = contentType; Properties.EnsurePropertyTypes(PropertyTypes); + + Properties.CollectionChanged -= PropertiesChanged; // be sure not to double add Properties.CollectionChanged += PropertiesChanged; } @@ -95,6 +94,8 @@ namespace Umbraco.Core.Models _contentType = contentType; ContentTypeBase = contentType; Properties.EnsureCleanPropertyTypes(PropertyTypes); + + Properties.CollectionChanged -= PropertiesChanged; // be sure not to double add Properties.CollectionChanged += PropertiesChanged; return; } diff --git a/src/Umbraco.Core/Models/Member.cs b/src/Umbraco.Core/Models/Member.cs index 7576f01ce0..38927898cf 100644 --- a/src/Umbraco.Core/Models/Member.cs +++ b/src/Umbraco.Core/Models/Member.cs @@ -598,20 +598,15 @@ namespace Umbraco.Core.Models return true; } - public override object DeepClone() + protected override void PerformDeepClone(object clone) { - var clone = (Member)base.DeepClone(); - //turn off change tracking - clone.DisableChangeTracking(); + base.PerformDeepClone(clone); + + var clonedEntity = (Member)clone; + //need to manually clone this since it's not settable - clone._contentType = (IMemberType)ContentType.DeepClone(); - //this shouldn't really be needed since we're not tracking - clone.ResetDirtyProperties(false); - //re-enable tracking - clone.EnableChangeTracking(); - - return clone; - + clonedEntity._contentType = (IMemberType)ContentType.DeepClone(); + } /// diff --git a/src/Umbraco.Core/Models/Membership/User.cs b/src/Umbraco.Core/Models/Membership/User.cs index 9066674193..0694194996 100644 --- a/src/Umbraco.Core/Models/Membership/User.cs +++ b/src/Umbraco.Core/Models/Membership/User.cs @@ -448,18 +448,19 @@ namespace Umbraco.Core.Models.Membership [DoNotClone] internal object AdditionalDataLock { get { return _additionalDataLock; } } - public override object DeepClone() + protected override void PerformDeepClone(object clone) { - var clone = (User)base.DeepClone(); - //turn off change tracking - clone.DisableChangeTracking(); + base.PerformDeepClone(clone); + + var clonedEntity = (User)clone; + //manually clone the start node props - clone._startContentIds = _startContentIds.ToArray(); - clone._startMediaIds = _startMediaIds.ToArray(); + clonedEntity._startContentIds = _startContentIds.ToArray(); + clonedEntity._startMediaIds = _startMediaIds.ToArray(); // this value has been cloned and points to the same object // which obviously is bad - needs to point to a new object - clone._additionalDataLock = new object(); + clonedEntity._additionalDataLock = new object(); if (_additionalData != null) { @@ -467,7 +468,7 @@ namespace Umbraco.Core.Models.Membership // changing one clone impacts all of them - so we need to reset it with a fresh // dictionary that will contain the same values - and, if some values are deep // cloneable, they should be deep-cloned too - var cloneAdditionalData = clone._additionalData = new Dictionary(); + var cloneAdditionalData = clonedEntity._additionalData = new Dictionary(); lock (_additionalDataLock) { @@ -480,15 +481,9 @@ namespace Umbraco.Core.Models.Membership } //need to create new collections otherwise they'll get copied by ref - clone._userGroups = new HashSet(_userGroups); - clone._allowedSections = _allowedSections != null ? new List(_allowedSections) : null; - //re-create the event handler - //this shouldn't really be needed since we're not tracking - clone.ResetDirtyProperties(false); - //re-enable tracking - clone.EnableChangeTracking(); - - return clone; + clonedEntity._userGroups = new HashSet(_userGroups); + clonedEntity._allowedSections = _allowedSections != null ? new List(_allowedSections) : null; + } /// diff --git a/src/Umbraco.Core/Models/NotificationEmailBodyParams.cs b/src/Umbraco.Core/Models/NotificationEmailBodyParams.cs new file mode 100644 index 0000000000..e85284fe5a --- /dev/null +++ b/src/Umbraco.Core/Models/NotificationEmailBodyParams.cs @@ -0,0 +1,32 @@ +using System; + +namespace Umbraco.Core.Models +{ + public class NotificationEmailBodyParams + { + public NotificationEmailBodyParams(string recipientName, string action, string itemName, string itemId, string itemUrl, string editedUser, string siteUrl, string summary) + { + RecipientName = recipientName ?? throw new ArgumentNullException(nameof(recipientName)); + Action = action ?? throw new ArgumentNullException(nameof(action)); + ItemName = itemName ?? throw new ArgumentNullException(nameof(itemName)); + ItemId = itemId ?? throw new ArgumentNullException(nameof(itemId)); + ItemUrl = itemUrl ?? throw new ArgumentNullException(nameof(itemUrl)); + Summary = summary ?? throw new ArgumentNullException(nameof(summary)); + EditedUser = editedUser ?? throw new ArgumentNullException(nameof(editedUser)); + SiteUrl = siteUrl ?? throw new ArgumentNullException(nameof(siteUrl)); + } + + public string RecipientName { get; } + public string Action { get; } + public string ItemName { get; } + public string ItemId { get; } + public string ItemUrl { get; } + + /// + /// This will either be an HTML or text based summary depending on the email type being sent + /// + public string Summary { get; } + public string EditedUser { get; } + public string SiteUrl { get; } + } +} diff --git a/src/Umbraco.Core/Models/NotificationEmailSubjectParams.cs b/src/Umbraco.Core/Models/NotificationEmailSubjectParams.cs new file mode 100644 index 0000000000..07b26dbcc1 --- /dev/null +++ b/src/Umbraco.Core/Models/NotificationEmailSubjectParams.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Umbraco.Core.Models +{ + + public class NotificationEmailSubjectParams + { + public NotificationEmailSubjectParams(string siteUrl, string action, string itemName) + { + SiteUrl = siteUrl ?? throw new ArgumentNullException(nameof(siteUrl)); + Action = action ?? throw new ArgumentNullException(nameof(action)); + ItemName = itemName ?? throw new ArgumentNullException(nameof(itemName)); + } + + public string SiteUrl { get; } + public string Action { get; } + public string ItemName { get; } + } +} diff --git a/src/Umbraco.Core/Models/Property.cs b/src/Umbraco.Core/Models/Property.cs index bb922a740b..0c71544111 100644 --- a/src/Umbraco.Core/Models/Property.cs +++ b/src/Umbraco.Core/Models/Property.cs @@ -55,6 +55,9 @@ namespace Umbraco.Core.Models /// public class PropertyValue { + //TODO: Either we allow change tracking at this class level, or we add some special change tracking collections to the Property + // class to deal with change tracking which variants have changed + private string _culture; private string _segment; @@ -100,6 +103,7 @@ namespace Umbraco.Core.Models // ReSharper disable once ClassNeverInstantiated.Local private class PropertySelectors { + //TODO: This allows us to track changes for an entire Property, but doesn't allow us to track changes at the variant level public readonly PropertyInfo ValuesSelector = ExpressionHelper.GetPropertyInfo(x => x.Values); public readonly DelegateEqualityComparer PropertyValueComparer = new DelegateEqualityComparer( @@ -388,21 +392,14 @@ namespace Umbraco.Core.Models return PropertyType.IsPropertyValueValid(value); } - public override object DeepClone() + protected override void PerformDeepClone(object clone) { - var clone = (Property) base.DeepClone(); + base.PerformDeepClone(clone); - //turn off change tracking - clone.DisableChangeTracking(); + var clonedEntity = (Property)clone; //need to manually assign since this is a readonly property - clone.PropertyType = (PropertyType) PropertyType.DeepClone(); - - //re-enable tracking - clone.ResetDirtyProperties(false); // not needed really, since we're not tracking - clone.EnableChangeTracking(); - - return clone; + clonedEntity.PropertyType = (PropertyType) PropertyType.DeepClone(); } } } diff --git a/src/Umbraco.Core/Models/PropertyGroup.cs b/src/Umbraco.Core/Models/PropertyGroup.cs index 286e165764..1d0b949932 100644 --- a/src/Umbraco.Core/Models/PropertyGroup.cs +++ b/src/Umbraco.Core/Models/PropertyGroup.cs @@ -66,7 +66,11 @@ namespace Umbraco.Core.Models /// /// Gets or sets a collection of PropertyTypes for this PropertyGroup /// + /// + /// Marked DoNotClone because we will manually deal with cloning and the event handlers + /// [DataMember] + [DoNotClone] public PropertyTypeCollection PropertyTypes { get => _propertyTypes; @@ -95,5 +99,19 @@ namespace Umbraco.Core.Models var nameHash = Name.ToLowerInvariant().GetHashCode(); return baseHash ^ nameHash; } + + protected override void PerformDeepClone(object clone) + { + base.PerformDeepClone(clone); + + var clonedEntity = (PropertyGroup)clone; + + if (clonedEntity._propertyTypes != null) + { + clonedEntity._propertyTypes.CollectionChanged -= PropertyTypesChanged; //clear this event handler if any + clonedEntity._propertyTypes = (PropertyTypeCollection) _propertyTypes.DeepClone(); //manually deep clone + clonedEntity._propertyTypes.CollectionChanged += clonedEntity.PropertyTypesChanged; //re-assign correct event handler + } + } } } diff --git a/src/Umbraco.Core/Models/PropertyGroupCollection.cs b/src/Umbraco.Core/Models/PropertyGroupCollection.cs index d10b375285..c5768c66db 100644 --- a/src/Umbraco.Core/Models/PropertyGroupCollection.cs +++ b/src/Umbraco.Core/Models/PropertyGroupCollection.cs @@ -8,15 +8,18 @@ using System.Threading; namespace Umbraco.Core.Models { + /// /// Represents a collection of objects /// [Serializable] [DataContract] + //TODO: Change this to ObservableDictionary so we can reduce the INotifyCollectionChanged implementation details public class PropertyGroupCollection : KeyedCollection, INotifyCollectionChanged, IDeepCloneable { private readonly ReaderWriterLockSlim _addLocker = new ReaderWriterLockSlim(); + //fixme: this doesn't seem to be used anywhere internal Action OnAdd; internal PropertyGroupCollection() @@ -168,7 +171,7 @@ namespace Umbraco.Core.Models var clone = new PropertyGroupCollection(); foreach (var group in this) { - clone.Add((PropertyGroup) group.DeepClone()); + clone.Add((PropertyGroup)group.DeepClone()); } return clone; } diff --git a/src/Umbraco.Core/Models/PropertyType.cs b/src/Umbraco.Core/Models/PropertyType.cs index a34fdb04ed..d44e7d464f 100644 --- a/src/Umbraco.Core/Models/PropertyType.cs +++ b/src/Umbraco.Core/Models/PropertyType.cs @@ -424,22 +424,17 @@ namespace Umbraco.Core.Models } /// - public override object DeepClone() + protected override void PerformDeepClone(object clone) { - var clone = (PropertyType)base.DeepClone(); - //turn off change tracking - clone.DisableChangeTracking(); + base.PerformDeepClone(clone); + + var clonedEntity = (PropertyType)clone; + //need to manually assign the Lazy value as it will not be automatically mapped if (PropertyGroupId != null) { - clone._propertyGroupId = new Lazy(() => PropertyGroupId.Value); + clonedEntity._propertyGroupId = new Lazy(() => PropertyGroupId.Value); } - //this shouldn't really be needed since we're not tracking - clone.ResetDirtyProperties(false); - //re-enable tracking - clone.EnableChangeTracking(); - - return clone; } } } diff --git a/src/Umbraco.Core/Models/PropertyTypeCollection.cs b/src/Umbraco.Core/Models/PropertyTypeCollection.cs index 47710e04cb..6053a6a5bf 100644 --- a/src/Umbraco.Core/Models/PropertyTypeCollection.cs +++ b/src/Umbraco.Core/Models/PropertyTypeCollection.cs @@ -13,11 +13,13 @@ namespace Umbraco.Core.Models /// [Serializable] [DataContract] + //TODO: Change this to ObservableDictionary so we can reduce the INotifyCollectionChanged implementation details public class PropertyTypeCollection : KeyedCollection, INotifyCollectionChanged, IDeepCloneable { [IgnoreDataMember] private readonly ReaderWriterLockSlim _addLocker = new ReaderWriterLockSlim(); + //fixme: This doesn't seem to be used [IgnoreDataMember] internal Action OnAdd; diff --git a/src/Umbraco.Core/Models/PublicAccessEntry.cs b/src/Umbraco.Core/Models/PublicAccessEntry.cs index a9f568e43a..e93dc56e35 100644 --- a/src/Umbraco.Core/Models/PublicAccessEntry.cs +++ b/src/Umbraco.Core/Models/PublicAccessEntry.cs @@ -152,5 +152,18 @@ namespace Umbraco.Core.Models publicAccessRule.ResetDirtyProperties(rememberDirty); } } + + protected override void PerformDeepClone(object clone) + { + base.PerformDeepClone(clone); + + var cloneEntity = (PublicAccessEntry)clone; + + if (cloneEntity._ruleCollection != null) + { + cloneEntity._ruleCollection.CollectionChanged -= _ruleCollection_CollectionChanged; //clear this event handler if any + cloneEntity._ruleCollection.CollectionChanged += cloneEntity._ruleCollection_CollectionChanged; //re-assign correct event handler + } + } } } diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs index e611ded6c8..f1937c1c0c 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs @@ -94,10 +94,10 @@ namespace Umbraco.Core.Models.PublishedContent { "Comments", (Constants.DataTypes.Textbox, Constants.PropertyEditors.Aliases.TextBox) }, { "IsApproved", (Constants.DataTypes.Boolean, Constants.PropertyEditors.Aliases.Boolean) }, { "IsLockedOut", (Constants.DataTypes.Boolean, Constants.PropertyEditors.Aliases.Boolean) }, - { "LastLockoutDate", (Constants.DataTypes.Datetime, Constants.PropertyEditors.Aliases.DateTime) }, - { "CreateDate", (Constants.DataTypes.Datetime, Constants.PropertyEditors.Aliases.DateTime) }, - { "LastLoginDate", (Constants.DataTypes.Datetime, Constants.PropertyEditors.Aliases.DateTime) }, - { "LastPasswordChangeDate", (Constants.DataTypes.Datetime, Constants.PropertyEditors.Aliases.DateTime) }, + { "LastLockoutDate", (Constants.DataTypes.DateTime, Constants.PropertyEditors.Aliases.DateTime) }, + { "CreateDate", (Constants.DataTypes.DateTime, Constants.PropertyEditors.Aliases.DateTime) }, + { "LastLoginDate", (Constants.DataTypes.DateTime, Constants.PropertyEditors.Aliases.DateTime) }, + { "LastPasswordChangeDate", (Constants.DataTypes.DateTime, Constants.PropertyEditors.Aliases.DateTime) }, }; #region Content type diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedModelFactory.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedModelFactory.cs index c72a89c1f2..67758c1c69 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedModelFactory.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedModelFactory.cs @@ -71,7 +71,7 @@ namespace Umbraco.Core.Models.PublishedContent throw new InvalidOperationException($"Both types {type.FullName} and {modelInfo.ModelType.FullName} want to be a model type for content type with alias \"{typeName}\"."); // have to use an unsafe ctor because we don't know the types, really - var modelCtor = ReflectionUtilities.EmitCtorUnsafe>(constructor); + var modelCtor = ReflectionUtilities.EmitConstructorUnsafe>(constructor); modelInfos[typeName] = new ModelInfo { ParameterType = parameterType, ModelType = type, Ctor = modelCtor }; modelTypeMap[typeName] = type; } @@ -112,7 +112,7 @@ namespace Umbraco.Core.Models.PublishedContent if (ctor != null) return ctor(); var listType = typeof(List<>).MakeGenericType(modelInfo.ModelType); - ctor = modelInfo.ListCtor = ReflectionUtilities.EmitCtor>(declaring: listType); + ctor = modelInfo.ListCtor = ReflectionUtilities.EmitConstuctor>(declaring: listType); return ctor(); } diff --git a/src/Umbraco.Core/Models/Stylesheet.cs b/src/Umbraco.Core/Models/Stylesheet.cs index a228b70105..19b97044bd 100644 --- a/src/Umbraco.Core/Models/Stylesheet.cs +++ b/src/Umbraco.Core/Models/Stylesheet.cs @@ -4,7 +4,6 @@ using System.ComponentModel; using System.Data; using System.Linq; using System.Runtime.Serialization; -using Umbraco.Core.IO; using Umbraco.Core.Strings.Css; namespace Umbraco.Core.Models @@ -21,7 +20,7 @@ namespace Umbraco.Core.Models { } internal Stylesheet(string path, Func getFileContent) - : base(path.EnsureEndsWith(".css"), getFileContent) + : base(string.IsNullOrEmpty(path) ? path : path.EnsureEndsWith(".css"), getFileContent) { InitializeProperties(); } diff --git a/src/Umbraco.Core/Models/UserExtensions.cs b/src/Umbraco.Core/Models/UserExtensions.cs index 82e4935616..ea61228864 100644 --- a/src/Umbraco.Core/Models/UserExtensions.cs +++ b/src/Umbraco.Core/Models/UserExtensions.cs @@ -9,6 +9,7 @@ using Umbraco.Core.Composing; using Umbraco.Core.Models.Entities; using Umbraco.Core.Models.Membership; using Umbraco.Core.Services; +using Umbraco.Core.Security; namespace Umbraco.Core.Models { @@ -146,68 +147,46 @@ namespace Umbraco.Core.Models internal static bool HasContentRootAccess(this IUser user, IEntityService entityService) { - return HasPathAccess(Constants.System.Root.ToInvariantString(), user.CalculateContentStartNodeIds(entityService), Constants.System.RecycleBinContent); + return ContentPermissionsHelper.HasPathAccess(Constants.System.Root.ToInvariantString(), user.CalculateContentStartNodeIds(entityService), Constants.System.RecycleBinContent); } internal static bool HasContentBinAccess(this IUser user, IEntityService entityService) { - return HasPathAccess(Constants.System.RecycleBinContent.ToInvariantString(), user.CalculateContentStartNodeIds(entityService), Constants.System.RecycleBinContent); + return ContentPermissionsHelper.HasPathAccess(Constants.System.RecycleBinContent.ToInvariantString(), user.CalculateContentStartNodeIds(entityService), Constants.System.RecycleBinContent); } internal static bool HasMediaRootAccess(this IUser user, IEntityService entityService) { - return HasPathAccess(Constants.System.Root.ToInvariantString(), user.CalculateMediaStartNodeIds(entityService), Constants.System.RecycleBinMedia); + return ContentPermissionsHelper.HasPathAccess(Constants.System.Root.ToInvariantString(), user.CalculateMediaStartNodeIds(entityService), Constants.System.RecycleBinMedia); } internal static bool HasMediaBinAccess(this IUser user, IEntityService entityService) { - return HasPathAccess(Constants.System.RecycleBinMedia.ToInvariantString(), user.CalculateMediaStartNodeIds(entityService), Constants.System.RecycleBinMedia); + return ContentPermissionsHelper.HasPathAccess(Constants.System.RecycleBinMedia.ToInvariantString(), user.CalculateMediaStartNodeIds(entityService), Constants.System.RecycleBinMedia); } internal static bool HasPathAccess(this IUser user, IContent content, IEntityService entityService) { - return HasPathAccess(content.Path, user.CalculateContentStartNodeIds(entityService), Constants.System.RecycleBinContent); + if (content == null) throw new ArgumentNullException(nameof(content)); + return ContentPermissionsHelper.HasPathAccess(content.Path, user.CalculateContentStartNodeIds(entityService), Constants.System.RecycleBinContent); } internal static bool HasPathAccess(this IUser user, IMedia media, IEntityService entityService) { - return HasPathAccess(media.Path, user.CalculateMediaStartNodeIds(entityService), Constants.System.RecycleBinMedia); + if (media == null) throw new ArgumentNullException(nameof(media)); + return ContentPermissionsHelper.HasPathAccess(media.Path, user.CalculateMediaStartNodeIds(entityService), Constants.System.RecycleBinMedia); } - internal static bool HasPathAccess(this IUser user, IUmbracoEntity entity, IEntityService entityService, int recycleBinId) + internal static bool HasContentPathAccess(this IUser user, IUmbracoEntity entity, IEntityService entityService) { - switch (recycleBinId) - { - case Constants.System.RecycleBinMedia: - return HasPathAccess(entity.Path, user.CalculateMediaStartNodeIds(entityService), recycleBinId); - case Constants.System.RecycleBinContent: - return HasPathAccess(entity.Path, user.CalculateContentStartNodeIds(entityService), recycleBinId); - default: - throw new NotSupportedException("Path access is only determined on content or media"); - } + if (entity == null) throw new ArgumentNullException(nameof(entity)); + return ContentPermissionsHelper.HasPathAccess(entity.Path, user.CalculateContentStartNodeIds(entityService), Constants.System.RecycleBinContent); } - internal static bool HasPathAccess(string path, int[] startNodeIds, int recycleBinId) + internal static bool HasMediaPathAccess(this IUser user, IUmbracoEntity entity, IEntityService entityService) { - if (string.IsNullOrWhiteSpace(path)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(path)); - - // check for no access - if (startNodeIds.Length == 0) - return false; - - // check for root access - if (startNodeIds.Contains(Constants.System.Root)) - return true; - - var formattedPath = string.Concat(",", path, ","); - - // only users with root access have access to the recycle bin, - // if the above check didn't pass then access is denied - if (formattedPath.Contains(string.Concat(",", recycleBinId, ","))) - return false; - - // check for a start node in the path - return startNodeIds.Any(x => formattedPath.Contains(string.Concat(",", x, ","))); + if (entity == null) throw new ArgumentNullException(nameof(entity)); + return ContentPermissionsHelper.HasPathAccess(entity.Path, user.CalculateMediaStartNodeIds(entityService), Constants.System.RecycleBinMedia); } internal static bool IsInBranchOfStartNode(this IUser user, IUmbracoEntity entity, IEntityService entityService, int recycleBinId, out bool hasPathAccess) @@ -215,58 +194,14 @@ namespace Umbraco.Core.Models switch (recycleBinId) { case Constants.System.RecycleBinMedia: - return IsInBranchOfStartNode(entity.Path, user.CalculateMediaStartNodeIds(entityService), user.GetMediaStartNodePaths(entityService), out hasPathAccess); + return ContentPermissionsHelper.IsInBranchOfStartNode(entity.Path, user.CalculateMediaStartNodeIds(entityService), user.GetMediaStartNodePaths(entityService), out hasPathAccess); case Constants.System.RecycleBinContent: - return IsInBranchOfStartNode(entity.Path, user.CalculateContentStartNodeIds(entityService), user.GetContentStartNodePaths(entityService), out hasPathAccess); + return ContentPermissionsHelper.IsInBranchOfStartNode(entity.Path, user.CalculateContentStartNodeIds(entityService), user.GetContentStartNodePaths(entityService), out hasPathAccess); default: throw new NotSupportedException("Path access is only determined on content or media"); } } - internal static bool IsInBranchOfStartNode(string path, int[] startNodeIds, string[] startNodePaths, out bool hasPathAccess) - { - if (string.IsNullOrWhiteSpace(path)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(path)); - - hasPathAccess = false; - - // check for no access - if (startNodeIds.Length == 0) - return false; - - // check for root access - if (startNodeIds.Contains(Constants.System.Root)) - { - hasPathAccess = true; - return true; - } - - //is it self? - var self = startNodePaths.Any(x => x == path); - if (self) - { - hasPathAccess = true; - return true; - } - - //is it ancestor? - var ancestor = startNodePaths.Any(x => x.StartsWith(path)); - if (ancestor) - { - //hasPathAccess = false; - return true; - } - - //is it descendant? - var descendant = startNodePaths.Any(x => path.StartsWith(x)); - if (descendant) - { - hasPathAccess = true; - return true; - } - - return false; - } - /// /// Determines whether this user has access to view sensitive data /// diff --git a/src/Umbraco.Core/Packaging/PackageInstallation.cs b/src/Umbraco.Core/Packaging/PackageInstallation.cs index a6f1dd0f25..556c5203ec 100644 --- a/src/Umbraco.Core/Packaging/PackageInstallation.cs +++ b/src/Umbraco.Core/Packaging/PackageInstallation.cs @@ -576,7 +576,6 @@ namespace Umbraco.Core.Packaging { //this is experimental and undocumented... path = path.Replace("[$UMBRACO]", SystemDirectories.Umbraco); - path = path.Replace("[$UMBRACOCLIENT]", SystemDirectories.UmbracoClient); path = path.Replace("[$CONFIG]", SystemDirectories.Config); path = path.Replace("[$DATA]", SystemDirectories.Data); } diff --git a/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs b/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs index 523d09c1f7..9eb4c3f90f 100644 --- a/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs +++ b/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs @@ -31,6 +31,7 @@ namespace Umbraco.Core public const string DocumentCultureVariation = TableNamePrefix + "DocumentCultureVariation"; public const string DocumentVersion = TableNamePrefix + "DocumentVersion"; public const string MediaVersion = TableNamePrefix + "MediaVersion"; + public const string ContentSchedule = TableNamePrefix + "ContentSchedule"; public const string PropertyType = /*TableNamePrefix*/ "cms" + "PropertyType"; public const string PropertyTypeGroup = /*TableNamePrefix*/ "cms" + "PropertyTypeGroup"; diff --git a/src/Umbraco.Core/Persistence/DatabaseModelDefinitions/DefinitionFactory.cs b/src/Umbraco.Core/Persistence/DatabaseModelDefinitions/DefinitionFactory.cs index 97f995e99d..5a0e44a281 100644 --- a/src/Umbraco.Core/Persistence/DatabaseModelDefinitions/DefinitionFactory.cs +++ b/src/Umbraco.Core/Persistence/DatabaseModelDefinitions/DefinitionFactory.cs @@ -159,9 +159,7 @@ namespace Umbraco.Core.Persistence.DatabaseModelDefinitions Name = indexName, IndexType = attribute.IndexType, ColumnName = columnName, - TableName = tableName, - IsClustered = attribute.IndexType == IndexTypes.Clustered, - IsUnique = attribute.IndexType == IndexTypes.UniqueNonClustered + TableName = tableName, }; if (string.IsNullOrEmpty(attribute.ForColumns) == false) diff --git a/src/Umbraco.Core/Persistence/DatabaseModelDefinitions/IndexDefinition.cs b/src/Umbraco.Core/Persistence/DatabaseModelDefinitions/IndexDefinition.cs index d4f2a27ae6..582f9a40f7 100644 --- a/src/Umbraco.Core/Persistence/DatabaseModelDefinitions/IndexDefinition.cs +++ b/src/Umbraco.Core/Persistence/DatabaseModelDefinitions/IndexDefinition.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Umbraco.Core.Persistence.DatabaseAnnotations; namespace Umbraco.Core.Persistence.DatabaseModelDefinitions @@ -14,8 +15,7 @@ namespace Umbraco.Core.Persistence.DatabaseModelDefinitions public virtual string SchemaName { get; set; } public virtual string TableName { get; set; } public virtual string ColumnName { get; set; } - public virtual bool IsUnique { get; set; } - public bool IsClustered { get; set; } + public virtual ICollection Columns { get; set; } public IndexTypes IndexType { get; set; } } diff --git a/src/Umbraco.Core/Persistence/Dtos/ContentScheduleDto.cs b/src/Umbraco.Core/Persistence/Dtos/ContentScheduleDto.cs new file mode 100644 index 0000000000..492a3d7cbd --- /dev/null +++ b/src/Umbraco.Core/Persistence/Dtos/ContentScheduleDto.cs @@ -0,0 +1,33 @@ +using System; +using NPoco; +using Umbraco.Core.Persistence.DatabaseAnnotations; + +namespace Umbraco.Core.Persistence.Dtos +{ + [TableName(TableName)] + [PrimaryKey("id", AutoIncrement = false)] + [ExplicitColumns] + internal class ContentScheduleDto + { + public const string TableName = Constants.DatabaseSchema.Tables.ContentSchedule; + + [Column("id")] + [PrimaryKeyColumn(AutoIncrement = false)] + public Guid Id { get; set; } + + [Column("nodeId")] + [ForeignKey(typeof(ContentDto))] + public int NodeId { get; set; } + + [Column("languageId")] + [ForeignKey(typeof(LanguageDto))] + [NullSetting(NullSetting = NullSettings.Null)] // can be invariant + public int? LanguageId { get; set; } + + [Column("date")] + public DateTime Date { get; set; } + + [Column("action")] + public string Action { get; set; } + } +} diff --git a/src/Umbraco.Core/Persistence/Dtos/DocumentDto.cs b/src/Umbraco.Core/Persistence/Dtos/DocumentDto.cs index fd3df69b8a..abe13a0e23 100644 --- a/src/Umbraco.Core/Persistence/Dtos/DocumentDto.cs +++ b/src/Umbraco.Core/Persistence/Dtos/DocumentDto.cs @@ -1,9 +1,9 @@ -using System; -using NPoco; +using NPoco; using Umbraco.Core.Persistence.DatabaseAnnotations; namespace Umbraco.Core.Persistence.Dtos { + [TableName(TableName)] [PrimaryKey("nodeId", AutoIncrement = false)] [ExplicitColumns] @@ -23,14 +23,6 @@ namespace Umbraco.Core.Persistence.Dtos [Column("edited")] public bool Edited { get; set; } - [Column("releaseDate")] - [NullSetting(NullSetting = NullSettings.Null)] - public DateTime? ReleaseDate { get; set; } - - [Column("expireDate")] - [NullSetting(NullSetting = NullSettings.Null)] - public DateTime? ExpiresDate { get; set; } - //[Column("publishDate")] //[NullSetting(NullSetting = NullSettings.Null)] // is contentVersionDto.VersionDate for the published version //public DateTime? PublishDate { get; set; } diff --git a/src/Umbraco.Core/Persistence/Dtos/LockDto.cs b/src/Umbraco.Core/Persistence/Dtos/LockDto.cs index 833d262e26..b5878141f3 100644 --- a/src/Umbraco.Core/Persistence/Dtos/LockDto.cs +++ b/src/Umbraco.Core/Persistence/Dtos/LockDto.cs @@ -4,12 +4,12 @@ using Umbraco.Core.Persistence.DatabaseAnnotations; namespace Umbraco.Core.Persistence.Dtos { [TableName(Constants.DatabaseSchema.Tables.Lock)] - [PrimaryKey("id")] + [PrimaryKey("id", AutoIncrement = false)] [ExplicitColumns] internal class LockDto { [Column("id")] - [PrimaryKeyColumn(Name = "PK_umbracoLock")] + [PrimaryKeyColumn(Name = "PK_umbracoLock", AutoIncrement = false)] public int Id { get; set; } [Column("value")] diff --git a/src/Umbraco.Core/Persistence/Dtos/LogDto.cs b/src/Umbraco.Core/Persistence/Dtos/LogDto.cs index 2ecf85e87c..9a710c1fec 100644 --- a/src/Umbraco.Core/Persistence/Dtos/LogDto.cs +++ b/src/Umbraco.Core/Persistence/Dtos/LogDto.cs @@ -5,11 +5,13 @@ using Umbraco.Core.Persistence.DatabaseModelDefinitions; namespace Umbraco.Core.Persistence.Dtos { - [TableName(Constants.DatabaseSchema.Tables.Log)] + [TableName(TableName)] [PrimaryKey("id")] [ExplicitColumns] internal class LogDto { + public const string TableName = Constants.DatabaseSchema.Tables.Log; + private int? _userId; [Column("id")] @@ -25,10 +27,20 @@ namespace Umbraco.Core.Persistence.Dtos [Index(IndexTypes.NonClustered, Name = "IX_umbracoLog")] public int NodeId { get; set; } + /// + /// This is the entity type associated with the log + /// + [Column("entityType")] + [Length(50)] + [NullSetting(NullSetting = NullSettings.Null)] + public string EntityType { get; set; } + + //TODO: Should we have an index on this since we allow searching on it? [Column("Datestamp")] [Constraint(Default = SystemMethods.CurrentDateTime)] public DateTime Datestamp { get; set; } + //TODO: Should we have an index on this since we allow searching on it? [Column("logHeader")] [Length(50)] public string Header { get; set; } @@ -37,5 +49,13 @@ namespace Umbraco.Core.Persistence.Dtos [NullSetting(NullSetting = NullSettings.Null)] [Length(4000)] public string Comment { get; set; } + + /// + /// Used to store additional data parameters for the log + /// + [Column("parameters")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(500)] + public string Parameters { get; set; } } } diff --git a/src/Umbraco.Core/Persistence/Dtos/TemplateDto.cs b/src/Umbraco.Core/Persistence/Dtos/TemplateDto.cs index 2f0d149ee7..a73425db8d 100644 --- a/src/Umbraco.Core/Persistence/Dtos/TemplateDto.cs +++ b/src/Umbraco.Core/Persistence/Dtos/TemplateDto.cs @@ -22,10 +22,6 @@ namespace Umbraco.Core.Persistence.Dtos [NullSetting(NullSetting = NullSettings.Null)] public string Alias { get; set; } - [Column("design")] - [SpecialDbType(SpecialDbTypes.NTEXT)] - public string Design { get; set; } - [ResultColumn] [Reference(ReferenceType.OneToOne, ColumnName = "NodeId")] public NodeDto NodeDto { get; set; } diff --git a/src/Umbraco.Core/Persistence/Factories/ContentBaseFactory.cs b/src/Umbraco.Core/Persistence/Factories/ContentBaseFactory.cs index ec364c7c6a..c8467f47e2 100644 --- a/src/Umbraco.Core/Persistence/Factories/ContentBaseFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/ContentBaseFactory.cs @@ -1,7 +1,10 @@ using System; +using System.Collections.Generic; +using System.Linq; using System.Text.RegularExpressions; using Umbraco.Core.Models; using Umbraco.Core.Persistence.Dtos; +using Umbraco.Core.Persistence.Repositories; namespace Umbraco.Core.Persistence.Factories { @@ -45,8 +48,6 @@ namespace Umbraco.Core.Persistence.Factories content.Published = dto.Published; content.Edited = dto.Edited; - content.ExpireDate = dto.ExpiresDate; - content.ReleaseDate = dto.ReleaseDate; // fixme - shall we get published infos or not? //if (dto.Published) @@ -142,7 +143,7 @@ namespace Umbraco.Core.Persistence.Factories content.CreateDate = nodeDto.CreateDate; content.UpdateDate = contentVersionDto.VersionDate; - content.ProviderUserKey = content.Key; // fixme explain + content.ProviderUserKey = content.Key; // The `ProviderUserKey` is a membership provider thing // reset dirty initial properties (U4-1946) content.ResetDirtyProperties(false); @@ -155,7 +156,7 @@ namespace Umbraco.Core.Persistence.Factories } /// - /// Buils a dto from an IContent item. + /// Builds a dto from an IContent item. /// public static DocumentDto BuildDto(IContent entity, Guid objectType) { @@ -165,9 +166,6 @@ namespace Umbraco.Core.Persistence.Factories { NodeId = entity.Id, Published = entity.Published, - ReleaseDate = entity.ReleaseDate, - ExpiresDate = entity.ExpireDate, - ContentDto = contentDto, DocumentVersionDto = BuildDocumentVersionDto(entity, contentDto) }; @@ -175,6 +173,19 @@ namespace Umbraco.Core.Persistence.Factories return dto; } + public static IEnumerable<(ContentSchedule Model, ContentScheduleDto Dto)> BuildScheduleDto(IContent entity, ILanguageRepository languageRepository) + { + return entity.ContentSchedule.FullSchedule.Select(x => + (x, new ContentScheduleDto + { + Action = x.Action.ToString(), + Date = x.Date, + NodeId = entity.Id, + LanguageId = languageRepository.GetIdByIsoCode(x.Culture, false), + Id = x.Id + })); + } + /// /// Buils a dto from an IMedia item. /// @@ -302,6 +313,9 @@ namespace Umbraco.Core.Persistence.Factories // more dark magic ;-( internal static bool TryMatch(string text, out string path) { + //fixme: In v8 we should allow exposing this via the property editor in a much nicer way so that the property editor + // can tell us directly what any URL is for a given property if it contains an asset + path = null; if (string.IsNullOrWhiteSpace(text)) return false; diff --git a/src/Umbraco.Core/Persistence/Factories/DataTypeFactory.cs b/src/Umbraco.Core/Persistence/Factories/DataTypeFactory.cs index fa6513775f..01b0e59a1d 100644 --- a/src/Umbraco.Core/Persistence/Factories/DataTypeFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/DataTypeFactory.cs @@ -53,7 +53,7 @@ namespace Umbraco.Core.Persistence.Factories EditorAlias = entity.EditorAlias, NodeId = entity.Id, DbType = entity.DatabaseType.ToString(), - Configuration = entity.Configuration == null ? null : JsonConvert.SerializeObject(entity.Configuration, ConfigurationEditor.ConfigurationJsonSettings), + Configuration = ConfigurationEditor.ToDatabase(entity.Configuration), NodeDto = BuildNodeDto(entity) }; diff --git a/src/Umbraco.Core/Persistence/Factories/TemplateFactory.cs b/src/Umbraco.Core/Persistence/Factories/TemplateFactory.cs index b2b5be872e..7682ea47db 100644 --- a/src/Umbraco.Core/Persistence/Factories/TemplateFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/TemplateFactory.cs @@ -45,7 +45,6 @@ namespace Umbraco.Core.Persistence.Factories var dto = new TemplateDto { Alias = entity.Alias, - Design = entity.Content ?? string.Empty, NodeDto = BuildNodeDto(entity, nodeObjectTypeId) }; diff --git a/src/Umbraco.Core/Persistence/Mappers/ContentMapper.cs b/src/Umbraco.Core/Persistence/Mappers/ContentMapper.cs index d2e0e7245c..2cc3a5b140 100644 --- a/src/Umbraco.Core/Persistence/Mappers/ContentMapper.cs +++ b/src/Umbraco.Core/Persistence/Mappers/ContentMapper.cs @@ -37,8 +37,6 @@ namespace Umbraco.Core.Persistence.Mappers CacheMap(src => src.ContentTypeId, dto => dto.ContentTypeId); CacheMap(src => src.UpdateDate, dto => dto.VersionDate); - CacheMap(src => src.ExpireDate, dto => dto.ExpiresDate); - CacheMap(src => src.ReleaseDate, dto => dto.ReleaseDate); CacheMap(src => src.Published, dto => dto.Published); //CacheMap(src => src.Name, dto => dto.Alias); diff --git a/src/Umbraco.Core/Persistence/Mappers/TemplateMapper.cs b/src/Umbraco.Core/Persistence/Mappers/TemplateMapper.cs index f402081e08..ca5faab134 100644 --- a/src/Umbraco.Core/Persistence/Mappers/TemplateMapper.cs +++ b/src/Umbraco.Core/Persistence/Mappers/TemplateMapper.cs @@ -24,7 +24,6 @@ namespace Umbraco.Core.Persistence.Mappers CacheMap(src => src.MasterTemplateId, dto => dto.ParentId); CacheMap(src => src.Key, dto => dto.UniqueId); CacheMap(src => src.Alias, dto => dto.Alias); - CacheMap(src => src.Content, dto => dto.Design); } } } diff --git a/src/Umbraco.Core/Persistence/NPocoSqlExtensions.cs b/src/Umbraco.Core/Persistence/NPocoSqlExtensions.cs index 4fdc72f52f..a5ab62d25f 100644 --- a/src/Umbraco.Core/Persistence/NPocoSqlExtensions.cs +++ b/src/Umbraco.Core/Persistence/NPocoSqlExtensions.cs @@ -73,7 +73,7 @@ namespace Umbraco.Core.Persistence /// The Sql statement. public static Sql Where(this Sql sql, Expression> predicate, string alias = null) { - var (s, a) = sql.SqlContext.Visit(predicate, alias); + var (s, a) = sql.SqlContext.VisitDto(predicate, alias); return sql.Where(s, a); } @@ -89,7 +89,7 @@ namespace Umbraco.Core.Persistence /// The Sql statement. public static Sql Where(this Sql sql, Expression> predicate, string alias1 = null, string alias2 = null) { - var (s, a) = sql.SqlContext.Visit(predicate, alias1, alias2); + var (s, a) = sql.SqlContext.VisitDto(predicate, alias1, alias2); return sql.Where(s, a); } @@ -321,9 +321,9 @@ namespace Umbraco.Core.Persistence /// Appends an ORDER BY DESC clause to the Sql statement. /// /// The Sql statement. - /// Expression specifying the fields. + /// Fields. /// The Sql statement. - public static Sql OrderByDescending(this Sql sql, params object[] fields) + public static Sql OrderByDescending(this Sql sql, params string[] fields) { return sql.Append("ORDER BY " + string.Join(", ", fields.Select(x => x + " DESC"))); } @@ -660,6 +660,18 @@ namespace Umbraco.Core.Persistence return sql.Select(sql.GetColumns(tableAlias: tableAlias, columnExpressions: fields)); } + /// + /// Adds columns to a SELECT Sql statement. + /// + /// The origin sql. + /// Columns to select. + /// The Sql statement. + public static Sql AndSelect(this Sql sql, params string[] fields) + { + if (sql == null) throw new ArgumentNullException(nameof(sql)); + return sql.Append(", " + string.Join(", ", fields)); + } + /// /// Adds columns to a SELECT Sql statement. /// @@ -676,7 +688,6 @@ namespace Umbraco.Core.Persistence return sql.Append(", " + string.Join(", ", sql.GetColumns(columnExpressions: fields))); } - /// /// Adds columns to a SELECT Sql statement. /// diff --git a/src/Umbraco.Core/Persistence/Querying/ExpressionVisitorBase.cs b/src/Umbraco.Core/Persistence/Querying/ExpressionVisitorBase.cs index 76116a8d03..d313d27bbc 100644 --- a/src/Umbraco.Core/Persistence/Querying/ExpressionVisitorBase.cs +++ b/src/Umbraco.Core/Persistence/Querying/ExpressionVisitorBase.cs @@ -32,7 +32,7 @@ namespace Umbraco.Core.Persistence.Querying /// /// Gets or sets the SQL syntax provider for the current database. /// - protected ISqlSyntaxProvider SqlSyntax { get; private set; } + protected ISqlSyntaxProvider SqlSyntax { get; } /// /// Gets the list of SQL parameters. @@ -56,6 +56,8 @@ namespace Umbraco.Core.Persistence.Querying /// Also populates the SQL parameters. public virtual string Visit(Expression expression) { + if (expression == null) return string.Empty; + // if the expression is a CachedExpression, // visit the inner expression if not already visited var cachedExpression = expression as CachedExpression; @@ -65,8 +67,6 @@ namespace Umbraco.Core.Persistence.Querying expression = cachedExpression.InnerExpression; } - if (expression == null) return string.Empty; - string result; switch (expression.NodeType) @@ -135,35 +135,28 @@ namespace Umbraco.Core.Persistence.Querying // if the expression is a CachedExpression, // and is not already compiled, assign the result - if (cachedExpression != null) - { - if (cachedExpression.Visited == false) - cachedExpression.VisitResult = result; - result = cachedExpression.VisitResult; - } - - return result; + if (cachedExpression == null) + return result; + if (!cachedExpression.Visited) + cachedExpression.VisitResult = result; + return cachedExpression.VisitResult; } protected abstract string VisitMemberAccess(MemberExpression m); protected virtual string VisitLambda(LambdaExpression lambda) { - if (lambda.Body.NodeType == ExpressionType.MemberAccess) - { - var m = lambda.Body as MemberExpression; - - if (m != null && m.Expression != null) + if (lambda.Body.NodeType == ExpressionType.MemberAccess && + lambda.Body is MemberExpression memberExpression && memberExpression.Expression != null) { //This deals with members that are boolean (i.e. x => IsTrashed ) - var r = VisitMemberAccess(m); + var result = VisitMemberAccess(memberExpression); SqlParameters.Add(true); - return Visited ? string.Empty : string.Format("{0} = @{1}", r, SqlParameters.Count - 1); + return Visited ? string.Empty : $"{result} = @{SqlParameters.Count - 1}"; } - } return Visit(lambda.Body); } @@ -248,21 +241,10 @@ namespace Umbraco.Core.Persistence.Querying { case "MOD": case "COALESCE": - //don't execute if compiled - if (Visited == false) - { - return string.Format("{0}({1},{2})", operand, left, right); - } - //already compiled, return - return string.Empty; + return Visited ? string.Empty : $"{operand}({left},{right})"; + default: - //don't execute if compiled - if (Visited == false) - { - return string.Concat("(", left, " ", operand, " ", right, ")"); - } - //already compiled, return - return string.Empty; + return Visited ? string.Empty : $"({left} {operand} {right})"; } } @@ -284,10 +266,10 @@ namespace Umbraco.Core.Persistence.Querying return list; } - protected virtual string VisitNew(NewExpression nex) + protected virtual string VisitNew(NewExpression newExpression) { // TODO : check ! - var member = Expression.Convert(nex, typeof(object)); + var member = Expression.Convert(newExpression, typeof(object)); var lambda = Expression.Lambda>(member); try { @@ -295,20 +277,16 @@ namespace Umbraco.Core.Persistence.Querying var o = getter(); SqlParameters.Add(o); + return Visited ? string.Empty : $"@{SqlParameters.Count - 1}"; } catch (InvalidOperationException) { - if (Visited) return string.Empty; + if (Visited) + return string.Empty; - var exprs = VisitExpressionList(nex.Arguments); - var r = new StringBuilder(); - foreach (var e in exprs) - { - if (r.Length > 0) r.Append(","); - r.Append(e); - } - return r.ToString(); + var exprs = VisitExpressionList(newExpression.Arguments); + return string.Join(",", exprs); } } @@ -323,6 +301,7 @@ namespace Umbraco.Core.Persistence.Querying return "null"; SqlParameters.Add(c.Value); + return Visited ? string.Empty : $"@{SqlParameters.Count - 1}"; } @@ -375,27 +354,11 @@ namespace Umbraco.Core.Persistence.Querying protected virtual string VisitNewArray(NewArrayExpression na) { var exprs = VisitExpressionList(na.Expressions); - - //don't execute if compiled - if (Visited == false) - { - var r = new StringBuilder(); - foreach (var e in exprs) - { - r.Append(r.Length > 0 ? "," + e : e); - } - - return r.ToString(); - } - //already compiled, return - return string.Empty; + return Visited ? string.Empty : string.Join(",", exprs); } protected virtual List VisitNewArrayFromExpressionList(NewArrayExpression na) - { - var exprs = VisitExpressionList(na.Expressions); - return exprs; - } + => VisitExpressionList(na.Expressions); protected virtual string BindOperant(ExpressionType e) { @@ -436,50 +399,61 @@ namespace Umbraco.Core.Persistence.Querying protected virtual string VisitMethodCall(MethodCallExpression m) { - //Here's what happens with a MethodCallExpression: - // If a method is called that contains a single argument, - // then m.Object is the object on the left hand side of the method call, example: - // x.Path.StartsWith(content.Path) - // m.Object = x.Path - // and m.Arguments.Length == 1, therefor m.Arguments[0] == content.Path - // If a method is called that contains multiple arguments, then m.Object == null and the - // m.Arguments collection contains the left hand side of the method call, example: - // x.Path.SqlStartsWith(content.Path, TextColumnType.NVarchar) - // m.Object == null - // m.Arguments.Length == 3, therefor, m.Arguments[0] == x.Path, m.Arguments[1] == content.Path, m.Arguments[2] == TextColumnType.NVarchar - // So, we need to cater for these scenarios. + // m.Object is the expression that represent the instance for instance method class, or null for static method calls + // m.Arguments is the collection of expressions that represent arguments of the called method + // m.MethodInfo is the method info for the method to be called - var objectForMethod = m.Object ?? m.Arguments[0]; - var visitedObjectForMethod = Visit(objectForMethod); + // assume that static methods are extension methods (probably not ok) + // and then, the method object is its first argument - get "safe" object + var methodObject = m.Object ?? m.Arguments[0]; + var visitedMethodObject = Visit(methodObject); + // and then, "safe" arguments are what would come after the first arg var methodArgs = m.Object == null - ? m.Arguments.Skip(1).ToArray() - : m.Arguments.ToArray(); + ? new ReadOnlyCollection(m.Arguments.Skip(1).ToList()) + : m.Arguments; switch (m.Method.Name) { case "ToString": - SqlParameters.Add(objectForMethod.ToString()); - //don't execute if compiled - if (Visited == false) - return string.Format("@{0}", SqlParameters.Count - 1); - //already compiled, return - return string.Empty; + SqlParameters.Add(methodObject.ToString()); + return Visited ? string.Empty : $"@{SqlParameters.Count - 1}"; + case "ToUpper": - //don't execute if compiled - if (Visited == false) - return string.Format("upper({0})", visitedObjectForMethod); - //already compiled, return - return string.Empty; + return Visited ? string.Empty : $"upper({visitedMethodObject})"; + case "ToLower": - //don't execute if compiled - if (Visited == false) - return string.Format("lower({0})", visitedObjectForMethod); - //already compiled, return - return string.Empty; + return Visited ? string.Empty : $"lower({visitedMethodObject})"; + + case "Contains": + // for 'Contains', it can either be the string.Contains(string) method, or a collection Contains + // method, which would then need to become a SQL IN clause - but beware that string is + // an enumerable of char, and string.Contains(char) is an extension method - but NOT an SQL IN + + var isCollectionContains = + ( + m.Object == null && // static (extension?) method + m.Arguments.Count == 2 && // with two args + m.Arguments[0].Type != typeof(string) && // but not for string + TypeHelper.IsTypeAssignableFrom(m.Arguments[0].Type) && // first arg being an enumerable + m.Arguments[1].NodeType == ExpressionType.MemberAccess // second arg being a member access + ) || + ( + m.Object != null && // instance method + TypeHelper.IsTypeAssignableFrom(m.Object.Type) && // of an enumerable + m.Object.Type != typeof(string) && // but not for string + m.Arguments.Count == 1 && // with 1 arg + m.Arguments[0].NodeType == ExpressionType.MemberAccess // arg being a member access + ); + + if (isCollectionContains) + goto case "SqlIn"; + else + goto case "Contains**String"; + case "SqlWildcard": case "StartsWith": case "EndsWith": - case "Contains": + case "Contains**String": // see "Contains" above case "Equals": case "SqlStartsWith": case "SqlEndsWith": @@ -490,18 +464,6 @@ namespace Umbraco.Core.Persistence.Querying case "InvariantContains": case "InvariantEquals": - //special case, if it is 'Contains' and the argumet that Contains is being called on is - //Enumerable and the methodArgs is the actual member access, then it's an SQL IN claus - if (m.Object == null - && m.Arguments[0].Type != typeof(string) - && m.Arguments.Count == 2 - && methodArgs.Length == 1 - && methodArgs[0].NodeType == ExpressionType.MemberAccess - && TypeHelper.IsTypeAssignableFrom(m.Arguments[0].Type)) - { - goto case "SqlIn"; - } - string compareValue; if (methodArgs[0].NodeType != ExpressionType.Constant) @@ -526,7 +488,7 @@ namespace Umbraco.Core.Persistence.Querying //then check if the col type argument has been passed to the current method (this will be the case for methods like // SqlContains and other Sql methods) - if (methodArgs.Length > 1) + if (methodArgs.Count > 1) { var colTypeArg = methodArgs.FirstOrDefault(x => x is ConstantExpression && x.Type == typeof(TextColumnType)); if (colTypeArg != null) @@ -535,7 +497,7 @@ namespace Umbraco.Core.Persistence.Querying } } - return HandleStringComparison(visitedObjectForMethod, compareValue, m.Method.Name, colType); + return HandleStringComparison(visitedMethodObject, compareValue, m.Method.Name, colType); case "Replace": string searchValue; @@ -581,14 +543,10 @@ namespace Umbraco.Core.Persistence.Querying } SqlParameters.Add(RemoveQuote(searchValue)); - SqlParameters.Add(RemoveQuote(replaceValue)); //don't execute if compiled - if (Visited == false) - return string.Format("replace({0}, @{1}, @{2})", visitedObjectForMethod, SqlParameters.Count - 2, SqlParameters.Count - 1); - //already compiled, return - return string.Empty; + return Visited ? string.Empty : $"replace({visitedMethodObject}, @{SqlParameters.Count - 2}, @{SqlParameters.Count - 1})"; //case "Substring": // var startIndex = Int32.Parse(args[0].ToString()) + 1; @@ -624,30 +582,33 @@ namespace Umbraco.Core.Persistence.Querying case "SqlIn": - if (m.Object == null && methodArgs.Length == 1 && methodArgs[0].NodeType == ExpressionType.MemberAccess) + if (methodArgs.Count != 1 || methodArgs[0].NodeType != ExpressionType.MemberAccess) + throw new NotSupportedException("SqlIn must contain the member being accessed."); + + var memberAccess = VisitMemberAccess((MemberExpression) methodArgs[0]); + + var inMember = Expression.Convert(methodObject, typeof(object)); + var inLambda = Expression.Lambda>(inMember); + var inGetter = inLambda.Compile(); + + var inArgs = (IEnumerable) inGetter(); + + var inBuilder = new StringBuilder(); + var inFirst = true; + + inBuilder.Append(memberAccess); + inBuilder.Append(" IN ("); + + foreach (var e in inArgs) { - var memberAccess = VisitMemberAccess((MemberExpression) methodArgs[0]); - - var member = Expression.Convert(m.Arguments[0], typeof(object)); - var lambda = Expression.Lambda>(member); - var getter = lambda.Compile(); - - var inArgs = (IEnumerable)getter(); - - var sIn = new StringBuilder(); - foreach (var e in inArgs) - { - SqlParameters.Add(e); - - sIn.AppendFormat("{0}{1}", - sIn.Length > 0 ? "," : "", - string.Format("@{0}", SqlParameters.Count - 1)); - } - - return string.Format("{0} IN ({1})", memberAccess, sIn); + SqlParameters.Add(e); + if (inFirst) inFirst = false; else inBuilder.Append(","); + inBuilder.Append("@"); + inBuilder.Append(SqlParameters.Count - 1); } - throw new NotSupportedException("SqlIn must contain the member being accessed"); + inBuilder.Append(")"); + return inBuilder.ToString(); //case "Desc": // return string.Format("{0} DESC", r); @@ -706,19 +667,13 @@ namespace Umbraco.Core.Persistence.Querying } public virtual string GetQuotedTableName(string tableName) - { - return Visited ? tableName : string.Format("\"{0}\"", tableName); - } + => GetQuotedName(tableName); public virtual string GetQuotedColumnName(string columnName) - { - return Visited ? columnName : string.Format("\"{0}\"", columnName); - } + => GetQuotedName(columnName); public virtual string GetQuotedName(string name) - { - return Visited ? name : string.Format("\"{0}\"", name); - } + => Visited ? name : "\"" + name + "\""; protected string HandleStringComparison(string col, string val, string verb, TextColumnType columnType) { @@ -726,115 +681,38 @@ namespace Umbraco.Core.Persistence.Querying { case "SqlWildcard": SqlParameters.Add(RemoveQuote(val)); - //don't execute if compiled - if (Visited == false) - return SqlSyntax.GetStringColumnWildcardComparison(col, SqlParameters.Count - 1, columnType); - //already compiled, return - return string.Empty; + return Visited ? string.Empty : SqlSyntax.GetStringColumnWildcardComparison(col, SqlParameters.Count - 1, columnType); + case "Equals": - SqlParameters.Add(RemoveQuote(val)); - //don't execute if compiled - if (Visited == false) - return SqlSyntax.GetStringColumnEqualComparison(col, SqlParameters.Count - 1, columnType); - //already compiled, return - return string.Empty; - case "StartsWith": - SqlParameters.Add(string.Format("{0}{1}", - RemoveQuote(val), - SqlSyntax.GetWildcardPlaceholder())); - //don't execute if compiled - if (Visited == false) - return SqlSyntax.GetStringColumnWildcardComparison(col, SqlParameters.Count - 1, columnType); - //already compiled, return - return string.Empty; - case "EndsWith": - SqlParameters.Add(string.Format("{0}{1}", - SqlSyntax.GetWildcardPlaceholder(), - RemoveQuote(val))); - //don't execute if compiled - if (Visited == false) - return SqlSyntax.GetStringColumnWildcardComparison(col, SqlParameters.Count - 1, columnType); - //already compiled, return - return string.Empty; - case "Contains": - SqlParameters.Add(string.Format("{0}{1}{0}", - SqlSyntax.GetWildcardPlaceholder(), - RemoveQuote(val))); - //don't execute if compiled - if (Visited == false) - return SqlSyntax.GetStringColumnWildcardComparison(col, SqlParameters.Count - 1, columnType); - //already compiled, return - return string.Empty; case "InvariantEquals": case "SqlEquals": - //recurse - return HandleStringComparison(col, val, "Equals", columnType); + SqlParameters.Add(RemoveQuote(val)); + return Visited ? string.Empty : SqlSyntax.GetStringColumnEqualComparison(col, SqlParameters.Count - 1, columnType); + + case "StartsWith": case "InvariantStartsWith": case "SqlStartsWith": - //recurse - return HandleStringComparison(col, val, "StartsWith", columnType); + SqlParameters.Add(RemoveQuote(val) + SqlSyntax.GetWildcardPlaceholder()); + return Visited ? string.Empty : SqlSyntax.GetStringColumnWildcardComparison(col, SqlParameters.Count - 1, columnType); + + case "EndsWith": case "InvariantEndsWith": case "SqlEndsWith": - //recurse - return HandleStringComparison(col, val, "EndsWith", columnType); + SqlParameters.Add(SqlSyntax.GetWildcardPlaceholder() + RemoveQuote(val)); + return Visited ? string.Empty : SqlSyntax.GetStringColumnWildcardComparison(col, SqlParameters.Count - 1, columnType); + + case "Contains": case "InvariantContains": case "SqlContains": - //recurse - return HandleStringComparison(col, val, "Contains", columnType); + var wildcardPlaceholder = SqlSyntax.GetWildcardPlaceholder(); + SqlParameters.Add(wildcardPlaceholder + RemoveQuote(val) + wildcardPlaceholder); + return Visited ? string.Empty : SqlSyntax.GetStringColumnWildcardComparison(col, SqlParameters.Count - 1, columnType); + default: - throw new ArgumentOutOfRangeException("verb"); + throw new ArgumentOutOfRangeException(nameof(verb)); } } - //public virtual string GetQuotedValue(object value, Type fieldType, Func escapeCallback = null, Func shouldQuoteCallback = null) - //{ - // if (value == null) return "NULL"; - - // if (escapeCallback == null) - // { - // escapeCallback = EscapeParam; - // } - // if (shouldQuoteCallback == null) - // { - // shouldQuoteCallback = ShouldQuoteValue; - // } - - // if (!fieldType.UnderlyingSystemType.IsValueType && fieldType != typeof(string)) - // { - // //if (TypeSerializer.CanCreateFromString(fieldType)) - // //{ - // // return "'" + escapeCallback(TypeSerializer.SerializeToString(value)) + "'"; - // //} - - // throw new NotSupportedException( - // string.Format("Property of type: {0} is not supported", fieldType.FullName)); - // } - - // if (fieldType == typeof(int)) - // return ((int)value).ToString(CultureInfo.InvariantCulture); - - // if (fieldType == typeof(float)) - // return ((float)value).ToString(CultureInfo.InvariantCulture); - - // if (fieldType == typeof(double)) - // return ((double)value).ToString(CultureInfo.InvariantCulture); - - // if (fieldType == typeof(decimal)) - // return ((decimal)value).ToString(CultureInfo.InvariantCulture); - - // if (fieldType == typeof(DateTime)) - // { - // return "'" + escapeCallback(((DateTime)value).ToIsoString()) + "'"; - // } - - // if (fieldType == typeof(bool)) - // return ((bool)value) ? Convert.ToString(1, CultureInfo.InvariantCulture) : Convert.ToString(0, CultureInfo.InvariantCulture); - - // return shouldQuoteCallback(fieldType) - // ? "'" + escapeCallback(value) + "'" - // : value.ToString(); - //} - public virtual string EscapeParam(object paramValue, ISqlSyntaxProvider sqlSyntax) { return paramValue == null @@ -842,34 +720,14 @@ namespace Umbraco.Core.Persistence.Querying : sqlSyntax.EscapeString(paramValue.ToString()); } - public virtual bool ShouldQuoteValue(Type fieldType) - { - return true; - } - protected virtual string RemoveQuote(string exp) { - if ((exp.StartsWith("\"") || exp.StartsWith("`") || exp.StartsWith("'")) - && - (exp.EndsWith("\"") || exp.EndsWith("`") || exp.EndsWith("'"))) - { - exp = exp.Remove(0, 1); - exp = exp.Remove(exp.Length - 1, 1); - } - return exp; + if (exp.IsNullOrWhiteSpace()) return exp; + + var c = exp[0]; + return (c == '"' || c == '`' || c == '\'') && exp[exp.Length - 1] == c + ? exp.Substring(1, exp.Length - 2) + : exp; } - - //protected virtual string RemoveQuoteFromAlias(string expression) - //{ - - // if ((expression.StartsWith("\"") || expression.StartsWith("`") || expression.StartsWith("'")) - // && - // (expression.EndsWith("\"") || expression.EndsWith("`") || expression.EndsWith("'"))) - // { - // expression = expression.Remove(0, 1); - // expression = expression.Remove(expression.Length - 1, 1); - // } - // return expression; - //} } } diff --git a/src/Umbraco.Core/Persistence/Repositories/IContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IContentRepository.cs index f7341d112b..217719e144 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IContentRepository.cs @@ -19,6 +19,12 @@ namespace Umbraco.Core.Persistence.Repositories /// Current version is first, and then versions are ordered with most recent first. IEnumerable GetAllVersions(int nodeId); + /// + /// Gets versions. + /// + /// Current version is first, and then versions are ordered with most recent first. + IEnumerable GetAllVersionsSlim(int nodeId, int skip, int take); + /// /// Gets version identifiers. /// diff --git a/src/Umbraco.Core/Persistence/Repositories/IContentTypeRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/IContentTypeRepositoryBase.cs index 3bb1ac38ca..cc9b86c56b 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IContentTypeRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IContentTypeRepositoryBase.cs @@ -11,13 +11,6 @@ namespace Umbraco.Core.Persistence.Repositories TItem Get(string alias); IEnumerable> Move(TItem moving, EntityContainer container); - /// - /// Returns the content types that are direct compositions of the content type - /// - /// The content type id - /// - IEnumerable GetTypesDirectlyComposedOf(int id); - /// /// Derives a unique alias from an existing alias. /// diff --git a/src/Umbraco.Core/Persistence/Repositories/IDocumentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IDocumentRepository.cs index a3fb50149d..fc5382499f 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IDocumentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IDocumentRepository.cs @@ -7,6 +7,29 @@ namespace Umbraco.Core.Persistence.Repositories { public interface IDocumentRepository : IContentRepository, IReadRepository { + /// + /// Clears the publishing schedule for all entries having an a date before (lower than, or equal to) a specified date. + /// + void ClearSchedule(DateTime date); + + /// + /// Gets objects having an expiration date before (lower than, or equal to) a specified date. + /// + /// + /// The content returned from this method may be culture variant, in which case the resulting should be queried + /// for which culture(s) have been scheduled. + /// + IEnumerable GetContentForExpiration(DateTime date); + + /// + /// Gets objects having a release date before (lower than, or equal to) a specified date. + /// + /// + /// The content returned from this method may be culture variant, in which case the resulting should be queried + /// for which culture(s) have been scheduled. + /// + IEnumerable GetContentForRelease(DateTime date); + /// /// Get the count of published items /// diff --git a/src/Umbraco.Core/Persistence/Repositories/ILanguageRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ILanguageRepository.cs index b86898f97a..fbcbf13651 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ILanguageRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ILanguageRepository.cs @@ -12,7 +12,7 @@ namespace Umbraco.Core.Persistence.Repositories /// /// This can be optimized and bypass all deep cloning. /// - int? GetIdByIsoCode(string isoCode); + int? GetIdByIsoCode(string isoCode, bool throwOnNotFound = true); /// /// Gets a language ISO code from its identifier. @@ -20,7 +20,7 @@ namespace Umbraco.Core.Persistence.Repositories /// /// This can be optimized and bypass all deep cloning. /// - string GetIsoCodeById(int? id); + string GetIsoCodeById(int? id, bool throwOnNotFound = true); /// /// Gets the default language ISO code. diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/AuditRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/AuditRepository.cs index 5d386d9cb4..6c61fe7ad5 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/AuditRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/AuditRepository.cs @@ -28,20 +28,24 @@ namespace Umbraco.Core.Persistence.Repositories.Implement Datestamp = DateTime.Now, Header = entity.AuditType.ToString(), NodeId = entity.Id, - UserId = entity.UserId + UserId = entity.UserId, + EntityType = entity.EntityType, + Parameters = entity.Parameters }); } protected override void PersistUpdatedItem(IAuditItem entity) { - // wtf?! inserting when updating?! + // inserting when updating because we never update a log entry, perhaps this should throw? Database.Insert(new LogDto { Comment = entity.Comment, Datestamp = DateTime.Now, Header = entity.AuditType.ToString(), NodeId = entity.Id, - UserId = entity.UserId + UserId = entity.UserId, + EntityType = entity.EntityType, + Parameters = entity.Parameters }); } @@ -53,7 +57,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var dto = Database.First(sql); return dto == null ? null - : new AuditItem(dto.NodeId, dto.Comment, Enum.Parse(dto.Header), dto.UserId ?? Constants.Security.UnknownUserId); + : new AuditItem(dto.NodeId, Enum.Parse(dto.Header), dto.UserId ?? Constants.Security.UnknownUserId, dto.EntityType, dto.Comment, dto.Parameters); } protected override IEnumerable PerformGetAll(params int[] ids) @@ -69,7 +73,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var dtos = Database.Fetch(sql); - return dtos.Select(x => new AuditItem(x.NodeId, x.Comment, Enum.Parse(x.Header), x.UserId ?? Constants.Security.UnknownUserId)).ToArray(); + return dtos.Select(x => new AuditItem(x.NodeId, Enum.Parse(x.Header), x.UserId ?? Constants.Security.UnknownUserId, x.EntityType, x.Comment, x.Parameters)).ToList(); } protected override Sql GetBaseQuery(bool isCount) @@ -160,10 +164,10 @@ namespace Umbraco.Core.Persistence.Repositories.Implement totalRecords = page.TotalItems; var items = page.Items.Select( - dto => new AuditItem(dto.Id, dto.Comment, Enum.ParseOrNull(dto.Header) ?? AuditType.Custom, dto.UserId ?? Constants.Security.UnknownUserId)).ToArray(); + dto => new AuditItem(dto.Id, Enum.ParseOrNull(dto.Header) ?? AuditType.Custom, dto.UserId ?? Constants.Security.UnknownUserId, dto.EntityType, dto.Comment, dto.Parameters)).ToList(); // map the DateStamp - for (var i = 0; i < items.Length; i++) + for (var i = 0; i < items.Count; i++) items[i].CreateDate = page.Items[i].Datestamp; return items; diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs index c258a76b30..58f58c3d84 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs @@ -1,5 +1,4 @@ using System; -using System.Collections; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; @@ -10,7 +9,6 @@ using Umbraco.Core.Composing; using Umbraco.Core.Events; using Umbraco.Core.Logging; using Umbraco.Core.Models; -using Umbraco.Core.Models.Editors; using Umbraco.Core.Models.Entities; using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Persistence.Dtos; @@ -19,7 +17,6 @@ using Umbraco.Core.Persistence.Querying; using Umbraco.Core.PropertyEditors; using Umbraco.Core.Scoping; using Umbraco.Core.Services; -using Umbraco.Core.Services.Implement; using static Umbraco.Core.Persistence.NPocoSqlExtensions.Statics; namespace Umbraco.Core.Persistence.Repositories.Implement @@ -56,6 +53,10 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // gets all versions, current first public abstract IEnumerable GetAllVersions(int nodeId); + // gets all versions, current first + public virtual IEnumerable GetAllVersionsSlim(int nodeId, int skip, int take) + => GetAllVersions(nodeId).Skip(skip).Take(take); + // gets all version ids, current first public virtual IEnumerable GetVersionIds(int nodeId, int maxRows) { @@ -255,8 +256,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // if we do not do this then we end up with issues where we are ordering by a field that has duplicate values (i.e. the 'text' column // is empty for many nodes) - see: http://issues.umbraco.org/issue/U4-8831 - var dbfield = GetQuotedFieldName("umbracoNode", "id"); - (dbfield, _) = SqlContext.Visit(x => x.NodeId); // fixme?! + var (dbfield, _) = SqlContext.VisitDto(x => x.NodeId); if (ordering.IsCustomField || !ordering.OrderBy.InvariantEquals("id")) { psql.OrderBy(GetAliasedField(dbfield, sql)); // fixme why aliased? @@ -265,6 +265,19 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // create prepared sql // ensure it's single-line as NPoco PagingHelper has issues with multi-lines psql = Sql(psql.SQL.ToSingleLine(), psql.Arguments); + + // replace the magic culture parameter (see DocumentRepository.GetBaseQuery()) + if (!ordering.Culture.IsNullOrWhiteSpace()) + { + for (var i = 0; i < psql.Arguments.Length; i++) + { + if (psql.Arguments[i] is string s && s == "[[[ISOCODE]]]") + { + psql.Arguments[i] = ordering.Culture; + break; + } + } + } return psql; } @@ -342,20 +355,11 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (ordering.Culture.IsNullOrWhiteSpace()) return GetAliasedField(SqlSyntax.GetFieldName(x => x.Text), sql); - // culture = must work on variant name ?? invariant name - // insert proper join and return coalesced ordering field - - var joins = Sql() - .LeftJoin(nested => - nested.InnerJoin("lang").On((ccv, lang) => ccv.LanguageId == lang.Id && lang.IsoCode == ordering.Culture, "ccv", "lang"), "ccv") - .On((version, ccv) => version.Id == ccv.VersionId, aliasRight: "ccv"); - - // see notes in ApplyOrdering: the field MUST be selected + aliased - sql = Sql(InsertBefore(sql, "FROM", ", " + SqlContext.Visit((ccv, node) => ccv.Name ?? node.Text, "ccv").Sql + " AS ordering "), sql.Arguments); - - sql = InsertJoins(sql, joins); - - return "ordering"; + // "variantName" alias is defined in DocumentRepository.GetBaseQuery + // fixme - what if it is NOT a document but a ... media or whatever? + // previously, we inserted the join+select *here* so we were sure to have it, + // but now that's not the case anymore! + return "variantName"; } // previously, we'd accept anything and just sanitize it - not anymore diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepository.cs index aa61383f85..4bec3160a7 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepository.cs @@ -67,7 +67,6 @@ namespace Umbraco.Core.Persistence.Repositories.Implement return ContentTypeQueryMapper.GetContentTypes(Database, SqlSyntax, IsPublishing, this, _templateRepository); } - protected override IEnumerable PerformGetAll(params Guid[] ids) { // use the underlying GetAll which will force cache all content types diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs index 3f1ea3116e..3184c69dfe 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs @@ -119,7 +119,6 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected void PersistNewBaseContentType(IContentTypeComposition entity) { - var dto = ContentTypeFactory.BuildContentTypeDto(entity); //Cannot add a duplicate content type type @@ -234,7 +233,6 @@ AND umbracoNode.nodeObjectType = @objectType", protected void PersistUpdatedBaseContentType(IContentTypeComposition entity) { - var dto = ContentTypeFactory.BuildContentTypeDto(entity); // ensure the alias is not used already @@ -270,8 +268,8 @@ AND umbracoNode.id <> @id", // 1. Find content based on the current ContentType: entity.Id // 2. Find all PropertyTypes on the ContentType that was removed - tracked id (key) // 3. Remove properties based on property types from the removed content type where the content ids correspond to those found in step one - var compositionBase = entity as ContentTypeCompositionBase; - if (compositionBase != null && compositionBase.RemovedContentTypeKeyTracker != null && + if (entity is ContentTypeCompositionBase compositionBase && + compositionBase.RemovedContentTypeKeyTracker != null && compositionBase.RemovedContentTypeKeyTracker.Any()) { //TODO: Could we do the below with bulk SQL statements instead of looking everything up and then manipulating? @@ -314,7 +312,7 @@ AND umbracoNode.id <> @id", } } - // delete the allowed content type entries before re-inserting the collectino of allowed content types + // delete the allowed content type entries before re-inserting the collection of allowed content types Database.Delete("WHERE Id = @Id", new { entity.Id }); foreach (var allowedContentType in entity.AllowedContentTypes) { @@ -326,9 +324,11 @@ AND umbracoNode.id <> @id", }); } - // delete property types - // ... by excepting entries from db with entries from collections - if (entity.IsPropertyDirty("PropertyTypes") || entity.PropertyTypes.Any(x => x.IsDirty())) + // Delete property types ... by excepting entries from db with entries from collections. + // We check if the entity's own PropertyTypes has been modified and then also check + // any of the property groups PropertyTypes has been modified. + // This specifically tells us if any property type collections have changed. + if (entity.IsPropertyDirty("PropertyTypes") || entity.PropertyGroups.Any(x => x.IsPropertyDirty("PropertyTypes"))) { var dbPropertyTypes = Database.Fetch("WHERE contentTypeId = @Id", new { entity.Id }); var dbPropertyTypeAlias = dbPropertyTypes.Select(x => x.Id); @@ -338,10 +338,11 @@ AND umbracoNode.id <> @id", DeletePropertyType(entity.Id, item); } - // delete tabs - // ... by excepting entries from db with entries from collections + // Delete tabs ... by excepting entries from db with entries from collections. + // We check if the entity's own PropertyGroups has been modified. + // This specifically tells us if the property group collections have changed. List orphanPropertyTypeIds = null; - if (entity.IsPropertyDirty("PropertyGroups") || entity.PropertyGroups.Any(x => x.IsDirty())) + if (entity.IsPropertyDirty("PropertyGroups")) { // todo // we used to try to propagate tabs renaming downstream, relying on ParentId, but @@ -406,40 +407,34 @@ AND umbracoNode.id <> @id", } //check if the content type variation has been changed - var ctVariationChanging = entity.IsPropertyDirty("Variations"); - if (ctVariationChanging) + var contentTypeVariationDirty = entity.IsPropertyDirty("Variations"); + var oldContentTypeVariation = (ContentVariation) dtoPk.Variations; + var newContentTypeVariation = entity.Variations; + var contentTypeVariationChanging = contentTypeVariationDirty && oldContentTypeVariation != newContentTypeVariation; + if (contentTypeVariationChanging) { - //we've already looked up the previous version of the content type so we know it's previous variation state - MoveVariantData(entity, (ContentVariation)dtoPk.Variations, entity.Variations); + MoveContentTypeVariantData(entity, oldContentTypeVariation, newContentTypeVariation); Clear301Redirects(entity); ClearScheduledPublishing(entity); - } + } - //track any content type/property types that are changing variation which will require content updates - var propertyTypeVariationChanges = new Dictionary(); + // collect property types that have a dirty variation + List propertyTypeVariationDirty = null; - // insert or update properties - // all of them, no-group and in-groups + // note: this only deals with *local* property types, we're dealing w/compositions later below foreach (var propertyType in entity.PropertyTypes) { - //if the content type variation isn't changing track if any property type is changing - if (!ctVariationChanging) + if (contentTypeVariationChanging) { - if (propertyType.IsPropertyDirty("Variations")) + // content type is changing + switch (newContentTypeVariation) { - propertyTypeVariationChanges[propertyType.Id] = propertyType.Variations; - } - } - else - { - switch(entity.Variations) - { - case ContentVariation.Nothing: - //if the content type is changing to Nothing, then all property type's must change to nothing + case ContentVariation.Nothing: // changing to Nothing + // all property types must change to Nothing propertyType.Variations = ContentVariation.Nothing; break; - case ContentVariation.Culture: - //we don't need to modify the property type in this case + case ContentVariation.Culture: // changing to Culture + // all property types can remain Nothing break; case ContentVariation.CultureAndSegment: case ContentVariation.Segment: @@ -448,15 +443,65 @@ AND umbracoNode.id <> @id", } } - var groupId = propertyType.PropertyGroupId?.Value ?? default(int); + // then, track each property individually + if (propertyType.IsPropertyDirty("Variations")) + { + // allocate the list only when needed + if (propertyTypeVariationDirty == null) + propertyTypeVariationDirty = new List(); + + propertyTypeVariationDirty.Add(propertyType); + } + } + + // figure out dirty property types that have actually changed + // before we insert or update properties, so we can read the old variations + var propertyTypeVariationChanges = propertyTypeVariationDirty != null + ? GetPropertyVariationChanges(propertyTypeVariationDirty) + : null; + + // deal with composition property types + // add changes for property types obtained via composition, which change due + // to this content type variations change + if (contentTypeVariationChanging) + { + // must use RawComposedPropertyTypes here: only those types that are obtained + // via composition, with their original variations (ie not filtered by this + // content type variations - we need this true value to make decisions. + + foreach (var propertyType in ((ContentTypeCompositionBase) entity).RawComposedPropertyTypes) + { + if (propertyType.VariesBySegment() || newContentTypeVariation.VariesBySegment()) + throw new NotSupportedException(); // TODO: support this + + if (propertyType.Variations == ContentVariation.Culture) + { + if (propertyTypeVariationChanges == null) + propertyTypeVariationChanges = new Dictionary(); + + // if content type moves to Culture, property type becomes Culture here again + // if content type moves to Nothing, property type becomes Nothing here + if (newContentTypeVariation == ContentVariation.Culture) + propertyTypeVariationChanges[propertyType.Id] = (ContentVariation.Nothing, ContentVariation.Culture); + else if (newContentTypeVariation == ContentVariation.Nothing) + propertyTypeVariationChanges[propertyType.Id] = (ContentVariation.Culture, ContentVariation.Nothing); + } + } + } + + // insert or update properties + // all of them, no-group and in-groups + foreach (var propertyType in entity.PropertyTypes) + { // if the Id of the DataType is not set, we resolve it from the db by its PropertyEditorAlias - if (propertyType.DataTypeId == 0 || propertyType.DataTypeId == default(int)) + if (propertyType.DataTypeId == 0 || propertyType.DataTypeId == default) AssignDataTypeFromPropertyEditor(propertyType); // validate the alias ValidateAlias(propertyType); // insert or update property + var groupId = propertyType.PropertyGroupId?.Value ?? default; var propertyTypeDto = PropertyGroupFactory.BuildPropertyTypeDto(groupId, propertyType, entity.Id); var typeId = propertyType.HasIdentity ? Database.Update(propertyTypeDto) @@ -467,31 +512,22 @@ AND umbracoNode.id <> @id", typeId = propertyType.Id; // not an orphan anymore - if (orphanPropertyTypeIds != null) - orphanPropertyTypeIds.Remove(typeId); + orphanPropertyTypeIds?.Remove(typeId); } - //check if any property types were changing variation - if (propertyTypeVariationChanges.Count > 0) - { - var changes = new Dictionary(); + // must restrict property data changes to impacted content types - if changing a composing + // type, some composed types (those that do not vary) are not impacted and should be left + // unchanged + // + // getting 'all' from the cache policy is prone to race conditions - fast but dangerous + //var all = ((FullDataSetRepositoryCachePolicy)CachePolicy).GetAllCached(PerformGetAll); + var all = PerformGetAll(); - //now get the current property type variations for the changed ones so that we know which variation they - //are going from and to - var from = Database.Dictionary(Sql() - .Select(x => x.Id, x => x.Variations) - .From() - .WhereIn(x => x.Id, propertyTypeVariationChanges.Keys)); - - foreach (var f in from) - { - changes[f.Key] = (propertyTypeVariationChanges[f.Key], (ContentVariation)f.Value); - } - - //perform the move - MoveVariantData(changes); - } + var impacted = GetImpactedContentTypes(entity, all); + // if some property types have actually changed, move their variant data + if (propertyTypeVariationChanges != null) + MovePropertyTypeVariantData(propertyTypeVariationChanges, impacted); // deal with orphan properties: those that were in a deleted tab, // and have not been re-mapped to another tab or to 'generic properties' @@ -500,6 +536,77 @@ AND umbracoNode.id <> @id", DeletePropertyType(entity.Id, id); } + private IEnumerable GetImpactedContentTypes(IContentTypeComposition contentType, IEnumerable all) + { + var impact = new List(); + var set = new List { contentType }; + + var tree = new Dictionary>(); + foreach (var x in all) + foreach (var y in x.ContentTypeComposition) + { + if (!tree.TryGetValue(y.Id, out var list)) + list = tree[y.Id] = new List(); + list.Add(x); + } + + var nset = new List(); + do + { + impact.AddRange(set); + + foreach (var x in set) + { + if (!tree.TryGetValue(x.Id, out var list)) continue; + nset.AddRange(list.Where(y => y.VariesByCulture())); + } + + set = nset; + nset = new List(); + } while (set.Count > 0); + + return impact; + } + + // gets property types that have actually changed, and the corresponding changes + // returns null if no property type has actually changed + private Dictionary GetPropertyVariationChanges(IEnumerable propertyTypes) + { + var propertyTypesL = propertyTypes.ToList(); + + // select the current variations (before the change) from database + var selectCurrentVariations = Sql() + .Select(x => x.Id, x => x.Variations) + .From() + .WhereIn(x => x.Id, propertyTypesL.Select(x => x.Id)); + + var oldVariations = Database.Dictionary(selectCurrentVariations); + + // build a dictionary of actual changes + Dictionary changes = null; + + foreach (var propertyType in propertyTypesL) + { + // new property type, ignore + if (!oldVariations.TryGetValue(propertyType.Id, out var oldVariationB)) + continue; + var oldVariation = (ContentVariation) oldVariationB; // NPoco cannot fetch directly + + // only those property types that *actually* changed + var newVariation = propertyType.Variations; + if (oldVariation == newVariation) + continue; + + // allocate the dictionary only when needed + if (changes == null) + changes = new Dictionary(); + + changes[propertyType.Id] = (oldVariation, newVariation); + } + + return changes; + } + /// /// Clear any redirects associated with content for a content type /// @@ -526,28 +633,39 @@ AND umbracoNode.id <> @id", } /// - /// Moves variant data for property type changes + /// Gets the default language identifier. /// - /// - private void MoveVariantData(IDictionary propertyTypeChanges) + private int GetDefaultLanguageId() { - var defaultLangId = Database.First(Sql().Select(x => x.Id).From().Where(x => x.IsDefault)); + var selectDefaultLanguageId = Sql() + .Select(x => x.Id) + .From() + .Where(x => x.IsDefault); + + return Database.First(selectDefaultLanguageId); + } + + /// + /// Moves variant data for property type variation changes. + /// + private void MovePropertyTypeVariantData(IDictionary propertyTypeChanges, IEnumerable impacted) + { + var defaultLanguageId = GetDefaultLanguageId(); + var impactedL = impacted.Select(x => x.Id).ToList(); //Group by the "To" variation so we can bulk update in the correct batches - foreach(var g in propertyTypeChanges.GroupBy(x => x.Value.Item2)) + foreach(var grouping in propertyTypeChanges.GroupBy(x => x.Value.ToVariation)) { - var propertyTypeIds = g.Select(s => s.Key).ToList(); + var propertyTypeIds = grouping.Select(x => x.Key).ToList(); + var toVariation = grouping.Key; - //the ContentVariation that the data is moving "To" - var toVariantType = g.Key; - - switch(toVariantType) + switch (toVariation) { case ContentVariation.Culture: - MovePropertyDataToVariantCulture(defaultLangId, propertyTypeIds: propertyTypeIds); + CopyPropertyData(null, defaultLanguageId, propertyTypeIds, impactedL); break; case ContentVariation.Nothing: - MovePropertyDataToVariantNothing(defaultLangId, propertyTypeIds: propertyTypeIds); + CopyPropertyData(defaultLanguageId, null, propertyTypeIds, impactedL); break; case ContentVariation.CultureAndSegment: case ContentVariation.Segment: @@ -558,24 +676,17 @@ AND umbracoNode.id <> @id", } /// - /// Moves variant data for a content type variation change + /// Moves variant data for a content type variation change. /// - /// - /// - /// - private void MoveVariantData(IContentTypeComposition contentType, ContentVariation from, ContentVariation to) + private void MoveContentTypeVariantData(IContentTypeComposition contentType, ContentVariation fromVariation, ContentVariation toVariation) { - var defaultLangId = Database.First(Sql().Select(x => x.Id).From().Where(x => x.IsDefault)); + var defaultLanguageId = GetDefaultLanguageId(); - var sqlPropertyTypeIds = Sql().Select(x => x.Id).From().Where(x => x.ContentTypeId == contentType.Id); - switch (to) + switch (toVariation) { case ContentVariation.Culture: - //move the property data - MovePropertyDataToVariantCulture(defaultLangId, sqlPropertyTypeIds: sqlPropertyTypeIds); - - //now we need to move the names + //move the names //first clear out any existing names that might already exists under the default lang //there's 2x tables to update @@ -585,10 +696,11 @@ AND umbracoNode.id <> @id", .InnerJoin().On(x => x.Id, x => x.VersionId) .InnerJoin().On(x => x.NodeId, x => x.NodeId) .Where(x => x.ContentTypeId == contentType.Id) - .Where(x => x.LanguageId == defaultLangId); + .Where(x => x.LanguageId == defaultLanguageId); var sqlDelete = Sql() .Delete() .WhereIn(x => x.Id, sqlSelect); + Database.Execute(sqlDelete); //clear out the documentCultureVariation table @@ -596,10 +708,11 @@ AND umbracoNode.id <> @id", .From() .InnerJoin().On(x => x.NodeId, x => x.NodeId) .Where(x => x.ContentTypeId == contentType.Id) - .Where(x => x.LanguageId == defaultLangId); + .Where(x => x.LanguageId == defaultLanguageId); sqlDelete = Sql() .Delete() .WhereIn(x => x.Id, sqlSelect); + Database.Execute(sqlDelete); //now we need to insert names into these 2 tables based on the invariant data @@ -607,32 +720,31 @@ AND umbracoNode.id <> @id", //insert rows into the versionCultureVariationDto table based on the data from contentVersionDto for the default lang var cols = Sql().Columns(x => x.VersionId, x => x.Name, x => x.UpdateUserId, x => x.UpdateDate, x => x.LanguageId); sqlSelect = Sql().Select(x => x.Id, x => x.Text, x => x.UserId, x => x.VersionDate) - .Append($", {defaultLangId}") //default language ID + .Append($", {defaultLanguageId}") //default language ID .From() .InnerJoin().On(x => x.NodeId, x => x.NodeId) .Where(x => x.ContentTypeId == contentType.Id); var sqlInsert = Sql($"INSERT INTO {ContentVersionCultureVariationDto.TableName} ({cols})").Append(sqlSelect); + Database.Execute(sqlInsert); //insert rows into the documentCultureVariation table cols = Sql().Columns(x => x.NodeId, x => x.Edited, x => x.Published, x => x.Name, x => x.Available, x => x.LanguageId); sqlSelect = Sql().Select(x => x.NodeId, x => x.Edited, x => x.Published) .AndSelect(x => x.Text) - .Append($", 1, {defaultLangId}") //make Available + default language ID + .Append($", 1, {defaultLanguageId}") //make Available + default language ID .From() .InnerJoin().On(x => x.NodeId, x => x.NodeId) .InnerJoin().On(x => x.NodeId, x => x.NodeId) .Where(x => x.ContentTypeId == contentType.Id); sqlInsert = Sql($"INSERT INTO {DocumentCultureVariationDto.TableName} ({cols})").Append(sqlSelect); + Database.Execute(sqlInsert); break; case ContentVariation.Nothing: - //move the property data - MovePropertyDataToVariantNothing(defaultLangId, sqlPropertyTypeIds: sqlPropertyTypeIds); - - //we dont need to move the names! this is because we always keep the invariant names with the name of the default language. + //we don't need to move the names! this is because we always keep the invariant names with the name of the default language. //however, if we were to move names, we could do this: BUT this doesn't work with SQLCE, for that we'd have to update row by row :( // if we want these SQL statements back, look into GIT history @@ -646,73 +758,102 @@ AND umbracoNode.id <> @id", } /// - /// This will move all property data from variant to invariant + /// Copies property data from one language to another. /// - /// - /// Optional list of property type ids of the properties to be updated - /// Optional SQL statement used for the sub-query to select the properties type ids for the properties to be updated - private void MovePropertyDataToVariantNothing(int defaultLangId, IReadOnlyCollection propertyTypeIds = null, Sql sqlPropertyTypeIds = null) + /// The source language (can be null ie invariant). + /// The target language (can be null ie invariant) + /// The property type identifiers. + /// The content type identifiers. + private void CopyPropertyData(int? sourceLanguageId, int? targetLanguageId, IReadOnlyCollection propertyTypeIds, IReadOnlyCollection contentTypeIds = null) { - //first clear out any existing property data that might already exists under the default lang + // fixme - should we batch then? + var whereInArgsCount = propertyTypeIds.Count + (contentTypeIds?.Count ?? 0); + if (whereInArgsCount > 2000) + throw new NotSupportedException("Too many property/content types."); + + //first clear out any existing property data that might already exists under the target language var sqlDelete = Sql() - .Delete() - .Where(x => x.LanguageId == null); - if (sqlPropertyTypeIds != null) - sqlDelete.WhereIn(x => x.PropertyTypeId, sqlPropertyTypeIds); - if (propertyTypeIds != null) - sqlDelete.WhereIn(x => x.PropertyTypeId, propertyTypeIds); + .Delete(); + + // not ok for SqlCe (no JOIN in DELETE) + //if (contentTypeIds != null) + // sqlDelete + // .From() + // .InnerJoin().On((pdata, cversion) => pdata.VersionId == cversion.Id) + // .InnerJoin().On((cversion, c) => cversion.NodeId == c.NodeId); + + Sql inSql = null; + if (contentTypeIds != null) + { + inSql = Sql() + .Select(x => x.Id) + .From() + .InnerJoin().On((cversion, c) => cversion.NodeId == c.NodeId) + .WhereIn(x => x.ContentTypeId, contentTypeIds); + sqlDelete.WhereIn(x => x.VersionId, inSql); + } + + // NPoco cannot turn the clause into IS NULL with a nullable parameter - deal with it + if (targetLanguageId == null) + sqlDelete.Where(x => x.LanguageId == null); + else + sqlDelete.Where(x => x.LanguageId == targetLanguageId); + + sqlDelete + .WhereIn(x => x.PropertyTypeId, propertyTypeIds); + + // see note above, not ok for SqlCe + //if (contentTypeIds != null) + // sqlDelete + // .WhereIn(x => x.ContentTypeId, contentTypeIds); Database.Execute(sqlDelete); - //now insert all property data into the default language that exists under the invariant lang + //now insert all property data into the target language that exists under the source language + var targetLanguageIdS = targetLanguageId.HasValue ? targetLanguageId.ToString() : "NULL"; var cols = Sql().Columns(x => x.VersionId, x => x.PropertyTypeId, x => x.Segment, x => x.IntegerValue, x => x.DecimalValue, x => x.DateValue, x => x.VarcharValue, x => x.TextValue, x => x.LanguageId); var sqlSelectData = Sql().Select(x => x.VersionId, x => x.PropertyTypeId, x => x.Segment, x => x.IntegerValue, x => x.DecimalValue, x => x.DateValue, x => x.VarcharValue, x => x.TextValue) - .Append(", NULL") //null language ID - .From() - .Where(x => x.LanguageId == defaultLangId); - if (sqlPropertyTypeIds != null) - sqlSelectData.WhereIn(x => x.PropertyTypeId, sqlPropertyTypeIds); - if (propertyTypeIds != null) - sqlSelectData.WhereIn(x => x.PropertyTypeId, propertyTypeIds); + .Append(", " + targetLanguageIdS) //default language ID + .From(); + + if (contentTypeIds != null) + sqlSelectData + .InnerJoin().On((pdata, cversion) => pdata.VersionId == cversion.Id) + .InnerJoin().On((cversion, c) => cversion.NodeId == c.NodeId); + + // NPoco cannot turn the clause into IS NULL with a nullable parameter - deal with it + if (sourceLanguageId == null) + sqlSelectData.Where(x => x.LanguageId == null); + else + sqlSelectData.Where(x => x.LanguageId == sourceLanguageId); + + sqlSelectData + .WhereIn(x => x.PropertyTypeId, propertyTypeIds); + + if (contentTypeIds != null) + sqlSelectData + .WhereIn(x => x.ContentTypeId, contentTypeIds); var sqlInsert = Sql($"INSERT INTO {PropertyDataDto.TableName} ({cols})").Append(sqlSelectData); Database.Execute(sqlInsert); - } - /// - /// This will move all property data from invariant to variant - /// - /// - /// Optional list of property type ids of the properties to be updated - /// Optional SQL statement used for the sub-query to select the properties type ids for the properties to be updated - private void MovePropertyDataToVariantCulture(int defaultLangId, IReadOnlyCollection propertyTypeIds = null, Sql sqlPropertyTypeIds = null) - { - //first clear out any existing property data that might already exists under the default lang - var sqlDelete = Sql() - .Delete() - .Where(x => x.LanguageId == defaultLangId); - if (sqlPropertyTypeIds != null) - sqlDelete.WhereIn(x => x.PropertyTypeId, sqlPropertyTypeIds); - if (propertyTypeIds != null) - sqlDelete.WhereIn(x => x.PropertyTypeId, propertyTypeIds); + // when copying from Culture, keep the original values around in case we want to go back + // when copying from Nothing, kill the original values, we don't want them around + if (sourceLanguageId == null) + { + sqlDelete = Sql() + .Delete(); - Database.Execute(sqlDelete); + if (contentTypeIds != null) + sqlDelete.WhereIn(x => x.VersionId, inSql); - //now insert all property data into the default language that exists under the invariant lang - var cols = Sql().Columns(x => x.VersionId, x => x.PropertyTypeId, x => x.Segment, x => x.IntegerValue, x => x.DecimalValue, x => x.DateValue, x => x.VarcharValue, x => x.TextValue, x => x.LanguageId); - var sqlSelectData = Sql().Select(x => x.VersionId, x => x.PropertyTypeId, x => x.Segment, x => x.IntegerValue, x => x.DecimalValue, x => x.DateValue, x => x.VarcharValue, x => x.TextValue) - .Append($", {defaultLangId}") //default language ID - .From() - .Where(x => x.LanguageId == null); - if (sqlPropertyTypeIds != null) - sqlSelectData.WhereIn(x => x.PropertyTypeId, sqlPropertyTypeIds); - if (propertyTypeIds != null) - sqlSelectData.WhereIn(x => x.PropertyTypeId, propertyTypeIds); - - var sqlInsert = Sql($"INSERT INTO {PropertyDataDto.TableName} ({cols})").Append(sqlSelectData); + sqlDelete + .Where(x => x.LanguageId == null) + .WhereIn(x => x.PropertyTypeId, propertyTypeIds); - Database.Execute(sqlInsert); + Database.Execute(sqlDelete); + } } private void DeletePropertyType(int contentTypeId, int propertyTypeId) @@ -848,24 +989,6 @@ AND umbracoNode.id <> @id", } } - /// - public IEnumerable GetTypesDirectlyComposedOf(int id) - { - //fixme - this will probably be more efficient to simply load all content types and do the calculation, see GetWhereCompositionIsUsedInContentTypes - - var sql = Sql() - .SelectAll() - .From() - .InnerJoin() - .On(left => left.NodeId, right => right.ChildId) - .Where(x => x.NodeObjectType == NodeObjectTypeId) - .Where(x => x.ParentId == id); - var dtos = Database.Fetch(sql); - return dtos.Any() - ? GetMany(dtos.DistinctBy(x => x.NodeId).Select(x => x.NodeId).ToArray()) - : Enumerable.Empty(); - } - internal static class ContentTypeQueryMapper { public class AssociatedTemplate diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs index bf41cd1ad1..35496aaba7 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs @@ -83,19 +83,28 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var translator = new SqlTranslator(sqlClause, query); var sql = translator.Translate(); - sql // fixme why? - .OrderBy(x => x.Level) - .OrderBy(x => x.SortOrder); + AddGetByQueryOrderBy(sql); return MapDtosToContent(Database.Fetch(sql)); } + private void AddGetByQueryOrderBy(Sql sql) + { + sql // fixme why - this should be Path + .OrderBy(x => x.Level) + .OrderBy(x => x.SortOrder); + } + protected override Sql GetBaseQuery(QueryType queryType) { return GetBaseQuery(queryType, true); } - protected virtual Sql GetBaseQuery(QueryType queryType, bool current) + // gets the COALESCE expression for variant/invariant name + private string VariantNameSqlExpression + => SqlContext.VisitDto((ccv, node) => ccv.Name ?? node.Text, "ccv").Sql; + + protected Sql GetBaseQuery(QueryType queryType, bool current) { var sql = SqlContext.Sql(); @@ -110,12 +119,15 @@ namespace Umbraco.Core.Persistence.Repositories.Implement case QueryType.Single: case QueryType.Many: sql = sql.Select(r => - r.Select(documentDto => documentDto.ContentDto, r1 => - r1.Select(contentDto => contentDto.NodeDto)) - .Select(documentDto => documentDto.DocumentVersionDto, r1 => - r1.Select(documentVersionDto => documentVersionDto.ContentVersionDto)) - .Select(documentDto => documentDto.PublishedVersionDto, "pdv", r1 => - r1.Select(documentVersionDto => documentVersionDto.ContentVersionDto, "pcv"))); + r.Select(documentDto => documentDto.ContentDto, r1 => + r1.Select(contentDto => contentDto.NodeDto)) + .Select(documentDto => documentDto.DocumentVersionDto, r1 => + r1.Select(documentVersionDto => documentVersionDto.ContentVersionDto)) + .Select(documentDto => documentDto.PublishedVersionDto, "pdv", r1 => + r1.Select(documentVersionDto => documentVersionDto.ContentVersionDto, "pcv"))) + + // select the variant name, coalesce to the invariant name, as "variantName" + .AndSelect(VariantNameSqlExpression + " AS variantName"); break; } @@ -125,13 +137,23 @@ namespace Umbraco.Core.Persistence.Repositories.Implement .InnerJoin().On(left => left.NodeId, right => right.NodeId) // inner join on mandatory edited version - .InnerJoin().On((left, right) => left.NodeId == right.NodeId) - .InnerJoin().On((left, right) => left.Id == right.Id) + .InnerJoin() + .On((left, right) => left.NodeId == right.NodeId) + .InnerJoin() + .On((left, right) => left.Id == right.Id) // left join on optional published version .LeftJoin(nested => - nested.InnerJoin("pdv").On((left, right) => left.Id == right.Id && right.Published, "pcv", "pdv"), "pcv") - .On((left, right) => left.NodeId == right.NodeId, aliasRight: "pcv"); + nested.InnerJoin("pdv") + .On((left, right) => left.Id == right.Id && right.Published, "pcv", "pdv"), "pcv") + .On((left, right) => left.NodeId == right.NodeId, aliasRight: "pcv") + + //fixme - should we be joining this when the query type is not single/many? + // left join on optional culture variation + //the magic "[[[ISOCODE]]]" parameter value will be replaced in ContentRepositoryBase.GetPage() by the actual ISO code + .LeftJoin(nested => + nested.InnerJoin("lang").On((ccv, lang) => ccv.LanguageId == lang.Id && lang.IsoCode == "[[[ISOCODE]]]", "ccv", "lang"), "ccv") + .On((version, ccv) => version.Id == ccv.VersionId, aliasRight: "ccv"); sql .Where(x => x.NodeObjectType == NodeObjectTypeId); @@ -166,6 +188,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement { var list = new List { + "DELETE FROM " + Constants.DatabaseSchema.Tables.ContentSchedule + " WHERE nodeId = @id", "DELETE FROM " + Constants.DatabaseSchema.Tables.RedirectUrl + " WHERE contentKey IN (SELECT uniqueId FROM " + Constants.DatabaseSchema.Tables.Node + " WHERE id = @id)", "DELETE FROM " + Constants.DatabaseSchema.Tables.User2NodeNotify + " WHERE nodeId = @id", "DELETE FROM " + Constants.DatabaseSchema.Tables.UserGroup2NodePermission + " WHERE nodeId = @id", @@ -204,6 +227,16 @@ namespace Umbraco.Core.Persistence.Repositories.Implement return MapDtosToContent(Database.Fetch(sql), true); } + public override IEnumerable GetAllVersionsSlim(int nodeId, int skip, int take) + { + var sql = GetBaseQuery(QueryType.Many, false) + .Where(x => x.NodeId == nodeId) + .OrderByDescending(x => x.Current) + .AndByDescending(x => x.VersionDate); + + return MapDtosToContent(Database.Fetch(sql), true, true); + } + public override IContent GetVersion(int versionId) { var sql = GetBaseQuery(QueryType.Single, false) @@ -234,7 +267,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // however, it's not just so we have access to AddingEntity // there are tons of things at the end of the methods, that can only work with a true Content // and basically, the repository requires a Content, not an IContent - var content = (Content) entity; + var content = (Content)entity; content.AddingEntity(); var publishing = content.PublishedState == PublishedState.Publishing; @@ -293,7 +326,6 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var contentVersionDto = dto.DocumentVersionDto.ContentVersionDto; contentVersionDto.NodeId = nodeDto.NodeId; contentVersionDto.Current = !publishing; - contentVersionDto.Text = content.Name; Database.Insert(contentVersionDto); content.VersionId = contentVersionDto.Id; @@ -331,19 +363,26 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // persist the document dto // at that point, when publishing, the entity still has its old Published value - // so we need to explicitely update the dto to persist the correct value + // so we need to explicitly update the dto to persist the correct value if (content.PublishedState == PublishedState.Publishing) dto.Published = true; dto.NodeId = nodeDto.NodeId; content.Edited = dto.Edited = !dto.Published || edited; // if not published, always edited Database.Insert(dto); + //insert the schedule + PersistContentSchedule(content, false); + // persist the variations if (content.ContentType.VariesByCulture()) { + // bump dates to align cultures to version + if (publishing) + content.AdjustDates(contentVersionDto.VersionDate); + // names also impact 'edited' - foreach (var (culture, name) in content.CultureNames) - if (name != content.GetPublishName(culture)) + foreach (var (culture, infos) in content.CultureInfos) + if (infos.Name != content.GetPublishName(culture)) (editedCultures ?? (editedCultures = new HashSet(StringComparer.OrdinalIgnoreCase))).Add(culture); // insert content variations @@ -404,16 +443,17 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // however, it's not just so we have access to AddingEntity // there are tons of things at the end of the methods, that can only work with a true Content // and basically, the repository requires a Content, not an IContent - var content = (Content) entity; + var content = (Content)entity; // check if we need to make any database changes at all - if ((content.PublishedState == PublishedState.Published || content.PublishedState == PublishedState.Unpublished) && !content.IsEntityDirty() && !content.IsAnyUserPropertyDirty()) + if ((content.PublishedState == PublishedState.Published || content.PublishedState == PublishedState.Unpublished) + && !content.IsEntityDirty() && !content.IsAnyUserPropertyDirty()) return; // no change to save, do nothing, don't even update dates // whatever we do, we must check that we are saving the current version // fixme maybe we can just fetch Current (bool) var version = Database.Fetch(SqlContext.Sql().Select().From().Where(x => x.Id == content.VersionId)).FirstOrDefault(); - if (version == null || !version.Current ) + if (version == null || !version.Current) throw new InvalidOperationException("Cannot save a non-current version."); // update @@ -461,7 +501,6 @@ namespace Umbraco.Core.Persistence.Repositories.Implement { documentVersionDto.Published = true; // now published contentVersionDto.Current = false; // no more current - contentVersionDto.Text = content.Name; } Database.Update(contentVersionDto); Database.Update(documentVersionDto); @@ -499,9 +538,13 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (content.ContentType.VariesByCulture()) { + // bump dates to align cultures to version + if (publishing) + content.AdjustDates(contentVersionDto.VersionDate); + // names also impact 'edited' - foreach (var (culture, name) in content.CultureNames) - if (name != content.GetPublishName(culture)) + foreach (var (culture, infos) in content.CultureInfos) + if (infos.Name != content.GetPublishName(culture)) { edited = true; (editedCultures ?? (editedCultures = new HashSet(StringComparer.OrdinalIgnoreCase))).Add(culture); @@ -540,7 +583,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // update the document dto // at that point, when un/publishing, the entity still has its old Published value - // so we need to explicitely update the dto to persist the correct value + // so we need to explicitly update the dto to persist the correct value if (content.PublishedState == PublishedState.Publishing) dto.Published = true; else if (content.PublishedState == PublishedState.Unpublishing) @@ -548,8 +591,12 @@ namespace Umbraco.Core.Persistence.Repositories.Implement content.Edited = dto.Edited = !dto.Published || edited; // if not published, always edited Database.Update(dto); + //update the schedule + if (content.IsPropertyDirty("ContentSchedule")) + PersistContentSchedule(content, true); + // if entity is publishing, update tags, else leave tags there - // means that implicitely unpublished, or trashed, entities *still* have tags in db + // means that implicitly unpublished, or trashed, entities *still* have tags in db if (content.PublishedState == PublishedState.Publishing) SetEntityTags(entity, _tagRepository); @@ -579,8 +626,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement ClearEntityTags(entity, _tagRepository); } - // note re. tags: explicitely unpublished entities have cleared tags, - // but masked or trashed entitites *still* have tags in the db fixme so what? + // note re. tags: explicitly unpublished entities have cleared tags, + // but masked or trashed entities *still* have tags in the db fixme so what? entity.ResetDirtyProperties(); @@ -597,6 +644,35 @@ namespace Umbraco.Core.Persistence.Repositories.Implement //} } + private void PersistContentSchedule(IContent content, bool update) + { + var schedules = ContentBaseFactory.BuildScheduleDto(content, LanguageRepository).ToList(); + + //remove any that no longer exist + if (update) + { + var ids = schedules.Where(x => x.Model.Id != Guid.Empty).Select(x => x.Model.Id).Distinct(); + Database.Execute(Sql() + .Delete() + .Where(x => x.NodeId == content.Id) + .WhereNotIn(x => x.Id, ids)); + } + + //add/update the rest + foreach (var schedule in schedules) + { + if (schedule.Model.Id == Guid.Empty) + { + schedule.Model.Id = schedule.Dto.Id = Guid.NewGuid(); + Database.Insert(schedule.Dto); + } + else + { + Database.Update(schedule.Dto); + } + } + } + protected override void PersistDeletedItem(IContent entity) { // raise event first else potential FK issues @@ -682,16 +758,33 @@ namespace Umbraco.Core.Persistence.Repositories.Implement /// public override IEnumerable GetPage(IQuery query, - long pageIndex, int pageSize, out long totalRecords, + long pageIndex, int pageSize, out long totalRecords, IQuery filter, Ordering ordering) { Sql filterSql = null; + // if we have a filter, map its clauses to an Sql statement if (filter != null) { + // if the clause works on "name", we need to swap the field and use the variantName instead, + // so that querying also works on variant content (for instance when searching a listview). + + // figure out how the "name" field is going to look like - so we can look for it + var nameField = SqlContext.VisitModelField(x => x.Name); + filterSql = Sql(); foreach (var filterClause in filter.GetWhereClauses()) - filterSql.Append($"AND ({filterClause.Item1})", filterClause.Item2); + { + var clauseSql = filterClause.Item1; + var clauseArgs = filterClause.Item2; + + // replace the name field + // we cannot reference an aliased field in a WHERE clause, so have to repeat the expression here + clauseSql = clauseSql.Replace(nameField, VariantNameSqlExpression); + + // append the clause + filterSql.Append($"AND ({clauseSql})", clauseArgs); + } } return GetPage(query, pageIndex, pageSize, out totalRecords, @@ -818,6 +911,49 @@ namespace Umbraco.Core.Persistence.Repositories.Implement #endregion + #region Schedule + + /// + public void ClearSchedule(DateTime date) + { + var sql = Sql().Delete().Where(x => x.Date <= date); + Database.Execute(sql); + } + + /// + public IEnumerable GetContentForRelease(DateTime date) + { + var action = ContentScheduleAction.Release.ToString(); + + var sql = GetBaseQuery(QueryType.Many) + .WhereIn(x => x.NodeId, Sql() + .Select(x => x.NodeId) + .From() + .Where(x => x.Action == action && x.Date <= date)); + + AddGetByQueryOrderBy(sql); + + return MapDtosToContent(Database.Fetch(sql)); + } + + /// + public IEnumerable GetContentForExpiration(DateTime date) + { + var action = ContentScheduleAction.Expire.ToString(); + + var sql = GetBaseQuery(QueryType.Many) + .WhereIn(x => x.NodeId, Sql() + .Select(x => x.NodeId) + .From() + .Where(x => x.Action == action && x.Date <= date)); + + AddGetByQueryOrderBy(sql); + + return MapDtosToContent(Database.Fetch(sql)); + } + + #endregion + protected override string ApplySystemOrdering(ref Sql sql, Ordering ordering) { // note: 'updater' is the user who created the latest draft version, @@ -828,7 +964,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement .InnerJoin("updaterUser").On((version, user) => version.UserId == user.Id, aliasRight: "updaterUser"); // see notes in ApplyOrdering: the field MUST be selected + aliased - sql = Sql(InsertBefore(sql, "FROM", SqlSyntax.GetFieldName(x => x.UserName, "updaterUser") + " AS ordering"), sql.Arguments); + sql = Sql(InsertBefore(sql, "FROM", ", " + SqlSyntax.GetFieldName(x => x.UserName, "updaterUser") + " AS ordering "), sql.Arguments); sql = InsertJoins(sql, joins); @@ -850,10 +986,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // variant: left join may yield NULL or something, and that determines published var joins = Sql() - .InnerJoin("ctype").On((content, contentType) => content.ContentTypeId == contentType.NodeId, aliasRight: "ctype") - .LeftJoin(nested => - nested.InnerJoin("lang").On((ccv, lang) => ccv.LanguageId == lang.Id && lang.IsoCode == ordering.Culture, "ccv", "lang"), "ccv") - .On((pcv, ccv) => pcv.Id == ccv.VersionId, "pcv", "ccv"); // join on *published* content version + .InnerJoin("ctype").On((content, contentType) => content.ContentTypeId == contentType.NodeId, aliasRight: "ctype"); sql = InsertJoins(sql, joins); @@ -873,7 +1006,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement return base.ApplySystemOrdering(ref sql, ordering); } - private IEnumerable MapDtosToContent(List dtos, bool withCache = false) + private IEnumerable MapDtosToContent(List dtos, bool withCache = false, bool slim = false) { var temps = new List>(); var contentTypes = new Dictionary(); @@ -891,7 +1024,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var cached = IsolatedCache.GetCacheItem(RepositoryCacheKeys.GetKey(dto.NodeId)); if (cached != null && cached.VersionId == dto.DocumentVersionDto.ContentVersionDto.Id) { - content[i] = (Content) cached; + content[i] = (Content)cached; continue; } } @@ -906,18 +1039,21 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var c = content[i] = ContentBaseFactory.BuildEntity(dto, contentType); - // need templates - var templateId = dto.DocumentVersionDto.TemplateId; - if (templateId.HasValue && templateId.Value > 0) - templateIds.Add(templateId.Value); - if (dto.Published) + if (!slim) { - templateId = dto.PublishedVersionDto.TemplateId; + // need templates + var templateId = dto.DocumentVersionDto.TemplateId; if (templateId.HasValue && templateId.Value > 0) templateIds.Add(templateId.Value); + if (dto.Published) + { + templateId = dto.PublishedVersionDto.TemplateId; + if (templateId.HasValue && templateId.Value > 0) + templateIds.Add(templateId.Value); + } } - // need properties + // need temps, for properties, templates and variations var versionId = dto.DocumentVersionDto.Id; var publishedVersionId = dto.Published ? dto.PublishedVersionDto.Id : 0; var temp = new TempContent(dto.NodeId, versionId, publishedVersionId, contentType, c) @@ -928,25 +1064,34 @@ namespace Umbraco.Core.Persistence.Repositories.Implement temps.Add(temp); } - // load all required templates in 1 query, and index - var templates = _templateRepository.GetMany(templateIds.ToArray()) - .ToDictionary(x => x.Id, x => x); - - // load all properties for all documents from database in 1 query - indexed by version id - var properties = GetPropertyCollections(temps); - - // assign templates and properties - foreach (var temp in temps) + if (!slim) { - // complete the item - if (temp.Template1Id.HasValue && templates.TryGetValue(temp.Template1Id.Value, out var template)) - temp.Content.Template = template; - if (temp.Template2Id.HasValue && templates.TryGetValue(temp.Template2Id.Value, out template)) - temp.Content.PublishTemplate = template; - temp.Content.Properties = properties[temp.VersionId]; + // load all required templates in 1 query, and index + var templates = _templateRepository.GetMany(templateIds.ToArray()) + .ToDictionary(x => x.Id, x => x); - // reset dirty initial properties (U4-1946) - temp.Content.ResetDirtyProperties(false); + // load all properties for all documents from database in 1 query - indexed by version id + var properties = GetPropertyCollections(temps); + var schedule = GetContentSchedule(temps.Select(x => x.Content.Id).ToArray()); + + // assign templates and properties + foreach (var temp in temps) + { + // complete the item + if (temp.Template1Id.HasValue && templates.TryGetValue(temp.Template1Id.Value, out var template)) + temp.Content.Template = template; + if (temp.Template2Id.HasValue && templates.TryGetValue(temp.Template2Id.Value, out template)) + temp.Content.PublishTemplate = template; + + if (properties.ContainsKey(temp.VersionId)) + temp.Content.Properties = properties[temp.VersionId]; + else + throw new InvalidOperationException($"No property data found for version: '{temp.VersionId}'."); + + //load in the schedule + if (schedule.TryGetValue(temp.Content.Id, out var s)) + temp.Content.ContentSchedule = s; + } } // set variations, if varying @@ -960,6 +1105,9 @@ namespace Umbraco.Core.Persistence.Repositories.Implement SetVariations(temp.Content, contentVariations, documentVariations); } + foreach(var c in content) + c.ResetDirtyProperties(false); // reset dirty initial properties (U4-1946) + return content; } @@ -968,33 +1116,72 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var contentType = _contentTypeRepository.Get(dto.ContentDto.ContentTypeId); var content = ContentBaseFactory.BuildEntity(dto, contentType); - // get template - if (dto.DocumentVersionDto.TemplateId.HasValue && dto.DocumentVersionDto.TemplateId.Value > 0) - content.Template = _templateRepository.Get(dto.DocumentVersionDto.TemplateId.Value); - - // get properties - indexed by version id - var versionId = dto.DocumentVersionDto.Id; - - // fixme - shall we get published properties or not? - //var publishedVersionId = dto.Published ? dto.PublishedVersionDto.Id : 0; - var publishedVersionId = dto.PublishedVersionDto != null ? dto.PublishedVersionDto.Id : 0; - - var temp = new TempContent(dto.NodeId, versionId, publishedVersionId, contentType); - var ltemp = new List> { temp }; - var properties = GetPropertyCollections(ltemp); - content.Properties = properties[dto.DocumentVersionDto.Id]; - - // set variations, if varying - if (contentType.VariesByCulture()) + try { - var contentVariations = GetContentVariations(ltemp); - var documentVariations = GetDocumentVariations(ltemp); - SetVariations(content, contentVariations, documentVariations); + content.DisableChangeTracking(); + + // get template + if (dto.DocumentVersionDto.TemplateId.HasValue && dto.DocumentVersionDto.TemplateId.Value > 0) + content.Template = _templateRepository.Get(dto.DocumentVersionDto.TemplateId.Value); + + // get properties - indexed by version id + var versionId = dto.DocumentVersionDto.Id; + + // fixme - shall we get published properties or not? + //var publishedVersionId = dto.Published ? dto.PublishedVersionDto.Id : 0; + var publishedVersionId = dto.PublishedVersionDto != null ? dto.PublishedVersionDto.Id : 0; + + var temp = new TempContent(dto.NodeId, versionId, publishedVersionId, contentType); + var ltemp = new List> { temp }; + var properties = GetPropertyCollections(ltemp); + content.Properties = properties[dto.DocumentVersionDto.Id]; + + // set variations, if varying + if (contentType.VariesByCulture()) + { + var contentVariations = GetContentVariations(ltemp); + var documentVariations = GetDocumentVariations(ltemp); + SetVariations(content, contentVariations, documentVariations); + } + + //load in the schedule + var schedule = GetContentSchedule(dto.NodeId); + if (schedule.TryGetValue(dto.NodeId, out var s)) + content.ContentSchedule = s; + + // reset dirty initial properties (U4-1946) + content.ResetDirtyProperties(false); + return content; + } + finally + { + content.EnableChangeTracking(); + } + } + + private IDictionary GetContentSchedule(params int[] contentIds) + { + var result = new Dictionary(); + + var scheduleDtos = Database.FetchByGroups(contentIds, 2000, batch => Sql() + .Select() + .From() + .WhereIn(x => x.NodeId, batch)); + + foreach (var scheduleDto in scheduleDtos) + { + if (!result.TryGetValue(scheduleDto.NodeId, out var col)) + col = result[scheduleDto.NodeId] = new ContentScheduleCollection(); + + col.Add(new ContentSchedule(scheduleDto.Id, + LanguageRepository.GetIsoCodeById(scheduleDto.LanguageId) ?? string.Empty, + scheduleDto.Date, + scheduleDto.Action == ContentScheduleAction.Release.ToString() + ? ContentScheduleAction.Release + : ContentScheduleAction.Expire)); } - // reset dirty initial properties (U4-1946) - content.ResetDirtyProperties(false); - return content; + return result; } private void SetVariations(Content content, IDictionary> contentVariations, IDictionary> documentVariations) @@ -1077,13 +1264,13 @@ namespace Umbraco.Core.Persistence.Repositories.Implement private IEnumerable GetContentVariationDtos(IContent content, bool publishing) { // create dtos for the 'current' (non-published) version, all cultures - foreach (var (culture, name) in content.CultureNames) + foreach (var (culture, name) in content.CultureInfos) yield return new ContentVersionCultureVariationDto { VersionId = content.VersionId, LanguageId = LanguageRepository.GetIdByIsoCode(culture) ?? throw new InvalidOperationException("Not a valid culture."), Culture = culture, - Name = name, + Name = name.Name, UpdateDate = content.GetUpdateDate(culture) ?? DateTime.MinValue // we *know* there is a value }; @@ -1092,13 +1279,13 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (!publishing) yield break; // create dtos for the 'published' version, for published cultures (those having a name) - foreach (var (culture, name) in content.PublishNames) + foreach (var (culture, name) in content.PublishCultureInfos) yield return new ContentVersionCultureVariationDto { VersionId = content.PublishedVersionId, LanguageId = LanguageRepository.GetIdByIsoCode(culture) ?? throw new InvalidOperationException("Not a valid culture."), Culture = culture, - Name = name, + Name = name.Name, UpdateDate = content.GetPublishDate(culture) ?? DateTime.MinValue // we *know* there is a value }; } @@ -1165,15 +1352,15 @@ namespace Umbraco.Core.Persistence.Repositories.Implement { // content varies by culture // then it must have at least a variant name, else it makes no sense - if (content.CultureNames.Count == 0) + if (content.CultureInfos.Count == 0) throw new InvalidOperationException("Cannot save content with an empty name."); // and then, we need to set the invariant name implicitely, // using the default culture if it has a name, otherwise anything we can var defaultCulture = LanguageRepository.GetDefaultIsoCode(); - content.Name = defaultCulture != null && content.CultureNames.TryGetValue(defaultCulture, out var cultureName) - ? cultureName - : content.CultureNames.First().Value; + content.Name = defaultCulture != null && content.CultureInfos.TryGetValue(defaultCulture, out var cultureName) + ? cultureName.Name + : content.CultureInfos.First().Value.Name; } else { @@ -1206,7 +1393,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement private void EnsureVariantNamesAreUnique(Content content, bool publishing) { - if (!EnsureUniqueNaming || !content.ContentType.VariesByCulture() || content.CultureNames.Count == 0) return; + if (!EnsureUniqueNaming || !content.ContentType.VariesByCulture() || content.CultureInfos.Count == 0) return; // get names per culture, at same level (ie all siblings) var sql = SqlEnsureVariantNamesAreUnique.Sql(true, NodeObjectTypeId, content.ParentId, content.Id); @@ -1220,7 +1407,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // of whether the name has changed (ie the culture has been updated) - some saving culture // fr-FR could cause culture en-UK name to change - not sure that is clean - foreach(var (culture, name) in content.CultureNames) + foreach (var (culture, name) in content.CultureInfos) { var langId = LanguageRepository.GetIdByIsoCode(culture); if (!langId.HasValue) continue; @@ -1228,13 +1415,13 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // get a unique name var otherNames = cultureNames.Select(x => new SimilarNodeName { Id = x.Id, Name = x.Name }); - var uniqueName = SimilarNodeName.GetUniqueName(otherNames, 0, name); + var uniqueName = SimilarNodeName.GetUniqueName(otherNames, 0, name.Name); if (uniqueName == content.GetCultureName(culture)) continue; // update the name, and the publish name if published content.SetCultureName(uniqueName, culture); - if (publishing && content.PublishNames.ContainsKey(culture)) + if (publishing && content.PublishCultureInfos.ContainsKey(culture)) content.SetPublishInfo(culture, uniqueName, DateTime.Now); } } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs index fb8c2732e6..8d6f67e9db 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs @@ -66,6 +66,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (isContent) BuildVariants(entities.Cast()); + //fixme - see https://github.com/umbraco/Umbraco-CMS/pull/3460#issuecomment-434903930 we need to not load any property data at all for media if (isMedia) BuildProperties(entities, dtos); @@ -110,14 +111,14 @@ namespace Umbraco.Core.Persistence.Repositories.Implement return GetEntity(sql, isContent, isMedia); } - public virtual IEntitySlim Get(int id) + public IEntitySlim Get(int id) { var sql = GetBaseWhere(false, false, false, id); var dto = Database.FirstOrDefault(sql); return dto == null ? null : BuildEntity(false, false, dto); } - public virtual IEntitySlim Get(int id, Guid objectTypeId) + public IEntitySlim Get(int id, Guid objectTypeId) { var isContent = objectTypeId == Constants.ObjectTypes.Document || objectTypeId == Constants.ObjectTypes.DocumentBlueprint; var isMedia = objectTypeId == Constants.ObjectTypes.Media; @@ -126,21 +127,21 @@ namespace Umbraco.Core.Persistence.Repositories.Implement return GetEntity(sql, isContent, isMedia); } - public virtual IEnumerable GetAll(Guid objectType, params int[] ids) + public IEnumerable GetAll(Guid objectType, params int[] ids) { return ids.Length > 0 ? PerformGetAll(objectType, sql => sql.WhereIn(x => x.NodeId, ids.Distinct())) : PerformGetAll(objectType); } - public virtual IEnumerable GetAll(Guid objectType, params Guid[] keys) + public IEnumerable GetAll(Guid objectType, params Guid[] keys) { return keys.Length > 0 ? PerformGetAll(objectType, sql => sql.WhereIn(x => x.UniqueId, keys.Distinct())) : PerformGetAll(objectType); } - private IEnumerable GetEntities(Sql sql, bool isContent, bool isMedia) + private IEnumerable GetEntities(Sql sql, bool isContent, bool isMedia, bool loadMediaProperties) { //isContent is going to return a 1:M result now with the variants so we need to do different things if (isContent) @@ -157,7 +158,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var entities = dtos.Select(x => BuildEntity(false, isMedia, x)).ToArray(); - if (isMedia) + //fixme - see https://github.com/umbraco/Umbraco-CMS/pull/3460#issuecomment-434903930 we need to not load any property data at all for media + if (isMedia && loadMediaProperties) BuildProperties(entities, dtos); return entities; @@ -169,17 +171,17 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var isMedia = objectType == Constants.ObjectTypes.Media; var sql = GetFullSqlForEntityType(isContent, isMedia, objectType, filter); - return GetEntities(sql, isContent, isMedia); + return GetEntities(sql, isContent, isMedia, true); } - public virtual IEnumerable GetAllPaths(Guid objectType, params int[] ids) + public IEnumerable GetAllPaths(Guid objectType, params int[] ids) { return ids.Any() ? PerformGetAllPaths(objectType, sql => sql.WhereIn(x => x.NodeId, ids.Distinct())) : PerformGetAllPaths(objectType); } - public virtual IEnumerable GetAllPaths(Guid objectType, params Guid[] keys) + public IEnumerable GetAllPaths(Guid objectType, params Guid[] keys) { return keys.Any() ? PerformGetAllPaths(objectType, sql => sql.WhereIn(x => x.UniqueId, keys.Distinct())) @@ -193,7 +195,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement return Database.Fetch(sql); } - public virtual IEnumerable GetByQuery(IQuery query) + public IEnumerable GetByQuery(IQuery query) { var sqlClause = GetBase(false, false, null); var translator = new SqlTranslator(sqlClause, query); @@ -203,7 +205,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement return dtos.Select(x => BuildEntity(false, false, x)).ToList(); } - public virtual IEnumerable GetByQuery(IQuery query, Guid objectType) + public IEnumerable GetByQuery(IQuery query, Guid objectType) { var isContent = objectType == Constants.ObjectTypes.Document || objectType == Constants.ObjectTypes.DocumentBlueprint; var isMedia = objectType == Constants.ObjectTypes.Media; @@ -214,7 +216,22 @@ namespace Umbraco.Core.Persistence.Repositories.Implement sql = translator.Translate(); sql = AddGroupBy(isContent, isMedia, sql); - return GetEntities(sql, isContent, isMedia); + return GetEntities(sql, isContent, isMedia, true); + } + + //fixme - see https://github.com/umbraco/Umbraco-CMS/pull/3460#issuecomment-434903930 we need to not load any property data at all for media + internal IEnumerable GetMediaByQueryWithoutPropertyData(IQuery query) + { + var isContent = false; + var isMedia = true; + + var sql = GetBaseWhere(isContent, isMedia, false, null, Constants.ObjectTypes.Media); + + var translator = new SqlTranslator(sql, query); + sql = translator.Translate(); + sql = AddGroupBy(isContent, isMedia, sql); + + return GetEntities(sql, isContent, isMedia, false); } public UmbracoObjectTypes GetObjectType(int id) @@ -241,6 +258,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement return Database.ExecuteScalar(sql) > 0; } + //fixme - see https://github.com/umbraco/Umbraco-CMS/pull/3460#issuecomment-434903930 we need to not load any property data at all for media private void BuildProperties(EntitySlim entity, BaseDto dto) { var pdtos = Database.Fetch(GetPropertyData(dto.VersionId)); @@ -248,6 +266,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement BuildProperty(entity, pdto); } + //fixme - see https://github.com/umbraco/Umbraco-CMS/pull/3460#issuecomment-434903930 we need to not load any property data at all for media private void BuildProperties(EntitySlim[] entities, List dtos) { var versionIds = dtos.Select(x => x.VersionId).Distinct().ToList(); @@ -263,6 +282,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement } } + //fixme - see https://github.com/umbraco/Umbraco-CMS/pull/3460#issuecomment-434903930 we need to not load any property data at all for media private void BuildProperty(EntitySlim entity, PropertyDataDto pdto) { // explain ?! diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs index e316d1d04b..09fb664ffe 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs @@ -236,9 +236,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // fast way of getting an id for an isoCode - avoiding cloning // _codeIdMap is rebuilt whenever PerformGetAll runs - public int? GetIdByIsoCode(string isoCode) => GetIdByIsoCode(isoCode, throwOnNotFound: true); - - private int? GetIdByIsoCode(string isoCode, bool throwOnNotFound) + public int? GetIdByIsoCode(string isoCode, bool throwOnNotFound = true) { if (isoCode == null) return null; @@ -254,14 +252,13 @@ namespace Umbraco.Core.Persistence.Repositories.Implement } if (throwOnNotFound) throw new ArgumentException($"Code {isoCode} does not correspond to an existing language.", nameof(isoCode)); - return 0; - } + return null; + } + // fast way of getting an isoCode for an id - avoiding cloning // _idCodeMap is rebuilt whenever PerformGetAll runs - public string GetIsoCodeById(int? id) => GetIsoCodeById(id, throwOnNotFound: true); - - private string GetIsoCodeById(int? id, bool throwOnNotFound) + public string GetIsoCodeById(int? id, bool throwOnNotFound = true) { if (id == null) return null; @@ -277,6 +274,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement } if (throwOnNotFound) throw new ArgumentException($"Id {id} does not correspond to an existing language.", nameof(id)); + return null; } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/MacroRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/MacroRepository.cs index 546be0b4a8..594f26fa72 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/MacroRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/MacroRepository.cs @@ -160,7 +160,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement //update the properties if they've changed var macro = (Macro)entity; - if (macro.IsPropertyDirty("Properties") || macro.Properties.Any(x => x.IsDirty())) + if (macro.IsPropertyDirty("Properties") || macro.Properties.Values.Any(x => x.IsDirty())) { var ids = dto.MacroPropertyDtos.Where(x => x.Id > 0).Select(x => x.Id).ToArray(); if (ids.Length > 0) @@ -173,7 +173,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var aliases = new Dictionary(); foreach (var propDto in dto.MacroPropertyDtos) { - var prop = macro.Properties.FirstOrDefault(x => x.Id == propDto.Id); + var prop = macro.Properties.Values.FirstOrDefault(x => x.Id == propDto.Id); if (prop == null) throw new Exception("oops: property."); if (propDto.Id == 0 || prop.IsPropertyDirty("Alias")) { @@ -195,7 +195,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement else { // update - var property = macro.Properties.FirstOrDefault(x => x.Id == propDto.Id); + var property = macro.Properties.Values.FirstOrDefault(x => x.Id == propDto.Id); if (property == null) throw new Exception("oops: property."); if (property.IsDirty()) Database.Update(propDto); diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs index 2390ce9a7b..dbfdc8e980 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs @@ -8,12 +8,12 @@ using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Exceptions; using Umbraco.Core.Logging; using Umbraco.Core.Models; -using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Factories; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Scoping; using Umbraco.Core.Services; +using static Umbraco.Core.Persistence.NPocoSqlExtensions.Statics; namespace Umbraco.Core.Persistence.Repositories.Implement { @@ -100,7 +100,12 @@ namespace Umbraco.Core.Persistence.Repositories.Implement case QueryType.Many: sql = sql.Select(r => r.Select(x => x.NodeDto) - .Select(x => x.ContentVersionDto)); + .Select(x => x.ContentVersionDto)) + + // ContentRepositoryBase expects a variantName field to order by name + // for now, just return the plain invariant node name + // fixme media should support variants !! + .AndSelect(x => Alias(x.Text, "variantName")); break; } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/MemberGroupRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/MemberGroupRepository.cs index e6783eaf6d..e80faaa44a 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/MemberGroupRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/MemberGroupRepository.cs @@ -22,7 +22,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected override IMemberGroup PerformGet(int id) { var sql = GetBaseQuery(false); - sql.Where(GetBaseWhereClause(), new { Id = id }); + sql.Where(GetBaseWhereClause(), new { id = id }); var dto = Database.Fetch(SqlSyntax.SelectTop(sql, 1)).FirstOrDefault(); @@ -262,7 +262,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var nonAssignedRoles = roleNames.Except(assignedRoles, StringComparer.CurrentCultureIgnoreCase); foreach (var toAssign in nonAssignedRoles) { - var groupId = rolesForNames.First(x => x.Text == toAssign).NodeId; + var groupId = rolesForNames.First(x => x.Text.InvariantEquals(toAssign)).NodeId; Database.Insert(new Member2MemberGroupDto { Member = mId, MemberGroup = groupId }); } } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs index 84ef154ae8..fd79b231de 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs @@ -6,12 +6,12 @@ using NPoco; using Umbraco.Core.Cache; using Umbraco.Core.Logging; using Umbraco.Core.Models; -using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Factories; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Scoping; using Umbraco.Core.Services; +using static Umbraco.Core.Persistence.NPocoSqlExtensions.Statics; namespace Umbraco.Core.Persistence.Repositories.Implement { @@ -114,9 +114,13 @@ namespace Umbraco.Core.Persistence.Repositories.Implement case QueryType.Single: case QueryType.Many: sql = sql.Select(r => - r.Select(x => x.ContentVersionDto) - .Select(x => x.ContentDto, r1 => - r1.Select(x => x.NodeDto))); + r.Select(x => x.ContentVersionDto) + .Select(x => x.ContentDto, r1 => + r1.Select(x => x.NodeDto))) + + // ContentRepositoryBase expects a variantName field to order by name + // so get it here, though for members it's just the plain node name + .AndSelect(x => Alias(x.Text, "variantName")); break; } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/TemplateRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/TemplateRepository.cs index 415aa2bcb1..83876db599 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/TemplateRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/TemplateRepository.cs @@ -181,7 +181,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement template.Path = nodeDto.Path; //now do the file work - SaveFile(template, dto); + SaveFile(template); template.ResetDirtyProperties(); @@ -236,7 +236,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement template.IsMasterTemplate = axisDefs.Any(x => x.ParentId == dto.NodeId); //now do the file work - SaveFile((Template) entity, dto, originalAlias); + SaveFile((Template) entity, originalAlias); entity.ResetDirtyProperties(); @@ -245,7 +245,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement template.GetFileContent = file => GetFileContent((Template) file, false); } - private void SaveFile(Template template, TemplateDto dto, string originalAlias = null) + private void SaveFile(Template template, string originalAlias = null) { string content; @@ -275,10 +275,6 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // once content has been set, "template on disk" are not "on disk" anymore template.Content = content; SetVirtualPath(template); - - if (dto.Design == content) return; - dto.Design = content; - Database.Update(dto); // though... we don't care about the db value really??!! } protected override void PersistDeletedItem(ITemplate entity) diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/UserRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/UserRepository.cs index 23c7c055f8..b14c7659a3 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/UserRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/UserRepository.cs @@ -129,7 +129,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement public IProfile GetProfile(string username) { - var dto = GetDtoWith(sql => sql.Where(x => x.UserName == username), false); + var dto = GetDtoWith(sql => sql.Where(x => x.Login == username), false); return dto == null ? null : new UserProfile(dto.Id, dto.UserName); } diff --git a/src/Umbraco.Core/Persistence/SqlContextExtensions.cs b/src/Umbraco.Core/Persistence/SqlContextExtensions.cs index e28816b6a4..249e2cafd0 100644 --- a/src/Umbraco.Core/Persistence/SqlContextExtensions.cs +++ b/src/Umbraco.Core/Persistence/SqlContextExtensions.cs @@ -17,11 +17,11 @@ namespace Umbraco.Core.Persistence /// An expression to visit. /// An optional table alias. /// A SQL statement, and arguments, corresponding to the expression. - public static (string Sql, object[] Args) Visit(this ISqlContext sqlContext, Expression> expression, string alias = null) + public static (string Sql, object[] Args) VisitDto(this ISqlContext sqlContext, Expression> expression, string alias = null) { - var expresionist = new PocoToSqlExpressionVisitor(sqlContext, alias); - var visited = expresionist.Visit(expression); - return (visited, expresionist.GetSqlParameters()); + var visitor = new PocoToSqlExpressionVisitor(sqlContext, alias); + var visited = visitor.Visit(expression); + return (visited, visitor.GetSqlParameters()); } /// @@ -33,11 +33,11 @@ namespace Umbraco.Core.Persistence /// An expression to visit. /// An optional table alias. /// A SQL statement, and arguments, corresponding to the expression. - public static (string Sql, object[] Args) Visit(this ISqlContext sqlContext, Expression> expression, string alias = null) + public static (string Sql, object[] Args) VisitDto(this ISqlContext sqlContext, Expression> expression, string alias = null) { - var expresionist = new PocoToSqlExpressionVisitor(sqlContext, alias); - var visited = expresionist.Visit(expression); - return (visited, expresionist.GetSqlParameters()); + var visitor = new PocoToSqlExpressionVisitor(sqlContext, alias); + var visited = visitor.Visit(expression); + return (visited, visitor.GetSqlParameters()); } /// @@ -50,11 +50,11 @@ namespace Umbraco.Core.Persistence /// An optional table alias for the first DTO. /// An optional table alias for the second DTO. /// A SQL statement, and arguments, corresponding to the expression. - public static (string Sql, object[] Args) Visit(this ISqlContext sqlContext, Expression> expression, string alias1 = null, string alias2 = null) + public static (string Sql, object[] Args) VisitDto(this ISqlContext sqlContext, Expression> expression, string alias1 = null, string alias2 = null) { - var expresionist = new PocoToSqlExpressionVisitor(sqlContext, alias1, alias2); - var visited = expresionist.Visit(expression); - return (visited, expresionist.GetSqlParameters()); + var visitor = new PocoToSqlExpressionVisitor(sqlContext, alias1, alias2); + var visited = visitor.Visit(expression); + return (visited, visitor.GetSqlParameters()); } /// @@ -68,11 +68,42 @@ namespace Umbraco.Core.Persistence /// An optional table alias for the first DTO. /// An optional table alias for the second DTO. /// A SQL statement, and arguments, corresponding to the expression. - public static (string Sql, object[] Args) Visit(this ISqlContext sqlContext, Expression> expression, string alias1 = null, string alias2 = null) + public static (string Sql, object[] Args) VisitDto(this ISqlContext sqlContext, Expression> expression, string alias1 = null, string alias2 = null) { - var expresionist = new PocoToSqlExpressionVisitor(sqlContext, alias1, alias2); - var visited = expresionist.Visit(expression); - return (visited, expresionist.GetSqlParameters()); + var visitor = new PocoToSqlExpressionVisitor(sqlContext, alias1, alias2); + var visited = visitor.Visit(expression); + return (visited, visitor.GetSqlParameters()); + } + + /// + /// Visit a model expression. + /// + /// The type of the model. + /// An . + /// An expression to visit. + /// A SQL statement, and arguments, corresponding to the expression. + public static (string Sql, object[] Args) VisitModel(this ISqlContext sqlContext, Expression> expression) + { + var visitor = new ModelToSqlExpressionVisitor(sqlContext.SqlSyntax, sqlContext.Mappers); + var visited = visitor.Visit(expression); + return (visited, visitor.GetSqlParameters()); + } + + /// + /// Visit a model expression representing a field. + /// + /// The type of the model. + /// An . + /// An expression to visit, representing a field. + /// The name of the field. + public static string VisitModelField(this ISqlContext sqlContext, Expression> field) + { + var (sql, _) = sqlContext.VisitModel(field); + + // going to return " = @0" + // take the first part only + var pos = sql.IndexOf(' '); + return sql.Substring(0, pos); } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/MySqlSyntaxProvider.cs b/src/Umbraco.Core/Persistence/SqlSyntax/MySqlSyntaxProvider.cs index 4ff0545281..d69786fbfc 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/MySqlSyntaxProvider.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/MySqlSyntaxProvider.cs @@ -334,14 +334,9 @@ ORDER BY TABLE_NAME, INDEX_NAME", switch (systemMethod) { case SystemMethods.NewGuid: - return null; // NOT SUPPORTED! - //return "NEWID()"; + return null; // NOT SUPPORTED! case SystemMethods.CurrentDateTime: return "CURRENT_TIMESTAMP"; - //case SystemMethods.NewSequentialId: - // return "NEWSEQUENTIALID()"; - //case SystemMethods.CurrentUTCDateTime: - // return "GETUTCDATE()"; } return null; diff --git a/src/Umbraco.Core/PropertyEditors/ConfigurationEditor.cs b/src/Umbraco.Core/PropertyEditors/ConfigurationEditor.cs index e311c3bbfc..bb58a8ef72 100644 --- a/src/Umbraco.Core/PropertyEditors/ConfigurationEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/ConfigurationEditor.cs @@ -53,6 +53,12 @@ namespace Umbraco.Core.PropertyEditors throw new InvalidCastException($"Cannot cast configuration of type {obj.GetType().Name} to {typeof(TConfiguration).Name}."); } + /// + /// Converts a configuration object into a serialized database value. + /// + public static string ToDatabase(object configuration) + => configuration == null ? null : JsonConvert.SerializeObject(configuration, ConfigurationJsonSettings); + /// [JsonProperty("defaultConfig")] public virtual IDictionary DefaultConfiguration => new Dictionary(); diff --git a/src/Umbraco.Web/PropertyEditors/DropDownFlexibleConfiguration.cs b/src/Umbraco.Core/PropertyEditors/DropDownFlexibleConfiguration.cs similarity index 79% rename from src/Umbraco.Web/PropertyEditors/DropDownFlexibleConfiguration.cs rename to src/Umbraco.Core/PropertyEditors/DropDownFlexibleConfiguration.cs index 4d4d47ad5b..d1c2d23c4f 100644 --- a/src/Umbraco.Web/PropertyEditors/DropDownFlexibleConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/DropDownFlexibleConfiguration.cs @@ -1,6 +1,4 @@ -using Umbraco.Core.PropertyEditors; - -namespace Umbraco.Web.PropertyEditors +namespace Umbraco.Core.PropertyEditors { internal class DropDownFlexibleConfiguration : ValueListConfiguration { diff --git a/src/Umbraco.Core/PropertyEditors/IConfigurationEditor.cs b/src/Umbraco.Core/PropertyEditors/IConfigurationEditor.cs index 003fe9a80e..875e2c0e8f 100644 --- a/src/Umbraco.Core/PropertyEditors/IConfigurationEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/IConfigurationEditor.cs @@ -47,7 +47,7 @@ namespace Umbraco.Core.PropertyEditors /// Converts the serialized database value into the actual configuration object. /// /// Converting the configuration object to the serialized database value is - /// achieved by simply serializing the configuration. + /// achieved by simply serializing the configuration. See . object FromDatabase(string configurationJson); /// diff --git a/src/Umbraco.Core/PropertyEditors/ImageCropperConfiguration.cs b/src/Umbraco.Core/PropertyEditors/ImageCropperConfiguration.cs index 54b69cea45..2ce6e2ec04 100644 --- a/src/Umbraco.Core/PropertyEditors/ImageCropperConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/ImageCropperConfiguration.cs @@ -7,7 +7,7 @@ namespace Umbraco.Core.PropertyEditors /// public class ImageCropperConfiguration { - [ConfigurationField("crops", "Crop sizes", "views/propertyeditors/imagecropper/imagecropper.prevalues.html")] + [ConfigurationField("crops", "Define crops", "views/propertyeditors/imagecropper/imagecropper.prevalues.html")] public Crop[] Crops { get; set; } public class Crop diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/DropdownListMultipleValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/DropdownListMultipleValueConverter.cs deleted file mode 100644 index d91f45292c..0000000000 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/DropdownListMultipleValueConverter.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Umbraco.Core.Models.PublishedContent; - -namespace Umbraco.Core.PropertyEditors.ValueConverters -{ - [DefaultPropertyValueConverter] - public class DropdownListMultipleValueConverter : PropertyValueConverterBase - { - public override bool IsConverter(PublishedPropertyType propertyType) - => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.DropDownListMultiple); - - public override Type GetPropertyValueType(PublishedPropertyType propertyType) - => typeof (IEnumerable); - - public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) - => PropertyCacheLevel.Element; - - public override object ConvertIntermediateToObject(IPublishedElement owner, PublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object source, bool preview) - { - var sourceString = (source ?? "").ToString(); - - if (string.IsNullOrEmpty(sourceString)) - return Enumerable.Empty(); - - var values = - sourceString.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(v => v.Trim()); - - return values; - } - } -} diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/DropdownListMultipleWithKeysValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/DropdownListMultipleWithKeysValueConverter.cs deleted file mode 100644 index bceebc232b..0000000000 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/DropdownListMultipleWithKeysValueConverter.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Umbraco.Core.Models.PublishedContent; - -namespace Umbraco.Core.PropertyEditors.ValueConverters -{ - [DefaultPropertyValueConverter] - public class DropdownListMultipleWithKeysValueConverter : PropertyValueConverterBase - { - public override bool IsConverter(PublishedPropertyType propertyType) - => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.DropdownlistMultiplePublishKeys); - - public override Type GetPropertyValueType(PublishedPropertyType propertyType) - => typeof (IEnumerable); - - public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) - => PropertyCacheLevel.Element; - - public override object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview) - { - if (source == null) - return new int[] { }; - - var prevalueIds = source.ToString() - .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) - .Select(p => p.Trim()) - .Select(int.Parse) - .ToArray(); - - return prevalueIds; - } - } -} diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/DropdownListValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/DropdownListValueConverter.cs deleted file mode 100644 index 5fe1967f32..0000000000 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/DropdownListValueConverter.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; -using Umbraco.Core.Models.PublishedContent; - -namespace Umbraco.Core.PropertyEditors.ValueConverters -{ - [DefaultPropertyValueConverter] - public class DropdownListValueConverter : PropertyValueConverterBase - { - public override bool IsConverter(PublishedPropertyType propertyType) - => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.DropDownList); - - public override Type GetPropertyValueType(PublishedPropertyType propertyType) - => typeof (string); - - public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) - => PropertyCacheLevel.Element; - - public override object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview) - { - return source?.ToString() ?? string.Empty; - } - } -} diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/DropdownListWithKeysValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/DropdownListWithKeysValueConverter.cs deleted file mode 100644 index 960cd4afa6..0000000000 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/DropdownListWithKeysValueConverter.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using Umbraco.Core.Models.PublishedContent; - -namespace Umbraco.Core.PropertyEditors.ValueConverters -{ - [DefaultPropertyValueConverter] - public class DropdownListWithKeysValueConverter : PropertyValueConverterBase - { - public override bool IsConverter(PublishedPropertyType propertyType) - => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.DropdownlistPublishKeys); - - public override Type GetPropertyValueType(PublishedPropertyType propertyType) - => typeof (int); - - public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) - => PropertyCacheLevel.Element; - - public override object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview) - { - var intAttempt = source.TryConvertTo(); - if (intAttempt.Success) - return intAttempt.Result; - - return null; - } - } -} diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/TagsValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/TagsValueConverter.cs index fcf6cef868..9b857c2dff 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/TagsValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/TagsValueConverter.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; -using System.Linq; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Umbraco.Core.Models; @@ -31,7 +30,9 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters public override object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview) { - // if Json storage type deserialzie and return as string array + if (source == null) return Array.Empty(); + + // if Json storage type deserialize and return as string array if (JsonStorageType(propertyType.DataType.Id)) { var jArray = JsonConvert.DeserializeObject(source.ToString()); @@ -39,11 +40,7 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters } // Otherwise assume CSV storage type and return as string array - var sourceString = source?.ToString() ?? string.Empty; - var csvTags = sourceString - .Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries) - .ToArray(); - return csvTags; + return source.ToString().Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries); } public override object ConvertIntermediateToObject(IPublishedElement owner, PublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object source, bool preview) @@ -62,10 +59,9 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters /// private bool JsonStorageType(int dataTypeId) { - // fixme - // GetPreValuesCollectionByDataTypeId is cached at repository level; - // still, the collection is deep-cloned so this is kinda expensive, - // better to cache here + trigger refresh in DataTypeCacheRefresher + // GetDataType(id) is cached at repository level; still, there is some + // deep-cloning involved (expensive) - better cache here + trigger + // refresh in DataTypeCacheRefresher return Storages.GetOrAdd(dataTypeId, id => { diff --git a/src/Umbraco.Core/Publishing/ScheduledPublisher.cs b/src/Umbraco.Core/Publishing/ScheduledPublisher.cs deleted file mode 100644 index 73cc752508..0000000000 --- a/src/Umbraco.Core/Publishing/ScheduledPublisher.cs +++ /dev/null @@ -1,87 +0,0 @@ -using System; -using System.Linq; -using Umbraco.Core.Logging; -using Umbraco.Core.Models; -using Umbraco.Core.Services; -using Umbraco.Core.Services.Implement; - -namespace Umbraco.Core.Publishing -{ - /// - /// Used to perform scheduled publishing/unpublishing - /// - internal class ScheduledPublisher - { - private readonly IContentService _contentService; - private readonly ILogger _logger; - private readonly IUserService _userService; - - public ScheduledPublisher(IContentService contentService, ILogger logger, IUserService userService) - { - _contentService = contentService; - _logger = logger; - _userService = userService; - } - - /// - /// Processes scheduled operations - /// - /// - /// Returns the number of items successfully completed - /// - public int CheckPendingAndProcess() - { - // fixme isn't this done in ContentService already? - var counter = 0; - var contentForRelease = _contentService.GetContentForRelease().ToArray(); - if (contentForRelease.Length > 0) - _logger.Debug("There's {ContentItemsForRelease} item(s) of content to be published", contentForRelease.Length); - foreach (var d in contentForRelease) - { - try - { - d.ReleaseDate = null; - d.PublishCulture(); // fixme variants? - var result = _contentService.SaveAndPublish(d, userId: _userService.GetProfileById(d.WriterId).Id); - _logger.Debug("Result of publish attempt: {PublishResult}", result.Result); - if (result.Success == false) - { - _logger.Error(null, "Error publishing node {NodeId}", d.Id); - } - else - { - counter++; - } - } - catch (Exception ex) - { - _logger.Error(ex, "Error publishing node {NodeId}", d.Id); - throw; - } - } - - var contentForExpiration = _contentService.GetContentForExpiration().ToArray(); - if (contentForExpiration.Length > 0) - _logger.Debug("There's {ContentItemsForExpiration} item(s) of content to be unpublished", contentForExpiration.Length); - foreach (var d in contentForExpiration) - { - try - { - d.ExpireDate = null; - var result = _contentService.Unpublish(d, userId: _userService.GetProfileById(d.WriterId).Id); - if (result.Success) - { - counter++; - } - } - catch (Exception ex) - { - _logger.Error(ex, "Error unpublishing node {NodeId}", d.Id); - throw; - } - } - - return counter; - } - } -} diff --git a/src/Umbraco.Core/ReflectionUtilities.cs b/src/Umbraco.Core/ReflectionUtilities.cs index a5acfe78e3..870cb9ec13 100644 --- a/src/Umbraco.Core/ReflectionUtilities.cs +++ b/src/Umbraco.Core/ReflectionUtilities.cs @@ -295,7 +295,7 @@ namespace Umbraco.Core /// Occurs when the constructor does not exist and is true. /// Occurs when is not a Func or when /// is specified and does not match the function's returned type. - public static TLambda EmitCtor(bool mustExist = true, Type declaring = null) + public static TLambda EmitConstuctor(bool mustExist = true, Type declaring = null) { var (_, lambdaParameters, lambdaReturned) = AnalyzeLambda(true, true); @@ -313,7 +313,7 @@ namespace Umbraco.Core } // emit - return EmitCtorSafe(lambdaParameters, lambdaReturned, ctor); + return EmitConstructorSafe(lambdaParameters, lambdaReturned, ctor); } /// @@ -325,16 +325,16 @@ namespace Umbraco.Core /// Occurs when is not a Func or when its generic /// arguments do not match those of . /// Occurs when is null. - public static TLambda EmitCtor(ConstructorInfo ctor) + public static TLambda EmitConstructor(ConstructorInfo ctor) { if (ctor == null) throw new ArgumentNullException(nameof(ctor)); var (_, lambdaParameters, lambdaReturned) = AnalyzeLambda(true, true); - return EmitCtorSafe(lambdaParameters, lambdaReturned, ctor); + return EmitConstructorSafe(lambdaParameters, lambdaReturned, ctor); } - private static TLambda EmitCtorSafe(Type[] lambdaParameters, Type returned, ConstructorInfo ctor) + private static TLambda EmitConstructorSafe(Type[] lambdaParameters, Type returned, ConstructorInfo ctor) { // get type and args var ctorDeclaring = ctor.DeclaringType; @@ -350,7 +350,7 @@ namespace Umbraco.Core ThrowInvalidLambda("ctor", ctorDeclaring, ctorParameters); // emit - return EmitCtor(ctorDeclaring, ctorParameters, ctor); + return EmitConstructor(ctorDeclaring, ctorParameters, ctor); } /// @@ -367,17 +367,17 @@ namespace Umbraco.Core /// Occurs when is not a Func or when its generic /// arguments do not match those of . /// Occurs when is null. - public static TLambda EmitCtorUnsafe(ConstructorInfo ctor) + public static TLambda EmitConstructorUnsafe(ConstructorInfo ctor) { if (ctor == null) throw new ArgumentNullException(nameof(ctor)); var (_, lambdaParameters, lambdaReturned) = AnalyzeLambda(true, true); // emit - unsafe - use lambda's args and assume they are correct - return EmitCtor(lambdaReturned, lambdaParameters, ctor); + return EmitConstructor(lambdaReturned, lambdaParameters, ctor); } - private static TLambda EmitCtor(Type declaring, Type[] lambdaParameters, ConstructorInfo ctor) + private static TLambda EmitConstructor(Type declaring, Type[] lambdaParameters, ConstructorInfo ctor) { // gets the method argument types var ctorParameters = GetParameters(ctor); diff --git a/src/Umbraco.Core/Runtime/CoreRuntime.cs b/src/Umbraco.Core/Runtime/CoreRuntime.cs old mode 100644 new mode 100755 index 8f3b8991e4..cf2712974d --- a/src/Umbraco.Core/Runtime/CoreRuntime.cs +++ b/src/Umbraco.Core/Runtime/CoreRuntime.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Configuration; +using System.Reflection; using System.Threading; using System.Web; using LightInject; @@ -12,12 +13,14 @@ using Umbraco.Core.Configuration; using Umbraco.Core.Exceptions; using Umbraco.Core.IO; using Umbraco.Core.Logging; +using Umbraco.Core.Logging.Serilog; using Umbraco.Core.Migrations.Upgrade; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Mappers; using Umbraco.Core.Persistence.SqlSyntax; using Umbraco.Core.Scoping; +using Umbraco.Core.Services.Implement; namespace Umbraco.Core.Runtime { @@ -28,26 +31,29 @@ namespace Umbraco.Core.Runtime /// should be possible to use this runtime in console apps. public class CoreRuntime : IRuntime { - private readonly UmbracoApplicationBase _app; private BootLoader _bootLoader; private RuntimeState _state; /// /// Initializes a new instance of the class. /// - /// The Umbraco HttpApplication. - public CoreRuntime(UmbracoApplicationBase umbracoApplication) - { - _app = umbracoApplication ?? throw new ArgumentNullException(nameof(umbracoApplication)); - } + public CoreRuntime() + { } /// public virtual void Boot(ServiceContainer container) { - // some components may want to initialize with the UmbracoApplicationBase - // well, they should not - we should not do this - // TODO remove this eventually. - container.RegisterInstance(_app); + container.ConfigureUmbracoCore(); // also sets Current.Container + + // register the essential stuff, + // ie the global application logger + // (profiler etc depend on boot manager) + var logger = GetLogger(); + container.RegisterInstance(logger); + // now it is ok to use Current.Logger + + ConfigureUnhandledException(logger); + ConfigureAssemblyResolve(logger); Compose(container); @@ -114,6 +120,46 @@ namespace Umbraco.Core.Runtime //sa.Scope?.Dispose(); } + /// + /// Gets a logger. + /// + protected virtual ILogger GetLogger() + { + return SerilogLogger.CreateWithDefaultConfiguration(); + } + + protected virtual void ConfigureUnhandledException(ILogger logger) + { + //take care of unhandled exceptions - there is nothing we can do to + // prevent the launch process to go down but at least we can try + // and log the exception + AppDomain.CurrentDomain.UnhandledException += (_, args) => + { + var exception = (Exception)args.ExceptionObject; + var isTerminating = args.IsTerminating; // always true? + + var msg = "Unhandled exception in AppDomain"; + if (isTerminating) msg += " (terminating)"; + msg += "."; + logger.Error(exception, msg); + }; + } + + protected virtual void ConfigureAssemblyResolve(ILogger logger) + { + // When an assembly can't be resolved. In here we can do magic with the assembly name and try loading another. + // This is used for loading a signed assembly of AutoMapper (v. 3.1+) without having to recompile old code. + AppDomain.CurrentDomain.AssemblyResolve += (sender, args) => + { + // ensure the assembly is indeed AutoMapper and that the PublicKeyToken is null before trying to Load again + // do NOT just replace this with 'return Assembly', as it will cause an infinite loop -> stackoverflow + if (args.Name.StartsWith("AutoMapper") && args.Name.EndsWith("PublicKeyToken=null")) + return Assembly.Load(args.Name.Replace(", PublicKeyToken=null", ", PublicKeyToken=be96cd2c38ef1005")); + return null; + }; + } + + private void AquireMainDom(IServiceFactory container) { using (var timer = ProfilingLogger.DebugDuration("Acquiring MainDom.", "Aquired.")) @@ -245,7 +291,7 @@ namespace Umbraco.Core.Runtime private void SetRuntimeStateLevel(IUmbracoDatabaseFactory databaseFactory, ILogger logger) { - var localVersion = UmbracoVersion.Local; // the local, files, version + var localVersion = UmbracoVersion.LocalVersion; // the local, files, version var codeVersion = _state.SemanticVersion; // the executing code version var connect = false; @@ -306,28 +352,26 @@ namespace Umbraco.Core.Runtime throw new BootFailedException("A connection string is configured but Umbraco could not connect to the database."); } - // if we already know we want to upgrade, no need to look for migrations... - if (_state.Level == RuntimeLevel.Upgrade) - return; + // if we already know we want to upgrade, + // still run EnsureUmbracoUpgradeState to get the states + // (v7 will just get a null state, that's ok) // else // look for a matching migration entry - bypassing services entirely - they are not 'up' yet // fixme - in a LB scenario, ensure that the DB gets upgraded only once! - bool exists; + bool noUpgrade; try { - exists = EnsureUmbracoUpgradeState(databaseFactory, logger); + noUpgrade = EnsureUmbracoUpgradeState(databaseFactory, logger); } catch (Exception e) { - // can connect to the database but cannot access the migration table... need to install + // can connect to the database but cannot check the upgrade state... oops logger.Warn(e, "Could not check the upgrade state."); - logger.Debug("Could not check the upgrade state, need to install Umbraco."); - _state.Level = RuntimeLevel.Install; - return; + throw new BootFailedException("Could not check the upgrade state.", e); } - if (exists) + if (noUpgrade) { // the database version matches the code & files version, all clear, can run _state.Level = RuntimeLevel.Run; @@ -345,27 +389,19 @@ namespace Umbraco.Core.Runtime protected virtual bool EnsureUmbracoUpgradeState(IUmbracoDatabaseFactory databaseFactory, ILogger logger) { - // no scope, no key value service - just directly accessing the database - var umbracoPlan = new UmbracoPlan(); var stateValueKey = Upgrader.GetStateValueKey(umbracoPlan); - string state; + // no scope, no service - just directly accessing the database using (var database = databaseFactory.CreateDatabase()) { - var sql = databaseFactory.SqlContext.Sql() - .Select() - .From() - .Where(x => x.Key == stateValueKey); - state = database.FirstOrDefault(sql)?.Value; + _state.CurrentMigrationState = KeyValueService.GetValue(database, stateValueKey); + _state.FinalMigrationState = umbracoPlan.FinalState; } - _state.CurrentMigrationState = state; - _state.FinalMigrationState = umbracoPlan.FinalState; + logger.Debug("Final upgrade state is {FinalMigrationState}, database contains {DatabaseState}", _state.FinalMigrationState, _state.CurrentMigrationState ?? ""); - logger.Debug("Final upgrade state is {FinalMigrationState}, database contains {DatabaseState}", _state.FinalMigrationState, state ?? ""); - - return state == _state.FinalMigrationState; + return _state.CurrentMigrationState == _state.FinalMigrationState; } #region Locals diff --git a/src/Umbraco.Core/RuntimeState.cs b/src/Umbraco.Core/RuntimeState.cs index 0177619a68..4f6f56531b 100644 --- a/src/Umbraco.Core/RuntimeState.cs +++ b/src/Umbraco.Core/RuntimeState.cs @@ -55,7 +55,7 @@ namespace Umbraco.Core /// /// Gets the version comment of the executing code. /// - public string VersionComment => UmbracoVersion.CurrentComment; + public string VersionComment => UmbracoVersion.Comment; /// /// Gets the semantic version of the executing code. diff --git a/src/Umbraco.Core/Security/AuthenticationExtensions.cs b/src/Umbraco.Core/Security/AuthenticationExtensions.cs index b65ab83439..7c3e835a77 100644 --- a/src/Umbraco.Core/Security/AuthenticationExtensions.cs +++ b/src/Umbraco.Core/Security/AuthenticationExtensions.cs @@ -1,15 +1,10 @@ using System; using System.Collections.Concurrent; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Globalization; using System.Globalization; using System.Linq; using System.Security.Claims; using System.Security.Principal; -using System.Text; using System.Threading; -using System.Threading.Tasks; namespace Umbraco.Core.Security { diff --git a/src/Umbraco.Core/Security/ContentPermissionsHelper.cs b/src/Umbraco.Core/Security/ContentPermissionsHelper.cs new file mode 100644 index 0000000000..1a329fcdcb --- /dev/null +++ b/src/Umbraco.Core/Security/ContentPermissionsHelper.cs @@ -0,0 +1,263 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Umbraco.Core.Models; +using Umbraco.Core.Models.Entities; +using Umbraco.Core.Models.Membership; +using Umbraco.Core.Services; + +namespace Umbraco.Core.Security +{ + internal class ContentPermissionsHelper + { + public enum ContentAccess + { + Granted, + Denied, + NotFound + } + + public static ContentAccess CheckPermissions( + IContent content, + IUser user, + IUserService userService, + IEntityService entityService, + params char[] permissionsToCheck) + { + if (user == null) throw new ArgumentNullException("user"); + if (userService == null) throw new ArgumentNullException("userService"); + if (entityService == null) throw new ArgumentNullException("entityService"); + + if (content == null) return ContentAccess.NotFound; + + var hasPathAccess = user.HasPathAccess(content, entityService); + + if (hasPathAccess == false) + return ContentAccess.Denied; + + if (permissionsToCheck == null || permissionsToCheck.Length == 0) + return ContentAccess.Granted; + + //get the implicit/inherited permissions for the user for this path + return CheckPermissionsPath(content.Path, user, userService, permissionsToCheck) + ? ContentAccess.Granted + : ContentAccess.Denied; + } + + public static ContentAccess CheckPermissions( + IUmbracoEntity entity, + IUser user, + IUserService userService, + IEntityService entityService, + params char[] permissionsToCheck) + { + if (user == null) throw new ArgumentNullException("user"); + if (userService == null) throw new ArgumentNullException("userService"); + if (entityService == null) throw new ArgumentNullException("entityService"); + + if (entity == null) return ContentAccess.NotFound; + + var hasPathAccess = user.HasContentPathAccess(entity, entityService); + + if (hasPathAccess == false) + return ContentAccess.Denied; + + if (permissionsToCheck == null || permissionsToCheck.Length == 0) + return ContentAccess.Granted; + + //get the implicit/inherited permissions for the user for this path + return CheckPermissionsPath(entity.Path, user, userService, permissionsToCheck) + ? ContentAccess.Granted + : ContentAccess.Denied; + } + + /// + /// Checks if the user has access to the specified node and permissions set + /// + /// + /// + /// + /// + /// The item resolved if one was found for the id + /// + /// + public static ContentAccess CheckPermissions( + int nodeId, + IUser user, + IUserService userService, + IEntityService entityService, + out IUmbracoEntity entity, + params char[] permissionsToCheck) + { + if (user == null) throw new ArgumentNullException("user"); + if (userService == null) throw new ArgumentNullException("userService"); + if (entityService == null) throw new ArgumentNullException("entityService"); + + bool? hasPathAccess = null; + entity = null; + + if (nodeId == Constants.System.Root) + hasPathAccess = user.HasContentRootAccess(entityService); + else if (nodeId == Constants.System.RecycleBinContent) + hasPathAccess = user.HasContentBinAccess(entityService); + + if (hasPathAccess.HasValue) + return hasPathAccess.Value ? ContentAccess.Granted : ContentAccess.Denied; + + entity = entityService.Get(nodeId, UmbracoObjectTypes.Document); + if (entity == null) return ContentAccess.NotFound; + hasPathAccess = user.HasContentPathAccess(entity, entityService); + + if (hasPathAccess == false) + return ContentAccess.Denied; + + if (permissionsToCheck == null || permissionsToCheck.Length == 0) + return ContentAccess.Granted; + + //get the implicit/inherited permissions for the user for this path + return CheckPermissionsPath(entity.Path, user, userService, permissionsToCheck) + ? ContentAccess.Granted + : ContentAccess.Denied; + } + + /// + /// Checks if the user has access to the specified node and permissions set + /// + /// + /// + /// + /// + /// + /// The item resolved if one was found for the id + /// + /// + public static ContentAccess CheckPermissions( + int nodeId, + IUser user, + IUserService userService, + IContentService contentService, + IEntityService entityService, + out IContent contentItem, + params char[] permissionsToCheck) + { + if (user == null) throw new ArgumentNullException("user"); + if (userService == null) throw new ArgumentNullException("userService"); + if (contentService == null) throw new ArgumentNullException("contentService"); + if (entityService == null) throw new ArgumentNullException("entityService"); + + bool? hasPathAccess = null; + contentItem = null; + + if (nodeId == Constants.System.Root) + hasPathAccess = user.HasContentRootAccess(entityService); + else if (nodeId == Constants.System.RecycleBinContent) + hasPathAccess = user.HasContentBinAccess(entityService); + + if (hasPathAccess.HasValue) + return hasPathAccess.Value ? ContentAccess.Granted : ContentAccess.Denied; + + contentItem = contentService.GetById(nodeId); + if (contentItem == null) return ContentAccess.NotFound; + hasPathAccess = user.HasPathAccess(contentItem, entityService); + + if (hasPathAccess == false) + return ContentAccess.Denied; + + if (permissionsToCheck == null || permissionsToCheck.Length == 0) + return ContentAccess.Granted; + + //get the implicit/inherited permissions for the user for this path + return CheckPermissionsPath(contentItem.Path, user, userService, permissionsToCheck) + ? ContentAccess.Granted + : ContentAccess.Denied; + } + + private static bool CheckPermissionsPath(string path, IUser user, IUserService userService, params char[] permissionsToCheck) + { + //get the implicit/inherited permissions for the user for this path, + //if there is no content item for this id, than just use the id as the path (i.e. -1 or -20) + var permission = userService.GetPermissionsForPath(user, path); + + var allowed = true; + foreach (var p in permissionsToCheck) + { + if (permission == null + || permission.GetAllPermissions().Contains(p.ToString(CultureInfo.InvariantCulture)) == false) + { + allowed = false; + } + } + return allowed; + } + + public static bool HasPathAccess(string path, int[] startNodeIds, int recycleBinId) + { + if (string.IsNullOrWhiteSpace(path)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(path)); + + // check for no access + if (startNodeIds.Length == 0) + return false; + + // check for root access + if (startNodeIds.Contains(Constants.System.Root)) + return true; + + var formattedPath = string.Concat(",", path, ","); + + // only users with root access have access to the recycle bin, + // if the above check didn't pass then access is denied + if (formattedPath.Contains(string.Concat(",", recycleBinId, ","))) + return false; + + // check for a start node in the path + return startNodeIds.Any(x => formattedPath.Contains(string.Concat(",", x, ","))); + } + + internal static bool IsInBranchOfStartNode(string path, int[] startNodeIds, string[] startNodePaths, out bool hasPathAccess) + { + if (string.IsNullOrWhiteSpace(path)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(path)); + + hasPathAccess = false; + + // check for no access + if (startNodeIds.Length == 0) + return false; + + // check for root access + if (startNodeIds.Contains(Constants.System.Root)) + { + hasPathAccess = true; + return true; + } + + //is it self? + var self = startNodePaths.Any(x => x == path); + if (self) + { + hasPathAccess = true; + return true; + } + + //is it ancestor? + var ancestor = startNodePaths.Any(x => x.StartsWith(path)); + if (ancestor) + { + //hasPathAccess = false; + return true; + } + + //is it descendant? + var descendant = startNodePaths.Any(x => path.StartsWith(x)); + if (descendant) + { + hasPathAccess = true; + return true; + } + + return false; + } + } +} diff --git a/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs b/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs index 66b3982b49..06ba1ada79 100644 --- a/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs +++ b/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs @@ -30,12 +30,12 @@ namespace Umbraco.Core.Services string[] filterPropertyTypes = null) { filterContentTypes = filterContentTypes == null - ? new string[] { } - : filterContentTypes.Where(x => x.IsNullOrWhiteSpace() == false).ToArray(); + ? Array.Empty() + : filterContentTypes.Where(x => !x.IsNullOrWhiteSpace()).ToArray(); filterPropertyTypes = filterPropertyTypes == null - ? new string[] {} - : filterPropertyTypes.Where(x => x.IsNullOrWhiteSpace() == false).ToArray(); + ? Array.Empty() + : filterPropertyTypes.Where(x => !x.IsNullOrWhiteSpace()).ToArray(); //create the full list of property types to use as the filter //this is the combination of all property type aliases found in the content types passed in for the filter @@ -47,7 +47,7 @@ namespace Umbraco.Core.Services .Union(filterPropertyTypes) .ToArray(); - var sourceId = source != null ? source.Id : 0; + var sourceId = source?.Id ?? 0; // find out if any content type uses this content type var isUsing = allContentTypes.Where(x => x.ContentTypeComposition.Any(y => y.Id == sourceId)).ToArray(); @@ -161,6 +161,5 @@ namespace Umbraco.Core.Services return all; } - } } diff --git a/src/Umbraco.Core/Services/EntityXmlSerializer.cs b/src/Umbraco.Core/Services/EntityXmlSerializer.cs index e418c8d3e6..5b64584dc6 100644 --- a/src/Umbraco.Core/Services/EntityXmlSerializer.cs +++ b/src/Umbraco.Core/Services/EntityXmlSerializer.cs @@ -30,7 +30,7 @@ namespace Umbraco.Core.Services IEnumerable urlSegmentProviders, IContent content, bool published, - bool withDescendants = false) // fixme take care of usage! + bool withDescendants = false) //fixme take care of usage! only used for the packager { if (contentService == null) throw new ArgumentNullException(nameof(contentService)); if (dataTypeService == null) throw new ArgumentNullException(nameof(dataTypeService)); @@ -58,9 +58,15 @@ namespace Umbraco.Core.Services if (withDescendants) { - var descendants = contentService.GetDescendants(content).ToArray(); - var currentChildren = descendants.Where(x => x.ParentId == content.Id); - SerializeDescendants(contentService, dataTypeService, userService, localizationService, urlSegmentProviders, descendants, currentChildren, xml, published); + const int pageSize = 500; + var page = 0; + var total = long.MaxValue; + while(page * pageSize < total) + { + var children = contentService.GetPagedChildren(content.Id, page++, pageSize, out total); + SerializeChildren(contentService, dataTypeService, userService, localizationService, urlSegmentProviders, children, xml, published); + } + } return xml; @@ -103,9 +109,14 @@ namespace Umbraco.Core.Services if (withDescendants) { - var descendants = mediaService.GetDescendants(media).ToArray(); - var currentChildren = descendants.Where(x => x.ParentId == media.Id); - SerializeDescendants(mediaService, dataTypeService, userService, localizationService, urlSegmentProviders, descendants, currentChildren, xml); + const int pageSize = 500; + var page = 0; + var total = long.MaxValue; + while (page * pageSize < total) + { + var children = mediaService.GetPagedChildren(media.Id, page++, pageSize, out total); + SerializeChildren(mediaService, dataTypeService, userService, localizationService, urlSegmentProviders, children, xml); + } } return xml; @@ -451,7 +462,7 @@ namespace Umbraco.Core.Services } // exports an IContent item descendants. - private static void SerializeDescendants(IContentService contentService, IDataTypeService dataTypeService, IUserService userService, ILocalizationService localizationService, IEnumerable urlSegmentProviders, IContent[] originalDescendants, IEnumerable children, XElement xml, bool published) + private static void SerializeChildren(IContentService contentService, IDataTypeService dataTypeService, IUserService userService, ILocalizationService localizationService, IEnumerable urlSegmentProviders, IEnumerable children, XElement xml, bool published) { foreach (var child in children) { @@ -459,17 +470,20 @@ namespace Umbraco.Core.Services var childXml = Serialize(contentService, dataTypeService, userService, localizationService, urlSegmentProviders, child, published); xml.Add(childXml); - // capture id (out of closure) and get the grandChildren (children of the child) - var parentId = child.Id; - var grandChildren = originalDescendants.Where(x => x.ParentId == parentId); - - // recurse - SerializeDescendants(contentService, dataTypeService, userService, localizationService, urlSegmentProviders, originalDescendants, grandChildren, childXml, published); + const int pageSize = 500; + var page = 0; + var total = long.MaxValue; + while(page * pageSize < total) + { + var grandChildren = contentService.GetPagedChildren(child.Id, page++, pageSize, out total); + // recurse + SerializeChildren(contentService, dataTypeService, userService, localizationService, urlSegmentProviders, grandChildren, childXml, published); + } } } // exports an IMedia item descendants. - private static void SerializeDescendants(IMediaService mediaService, IDataTypeService dataTypeService, IUserService userService, ILocalizationService localizationService, IEnumerable urlSegmentProviders, IMedia[] originalDescendants, IEnumerable children, XElement xml) + private static void SerializeChildren(IMediaService mediaService, IDataTypeService dataTypeService, IUserService userService, ILocalizationService localizationService, IEnumerable urlSegmentProviders, IEnumerable children, XElement xml) { foreach (var child in children) { @@ -477,12 +491,15 @@ namespace Umbraco.Core.Services var childXml = Serialize(mediaService, dataTypeService, userService, localizationService, urlSegmentProviders, child); xml.Add(childXml); - // capture id (out of closure) and get the grandChildren (children of the child) - var parentId = child.Id; - var grandChildren = originalDescendants.Where(x => x.ParentId == parentId); - - // recurse - SerializeDescendants(mediaService, dataTypeService, userService, localizationService, urlSegmentProviders, originalDescendants, grandChildren, childXml); + const int pageSize = 500; + var page = 0; + var total = long.MaxValue; + while (page * pageSize < total) + { + var grandChildren = mediaService.GetPagedChildren(child.Id, page++, pageSize, out total); + // recurse + SerializeChildren(mediaService, dataTypeService, userService, localizationService, urlSegmentProviders, grandChildren, childXml); + } } } } diff --git a/src/Umbraco.Core/Services/IApplicationTreeService.cs b/src/Umbraco.Core/Services/IApplicationTreeService.cs index 5b6976c021..691a3a0b63 100644 --- a/src/Umbraco.Core/Services/IApplicationTreeService.cs +++ b/src/Umbraco.Core/Services/IApplicationTreeService.cs @@ -1,4 +1,6 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Linq; using Umbraco.Core.Models; namespace Umbraco.Core.Services @@ -40,7 +42,7 @@ namespace Umbraco.Core.Services /// /// Returns a ApplicationTree Array IEnumerable GetAll(); - + /// /// Gets the application tree for the applcation with the specified alias /// @@ -55,6 +57,14 @@ namespace Umbraco.Core.Services /// /// Returns a ApplicationTree Array IEnumerable GetApplicationTrees(string applicationAlias, bool onlyInitialized); + + /// + /// Gets the grouped application trees for the application with the specified alias + /// + /// + /// + /// + IDictionary> GetGroupedApplicationTrees(string applicationAlias, bool onlyInitialized); } /// @@ -113,6 +123,11 @@ namespace Umbraco.Core.Services throw new System.NotImplementedException(); } + public IDictionary> GetGroupedApplicationTrees(string applicationAlias, bool onlyInitialized) + { + throw new System.NotImplementedException(); + } + /// /// Gets the application tree for the applcation with the specified alias /// diff --git a/src/Umbraco.Core/Services/IAuditService.cs b/src/Umbraco.Core/Services/IAuditService.cs index 13d84f802e..f9b5aa2d87 100644 --- a/src/Umbraco.Core/Services/IAuditService.cs +++ b/src/Umbraco.Core/Services/IAuditService.cs @@ -12,7 +12,7 @@ namespace Umbraco.Core.Services /// public interface IAuditService : IService { - void Add(AuditType type, string comment, int userId, int objectId); + void Add(AuditType type, int userId, int objectId, string entityType, string comment, string parameters = null); IEnumerable GetLogs(int objectId); IEnumerable GetUserLogs(int userId, AuditType type, DateTime? sinceDate = null); diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs index 022bee8b41..22138a5e8c 100644 --- a/src/Umbraco.Core/Services/IContentService.cs +++ b/src/Umbraco.Core/Services/IContentService.cs @@ -78,26 +78,11 @@ namespace Umbraco.Core.Services /// IEnumerable GetByIds(IEnumerable ids); - /// - /// Gets documents of a given document type. - /// - IEnumerable GetByType(int documentTypeId); - /// /// Gets documents at a given level. /// IEnumerable GetByLevel(int level); - /// - /// Gets child documents of a given parent. - /// - IEnumerable GetChildren(int parentId); - - /// - /// Gets child documents of a document, (partially) matching a name. - /// - IEnumerable GetChildren(int parentId, string name); - /// /// Gets the parent of a document. /// @@ -119,20 +104,16 @@ namespace Umbraco.Core.Services IEnumerable GetAncestors(IContent content); /// - /// Gets descendant documents of a document. + /// Gets all versions of a document. /// - IEnumerable GetDescendants(int id); - - /// - /// Gets descendant documents of a document. - /// - IEnumerable GetDescendants(IContent content); + /// Versions are ordered with current first, then most recent first. + IEnumerable GetVersions(int id); /// /// Gets all versions of a document. /// /// Versions are ordered with current first, then most recent first. - IEnumerable GetVersions(int id); + IEnumerable GetVersionsSlim(int id, int skip, int take); /// /// Gets top versions of a document. @@ -151,31 +132,30 @@ namespace Umbraco.Core.Services IEnumerable GetRootContent(); /// - /// Gets documents with an expiration date greater then today. + /// Gets documents having an expiration date before (lower than, or equal to) a specified date. /// - IEnumerable GetContentForExpiration(); + /// An Enumerable list of objects + /// + /// The content returned from this method may be culture variant, in which case the resulting should be queried + /// for which culture(s) have been scheduled. + /// + IEnumerable GetContentForExpiration(DateTime date); /// - /// Gets documents with a release date greater then today. + /// Gets documents having a release date before (lower than, or equal to) a specified date. /// - IEnumerable GetContentForRelease(); + /// An Enumerable list of objects + /// + /// The content returned from this method may be culture variant, in which case the resulting should be queried + /// for which culture(s) have been scheduled. + /// + IEnumerable GetContentForRelease(DateTime date); /// /// Gets documents in the recycle bin. /// - IEnumerable GetContentInRecycleBin(); - - /// - /// Gets child documents of a parent. - /// - /// The parent identifier. - /// The page number. - /// The page size. - /// Total number of documents. - /// Search text filter. - /// Ordering infos. - IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalRecords, - string filter = null, Ordering ordering = null); + IEnumerable GetPagedContentInRecycleBin(long pageIndex, int pageSize, out long totalRecords, + IQuery filter = null, Ordering ordering = null); /// /// Gets child documents of a parent. @@ -187,20 +167,7 @@ namespace Umbraco.Core.Services /// Query filter. /// Ordering infos. IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalRecords, - IQuery filter, Ordering ordering = null); - - /// - /// Gets descendant documents of a given parent. - /// - /// The parent identifier. - /// The page number. - /// The page size. - /// Total number of documents. - /// A field to order by. - /// The ordering direction. - /// Search text filter. - IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalRecords, - string orderBy = "path", Direction orderDirection = Direction.Ascending, string filter = ""); + IQuery filter = null, Ordering ordering = null); /// /// Gets descendant documents of a given parent. @@ -214,7 +181,31 @@ namespace Umbraco.Core.Services /// A flag indicating whether the ordering field is a system field. /// Query filter. IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalRecords, - string orderBy, Direction orderDirection, bool orderBySystemField, IQuery filter); + IQuery filter = null, Ordering ordering = null); + + /// + /// Gets paged documents of a content content + /// + /// The page number. + /// The page number. + /// The page size. + /// Total number of documents. + /// Search text filter. + /// Ordering infos. + IEnumerable GetPagedOfType(int contentTypeId, long pageIndex, int pageSize, out long totalRecords, + IQuery filter, Ordering ordering = null); + + /// + /// Gets paged documents for specified content types + /// + /// The page number. + /// The page number. + /// The page size. + /// Total number of documents. + /// Search text filter. + /// Ordering infos. + IEnumerable GetPagedOfTypes(int[] contentTypeIds, long pageIndex, int pageSize, out long totalRecords, + IQuery filter, Ordering ordering = null); /// /// Counts documents of a given document type. @@ -331,12 +322,12 @@ namespace Umbraco.Core.Services /// /// Sorts documents. /// - bool Sort(IEnumerable items, int userId = 0, bool raiseEvents = true); + OperationResult Sort(IEnumerable items, int userId = 0, bool raiseEvents = true); /// /// Sorts documents. /// - bool Sort(IEnumerable ids, int userId = 0, bool raiseEvents = true); + OperationResult Sort(IEnumerable ids, int userId = 0, bool raiseEvents = true); #endregion @@ -362,6 +353,11 @@ namespace Umbraco.Core.Services /// A publishing document is a document with values that are being published, i.e. /// that have been published or cleared via and /// . + /// When one needs to publish or unpublish a single culture, or all cultures, using + /// and is the way to go. But if one needs to, say, publish two cultures and unpublish a third + /// one, in one go, then one needs to invoke and + /// on the content itself - this prepares the content, but does not commit anything - and then, invoke + /// to actually commit the changes to the database. /// The document is *always* saved, even when publishing fails. /// PublishResult SavePublishing(IContent content, int userId = 0, bool raiseEvents = true); @@ -369,12 +365,57 @@ namespace Umbraco.Core.Services /// /// Saves and publishes a document branch. /// + /// The root document. + /// A value indicating whether to force-publish documents that are not already published. + /// A culture, or "*" for all cultures. + /// The identifier of the user performing the operation. + /// + /// Unless specified, all cultures are re-published. Otherwise, one culture can be specified. To act on more + /// than one culture, see the other overloads of this method. + /// The parameter determines which documents are published. When false, + /// only those documents that are already published, are republished. When true, all documents are + /// published. The root of the branch is always published, regardless of . + /// IEnumerable SaveAndPublishBranch(IContent content, bool force, string culture = "*", int userId = 0); /// /// Saves and publishes a document branch. /// - IEnumerable SaveAndPublishBranch(IContent content, bool force, Func editing, Func publishCultures, int userId = 0); + /// The root document. + /// A value indicating whether to force-publish documents that are not already published. + /// The cultures to publish. + /// The identifier of the user performing the operation. + /// + /// The parameter determines which documents are published. When false, + /// only those documents that are already published, are republished. When true, all documents are + /// published. The root of the branch is always published, regardless of . + /// + IEnumerable SaveAndPublishBranch(IContent content, bool force, string[] cultures, int userId = 0); + + /// + /// Saves and publishes a document branch. + /// + /// The root document. + /// A value indicating whether to force-publish documents that are not already published. + /// A function determining cultures to publish. + /// A function publishing cultures. + /// The identifier of the user performing the operation. + /// + /// The parameter determines which documents are published. When false, + /// only those documents that are already published, are republished. When true, all documents are + /// published. The root of the branch is always published, regardless of . + /// The parameter is a function which determines whether a document has + /// changes to publish (else there is no need to publish it). If one wants to publish only a selection of + /// cultures, one may want to check that only properties for these cultures have changed. Otherwise, other + /// cultures may trigger an unwanted republish. + /// The parameter is a function to execute to publish cultures, on + /// each document. It can publish all, one, or a selection of cultures. It returns a boolean indicating + /// whether the cultures could be published. + /// + IEnumerable SaveAndPublishBranch(IContent content, bool force, + Func> shouldPublish, + Func, bool> publishCultures, + int userId = 0); /// /// Unpublishes a document. @@ -386,7 +427,7 @@ namespace Umbraco.Core.Services /// If the content type is variant, then culture can be either '*' or an actual culture, but neither null nor /// empty. If the content type is invariant, then culture can be either '*' or null or empty. /// - UnpublishResult Unpublish(IContent content, string culture = "*", int userId = 0); + PublishResult Unpublish(IContent content, string culture = "*", int userId = 0); /// /// Gets a value indicating whether a document is path-publishable. @@ -408,7 +449,7 @@ namespace Umbraco.Core.Services /// /// Publishes and unpublishes scheduled documents. /// - IEnumerable PerformScheduledPublish(); + IEnumerable PerformScheduledPublish(DateTime date); #endregion @@ -461,5 +502,21 @@ namespace Umbraco.Core.Services IContent CreateAndSave(string name, IContent parent, string contentTypeAlias, int userId = 0); #endregion + + #region Rollback + + /// + /// Rolls back the content to a specific version. + /// + /// The id of the content node. + /// The version id to roll back to. + /// An optional culture to roll back. + /// The identifier of the user who is performing the roll back. + /// + /// When no culture is specified, all cultures are rolled back. + /// + OperationResult Rollback(int id, int versionId, string culture = "*", int userId = 0); + + #endregion } } diff --git a/src/Umbraco.Core/Services/IMediaService.cs b/src/Umbraco.Core/Services/IMediaService.cs index 31c2e74fd4..ce46b197a0 100644 --- a/src/Umbraco.Core/Services/IMediaService.cs +++ b/src/Umbraco.Core/Services/IMediaService.cs @@ -10,7 +10,7 @@ using Umbraco.Core.Persistence.Querying; namespace Umbraco.Core.Services { - /// + /// /// Defines the Media Service, which is an easy access to operations involving /// public interface IMediaService : IContentServiceBase @@ -78,27 +78,6 @@ namespace Umbraco.Core.Services /// IMedia GetById(int id); - /// - /// Gets a collection of objects by Parent Id - /// - /// Id of the Parent to retrieve Children from - /// An Enumerable list of objects - IEnumerable GetChildren(int id); - - /// - /// Gets a collection of objects by Parent Id - /// - /// Id of the Parent to retrieve Children from - /// Page number - /// Page size - /// Total records query would return without paging - /// Field to order by - /// Direction to order by - /// Search text filter - /// An Enumerable list of objects - IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalRecords, - string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, string filter = ""); - /// /// Gets a collection of objects by Parent Id /// @@ -112,37 +91,7 @@ namespace Umbraco.Core.Services /// /// An Enumerable list of objects IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalRecords, - string orderBy, Direction orderDirection, bool orderBySystemField, IQuery filter); - - /// - /// Gets a collection of objects by Parent Id - /// - /// Id of the Parent to retrieve Children from - /// Page number - /// Page size - /// Total records query would return without paging - /// Field to order by - /// Direction to order by - /// Flag to indicate when ordering by system field - /// Search text filter - /// A list of content type Ids to filter the list by - /// An Enumerable list of objects - IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalRecords, - string orderBy, Direction orderDirection, bool orderBySystemField, string filter, int[] contentTypeFilter); - - /// - /// Gets a collection of objects by Parent Id - /// - /// Id of the Parent to retrieve Descendants from - /// Page number - /// Page size - /// Total records query would return without paging - /// Field to order by - /// Direction to order by - /// Search text filter - /// An Enumerable list of objects - IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalRecords, - string orderBy = "path", Direction orderDirection = Direction.Ascending, string filter = ""); + IQuery filter = null, Ordering ordering = null); /// /// Gets a collection of objects by Parent Id @@ -157,21 +106,31 @@ namespace Umbraco.Core.Services /// /// An Enumerable list of objects IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalRecords, - string orderBy, Direction orderDirection, bool orderBySystemField, IQuery filter); + IQuery filter = null, Ordering ordering = null); /// - /// Gets descendants of a object by its Id + /// Gets paged documents of a content content /// - /// Id of the Parent to retrieve descendants from - /// An Enumerable flat list of objects - IEnumerable GetDescendants(int id); + /// The page number. + /// The page number. + /// The page size. + /// Total number of documents. + /// Search text filter. + /// Ordering infos. + IEnumerable GetPagedOfType(int contentTypeId, long pageIndex, int pageSize, out long totalRecords, + IQuery filter = null, Ordering ordering = null); /// - /// Gets a collection of objects by the Id of the + /// Gets paged documents for specified content types /// - /// Id of the - /// An Enumerable list of objects - IEnumerable GetMediaOfMediaType(int id); + /// The page number. + /// The page number. + /// The page size. + /// Total number of documents. + /// Search text filter. + /// Ordering infos. + IEnumerable GetPagedOfTypes(int[] contentTypeIds, long pageIndex, int pageSize, out long totalRecords, + IQuery filter = null, Ordering ordering = null); /// /// Gets a collection of objects, which reside at the first level / root @@ -183,7 +142,8 @@ namespace Umbraco.Core.Services /// Gets a collection of an objects, which resides in the Recycle Bin /// /// An Enumerable list of objects - IEnumerable GetMediaInRecycleBin(); + IEnumerable GetPagedMediaInRecycleBin(long pageIndex, int pageSize, out long totalRecords, + IQuery filter = null, Ordering ordering = null); /// /// Moves an object to a new location @@ -191,8 +151,9 @@ namespace Umbraco.Core.Services /// The to move /// Id of the Media's new Parent /// Id of the User moving the Media - void Move(IMedia media, int parentId, int userId = 0); - + /// True if moving succeeded, otherwise False + Attempt Move(IMedia media, int parentId, int userId = 0); + /// /// Deletes an object by moving it to the Recycle Bin /// @@ -321,13 +282,6 @@ namespace Umbraco.Core.Services /// An Enumerable list of objects IEnumerable GetAncestors(IMedia media); - /// - /// Gets descendants of a object by its Id - /// - /// The Parent object to retrieve descendants from - /// An Enumerable flat list of objects - IEnumerable GetDescendants(IMedia media); - /// /// Gets the parent of the current media as an item. /// diff --git a/src/Umbraco.Core/Services/INotificationService.cs b/src/Umbraco.Core/Services/INotificationService.cs index 3af603a31c..a990b1e0ff 100644 --- a/src/Umbraco.Core/Services/INotificationService.cs +++ b/src/Umbraco.Core/Services/INotificationService.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; -using System.Web; using Umbraco.Core.Models; using Umbraco.Core.Models.Entities; using Umbraco.Core.Models.Membership; @@ -13,20 +12,6 @@ namespace Umbraco.Core.Services { public interface INotificationService : IService { - /// - /// Sends the notifications for the specified user regarding the specified node and action. - /// - /// - /// - /// - /// - /// - /// - /// - void SendNotifications(IUser operatingUser, IUmbracoEntity entity, string action, string actionName, HttpContextBase http, - Func createSubject, - Func createBody); - /// /// Sends the notifications for the specified user regarding the specified nodes and action. /// @@ -37,9 +22,9 @@ namespace Umbraco.Core.Services /// /// /// - void SendNotifications(IUser operatingUser, IEnumerable entities, string action, string actionName, HttpContextBase http, - Func createSubject, - Func createBody); + void SendNotifications(IUser operatingUser, IEnumerable entities, string action, string actionName, Uri siteUri, + Func<(IUser user, NotificationEmailSubjectParams subject), string> createSubject, + Func<(IUser user, NotificationEmailBodyParams body, bool isHtml), string> createBody); /// /// Gets the notifications for the user diff --git a/src/Umbraco.Core/Services/Implement/AuditService.cs b/src/Umbraco.Core/Services/Implement/AuditService.cs index 389a2337d1..d02d7f541b 100644 --- a/src/Umbraco.Core/Services/Implement/AuditService.cs +++ b/src/Umbraco.Core/Services/Implement/AuditService.cs @@ -27,11 +27,11 @@ namespace Umbraco.Core.Services.Implement _isAvailable = new Lazy(DetermineIsAvailable); } - public void Add(AuditType type, string comment, int userId, int objectId) + public void Add(AuditType type, int userId, int objectId, string entityType, string comment, string parameters = null) { using (var scope = ScopeProvider.CreateScope()) { - _auditRepository.Save(new AuditItem(objectId, comment, type, userId)); + _auditRepository.Save(new AuditItem(objectId, type, userId, entityType, comment, parameters)); scope.Complete(); } } diff --git a/src/Umbraco.Core/Services/Implement/ContentService.cs b/src/Umbraco.Core/Services/Implement/ContentService.cs index a849813b13..0412ecb409 100644 --- a/src/Umbraco.Core/Services/Implement/ContentService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentService.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; -using Umbraco.Core.Composing; using Umbraco.Core.Events; using Umbraco.Core.Exceptions; using Umbraco.Core.IO; @@ -328,7 +327,7 @@ namespace Umbraco.Core.Services.Implement if (withIdentity == false) return; - Audit(AuditType.New, $"Content '{content.Name}' was created with Id {content.Id}", content.CreatorId, content.Id); + Audit(AuditType.New, content.CreatorId, content.Id, $"Content '{content.Name}' was created with Id {content.Id}"); } #endregion @@ -405,28 +404,40 @@ namespace Umbraco.Core.Services.Implement } } - /// - /// Gets a collection of objects by the Id of the - /// - /// Id of the - /// An Enumerable list of objects - public IEnumerable GetByType(int id) + /// + public IEnumerable GetPagedOfType(int contentTypeId, long pageIndex, int pageSize, out long totalRecords + , IQuery filter = null, Ordering ordering = null) { + if (pageIndex < 0) throw new ArgumentOutOfRangeException(nameof(pageIndex)); + if (pageSize <= 0) throw new ArgumentOutOfRangeException(nameof(pageSize)); + + if (ordering == null) + ordering = Ordering.By("sortOrder"); + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.ReadLock(Constants.Locks.ContentTree); - var query = Query().Where(x => x.ContentTypeId == id); - return _documentRepository.Get(query); + return _documentRepository.GetPage( + Query().Where(x => x.ContentTypeId == contentTypeId), + pageIndex, pageSize, out totalRecords, filter, ordering); } } - internal IEnumerable GetPublishedContentOfContentType(int id) + /// + public IEnumerable GetPagedOfTypes(int[] contentTypeIds, long pageIndex, int pageSize, out long totalRecords, IQuery filter, Ordering ordering = null) { + if (pageIndex < 0) throw new ArgumentOutOfRangeException(nameof(pageIndex)); + if (pageSize <= 0) throw new ArgumentOutOfRangeException(nameof(pageSize)); + + if (ordering == null) + ordering = Ordering.By("sortOrder"); + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.ReadLock(Constants.Locks.ContentTree); - var query = Query().Where(x => x.ContentTypeId == id); - return _documentRepository.Get(query); + return _documentRepository.GetPage( + Query().Where(x => contentTypeIds.Contains(x.ContentTypeId)), + pageIndex, pageSize, out totalRecords, filter, ordering); } } @@ -474,6 +485,19 @@ namespace Umbraco.Core.Services.Implement } } + /// + /// Gets a collection of an objects versions by Id + /// + /// An Enumerable list of objects + public IEnumerable GetVersionsSlim(int id, int skip, int take) + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + scope.ReadLock(Constants.Locks.ContentTree); + return _documentRepository.GetAllVersionsSlim(id, skip, take); + } + } + /// /// Gets a list of all version Ids for the given content item ordered so latest is first /// @@ -523,21 +547,6 @@ namespace Umbraco.Core.Services.Implement } } - /// - /// Gets a collection of objects by Parent Id - /// - /// Id of the Parent to retrieve Children from - /// An Enumerable list of objects - public IEnumerable GetChildren(int id) - { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - scope.ReadLock(Constants.Locks.ContentTree); - var query = Query().Where(x => x.ParentId == id); - return _documentRepository.Get(query).OrderBy(x => x.SortOrder); - } - } - /// /// Gets a collection of published objects by Parent Id /// @@ -555,18 +564,7 @@ namespace Umbraco.Core.Services.Implement /// public IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalChildren, - string filter = null, Ordering ordering = null) - { - var filterQuery = filter.IsNullOrWhiteSpace() - ? null - : Query().Where(x => x.Name.Contains(filter)); - - return GetPagedChildren(id, pageIndex, pageSize, out totalChildren, filterQuery, ordering); - } - - /// - public IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalChildren, - IQuery filter, Ordering ordering = null) + IQuery filter = null, Ordering ordering = null) { if (pageIndex < 0) throw new ArgumentOutOfRangeException(nameof(pageIndex)); if (pageSize <= 0) throw new ArgumentOutOfRangeException(nameof(pageSize)); @@ -584,27 +582,16 @@ namespace Umbraco.Core.Services.Implement } /// - public IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalChildren, string orderBy = "Path", Direction orderDirection = Direction.Ascending, string filter = "") + public IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalChildren, + IQuery filter = null, Ordering ordering = null) { - var filterQuery = filter.IsNullOrWhiteSpace() - ? null - : Query().Where(x => x.Name.Contains(filter)); - - return GetPagedDescendants(id, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, true, filterQuery); - } - - /// - public IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalChildren, string orderBy, Direction orderDirection, bool orderBySystemField, IQuery filter) - { - if (pageIndex < 0) throw new ArgumentOutOfRangeException(nameof(pageIndex)); - if (pageSize <= 0) throw new ArgumentOutOfRangeException(nameof(pageSize)); + if (ordering == null) + ordering = Ordering.By("Path"); using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.ReadLock(Constants.Locks.ContentTree); - var query = Query(); - //if the id is System Root, then just get all if (id != Constants.System.Root) { @@ -614,65 +601,24 @@ namespace Umbraco.Core.Services.Implement totalChildren = 0; return Enumerable.Empty(); } - query.Where(x => x.Path.SqlStartsWith($"{contentPath[0].Path},", TextColumnType.NVarchar)); + return GetPagedDescendantsLocked(contentPath[0].Path, pageIndex, pageSize, out totalChildren, filter, ordering); } - - return _documentRepository.GetPage(query, pageIndex, pageSize, out totalChildren, filter, Ordering.By(orderBy, orderDirection, isCustomField: !orderBySystemField)); + return GetPagedDescendantsLocked(null, pageIndex, pageSize, out totalChildren, filter, ordering); } } - /// - /// Gets a collection of objects by its name or partial name - /// - /// Id of the Parent to retrieve Children from - /// Full or partial name of the children - /// An Enumerable list of objects - public IEnumerable GetChildren(int parentId, string name) + private IEnumerable GetPagedDescendantsLocked(string contentPath, long pageIndex, int pageSize, out long totalChildren, + IQuery filter, Ordering ordering) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - scope.ReadLock(Constants.Locks.ContentTree); - var query = Query().Where(x => x.ParentId == parentId && x.Name.Contains(name)); - return _documentRepository.Get(query); - } - } + if (pageIndex < 0) throw new ArgumentOutOfRangeException(nameof(pageIndex)); + if (pageSize <= 0) throw new ArgumentOutOfRangeException(nameof(pageSize)); + if (ordering == null) throw new ArgumentNullException(nameof(ordering)); - /// - /// Gets a collection of objects by Parent Id - /// - /// Id of the Parent to retrieve Descendants from - /// An Enumerable list of objects - public IEnumerable GetDescendants(int id) - { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - scope.ReadLock(Constants.Locks.ContentTree); - var content = GetById(id); - if (content == null) - { - scope.Complete(); // else causes rollback - return Enumerable.Empty(); - } - var pathMatch = content.Path + ","; - var query = Query().Where(x => x.Id != content.Id && x.Path.StartsWith(pathMatch)); - return _documentRepository.Get(query); - } - } + var query = Query(); + if (!contentPath.IsNullOrWhiteSpace()) + query.Where(x => x.Path.SqlStartsWith($"{contentPath},", TextColumnType.NVarchar)); - /// - /// Gets a collection of objects by Parent Id - /// - /// item to retrieve Descendants from - /// An Enumerable list of objects - public IEnumerable GetDescendants(IContent content) - { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - scope.ReadLock(Constants.Locks.ContentTree); - var pathMatch = content.Path + ","; - var query = Query().Where(x => x.Id != content.Id && x.Path.StartsWith(pathMatch)); - return _documentRepository.Get(query); - } + return _documentRepository.GetPage(query, pageIndex, pageSize, out totalChildren, filter, ordering); } /// @@ -727,31 +673,23 @@ namespace Umbraco.Core.Services.Implement } } - /// - /// Gets a collection of objects, which has an expiration date less than or equal to today. - /// - /// An Enumerable list of objects - public IEnumerable GetContentForExpiration() + /// + public IEnumerable GetContentForExpiration(DateTime date) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.ReadLock(Constants.Locks.ContentTree); - var query = Query().Where(x => x.Published && x.ExpireDate <= DateTime.Now); - return _documentRepository.Get(query); + return _documentRepository.GetContentForExpiration(date); } } - /// - /// Gets a collection of objects, which has a release date less than or equal to today. - /// - /// An Enumerable list of objects - public IEnumerable GetContentForRelease() + /// + public IEnumerable GetContentForRelease(DateTime date) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.ReadLock(Constants.Locks.ContentTree); - var query = Query().Where(x => x.Published == false && x.ReleaseDate <= DateTime.Now); - return _documentRepository.Get(query); + return _documentRepository.GetContentForRelease(date); } } @@ -759,13 +697,17 @@ namespace Umbraco.Core.Services.Implement /// Gets a collection of an objects, which resides in the Recycle Bin /// /// An Enumerable list of objects - public IEnumerable GetContentInRecycleBin() + public IEnumerable GetPagedContentInRecycleBin(long pageIndex, int pageSize, out long totalRecords, + IQuery filter = null, Ordering ordering = null) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { + if (ordering == null) + ordering = Ordering.By("Path"); + scope.ReadLock(Constants.Locks.ContentTree); var query = Query().Where(x => x.Path.StartsWith(Constants.System.RecycleBinContentPathPrefix)); - return _documentRepository.Get(query); + return _documentRepository.GetPage(query, pageIndex, pageSize, out totalRecords, filter, ordering); } } @@ -834,6 +776,15 @@ namespace Umbraco.Core.Services.Implement content.CreatorId = userId; content.WriterId = userId; + //track the cultures that have changed + var culturesChanging = content.ContentType.VariesByCulture() + ? content.CultureInfos.Where(x => x.Value.IsDirty()).Select(x => x.Key).ToList() + : null; + //TODO: Currently there's no way to change track which variant properties have changed, we only have change + // tracking enabled on all values on the Property which doesn't allow us to know which variants have changed. + // in this particular case, determining which cultures have changed works with the above with names since it will + // have always changed if it's been saved in the back office but that's not really fail safe. + _documentRepository.Save(content); if (raiseEvents) @@ -843,7 +794,17 @@ namespace Umbraco.Core.Services.Implement } var changeType = TreeChangeTypes.RefreshNode; scope.Events.Dispatch(TreeChanged, this, new TreeChange(content, changeType).ToEventArgs()); - Audit(AuditType.Save, "Saved by user", userId, content.Id); + + if (culturesChanging != null) + { + var langs = string.Join(", ", _languageRepository.GetMany() + .Where(x => culturesChanging.InvariantContains(x.IsoCode)) + .Select(x => x.CultureName)); + Audit(AuditType.SaveVariant, userId, content.Id, $"Saved languages: {langs}", langs); + } + else + Audit(AuditType.Save, userId, content.Id); + scope.Complete(); } @@ -883,7 +844,7 @@ namespace Umbraco.Core.Services.Implement scope.Events.Dispatch(Saved, this, saveEventArgs, "Saved"); } scope.Events.Dispatch(TreeChanged, this, treeChanges.ToEventArgs()); - Audit(AuditType.Save, "Bulk-saved by user", userId == -1 ? 0 : userId, Constants.System.Root); + Audit(AuditType.Save, userId == -1 ? 0 : userId, Constants.System.Root, "Saved multiple content"); scope.Complete(); } @@ -922,13 +883,13 @@ namespace Umbraco.Core.Services.Implement // publish the invariant values var publishInvariant = content.PublishCulture(null); if (!publishInvariant) - return new PublishResult(PublishResultType.FailedContentInvalid, evtMsgs, content); + return new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, content); } // publish the culture(s) var publishCulture = content.PublishCulture(culture); if (!publishCulture) - return new PublishResult(PublishResultType.FailedContentInvalid, evtMsgs, content); + return new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, content); // finally, "save publishing" // what happens next depends on whether the content can be published or not @@ -936,7 +897,7 @@ namespace Umbraco.Core.Services.Implement } /// - public UnpublishResult Unpublish(IContent content, string culture = "*", int userId = 0) + public PublishResult Unpublish(IContent content, string culture = "*", int userId = 0) { var evtMsgs = EventMessagesFactory.Get(); @@ -961,284 +922,490 @@ namespace Umbraco.Core.Services.Implement // if the content is not published, nothing to do if (!content.Published) - return new UnpublishResult(UnpublishResultType.SuccessAlready, evtMsgs, content); + return new PublishResult(PublishResultType.SuccessUnpublishAlready, evtMsgs, content); // all cultures = unpublish whole if (culture == "*" || (!content.ContentType.VariesByCulture() && culture == null)) { - ((Content) content).PublishedState = PublishedState.Unpublishing; + ((Content)content).PublishedState = PublishedState.Unpublishing; } else { // if the culture we want to unpublish was already unpublished, nothing to do if (!content.WasCulturePublished(culture)) - return new UnpublishResult(UnpublishResultType.SuccessAlready, evtMsgs, content); + return new PublishResult(PublishResultType.SuccessUnpublishAlready, evtMsgs, content); // unpublish the culture content.UnpublishCulture(culture); } // finally, "save publishing" - // what happens next depends on whether the content can be published or not - using (var scope = ScopeProvider.CreateScope()) - { - var saved = SavePublishing(content, userId); - if (saved.Success) - { - UnpublishResultType result; - if (culture == "*" || culture == null) - { - Audit(AuditType.Unpublish, "Unpublished by user", userId, content.Id); - result = UnpublishResultType.Success; - } - else - { - Audit(AuditType.Unpublish, $"Culture \"{culture}\" unpublished by user", userId, content.Id); - if (!content.Published) - Audit(AuditType.Unpublish, $"Unpublished (culture \"{culture}\" is mandatory) by user", userId, content.Id); - result = content.Published ? UnpublishResultType.SuccessCulture : UnpublishResultType.SuccessMandatoryCulture; - } - scope.Complete(); - return new UnpublishResult(result, evtMsgs, content); - } - - // failed - map result - var r = saved.Result == PublishResultType.FailedCancelledByEvent - ? UnpublishResultType.FailedCancelledByEvent - : UnpublishResultType.Failed; - return new UnpublishResult(r, evtMsgs, content); - } + return SavePublishing(content, userId); } /// public PublishResult SavePublishing(IContent content, int userId = 0, bool raiseEvents = true) + { + using (var scope = ScopeProvider.CreateScope()) + { + scope.WriteLock(Constants.Locks.ContentTree); + var result = SavePublishingInternal(scope, content, userId, raiseEvents); + scope.Complete(); + return result; + } + } + + private PublishResult SavePublishingInternal(IScope scope, IContent content, int userId = 0, bool raiseEvents = true, bool branchOne = false, bool branchRoot = false) { var evtMsgs = EventMessagesFactory.Get(); PublishResult publishResult = null; - UnpublishResult unpublishResult = null; + PublishResult unpublishResult = null; // nothing set = republish it all if (content.PublishedState != PublishedState.Publishing && content.PublishedState != PublishedState.Unpublishing) - ((Content) content).PublishedState = PublishedState.Publishing; + ((Content)content).PublishedState = PublishedState.Publishing; // state here is either Publishing or Unpublishing + // (even though, Publishing to unpublish a culture may end up unpublishing everything) var publishing = content.PublishedState == PublishedState.Publishing; var unpublishing = content.PublishedState == PublishedState.Unpublishing; - using (var scope = ScopeProvider.CreateScope()) - { - // is the content going to end up published, or unpublished? - if (publishing && content.ContentType.VariesByCulture()) - { - var publishedCultures = content.PublishedCultures.ToList(); - var cannotBePublished = publishedCultures.Count == 0; // no published cultures = cannot be published - if (!cannotBePublished) - { - var mandatoryCultures = _languageRepository.GetMany().Where(x => x.IsMandatory).Select(x => x.IsoCode); - cannotBePublished = mandatoryCultures.Any(x => !publishedCultures.Contains(x, StringComparer.OrdinalIgnoreCase)); // missing mandatory culture = cannot be published - } + var variesByCulture = content.ContentType.VariesByCulture(); - if (cannotBePublished) + //track cultures that are being published, changed, unpublished + IReadOnlyList culturesPublishing = null; + IReadOnlyList culturesUnpublishing = null; + IReadOnlyList culturesChanging = variesByCulture + ? content.CultureInfos.Where(x => x.Value.IsDirty()).Select(x => x.Key).ToList() + : null; + + var isNew = !content.HasIdentity; + var changeType = isNew ? TreeChangeTypes.RefreshNode : TreeChangeTypes.RefreshBranch; + var previouslyPublished = content.HasIdentity && content.Published; + + // always save + var saveEventArgs = new SaveEventArgs(content, evtMsgs); + if (raiseEvents && scope.Events.DispatchCancelable(Saving, this, saveEventArgs, "Saving")) + return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content); + + if (publishing) + { + culturesUnpublishing = content.GetCulturesUnpublishing(); + culturesPublishing = variesByCulture + ? content.PublishCultureInfos.Where(x => x.Value.IsDirty()).Select(x => x.Key).ToList() + : null; + + // ensure that the document can be published, and publish handling events, business rules, etc + publishResult = StrategyCanPublish(scope, content, userId, /*checkPath:*/ (!branchOne || branchRoot), culturesPublishing, culturesUnpublishing, evtMsgs); + if (publishResult.Success) + { + // note: StrategyPublish flips the PublishedState to Publishing! + publishResult = StrategyPublish(scope, content, userId, culturesPublishing, culturesUnpublishing, evtMsgs); + } + else + { + // in a branch, just give up + if (branchOne && !branchRoot) + return publishResult; + + //check for mandatory culture missing, and then unpublish document as a whole + if (publishResult.Result == PublishResultType.FailedPublishMandatoryCultureMissing) { publishing = false; unpublishing = content.Published; // if not published yet, nothing to do // we may end up in a state where we won't publish nor unpublish - // keep going, though, as we want to save anways + // keep going, though, as we want to save anyways } + + //fixme - casting + // reset published state from temp values (publishing, unpublishing) to original value + // (published, unpublished) in order to save the document, unchanged + ((Content)content).Published = content.Published; } + } - var isNew = !content.HasIdentity; - var changeType = isNew ? TreeChangeTypes.RefreshNode : TreeChangeTypes.RefreshBranch; - var previouslyPublished = content.HasIdentity && content.Published; + if (unpublishing) // won't happen in a branch + { + var newest = GetById(content.Id); // ensure we have the newest version - in scope + if (content.VersionId != newest.VersionId) + return new PublishResult(PublishResultType.FailedPublishConcurrencyViolation, evtMsgs, content); - scope.WriteLock(Constants.Locks.ContentTree); - - // always save - var saveEventArgs = new SaveEventArgs(content, evtMsgs); - if (raiseEvents && scope.Events.DispatchCancelable(Saving, this, saveEventArgs, "Saving")) + if (content.Published) { - scope.Complete(); - return new PublishResult(PublishResultType.FailedCancelledByEvent, evtMsgs, content); - } - - if (publishing) - { - // ensure that the document can be published, and publish + // ensure that the document can be unpublished, and unpublish // handling events, business rules, etc - // note: StrategyPublish flips the PublishedState to Publishing! - publishResult = StrategyCanPublish(scope, content, userId, /*checkPath:*/ true, evtMsgs); - if (publishResult.Success) - publishResult = StrategyPublish(scope, content, /*canPublish:*/ true, userId, evtMsgs); - if (!publishResult.Success) - ((Content) content).Published = content.Published; // reset published state = save unchanged - } - - if (unpublishing) - { - var newest = GetById(content.Id); // ensure we have the newest version - in scope - if (content.VersionId != newest.VersionId) // but use the original object if it's already the newest version - content = newest; - - if (content.Published) - { - // ensure that the document can be unpublished, and unpublish - // handling events, business rules, etc - // note: StrategyUnpublish flips the PublishedState to Unpublishing! - unpublishResult = StrategyCanUnpublish(scope, content, userId, evtMsgs); - if (unpublishResult.Success) - unpublishResult = StrategyUnpublish(scope, content, true, userId, evtMsgs); - if (!unpublishResult.Success) - ((Content) content).Published = content.Published; // reset published state = save unchanged - } + // note: StrategyUnpublish flips the PublishedState to Unpublishing! + // note: This unpublishes the entire document (not different variants) + unpublishResult = StrategyCanUnpublish(scope, content, userId, evtMsgs); + if (unpublishResult.Success) + unpublishResult = StrategyUnpublish(scope, content, userId, evtMsgs); else { - // already unpublished - optimistic concurrency collision, really, - // and I am not sure at all what we should do, better die fast, else - // we may end up corrupting the db - throw new InvalidOperationException("Concurrency collision."); + //fixme - casting + // reset published state from temp values (publishing, unpublishing) to original value + // (published, unpublished) in order to save the document, unchanged + ((Content)content).Published = content.Published; } } - - // save, always - if (content.HasIdentity == false) - content.CreatorId = userId; - content.WriterId = userId; - - // saving does NOT change the published version, unless PublishedState is Publishing or Unpublishing - _documentRepository.Save(content); - - // raise the Saved event, always - if (raiseEvents) + else { - saveEventArgs.CanCancel = false; - scope.Events.Dispatch(Saved, this, saveEventArgs, "Saved"); + // already unpublished - optimistic concurrency collision, really, + // and I am not sure at all what we should do, better die fast, else + // we may end up corrupting the db + throw new InvalidOperationException("Concurrency collision."); } + } - if (unpublishing) // we have tried to unpublish + // save, always + if (content.HasIdentity == false) + content.CreatorId = userId; + content.WriterId = userId; + + // saving does NOT change the published version, unless PublishedState is Publishing or Unpublishing + _documentRepository.Save(content); + + // raise the Saved event, always + if (raiseEvents) + { + saveEventArgs.CanCancel = false; + scope.Events.Dispatch(Saved, this, saveEventArgs, "Saved"); + } + + if (unpublishing) // we have tried to unpublish - won't happen in a branch + { + if (unpublishResult.Success) // and succeeded, trigger events { - if (unpublishResult.Success) // and succeeded, trigger events + // events and audit + scope.Events.Dispatch(Unpublished, this, new PublishEventArgs(content, false, false), "Unpublished"); + scope.Events.Dispatch(TreeChanged, this, new TreeChange(content, TreeChangeTypes.RefreshBranch).ToEventArgs()); + + if (culturesUnpublishing != null) { - // events and audit - scope.Events.Dispatch(Unpublished, this, new PublishEventArgs(content, false, false), "Unpublished"); - scope.Events.Dispatch(TreeChanged, this, new TreeChange(content, TreeChangeTypes.RefreshBranch).ToEventArgs()); - Audit(AuditType.Unpublish, "Unpublished by user", userId, content.Id); - scope.Complete(); - return new PublishResult(PublishResultType.Success, evtMsgs, content); + //If we are here, it means we tried unpublishing a culture but it was mandatory so now everything is unpublished + var langs = string.Join(", ", _languageRepository.GetMany() + .Where(x => culturesUnpublishing.InvariantContains(x.IsoCode)) + .Select(x => x.CultureName)); + Audit(AuditType.UnpublishVariant, userId, content.Id, $"Unpublished languages: {langs}", langs); + //log that the whole content item has been unpublished due to mandatory culture unpublished + Audit(AuditType.Unpublish, userId, content.Id, "Unpublished (mandatory language unpublished)"); } + else + Audit(AuditType.Unpublish, userId, content.Id); - // or, failed - scope.Events.Dispatch(TreeChanged, this, new TreeChange(content, changeType).ToEventArgs()); - scope.Complete(); // compete the save - return new PublishResult(PublishResultType.FailedToUnpublish, evtMsgs, content); // bah + return new PublishResult(PublishResultType.SuccessUnpublish, evtMsgs, content); } - if (publishing) // we have tried to publish - { - if (publishResult.Success) // and succeeded, trigger events - { - if (isNew == false && previouslyPublished == false) - changeType = TreeChangeTypes.RefreshBranch; // whole branch + // or, failed + scope.Events.Dispatch(TreeChanged, this, new TreeChange(content, changeType).ToEventArgs()); + return new PublishResult(PublishResultType.FailedUnpublish, evtMsgs, content); // bah + } - // invalidate the node/branch + if (publishing) // we have tried to publish + { + if (publishResult.Success) // and succeeded, trigger events + { + if (isNew == false && previouslyPublished == false) + changeType = TreeChangeTypes.RefreshBranch; // whole branch + + // invalidate the node/branch + if (!branchOne) // for branches, handled by SaveAndPublishBranch + { scope.Events.Dispatch(TreeChanged, this, new TreeChange(content, changeType).ToEventArgs()); scope.Events.Dispatch(Published, this, new PublishEventArgs(content, false, false), "Published"); - - // if was not published and now is... descendants that were 'published' (but - // had an unpublished ancestor) are 're-published' ie not explicitely published - // but back as 'published' nevertheless - if (isNew == false && previouslyPublished == false && HasChildren(content.Id)) - { - var descendants = GetPublishedDescendantsLocked(content).ToArray(); - scope.Events.Dispatch(Published, this, new PublishEventArgs(descendants, false, false), "Published"); - } - - Audit(AuditType.Publish, "Published by user", userId, content.Id); - scope.Complete(); - return publishResult; } - // or, failed - scope.Events.Dispatch(TreeChanged, this, new TreeChange(content, changeType).ToEventArgs()); - scope.Complete(); // compete the save + // if was not published and now is... descendants that were 'published' (but + // had an unpublished ancestor) are 're-published' ie not explicitely published + // but back as 'published' nevertheless + if (!branchOne && isNew == false && previouslyPublished == false && HasChildren(content.Id)) + { + var descendants = GetPublishedDescendantsLocked(content).ToArray(); + scope.Events.Dispatch(Published, this, new PublishEventArgs(descendants, false, false), "Published"); + } + + switch (publishResult.Result) + { + case PublishResultType.SuccessPublish: + Audit(AuditType.Publish, userId, content.Id); + break; + case PublishResultType.SuccessPublishCulture: + if (culturesPublishing != null) + { + var langs = string.Join(", ", _languageRepository.GetMany() + .Where(x => culturesPublishing.InvariantContains(x.IsoCode)) + .Select(x => x.CultureName)); + Audit(AuditType.PublishVariant, userId, content.Id, $"Published languages: {langs}", langs); + } + break; + case PublishResultType.SuccessUnpublishCulture: + if (culturesUnpublishing != null) + { + var langs = string.Join(", ", _languageRepository.GetMany() + .Where(x => culturesUnpublishing.InvariantContains(x.IsoCode)) + .Select(x => x.CultureName)); + Audit(AuditType.UnpublishVariant, userId, content.Id, $"Unpublished languages: {langs}", langs); + } + break; + } + return publishResult; } - - // both publishing and unpublishing are false - // this means that we wanted to publish, in a variant scenario, a document that - // was not published yet, and we could not, due to cultures issues - // - // raise event (we saved), report - - scope.Events.Dispatch(TreeChanged, this, new TreeChange(content, changeType).ToEventArgs()); - scope.Complete(); // compete the save - return new PublishResult(PublishResultType.FailedByCulture, evtMsgs, content); } + + // should not happen + if (branchOne && !branchRoot) + throw new Exception("panic"); + + //if publishing didn't happen or if it has failed, we still need to log which cultures were saved + if (!branchOne && (publishResult == null || !publishResult.Success)) + { + if (culturesChanging != null) + { + var langs = string.Join(", ", _languageRepository.GetMany() + .Where(x => culturesChanging.InvariantContains(x.IsoCode)) + .Select(x => x.CultureName)); + Audit(AuditType.SaveVariant, userId, content.Id, $"Saved languages: {langs}", langs); + } + else + { + Audit(AuditType.Save, userId, content.Id); + } + } + + // or, failed + scope.Events.Dispatch(TreeChanged, this, new TreeChange(content, changeType).ToEventArgs()); + return publishResult; } /// - public IEnumerable PerformScheduledPublish() + public IEnumerable PerformScheduledPublish(DateTime date) + => PerformScheduledPublishInternal(date).ToList(); + + // beware! this method yields results, so the returned IEnumerable *must* be + // enumerated for anything to happen - dangerous, so private + exposed via + // the public method above, which forces ToList(). + private IEnumerable PerformScheduledPublishInternal(DateTime date) { + var evtMsgs = EventMessagesFactory.Get(); + using (var scope = ScopeProvider.CreateScope()) { scope.WriteLock(Constants.Locks.ContentTree); - foreach (var d in GetContentForRelease()) + foreach (var d in _documentRepository.GetContentForRelease(date)) { PublishResult result; - try + if (d.ContentType.VariesByCulture()) { - d.ReleaseDate = null; - d.PublishCulture(); // fixme variants? - result = SaveAndPublish(d, userId: d.WriterId); + //find which cultures have pending schedules + var pendingCultures = d.ContentSchedule.GetPending(ContentScheduleAction.Release, date) + .Select(x => x.Culture) + .Distinct() + .ToList(); + + var publishing = true; + foreach (var culture in pendingCultures) + { + //Clear this schedule for this culture + d.ContentSchedule.Clear(culture, ContentScheduleAction.Release, date); + + if (d.Trashed) continue; // won't publish + + publishing &= d.PublishCulture(culture); //set the culture to be published + if (!publishing) break; // no point continuing + } + + if (d.Trashed) + result = new PublishResult(PublishResultType.FailedPublishIsTrashed, evtMsgs, d); + else if (!publishing) + result = new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, d); + else + result = SavePublishing(d, d.WriterId); + if (result.Success == false) Logger.Error(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result); + + yield return result; } - catch (Exception e) + else { - Logger.Error(e, "Failed to publish document id={DocumentId}, an exception was thrown.", d.Id); - throw; + //Clear this schedule + d.ContentSchedule.Clear(ContentScheduleAction.Release, date); + + result = d.Trashed + ? new PublishResult(PublishResultType.FailedPublishIsTrashed, evtMsgs, d) + : SaveAndPublish(d, userId: d.WriterId); + + if (result.Success == false) + Logger.Error(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result); + + yield return result; } - yield return result; } - foreach (var d in GetContentForExpiration()) + + foreach (var d in _documentRepository.GetContentForExpiration(date)) { - try + PublishResult result; + if (d.ContentType.VariesByCulture()) { - d.ExpireDate = null; - var result = Unpublish(d, userId: d.WriterId); + //find which cultures have pending schedules + var pendingCultures = d.ContentSchedule.GetPending(ContentScheduleAction.Expire, date) + .Select(x => x.Culture) + .Distinct() + .ToList(); + + foreach (var c in pendingCultures) + { + //Clear this schedule for this culture + d.ContentSchedule.Clear(c, ContentScheduleAction.Expire, date); + //set the culture to be published + d.UnpublishCulture(c); + } + + if (pendingCultures.Count > 0) + { + result = SavePublishing(d, d.WriterId); + if (result.Success == false) + Logger.Error(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result); + yield return result; + } + } + else + { + //Clear this schedule + d.ContentSchedule.Clear(ContentScheduleAction.Expire, date); + result = Unpublish(d, userId: d.WriterId); if (result.Success == false) Logger.Error(null, "Failed to unpublish document id={DocumentId}, reason={Reason}.", d.Id, result.Result); + yield return result; } - catch (Exception e) - { - Logger.Error(e, "Failed to unpublish document id={DocumentId}, an exception was thrown.", d.Id); - throw; - } + + } + _documentRepository.ClearSchedule(date); + scope.Complete(); } } + private bool SaveAndPublishBranch_PublishCultures(IContent c, HashSet culturesToPublish) + { + // variant content type - publish specified cultures + // invariant content type - publish only the invariant culture + return c.ContentType.VariesByCulture() + ? culturesToPublish.All(c.PublishCulture) + : c.PublishCulture(); + } + + private HashSet SaveAndPublishBranch_ShouldPublish3(ref HashSet cultures, string c, bool published, bool edited, bool isRoot, bool force) + { + // if published, republish + if (published) + { + if (cultures == null) cultures = new HashSet(); // empty means 'already published' + if (edited) cultures.Add(c); // means 'republish this culture' + return cultures; + } + + // if not published, publish if force/root else do nothing + if (!force && !isRoot) return cultures; // null means 'nothing to do' + + if (cultures == null) cultures = new HashSet(); + cultures.Add(c); // means 'publish this culture' + return cultures; + } + + /// public IEnumerable SaveAndPublishBranch(IContent content, bool force, string culture = "*", int userId = 0) { // note: EditedValue and PublishedValue are objects here, so it is important to .Equals() // and not to == them, else we would be comparing references, and that is a bad thing - bool IsEditing(IContent c, string l) - => c.PublishName != c.Name || - c.PublishedCultures.Any(x => c.GetCultureName(x) != c.GetPublishName(x)) || - c.Properties.Any(x => x.Values.Where(y => culture == "*" || y.Culture == l).Any(y => !y.EditedValue.Equals(y.PublishedValue))); + // determines whether the document is edited, and thus needs to be published, + // for the specified culture (it may be edited for other cultures and that + // should not trigger a publish). - return SaveAndPublishBranch(content, force, document => IsEditing(document, culture), document => document.PublishCulture(culture), userId); + // determines cultures to be published + // can be: null (content is not impacted), an empty set (content is impacted but already published), or cultures + HashSet ShouldPublish(IContent c) + { + var isRoot = c.Id == content.Id; + HashSet culturesToPublish = null; + + if (!c.ContentType.VariesByCulture()) // invariant content type + return SaveAndPublishBranch_ShouldPublish3(ref culturesToPublish, "*", c.Published, c.Edited, isRoot, force); + + if (culture != "*") // variant content type, specific culture + return SaveAndPublishBranch_ShouldPublish3(ref culturesToPublish, culture, c.IsCulturePublished(culture), c.IsCultureEdited(culture), isRoot, force); + + // variant content type, all cultures + if (c.Published) + { + // then some (and maybe all) cultures will be 'already published' (unless forcing), + // others will have to 'republish this culture' + foreach (var x in c.AvailableCultures) + SaveAndPublishBranch_ShouldPublish3(ref culturesToPublish, x, c.IsCulturePublished(x), c.IsCultureEdited(x), isRoot, force); + return culturesToPublish; + } + + // if not published, publish if force/root else do nothing + return force || isRoot + ? new HashSet { "*" } // "*" means 'publish all' + : null; // null means 'nothing to do' + } + + return SaveAndPublishBranch(content, force, ShouldPublish, SaveAndPublishBranch_PublishCultures, userId); + } + + /// + public IEnumerable SaveAndPublishBranch(IContent content, bool force, string[] cultures, int userId = 0) + { + // note: EditedValue and PublishedValue are objects here, so it is important to .Equals() + // and not to == them, else we would be comparing references, and that is a bad thing + + cultures = cultures ?? Array.Empty(); + + // determines cultures to be published + // can be: null (content is not impacted), an empty set (content is impacted but already published), or cultures + HashSet ShouldPublish(IContent c) + { + var isRoot = c.Id == content.Id; + HashSet culturesToPublish = null; + + if (!c.ContentType.VariesByCulture()) // invariant content type + return SaveAndPublishBranch_ShouldPublish3(ref culturesToPublish, "*", c.Published, c.Edited, isRoot, force); + + // variant content type, specific cultures + if (c.Published) + { + // then some (and maybe all) cultures will be 'already published' (unless forcing), + // others will have to 'republish this culture' + foreach (var x in cultures) + SaveAndPublishBranch_ShouldPublish3(ref culturesToPublish, x, c.IsCulturePublished(x), c.IsCultureEdited(x), isRoot, force); + return culturesToPublish; + } + + // if not published, publish if force/root else do nothing + return force || isRoot + ? new HashSet(cultures) // means 'publish specified cultures' + : null; // null means 'nothing to do' + } + + return SaveAndPublishBranch(content, force, ShouldPublish, SaveAndPublishBranch_PublishCultures, userId); } /// public IEnumerable SaveAndPublishBranch(IContent document, bool force, - Func editing, Func publishCultures, int userId = 0) + Func> shouldPublish, + Func, bool> publishCultures, + int userId = 0) { + if (shouldPublish == null) throw new ArgumentNullException(nameof(shouldPublish)); + if (publishCultures == null) throw new ArgumentNullException(nameof(publishCultures)); + var evtMsgs = EventMessagesFactory.Get(); var results = new List(); var publishedDocuments = new List(); @@ -1250,44 +1417,63 @@ namespace Umbraco.Core.Services.Implement // fixme events?! if (!document.HasIdentity) - throw new InvalidOperationException("Do not branch-publish a new document."); + throw new InvalidOperationException("Cannot not branch-publish a new document."); - var publishedState = ((Content) document).PublishedState; + var publishedState = ((Content)document).PublishedState; if (publishedState == PublishedState.Publishing) - throw new InvalidOperationException("Do not publish values when publishing branches."); + throw new InvalidOperationException("Cannot mix PublishCulture and SaveAndPublishBranch."); // deal with the branch root - if it fails, abort - var result = SaveAndPublishBranchOne(scope, document, editing, publishCultures, true, publishedDocuments, evtMsgs, userId); - results.Add(result); - if (!result.Success) return results; + var result = SaveAndPublishBranchOne(scope, document, shouldPublish, publishCultures, true, publishedDocuments, evtMsgs, userId); + if (result != null) + { + results.Add(result); + if (!result.Success) return results; + } // deal with descendants // if one fails, abort its branch var exclude = new HashSet(); - foreach (var d in GetDescendants(document)) + + int count; + var page = 0; + const int pageSize = 100; + do { - // if parent is excluded, exclude document and ignore - // if not forcing, and not publishing, exclude document and ignore - if (exclude.Contains(d.ParentId) || !force && !d.Published) + count = 0; + // important to order by Path ASC so make it explicit in case defaults change + // ReSharper disable once RedundantArgumentDefaultValue + foreach (var d in GetPagedDescendants(document.Id, page, pageSize, out _, ordering: Ordering.By("Path", Direction.Ascending))) { + count++; + + // if parent is excluded, exclude child too + if (exclude.Contains(d.ParentId)) + { + exclude.Add(d.Id); + continue; + } + + // no need to check path here, parent has to be published here + result = SaveAndPublishBranchOne(scope, d, shouldPublish, publishCultures, false, publishedDocuments, evtMsgs, userId); + if (result != null) + { + results.Add(result); + if (result.Success) continue; + } + + // if we could not publish the document, cut its branch exclude.Add(d.Id); - continue; } - // no need to check path here, - // 1. because we know the parent is path-published (we just published it) - // 2. because it would not work as nothing's been written out to the db until the uow completes - result = SaveAndPublishBranchOne(scope, d, editing, publishCultures, false, publishedDocuments, evtMsgs, userId); - results.Add(result); - if (result.Success) continue; + page++; + } while (count > 0); - // abort branch - exclude.Add(d.Id); - } + Audit(AuditType.Publish, userId, document.Id, "Branch published"); + // trigger events for the entire branch scope.Events.Dispatch(TreeChanged, this, new TreeChange(document, TreeChangeTypes.RefreshBranch).ToEventArgs()); scope.Events.Dispatch(Published, this, new PublishEventArgs(publishedDocuments, false, false), "Published"); - Audit(AuditType.Publish, "Branch published by user", userId, document.Id); scope.Complete(); } @@ -1295,36 +1481,30 @@ namespace Umbraco.Core.Services.Implement return results; } + // shouldPublish: a function determining whether the document has changes that need to be published + // note - 'force' is handled by 'editing' + // publishValues: a function publishing values (using the appropriate PublishCulture calls) private PublishResult SaveAndPublishBranchOne(IScope scope, IContent document, - Func editing, Func publishValues, - bool checkPath, - List publishedDocuments, + Func> shouldPublish, + Func, bool> publishCultures, + bool isRoot, + ICollection publishedDocuments, EventMessages evtMsgs, int userId) { - // if already published, and values haven't changed - i.e. not changing anything - // nothing to do - fixme - unless we *want* to bump dates? - if (document.Published && (editing == null || !editing(document))) - return new PublishResult(PublishResultType.SuccessAlready, evtMsgs, document); + var culturesToPublish = shouldPublish(document); + if (culturesToPublish == null) // null = do not include + return null; + if (culturesToPublish.Count == 0) // empty = already published + return new PublishResult(PublishResultType.SuccessPublishAlready, evtMsgs, document); // publish & check if values are valid - if (publishValues != null && !publishValues(document)) - return new PublishResult(PublishResultType.FailedContentInvalid, evtMsgs, document); + if (!publishCultures(document, culturesToPublish)) + return new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, document); - // check if we can publish - var result = StrategyCanPublish(scope, document, userId, checkPath, evtMsgs); - if (!result.Success) - return result; - - // publish - should be successful - var publishResult = StrategyPublish(scope, document, /*canPublish:*/ true, userId, evtMsgs); - if (!publishResult.Success) - throw new Exception("oops: failed to publish."); - - // save - document.WriterId = userId; - _documentRepository.Save(document); - publishedDocuments.Add(document); - return publishResult; + var result = SavePublishingInternal(scope, document, userId, branchOne: true, branchRoot: isRoot); + if (result.Success) + publishedDocuments.Add(document); + return result; } #endregion @@ -1356,7 +1536,7 @@ namespace Umbraco.Core.Services.Implement DeleteLocked(scope, content); scope.Events.Dispatch(TreeChanged, this, new TreeChange(content, TreeChangeTypes.Remove).ToEventArgs()); - Audit(AuditType.Delete, "Deleted by user", userId, content.Id); + Audit(AuditType.Delete, userId, content.Id); scope.Complete(); } @@ -1366,25 +1546,8 @@ namespace Umbraco.Core.Services.Implement private void DeleteLocked(IScope scope, IContent content) { - // then recursively delete descendants, bottom-up - // just repository.Delete + an event - var stack = new Stack(); - stack.Push(content); - var level = 1; - while (stack.Count > 0) + void DoDelete(IContent c) { - var c = stack.Peek(); - IContent[] cc; - if (c.Level == level) - while ((cc = c.Children(this).ToArray()).Length > 0) - { - foreach (var ci in cc) - stack.Push(ci); - c = cc[cc.Length - 1]; - } - c = stack.Pop(); - level = c.Level; - _documentRepository.Delete(c); var args = new DeleteEventArgs(c, false); // raise event & get flagged files scope.Events.Dispatch(Deleted, this, args, nameof(Deleted)); @@ -1393,6 +1556,18 @@ namespace Umbraco.Core.Services.Implement _mediaFileSystem.DeleteFiles(args.MediaFilesToDelete, // remove flagged files (file, e) => Logger.Error(e, "An error occurred while deleting file attached to nodes: {File}", file)); } + + const int pageSize = 500; + var page = 0; + var total = long.MaxValue; + while (page * pageSize < total) + { + //get descendants - ordered from deepest to shallowest + var descendants = GetPagedDescendants(content.Id, page, pageSize, out total, ordering: Ordering.By("Path", Direction.Descending)); + foreach (var c in descendants) + DoDelete(c); + } + DoDelete(content); } //TODO: @@ -1424,7 +1599,7 @@ namespace Umbraco.Core.Services.Implement deleteRevisionsEventArgs.CanCancel = false; scope.Events.Dispatch(DeletedVersions, this, deleteRevisionsEventArgs); - Audit(AuditType.Delete, "Delete (by version date) by user", userId, Constants.System.Root); + Audit(AuditType.Delete, userId, Constants.System.Root, "Delete (by version date)"); scope.Complete(); } @@ -1461,7 +1636,7 @@ namespace Umbraco.Core.Services.Implement _documentRepository.DeleteVersion(versionId); scope.Events.Dispatch(DeletedVersions, this, new DeleteRevisionsEventArgs(id, false,/* specificVersion:*/ versionId)); - Audit(AuditType.Delete, "Delete (by version) by user", userId, Constants.System.Root); + Audit(AuditType.Delete, userId, Constants.System.Root, "Delete (by version)"); scope.Complete(); } @@ -1506,7 +1681,7 @@ namespace Umbraco.Core.Services.Implement moveEventArgs.CanCancel = false; moveEventArgs.MoveInfoCollection = moveInfo; scope.Events.Dispatch(Trashed, this, moveEventArgs, nameof(Trashed)); - Audit(AuditType.Move, "Moved to Recycle Bin by user", userId, content.Id); + Audit(AuditType.Move, userId, content.Id, "Moved to recycle bin"); scope.Complete(); } @@ -1564,7 +1739,7 @@ namespace Umbraco.Core.Services.Implement { // however, it had been masked when being trashed, so there's no need for // any special event here - just change its state - ((Content) content).PublishedState = PublishedState.Unpublishing; + ((Content)content).PublishedState = PublishedState.Unpublishing; } PerformMoveLocked(content, parentId, parent, userId, moves, trashed); @@ -1578,7 +1753,7 @@ namespace Umbraco.Core.Services.Implement moveEventArgs.MoveInfoCollection = moveInfo; moveEventArgs.CanCancel = false; scope.Events.Dispatch(Moved, this, moveEventArgs, nameof(Moved)); - Audit(AuditType.Move, "Moved by user", userId, content.Id); + Audit(AuditType.Move, userId, content.Id); scope.Complete(); } @@ -1602,8 +1777,8 @@ namespace Umbraco.Core.Services.Implement moves.Add(Tuple.Create(content, content.Path)); // capture original path - // get before moving, in case uow is immediate - var descendants = GetDescendants(content); + //need to store the original path to lookup descendants based on it below + var originalPath = content.Path; // these will be updated by the repo because we changed parentId //content.Path = (parent == null ? "-1" : parent.Path) + "," + content.Id; @@ -1616,23 +1791,29 @@ namespace Umbraco.Core.Services.Implement //paths[content.Id] = content.Path; paths[content.Id] = (parent == null ? (parentId == Constants.System.RecycleBinContent ? "-1,-20" : "-1") : parent.Path) + "," + content.Id; - foreach (var descendant in descendants) + const int pageSize = 500; + var page = 0; + var total = long.MaxValue; + while (page * pageSize < total) { - moves.Add(Tuple.Create(descendant, descendant.Path)); // capture original path + var descendants = GetPagedDescendantsLocked(originalPath, page++, pageSize, out total, null, Ordering.By("Path", Direction.Ascending)); + foreach (var descendant in descendants) + { + moves.Add(Tuple.Create(descendant, descendant.Path)); // capture original path - // update path and level since we do not update parentId - if (paths.ContainsKey(descendant.ParentId) == false) - Console.WriteLine("oops on " + descendant.ParentId + " for " + content.Path + " " + parent?.Path); - descendant.Path = paths[descendant.Id] = paths[descendant.ParentId] + "," + descendant.Id; - Console.WriteLine("path " + descendant.Id + " = " + paths[descendant.Id]); - descendant.Level += levelDelta; - PerformMoveContentLocked(descendant, userId, trash); + // update path and level since we do not update parentId + descendant.Path = paths[descendant.Id] = paths[descendant.ParentId] + "," + descendant.Id; + descendant.Level += levelDelta; + PerformMoveContentLocked(descendant, userId, trash); + } } + } private void PerformMoveContentLocked(IContent content, int userId, bool? trash) { - if (trash.HasValue) ((ContentBase) content).Trashed = trash.Value; + //fixme no casting + if (trash.HasValue) ((ContentBase)content).Trashed = trash.Value; content.WriterId = userId; _documentRepository.Save(content); } @@ -1675,7 +1856,7 @@ namespace Umbraco.Core.Services.Implement recycleBinEventArgs.RecycleBinEmptiedSuccessfully = true; // oh my?! scope.Events.Dispatch(EmptiedRecycleBin, this, recycleBinEventArgs); scope.Events.Dispatch(TreeChanged, this, deleted.Select(x => new TreeChange(x, TreeChangeTypes.Remove)).ToEventArgs()); - Audit(AuditType.Delete, "Recycle Bin emptied by user", 0, Constants.System.RecycleBinContent); + Audit(AuditType.Delete, 0, Constants.System.RecycleBinContent, "Recycle bin emptied"); scope.Complete(); } @@ -1736,7 +1917,7 @@ namespace Umbraco.Core.Services.Implement // a copy is not published (but not really unpublishing either) // update the create author and last edit author if (copy.Published) - ((Content) copy).Published = false; + ((Content)copy).Published = false; copy.CreatorId = userId; copy.WriterId = userId; @@ -1760,29 +1941,36 @@ namespace Umbraco.Core.Services.Implement if (recursive) // process descendants { - foreach (var descendant in GetDescendants(content)) + const int pageSize = 500; + var page = 0; + var total = long.MaxValue; + while (page * pageSize < total) { - // if parent has not been copied, skip, else gets its copy id - if (idmap.TryGetValue(descendant.ParentId, out parentId) == false) continue; + var descendants = GetPagedDescendants(content.Id, page++, pageSize, out total); + foreach (var descendant in descendants) + { + // if parent has not been copied, skip, else gets its copy id + if (idmap.TryGetValue(descendant.ParentId, out parentId) == false) continue; - var descendantCopy = descendant.DeepCloneWithResetIdentities(); - descendantCopy.ParentId = parentId; + var descendantCopy = descendant.DeepCloneWithResetIdentities(); + descendantCopy.ParentId = parentId; - if (scope.Events.DispatchCancelable(Copying, this, new CopyEventArgs(descendant, descendantCopy, parentId))) - continue; + if (scope.Events.DispatchCancelable(Copying, this, new CopyEventArgs(descendant, descendantCopy, parentId))) + continue; - // a copy is not published (but not really unpublishing either) - // update the create author and last edit author - if (descendantCopy.Published) - ((Content) descendantCopy).Published = false; - descendantCopy.CreatorId = userId; - descendantCopy.WriterId = userId; + // a copy is not published (but not really unpublishing either) + // update the create author and last edit author + if (descendantCopy.Published) + ((Content)descendantCopy).Published = false; + descendantCopy.CreatorId = userId; + descendantCopy.WriterId = userId; - // save and flush (see above) - _documentRepository.Save(descendantCopy); + // save and flush (see above) + _documentRepository.Save(descendantCopy); - copies.Add(Tuple.Create(descendant, descendantCopy)); - idmap[descendant.Id] = descendantCopy.Id; + copies.Add(Tuple.Create(descendant, descendantCopy)); + idmap[descendant.Id] = descendantCopy.Id; + } } } @@ -1793,7 +1981,7 @@ namespace Umbraco.Core.Services.Implement scope.Events.Dispatch(TreeChanged, this, new TreeChange(copy, TreeChangeTypes.RefreshBranch).ToEventArgs()); foreach (var x in copies) scope.Events.Dispatch(Copied, this, new CopyEventArgs(x.Item1, x.Item2, false, x.Item2.ParentId, relateToOriginal)); - Audit(AuditType.Copy, "Copy Content performed by user", userId, content.Id); + Audit(AuditType.Copy, userId, content.Id); scope.Complete(); } @@ -1818,16 +2006,35 @@ namespace Umbraco.Core.Services.Implement return false; } + //track the cultures changing for auditing + var culturesChanging = content.ContentType.VariesByCulture() + ? string.Join(",", content.CultureInfos.Where(x => x.Value.IsDirty()).Select(x => x.Key)) + : null; + + //TODO: Currently there's no way to change track which variant properties have changed, we only have change + // tracking enabled on all values on the Property which doesn't allow us to know which variants have changed. + // in this particular case, determining which cultures have changed works with the above with names since it will + // have always changed if it's been saved in the back office but that's not really fail safe. + //Save before raising event - // fixme - nesting uow? - Save(content, userId); + var saveResult = Save(content, userId); + + // always complete (but maybe return a failed status) + scope.Complete(); + + if (!saveResult.Success) + return saveResult.Success; sendToPublishEventArgs.CanCancel = false; scope.Events.Dispatch(SentToPublish, this, sendToPublishEventArgs); - Audit(AuditType.SendToPublish, "Send to Publish performed by user", content.WriterId, content.Id); - } - return true; + if (culturesChanging != null) + Audit(AuditType.SendToPublishVariant, userId, content.Id, $"Send To Publish for cultures: {culturesChanging}", culturesChanging); + else + Audit(AuditType.SendToPublish, content.WriterId, content.Id); + + return saveResult.Success; + } } /// @@ -1841,17 +2048,19 @@ namespace Umbraco.Core.Services.Implement /// /// /// - /// True if sorting succeeded, otherwise False - public bool Sort(IEnumerable items, int userId = 0, bool raiseEvents = true) + /// Result indicating what action was taken when handling the command. + public OperationResult Sort(IEnumerable items, int userId = 0, bool raiseEvents = true) { + var evtMsgs = EventMessagesFactory.Get(); + var itemsA = items.ToArray(); - if (itemsA.Length == 0) return true; + if (itemsA.Length == 0) return new OperationResult(OperationResultType.NoOperation, evtMsgs); using (var scope = ScopeProvider.CreateScope()) { scope.WriteLock(Constants.Locks.ContentTree); - var ret = Sort(scope, itemsA, userId, raiseEvents); + var ret = Sort(scope, itemsA, userId, evtMsgs, raiseEvents); scope.Complete(); return ret; } @@ -1868,28 +2077,38 @@ namespace Umbraco.Core.Services.Implement /// /// /// - /// True if sorting succeeded, otherwise False - public bool Sort(IEnumerable ids, int userId = 0, bool raiseEvents = true) + /// Result indicating what action was taken when handling the command. + public OperationResult Sort(IEnumerable ids, int userId = 0, bool raiseEvents = true) { + var evtMsgs = EventMessagesFactory.Get(); + var idsA = ids.ToArray(); - if (idsA.Length == 0) return true; + if (idsA.Length == 0) return new OperationResult(OperationResultType.NoOperation, evtMsgs); using (var scope = ScopeProvider.CreateScope()) { scope.WriteLock(Constants.Locks.ContentTree); var itemsA = GetByIds(idsA).ToArray(); - var ret = Sort(scope, itemsA, userId, raiseEvents); + var ret = Sort(scope, itemsA, userId, evtMsgs, raiseEvents); scope.Complete(); return ret; } } - private bool Sort(IScope scope, IContent[] itemsA, int userId, bool raiseEvents) + private OperationResult Sort(IScope scope, IContent[] itemsA, int userId, EventMessages evtMsgs, bool raiseEvents) { var saveEventArgs = new SaveEventArgs(itemsA); - if (raiseEvents && scope.Events.DispatchCancelable(Saving, this, saveEventArgs, "Saving")) - return false; + if (raiseEvents) + { + //raise cancelable sorting event + if (scope.Events.DispatchCancelable(Saving, this, saveEventArgs, nameof(Sorting))) + return OperationResult.Cancel(evtMsgs); + + //raise saving event (this one cannot be canceled) + saveEventArgs.CanCancel = false; + scope.Events.Dispatch(Saving, this, saveEventArgs, nameof(Saving)); + } var published = new List(); var saved = new List(); @@ -1921,8 +2140,9 @@ namespace Umbraco.Core.Services.Implement if (raiseEvents) { - saveEventArgs.CanCancel = false; - scope.Events.Dispatch(Saved, this, saveEventArgs, "Saved"); + //first saved, then sorted + scope.Events.Dispatch(Saved, this, saveEventArgs, nameof(Saved)); + scope.Events.Dispatch(Sorted, this, saveEventArgs, nameof(Sorted)); } scope.Events.Dispatch(TreeChanged, this, saved.Select(x => new TreeChange(x, TreeChangeTypes.RefreshNode)).ToEventArgs()); @@ -1930,8 +2150,8 @@ namespace Umbraco.Core.Services.Implement if (raiseEvents && published.Any()) scope.Events.Dispatch(Published, this, new PublishEventArgs(published, false, false), "Published"); - Audit(AuditType.Sort, "Sorting content performed by user", userId, 0); - return true; + Audit(AuditType.Sort, userId, 0, "Sorting content performed by user"); + return OperationResult.Succeed(evtMsgs); } #endregion @@ -1977,9 +2197,9 @@ namespace Umbraco.Core.Services.Implement #region Private Methods - private void Audit(AuditType type, string message, int userId, int objectId) + private void Audit(AuditType type, int userId, int objectId, string message = null, string parameters = null) { - _auditRepository.Save(new AuditItem(objectId, message, type, userId)); + _auditRepository.Save(new AuditItem(objectId, type, userId, ObjectTypes.GetName(UmbracoObjectTypes.Document), message, parameters)); } #endregion @@ -2006,6 +2226,16 @@ namespace Umbraco.Core.Services.Implement /// public static event TypedEventHandler DeletedVersions; + /// + /// Occurs before Sorting + /// + public static event TypedEventHandler> Sorting; + + /// + /// Occurs after Sorting + /// + public static event TypedEventHandler> Sorted; + /// /// Occurs before Save /// @@ -2124,110 +2354,197 @@ namespace Umbraco.Core.Services.Implement #region Publishing Strategies - // ensures that a document can be published - internal PublishResult StrategyCanPublish(IScope scope, IContent content, int userId, bool checkPath, EventMessages evtMsgs) + /// + /// Ensures that a document can be published + /// + /// + /// + /// + /// + /// + /// + private PublishResult StrategyCanPublish(IScope scope, IContent content, int userId, bool checkPath, IReadOnlyList culturesPublishing, IReadOnlyList culturesUnpublishing, EventMessages evtMsgs) { // raise Publishing event if (scope.Events.DispatchCancelable(Publishing, this, new PublishEventArgs(content, evtMsgs))) { Logger.Info("Document {ContentName} (id={ContentId}) cannot be published: {Reason}", content.Name, content.Id, "publishing was cancelled"); - return new PublishResult(PublishResultType.FailedCancelledByEvent, evtMsgs, content); + return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content); + } + + var variesByCulture = content.ContentType.VariesByCulture(); + + //First check if mandatory languages fails, if this fails it will mean anything that the published flag on the document will + // be changed to Unpublished and any culture currently published will not be visible. + if (variesByCulture) + { + if (content.Published && culturesPublishing.Count == 0 && culturesUnpublishing.Count == 0) // no published cultures = cannot be published + return new PublishResult(PublishResultType.FailedPublishNothingToPublish, evtMsgs, content); + + // missing mandatory culture = cannot be published + var mandatoryCultures = _languageRepository.GetMany().Where(x => x.IsMandatory).Select(x => x.IsoCode); + var mandatoryMissing = mandatoryCultures.Any(x => !content.PublishedCultures.Contains(x, StringComparer.OrdinalIgnoreCase)); + if (mandatoryMissing) + return new PublishResult(PublishResultType.FailedPublishMandatoryCultureMissing, evtMsgs, content); + + if (culturesPublishing.Count == 0 && culturesUnpublishing.Count > 0) + return new PublishResult(PublishResultType.SuccessUnpublishCulture, evtMsgs, content); } // ensure that the document has published values // either because it is 'publishing' or because it already has a published version - if (((Content) content).PublishedState != PublishedState.Publishing && content.PublishedVersionId == 0) + //fixme - casting + if (((Content)content).PublishedState != PublishedState.Publishing && content.PublishedVersionId == 0) { Logger.Info("Document {ContentName} (id={ContentId}) cannot be published: {Reason}", content.Name, content.Id, "document does not have published values"); - return new PublishResult(PublishResultType.FailedNoPublishedValues, evtMsgs, content); + return new PublishResult(PublishResultType.FailedPublishNothingToPublish, evtMsgs, content); } - // ensure that the document status is correct - switch (content.Status) + //loop over each culture publishing - or string.Empty for invariant + foreach (var culture in culturesPublishing ?? (new[] { string.Empty })) { - case ContentStatus.Expired: - Logger.Info("Document {ContentName} (id={ContentId}) cannot be published: {Reason}", content.Name, content.Id, "document has expired"); - return new PublishResult(PublishResultType.FailedHasExpired, evtMsgs, content); + // ensure that the document status is correct + // note: culture will be string.Empty for invariant + switch (content.GetStatus(culture)) + { + case ContentStatus.Expired: + if (!variesByCulture) + Logger.Info("Document {ContentName} (id={ContentId}) cannot be published: {Reason}", content.Name, content.Id, "document has expired"); + else + Logger.Info("Document {ContentName} (id={ContentId}) culture {Culture} cannot be published: {Reason}", content.Name, content.Id, culture, "document culture has expired"); + return new PublishResult(!variesByCulture ? PublishResultType.FailedPublishHasExpired : PublishResultType.FailedPublishCultureHasExpired, evtMsgs, content); - case ContentStatus.AwaitingRelease: - Logger.Info("Document {ContentName} (id={ContentId}) cannot be published: {Reason}", content.Name, content.Id, "document is awaiting release"); - return new PublishResult(PublishResultType.FailedAwaitingRelease, evtMsgs, content); + case ContentStatus.AwaitingRelease: + if (!variesByCulture) + Logger.Info("Document {ContentName} (id={ContentId}) cannot be published: {Reason}", content.Name, content.Id, "document is awaiting release"); + else + Logger.Info("Document {ContentName} (id={ContentId}) culture {Culture} cannot be published: {Reason}", content.Name, content.Id, culture, "document is culture awaiting release"); + return new PublishResult(!variesByCulture ? PublishResultType.FailedPublishAwaitingRelease : PublishResultType.FailedPublishCultureAwaitingRelease, evtMsgs, content); - case ContentStatus.Trashed: - Logger.Info("Document {ContentName} (id={ContentId}) cannot be published: {Reason}", content.Name, content.Id, "document is trashed"); - return new PublishResult(PublishResultType.FailedIsTrashed, evtMsgs, content); + case ContentStatus.Trashed: + Logger.Info("Document {ContentName} (id={ContentId}) cannot be published: {Reason}", content.Name, content.Id, "document is trashed"); + return new PublishResult(PublishResultType.FailedPublishIsTrashed, evtMsgs, content); + } } - if (!checkPath) return new PublishResult(evtMsgs, content); - - // check if the content can be path-published - // root content can be published - // else check ancestors - we know we are not trashed - var pathIsOk = content.ParentId == Constants.System.Root || IsPathPublished(GetParent(content)); - if (pathIsOk == false) + if (checkPath) { - Logger.Info("Document {ContentName} (id={ContentId}) cannot be published: {Reason}", content.Name, content.Id, "parent is not published"); - return new PublishResult(PublishResultType.FailedPathNotPublished, evtMsgs, content); + // check if the content can be path-published + // root content can be published + // else check ancestors - we know we are not trashed + var pathIsOk = content.ParentId == Constants.System.Root || IsPathPublished(GetParent(content)); + if (!pathIsOk) + { + Logger.Info("Document {ContentName} (id={ContentId}) cannot be published: {Reason}", content.Name, content.Id, "parent is not published"); + return new PublishResult(PublishResultType.FailedPublishPathNotPublished, evtMsgs, content); + } } + //If we are both publishing and unpublishing cultures, then return a mixed status + if (variesByCulture && culturesPublishing.Count > 0 && culturesUnpublishing.Count > 0) + return new PublishResult(PublishResultType.SuccessMixedCulture, evtMsgs, content); + return new PublishResult(evtMsgs, content); } - // publishes a document - internal PublishResult StrategyPublish(IScope scope, IContent content, bool canPublish, int userId, EventMessages evtMsgs) + /// + /// Publishes a document + /// + /// + /// + /// + /// + /// + /// + /// It is assumed that all publishing checks have passed before calling this method like + /// + private PublishResult StrategyPublish(IScope scope, IContent content, int userId, + IReadOnlyList culturesPublishing, IReadOnlyList culturesUnpublishing, + EventMessages evtMsgs) { - // note: when used at top-level, StrategyCanPublish with checkPath=true should have run already - // and alreadyCheckedCanPublish should be true, so not checking again. when used at nested level, - // there is no need to check the path again. so, checkPath=false in StrategyCanPublish below - - var result = canPublish - ? new PublishResult(evtMsgs, content) // already know we can - : StrategyCanPublish(scope, content, userId, /*checkPath:*/ false, evtMsgs); // else check - - if (result.Success == false) - return result; - // change state to publishing - ((Content) content).PublishedState = PublishedState.Publishing; + // fixme - casting + ((Content)content).PublishedState = PublishedState.Publishing; + + //if this is a variant then we need to log which cultures have been published/unpublished and return an appropriate result + if (content.ContentType.VariesByCulture()) + { + if (content.Published && culturesUnpublishing.Count == 0 && culturesPublishing.Count == 0) + return new PublishResult(PublishResultType.FailedPublishNothingToPublish, evtMsgs, content); + + if (culturesUnpublishing.Count > 0) + Logger.Info("Document {ContentName} (id={ContentId}) cultures: {Cultures} have been unpublished.", + content.Name, content.Id, string.Join(",", culturesUnpublishing)); + + if (culturesPublishing.Count > 0) + Logger.Info("Document {ContentName} (id={ContentId}) cultures: {Cultures} have been published.", + content.Name, content.Id, string.Join(",", culturesPublishing)); + + if (culturesUnpublishing.Count > 0 && culturesPublishing.Count > 0) + return new PublishResult(PublishResultType.SuccessMixedCulture, evtMsgs, content); + + if (culturesUnpublishing.Count > 0 && culturesPublishing.Count == 0) + return new PublishResult(PublishResultType.SuccessUnpublishCulture, evtMsgs, content); + + return new PublishResult(PublishResultType.SuccessPublishCulture, evtMsgs, content); + } + Logger.Info("Document {ContentName} (id={ContentId}) has been published.", content.Name, content.Id); - return result; + return new PublishResult(evtMsgs, content); } - // ensures that a document can be unpublished - internal UnpublishResult StrategyCanUnpublish(IScope scope, IContent content, int userId, EventMessages evtMsgs) + /// + /// Ensures that a document can be unpublished + /// + /// + /// + /// + /// + /// + private PublishResult StrategyCanUnpublish(IScope scope, IContent content, int userId, EventMessages evtMsgs) { // raise Unpublishing event if (scope.Events.DispatchCancelable(Unpublishing, this, new PublishEventArgs(content, evtMsgs))) { Logger.Info("Document {ContentName} (id={ContentId}) cannot be unpublished: unpublishing was cancelled.", content.Name, content.Id); - return new UnpublishResult(UnpublishResultType.FailedCancelledByEvent, evtMsgs, content); + return new PublishResult(PublishResultType.FailedUnpublishCancelledByEvent, evtMsgs, content); } - return new UnpublishResult(evtMsgs, content); + return new PublishResult(PublishResultType.SuccessUnpublish, evtMsgs, content); } - // unpublishes a document - internal UnpublishResult StrategyUnpublish(IScope scope, IContent content, bool canUnpublish, int userId, EventMessages evtMsgs) + /// + /// Unpublishes a document + /// + /// + /// + /// + /// + /// + /// + /// It is assumed that all unpublishing checks have passed before calling this method like + /// + private PublishResult StrategyUnpublish(IScope scope, IContent content, int userId, EventMessages evtMsgs) { - var attempt = canUnpublish - ? new UnpublishResult(evtMsgs, content) // already know we can - : StrategyCanUnpublish(scope, content, userId, evtMsgs); // else check + var attempt = new PublishResult(PublishResultType.SuccessUnpublish, evtMsgs, content); if (attempt.Success == false) return attempt; - // if the document has a release date set to before now, - // it should be removed so it doesn't interrupt an unpublish + // if the document has any release dates set to before now, + // they should be removed so they don't interrupt an unpublish // otherwise it would remain released == published - if (content.ReleaseDate.HasValue && content.ReleaseDate.Value <= DateTime.Now) - { - content.ReleaseDate = null; + + var pastReleases = content.ContentSchedule.GetPending(ContentScheduleAction.Expire, DateTime.Now); + foreach (var p in pastReleases) + content.ContentSchedule.Remove(p); + if (pastReleases.Count > 0) Logger.Info("Document {ContentName} (id={ContentId}) had its release date removed, because it was unpublished.", content.Name, content.Id); - } // change state to unpublishing - ((Content) content).PublishedState = PublishedState.Unpublishing; + // fixme - casting + ((Content)content).PublishedState = PublishedState.Unpublishing; Logger.Info("Document {ContentName} (id={ContentId}) has been unpublished.", content.Name, content.Id); return attempt; @@ -2311,7 +2628,7 @@ namespace Umbraco.Core.Services.Implement scope.Events.Dispatch(Trashed, this, new MoveEventArgs(false, moveInfos), nameof(Trashed)); scope.Events.Dispatch(TreeChanged, this, changes.ToEventArgs()); - Audit(AuditType.Delete, $"Delete Content of Type {string.Join(",", contentTypeIdsA)} performed by user", userId, Constants.System.Root); + Audit(AuditType.Delete, userId, Constants.System.Root, $"Delete content of type {string.Join(",", contentTypeIdsA)}"); scope.Complete(); } @@ -2364,7 +2681,7 @@ namespace Umbraco.Core.Services.Implement scope.ReadLock(Constants.Locks.ContentTree); var blueprint = _documentBlueprintRepository.Get(id); if (blueprint != null) - ((Content) blueprint).Blueprint = true; + ((Content)blueprint).Blueprint = true; return blueprint; } } @@ -2376,7 +2693,7 @@ namespace Umbraco.Core.Services.Implement scope.ReadLock(Constants.Locks.ContentTree); var blueprint = _documentBlueprintRepository.Get(id); if (blueprint != null) - ((Content) blueprint).Blueprint = true; + ((Content)blueprint).Blueprint = true; return blueprint; } } @@ -2387,7 +2704,7 @@ namespace Umbraco.Core.Services.Implement if (content.ParentId != -1) content.ParentId = -1; - ((Content) content).Blueprint = true; + ((Content)content).Blueprint = true; using (var scope = ScopeProvider.CreateScope()) { @@ -2451,7 +2768,7 @@ namespace Umbraco.Core.Services.Implement } return _documentBlueprintRepository.Get(query).Select(x => { - ((Content) x).Blueprint = true; + ((Content)x).Blueprint = true; return x; }); } @@ -2470,7 +2787,7 @@ namespace Umbraco.Core.Services.Implement var blueprints = _documentBlueprintRepository.Get(query).Select(x => { - ((Content) x).Blueprint = true; + ((Content)x).Blueprint = true; return x; }).ToArray(); @@ -2490,5 +2807,68 @@ namespace Umbraco.Core.Services.Implement } #endregion + + #region Rollback + + public OperationResult Rollback(int id, int versionId, string culture = "*", int userId = 0) + { + var evtMsgs = EventMessagesFactory.Get(); + + //Get the current copy of the node + var content = GetById(id); + + //Get the version + var version = GetVersion(versionId); + + //Good ole null checks + if (content == null || version == null) + { + return new OperationResult(OperationResultType.FailedCannot, evtMsgs); + } + + //Store the result of doing the save of content for the rollback + OperationResult rollbackSaveResult; + + using (var scope = ScopeProvider.CreateScope()) + { + var rollbackEventArgs = new RollbackEventArgs(content); + + //Emit RollingBack event aka before + if (scope.Events.DispatchCancelable(RollingBack, this, rollbackEventArgs)) + { + scope.Complete(); + return OperationResult.Cancel(evtMsgs); + } + + //Copy the changes from the version + content.CopyFrom(version, culture); + + //Save the content for the rollback + rollbackSaveResult = Save(content, userId); + + //Depending on the save result - is what we log & audit along with what we return + if (rollbackSaveResult.Success == false) + { + //Log the error/warning + Logger.Error("User '{UserId}' was unable to rollback content '{ContentId}' to version '{VersionId}'", userId, id, versionId); + } + else + { + //Emit RolledBack event aka after + rollbackEventArgs.CanCancel = false; + scope.Events.Dispatch(RolledBack, this, rollbackEventArgs); + + //Logging & Audit message + Logger.Info("User '{UserId}' rolled back content '{ContentId}' to version '{VersionId}'", userId, id, versionId); + Audit(AuditType.RollBack, userId, id, $"Content '{content.Name}' was rolled back to version '{versionId}'"); + } + + scope.Complete(); + } + + return rollbackSaveResult; + } + + #endregion } } diff --git a/src/Umbraco.Core/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs b/src/Umbraco.Core/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs index a114f415cc..b74abc03f7 100644 --- a/src/Umbraco.Core/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs @@ -344,37 +344,18 @@ namespace Umbraco.Core.Services.Implement } } + public IEnumerable GetComposedOf(int id, IEnumerable all) + { + return all.Where(x => x.ContentTypeComposition.Any(y => y.Id == id)); + + } + public IEnumerable GetComposedOf(int id) { - //fixme: this is essentially the same as ContentTypeServiceExtensions.GetWhereCompositionIsUsedInContentTypes which loads - // all content types to figure this out, this instead makes quite a few queries so should be replaced - - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - scope.ReadLock(ReadLockIds); - - // hash set handles duplicates - var composed = new HashSet(new DelegateEqualityComparer( - (x, y) => x.Id == y.Id, - x => x.Id.GetHashCode())); - - var ids = new Stack(); - ids.Push(id); - - while (ids.Count > 0) - { - var i = ids.Pop(); - var result = Repository.GetTypesDirectlyComposedOf(i).ToArray(); - - foreach (var c in result) - { - composed.Add(c); - ids.Push(c.Id); - } - } - - return composed.ToArray(); - } + // GetAll is cheap, repository has a full dataset cache policy + // fixme - still, because it uses the cache, race conditions! + var allContentTypes = GetAll(Array.Empty()); + return GetComposedOf(id, allContentTypes); } public int Count() @@ -423,7 +404,7 @@ namespace Umbraco.Core.Services.Implement saveEventArgs.CanCancel = false; OnSaved(scope, saveEventArgs); - Audit(AuditType.Save, $"Save {typeof(TItem).Name} performed by user", userId, item.Id); + Audit(AuditType.Save, userId, item.Id); scope.Complete(); } } @@ -465,7 +446,7 @@ namespace Umbraco.Core.Services.Implement saveEventArgs.CanCancel = false; OnSaved(scope, saveEventArgs); - Audit(AuditType.Save, $"Save {typeof(TItem).Name} performed by user", userId, -1); + Audit(AuditType.Save, userId, -1); scope.Complete(); } } @@ -523,7 +504,7 @@ namespace Umbraco.Core.Services.Implement deleteEventArgs.CanCancel = false; OnDeleted(scope, deleteEventArgs); - Audit(AuditType.Delete, $"Delete {typeof(TItem).Name} performed by user", userId, item.Id); + Audit(AuditType.Delete, userId, item.Id); scope.Complete(); } } @@ -576,7 +557,7 @@ namespace Umbraco.Core.Services.Implement deleteEventArgs.CanCancel = false; OnDeleted(scope, deleteEventArgs); - Audit(AuditType.Delete, $"Delete {typeof(TItem).Name} performed by user", userId, -1); + Audit(AuditType.Delete, userId, -1); scope.Complete(); } } @@ -963,9 +944,10 @@ namespace Umbraco.Core.Services.Implement #region Audit - private void Audit(AuditType type, string message, int userId, int objectId) + private void Audit(AuditType type, int userId, int objectId) { - _auditRepository.Save(new AuditItem(objectId, message, type, userId)); + _auditRepository.Save(new AuditItem(objectId, type, userId, + ObjectTypes.GetUmbracoObjectType(ContainedObjectType).GetName())); } #endregion diff --git a/src/Umbraco.Core/Services/Implement/DataTypeService.cs b/src/Umbraco.Core/Services/Implement/DataTypeService.cs index c105b6cfe6..79ca851de9 100644 --- a/src/Umbraco.Core/Services/Implement/DataTypeService.cs +++ b/src/Umbraco.Core/Services/Implement/DataTypeService.cs @@ -353,7 +353,7 @@ namespace Umbraco.Core.Services.Implement saveEventArgs.CanCancel = false; scope.Events.Dispatch(Saved, this, saveEventArgs); - Audit(AuditType.Save, "Save DataTypeDefinition performed by user", userId, dataType.Id); + Audit(AuditType.Save, userId, dataType.Id); scope.Complete(); } } @@ -398,7 +398,7 @@ namespace Umbraco.Core.Services.Implement saveEventArgs.CanCancel = false; scope.Events.Dispatch(Saved, this, saveEventArgs); } - Audit(AuditType.Save, "Save DataTypeDefinition performed by user", userId, -1); + Audit(AuditType.Save, userId, -1); scope.Complete(); } @@ -456,15 +456,15 @@ namespace Umbraco.Core.Services.Implement deleteEventArgs.CanCancel = false; scope.Events.Dispatch(Deleted, this, deleteEventArgs); - Audit(AuditType.Delete, "Delete DataTypeDefinition performed by user", userId, dataType.Id); + Audit(AuditType.Delete, userId, dataType.Id); scope.Complete(); } } - private void Audit(AuditType type, string message, int userId, int objectId) + private void Audit(AuditType type, int userId, int objectId) { - _auditRepository.Save(new AuditItem(objectId, message, type, userId)); + _auditRepository.Save(new AuditItem(objectId, type, userId, ObjectTypes.GetName(UmbracoObjectTypes.DataType))); } #region Event Handlers diff --git a/src/Umbraco.Core/Services/Implement/EntityService.cs b/src/Umbraco.Core/Services/Implement/EntityService.cs index 385b5eabe0..45c229214a 100644 --- a/src/Umbraco.Core/Services/Implement/EntityService.cs +++ b/src/Umbraco.Core/Services/Implement/EntityService.cs @@ -11,6 +11,7 @@ using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence.Repositories; +using Umbraco.Core.Persistence.Repositories.Implement; using Umbraco.Core.Scoping; namespace Umbraco.Core.Services.Implement @@ -373,6 +374,22 @@ namespace Umbraco.Core.Services.Implement } } + /// + /// Gets a collection of children by the parent's Id and UmbracoObjectType without adding property data + /// + /// Id of the parent to retrieve children for + /// An enumerable list of objects + internal IEnumerable GetMediaChildrenWithoutPropertyData(int parentId) + { + using (ScopeProvider.CreateScope(autoComplete: true)) + { + var query = Query().Where(x => x.ParentId == parentId); + + //fixme - see https://github.com/umbraco/Umbraco-CMS/pull/3460#issuecomment-434903930 we need to not load any property data at all for media + return ((EntityRepository)_entityRepository).GetMediaByQueryWithoutPropertyData(query); + } + } + /// public virtual IEnumerable GetDescendants(int id) { diff --git a/src/Umbraco.Core/Services/Implement/FileService.cs b/src/Umbraco.Core/Services/Implement/FileService.cs index c3a8b790cc..f15f0d7d47 100644 --- a/src/Umbraco.Core/Services/Implement/FileService.cs +++ b/src/Umbraco.Core/Services/Implement/FileService.cs @@ -91,7 +91,7 @@ namespace Umbraco.Core.Services.Implement saveEventArgs.CanCancel = false; scope.Events.Dispatch(SavedStylesheet, this, saveEventArgs); - Audit(AuditType.Save, "Save Stylesheet performed by user", userId, -1); + Audit(AuditType.Save, userId, -1, ObjectTypes.GetName(UmbracoObjectTypes.Stylesheet)); scope.Complete(); } } @@ -123,7 +123,7 @@ namespace Umbraco.Core.Services.Implement deleteEventArgs.CanCancel = false; scope.Events.Dispatch(DeletedStylesheet, this, deleteEventArgs); - Audit(AuditType.Delete, "Delete Stylesheet performed by user", userId, -1); + Audit(AuditType.Delete, userId, -1, ObjectTypes.GetName(UmbracoObjectTypes.Stylesheet)); scope.Complete(); } } @@ -215,7 +215,7 @@ namespace Umbraco.Core.Services.Implement saveEventArgs.CanCancel = false; scope.Events.Dispatch(SavedScript, this, saveEventArgs); - Audit(AuditType.Save, "Save Script performed by user", userId, -1); + Audit(AuditType.Save, userId, -1, "Script"); scope.Complete(); } } @@ -247,7 +247,7 @@ namespace Umbraco.Core.Services.Implement deleteEventArgs.CanCancel = false; scope.Events.Dispatch(DeletedScript, this, deleteEventArgs); - Audit(AuditType.Delete, "Delete Script performed by user", userId, -1); + Audit(AuditType.Delete, userId, -1, "Script"); scope.Complete(); } } @@ -362,7 +362,7 @@ namespace Umbraco.Core.Services.Implement saveEventArgs.CanCancel = false; scope.Events.Dispatch(SavedTemplate, this, saveEventArgs); - Audit(AuditType.Save, "Save Template performed by user", userId, template.Id); + Audit(AuditType.Save, userId, template.Id, ObjectTypes.GetName(UmbracoObjectTypes.Template)); scope.Complete(); } @@ -525,7 +525,7 @@ namespace Umbraco.Core.Services.Implement scope.Events.Dispatch(SavedTemplate, this, new SaveEventArgs(template, false)); - Audit(AuditType.Save, "Save Template performed by user", userId, template.Id); + Audit(AuditType.Save, userId, template.Id, ObjectTypes.GetName(UmbracoObjectTypes.Template)); scope.Complete(); } } @@ -551,7 +551,7 @@ namespace Umbraco.Core.Services.Implement scope.Events.Dispatch(SavedTemplate, this, new SaveEventArgs(templatesA, false)); - Audit(AuditType.Save, "Save Template performed by user", userId, -1); + Audit(AuditType.Save, userId, -1, ObjectTypes.GetName(UmbracoObjectTypes.Template)); scope.Complete(); } } @@ -605,7 +605,7 @@ namespace Umbraco.Core.Services.Implement args.CanCancel = false; scope.Events.Dispatch(DeletedTemplate, this, args); - Audit(AuditType.Delete, "Delete Template performed by user", userId, template.Id); + Audit(AuditType.Delete, userId, template.Id, ObjectTypes.GetName(UmbracoObjectTypes.Template)); scope.Complete(); } } @@ -788,7 +788,7 @@ namespace Umbraco.Core.Services.Implement newEventArgs.CanCancel = false; scope.Events.Dispatch(CreatedPartialView, this, newEventArgs); - Audit(AuditType.Save, $"Save {partialViewType} performed by user", userId, -1); + Audit(AuditType.Save, userId, -1, partialViewType.ToString()); scope.Complete(); } @@ -828,7 +828,7 @@ namespace Umbraco.Core.Services.Implement repository.Delete(partialView); deleteEventArgs.CanCancel = false; scope.Events.Dispatch(DeletedPartialView, this, deleteEventArgs); - Audit(AuditType.Delete, $"Delete {partialViewType} performed by user", userId, -1); + Audit(AuditType.Delete, userId, -1, partialViewType.ToString()); scope.Complete(); } @@ -860,7 +860,7 @@ namespace Umbraco.Core.Services.Implement var repository = GetPartialViewRepository(partialViewType); repository.Save(partialView); saveEventArgs.CanCancel = false; - Audit(AuditType.Save, $"Save {partialViewType} performed by user", userId, -1); + Audit(AuditType.Save, userId, -1, partialViewType.ToString()); scope.Events.Dispatch(SavedPartialView, this, saveEventArgs); scope.Complete(); @@ -1038,9 +1038,9 @@ namespace Umbraco.Core.Services.Implement #endregion - private void Audit(AuditType type, string message, int userId, int objectId) + private void Audit(AuditType type, int userId, int objectId, string entityType) { - _auditRepository.Save(new AuditItem(objectId, message, type, userId)); + _auditRepository.Save(new AuditItem(objectId, type, userId, entityType)); } //TODO Method to change name and/or alias of view/masterpage template diff --git a/src/Umbraco.Core/Services/Implement/KeyValueService.cs b/src/Umbraco.Core/Services/Implement/KeyValueService.cs index cb1c423535..b30543ed48 100644 --- a/src/Umbraco.Core/Services/Implement/KeyValueService.cs +++ b/src/Umbraco.Core/Services/Implement/KeyValueService.cs @@ -1,9 +1,8 @@ using System; -using System.Collections.Generic; using System.Linq; +using Umbraco.Core.Configuration; using Umbraco.Core.Logging; using Umbraco.Core.Migrations; -using Umbraco.Core.Migrations.Expressions.Create; using Umbraco.Core.Scoping; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Dtos; @@ -35,23 +34,23 @@ namespace Umbraco.Core.Services.Implement private void Initialize() { - // all this cannot be achieved via an UmbracoPlan migration since it needs to - // run before any migration, in order to figure out the current plan's state. - // (does not prevent us from using a migration to do it, though) + // the key/value service is entirely self-managed, because it is used by the + // upgrader and anything we might change need to happen before everything else + + // if already running 8, either following an upgrade or an install, + // then everything should be ok (the table should exist, etc) + + if (UmbracoVersion.LocalVersion.Major >= 8) + return; + + // else we are upgrading from 7, we can assume that the locks table + // exists, but we need to create everything for key/value using (var scope = _scopeProvider.CreateScope()) { - // assume that if the lock object for key/value exists, then everything is ok - if (scope.Database.Exists(Constants.Locks.KeyValues)) - { - scope.Complete(); - return; - } - var context = new MigrationContext(scope.Database, _logger); var initMigration = new InitializeMigration(context); initMigration.Migrate(); - scope.Complete(); } } @@ -59,7 +58,7 @@ namespace Umbraco.Core.Services.Implement /// /// A custom migration that executes standalone during the Initialize phase of this service. /// - private class InitializeMigration : MigrationBase + internal class InitializeMigration : MigrationBase { public InitializeMigration(IMigrationContext context) : base(context) @@ -67,26 +66,47 @@ namespace Umbraco.Core.Services.Implement public override void Migrate() { + // as long as we are still running 7 this migration will be invoked, + // but due to multiple restarts during upgrades, maybe the table + // exists already + if (TableExists(Constants.DatabaseSchema.Tables.KeyValue)) + return; + + Logger.Info("Creating KeyValue structure."); + + // the locks table was initially created with an identity (auto-increment) primary key, + // but we don't want this, especially as we are about to insert a new row into the table, + // so here we drop that identity + DropLockTableIdentity(); + + // insert the lock object for key/value + Insert.IntoTable(Constants.DatabaseSchema.Tables.Lock).Row(new {id = Constants.Locks.KeyValues, name = "KeyValues", value = 1}).Do(); + + // create the key-value table + Create.Table().Do(); + } + + private void DropLockTableIdentity() + { + // one cannot simply drop an identity, that requires a bit of work + // create a temp. id column and copy values Alter.Table(Constants.DatabaseSchema.Tables.Lock).AddColumn("nid").AsInt32().Nullable().Do(); Execute.Sql("update umbracoLock set nid = id").Do(); + // drop the id column entirely (cannot just drop identity) Delete.PrimaryKey("PK_umbracoLock").FromTable(Constants.DatabaseSchema.Tables.Lock).Do(); Delete.Column("id").FromTable(Constants.DatabaseSchema.Tables.Lock).Do(); + // recreate the id column without identity and copy values Alter.Table(Constants.DatabaseSchema.Tables.Lock).AddColumn("id").AsInt32().Nullable().Do(); Execute.Sql("update umbracoLock set id = nid").Do(); + // drop the temp. id column Delete.Column("nid").FromTable(Constants.DatabaseSchema.Tables.Lock).Do(); + // complete the primary key Alter.Table(Constants.DatabaseSchema.Tables.Lock).AlterColumn("id").AsInt32().NotNullable().PrimaryKey("PK_umbracoLock").Do(); - - // insert the key-value lock - Insert.IntoTable(Constants.DatabaseSchema.Tables.Lock).Row(new {id = Constants.Locks.KeyValues, name = "KeyValues", value = 1}).Do(); - - // create the key-value table if it's not there - if (TableExists(Constants.DatabaseSchema.Tables.KeyValue) == false) - Create.Table().Do(); } } @@ -169,5 +189,22 @@ namespace Umbraco.Core.Services.Implement return true; } + + /// + /// Gets a value directly from the database, no scope, nothing. + /// + /// Used by to determine the runtime state. + internal static string GetValue(IUmbracoDatabase database, string key) + { + // not 8 yet = no key/value table, no value + if (UmbracoVersion.LocalVersion.Major < 8) + return null; + + var sql = database.SqlContext.Sql() + .Select() + .From() + .Where(x => x.Key == key); + return database.FirstOrDefault(sql)?.Value; + } } } diff --git a/src/Umbraco.Core/Services/Implement/LocalizationService.cs b/src/Umbraco.Core/Services/Implement/LocalizationService.cs index 49a764b533..c972b949d6 100644 --- a/src/Umbraco.Core/Services/Implement/LocalizationService.cs +++ b/src/Umbraco.Core/Services/Implement/LocalizationService.cs @@ -245,7 +245,7 @@ namespace Umbraco.Core.Services.Implement EnsureDictionaryItemLanguageCallback(dictionaryItem); scope.Events.Dispatch(SavedDictionaryItem, this, new SaveEventArgs(dictionaryItem, false)); - Audit(AuditType.Save, "Save DictionaryItem performed by user", userId, dictionaryItem.Id); + Audit(AuditType.Save, "Save DictionaryItem", userId, dictionaryItem.Id, "DictionaryItem"); scope.Complete(); } } @@ -271,7 +271,7 @@ namespace Umbraco.Core.Services.Implement deleteEventArgs.CanCancel = false; scope.Events.Dispatch(DeletedDictionaryItem, this, deleteEventArgs); - Audit(AuditType.Delete, "Delete DictionaryItem performed by user", userId, dictionaryItem.Id); + Audit(AuditType.Delete, "Delete DictionaryItem", userId, dictionaryItem.Id, "DictionaryItem"); scope.Complete(); } @@ -384,7 +384,7 @@ namespace Umbraco.Core.Services.Implement saveEventArgs.CanCancel = false; scope.Events.Dispatch(SavedLanguage, this, saveEventArgs); - Audit(AuditType.Save, "Save Language performed by user", userId, language.Id); + Audit(AuditType.Save, "Save Language", userId, language.Id, ObjectTypes.GetName(UmbracoObjectTypes.Language)); scope.Complete(); } @@ -429,14 +429,14 @@ namespace Umbraco.Core.Services.Implement scope.Events.Dispatch(DeletedLanguage, this, deleteEventArgs); - Audit(AuditType.Delete, "Delete Language performed by user", userId, language.Id); + Audit(AuditType.Delete, "Delete Language", userId, language.Id, ObjectTypes.GetName(UmbracoObjectTypes.Language)); scope.Complete(); } } - private void Audit(AuditType type, string message, int userId, int objectId) + private void Audit(AuditType type, string message, int userId, int objectId, string entityType) { - _auditRepository.Save(new AuditItem(objectId, message, type, userId)); + _auditRepository.Save(new AuditItem(objectId, type, userId, entityType, message)); } /// diff --git a/src/Umbraco.Core/Services/Implement/MacroService.cs b/src/Umbraco.Core/Services/Implement/MacroService.cs index fdcc8e2ee0..5176e2eb22 100644 --- a/src/Umbraco.Core/Services/Implement/MacroService.cs +++ b/src/Umbraco.Core/Services/Implement/MacroService.cs @@ -95,7 +95,7 @@ namespace Umbraco.Core.Services.Implement _macroRepository.Delete(macro); deleteEventArgs.CanCancel = false; scope.Events.Dispatch(Deleted, this, deleteEventArgs); - Audit(AuditType.Delete, "Delete Macro performed by user", userId, -1); + Audit(AuditType.Delete, userId, -1); scope.Complete(); } @@ -125,7 +125,7 @@ namespace Umbraco.Core.Services.Implement _macroRepository.Save(macro); saveEventArgs.CanCancel = false; scope.Events.Dispatch(Saved, this, saveEventArgs); - Audit(AuditType.Save, "Save Macro performed by user", userId, -1); + Audit(AuditType.Save, userId, -1); scope.Complete(); } @@ -150,9 +150,9 @@ namespace Umbraco.Core.Services.Implement // return MacroPropertyTypeResolver.Current.MacroPropertyTypes.FirstOrDefault(x => x.Alias == alias); //} - private void Audit(AuditType type, string message, int userId, int objectId) + private void Audit(AuditType type, int userId, int objectId) { - _auditRepository.Save(new AuditItem(objectId, message, type, userId)); + _auditRepository.Save(new AuditItem(objectId, type, userId, "Macro")); } #region Event Handlers diff --git a/src/Umbraco.Core/Services/Implement/MediaService.cs b/src/Umbraco.Core/Services/Implement/MediaService.cs index 431e20044c..1d04462836 100644 --- a/src/Umbraco.Core/Services/Implement/MediaService.cs +++ b/src/Umbraco.Core/Services/Implement/MediaService.cs @@ -295,7 +295,7 @@ namespace Umbraco.Core.Services.Implement if (withIdentity == false) return; - Audit(AuditType.New, $"Media '{media.Name}' was created with Id {media.Id}", media.CreatorId, media.Id); + Audit(AuditType.New, media.CreatorId, media.Id, $"Media '{media.Name}' was created with Id {media.Id}"); } #endregion @@ -364,18 +364,39 @@ namespace Umbraco.Core.Services.Implement } } - /// - /// Gets a collection of objects by the Id of the - /// - /// Id of the - /// An Enumerable list of objects - public IEnumerable GetMediaOfMediaType(int id) + /// + public IEnumerable GetPagedOfType(int contentTypeId, long pageIndex, int pageSize, out long totalRecords, IQuery filter = null, Ordering ordering = null) { + if (pageIndex < 0) throw new ArgumentOutOfRangeException(nameof(pageIndex)); + if (pageSize <= 0) throw new ArgumentOutOfRangeException(nameof(pageSize)); + + if (ordering == null) + ordering = Ordering.By("sortOrder"); + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { - scope.ReadLock(Constants.Locks.MediaTree); - var query = Query().Where(x => x.ContentTypeId == id); - return _mediaRepository.Get(query); + scope.ReadLock(Constants.Locks.ContentTree); + return _mediaRepository.GetPage( + Query().Where(x => x.ContentTypeId == contentTypeId), + pageIndex, pageSize, out totalRecords, filter, ordering); + } + } + + /// + public IEnumerable GetPagedOfTypes(int[] contentTypeIds, long pageIndex, int pageSize, out long totalRecords, IQuery filter = null, Ordering ordering = null) + { + if (pageIndex < 0) throw new ArgumentOutOfRangeException(nameof(pageIndex)); + if (pageSize <= 0) throw new ArgumentOutOfRangeException(nameof(pageSize)); + + if (ordering == null) + ordering = Ordering.By("sortOrder"); + + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + scope.ReadLock(Constants.Locks.ContentTree); + return _mediaRepository.GetPage( + Query().Where(x => contentTypeIds.Contains(x.ContentTypeId)), + pageIndex, pageSize, out totalRecords, filter, ordering); } } @@ -460,149 +481,36 @@ namespace Umbraco.Core.Services.Implement } } - /// - /// Gets a collection of objects by Parent Id - /// - /// Id of the Parent to retrieve Children from - /// An Enumerable list of objects - public IEnumerable GetChildren(int id) - { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - scope.ReadLock(Constants.Locks.MediaTree); - var query = Query().Where(x => x.ParentId == id); - return _mediaRepository.Get(query).OrderBy(x => x.SortOrder); - } - } - - /// - /// Gets a collection of objects by Parent Id - /// - /// Id of the Parent to retrieve Children from - /// Page index (zero based) - /// Page size - /// Total records query would return without paging - /// Field to order by - /// Direction to order by - /// Search text filter - /// An Enumerable list of objects - public IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalChildren, string orderBy, Direction orderDirection, string filter = "") - { - var filterQuery = filter.IsNullOrWhiteSpace() - ? null - : Query().Where(x => x.Name.Contains(filter)); - - return GetPagedChildren(id, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, true, filterQuery); - } - - /// - /// Gets a collection of objects by Parent Id - /// - /// Id of the Parent to retrieve Children from - /// Page index (zero based) - /// Page size - /// Total records query would return without paging - /// Field to order by - /// Direction to order by - /// Flag to indicate when ordering by system field - /// - /// An Enumerable list of objects - public IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalChildren, string orderBy, Direction orderDirection, bool orderBySystemField, IQuery filter) + /// + public IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalChildren, + IQuery filter = null, Ordering ordering = null) { if (pageIndex < 0) throw new ArgumentOutOfRangeException(nameof(pageIndex)); if (pageSize <= 0) throw new ArgumentOutOfRangeException(nameof(pageSize)); + if (ordering == null) + ordering = Ordering.By("sortOrder"); + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.ReadLock(Constants.Locks.MediaTree); var query = Query().Where(x => x.ParentId == id); - return _mediaRepository.GetPage(query, pageIndex, pageSize, out totalChildren, filter, Ordering.By(orderBy, orderDirection, isCustomField: !orderBySystemField)); + return _mediaRepository.GetPage(query, pageIndex, pageSize, out totalChildren, filter, ordering); } } - /// - /// Gets a collection of objects by Parent Id - /// - /// Id of the Parent to retrieve Children from - /// Page number - /// Page size - /// Total records query would return without paging - /// Field to order by - /// Direction to order by - /// Flag to indicate when ordering by system field - /// Search text filter - /// A list of content type Ids to filter the list by - /// An Enumerable list of objects - public IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalChildren, string orderBy, Direction orderDirection, bool orderBySystemField, string filter, int[] contentTypeFilter) + /// + public IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalChildren, + IQuery filter = null, Ordering ordering = null) { - if (pageIndex < 0) throw new ArgumentOutOfRangeException(nameof(pageIndex)); - if (pageSize <= 0) throw new ArgumentOutOfRangeException(nameof(pageSize)); + if (ordering == null) + ordering = Ordering.By("Path"); using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.ReadLock(Constants.Locks.MediaTree); - var query = Query(); - // always check for a parent - else it will also get decendants (and then you should use the GetPagedDescendants method) - - query.Where(x => x.ParentId == id); - - if (contentTypeFilter != null && contentTypeFilter.Length > 0) - { - query.Where(x => contentTypeFilter.Contains(x.ContentTypeId)); - } - - var filterQuery = filter.IsNullOrWhiteSpace() - ? null - : Query().Where(x => x.Name.Contains(filter)); - return _mediaRepository.GetPage(query, pageIndex, pageSize, out totalChildren, filterQuery, Ordering.By(orderBy, orderDirection, isCustomField: !orderBySystemField)); - } - } - - /// - /// Gets a collection of objects by Parent Id - /// - /// Id of the Parent to retrieve Descendants from - /// Page number - /// Page size - /// Total records query would return without paging - /// Field to order by - /// Direction to order by - /// Search text filter - /// An Enumerable list of objects - public IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalChildren, string orderBy = "Path", Direction orderDirection = Direction.Ascending, string filter = "") - { - var filterQuery = filter.IsNullOrWhiteSpace() - ? null - : Query().Where(x => x.Name.Contains(filter)); - - return GetPagedDescendants(id, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, true, filterQuery); - } - - /// - /// Gets a collection of objects by Parent Id - /// - /// Id of the Parent to retrieve Descendants from - /// Page number - /// Page size - /// Total records query would return without paging - /// Field to order by - /// Direction to order by - /// Flag to indicate when ordering by system field - /// - /// An Enumerable list of objects - public IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalChildren, string orderBy, Direction orderDirection, bool orderBySystemField, IQuery filter) - { - if (pageIndex < 0) throw new ArgumentOutOfRangeException(nameof(pageIndex)); - if (pageSize <= 0) throw new ArgumentOutOfRangeException(nameof(pageSize)); - - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - scope.ReadLock(Constants.Locks.MediaTree); - - var query = Query(); - //if the id is System Root, then just get all if (id != Constants.System.Root) { @@ -612,47 +520,24 @@ namespace Umbraco.Core.Services.Implement totalChildren = 0; return Enumerable.Empty(); } - query.Where(x => x.Path.SqlStartsWith(mediaPath[0].Path + ",", TextColumnType.NVarchar)); + return GetPagedDescendantsLocked(mediaPath[0].Path, pageIndex, pageSize, out totalChildren, filter, ordering); } - - return _mediaRepository.GetPage(query, pageIndex, pageSize, out totalChildren, filter, Ordering.By(orderBy, orderDirection, isCustomField: !orderBySystemField)); + return GetPagedDescendantsLocked(null, pageIndex, pageSize, out totalChildren, filter, ordering); } } - /// - /// Gets descendants of a object by its Id - /// - /// Id of the Parent to retrieve descendants from - /// An Enumerable flat list of objects - public IEnumerable GetDescendants(int id) + private IEnumerable GetPagedDescendantsLocked(string mediaPath, long pageIndex, int pageSize, out long totalChildren, + IQuery filter, Ordering ordering) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - scope.ReadLock(Constants.Locks.MediaTree); - var media = GetById(id); - if (media == null) - return Enumerable.Empty(); + if (pageIndex < 0) throw new ArgumentOutOfRangeException(nameof(pageIndex)); + if (pageSize <= 0) throw new ArgumentOutOfRangeException(nameof(pageSize)); + if (ordering == null) throw new ArgumentNullException(nameof(ordering)); - var pathMatch = media.Path + ","; - var query = Query().Where(x => x.Id != media.Id && x.Path.StartsWith(pathMatch)); - return _mediaRepository.Get(query); - } - } + var query = Query(); + if (!mediaPath.IsNullOrWhiteSpace()) + query.Where(x => x.Path.SqlStartsWith(mediaPath + ",", TextColumnType.NVarchar)); - /// - /// Gets descendants of a object by its Id - /// - /// The Parent object to retrieve descendants from - /// An Enumerable flat list of objects - public IEnumerable GetDescendants(IMedia media) - { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - scope.ReadLock(Constants.Locks.MediaTree); - var pathMatch = media.Path + ","; - var query = Query().Where(x => x.Id != media.Id && x.Path.StartsWith(pathMatch)); - return _mediaRepository.Get(query); - } + return _mediaRepository.GetPage(query, pageIndex, pageSize, out totalChildren, filter, ordering); } /// @@ -694,17 +579,18 @@ namespace Umbraco.Core.Services.Implement } } - /// - /// Gets a collection of an objects, which resides in the Recycle Bin - /// - /// An Enumerable list of objects - public IEnumerable GetMediaInRecycleBin() + /// + public IEnumerable GetPagedMediaInRecycleBin(long pageIndex, int pageSize, out long totalRecords, + IQuery filter = null, Ordering ordering = null) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { + if (ordering == null) + ordering = Ordering.By("Path"); + scope.ReadLock(Constants.Locks.MediaTree); var query = Query().Where(x => x.Path.StartsWith(Constants.System.RecycleBinMediaPathPrefix)); - return _mediaRepository.Get(query); + return _mediaRepository.GetPage(query, pageIndex, pageSize, out totalRecords, filter, ordering); } } @@ -778,7 +664,7 @@ namespace Umbraco.Core.Services.Implement var changeType = TreeChangeTypes.RefreshNode; scope.Events.Dispatch(TreeChanged, this, new TreeChange(media, changeType).ToEventArgs()); - Audit(AuditType.Save, "Save Media performed by user", userId, media.Id); + Audit(AuditType.Save, userId, media.Id); scope.Complete(); } @@ -821,7 +707,7 @@ namespace Umbraco.Core.Services.Implement scope.Events.Dispatch(Saved, this, saveEventArgs); } scope.Events.Dispatch(TreeChanged, this, treeChanges.ToEventArgs()); - Audit(AuditType.Save, "Bulk Save media performed by user", userId == -1 ? 0 : userId, Constants.System.Root); + Audit(AuditType.Save, userId == -1 ? 0 : userId, Constants.System.Root, "Bulk save media"); scope.Complete(); } @@ -855,7 +741,7 @@ namespace Umbraco.Core.Services.Implement DeleteLocked(scope, media); scope.Events.Dispatch(TreeChanged, this, new TreeChange(media, TreeChangeTypes.Remove).ToEventArgs()); - Audit(AuditType.Delete, "Delete Media performed by user", userId, media.Id); + Audit(AuditType.Delete, userId, media.Id); scope.Complete(); } @@ -865,25 +751,8 @@ namespace Umbraco.Core.Services.Implement private void DeleteLocked(IScope scope, IMedia media) { - // then recursively delete descendants, bottom-up - // just repository.Delete + an event - var stack = new Stack(); - stack.Push(media); - var level = 1; - while (stack.Count > 0) + void DoDelete(IMedia c) { - var c = stack.Peek(); - IMedia[] cc; - if (c.Level == level) - while ((cc = c.Children(this).ToArray()).Length > 0) - { - foreach (var ci in cc) - stack.Push(ci); - c = cc[cc.Length - 1]; - } - c = stack.Pop(); - level = c.Level; - _mediaRepository.Delete(c); var args = new DeleteEventArgs(c, false); // raise event & get flagged files scope.Events.Dispatch(Deleted, this, args); @@ -891,6 +760,18 @@ namespace Umbraco.Core.Services.Implement _mediaFileSystem.DeleteFiles(args.MediaFilesToDelete, // remove flagged files (file, e) => Logger.Error(e, "An error occurred while deleting file attached to nodes: {File}", file)); } + + const int pageSize = 500; + var page = 0; + var total = long.MaxValue; + while(page * pageSize < total) + { + //get descendants - ordered from deepest to shallowest + var descendants = GetPagedDescendants(media.Id, page, pageSize, out total, ordering: Ordering.By("Path", Direction.Descending)); + foreach (var c in descendants) + DoDelete(c); + } + DoDelete(media); } //TODO: @@ -924,7 +805,7 @@ namespace Umbraco.Core.Services.Implement //repository.DeleteVersions(id, versionDate); //uow.Events.Dispatch(DeletedVersions, this, new DeleteRevisionsEventArgs(id, false, dateToRetain: versionDate)); - //Audit(uow, AuditType.Delete, "Delete Media by version date performed by user", userId, Constants.System.Root); + //Audit(uow, AuditType.Delete, "Delete Media by version date, userId, Constants.System.Root); //uow.Complete(); } @@ -942,7 +823,7 @@ namespace Umbraco.Core.Services.Implement args.CanCancel = false; scope.Events.Dispatch(DeletedVersions, this, args); - Audit(AuditType.Delete, "Delete Media by version date performed by user", userId, Constants.System.Root); + Audit(AuditType.Delete, userId, Constants.System.Root, "Delete Media by version date"); } /// @@ -978,7 +859,7 @@ namespace Umbraco.Core.Services.Implement args.CanCancel = false; scope.Events.Dispatch(DeletedVersions, this, args); - Audit(AuditType.Delete, "Delete Media by version performed by user", userId, Constants.System.Root); + Audit(AuditType.Delete, userId, Constants.System.Root, "Delete Media by version"); scope.Complete(); } @@ -1007,7 +888,9 @@ namespace Umbraco.Core.Services.Implement var originalPath = media.Path; - if (scope.Events.DispatchCancelable(Trashing, this, new MoveEventArgs(new MoveEventInfo(media, originalPath, Constants.System.RecycleBinMedia)), nameof(Trashing))) + var moveEventInfo = new MoveEventInfo(media, originalPath, Constants.System.RecycleBinMedia); + var moveEventArgs = new MoveEventArgs(true, evtMsgs, moveEventInfo); + if (scope.Events.DispatchCancelable(Trashing, this, moveEventArgs, nameof(Trashing))) { scope.Complete(); return OperationResult.Attempt.Cancel(evtMsgs); @@ -1018,9 +901,10 @@ namespace Umbraco.Core.Services.Implement scope.Events.Dispatch(TreeChanged, this, new TreeChange(media, TreeChangeTypes.RefreshBranch).ToEventArgs()); var moveInfo = moves.Select(x => new MoveEventInfo(x.Item1, x.Item2, x.Item1.ParentId)) .ToArray(); - - scope.Events.Dispatch(Trashed, this, new MoveEventArgs(false, evtMsgs, moveInfo), nameof(Trashed)); - Audit(AuditType.Move, "Move Media to Recycle Bin performed by user", userId, media.Id); + moveEventArgs.MoveInfoCollection = moveInfo; + moveEventArgs.CanCancel = false; + scope.Events.Dispatch(Trashed, this, moveEventArgs, nameof(Trashed)); + Audit(AuditType.Move, userId, media.Id, "Move Media to recycle bin"); scope.Complete(); } @@ -1034,13 +918,15 @@ namespace Umbraco.Core.Services.Implement /// The to move /// Id of the Media's new Parent /// Id of the User moving the Media - public void Move(IMedia media, int parentId, int userId = 0) + public Attempt Move(IMedia media, int parentId, int userId = 0) { + var evtMsgs = EventMessagesFactory.Get(); + // if moving to the recycle bin then use the proper method if (parentId == Constants.System.RecycleBinMedia) { MoveToRecycleBin(media, userId); - return; + return OperationResult.Attempt.Succeed(evtMsgs); } var moves = new List>(); @@ -1054,11 +940,11 @@ namespace Umbraco.Core.Services.Implement throw new InvalidOperationException("Parent does not exist or is trashed."); // causes rollback // causes rollback var moveEventInfo = new MoveEventInfo(media, media.Path, parentId); - var moveEventArgs = new MoveEventArgs(moveEventInfo); + var moveEventArgs = new MoveEventArgs(true, evtMsgs, moveEventInfo); if (scope.Events.DispatchCancelable(Moving, this, moveEventArgs, nameof(Moving))) { scope.Complete(); - return; + return OperationResult.Attempt.Cancel(evtMsgs); } // if media was trashed, and since we're not moving to the recycle bin, @@ -1080,9 +966,10 @@ namespace Umbraco.Core.Services.Implement moveEventArgs.MoveInfoCollection = moveInfo; moveEventArgs.CanCancel = false; scope.Events.Dispatch(Moved, this, moveEventArgs, nameof(Moved)); - Audit(AuditType.Move, "Move Media performed by user", userId, media.Id); + Audit(AuditType.Move, userId, media.Id); scope.Complete(); } + return OperationResult.Attempt.Succeed(evtMsgs); } // MUST be called from within WriteLock @@ -1100,8 +987,8 @@ namespace Umbraco.Core.Services.Implement moves.Add(Tuple.Create(media, media.Path)); // capture original path - // get before moving, in case uow is immediate - var descendants = GetDescendants(media); + //need to store the original path to lookup descendants based on it below + var originalPath = media.Path; // these will be updated by the repo because we changed parentId //media.Path = (parent == null ? "-1" : parent.Path) + "," + media.Id; @@ -1114,14 +1001,21 @@ namespace Umbraco.Core.Services.Implement //paths[media.Id] = media.Path; paths[media.Id] = (parent == null ? (parentId == Constants.System.RecycleBinMedia ? "-1,-21" : "-1") : parent.Path) + "," + media.Id; - foreach (var descendant in descendants) + const int pageSize = 500; + var page = 0; + var total = long.MaxValue; + while (page * pageSize < total) { - moves.Add(Tuple.Create(descendant, descendant.Path)); // capture original path + var descendants = GetPagedDescendantsLocked(originalPath, page++, pageSize, out total, null, Ordering.By("Path", Direction.Ascending)); + foreach (var descendant in descendants) + { + moves.Add(Tuple.Create(descendant, descendant.Path)); // capture original path - // update path and level since we do not update parentId - descendant.Path = paths[descendant.Id] = paths[descendant.ParentId] + "," + descendant.Id; - descendant.Level += levelDelta; - PerformMoveMediaLocked(descendant, userId, trash); + // update path and level since we do not update parentId + descendant.Path = paths[descendant.Id] = paths[descendant.ParentId] + "," + descendant.Id; + descendant.Level += levelDelta; + PerformMoveMediaLocked(descendant, userId, trash); + } } } @@ -1173,7 +1067,7 @@ namespace Umbraco.Core.Services.Implement args.CanCancel = false; scope.Events.Dispatch(EmptiedRecycleBin, this, args); scope.Events.Dispatch(TreeChanged, this, deleted.Select(x => new TreeChange(x, TreeChangeTypes.Remove)).ToEventArgs()); - Audit(AuditType.Delete, "Empty Media Recycle Bin performed by user", 0, Constants.System.RecycleBinMedia); + Audit(AuditType.Delete, 0, Constants.System.RecycleBinMedia, "Empty Media recycle bin"); scope.Complete(); } @@ -1238,7 +1132,7 @@ namespace Umbraco.Core.Services.Implement scope.Events.Dispatch(Saved, this, args); } scope.Events.Dispatch(TreeChanged, this, saved.Select(x => new TreeChange(x, TreeChangeTypes.RefreshNode)).ToEventArgs()); - Audit(AuditType.Sort, "Sorting Media performed by user", userId, 0); + Audit(AuditType.Sort, userId, 0); scope.Complete(); } @@ -1250,9 +1144,9 @@ namespace Umbraco.Core.Services.Implement #region Private Methods - private void Audit(AuditType type, string message, int userId, int objectId) + private void Audit(AuditType type, int userId, int objectId, string message = null) { - _auditRepository.Save(new AuditItem(objectId, message, type, userId)); + _auditRepository.Save(new AuditItem(objectId, type, userId, ObjectTypes.GetName(UmbracoObjectTypes.Media), message)); } #endregion @@ -1434,7 +1328,7 @@ namespace Umbraco.Core.Services.Implement scope.Events.Dispatch(Trashed, this, new MoveEventArgs(false, moveInfos), nameof(Trashed)); scope.Events.Dispatch(TreeChanged, this, changes.ToEventArgs()); - Audit(AuditType.Delete, $"Delete Media of types {string.Join(",", mediaTypeIdsA)} performed by user", userId, Constants.System.Root); + Audit(AuditType.Delete, userId, Constants.System.Root, $"Delete Media of types {string.Join(",", mediaTypeIdsA)}"); scope.Complete(); } diff --git a/src/Umbraco.Core/Services/Implement/MemberService.cs b/src/Umbraco.Core/Services/Implement/MemberService.cs index 211e30d01c..5a644cfec1 100644 --- a/src/Umbraco.Core/Services/Implement/MemberService.cs +++ b/src/Umbraco.Core/Services/Implement/MemberService.cs @@ -337,7 +337,7 @@ namespace Umbraco.Core.Services.Implement if (withIdentity == false) return; - Audit(AuditType.New, $"Member '{member.Name}' was created with Id {member.Id}", member.CreatorId, member.Id); + Audit(AuditType.New, member.CreatorId, member.Id, $"Member '{member.Name}' was created with Id {member.Id}"); } #endregion @@ -393,12 +393,14 @@ namespace Umbraco.Core.Services.Implement // fixme get rid of string filter? - public IEnumerable GetAll(long pageIndex, int pageSize, out long totalRecords, string orderBy, Direction orderDirection, string memberTypeAlias = null, string filter = "") + public IEnumerable GetAll(long pageIndex, int pageSize, out long totalRecords, + string orderBy, Direction orderDirection, string memberTypeAlias = null, string filter = "") { return GetAll(pageIndex, pageSize, out totalRecords, orderBy, orderDirection, true, memberTypeAlias, filter); } - public IEnumerable GetAll(long pageIndex, int pageSize, out long totalRecords, string orderBy, Direction orderDirection, bool orderBySystemField, string memberTypeAlias, string filter) + public IEnumerable GetAll(long pageIndex, int pageSize, out long totalRecords, + string orderBy, Direction orderDirection, bool orderBySystemField, string memberTypeAlias, string filter) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { @@ -843,7 +845,7 @@ namespace Umbraco.Core.Services.Implement saveEventArgs.CanCancel = false; scope.Events.Dispatch(Saved, this, saveEventArgs); } - Audit(AuditType.Save, "Save Member performed by user", 0, member.Id); + Audit(AuditType.Save, 0, member.Id); scope.Complete(); } @@ -884,7 +886,7 @@ namespace Umbraco.Core.Services.Implement saveEventArgs.CanCancel = false; scope.Events.Dispatch(Saved, this, saveEventArgs); } - Audit(AuditType.Save, "Save Member items performed by user", 0, -1); + Audit(AuditType.Save, 0, -1, "Save multiple Members"); scope.Complete(); } @@ -912,7 +914,7 @@ namespace Umbraco.Core.Services.Implement scope.WriteLock(Constants.Locks.MemberTree); DeleteLocked(scope, member, deleteEventArgs); - Audit(AuditType.Delete, "Delete Member performed by user", 0, member.Id); + Audit(AuditType.Delete, 0, member.Id); scope.Complete(); } } @@ -1089,9 +1091,9 @@ namespace Umbraco.Core.Services.Implement #region Private Methods - private void Audit(AuditType type, string message, int userId, int objectId) + private void Audit(AuditType type, int userId, int objectId, string message = null) { - _auditRepository.Save(new AuditItem(objectId, message, type, userId)); + _auditRepository.Save(new AuditItem(objectId, type, userId, ObjectTypes.GetName(UmbracoObjectTypes.Member), message)); } #endregion diff --git a/src/Umbraco.Core/Services/Implement/NotificationService.cs b/src/Umbraco.Core/Services/Implement/NotificationService.cs index 3afb7c3777..ef2bfafcf6 100644 --- a/src/Umbraco.Core/Services/Implement/NotificationService.cs +++ b/src/Umbraco.Core/Services/Implement/NotificationService.cs @@ -6,8 +6,8 @@ using System.Linq; using System.Net.Mail; using System.Text; using System.Threading; -using System.Web; using Umbraco.Core.Configuration; +using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Models; @@ -24,91 +24,25 @@ namespace Umbraco.Core.Services.Implement private readonly IScopeProvider _uowProvider; private readonly IUserService _userService; private readonly IContentService _contentService; + private readonly ILocalizationService _localizationService; private readonly INotificationsRepository _notificationsRepository; private readonly IGlobalSettings _globalSettings; + private readonly IContentSection _contentSection; private readonly ILogger _logger; - public NotificationService(IScopeProvider provider, IUserService userService, IContentService contentService, ILogger logger, - INotificationsRepository notificationsRepository, IGlobalSettings globalSettings) + public NotificationService(IScopeProvider provider, IUserService userService, IContentService contentService, ILocalizationService localizationService, + ILogger logger, INotificationsRepository notificationsRepository, IGlobalSettings globalSettings, IContentSection contentSection) { _notificationsRepository = notificationsRepository; _globalSettings = globalSettings; + _contentSection = contentSection; _uowProvider = provider ?? throw new ArgumentNullException(nameof(provider)); _userService = userService ?? throw new ArgumentNullException(nameof(userService)); _contentService = contentService ?? throw new ArgumentNullException(nameof(contentService)); + _localizationService = localizationService; _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } - /// - /// Sends the notifications for the specified user regarding the specified node and action. - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// Currently this will only work for Content entities! - /// - public void SendNotifications(IUser operatingUser, IUmbracoEntity entity, string action, string actionName, HttpContextBase http, - Func createSubject, - Func createBody) - { - if (entity is IContent == false) - throw new NotSupportedException(); - - var content = (IContent) entity; - - // lazily get previous version - IContentBase prevVersion = null; - - // do not load *all* users in memory at once - // do not load notifications *per user* (N+1 select) - // cannot load users & notifications in 1 query (combination btw User2AppDto and User2NodeNotifyDto) - // => get batches of users, get all their notifications in 1 query - // re. users: - // users being (dis)approved = not an issue, filtered in memory not in SQL - // users being modified or created = not an issue, ordering by ID, as long as we don't *insert* low IDs - // users being deleted = not an issue for GetNextUsers - var id = 0; - var nodeIds = content.Path.Split(',').Select(int.Parse).ToArray(); - const int pagesz = 400; // load batches of 400 users - do - { - // users are returned ordered by id, notifications are returned ordered by user id - var users = ((UserService) _userService).GetNextUsers(id, pagesz).Where(x => x.IsApproved).ToList(); - var notifications = GetUsersNotifications(users.Select(x => x.Id), action, nodeIds, Constants.ObjectTypes.Document).ToList(); - if (notifications.Count == 0) break; - - var i = 0; - foreach (var user in users) - { - // continue if there's no notification for this user - if (notifications[i].UserId != user.Id) continue; // next user - - // lazy load prev version - if (prevVersion == null) - { - prevVersion = GetPreviousVersion(entity.Id); - } - - // queue notification - var req = CreateNotificationRequest(operatingUser, user, content, prevVersion, actionName, http, createSubject, createBody); - Enqueue(req); - - // skip other notifications for this user - while (i < notifications.Count && notifications[i++].UserId == user.Id) ; - if (i >= notifications.Count) break; // break if no more notifications - } - - // load more users if any - id = users.Count == pagesz ? users.Last().Id + 1 : -1; - - } while (id > 0); - } - /// /// Gets the previous version to the latest version of the content item if there is one /// @@ -131,20 +65,14 @@ namespace Umbraco.Core.Services.Implement /// /// /// - /// + /// /// /// - /// - /// Currently this will only work for Content entities! - /// - public void SendNotifications(IUser operatingUser, IEnumerable entities, string action, string actionName, HttpContextBase http, - Func createSubject, - Func createBody) + public void SendNotifications(IUser operatingUser, IEnumerable entities, string action, string actionName, Uri siteUri, + Func<(IUser user, NotificationEmailSubjectParams subject), string> createSubject, + Func<(IUser user, NotificationEmailBodyParams body, bool isHtml), string> createBody) { - if (entities is IEnumerable == false) - throw new NotSupportedException(); - - var entitiesL = entities as List ?? entities.Cast().ToList(); + var entitiesL = entities.ToList(); //exit if there are no entities if (entitiesL.Count == 0) return; @@ -156,7 +84,7 @@ namespace Umbraco.Core.Services.Implement var prevVersionDictionary = new Dictionary(); // see notes above - var id = 0; + var id = Constants.Security.SuperUserId; const int pagesz = 400; // load batches of 400 users do { @@ -185,7 +113,7 @@ namespace Umbraco.Core.Services.Implement } // queue notification - var req = CreateNotificationRequest(operatingUser, user, content, prevVersionDictionary[content.Id], actionName, http, createSubject, createBody); + var req = CreateNotificationRequest(operatingUser, user, content, prevVersionDictionary[content.Id], actionName, siteUri, createSubject, createBody); Enqueue(req); } @@ -350,118 +278,141 @@ namespace Umbraco.Core.Services.Implement /// /// /// The action readable name - currently an action is just a single letter, this is the name associated with the letter - /// + /// /// Callback to create the mail subject /// Callback to create the mail body - private NotificationRequest CreateNotificationRequest(IUser performingUser, IUser mailingUser, IContentBase content, IContentBase oldDoc, - string actionName, HttpContextBase http, - Func createSubject, - Func createBody) + private NotificationRequest CreateNotificationRequest(IUser performingUser, IUser mailingUser, IContent content, IContentBase oldDoc, + string actionName, + Uri siteUri, + Func<(IUser user, NotificationEmailSubjectParams subject), string> createSubject, + Func<(IUser user, NotificationEmailBodyParams body, bool isHtml), string> createBody) { if (performingUser == null) throw new ArgumentNullException("performingUser"); if (mailingUser == null) throw new ArgumentNullException("mailingUser"); if (content == null) throw new ArgumentNullException("content"); - if (http == null) throw new ArgumentNullException("http"); + if (siteUri == null) throw new ArgumentNullException("siteUri"); if (createSubject == null) throw new ArgumentNullException("createSubject"); if (createBody == null) throw new ArgumentNullException("createBody"); // build summary var summary = new StringBuilder(); - var props = content.Properties.ToArray(); - foreach (var p in props) + + if (content.ContentType.VariesByNothing()) { - //fixme doesn't take into account variants - - var newText = p.GetValue() != null ? p.GetValue().ToString() : ""; - var oldText = newText; - - // check if something was changed and display the changes otherwise display the fields - if (oldDoc.Properties.Contains(p.PropertyType.Alias)) + if (!_contentSection.DisableHtmlEmail) { - var oldProperty = oldDoc.Properties[p.PropertyType.Alias]; - oldText = oldProperty.GetValue() != null ? oldProperty.GetValue().ToString() : ""; + //create the html summary for invariant content - // replace html with char equivalent - ReplaceHtmlSymbols(ref oldText); - ReplaceHtmlSymbols(ref newText); + //list all of the property values like we used to + summary.Append(""); + foreach (var p in content.Properties) + { + //fixme doesn't take into account variants + + var newText = p.GetValue() != null ? p.GetValue().ToString() : ""; + var oldText = newText; + + // check if something was changed and display the changes otherwise display the fields + if (oldDoc.Properties.Contains(p.PropertyType.Alias)) + { + var oldProperty = oldDoc.Properties[p.PropertyType.Alias]; + oldText = oldProperty.GetValue() != null ? oldProperty.GetValue().ToString() : ""; + + // replace html with char equivalent + ReplaceHtmlSymbols(ref oldText); + ReplaceHtmlSymbols(ref newText); + } + + //show the values + summary.Append(""); + summary.Append(""); + summary.Append(""); + summary.Append(""); + } + summary.Append("
"); + summary.Append(p.PropertyType.Name); + summary.Append(""); + summary.Append(newText); + summary.Append("
"); } + + } + else if (content.ContentType.VariesByCulture()) + { + //it's variant, so detect what cultures have changed - - // make sure to only highlight changes done using TinyMCE editor... other changes will be displayed using default summary - // TODO: We should probably allow more than just tinymce?? - if ((p.PropertyType.PropertyEditorAlias == Constants.PropertyEditors.Aliases.TinyMce) - && string.CompareOrdinal(oldText, newText) != 0) + if (!_contentSection.DisableHtmlEmail) { - summary.Append(""); - summary.Append(" Note: "); - summary.Append( - " Red for deleted characters Yellow for inserted characters"); - summary.Append(""); - summary.Append(""); - summary.Append(" New "); - summary.Append(p.PropertyType.Name); - summary.Append(""); - summary.Append(""); - summary.Append(ReplaceLinks(CompareText(oldText, newText, true, false, "", string.Empty), http.Request)); - summary.Append(""); - summary.Append(""); - summary.Append(""); - summary.Append(" Old "); - summary.Append(p.PropertyType.Name); - summary.Append(""); - summary.Append(""); - summary.Append(ReplaceLinks(CompareText(newText, oldText, true, false, "", string.Empty), http.Request)); - summary.Append(""); - summary.Append(""); + //Create the html based summary (ul of culture names) + + var culturesChanged = content.CultureInfos.Where(x => x.Value.WasDirty()) + .Select(x => x.Key) + .Select(_localizationService.GetLanguageByIsoCode) + .WhereNotNull() + .Select(x => x.CultureName); + summary.Append("
    "); + foreach (var culture in culturesChanged) + { + summary.Append("
  • "); + summary.Append(culture); + summary.Append("
  • "); + } + summary.Append("
"); } else { - summary.Append(""); - summary.Append(""); - summary.Append(p.PropertyType.Name); - summary.Append(""); - summary.Append(""); - summary.Append(newText); - summary.Append(""); - summary.Append(""); + //Create the text based summary (csv of culture names) + + var culturesChanged = string.Join(", ", content.CultureInfos.Where(x => x.Value.WasDirty()) + .Select(x => x.Key) + .Select(_localizationService.GetLanguageByIsoCode) + .WhereNotNull() + .Select(x => x.CultureName)); + + summary.Append("'"); + summary.Append(culturesChanged); + summary.Append("'"); } - summary.Append( - " "); + } + else + { + //not supported yet... + throw new NotSupportedException(); } - string protocol = _globalSettings.UseHttps ? "https" : "http"; + var protocol = _globalSettings.UseHttps ? "https" : "http"; + var subjectVars = new NotificationEmailSubjectParams( + string.Concat(siteUri.Authority, IOHelper.ResolveUrl(SystemDirectories.Umbraco)), + actionName, + content.Name); - string[] subjectVars = { - string.Concat(http.Request.ServerVariables["SERVER_NAME"], ":", http.Request.Url.Port, IOHelper.ResolveUrl(SystemDirectories.Umbraco)), - actionName, - content.Name - }; - string[] bodyVars = { - mailingUser.Name, - actionName, - content.Name, - performingUser.Name, - string.Concat(http.Request.ServerVariables["SERVER_NAME"], ":", http.Request.Url.Port, IOHelper.ResolveUrl(SystemDirectories.Umbraco)), - content.Id.ToString(CultureInfo.InvariantCulture), summary.ToString(), - string.Format("{2}://{0}/{1}", - string.Concat(http.Request.ServerVariables["SERVER_NAME"], ":", http.Request.Url.Port), - //TODO: RE-enable this so we can have a nice url - /*umbraco.library.NiceUrl(documentObject.Id))*/ - string.Concat(content.Id, ".aspx"), - protocol) - - }; + var bodyVars = new NotificationEmailBodyParams( + mailingUser.Name, + actionName, + content.Name, + content.Id.ToString(CultureInfo.InvariantCulture), + string.Format("{2}://{0}/{1}", + string.Concat(siteUri.Authority), + //TODO: RE-enable this so we can have a nice url + /*umbraco.library.NiceUrl(documentObject.Id))*/ + string.Concat(content.Id, ".aspx"), + protocol), + performingUser.Name, + string.Concat(siteUri.Authority, IOHelper.ResolveUrl(SystemDirectories.Umbraco)), + summary.ToString()); // create the mail message - var mail = new MailMessage(UmbracoConfig.For.UmbracoSettings().Content.NotificationEmailAddress, mailingUser.Email); + var mail = new MailMessage(_contentSection.NotificationEmailAddress, mailingUser.Email); // populate the message - mail.Subject = createSubject(mailingUser, subjectVars); - if (UmbracoConfig.For.UmbracoSettings().Content.DisableHtmlEmail) + + + mail.Subject = createSubject((mailingUser, subjectVars)); + if (_contentSection.DisableHtmlEmail) { mail.IsBodyHtml = false; - mail.Body = createBody(mailingUser, bodyVars); + mail.Body = createBody((user: mailingUser, body: bodyVars, false)); } else { @@ -470,14 +421,14 @@ namespace Umbraco.Core.Services.Implement string.Concat(@" -", createBody(mailingUser, bodyVars)); +", createBody((user: mailingUser, body: bodyVars, true))); } // nh, issue 30724. Due to hardcoded http strings in resource files, we need to check for https replacements here // adding the server name to make sure we don't replace external links if (_globalSettings.UseHttps && string.IsNullOrEmpty(mail.Body) == false) { - string serverName = http.Request.ServerVariables["SERVER_NAME"]; + string serverName = siteUri.Host; mail.Body = mail.Body.Replace( string.Format("http://{0}", serverName), string.Format("https://{0}", serverName)); @@ -486,12 +437,10 @@ namespace Umbraco.Core.Services.Implement return new NotificationRequest(mail, actionName, mailingUser.Name, mailingUser.Email); } - private string ReplaceLinks(string text, HttpRequestBase request) + private string ReplaceLinks(string text, Uri siteUri) { var sb = new StringBuilder(_globalSettings.UseHttps ? "https://" : "http://"); - sb.Append(request.ServerVariables["SERVER_NAME"]); - sb.Append(":"); - sb.Append(request.Url.Port); + sb.Append(siteUri.Authority); sb.Append("/"); var domain = sb.ToString(); text = text.Replace("href=\"/", "href=\"" + domain); @@ -505,6 +454,7 @@ namespace Umbraco.Core.Services.Implement /// The old string. private static void ReplaceHtmlSymbols(ref string oldString) { + if (oldString.IsNullOrWhiteSpace()) return; oldString = oldString.Replace(" ", " "); oldString = oldString.Replace("’", "'"); oldString = oldString.Replace("&", "&"); @@ -512,69 +462,7 @@ namespace Umbraco.Core.Services.Implement oldString = oldString.Replace("”", "”"); oldString = oldString.Replace(""", "\""); } - - /// - /// Compares the text. - /// - /// The old text. - /// The new text. - /// if set to true [display inserted text]. - /// if set to true [display deleted text]. - /// The inserted style. - /// The deleted style. - /// - private static string CompareText(string oldText, string newText, bool displayInsertedText, - bool displayDeletedText, string insertedStyle, string deletedStyle) - { - var sb = new StringBuilder(); - var diffs = Diff.DiffText1(oldText, newText); - - int pos = 0; - for (var n = 0; n < diffs.Length; n++) - { - var it = diffs[n]; - - // write unchanged chars - while ((pos < it.StartB) && (pos < newText.Length)) - { - sb.Append(newText[pos]); - pos++; - } // while - - // write deleted chars - if (displayDeletedText && it.DeletedA > 0) - { - sb.Append(deletedStyle); - for (var m = 0; m < it.DeletedA; m++) - { - sb.Append(oldText[it.StartA + m]); - } // for - sb.Append("
"); - } - - // write inserted chars - if (displayInsertedText && pos < it.StartB + it.InsertedB) - { - sb.Append(insertedStyle); - while (pos < it.StartB + it.InsertedB) - { - sb.Append(newText[pos]); - pos++; - } // while - sb.Append("
"); - } // if - } // while - - // write rest of unchanged chars - while (pos < newText.Length) - { - sb.Append(newText[pos]); - pos++; - } // while - - return sb.ToString(); - } - + // manage notifications // ideally, would need to use IBackgroundTasks - but they are not part of Core! diff --git a/src/Umbraco.Core/Services/Implement/PackagingService.cs b/src/Umbraco.Core/Services/Implement/PackagingService.cs index 67e07e63b6..fff865e097 100644 --- a/src/Umbraco.Core/Services/Implement/PackagingService.cs +++ b/src/Umbraco.Core/Services/Implement/PackagingService.cs @@ -1318,7 +1318,7 @@ namespace Umbraco.Core.Services.Implement sortOrder = int.Parse(sortOrderAttribute.Value); } - if (macro.Properties.Any(x => string.Equals(x.Alias, propertyAlias, StringComparison.OrdinalIgnoreCase))) continue; + if (macro.Properties.Values.Any(x => string.Equals(x.Alias, propertyAlias, StringComparison.OrdinalIgnoreCase))) continue; macro.Properties.Add(new MacroProperty(propertyAlias, propertyName, sortOrder, editorAlias)); sortOrder++; } @@ -1485,7 +1485,7 @@ namespace Umbraco.Core.Services.Implement private void Audit(AuditType type, string message, int userId, int objectId) { - _auditRepository.Save(new AuditItem(objectId, message, type, userId)); + _auditRepository.Save(new AuditItem(objectId, type, userId, "Package", message)); } #endregion diff --git a/src/Umbraco.Core/Services/PublishResult.cs b/src/Umbraco.Core/Services/PublishResult.cs index 073d7ce1cb..4f1ff776a2 100644 --- a/src/Umbraco.Core/Services/PublishResult.cs +++ b/src/Umbraco.Core/Services/PublishResult.cs @@ -21,7 +21,7 @@ namespace Umbraco.Core.Services /// Initializes a new instance of the class. ///
public PublishResult(EventMessages eventMessages, IContent content) - : base(PublishResultType.Success, eventMessages, content) + : base(PublishResultType.SuccessPublish, eventMessages, content) { } /// diff --git a/src/Umbraco.Core/Services/PublishResultType.cs b/src/Umbraco.Core/Services/PublishResultType.cs index 15b2f503c7..f79dab91d3 100644 --- a/src/Umbraco.Core/Services/PublishResultType.cs +++ b/src/Umbraco.Core/Services/PublishResultType.cs @@ -1,80 +1,146 @@ namespace Umbraco.Core.Services { - /// - /// A value indicating the result of publishing a content item. + /// A value indicating the result of publishing or unpublishing a document. /// public enum PublishResultType : byte { // all "ResultType" enums must be byte-based, and declare Failed = 128, and declare // every failure codes as >128 - see OperationResult and OperationResultType for details. - /// - /// The publishing was successful. - /// - Success = 0, + #region Success - Publish /// - /// The item was already published. + /// The document was successfully published. /// - SuccessAlready = 1, + SuccessPublish = 0, + + /// + /// The specified document culture was successfully published. + /// + SuccessPublishCulture = 1, + + /// + /// The document was already published. + /// + SuccessPublishAlready = 2, + + #endregion + + #region Success - Unpublish + + /// + /// The document was successfully unpublished. + /// + SuccessUnpublish = 3, + + /// + /// The document was already unpublished. + /// + SuccessUnpublishAlready = 4, + + /// + /// The specified document culture was unpublished, the document item itself remains published. + /// + SuccessUnpublishCulture = 5, + + /// + /// The specified document culture was unpublished, and was a mandatory culture, therefore the document itself was unpublished. + /// + SuccessUnpublishMandatoryCulture = 6, + + #endregion + + #region Success - Mixed + + /// + /// Specified document cultures were successfully published and unpublished (in the same operation). + /// + SuccessMixedCulture = 7, + + #endregion + + #region Failed - Publish /// /// The operation failed. /// /// All values above this value indicate a failure. - Failed = 128, + FailedPublish = 128, /// - /// The content could not be published because it's ancestor path isn't published. + /// The document could not be published because its ancestor path is not published. /// - FailedPathNotPublished = Failed | 1, + FailedPublishPathNotPublished = FailedPublish | 1, /// - /// The content item was scheduled to be un-published and it has expired so we cannot force it to be + /// The document has expired so we cannot force it to be /// published again as part of a bulk publish operation. /// - FailedHasExpired = Failed | 2, + FailedPublishHasExpired = FailedPublish | 2, /// - /// The content item is scheduled to be released in the future and therefore we cannot force it to + /// The document is scheduled to be released in the future and therefore we cannot force it to /// be published during a bulk publish operation. /// - FailedAwaitingRelease = Failed | 3, + FailedPublishAwaitingRelease = FailedPublish | 3, /// - /// The content item could not be published because it is in the trash. + /// A document culture has expired so we cannot force it to be + /// published again as part of a bulk publish operation. /// - FailedIsTrashed = Failed | 4, + FailedPublishCultureHasExpired = FailedPublish | 4, + + /// + /// A document culture is scheduled to be released in the future and therefore we cannot force it to + /// be published during a bulk publish operation. + /// + FailedPublishCultureAwaitingRelease = FailedPublish | 5, + + /// + /// The document could not be published because it is in the trash. + /// + FailedPublishIsTrashed = FailedPublish | 6, /// /// The publish action has been cancelled by an event handler. /// - FailedCancelledByEvent = Failed | 5, + FailedPublishCancelledByEvent = FailedPublish | 7, /// - /// The content item could not be published because it contains invalid data (has not passed validation requirements). + /// The document could not be published because it contains invalid data (has not passed validation requirements). /// - FailedContentInvalid = Failed | 6, + FailedPublishContentInvalid = FailedPublish | 8, /// - /// Cannot republish a document that hasn't been published. + /// The document could not be published because it has no publishing flags or values. /// - FailedNoPublishedValues = Failed | 7, // in ContentService.StrategyCanPublish - fixme weird + FailedPublishNothingToPublish = FailedPublish | 9, // in ContentService.StrategyCanPublish - fixme weird /// - /// Some mandatory cultures are missing, or are not valid. + /// The document could not be published because some mandatory cultures are missing. /// - FailedCannotPublish = Failed | 8, // in ContentController.PublishInternal - fixme // FailedByCulture? + FailedPublishMandatoryCultureMissing = FailedPublish | 10, // in ContentService.SavePublishing /// - /// Publishing changes triggered an unpublishing, due to missing mandatory cultures, and unpublishing failed. + /// The document could not be published because it has been modified by another user. /// - FailedToUnpublish = Failed | 9, // in ContentService.SavePublishing + FailedPublishConcurrencyViolation = FailedPublish | 11, + + #endregion + + #region Failed - Unpublish /// - /// Some mandatory cultures are missing. + /// The document could not be unpublished. /// - FailedByCulture = Failed | 10, // in ContentService.SavePublishing + FailedUnpublish = FailedPublish | 11, // in ContentService.SavePublishing + + /// + /// The unpublish action has been cancelled by an event handler. + /// + FailedUnpublishCancelledByEvent = FailedPublish | 12, + + #endregion } } diff --git a/src/Umbraco.Core/Services/UnpublishResult.cs b/src/Umbraco.Core/Services/UnpublishResult.cs deleted file mode 100644 index 7cd1506e6c..0000000000 --- a/src/Umbraco.Core/Services/UnpublishResult.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Umbraco.Core.Events; -using Umbraco.Core.Models; - -namespace Umbraco.Core.Services -{ - /// - /// Represents the result of unpublishing a document. - /// - public class UnpublishResult : OperationResult - { - /// - /// Creates a successful result - /// - /// - /// - public UnpublishResult(EventMessages eventMessages, IContent entity) : base(UnpublishResultType.Success, eventMessages, entity) - { - } - - public UnpublishResult(UnpublishResultType result, EventMessages eventMessages, IContent entity) : base(result, eventMessages, entity) - { - } - - /// - /// Gets the document. - /// - public IContent Content => Entity; - } -} diff --git a/src/Umbraco.Core/Services/UnpublishResultType.cs b/src/Umbraco.Core/Services/UnpublishResultType.cs deleted file mode 100644 index e61e786a05..0000000000 --- a/src/Umbraco.Core/Services/UnpublishResultType.cs +++ /dev/null @@ -1,39 +0,0 @@ -namespace Umbraco.Core.Services -{ - /// - /// A value indicating the result of unpublishing a content item. - /// - public enum UnpublishResultType : byte - { - /// - /// The unpublishing was successful. - /// - Success = 0, - - /// - /// The item was already unpublished. - /// - SuccessAlready = 1, - - /// - /// The specified variant was unpublished, the content item itself remains published. - /// - SuccessCulture = 2, - - /// - /// The specified variant was a mandatory culture therefore it was unpublished and the content item itself is unpublished - /// - SuccessMandatoryCulture = 3, - - /// - /// The operation failed. - /// - /// All values above this value indicate a failure. - Failed = 128, - - /// - /// The publish action has been cancelled by an event handler. - /// - FailedCancelledByEvent = Failed | 5, - } -} diff --git a/src/Umbraco.Core/StringExtensions.cs b/src/Umbraco.Core/StringExtensions.cs index 9c686c4353..461b1d3c1e 100644 --- a/src/Umbraco.Core/StringExtensions.cs +++ b/src/Umbraco.Core/StringExtensions.cs @@ -540,7 +540,7 @@ namespace Umbraco.Core public static string StripHtml(this string text) { const string pattern = @"<(.|\n)*?>"; - return Regex.Replace(text, pattern, String.Empty); + return Regex.Replace(text, pattern, string.Empty); } /// diff --git a/src/Umbraco.Core/Strings/Css/StylesheetRule.cs b/src/Umbraco.Core/Strings/Css/StylesheetRule.cs index e97e6cff1e..6f91906250 100644 --- a/src/Umbraco.Core/Strings/Css/StylesheetRule.cs +++ b/src/Umbraco.Core/Strings/Css/StylesheetRule.cs @@ -20,8 +20,19 @@ namespace Umbraco.Core.Strings.Css sb.Append("*/"); sb.Append(Environment.NewLine); sb.Append(Selector); - sb.Append("{"); - sb.Append(Styles.IsNullOrWhiteSpace() ? "" : Styles.Trim()); + sb.Append(" {"); + sb.Append(Environment.NewLine); + // append nicely formatted style rules + // - using tabs because the back office code editor uses tabs + if (Styles.IsNullOrWhiteSpace() == false) + { + // since we already have a string builder in play here, we'll append to it the "hard" way + // instead of using string interpolation (for increased performance) + foreach (var style in Styles.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries)) + { + sb.Append("\t").Append(style.StripNewLines().Trim()).Append(";").Append(Environment.NewLine); + } + } sb.Append("}"); return sb.ToString(); diff --git a/src/Umbraco.Core/Sync/ServerSyncWebServiceClient.cs b/src/Umbraco.Core/Sync/ServerSyncWebServiceClient.cs index cac53e1f99..5ad9140811 100644 --- a/src/Umbraco.Core/Sync/ServerSyncWebServiceClient.cs +++ b/src/Umbraco.Core/Sync/ServerSyncWebServiceClient.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Web.Services; using Umbraco.Core.IO; @@ -8,6 +9,7 @@ namespace Umbraco.Core.Sync /// The client Soap service for making distrubuted cache calls between servers /// [WebServiceBinding(Name = "CacheRefresherSoap", Namespace = "http://umbraco.org/webservices/")] + [Obsolete("Legacy load balancing is obsolete and should be removed")] internal class ServerSyncWebServiceClient : System.Web.Services.Protocols.SoapHttpClientProtocol { diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj old mode 100644 new mode 100755 index aef18e59db..23b90aaf3c --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -143,7 +143,6 @@ - @@ -336,6 +335,7 @@ + @@ -352,18 +352,25 @@ + + + + + + + @@ -372,8 +379,13 @@ + + + + + @@ -390,6 +402,8 @@ + + @@ -403,6 +417,7 @@ + @@ -424,6 +439,7 @@ + @@ -592,9 +608,6 @@ - - - @@ -1238,10 +1251,6 @@ - - - - @@ -1259,7 +1268,6 @@ - @@ -1279,6 +1287,7 @@ + @@ -1401,8 +1410,6 @@ - - @@ -1449,7 +1456,6 @@ - diff --git a/src/Umbraco.Examine/UmbracoContentIndexer.cs b/src/Umbraco.Examine/UmbracoContentIndexer.cs index 94982c8591..fab9f226a4 100644 --- a/src/Umbraco.Examine/UmbracoContentIndexer.cs +++ b/src/Umbraco.Examine/UmbracoContentIndexer.cs @@ -258,7 +258,8 @@ namespace Umbraco.Examine else { //add the published filter - descendants = ContentService.GetPagedDescendants(contentParentId, pageIndex, pageSize, out total, "Path", Direction.Ascending, true, _publishedQuery); + descendants = ContentService.GetPagedDescendants(contentParentId, pageIndex, pageSize, out total, + _publishedQuery, Ordering.By("Path", Direction.Ascending)); } //if specific types are declared we need to post filter them diff --git a/src/Umbraco.Examine/UmbracoExamineIndexer.cs b/src/Umbraco.Examine/UmbracoExamineIndexer.cs index 64fd7b5c71..a4c1fb4336 100644 --- a/src/Umbraco.Examine/UmbracoExamineIndexer.cs +++ b/src/Umbraco.Examine/UmbracoExamineIndexer.cs @@ -418,7 +418,7 @@ namespace Umbraco.Examine //icon if (e.IndexItem.ValueSet.Values.TryGetValue("icon", out var icon) && e.IndexItem.ValueSet.Values.ContainsKey(IconFieldName) == false) { - e.IndexItem.ValueSet.Values[IconFieldName] = new List { icon }; + e.IndexItem.ValueSet.Values[IconFieldName] = icon; } } diff --git a/src/Umbraco.Examine/UmbracoMemberIndexer.cs b/src/Umbraco.Examine/UmbracoMemberIndexer.cs index b7cbcc19bc..82bf3b9cf6 100644 --- a/src/Umbraco.Examine/UmbracoMemberIndexer.cs +++ b/src/Umbraco.Examine/UmbracoMemberIndexer.cs @@ -175,13 +175,17 @@ namespace Umbraco.Examine if (e.IndexItem.ValueSet.Values.TryGetValue("key", out var key) && e.IndexItem.ValueSet.Values.ContainsKey("__key") == false) { //double __ prefix means it will be indexed as culture invariant - e.IndexItem.ValueSet.Values["__key"] = new List { key }; + e.IndexItem.ValueSet.Values["__key"] = key; } if (e.IndexItem.ValueSet.Values.TryGetValue("email", out var email) && e.IndexItem.ValueSet.Values.ContainsKey("_searchEmail") == false) { - //will be indexed as full text (the default anaylyzer) - e.IndexItem.ValueSet.Values["_searchEmail"] = new List { email?.ToString().Replace(".", " ").Replace("@", " ") }; + if (email.Count > 0) + { + //will be indexed as full text (the default anaylyzer) + e.IndexItem.ValueSet.Values["_searchEmail"] = new List { email[0]?.ToString().Replace(".", " ").Replace("@", " ") }; + } + } } diff --git a/src/Umbraco.Tests.Benchmarks/Config/QuickRunConfigAttribute.cs b/src/Umbraco.Tests.Benchmarks/Config/QuickRunConfigAttribute.cs index f7d6b6bb72..52d670de3c 100644 --- a/src/Umbraco.Tests.Benchmarks/Config/QuickRunConfigAttribute.cs +++ b/src/Umbraco.Tests.Benchmarks/Config/QuickRunConfigAttribute.cs @@ -1,13 +1,14 @@ -using BenchmarkDotNet.Configs; +using System; +using BenchmarkDotNet.Configs; using BenchmarkDotNet.Horology; using BenchmarkDotNet.Jobs; -using System; namespace Umbraco.Tests.Benchmarks.Config { /// /// Configures the benchmark to run with less warmup and a shorter iteration time than the standard benchmark. /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] public class QuickRunConfigAttribute : Attribute, IConfigSource { /// @@ -15,11 +16,11 @@ namespace Umbraco.Tests.Benchmarks.Config /// public QuickRunConfigAttribute() { - Config = (ManualConfig) ManualConfig.CreateEmpty() + this.Config = (ManualConfig)ManualConfig.CreateEmpty() .With(Job.Default.WithLaunchCount(1) // benchmark process will be launched only once .WithIterationTime(new TimeInterval(100, TimeUnit.Millisecond)) // 100ms per iteration .WithWarmupCount(3) // 3 warmup iteration - .WithTargetCount(3)); // 3 target iteration + .WithIterationCount(3)); // 3 target iteration } /// @@ -28,6 +29,6 @@ namespace Umbraco.Tests.Benchmarks.Config protected ManualConfig Config { get; } /// - IConfig IConfigSource.Config => Config; + IConfig IConfigSource.Config => this.Config; } -} \ No newline at end of file +} diff --git a/src/Umbraco.Tests.Benchmarks/CtorInvokeBenchmarks.cs b/src/Umbraco.Tests.Benchmarks/CtorInvokeBenchmarks.cs index 4855b161df..5588e13d12 100644 --- a/src/Umbraco.Tests.Benchmarks/CtorInvokeBenchmarks.cs +++ b/src/Umbraco.Tests.Benchmarks/CtorInvokeBenchmarks.cs @@ -27,7 +27,7 @@ namespace Umbraco.Tests.Benchmarks .WithLaunchCount(1) // benchmark process will be launched only once .WithIterationTime(TimeInterval.FromMilliseconds(400)) .WithWarmupCount(3) - .WithTargetCount(6)); + .WithIterationCount(6)); } } @@ -144,7 +144,7 @@ namespace Umbraco.Tests.Benchmarks // however, unfortunately, the generated "compiled to delegate" code cannot access private stuff :( - _emittedCtor = ReflectionUtilities.EmitCtor>(); + _emittedCtor = ReflectionUtilities.EmitConstuctor>(); } public IFoo IlCtor(IFoo foo) diff --git a/src/Umbraco.Tests.Benchmarks/Program.cs b/src/Umbraco.Tests.Benchmarks/Program.cs index c9332e7fa3..62137bd85d 100644 --- a/src/Umbraco.Tests.Benchmarks/Program.cs +++ b/src/Umbraco.Tests.Benchmarks/Program.cs @@ -2,11 +2,8 @@ namespace Umbraco.Tests.Benchmarks { - internal class Program + internal static class Program { - public static void Main(string[] args) - { - new BenchmarkSwitcher(typeof(Program).Assembly).Run(args); - } + private static void Main(string[] args) => new BenchmarkSwitcher(typeof(Program).Assembly).Run(args); } } diff --git a/src/Umbraco.Tests.Benchmarks/Properties/AssemblyInfo.cs b/src/Umbraco.Tests.Benchmarks/Properties/AssemblyInfo.cs index 2ab0051c26..9f5a3c7453 100644 --- a/src/Umbraco.Tests.Benchmarks/Properties/AssemblyInfo.cs +++ b/src/Umbraco.Tests.Benchmarks/Properties/AssemblyInfo.cs @@ -10,7 +10,7 @@ using System.Runtime.InteropServices; [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("Umbraco.Tests.Benchmarks")] -[assembly: AssemblyCopyright("Copyright © 2016")] +[assembly: AssemblyCopyright("Copyright © 2018")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] @@ -20,7 +20,7 @@ using System.Runtime.InteropServices; [assembly: ComVisible(false)] // The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("86deb346-089f-4106-89c8-d852b9cf2a33")] +[assembly: Guid("3a33adc9-c6c0-4db1-a613-a9af0210df3d")] // Version information for an assembly consists of the following four values: // diff --git a/src/Umbraco.Tests.Benchmarks/TryConvertToBenchmarks.cs b/src/Umbraco.Tests.Benchmarks/TryConvertToBenchmarks.cs index 57b47dc1d0..7e73c5e438 100644 --- a/src/Umbraco.Tests.Benchmarks/TryConvertToBenchmarks.cs +++ b/src/Umbraco.Tests.Benchmarks/TryConvertToBenchmarks.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; +using System.Linq; using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Engines; using Umbraco.Core; namespace Umbraco.Tests.Benchmarks @@ -12,9 +14,9 @@ namespace Umbraco.Tests.Benchmarks private static readonly string Date = "Saturday 10, November 2012"; [Benchmark(Description = "List to IEnumerable")] - public IEnumerable TryConvertToEnumerable() + public IList TryConvertToEnumerable() { - return List.TryConvertTo>().Result; + return List.TryConvertTo>().Result.ToList(); } [Benchmark(Description = "Int to Double")] @@ -41,4 +43,4 @@ namespace Umbraco.Tests.Benchmarks return Date.TryConvertTo().Result; } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj b/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj index 9755e4f9db..99bb768842 100644 --- a/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj +++ b/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj @@ -1,20 +1,17 @@  - + Debug AnyCPU - {86DEB346-089F-4106-89C8-D852B9CF2A33} + {3A33ADC9-C6C0-4DB1-A613-A9AF0210DF3D} Exe - Properties Umbraco.Tests.Benchmarks Umbraco.Tests.Benchmarks v4.7.2 512 - - - - false + true + true AnyCPU @@ -25,7 +22,6 @@ DEBUG;TRACE prompt 4 - latest AnyCPU @@ -35,92 +31,27 @@ TRACE prompt 4 - latest - - - Always + false + 7.3 - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - @@ -130,27 +61,34 @@ - + {31785bc3-256c-4613-b2f5-a1b0bdded8c1} Umbraco.Core + + {07fbc26b-2927-4a22-8d96-d644c667fecc} + Umbraco.Examine + {5d3b8245-ada6-453f-a008-50ed04bfe770} Umbraco.Tests + + {4c4c194c-b5e4-4991-8f87-4373e24cc19f} + Umbraco.Web.UI + {651e1350-91b6-44b7-bd60-7207006d7003} Umbraco.Web - - - $(NuGetPackageFolders.Split(';')[0]) - + + + 0.11.2 + + - - - \ No newline at end of file + diff --git a/src/Umbraco.Tests.Benchmarks/app.config b/src/Umbraco.Tests.Benchmarks/app.config index b5e577b22c..56efbc7b5f 100644 --- a/src/Umbraco.Tests.Benchmarks/app.config +++ b/src/Umbraco.Tests.Benchmarks/app.config @@ -1,298 +1,6 @@ - + - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/src/Umbraco.Tests/Cache/DefaultCachePolicyTests.cs b/src/Umbraco.Tests/Cache/DefaultCachePolicyTests.cs index a8021055a9..37488600c7 100644 --- a/src/Umbraco.Tests/Cache/DefaultCachePolicyTests.cs +++ b/src/Umbraco.Tests/Cache/DefaultCachePolicyTests.cs @@ -38,7 +38,7 @@ namespace Umbraco.Tests.Cache var defaultPolicy = new DefaultRepositoryCachePolicy(cache.Object, DefaultAccessor, new RepositoryCachePolicyOptions()); - var unused = defaultPolicy.Get(1, id => new AuditItem(1, "blah", AuditType.Copy, 123), o => null); + var unused = defaultPolicy.Get(1, id => new AuditItem(1, AuditType.Copy, 123, "test", "blah"), o => null); Assert.IsTrue(isCached); } @@ -46,7 +46,7 @@ namespace Umbraco.Tests.Cache public void Get_Single_From_Cache() { var cache = new Mock(); - cache.Setup(x => x.GetCacheItem(It.IsAny())).Returns(new AuditItem(1, "blah", AuditType.Copy, 123)); + cache.Setup(x => x.GetCacheItem(It.IsAny())).Returns(new AuditItem(1, AuditType.Copy, 123, "test", "blah")); var defaultPolicy = new DefaultRepositoryCachePolicy(cache.Object, DefaultAccessor, new RepositoryCachePolicyOptions()); @@ -71,8 +71,8 @@ namespace Umbraco.Tests.Cache var unused = defaultPolicy.GetAll(new object[] {}, ids => new[] { - new AuditItem(1, "blah", AuditType.Copy, 123), - new AuditItem(2, "blah2", AuditType.Copy, 123) + new AuditItem(1, AuditType.Copy, 123, "test", "blah"), + new AuditItem(2, AuditType.Copy, 123, "test", "blah2") }); Assert.AreEqual(2, cached.Count); @@ -84,8 +84,8 @@ namespace Umbraco.Tests.Cache var cache = new Mock(); cache.Setup(x => x.GetCacheItemsByKeySearch(It.IsAny())).Returns(new[] { - new AuditItem(1, "blah", AuditType.Copy, 123), - new AuditItem(2, "blah2", AuditType.Copy, 123) + new AuditItem(1, AuditType.Copy, 123, "test", "blah"), + new AuditItem(2, AuditType.Copy, 123, "test", "blah2") }); var defaultPolicy = new DefaultRepositoryCachePolicy(cache.Object, DefaultAccessor, new RepositoryCachePolicyOptions()); @@ -108,7 +108,7 @@ namespace Umbraco.Tests.Cache var defaultPolicy = new DefaultRepositoryCachePolicy(cache.Object, DefaultAccessor, new RepositoryCachePolicyOptions()); try { - defaultPolicy.Update(new AuditItem(1, "blah", AuditType.Copy, 123), item => throw new Exception("blah!")); + defaultPolicy.Update(new AuditItem(1, AuditType.Copy, 123, "test", "blah"), item => throw new Exception("blah!")); } catch { @@ -134,7 +134,7 @@ namespace Umbraco.Tests.Cache var defaultPolicy = new DefaultRepositoryCachePolicy(cache.Object, DefaultAccessor, new RepositoryCachePolicyOptions()); try { - defaultPolicy.Delete(new AuditItem(1, "blah", AuditType.Copy, 123), item => throw new Exception("blah!")); + defaultPolicy.Delete(new AuditItem(1, AuditType.Copy, 123, "test", "blah"), item => throw new Exception("blah!")); } catch { diff --git a/src/Umbraco.Tests/Cache/FullDataSetCachePolicyTests.cs b/src/Umbraco.Tests/Cache/FullDataSetCachePolicyTests.cs index a275a44964..404587bcfa 100644 --- a/src/Umbraco.Tests/Cache/FullDataSetCachePolicyTests.cs +++ b/src/Umbraco.Tests/Cache/FullDataSetCachePolicyTests.cs @@ -32,8 +32,8 @@ namespace Umbraco.Tests.Cache { var getAll = new[] { - new AuditItem(1, "blah", AuditType.Copy, 123), - new AuditItem(2, "blah2", AuditType.Copy, 123) + new AuditItem(1, AuditType.Copy, 123, "test", "blah"), + new AuditItem(2, AuditType.Copy, 123, "test", "blah2") }; var isCached = false; @@ -47,7 +47,7 @@ namespace Umbraco.Tests.Cache var policy = new FullDataSetRepositoryCachePolicy(cache.Object, DefaultAccessor, item => item.Id, false); - var unused = policy.Get(1, id => new AuditItem(1, "blah", AuditType.Copy, 123), ids => getAll); + var unused = policy.Get(1, id => new AuditItem(1, AuditType.Copy, 123, "test", "blah"), ids => getAll); Assert.IsTrue(isCached); } @@ -56,12 +56,12 @@ namespace Umbraco.Tests.Cache { var getAll = new[] { - new AuditItem(1, "blah", AuditType.Copy, 123), - new AuditItem(2, "blah2", AuditType.Copy, 123) + new AuditItem(1, AuditType.Copy, 123, "test", "blah"), + new AuditItem(2, AuditType.Copy, 123, "test", "blah2") }; var cache = new Mock(); - cache.Setup(x => x.GetCacheItem(It.IsAny())).Returns(new AuditItem(1, "blah", AuditType.Copy, 123)); + cache.Setup(x => x.GetCacheItem(It.IsAny())).Returns(new AuditItem(1, AuditType.Copy, 123, "test", "blah")); var defaultPolicy = new FullDataSetRepositoryCachePolicy(cache.Object, DefaultAccessor, item => item.Id, false); @@ -114,8 +114,8 @@ namespace Umbraco.Tests.Cache { var getAll = new[] { - new AuditItem(1, "blah", AuditType.Copy, 123), - new AuditItem(2, "blah2", AuditType.Copy, 123) + new AuditItem(1, AuditType.Copy, 123, "test", "blah"), + new AuditItem(2, AuditType.Copy, 123, "test", "blah2") }; var cached = new List(); @@ -149,8 +149,8 @@ namespace Umbraco.Tests.Cache cache.Setup(x => x.GetCacheItem(It.IsAny())).Returns(() => new DeepCloneableList(ListCloneBehavior.CloneOnce) { - new AuditItem(1, "blah", AuditType.Copy, 123), - new AuditItem(2, "blah2", AuditType.Copy, 123) + new AuditItem(1, AuditType.Copy, 123, "test", "blah"), + new AuditItem(2, AuditType.Copy, 123, "test", "blah2") }); var defaultPolicy = new FullDataSetRepositoryCachePolicy(cache.Object, DefaultAccessor, item => item.Id, false); @@ -164,8 +164,8 @@ namespace Umbraco.Tests.Cache { var getAll = new[] { - new AuditItem(1, "blah", AuditType.Copy, 123), - new AuditItem(2, "blah2", AuditType.Copy, 123) + new AuditItem(1, AuditType.Copy, 123, "test", "blah"), + new AuditItem(2, AuditType.Copy, 123, "test", "blah2") }; var cacheCleared = false; @@ -179,7 +179,7 @@ namespace Umbraco.Tests.Cache var defaultPolicy = new FullDataSetRepositoryCachePolicy(cache.Object, DefaultAccessor, item => item.Id, false); try { - defaultPolicy.Update(new AuditItem(1, "blah", AuditType.Copy, 123), item => { throw new Exception("blah!"); }); + defaultPolicy.Update(new AuditItem(1, AuditType.Copy, 123, "test", "blah"), item => { throw new Exception("blah!"); }); } catch { @@ -196,8 +196,8 @@ namespace Umbraco.Tests.Cache { var getAll = new[] { - new AuditItem(1, "blah", AuditType.Copy, 123), - new AuditItem(2, "blah2", AuditType.Copy, 123) + new AuditItem(1, AuditType.Copy, 123, "test", "blah"), + new AuditItem(2, AuditType.Copy, 123, "test", "blah2") }; var cacheCleared = false; @@ -211,7 +211,7 @@ namespace Umbraco.Tests.Cache var defaultPolicy = new FullDataSetRepositoryCachePolicy(cache.Object, DefaultAccessor, item => item.Id, false); try { - defaultPolicy.Delete(new AuditItem(1, "blah", AuditType.Copy, 123), item => { throw new Exception("blah!"); }); + defaultPolicy.Delete(new AuditItem(1, AuditType.Copy, 123, "test", "blah"), item => { throw new Exception("blah!"); }); } catch { diff --git a/src/Umbraco.Tests/Cache/SingleItemsOnlyCachePolicyTests.cs b/src/Umbraco.Tests/Cache/SingleItemsOnlyCachePolicyTests.cs index 9ab98bda7e..1c2227f79b 100644 --- a/src/Umbraco.Tests/Cache/SingleItemsOnlyCachePolicyTests.cs +++ b/src/Umbraco.Tests/Cache/SingleItemsOnlyCachePolicyTests.cs @@ -41,8 +41,8 @@ namespace Umbraco.Tests.Cache var unused = defaultPolicy.GetAll(new object[] { }, ids => new[] { - new AuditItem(1, "blah", AuditType.Copy, 123), - new AuditItem(2, "blah2", AuditType.Copy, 123) + new AuditItem(1, AuditType.Copy, 123, "test", "blah"), + new AuditItem(2, AuditType.Copy, 123, "test", "blah2") }); Assert.AreEqual(0, cached.Count); @@ -62,7 +62,7 @@ namespace Umbraco.Tests.Cache var defaultPolicy = new SingleItemsOnlyRepositoryCachePolicy(cache.Object, DefaultAccessor, new RepositoryCachePolicyOptions()); - var unused = defaultPolicy.Get(1, id => new AuditItem(1, "blah", AuditType.Copy, 123), ids => null); + var unused = defaultPolicy.Get(1, id => new AuditItem(1, AuditType.Copy, 123, "test", "blah"), ids => null); Assert.IsTrue(isCached); } } diff --git a/src/Umbraco.Tests/Clr/ReflectionUtilitiesTests.cs b/src/Umbraco.Tests/Clr/ReflectionUtilitiesTests.cs index 1f7f164b21..46dae8bcfd 100644 --- a/src/Umbraco.Tests/Clr/ReflectionUtilitiesTests.cs +++ b/src/Umbraco.Tests/Clr/ReflectionUtilitiesTests.cs @@ -13,16 +13,16 @@ namespace Umbraco.Tests.Clr [Test] public void EmitCtorEmits() { - var ctor1 = ReflectionUtilities.EmitCtor>(); + var ctor1 = ReflectionUtilities.EmitConstuctor>(); Assert.IsInstanceOf(ctor1()); - var ctor2 = ReflectionUtilities.EmitCtor>(declaring: typeof(Class1)); + var ctor2 = ReflectionUtilities.EmitConstuctor>(declaring: typeof(Class1)); Assert.IsInstanceOf(ctor2()); - var ctor3 = ReflectionUtilities.EmitCtor>(); + var ctor3 = ReflectionUtilities.EmitConstuctor>(); Assert.IsInstanceOf(ctor3(42)); - var ctor4 = ReflectionUtilities.EmitCtor>(declaring: typeof(Class3)); + var ctor4 = ReflectionUtilities.EmitConstuctor>(declaring: typeof(Class3)); Assert.IsInstanceOf(ctor4(42)); } @@ -30,40 +30,40 @@ namespace Umbraco.Tests.Clr public void EmitCtorEmitsFromInfo() { var ctorInfo = typeof(Class1).GetConstructor(BindingFlags.Public | BindingFlags.Instance, null, CallingConventions.Any, Array.Empty(), null); - var ctor1 = ReflectionUtilities.EmitCtor>(ctorInfo); + var ctor1 = ReflectionUtilities.EmitConstructor>(ctorInfo); Assert.IsInstanceOf(ctor1()); ctorInfo = typeof(Class1).GetConstructor(BindingFlags.Public | BindingFlags.Instance, null, CallingConventions.Any, new[] { typeof(int) }, null); - var ctor3 = ReflectionUtilities.EmitCtor>(ctorInfo); + var ctor3 = ReflectionUtilities.EmitConstructor>(ctorInfo); Assert.IsInstanceOf(ctor3(42)); - Assert.Throws(() => ReflectionUtilities.EmitCtor>(ctorInfo)); + Assert.Throws(() => ReflectionUtilities.EmitConstructor>(ctorInfo)); } [Test] public void EmitCtorEmitsPrivateCtor() { - var ctor = ReflectionUtilities.EmitCtor>(); + var ctor = ReflectionUtilities.EmitConstuctor>(); Assert.IsInstanceOf(ctor("foo")); } [Test] public void EmitCtorThrowsIfNotFound() { - Assert.Throws(() => ReflectionUtilities.EmitCtor>()); + Assert.Throws(() => ReflectionUtilities.EmitConstuctor>()); } [Test] public void EmitCtorThrowsIfInvalid() { var ctorInfo = typeof(Class1).GetConstructor(BindingFlags.Public | BindingFlags.Instance, null, CallingConventions.Any, Array.Empty(), null); - Assert.Throws(() => ReflectionUtilities.EmitCtor>(ctorInfo)); + Assert.Throws(() => ReflectionUtilities.EmitConstructor>(ctorInfo)); } [Test] public void EmitCtorReturnsNull() { - Assert.IsNull(ReflectionUtilities.EmitCtor>(false)); + Assert.IsNull(ReflectionUtilities.EmitConstuctor>(false)); } [Test] diff --git a/src/Umbraco.Tests/Composing/ActionCollectionTests.cs b/src/Umbraco.Tests/Composing/ActionCollectionTests.cs deleted file mode 100644 index 04bd0a2e1e..0000000000 --- a/src/Umbraco.Tests/Composing/ActionCollectionTests.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System.Linq; -using NUnit.Framework; -using Umbraco.Web; -using Umbraco.Web.UI.Pages; -using Umbraco.Web._Legacy.Actions; - -namespace Umbraco.Tests.Composing -{ - [TestFixture] - public class ActionCollectionTests : ComposingTestBase - { - [Test] - public void ActionCollectionBuilderWorks() - { - var collectionBuilder = new ActionCollectionBuilder(); - collectionBuilder.SetProducer(() => TypeLoader.GetActions()); - - var actions = collectionBuilder.CreateCollection(); - Assert.AreEqual(2, actions.Count()); - - // order is unspecified, but both must be there - var hasAction1 = actions.ElementAt(0) is SingletonAction || actions.ElementAt(1) is SingletonAction; - var hasAction2 = actions.ElementAt(0) is NonSingletonAction || actions.ElementAt(1) is NonSingletonAction; - Assert.IsTrue(hasAction1); - Assert.IsTrue(hasAction2); - - var singletonAction = (SingletonAction) (actions.ElementAt(0) is SingletonAction ? actions.ElementAt(0) : actions.ElementAt(1)); - - // ensure it is a singleton - Assert.AreSame(SingletonAction.Instance, singletonAction); - } - - #region Test Objects - - public class SingletonAction : IAction - { - public static SingletonAction Instance { get; } = new SingletonAction(); - - public char Letter => 'I'; - - public string JsFunctionName => $"{ClientTools.Scripts.GetAppActions}.actionAssignDomain()"; - - public string JsSource => null; - - public string Alias => "assignDomain"; - - public string Icon => ".sprDomain"; - - public bool ShowInNotifier => false; - - public bool CanBePermissionAssigned => true; - } - - public class NonSingletonAction : IAction - { - public char Letter => 'Q'; - - public string JsFunctionName => $"{ClientTools.Scripts.GetAppActions}.actionAssignDomain()"; - - public string JsSource => null; - - public string Alias => "asfasdf"; - - public string Icon => ".sprDomain"; - - public bool ShowInNotifier => false; - - public bool CanBePermissionAssigned => true; - } - - #endregion - } -} diff --git a/src/Umbraco.Tests/Composing/TypeFinderTests.cs b/src/Umbraco.Tests/Composing/TypeFinderTests.cs index a8624e8871..955f6f94c8 100644 --- a/src/Umbraco.Tests/Composing/TypeFinderTests.cs +++ b/src/Umbraco.Tests/Composing/TypeFinderTests.cs @@ -90,7 +90,7 @@ namespace Umbraco.Tests.Composing Assert.AreEqual(0, typesFound.Count()); // 0 classes in _assemblies are marked with [Tree] typesFound = TypeFinder.FindClassesWithAttribute(new[] { typeof (UmbracoContext).Assembly }); - Assert.AreEqual(22, typesFound.Count()); // + classes in Umbraco.Web are marked with [Tree] + Assert.AreEqual(21, typesFound.Count()); // + classes in Umbraco.Web are marked with [Tree] } private static ProfilingLogger GetTestProfilingLogger() diff --git a/src/Umbraco.Tests/Composing/TypeLoaderTests.cs b/src/Umbraco.Tests/Composing/TypeLoaderTests.cs index d7f2e7dd53..07625db9bf 100644 --- a/src/Umbraco.Tests/Composing/TypeLoaderTests.cs +++ b/src/Umbraco.Tests/Composing/TypeLoaderTests.cs @@ -268,25 +268,11 @@ AnotherContentFinder Assert.AreEqual(2, foundTypes1.Count()); } - [Test] - public void Resolves_Actions() - { - var actions = _typeLoader.GetActions(); - Assert.AreEqual(34, actions.Count()); - } - - [Test] - public void Resolves_Trees() - { - var trees = _typeLoader.GetTrees(); - Assert.AreEqual(1, trees.Count()); - } - [Test] public void GetDataEditors() { var types = _typeLoader.GetDataEditors(); - Assert.AreEqual(43, types.Count()); + Assert.AreEqual(39, types.Count()); } /// diff --git a/src/Umbraco.Tests/IO/IoHelperTests.cs b/src/Umbraco.Tests/IO/IoHelperTests.cs index 07436eff1a..b2ef5e4d31 100644 --- a/src/Umbraco.Tests/IO/IoHelperTests.cs +++ b/src/Umbraco.Tests/IO/IoHelperTests.cs @@ -43,8 +43,7 @@ namespace Umbraco.Tests.IO Assert.AreEqual(IOHelper.MapPath(SystemDirectories.Preview, true), IOHelper.MapPath(SystemDirectories.Preview, false)); Assert.AreEqual(IOHelper.MapPath(SystemDirectories.Root, true), IOHelper.MapPath(SystemDirectories.Root, false)); Assert.AreEqual(IOHelper.MapPath(SystemDirectories.Scripts, true), IOHelper.MapPath(SystemDirectories.Scripts, false)); - Assert.AreEqual(IOHelper.MapPath(SystemDirectories.Umbraco, true), IOHelper.MapPath(SystemDirectories.Umbraco, false)); - Assert.AreEqual(IOHelper.MapPath(SystemDirectories.UmbracoClient, true), IOHelper.MapPath(SystemDirectories.UmbracoClient, false)); + Assert.AreEqual(IOHelper.MapPath(SystemDirectories.Umbraco, true), IOHelper.MapPath(SystemDirectories.Umbraco, false)); Assert.AreEqual(IOHelper.MapPath(SystemDirectories.UserControls, true), IOHelper.MapPath(SystemDirectories.UserControls, false)); Assert.AreEqual(IOHelper.MapPath(SystemDirectories.WebServices, true), IOHelper.MapPath(SystemDirectories.WebServices, false)); } diff --git a/src/Umbraco.Tests/Integration/ContentEventsTests.cs b/src/Umbraco.Tests/Integration/ContentEventsTests.cs index 4ca63e9e96..af188c6a09 100644 --- a/src/Umbraco.Tests/Integration/ContentEventsTests.cs +++ b/src/Umbraco.Tests/Integration/ContentEventsTests.cs @@ -459,7 +459,7 @@ namespace Umbraco.Tests.Integration #region Utils private IEnumerable Children(IContent content) - => ServiceContext.ContentService.GetChildren(content.Id); + => ServiceContext.ContentService.GetPagedChildren(content.Id, 0, int.MaxValue, out var total); #endregion @@ -846,22 +846,18 @@ namespace Umbraco.Tests.Integration // force:true => all nodes are republished, refreshing all nodes - but only with changes - published w/out changes are not repub Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content1.Id}.u+p", _events[i++].ToString()); - //Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content1C[0].Id}.p+p", _events[i++].ToString()); - Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content1C[1].Id}.u+p", _events[i++].ToString()); - Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content1C[2].Id}.p+p", _events[i++].ToString()); - Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content1C[3].Id}.u+p", _events[i++].ToString()); - - // remember: ordered by level, sortOrder //Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content2C[0].Id}.p+p", _events[i++].ToString()); - Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content3C[0].Id}.u+p", _events[i++].ToString()); - //Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content4C[0].Id}.p+p", _events[i++].ToString()); - //Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content5C[0].Id}.p+p", _events[i++].ToString()); Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content2C[1].Id}.u+p", _events[i++].ToString()); + Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content1C[1].Id}.u+p", _events[i++].ToString()); + Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content3C[0].Id}.u+p", _events[i++].ToString()); Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content3C[1].Id}.u+p", _events[i++].ToString()); + Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content1C[2].Id}.p+p", _events[i++].ToString()); + //Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content4C[0].Id}.p+p", _events[i++].ToString()); Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content4C[1].Id}.u+p", _events[i++].ToString()); + Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content1C[3].Id}.u+p", _events[i++].ToString()); + //Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content5C[0].Id}.p+p", _events[i++].ToString()); Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content5C[1].Id}.u+p", _events[i++].ToString()); - Assert.AreEqual($"{m:000}: ContentCacheRefresher/RefreshBranch/{content1.Id}", _events[i++].ToString()); // repub content1 } @@ -1073,17 +1069,18 @@ namespace Umbraco.Tests.Integration Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content1.Id}.p=m", _events[i++].ToString()); Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content1C[0].Id}.p=m", _events[i++].ToString()); - Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content1C[1].Id}.u=u", _events[i++].ToString()); - Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content1C[2].Id}.p=m", _events[i++].ToString()); - Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content1C[3].Id}.u=u", _events[i++].ToString()); Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content2C[0].Id}.p=m", _events[i++].ToString()); - Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content3C[0].Id}.u=u", _events[i++].ToString()); - Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content4C[0].Id}.p=m", _events[i++].ToString()); - Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content5C[0].Id}.p=m", _events[i++].ToString()); Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content2C[1].Id}.u=u", _events[i++].ToString()); + Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content1C[1].Id}.u=u", _events[i++].ToString()); + Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content3C[0].Id}.u=u", _events[i++].ToString()); Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content3C[1].Id}.u=u", _events[i++].ToString()); + Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content1C[2].Id}.p=m", _events[i++].ToString()); + Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content4C[0].Id}.p=m", _events[i++].ToString()); Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content4C[1].Id}.u=u", _events[i++].ToString()); + Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content1C[3].Id}.u=u", _events[i++].ToString()); + Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content5C[0].Id}.p=m", _events[i++].ToString()); Assert.AreEqual($"{m:000}: ContentRepository/Refresh/{content5C[1].Id}.u=u", _events[i++].ToString()); + m++; Assert.AreEqual($"{m:000}: ContentCacheRefresher/RefreshBranch/{content1.Id}", _events[i].ToString()); } @@ -1706,16 +1703,16 @@ namespace Umbraco.Tests.Integration var content5C = Children(content1C[3]).ToArray(); Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content1.Id}.p=m", _events[i++].ToString()); Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content1C[0].Id}.p=m", _events[i++].ToString()); - Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content1C[1].Id}.u=u", _events[i++].ToString()); - Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content1C[2].Id}.p=m", _events[i++].ToString()); - Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content1C[3].Id}.u=u", _events[i++].ToString()); Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content2C[0].Id}.p=m", _events[i++].ToString()); - Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content3C[0].Id}.u=u", _events[i++].ToString()); - Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content4C[0].Id}.p=m", _events[i++].ToString()); - Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content5C[0].Id}.p=m", _events[i++].ToString()); Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content2C[1].Id}.u=u", _events[i++].ToString()); + Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content1C[1].Id}.u=u", _events[i++].ToString()); + Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content3C[0].Id}.u=u", _events[i++].ToString()); Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content3C[1].Id}.u=u", _events[i++].ToString()); + Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content1C[2].Id}.p=m", _events[i++].ToString()); + Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content4C[0].Id}.p=m", _events[i++].ToString()); Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content4C[1].Id}.u=u", _events[i++].ToString()); + Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content1C[3].Id}.u=u", _events[i++].ToString()); + Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content5C[0].Id}.p=m", _events[i++].ToString()); Assert.AreEqual($"{m:000}: ContentRepository/Refresh/{content5C[1].Id}.u=u", _events[i++].ToString()); m++; Assert.AreEqual($"{m:000}: ContentCacheRefresher/RefreshBranch/{content1.Id}", _events[i++].ToString()); @@ -1759,16 +1756,16 @@ namespace Umbraco.Tests.Integration var content5C = Children(content1C[3]).ToArray(); Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content1.Id}.p=p", _events[i++].ToString()); Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content1C[0].Id}.p=p", _events[i++].ToString()); - Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content1C[1].Id}.u=u", _events[i++].ToString()); - Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content1C[2].Id}.p=p", _events[i++].ToString()); - Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content1C[3].Id}.u=u", _events[i++].ToString()); Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content2C[0].Id}.p=p", _events[i++].ToString()); - Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content3C[0].Id}.u=u", _events[i++].ToString()); - Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content4C[0].Id}.p=p", _events[i++].ToString()); - Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content5C[0].Id}.p=m", _events[i++].ToString()); Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content2C[1].Id}.u=u", _events[i++].ToString()); + Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content1C[1].Id}.u=u", _events[i++].ToString()); + Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content3C[0].Id}.u=u", _events[i++].ToString()); Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content3C[1].Id}.u=u", _events[i++].ToString()); + Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content1C[2].Id}.p=p", _events[i++].ToString()); + Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content4C[0].Id}.p=p", _events[i++].ToString()); Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content4C[1].Id}.u=u", _events[i++].ToString()); + Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content1C[3].Id}.u=u", _events[i++].ToString()); + Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content5C[0].Id}.p=m", _events[i++].ToString()); Assert.AreEqual($"{m:000}: ContentRepository/Refresh/{content5C[1].Id}.u=u", _events[i++].ToString()); m++; Assert.AreEqual($"{m:000}: ContentCacheRefresher/RefreshBranch/{content1.Id}", _events[i++].ToString()); @@ -1816,16 +1813,16 @@ namespace Umbraco.Tests.Integration var content5C = Children(content1C[3]).ToArray(); Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content1.Id}.p=m", _events[i++].ToString()); Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content1C[0].Id}.p=m", _events[i++].ToString()); - Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content1C[1].Id}.u=u", _events[i++].ToString()); - Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content1C[2].Id}.p=m", _events[i++].ToString()); - Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content1C[3].Id}.u=u", _events[i++].ToString()); Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content2C[0].Id}.p=m", _events[i++].ToString()); - Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content3C[0].Id}.u=u", _events[i++].ToString()); - Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content4C[0].Id}.p=m", _events[i++].ToString()); - Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content5C[0].Id}.p=m", _events[i++].ToString()); Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content2C[1].Id}.u=u", _events[i++].ToString()); + Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content1C[1].Id}.u=u", _events[i++].ToString()); + Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content3C[0].Id}.u=u", _events[i++].ToString()); Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content3C[1].Id}.u=u", _events[i++].ToString()); + Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content1C[2].Id}.p=m", _events[i++].ToString()); + Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content4C[0].Id}.p=m", _events[i++].ToString()); Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content4C[1].Id}.u=u", _events[i++].ToString()); + Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content1C[3].Id}.u=u", _events[i++].ToString()); + Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content5C[0].Id}.p=m", _events[i++].ToString()); Assert.AreEqual($"{m:000}: ContentRepository/Refresh/{content5C[1].Id}.u=u", _events[i++].ToString()); m++; Assert.AreEqual($"{m:000}: ContentCacheRefresher/RefreshBranch/{content1.Id}", _events[i++].ToString()); @@ -1871,16 +1868,16 @@ namespace Umbraco.Tests.Integration var content5C = Children(content1C[3]).ToArray(); Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content1.Id}.p=p", _events[i++].ToString()); Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content1C[0].Id}.p=p", _events[i++].ToString()); - Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content1C[1].Id}.u=u", _events[i++].ToString()); - Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content1C[2].Id}.p=p", _events[i++].ToString()); - Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content1C[3].Id}.u=u", _events[i++].ToString()); Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content2C[0].Id}.p=p", _events[i++].ToString()); - Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content3C[0].Id}.u=u", _events[i++].ToString()); - Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content4C[0].Id}.p=p", _events[i++].ToString()); - Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content5C[0].Id}.p=m", _events[i++].ToString()); Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content2C[1].Id}.u=u", _events[i++].ToString()); + Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content1C[1].Id}.u=u", _events[i++].ToString()); + Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content3C[0].Id}.u=u", _events[i++].ToString()); Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content3C[1].Id}.u=u", _events[i++].ToString()); + Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content1C[2].Id}.p=p", _events[i++].ToString()); + Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content4C[0].Id}.p=p", _events[i++].ToString()); Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content4C[1].Id}.u=u", _events[i++].ToString()); + Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content1C[3].Id}.u=u", _events[i++].ToString()); + Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content5C[0].Id}.p=m", _events[i++].ToString()); Assert.AreEqual($"{m:000}: ContentRepository/Refresh/{content5C[1].Id}.u=u", _events[i++].ToString()); m++; Assert.AreEqual($"{m:000}: ContentCacheRefresher/RefreshBranch/{content1.Id}", _events[i++].ToString()); @@ -1925,16 +1922,16 @@ namespace Umbraco.Tests.Integration var content5C = Children(content1C[3]).ToArray(); Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content1.Id}.p=p", _events[i++].ToString()); Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content1C[0].Id}.p=p", _events[i++].ToString()); - Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content1C[1].Id}.u=u", _events[i++].ToString()); - Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content1C[2].Id}.p=p", _events[i++].ToString()); - Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content1C[3].Id}.u=u", _events[i++].ToString()); Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content2C[0].Id}.p=p", _events[i++].ToString()); - Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content3C[0].Id}.u=u", _events[i++].ToString()); - Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content4C[0].Id}.p=p", _events[i++].ToString()); - Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content5C[0].Id}.p=m", _events[i++].ToString()); Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content2C[1].Id}.u=u", _events[i++].ToString()); + Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content1C[1].Id}.u=u", _events[i++].ToString()); + Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content3C[0].Id}.u=u", _events[i++].ToString()); Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content3C[1].Id}.u=u", _events[i++].ToString()); + Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content1C[2].Id}.p=p", _events[i++].ToString()); + Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content4C[0].Id}.p=p", _events[i++].ToString()); Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content4C[1].Id}.u=u", _events[i++].ToString()); + Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content1C[3].Id}.u=u", _events[i++].ToString()); + Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content5C[0].Id}.p=m", _events[i++].ToString()); Assert.AreEqual($"{m:000}: ContentRepository/Refresh/{content5C[1].Id}.u=u", _events[i++].ToString()); m++; Assert.AreEqual($"{m:000}: ContentCacheRefresher/RefreshBranch/{content1.Id}", _events[i++].ToString()); @@ -1984,16 +1981,16 @@ namespace Umbraco.Tests.Integration var content5C = Children(content1C[3]).ToArray(); Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content1.Id}.p=p", _events[i++].ToString()); Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content1C[0].Id}.p=p", _events[i++].ToString()); - Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content1C[1].Id}.u=u", _events[i++].ToString()); - Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content1C[2].Id}.p=p", _events[i++].ToString()); - Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content1C[3].Id}.u=u", _events[i++].ToString()); Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content2C[0].Id}.p=p", _events[i++].ToString()); - Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content3C[0].Id}.u=u", _events[i++].ToString()); - Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content4C[0].Id}.p=p", _events[i++].ToString()); - Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content5C[0].Id}.p=m", _events[i++].ToString()); Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content2C[1].Id}.u=u", _events[i++].ToString()); + Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content1C[1].Id}.u=u", _events[i++].ToString()); + Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content3C[0].Id}.u=u", _events[i++].ToString()); Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content3C[1].Id}.u=u", _events[i++].ToString()); + Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content1C[2].Id}.p=p", _events[i++].ToString()); + Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content4C[0].Id}.p=p", _events[i++].ToString()); Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content4C[1].Id}.u=u", _events[i++].ToString()); + Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content1C[3].Id}.u=u", _events[i++].ToString()); + Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content5C[0].Id}.p=m", _events[i++].ToString()); Assert.AreEqual($"{m:000}: ContentRepository/Refresh/{content5C[1].Id}.u=u", _events[i++].ToString()); m++; Assert.AreEqual($"{m:000}: ContentCacheRefresher/RefreshBranch/{content1.Id}", _events[i++].ToString()); @@ -2098,16 +2095,16 @@ namespace Umbraco.Tests.Integration var m = 0; Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{copy.Id}.u=u", _events[i++].ToString()); Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{copyC[0].Id}.u=u", _events[i++].ToString()); - Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{copyC[1].Id}.u=u", _events[i++].ToString()); - Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{copyC[2].Id}.u=u", _events[i++].ToString()); - Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{copyC[3].Id}.u=u", _events[i++].ToString()); Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{copy2C[0].Id}.u=u", _events[i++].ToString()); - Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{copy3C[0].Id}.u=u", _events[i++].ToString()); - Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{copy4C[0].Id}.u=u", _events[i++].ToString()); - Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{copy5C[0].Id}.u=u", _events[i++].ToString()); Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{copy2C[1].Id}.u=u", _events[i++].ToString()); + Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{copyC[1].Id}.u=u", _events[i++].ToString()); + Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{copy3C[0].Id}.u=u", _events[i++].ToString()); Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{copy3C[1].Id}.u=u", _events[i++].ToString()); + Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{copyC[2].Id}.u=u", _events[i++].ToString()); + Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{copy4C[0].Id}.u=u", _events[i++].ToString()); Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{copy4C[1].Id}.u=u", _events[i++].ToString()); + Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{copyC[3].Id}.u=u", _events[i++].ToString()); + Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{copy5C[0].Id}.u=u", _events[i++].ToString()); Assert.AreEqual($"{m:000}: ContentRepository/Refresh/{copy5C[1].Id}.u=u", _events[i++].ToString()); m++; Assert.AreEqual($"{m:000}: ContentCacheRefresher/RefreshBranch/{copy.Id}", _events[i].ToString()); diff --git a/src/Umbraco.Tests/Manifest/ManifestContentAppTests.cs b/src/Umbraco.Tests/Manifest/ManifestContentAppTests.cs new file mode 100644 index 0000000000..eed0919149 --- /dev/null +++ b/src/Umbraco.Tests/Manifest/ManifestContentAppTests.cs @@ -0,0 +1,77 @@ +using System; +using System.Linq; +using Moq; +using Newtonsoft.Json; +using NUnit.Framework; +using Umbraco.Core.Manifest; +using Umbraco.Core.Models; +using Umbraco.Core.Models.Membership; + +namespace Umbraco.Tests.Manifest +{ + [TestFixture] + public class ManifestContentAppTests + { + [Test] + public void Test() + { + var contentType = Mock.Of(); + Mock.Get(contentType).Setup(x => x.Alias).Returns("type1"); + var content = Mock.Of(); + Mock.Get(content).Setup(x => x.ContentType).Returns(contentType); + + var group1 = Mock.Of(); + Mock.Get(group1).Setup(x => x.Alias).Returns("group1"); + var group2 = Mock.Of(); + Mock.Get(group2).Setup(x => x.Alias).Returns("group2"); + + // no rule = ok + AssertDefinition(content, true, Array.Empty(), new [] { group1, group2 }); + + // wildcards = ok + AssertDefinition(content, true, new [] { "+content/*" }, new [] { group1, group2 }); + AssertDefinition(content, false, new[] { "+media/*" }, new [] { group1, group2 }); + + // explicitly enabling / disabling + AssertDefinition(content, true, new[] { "+content/type1" }, new [] { group1, group2 }); + AssertDefinition(content, false, new[] { "-content/type1" }, new [] { group1, group2 }); + + // when there are type rules, failing to approve the type = no app + AssertDefinition(content, false, new[] { "+content/type2" }, new [] { group1, group2 }); + AssertDefinition(content, false, new[] { "+media/type1" }, new [] { group1, group2 }); + + // can have multiple rule, first one that matches = end + AssertDefinition(content, false, new[] { "-content/type1", "+content/*" }, new [] { group1, group2 }); + AssertDefinition(content, true, new[] { "-content/type2", "+content/*" }, new [] { group1, group2 }); + AssertDefinition(content, true, new[] { "+content/*", "-content/type1" }, new [] { group1, group2 }); + + // when there are role rules, failing to approve a role = no app + AssertDefinition(content, false, new[] { "+role/group33" }, new [] { group1, group2 }); + + // wildcards = ok + AssertDefinition(content, true, new[] { "+role/*" }, new [] { group1, group2 }); + + // explicitly enabling / disabling + AssertDefinition(content, true, new[] { "+role/group1" }, new [] { group1, group2 }); + AssertDefinition(content, false, new[] { "-role/group1" }, new [] { group1, group2 }); + + // can have multiple rule, first one that matches = end + AssertDefinition(content, true, new[] { "+role/group1", "-role/group2" }, new [] { group1, group2 }); + + // mixed type and role rules, both are evaluated and need to match + AssertDefinition(content, true, new[] { "+role/group1", "+content/type1" }, new [] { group1, group2 }); + AssertDefinition(content, false, new[] { "+role/group1", "+content/type2" }, new [] { group1, group2 }); + AssertDefinition(content, false, new[] { "+role/group33", "+content/type1" }, new [] { group1, group2 }); + } + + private void AssertDefinition(object source, bool expected, string[] show, IReadOnlyUserGroup[] groups) + { + var definition = JsonConvert.DeserializeObject("{" + (show.Length == 0 ? "" : " \"show\": [" + string.Join(",", show.Select(x => "\"" + x + "\"")) + "] ") + "}"); + var app = definition.GetContentAppFor(source, groups); + if (expected) + Assert.IsNotNull(app); + else + Assert.IsNull(app); + } + } +} diff --git a/src/Umbraco.Tests/Migrations/MigrationTests.cs b/src/Umbraco.Tests/Migrations/MigrationTests.cs index b408d351d7..d06cf2244e 100644 --- a/src/Umbraco.Tests/Migrations/MigrationTests.cs +++ b/src/Umbraco.Tests/Migrations/MigrationTests.cs @@ -1,16 +1,19 @@ using System; using System.Data; +using Moq; +using NUnit.Framework; using Semver; using Umbraco.Core.Events; -using Umbraco.Core.Logging; using Umbraco.Core.Migrations; using Umbraco.Core.Migrations.Upgrade; using Umbraco.Core.Persistence; using Umbraco.Core.Scoping; using Umbraco.Core.Services; +using ILogger = Umbraco.Core.Logging.ILogger; namespace Umbraco.Tests.Migrations { + [TestFixture] public class MigrationTests { public class TestUpgrader : Upgrader @@ -67,5 +70,68 @@ namespace Umbraco.Tests.Migrations public IScopeContext Context { get; set; } public ISqlContext SqlContext { get; set; } } + + [Test] + public void RunGoodMigration() + { + var migrationContext = new MigrationContext(Mock.Of(), Mock.Of()); + IMigration migration = new GoodMigration(migrationContext); + migration.Migrate(); + } + + [Test] + public void DetectBadMigration1() + { + var migrationContext = new MigrationContext(Mock.Of(), Mock.Of()); + IMigration migration = new BadMigration1(migrationContext); + Assert.Throws(() => migration.Migrate()); + } + + [Test] + public void DetectBadMigration2() + { + var migrationContext = new MigrationContext(Mock.Of(), Mock.Of()); + IMigration migration = new BadMigration2(migrationContext); + Assert.Throws(() => migration.Migrate()); + } + + public class GoodMigration : MigrationBase + { + public GoodMigration(IMigrationContext context) + : base(context) + { } + + public override void Migrate() + { + Execute.Sql("").Do(); + } + } + + public class BadMigration1 : MigrationBase + { + public BadMigration1(IMigrationContext context) + : base(context) + { } + + public override void Migrate() + { + Alter.Table("foo"); // stop here, don't Do it + } + } + + public class BadMigration2 : MigrationBase + { + public BadMigration2(IMigrationContext context) + : base(context) + { } + + public override void Migrate() + { + Alter.Table("foo"); // stop here, don't Do it + + // and try to start another one + Alter.Table("bar"); + } + } } } diff --git a/src/Umbraco.Tests/Models/ContentScheduleTests.cs b/src/Umbraco.Tests/Models/ContentScheduleTests.cs new file mode 100644 index 0000000000..5d03da5bae --- /dev/null +++ b/src/Umbraco.Tests/Models/ContentScheduleTests.cs @@ -0,0 +1,107 @@ +using System; +using System.Linq; +using NUnit.Framework; +using Umbraco.Core.Models; + +namespace Umbraco.Tests.Models +{ + [TestFixture] + public class ContentScheduleTests + { + [Test] + public void Release_Date_Less_Than_Expire_Date() + { + var now = DateTime.Now; + var schedule = new ContentScheduleCollection(); + Assert.IsFalse(schedule.Add(now, now)); + } + + [Test] + public void Cannot_Add_Duplicate_Dates_Invariant() + { + var now = DateTime.Now; + var schedule = new ContentScheduleCollection(); + schedule.Add(now, null); + Assert.Throws(() => schedule.Add(null, now)); + } + + [Test] + public void Cannot_Add_Duplicate_Dates_Variant() + { + var now = DateTime.Now; + var schedule = new ContentScheduleCollection(); + schedule.Add(now, null); + schedule.Add("en-US", now, null); + Assert.Throws(() => schedule.Add("en-US", null, now)); + Assert.Throws(() => schedule.Add(null, now)); + } + + [Test] + public void Can_Remove_Invariant() + { + var now = DateTime.Now; + var schedule = new ContentScheduleCollection(); + schedule.Add(now, null); + var invariantSched = schedule.GetSchedule(string.Empty); + schedule.Remove(invariantSched.First()); + Assert.AreEqual(0, schedule.FullSchedule.Count()); + } + + [Test] + public void Can_Remove_Variant() + { + var now = DateTime.Now; + var schedule = new ContentScheduleCollection(); + schedule.Add(now, null); + schedule.Add("en-US", now, null); + var invariantSched = schedule.GetSchedule(string.Empty); + schedule.Remove(invariantSched.First()); + Assert.AreEqual(0, schedule.GetSchedule(string.Empty).Count()); + Assert.AreEqual(1, schedule.FullSchedule.Count()); + var variantSched = schedule.GetSchedule("en-US"); + schedule.Remove(variantSched.First()); + Assert.AreEqual(0, schedule.GetSchedule("en-US").Count()); + Assert.AreEqual(0, schedule.FullSchedule.Count()); + } + + [Test] + public void Can_Clear_Start_Invariant() + { + var now = DateTime.Now; + var schedule = new ContentScheduleCollection(); + schedule.Add(now, now.AddDays(1)); + + schedule.Clear(ContentScheduleAction.Release); + + Assert.AreEqual(0, schedule.GetSchedule(ContentScheduleAction.Release).Count()); + Assert.AreEqual(1, schedule.GetSchedule(ContentScheduleAction.Expire).Count()); + Assert.AreEqual(1, schedule.FullSchedule.Count()); + } + + [Test] + public void Can_Clear_End_Variant() + { + var now = DateTime.Now; + var schedule = new ContentScheduleCollection(); + schedule.Add(now, now.AddDays(1)); + schedule.Add("en-US", now, now.AddDays(1)); + + schedule.Clear(ContentScheduleAction.Expire); + + Assert.AreEqual(0, schedule.GetSchedule(ContentScheduleAction.Expire).Count()); + Assert.AreEqual(1, schedule.GetSchedule(ContentScheduleAction.Release).Count()); + Assert.AreEqual(1, schedule.GetSchedule("en-US", ContentScheduleAction.Expire).Count()); + Assert.AreEqual(1, schedule.GetSchedule("en-US", ContentScheduleAction.Release).Count()); + Assert.AreEqual(3, schedule.FullSchedule.Count()); + + schedule.Clear("en-US", ContentScheduleAction.Expire); + + Assert.AreEqual(0, schedule.GetSchedule(ContentScheduleAction.Expire).Count()); + Assert.AreEqual(1, schedule.GetSchedule(ContentScheduleAction.Release).Count()); + Assert.AreEqual(0, schedule.GetSchedule("en-US", ContentScheduleAction.Expire).Count()); + Assert.AreEqual(1, schedule.GetSchedule("en-US", ContentScheduleAction.Release).Count()); + Assert.AreEqual(2, schedule.FullSchedule.Count()); + } + + } +} diff --git a/src/Umbraco.Tests/Models/ContentTests.cs b/src/Umbraco.Tests/Models/ContentTests.cs index 32fbd37d0e..3f9c6d1cd5 100644 --- a/src/Umbraco.Tests/Models/ContentTests.cs +++ b/src/Umbraco.Tests/Models/ContentTests.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.Linq; +using System.Threading; using Moq; using NUnit.Framework; using Umbraco.Core; @@ -21,6 +22,7 @@ using Umbraco.Tests.Testing; namespace Umbraco.Tests.Models { + [TestFixture] public class ContentTests : UmbracoTestBase { @@ -42,6 +44,66 @@ namespace Umbraco.Tests.Models Container.Register(_ => Mock.Of()); } + [Test] + public void Variant_Culture_Names_Track_Dirty_Changes() + { + var contentType = new ContentType(-1) { Alias = "contentType" }; + var content = new Content("content", -1, contentType) { Id = 1, VersionId = 1 }; + + const string langFr = "fr-FR"; + + contentType.Variations = ContentVariation.Culture; + + Assert.IsFalse(content.IsPropertyDirty("CultureInfos")); //hasn't been changed + + Thread.Sleep(500); //The "Date" wont be dirty if the test runs too fast since it will be the same date + content.SetCultureName("name-fr", langFr); + Assert.IsTrue(content.IsPropertyDirty("CultureInfos")); //now it will be changed since the collection has changed + var frCultureName = content.CultureInfos[langFr]; + Assert.IsTrue(frCultureName.IsPropertyDirty("Date")); + + content.ResetDirtyProperties(); + + Assert.IsFalse(content.IsPropertyDirty("CultureInfos")); //it's been reset + Assert.IsTrue(content.WasPropertyDirty("CultureInfos")); + + Thread.Sleep(500); //The "Date" wont be dirty if the test runs too fast since it will be the same date + content.SetCultureName("name-fr", langFr); + Assert.IsTrue(frCultureName.IsPropertyDirty("Date")); + Assert.IsTrue(content.IsPropertyDirty("CultureInfos")); //it's true now since we've updated a name + } + + [Test] + public void Variant_Published_Culture_Names_Track_Dirty_Changes() + { + var contentType = new ContentType(-1) { Alias = "contentType" }; + var content = new Content("content", -1, contentType) { Id = 1, VersionId = 1 }; + + const string langFr = "fr-FR"; + + contentType.Variations = ContentVariation.Culture; + + Assert.IsFalse(content.IsPropertyDirty("PublishCultureInfos")); //hasn't been changed + + Thread.Sleep(500); //The "Date" wont be dirty if the test runs too fast since it will be the same date + content.SetCultureName("name-fr", langFr); + content.PublishCulture(langFr); //we've set the name, now we're publishing it + Assert.IsTrue(content.IsPropertyDirty("PublishCultureInfos")); //now it will be changed since the collection has changed + var frCultureName = content.PublishCultureInfos[langFr]; + Assert.IsTrue(frCultureName.IsPropertyDirty("Date")); + + content.ResetDirtyProperties(); + + Assert.IsFalse(content.IsPropertyDirty("PublishCultureInfos")); //it's been reset + Assert.IsTrue(content.WasPropertyDirty("PublishCultureInfos")); + + Thread.Sleep(500); //The "Date" wont be dirty if the test runs too fast since it will be the same date + content.SetCultureName("name-fr", langFr); + content.PublishCulture(langFr); //we've set the name, now we're publishing it + Assert.IsTrue(frCultureName.IsPropertyDirty("Date")); + Assert.IsTrue(content.IsPropertyDirty("PublishCultureInfos")); //it's true now since we've updated a name + } + [Test] public void Get_Non_Grouped_Properties() { @@ -167,11 +229,10 @@ namespace Umbraco.Tests.Models content.Id = 10; content.CreateDate = DateTime.Now; content.CreatorId = 22; - content.ExpireDate = DateTime.Now; content.Key = Guid.NewGuid(); content.Level = 3; content.Path = "-1,4,10"; - content.ReleaseDate = DateTime.Now; + content.ContentSchedule.Add(DateTime.Now, DateTime.Now.AddDays(1)); //content.ChangePublishedState(PublishedState.Published); content.SortOrder = 5; content.Template = new Template((string) "Test Template", (string) "testTemplate") @@ -210,8 +271,13 @@ namespace Umbraco.Tests.Models // Arrange var contentType = MockedContentTypes.CreateTextpageContentType(); contentType.Id = 99; + contentType.Variations = ContentVariation.Culture; var content = MockedContent.CreateTextpageContent(contentType, "Textpage", -1); + content.SetCultureName("Hello", "en-US"); + content.SetCultureName("World", "es-ES"); + content.PublishCulture("en-US"); + // should not try to clone something that's not Published or Unpublished // (and in fact it will not work) // but we cannot directly set the state to Published - hence this trick @@ -226,11 +292,10 @@ namespace Umbraco.Tests.Models content.Id = 10; content.CreateDate = DateTime.Now; content.CreatorId = 22; - content.ExpireDate = DateTime.Now; content.Key = Guid.NewGuid(); content.Level = 3; content.Path = "-1,4,10"; - content.ReleaseDate = DateTime.Now; + content.ContentSchedule.Add(DateTime.Now, DateTime.Now.AddDays(1)); content.SortOrder = 5; content.Template = new Template((string) "Test Template", (string) "testTemplate") { @@ -240,6 +305,8 @@ namespace Umbraco.Tests.Models content.UpdateDate = DateTime.Now; content.WriterId = 23; + + // Act var clone = (Content)content.DeepClone(); @@ -265,11 +332,10 @@ namespace Umbraco.Tests.Models Assert.AreEqual(clone.ContentTypeId, content.ContentTypeId); Assert.AreEqual(clone.CreateDate, content.CreateDate); Assert.AreEqual(clone.CreatorId, content.CreatorId); - Assert.AreEqual(clone.ExpireDate, content.ExpireDate); Assert.AreEqual(clone.Key, content.Key); Assert.AreEqual(clone.Level, content.Level); Assert.AreEqual(clone.Path, content.Path); - Assert.AreEqual(clone.ReleaseDate, content.ReleaseDate); + Assert.IsTrue(clone.ContentSchedule.Equals(content.ContentSchedule)); Assert.AreEqual(clone.Published, content.Published); Assert.AreEqual(clone.PublishedState, content.PublishedState); Assert.AreEqual(clone.SortOrder, content.SortOrder); @@ -288,6 +354,22 @@ namespace Umbraco.Tests.Models Assert.AreEqual(clone.Properties[index], content.Properties[index]); } + Assert.AreNotSame(clone.PublishCultureInfos, content.PublishCultureInfos); + Assert.AreEqual(clone.PublishCultureInfos.Count, content.PublishCultureInfos.Count); + foreach (var key in content.PublishCultureInfos.Keys) + { + Assert.AreNotSame(clone.PublishCultureInfos[key], content.PublishCultureInfos[key]); + Assert.AreEqual(clone.PublishCultureInfos[key], content.PublishCultureInfos[key]); + } + + Assert.AreNotSame(clone.CultureInfos, content.CultureInfos); + Assert.AreEqual(clone.CultureInfos.Count, content.CultureInfos.Count); + foreach (var key in content.CultureInfos.Keys) + { + Assert.AreNotSame(clone.CultureInfos[key], content.CultureInfos[key]); + Assert.AreEqual(clone.CultureInfos[key], content.CultureInfos[key]); + } + //This double verifies by reflection var allProps = clone.GetType().GetProperties(); foreach (var propertyInfo in allProps) @@ -308,6 +390,85 @@ namespace Umbraco.Tests.Models Assert.IsTrue(asDirty.IsPropertyDirty("Properties")); } + [Test] + public void Remember_Dirty_Properties() + { + // Arrange + var contentType = MockedContentTypes.CreateTextpageContentType(); + contentType.Id = 99; + contentType.Variations = ContentVariation.Culture; + var content = MockedContent.CreateTextpageContent(contentType, "Textpage", -1); + + content.SetCultureName("Hello", "en-US"); + content.SetCultureName("World", "es-ES"); + content.PublishCulture("en-US"); + + var i = 200; + foreach (var property in content.Properties) + { + property.Id = ++i; + } + content.Id = 10; + content.CreateDate = DateTime.Now; + content.CreatorId = 22; + content.ContentSchedule.Add(DateTime.Now, DateTime.Now.AddDays(1)); + content.Key = Guid.NewGuid(); + content.Level = 3; + content.Path = "-1,4,10"; + content.SortOrder = 5; + content.Template = new Template((string)"Test Template", (string)"testTemplate") + { + Id = 88 + }; + + content.Trashed = true; + content.UpdateDate = DateTime.Now; + content.WriterId = 23; + + content.Template.UpdateDate = DateTime.Now; //update a child object + content.ContentType.UpdateDate = DateTime.Now; //update a child object + + // Act + content.ResetDirtyProperties(); + + // Assert + Assert.IsTrue(content.WasDirty()); + Assert.IsTrue(content.WasPropertyDirty("Id")); + Assert.IsTrue(content.WasPropertyDirty("CreateDate")); + Assert.IsTrue(content.WasPropertyDirty("CreatorId")); + Assert.IsTrue(content.WasPropertyDirty("Key")); + Assert.IsTrue(content.WasPropertyDirty("Level")); + Assert.IsTrue(content.WasPropertyDirty("Path")); + Assert.IsTrue(content.WasPropertyDirty("ContentSchedule")); + Assert.IsTrue(content.WasPropertyDirty("SortOrder")); + Assert.IsTrue(content.WasPropertyDirty("Template")); + Assert.IsTrue(content.WasPropertyDirty("Trashed")); + Assert.IsTrue(content.WasPropertyDirty("UpdateDate")); + Assert.IsTrue(content.WasPropertyDirty("WriterId")); + foreach (var prop in content.Properties) + { + Assert.IsTrue(prop.WasDirty()); + Assert.IsTrue(prop.WasPropertyDirty("Id")); + } + Assert.IsTrue(content.WasPropertyDirty("CultureInfos")); + foreach(var culture in content.CultureInfos) + { + Assert.IsTrue(culture.Value.WasDirty()); + Assert.IsTrue(culture.Value.WasPropertyDirty("Name")); + Assert.IsTrue(culture.Value.WasPropertyDirty("Date")); + } + Assert.IsTrue(content.WasPropertyDirty("PublishCultureInfos")); + foreach (var culture in content.PublishCultureInfos) + { + Assert.IsTrue(culture.Value.WasDirty()); + Assert.IsTrue(culture.Value.WasPropertyDirty("Name")); + Assert.IsTrue(culture.Value.WasPropertyDirty("Date")); + } + //verify child objects were reset too + Assert.IsTrue(content.Template.WasPropertyDirty("UpdateDate")); + Assert.IsTrue(content.ContentType.WasPropertyDirty("UpdateDate")); + } + [Test] public void Can_Serialize_Without_Error() { @@ -325,11 +486,10 @@ namespace Umbraco.Tests.Models content.Id = 10; content.CreateDate = DateTime.Now; content.CreatorId = 22; - content.ExpireDate = DateTime.Now; content.Key = Guid.NewGuid(); content.Level = 3; content.Path = "-1,4,10"; - content.ReleaseDate = DateTime.Now; + content.ContentSchedule.Add(DateTime.Now, DateTime.Now.AddDays(1)); //content.ChangePublishedState(PublishedState.Publishing); content.SortOrder = 5; content.Template = new Template((string) "Test Template", (string) "testTemplate") diff --git a/src/Umbraco.Tests/Models/StylesheetTests.cs b/src/Umbraco.Tests/Models/StylesheetTests.cs index 7ca2c0ef92..b8990f6d29 100644 --- a/src/Umbraco.Tests/Models/StylesheetTests.cs +++ b/src/Umbraco.Tests/Models/StylesheetTests.cs @@ -1,5 +1,4 @@ -using System; -using System.Diagnostics; +using System.Diagnostics; using System.Linq; using NUnit.Framework; using Umbraco.Core.Models; @@ -41,7 +40,7 @@ namespace Umbraco.Tests.Models Assert.AreEqual(1, stylesheet.Properties.Count()); Assert.AreEqual("Test", stylesheet.Properties.Single().Name); Assert.AreEqual("p", stylesheet.Properties.Single().Alias); - Assert.AreEqual("font-weight:bold; font-family:Arial;", stylesheet.Properties.Single().Value); + Assert.AreEqual("font-weight:bold;\r\nfont-family:Arial;", stylesheet.Properties.Single().Value); } [Test] @@ -76,7 +75,7 @@ namespace Umbraco.Tests.Models prop = stylesheet.Properties.Single(); Assert.AreEqual("li", prop.Alias); Assert.AreEqual("font-size:5em;", prop.Value); - Assert.AreEqual("body { color:#000; } /**umb_name:Hello*/\r\nli{font-size:5em;} .bold {font-weight:bold;}", stylesheet.Content); + Assert.AreEqual("body { color:#000; } /**umb_name:Hello*/\r\nli {\r\n\tfont-size:5em;\r\n} .bold {font-weight:bold;}", stylesheet.Content); } [Test] diff --git a/src/Umbraco.Tests/Models/VariationTests.cs b/src/Umbraco.Tests/Models/VariationTests.cs index 8d566e81f2..e6f4e53d26 100644 --- a/src/Umbraco.Tests/Models/VariationTests.cs +++ b/src/Umbraco.Tests/Models/VariationTests.cs @@ -236,11 +236,11 @@ namespace Umbraco.Tests.Models Assert.AreEqual("name-uk", content.GetCultureName(langUk)); // variant dictionary of names work - Assert.AreEqual(2, content.CultureNames.Count); - Assert.IsTrue(content.CultureNames.ContainsKey(langFr)); - Assert.AreEqual("name-fr", content.CultureNames[langFr]); - Assert.IsTrue(content.CultureNames.ContainsKey(langUk)); - Assert.AreEqual("name-uk", content.CultureNames[langUk]); + Assert.AreEqual(2, content.CultureInfos.Count); + Assert.IsTrue(content.CultureInfos.ContainsKey(langFr)); + Assert.AreEqual("name-fr", content.CultureInfos[langFr].Name); + Assert.IsTrue(content.CultureInfos.ContainsKey(langUk)); + Assert.AreEqual("name-uk", content.CultureInfos[langUk].Name); } [Test] diff --git a/src/Umbraco.Tests/Persistence/LocksTests.cs b/src/Umbraco.Tests/Persistence/LocksTests.cs index 4dfb90c8fd..56779ace0f 100644 --- a/src/Umbraco.Tests/Persistence/LocksTests.cs +++ b/src/Umbraco.Tests/Persistence/LocksTests.cs @@ -25,12 +25,9 @@ namespace Umbraco.Tests.Persistence using (var scope = ScopeProvider.CreateScope()) { var database = scope.Database; - database.Execute("SET IDENTITY_INSERT umbracoLock ON"); database.Insert("umbracoLock", "id", false, new LockDto { Id = 1, Name = "Lock.1" }); database.Insert("umbracoLock", "id", false, new LockDto { Id = 2, Name = "Lock.2" }); database.Insert("umbracoLock", "id", false, new LockDto { Id = 3, Name = "Lock.3" }); - database.Execute("SET IDENTITY_INSERT umbracoLock OFF"); - database.CompleteTransaction(); scope.Complete(); } } @@ -206,7 +203,14 @@ namespace Umbraco.Tests.Persistence Assert.IsNotNull(e1); Assert.IsInstanceOf(e1); - Assert.IsNull(e2); + // the assertion below depends on timing conditions - on a fast enough environment, + // thread1 dies (deadlock) and frees thread2, which succeeds - however on a slow + // environment (CI) both threads can end up dying due to deadlock - so, cannot test + // that e2 is null - but if it's not, can test that it's a timeout + // + //Assert.IsNull(e2); + if (e2 != null) + Assert.IsInstanceOf(e2); } private void DeadLockTestThread(int id1, int id2, EventWaitHandle myEv, WaitHandle otherEv, ref Exception exception) diff --git a/src/Umbraco.Tests/Persistence/NPocoTests/PetaPocoCachesTest.cs b/src/Umbraco.Tests/Persistence/NPocoTests/PetaPocoCachesTest.cs index 21a75b2e24..db3b02bacf 100644 --- a/src/Umbraco.Tests/Persistence/NPocoTests/PetaPocoCachesTest.cs +++ b/src/Umbraco.Tests/Persistence/NPocoTests/PetaPocoCachesTest.cs @@ -125,19 +125,13 @@ namespace Umbraco.Tests.Persistence.NPocoTests contentService.GetByLevel(2); - contentService.GetChildren(id1); - - contentService.GetDescendants(id2); - contentService.GetVersions(id3); contentService.GetRootContent(); - contentService.GetContentForExpiration(); + contentService.GetContentForExpiration(DateTime.Now); - contentService.GetContentForRelease(); - - contentService.GetContentInRecycleBin(); + contentService.GetContentForRelease(DateTime.Now); ((ContentService)contentService).GetPublishedDescendants(new Content("Test", -1, new ContentType(-1)) { diff --git a/src/Umbraco.Tests/Persistence/Querying/ContentTypeSqlMappingTests.cs b/src/Umbraco.Tests/Persistence/Querying/ContentTypeSqlMappingTests.cs index 64e8d41669..ce6447f4b5 100644 --- a/src/Umbraco.Tests/Persistence/Querying/ContentTypeSqlMappingTests.cs +++ b/src/Umbraco.Tests/Persistence/Querying/ContentTypeSqlMappingTests.cs @@ -35,8 +35,8 @@ namespace Umbraco.Tests.Persistence.Querying scope.Database.Execute(new Sql(string.Format("SET IDENTITY_INSERT {0} OFF ", SqlSyntax.GetQuotedTableName("umbracoNode")))); scope.Database.Execute(new Sql(string.Format("SET IDENTITY_INSERT {0} ON ", SqlSyntax.GetQuotedTableName("cmsTemplate")))); - scope.Database.Insert("cmsTemplate", "pk", false, new TemplateDto { NodeId = 55554, Alias = "testTemplate1", Design = "", PrimaryKey = 22221}); - scope.Database.Insert("cmsTemplate", "pk", false, new TemplateDto { NodeId = 55555, Alias = "testTemplate2", Design = "", PrimaryKey = 22222 }); + scope.Database.Insert("cmsTemplate", "pk", false, new TemplateDto { NodeId = 55554, Alias = "testTemplate1", PrimaryKey = 22221}); + scope.Database.Insert("cmsTemplate", "pk", false, new TemplateDto { NodeId = 55555, Alias = "testTemplate2", PrimaryKey = 22222 }); scope.Database.Execute(new Sql(string.Format("SET IDENTITY_INSERT {0} OFF ", SqlSyntax.GetQuotedTableName("cmsTemplate")))); scope.Database.Execute(new Sql(string.Format("SET IDENTITY_INSERT {0} ON ", SqlSyntax.GetQuotedTableName("cmsContentType")))); diff --git a/src/Umbraco.Tests/Persistence/Querying/ExpressionTests.cs b/src/Umbraco.Tests/Persistence/Querying/ExpressionTests.cs index f73831e8bc..70d70d4a31 100644 --- a/src/Umbraco.Tests/Persistence/Querying/ExpressionTests.cs +++ b/src/Umbraco.Tests/Persistence/Querying/ExpressionTests.cs @@ -52,9 +52,9 @@ namespace Umbraco.Tests.Persistence.Querying } [Test] - public void Can_Query_With_Content_Type_Aliases() + public void Can_Query_With_Content_Type_Aliases_IEnumerable() { - //Arrange + //Arrange - Contains is IEnumerable.Contains extension method var aliases = new[] { "Test1", "Test2" }; Expression> predicate = content => aliases.Contains(content.ContentType.Alias); var modelToSqlExpressionHelper = new ModelToSqlExpressionVisitor(SqlContext.SqlSyntax, Mappers); @@ -67,6 +67,22 @@ namespace Umbraco.Tests.Persistence.Querying Assert.AreEqual("Test2", modelToSqlExpressionHelper.GetSqlParameters()[2]); } + [Test] + public void Can_Query_With_Content_Type_Aliases_List() + { + //Arrange - Contains is List.Contains instance method + var aliases = new System.Collections.Generic.List { "Test1", "Test2" }; + Expression> predicate = content => aliases.Contains(content.ContentType.Alias); + var modelToSqlExpressionHelper = new ModelToSqlExpressionVisitor(SqlContext.SqlSyntax, Mappers); + var result = modelToSqlExpressionHelper.Visit(predicate); + + Debug.Print("Model to Sql ExpressionHelper: \n" + result); + + Assert.AreEqual("[cmsContentType].[alias] IN (@1,@2)", result); + Assert.AreEqual("Test1", modelToSqlExpressionHelper.GetSqlParameters()[1]); + Assert.AreEqual("Test2", modelToSqlExpressionHelper.GetSqlParameters()[2]); + } + [Test] public void CachedExpression_Can_Verify_Path_StartsWith_Predicate_In_Same_Result() { diff --git a/src/Umbraco.Tests/Persistence/Repositories/AuditRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/AuditRepositoryTest.cs index 6953634a31..eb85656ee4 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/AuditRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/AuditRepositoryTest.cs @@ -27,7 +27,7 @@ namespace Umbraco.Tests.Persistence.Repositories using (var scope = sp.CreateScope()) { var repo = new AuditRepository((IScopeAccessor) sp, CacheHelper, Logger); - repo.Save(new AuditItem(-1, "This is a System audit trail", AuditType.System, -1)); + repo.Save(new AuditItem(-1, AuditType.System, -1, ObjectTypes.GetName(UmbracoObjectTypes.Document), "This is a System audit trail")); var dtos = scope.Database.Fetch("WHERE id > -1"); @@ -46,8 +46,8 @@ namespace Umbraco.Tests.Persistence.Repositories for (var i = 0; i < 100; i++) { - repo.Save(new AuditItem(i, $"Content {i} created", AuditType.New, -1)); - repo.Save(new AuditItem(i, $"Content {i} published", AuditType.Publish, -1)); + repo.Save(new AuditItem(i, AuditType.New, -1, ObjectTypes.GetName(UmbracoObjectTypes.Document), $"Content {i} created")); + repo.Save(new AuditItem(i, AuditType.Publish, -1, ObjectTypes.GetName(UmbracoObjectTypes.Document), $"Content {i} published")); } scope.Complete(); @@ -74,8 +74,8 @@ namespace Umbraco.Tests.Persistence.Repositories for (var i = 0; i < 100; i++) { - repo.Save(new AuditItem(i, $"Content {i} created", AuditType.New, -1)); - repo.Save(new AuditItem(i, $"Content {i} published", AuditType.Publish, -1)); + repo.Save(new AuditItem(i, AuditType.New, -1, ObjectTypes.GetName(UmbracoObjectTypes.Document), $"Content {i} created")); + repo.Save(new AuditItem(i, AuditType.Publish, -1, ObjectTypes.GetName(UmbracoObjectTypes.Document), $"Content {i} published")); } scope.Complete(); @@ -117,8 +117,8 @@ namespace Umbraco.Tests.Persistence.Repositories for (var i = 0; i < 100; i++) { - repo.Save(new AuditItem(i, $"Content {i} created", AuditType.New, -1)); - repo.Save(new AuditItem(i, $"Content {i} published", AuditType.Publish, -1)); + repo.Save(new AuditItem(i, AuditType.New, -1, ObjectTypes.GetName(UmbracoObjectTypes.Document), $"Content {i} created")); + repo.Save(new AuditItem(i, AuditType.Publish, -1, ObjectTypes.GetName(UmbracoObjectTypes.Document), $"Content {i} published")); } scope.Complete(); @@ -148,8 +148,8 @@ namespace Umbraco.Tests.Persistence.Repositories for (var i = 0; i < 100; i++) { - repo.Save(new AuditItem(i, "Content created", AuditType.New, -1)); - repo.Save(new AuditItem(i, "Content published", AuditType.Publish, -1)); + repo.Save(new AuditItem(i, AuditType.New, -1, ObjectTypes.GetName(UmbracoObjectTypes.Document), "Content created")); + repo.Save(new AuditItem(i, AuditType.Publish, -1, ObjectTypes.GetName(UmbracoObjectTypes.Document), "Content published")); } scope.Complete(); diff --git a/src/Umbraco.Tests/Persistence/Repositories/DocumentRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/DocumentRepositoryTest.cs index 68e29c4efe..9f84d9faf5 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/DocumentRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/DocumentRepositoryTest.cs @@ -767,7 +767,7 @@ namespace Umbraco.Tests.Persistence.Repositories foreach (var r in result) { var isInvariant = r.ContentType.Alias == "umbInvariantTextpage"; - var name = isInvariant ? r.Name : r.CultureNames["en-US"]; + var name = isInvariant ? r.Name : r.CultureInfos["en-US"].Name; var namePrefix = isInvariant ? "INV" : "VAR"; //ensure the correct name (invariant vs variant) is in the result diff --git a/src/Umbraco.Tests/Persistence/Repositories/MacroRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/MacroRepositoryTest.cs index a3b9035c8d..5ae25d629f 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/MacroRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/MacroRepositoryTest.cs @@ -175,7 +175,7 @@ namespace Umbraco.Tests.Persistence.Repositories // Assert Assert.That(macro.HasIdentity, Is.True); Assert.That(macro.Id, Is.EqualTo(4));//With 3 existing entries the Id should be 4 - Assert.Greater(macro.Properties.Single().Id, 0); + Assert.Greater(macro.Properties.Values.Single().Id, 0); } } @@ -268,15 +268,14 @@ namespace Umbraco.Tests.Persistence.Repositories repository.Save(macro); - // Assert - Assert.Greater(macro.Properties.First().Id, 0); //ensure id is returned + Assert.Greater(macro.Properties.Values.First().Id, 0); //ensure id is returned var result = repository.Get(1); - Assert.Greater(result.Properties.First().Id, 0); - Assert.AreEqual(1, result.Properties.Count()); - Assert.AreEqual("new1", result.Properties.First().Alias); - Assert.AreEqual("New1", result.Properties.First().Name); - Assert.AreEqual(3, result.Properties.First().SortOrder); + Assert.Greater(result.Properties.Values.First().Id, 0); + Assert.AreEqual(1, result.Properties.Values.Count()); + Assert.AreEqual("new1", result.Properties.Values.First().Alias); + Assert.AreEqual("New1", result.Properties.Values.First().Name); + Assert.AreEqual(3, result.Properties.Values.First().SortOrder); } } @@ -298,10 +297,10 @@ namespace Umbraco.Tests.Persistence.Repositories // Assert var result = repository.Get(macro.Id); - Assert.AreEqual(1, result.Properties.Count()); - Assert.AreEqual("blah1", result.Properties.First().Alias); - Assert.AreEqual("New1", result.Properties.First().Name); - Assert.AreEqual(4, result.Properties.First().SortOrder); + Assert.AreEqual(1, result.Properties.Values.Count()); + Assert.AreEqual("blah1", result.Properties.Values.First().Alias); + Assert.AreEqual("New1", result.Properties.Values.First().Name); + Assert.AreEqual(4, result.Properties.Values.First().SortOrder); } } @@ -325,7 +324,7 @@ namespace Umbraco.Tests.Persistence.Repositories // Assert result = repository.Get(macro.Id); - Assert.AreEqual(0, result.Properties.Count()); + Assert.AreEqual(0, result.Properties.Values.Count()); } } @@ -355,8 +354,8 @@ namespace Umbraco.Tests.Persistence.Repositories // Assert var result = repository.Get(macro.Id); - Assert.AreEqual(1, result.Properties.Count()); - Assert.AreEqual("blah2", result.Properties.Single().Alias); + Assert.AreEqual(1, result.Properties.Values.Count()); + Assert.AreEqual("blah2", result.Properties.Values.Single().Alias); } } @@ -382,8 +381,8 @@ namespace Umbraco.Tests.Persistence.Repositories // Assert var result = repository.Get(1); - Assert.AreEqual("new1", result.Properties.First().Alias); - Assert.AreEqual("this is a new name", result.Properties.First().Name); + Assert.AreEqual("new1", result.Properties.Values.First().Alias); + Assert.AreEqual("this is a new name", result.Properties.Values.First().Name); } } @@ -408,7 +407,7 @@ namespace Umbraco.Tests.Persistence.Repositories // Assert var result = repository.Get(1); - Assert.AreEqual("newAlias", result.Properties.First().Alias); + Assert.AreEqual("newAlias", result.Properties.Values.First().Alias); } } diff --git a/src/Umbraco.Tests/Persistence/Repositories/StylesheetRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/StylesheetRepositoryTest.cs index c87a976ab8..dd0bc36ff3 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/StylesheetRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/StylesheetRepositoryTest.cs @@ -1,17 +1,11 @@ -using System; -using System.Data; +using System.Data; using System.IO; using System.Linq; using System.Text; -using Moq; using NUnit.Framework; using Umbraco.Core.IO; -using Umbraco.Core.Logging; using Umbraco.Core.Models; -using Umbraco.Core.Persistence; -using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Persistence.Repositories.Implement; -using Umbraco.Core.Scoping; using Umbraco.Tests.TestHelpers; using Umbraco.Tests.Testing; @@ -118,10 +112,7 @@ namespace Umbraco.Tests.Persistence.Repositories stylesheet = repository.Get(stylesheet.Name); //Assert - Assert.That(stylesheet.Content, Is.EqualTo(@"body { color:#000; } .bold {font-weight:bold;} - -/**umb_name:Test*/ -p{font-size:2em;}")); + Assert.That(stylesheet.Content, Is.EqualTo("body { color:#000; } .bold {font-weight:bold;}\r\n\r\n/**umb_name:Test*/\r\np {\r\n\tfont-size:2em;\r\n}")); Assert.AreEqual(1, stylesheet.Properties.Count()); } } diff --git a/src/Umbraco.Tests/Persistence/SyntaxProvider/SqlCeSyntaxProviderTests.cs b/src/Umbraco.Tests/Persistence/SyntaxProvider/SqlCeSyntaxProviderTests.cs index 8e7681d17a..df842dc43c 100644 --- a/src/Umbraco.Tests/Persistence/SyntaxProvider/SqlCeSyntaxProviderTests.cs +++ b/src/Umbraco.Tests/Persistence/SyntaxProvider/SqlCeSyntaxProviderTests.cs @@ -89,7 +89,7 @@ WHERE (([umbracoNode].[nodeObjectType] = @0))) x)".Replace(Environment.NewLine, var sqlSyntax = new SqlServerSyntaxProvider(new Lazy(() => null)); var indexDefinition = CreateIndexDefinition(); - indexDefinition.IsClustered = false; + indexDefinition.IndexType = IndexTypes.Clustered; var actual = sqlSyntax.Format(indexDefinition); Assert.AreEqual("CREATE CLUSTERED INDEX [IX_A] ON [TheTable] ([A])", actual); diff --git a/src/Umbraco.Tests/PropertyEditors/MultiValuePropertyEditorTests.cs b/src/Umbraco.Tests/PropertyEditors/MultiValuePropertyEditorTests.cs index 9ba5ccf6f2..129116bf61 100644 --- a/src/Umbraco.Tests/PropertyEditors/MultiValuePropertyEditorTests.cs +++ b/src/Umbraco.Tests/PropertyEditors/MultiValuePropertyEditorTests.cs @@ -21,28 +21,15 @@ namespace Umbraco.Tests.PropertyEditors /// Tests for the base classes of ValueEditors and PreValueEditors that are used for Property Editors that edit /// multiple values such as the drop down list, check box list, color picker, etc.... /// + /// + /// Mostly this used to test the we'd store INT Ids in the Db but publish STRING values or sometimes the INT values + /// to cache. Now we always just deal with strings and we'll keep the tests that show that. + /// [TestFixture] public class MultiValuePropertyEditorTests { - //TODO: Test the other formatting methods for the drop down classes - [Test] - public void DropDownMultipleValueEditor_With_Keys_Format_Data_For_Cache() - { - var dataTypeServiceMock = new Mock(); - var editor = new PublishValuesMultipleValueEditor(true, Mock.Of(), new DataEditorAttribute("key", "nam", "view")); - - var dataType = new DataType(new CheckBoxListPropertyEditor(Mock.Of(), Mock.Of())); - var prop = new Property(1, new PropertyType(dataType)); - prop.SetValue("1234,4567,8910"); - - var result = editor.ConvertDbToString(prop.PropertyType, prop.GetValue(), new Mock().Object); - - Assert.AreEqual("1234,4567,8910", result); - } - - [Test] - public void DropDownMultipleValueEditor_No_Keys_Format_Data_For_Cache() + public void DropDownMultipleValueEditor_Format_Data_For_Cache() { var dataType = new DataType(new CheckBoxListPropertyEditor(Mock.Of(), Mock.Of())) { @@ -61,7 +48,7 @@ namespace Umbraco.Tests.PropertyEditors var dataTypeService = new TestObjects.TestDataTypeService(dataType); var prop = new Property(1, new PropertyType(dataType)); - prop.SetValue("1234,4567,8910"); + prop.SetValue("Value 1,Value 2,Value 3"); var valueEditor = dataType.Editor.GetValueEditor(); ((DataValueEditor) valueEditor).Configuration = dataType.Configuration; @@ -90,7 +77,7 @@ namespace Umbraco.Tests.PropertyEditors var dataTypeService = new TestObjects.TestDataTypeService(dataType); var prop = new Property(1, new PropertyType(dataType)); - prop.SetValue("1234"); + prop.SetValue("Value 2"); var result = dataType.Editor.GetValueEditor().ConvertDbToString(prop.PropertyType, prop.GetValue(), dataTypeService); diff --git a/src/Umbraco.Tests/PropertyEditors/PropertyEditorValueConverterTests.cs b/src/Umbraco.Tests/PropertyEditors/PropertyEditorValueConverterTests.cs index 2c3a2d1583..7ec23158f6 100644 --- a/src/Umbraco.Tests/PropertyEditors/PropertyEditorValueConverterTests.cs +++ b/src/Umbraco.Tests/PropertyEditors/PropertyEditorValueConverterTests.cs @@ -1,8 +1,14 @@ using System; using System.Collections.Generic; +using System.Linq; +using Moq; using NUnit.Framework; +using Umbraco.Core.Models; +using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.PropertyEditors; using Umbraco.Core.PropertyEditors.ValueConverters; +using Umbraco.Web.PropertyEditors; +using Umbraco.Web.PropertyEditors.ValueConverters; namespace Umbraco.Tests.PropertyEditors { @@ -79,27 +85,26 @@ namespace Umbraco.Tests.PropertyEditors [TestCase(null, new string[] { })] public void CanConvertDropdownListMultiplePropertyEditor(object value, IEnumerable expected) { - var converter = new DropdownListMultipleValueConverter(); - var inter = converter.ConvertSourceToIntermediate(null, null, value, false); - var result = converter.ConvertIntermediateToObject(null, null, PropertyCacheLevel.Unknown, inter, false); + var mockPublishedContentTypeFactory = new Mock(); + mockPublishedContentTypeFactory.Setup(x => x.GetDataType(123)) + .Returns(new PublishedDataType(123, "test", new Lazy(() => new DropDownFlexibleConfiguration + { + Multiple = true + }))); + + var publishedPropType = new PublishedPropertyType( + new PublishedContentType(1234, "test", PublishedItemType.Content, Enumerable.Empty(), Enumerable.Empty(), ContentVariation.Nothing), + new PropertyType("test", ValueStorageType.Nvarchar) { DataTypeId = 123 }, + new PropertyValueConverterCollection(Enumerable.Empty()), + Mock.Of(), mockPublishedContentTypeFactory.Object); + + var converter = new FlexibleDropdownPropertyValueConverter(); + var inter = converter.ConvertSourceToIntermediate(null, publishedPropType, value, false); + var result = converter.ConvertIntermediateToObject(null, publishedPropType, PropertyCacheLevel.Unknown, inter, false); Assert.AreEqual(expected, result); } - - [TestCase("100", new[] { 100 })] - [TestCase("100,200", new[] { 100, 200 })] - [TestCase("100 , 200, 300 ", new[] { 100, 200, 300 })] - [TestCase("", new int[] { })] - [TestCase(null, new int[] { })] - public void CanConvertDropdownListMultipleWithKeysPropertyEditor(object value, IEnumerable expected) - { - var converter = new DropdownListMultipleWithKeysValueConverter(); - var inter = converter.ConvertSourceToIntermediate(null, null, value, false); - var result = converter.ConvertIntermediateToObject(null, null, PropertyCacheLevel.Unknown, inter, false); - - Assert.AreEqual(expected, result); - } - + [TestCase("1", 1)] [TestCase("1", 1)] [TestCase("0", 0)] diff --git a/src/Umbraco.Tests/Routing/RenderRouteHandlerTests.cs b/src/Umbraco.Tests/Routing/RenderRouteHandlerTests.cs index ecc5a3d281..b277194063 100644 --- a/src/Umbraco.Tests/Routing/RenderRouteHandlerTests.cs +++ b/src/Umbraco.Tests/Routing/RenderRouteHandlerTests.cs @@ -84,7 +84,6 @@ namespace Umbraco.Tests.Routing Template CreateTemplate(string alias) { - var path = "template"; var name = "Template"; var template = new Template(name, alias); template.Content = ""; // else saving throws with a dirty internal error diff --git a/src/Umbraco.Tests/Runtimes/CoreRuntimeTests.cs b/src/Umbraco.Tests/Runtimes/CoreRuntimeTests.cs old mode 100644 new mode 100755 index 770dead600..9a8164356a --- a/src/Umbraco.Tests/Runtimes/CoreRuntimeTests.cs +++ b/src/Umbraco.Tests/Runtimes/CoreRuntimeTests.cs @@ -13,7 +13,7 @@ using Umbraco.Core.Persistence; using Umbraco.Core.Runtime; using Umbraco.Tests.TestHelpers; using Umbraco.Tests.TestHelpers.Stubs; -using Umbraco.Examine; +using Umbraco.Web; namespace Umbraco.Tests.Runtimes { @@ -60,9 +60,13 @@ namespace Umbraco.Tests.Runtimes { protected override IRuntime GetRuntime() { - return new TestRuntime(this); + return new TestRuntime(); } + } + // test runtime + public class TestRuntime : CoreRuntime + { // the application's logger is created by the application // through GetLogger, that custom application can override protected override ILogger GetLogger() @@ -71,18 +75,6 @@ namespace Umbraco.Tests.Runtimes return new DebugDiagnosticsLogger(); } - // don't register anything against AppDomain - protected override void ConfigureUnhandledException(ILogger logger) - { } - } - - // test runtime - public class TestRuntime : CoreRuntime - { - public TestRuntime(UmbracoApplicationBase umbracoApplication) - : base(umbracoApplication) - { } - public override void Compose(ServiceContainer container) { base.Compose(container); diff --git a/src/Umbraco.Tests/Services/ContentServicePerformanceTest.cs b/src/Umbraco.Tests/Services/ContentServicePerformanceTest.cs index 28021f1e22..4d4825323c 100644 --- a/src/Umbraco.Tests/Services/ContentServicePerformanceTest.cs +++ b/src/Umbraco.Tests/Services/ContentServicePerformanceTest.cs @@ -105,10 +105,10 @@ namespace Umbraco.Tests.Services total.AddRange(ServiceContext.ContentService.GetRootContent()); foreach (var content in total.ToArray()) { - total.AddRange(ServiceContext.ContentService.GetDescendants(content)); + total.AddRange(ServiceContext.ContentService.GetPagedDescendants(content.Id, 0, int.MaxValue, out var _)); } TestProfiler.Disable(); - Current.Logger.Info("Returned " + total.Count + " items"); + Current.Logger.Info("Returned {Total} items", total.Count); } } diff --git a/src/Umbraco.Tests/Services/ContentServicePublishBranchTests.cs b/src/Umbraco.Tests/Services/ContentServicePublishBranchTests.cs new file mode 100644 index 0000000000..4afd5e33eb --- /dev/null +++ b/src/Umbraco.Tests/Services/ContentServicePublishBranchTests.cs @@ -0,0 +1,476 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Core.Services; +using Umbraco.Tests.TestHelpers; +using Umbraco.Tests.Testing; + +// ReSharper disable CommentTypo +// ReSharper disable StringLiteralTypo +namespace Umbraco.Tests.Services +{ + [TestFixture] + [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest, PublishedRepositoryEvents = true, WithApplication = true)] + public class ContentServicePublishBranchTests : TestWithDatabaseBase + { + [TestCase(1)] // use overload w/ culture: "*" + [TestCase(2)] // use overload w/ cultures: new [] { "*" } + public void Can_Publish_Invariant_Branch(int method) + { + CreateTypes(out var iContentType, out _); + + IContent iRoot = new Content("iroot", -1, iContentType); + iRoot.SetValue("ip", "iroot"); + ServiceContext.ContentService.Save(iRoot); + IContent ii1 = new Content("ii1", iRoot, iContentType); + ii1.SetValue("ip", "vii1"); + ServiceContext.ContentService.Save(ii1); + IContent ii2 = new Content("ii2", iRoot, iContentType); + ii2.SetValue("ip", "vii2"); + ServiceContext.ContentService.Save(ii2); + + // iroot !published !edited + // ii1 !published !edited + // ii2 !published !edited + + // !force = publishes those that are actually published, and have changes + // here: root (root is always published) + + var r = SaveAndPublishInvariantBranch(iRoot, false, method).ToArray(); + + // not forcing, ii1 and ii2 not published yet: only root got published + AssertPublishResults(r, x => x.Content.Name, + "iroot"); + AssertPublishResults(r, x => x.Result, + PublishResultType.SuccessPublish); + + // prepare + + ServiceContext.ContentService.SaveAndPublish(iRoot); + ServiceContext.ContentService.SaveAndPublish(ii1); + + IContent ii11 = new Content("ii11", ii1, iContentType); + ii11.SetValue("ip", "vii11"); + ServiceContext.ContentService.SaveAndPublish(ii11); + IContent ii12 = new Content("ii12", ii1, iContentType); + ii11.SetValue("ip", "vii12"); + ServiceContext.ContentService.Save(ii12); + + ServiceContext.ContentService.SaveAndPublish(ii2); + IContent ii21 = new Content("ii21", ii2, iContentType); + ii21.SetValue("ip", "vii21"); + ServiceContext.ContentService.SaveAndPublish(ii21); + IContent ii22 = new Content("ii22", ii2, iContentType); + ii22.SetValue("ip", "vii22"); + ServiceContext.ContentService.Save(ii22); + ServiceContext.ContentService.Unpublish(ii2); + + // iroot published !edited + // ii1 published !edited + // ii11 published !edited + // ii12 !published !edited + // ii2 !published !edited + // ii21 (published) !edited + // ii22 !published !edited + + // !force = publishes those that are actually published, and have changes + // here: nothing + + r = SaveAndPublishInvariantBranch(iRoot, false, method).ToArray(); + + // not forcing, ii12 and ii2, ii21, ii22 not published yet: only root, ii1, ii11 got published + AssertPublishResults(r, x => x.Content.Name, + "iroot", "ii1", "ii11"); + AssertPublishResults(r, x => x.Result, + PublishResultType.SuccessPublishAlready, + PublishResultType.SuccessPublishAlready, + PublishResultType.SuccessPublishAlready); + + // prepare + + iRoot.SetValue("ip", "changed"); + ServiceContext.ContentService.Save(iRoot); + ii11.SetValue("ip", "changed"); + ServiceContext.ContentService.Save(ii11); + + // iroot published edited *** + // ii1 published !edited + // ii11 published edited *** + // ii12 !published !edited + // ii2 !published !edited + // ii21 (published) !edited + // ii22 !published !edited + + // !force = publishes those that are actually published, and have changes + // here: iroot and ii11 + + // not forcing, ii12 and ii2, ii21, ii22 not published yet: only root, ii1, ii11 got published + r = SaveAndPublishInvariantBranch(iRoot, false, method).ToArray(); + AssertPublishResults(r, x => x.Content.Name, + "iroot", "ii1", "ii11"); + AssertPublishResults(r, x => x.Result, + PublishResultType.SuccessPublish, + PublishResultType.SuccessPublishAlready, + PublishResultType.SuccessPublish); + + // force = publishes everything that has changes + // here: ii12, ii2, ii22 - ii21 was published already but masked + + r = SaveAndPublishInvariantBranch(iRoot, true, method).ToArray(); + AssertPublishResults(r, x => x.Content.Name, + "iroot", "ii1", "ii11", "ii12", "ii2", "ii21", "ii22"); + AssertPublishResults(r, x => x.Result, + PublishResultType.SuccessPublishAlready, + PublishResultType.SuccessPublishAlready, + PublishResultType.SuccessPublishAlready, + PublishResultType.SuccessPublish, + PublishResultType.SuccessPublish, + PublishResultType.SuccessPublishAlready, // was masked + PublishResultType.SuccessPublish); + + ii21 = ServiceContext.ContentService.GetById(ii21.Id); + Assert.IsTrue(ii21.Published); + } + + [Test] + public void Can_Publish_Variant_Branch_When_No_Changes_On_Root_All_Cultures() + { + CreateTypes(out _, out var vContentType); + + //create/publish root + IContent vRoot = new Content("vroot", -1, vContentType, "de"); + vRoot.SetCultureName("vroot.de", "de"); + vRoot.SetCultureName("vroot.ru", "ru"); + vRoot.SetCultureName("vroot.es", "es"); + vRoot.SetValue("ip", "vroot"); + vRoot.SetValue("vp", "vroot.de", "de"); + vRoot.SetValue("vp", "vroot.ru", "ru"); + vRoot.SetValue("vp", "vroot.es", "es"); + ServiceContext.ContentService.SaveAndPublish(vRoot); + + //create/publish child + IContent iv1 = new Content("iv1", vRoot, vContentType, "de"); + iv1.SetCultureName("iv1.de", "de"); + iv1.SetCultureName("iv1.ru", "ru"); + iv1.SetCultureName("iv1.es", "es"); + iv1.SetValue("ip", "iv1"); + iv1.SetValue("vp", "iv1.de", "de"); + iv1.SetValue("vp", "iv1.ru", "ru"); + iv1.SetValue("vp", "iv1.es", "es"); + ServiceContext.ContentService.SaveAndPublish(iv1); + + //update the child + iv1.SetValue("vp", "UPDATED-iv1.de", "de"); + ServiceContext.ContentService.Save(iv1); + + var r = ServiceContext.ContentService.SaveAndPublishBranch(vRoot, false).ToArray(); //no culture specified so "*" is used, so all cultures + Assert.AreEqual(PublishResultType.SuccessPublishAlready, r[0].Result); + Assert.AreEqual(PublishResultType.SuccessPublishCulture, r[1].Result); + } + + [Test] + public void Can_Publish_Variant_Branch_When_No_Changes_On_Root_Specific_Culture() + { + CreateTypes(out _, out var vContentType); + + //create/publish root + IContent vRoot = new Content("vroot", -1, vContentType, "de"); + vRoot.SetCultureName("vroot.de", "de"); + vRoot.SetCultureName("vroot.ru", "ru"); + vRoot.SetCultureName("vroot.es", "es"); + vRoot.SetValue("ip", "vroot"); + vRoot.SetValue("vp", "vroot.de", "de"); + vRoot.SetValue("vp", "vroot.ru", "ru"); + vRoot.SetValue("vp", "vroot.es", "es"); + ServiceContext.ContentService.SaveAndPublish(vRoot); + + //create/publish child + IContent iv1 = new Content("iv1", vRoot, vContentType, "de"); + iv1.SetCultureName("iv1.de", "de"); + iv1.SetCultureName("iv1.ru", "ru"); + iv1.SetCultureName("iv1.es", "es"); + iv1.SetValue("ip", "iv1"); + iv1.SetValue("vp", "iv1.de", "de"); + iv1.SetValue("vp", "iv1.ru", "ru"); + iv1.SetValue("vp", "iv1.es", "es"); + ServiceContext.ContentService.SaveAndPublish(iv1); + + //update the child + iv1.SetValue("vp", "UPDATED-iv1.de", "de"); + ServiceContext.ContentService.Save(iv1); + + var r = ServiceContext.ContentService.SaveAndPublishBranch(vRoot, false, "de").ToArray(); + Assert.AreEqual(PublishResultType.SuccessPublishAlready, r[0].Result); + Assert.AreEqual(PublishResultType.SuccessPublishCulture, r[1].Result); + } + + [Test] + public void Can_Publish_Variant_Branch() + { + CreateTypes(out _, out var vContentType); + + IContent vRoot = new Content("vroot", -1, vContentType, "de"); + vRoot.SetCultureName("vroot.de", "de"); + vRoot.SetCultureName("vroot.ru", "ru"); + vRoot.SetCultureName("vroot.es", "es"); + vRoot.SetValue("ip", "vroot"); + vRoot.SetValue("vp", "vroot.de", "de"); + vRoot.SetValue("vp", "vroot.ru", "ru"); + vRoot.SetValue("vp", "vroot.es", "es"); + ServiceContext.ContentService.Save(vRoot); + + IContent iv1 = new Content("iv1", vRoot, vContentType, "de"); + iv1.SetCultureName("iv1.de", "de"); + iv1.SetCultureName("iv1.ru", "ru"); + iv1.SetCultureName("iv1.es", "es"); + iv1.SetValue("ip", "iv1"); + iv1.SetValue("vp", "iv1.de", "de"); + iv1.SetValue("vp", "iv1.ru", "ru"); + iv1.SetValue("vp", "iv1.es", "es"); + ServiceContext.ContentService.Save(iv1); + + IContent iv2 = new Content("iv2", vRoot, vContentType, "de"); + iv2.SetCultureName("iv2.de", "de"); + iv2.SetCultureName("iv2.ru", "ru"); + iv2.SetCultureName("iv2.es", "es"); + iv2.SetValue("ip", "iv2"); + iv2.SetValue("vp", "iv2.de", "de"); + iv2.SetValue("vp", "iv2.ru", "ru"); + iv2.SetValue("vp", "iv2.es", "es"); + ServiceContext.ContentService.Save(iv2); + + // vroot !published !edited + // iv1 !published !edited + // iv2 !published !edited + + // !force = publishes those that are actually published, and have changes + // here: nothing + + var r = ServiceContext.ContentService.SaveAndPublishBranch(vRoot, false).ToArray(); // no culture specified = all cultures + + // not forcing, iv1 and iv2 not published yet: only root got published + AssertPublishResults(r, x => x.Content.Name, + "vroot.de"); + AssertPublishResults(r, x => x.Result, + PublishResultType.SuccessPublishCulture); + + // prepare + vRoot.SetValue("ip", "changed"); + vRoot.SetValue("vp", "changed.de", "de"); + vRoot.SetValue("vp", "changed.ru", "ru"); + vRoot.SetValue("vp", "changed.es", "es"); + ServiceContext.ContentService.Save(vRoot); // now root has drafts in all cultures + + iv1.PublishCulture("de"); + iv1.PublishCulture("ru"); + ServiceContext.ContentService.SavePublishing(iv1); // now iv1 de and ru are published + + iv1.SetValue("ip", "changed"); + iv1.SetValue("vp", "changed.de", "de"); + iv1.SetValue("vp", "changed.ru", "ru"); + iv1.SetValue("vp", "changed.es", "es"); + ServiceContext.ContentService.Save(iv1); // now iv1 has drafts in all cultures + + // validate - everything published for root, because no culture was specified = all + Assert.IsTrue(vRoot.Published); + Assert.IsTrue(vRoot.IsCulturePublished("de")); + Assert.IsTrue(vRoot.IsCulturePublished("ru")); + Assert.IsTrue(vRoot.IsCulturePublished("es")); + + // validate - only some cultures published for iv1 + Assert.IsTrue(iv1.Published); + Assert.IsTrue(iv1.IsCulturePublished("de")); + Assert.IsTrue(iv1.IsCulturePublished("ru")); + Assert.IsFalse(iv1.IsCulturePublished("es")); + + r = ServiceContext.ContentService.SaveAndPublishBranch(vRoot, false, "de").ToArray(); + + // not forcing, iv2 not published yet: only root and iv1 got published + AssertPublishResults(r, x => x.Content.Name, + "vroot.de", "iv1.de"); + AssertPublishResults(r, x => x.Result, + PublishResultType.SuccessPublishCulture, + PublishResultType.SuccessPublishCulture); + + // reload - SaveAndPublishBranch has modified other instances + Reload(ref iv1); + Reload(ref iv2); + + // validate - root + Assert.IsTrue(vRoot.Published); + Assert.IsTrue(vRoot.IsCulturePublished("de")); + Assert.IsFalse(vRoot.IsCultureEdited("de")); // no drafts, this was just published + Assert.IsTrue(vRoot.IsCulturePublished("ru")); + Assert.IsTrue(vRoot.IsCultureEdited("ru")); // has draft + Assert.IsTrue(vRoot.IsCulturePublished("es")); + Assert.IsTrue(vRoot.IsCultureEdited("es")); // has draft + + Assert.AreEqual("changed", vRoot.GetValue("ip", published: true)); // publishing de implies publishing invariants + Assert.AreEqual("changed.de", vRoot.GetValue("vp", "de", published: true)); + + // validate - de and ru are published, es has not been published + Assert.IsTrue(iv1.Published); + Assert.IsTrue(iv1.IsCulturePublished("de")); + Assert.IsTrue(iv1.IsCulturePublished("ru")); + Assert.IsFalse(iv1.IsCulturePublished("es")); + Assert.AreEqual("changed", iv1.GetValue("ip", published: true)); + Assert.AreEqual("changed.de", iv1.GetValue("vp", "de", published: true)); + Assert.AreEqual("iv1.ru", iv1.GetValue("vp", "ru", published: true)); + } + + private void Can_Publish_Mixed_Branch(out IContent iRoot, out IContent ii1, out IContent iv11) + { + // invariant root -> variant -> invariant + // variant root -> variant -> invariant + // variant root -> invariant -> variant + + CreateTypes(out var iContentType, out var vContentType); + + // invariant root -> invariant -> variant + iRoot = new Content("iroot", -1, iContentType); + iRoot.SetValue("ip", "iroot"); + ServiceContext.ContentService.SaveAndPublish(iRoot); + ii1 = new Content("ii1", iRoot, iContentType); + ii1.SetValue("ip", "vii1"); + ServiceContext.ContentService.SaveAndPublish(ii1); + ii1.SetValue("ip", "changed"); + ServiceContext.ContentService.Save(ii1); + iv11 = new Content("iv11.de", ii1, vContentType, "de"); + iv11.SetValue("ip", "iv11"); + iv11.SetValue("vp", "iv11.de", "de"); + iv11.SetValue("vp", "iv11.ru", "ru"); + iv11.SetValue("vp", "iv11.es", "es"); + ServiceContext.ContentService.Save(iv11); + + iv11.PublishCulture("de"); + iv11.SetCultureName("iv11.ru", "ru"); + iv11.PublishCulture("ru"); + ServiceContext.ContentService.SavePublishing(iv11); + + Assert.AreEqual("iv11.de", iv11.GetValue("vp", "de", published: true)); + Assert.AreEqual("iv11.ru", iv11.GetValue("vp", "ru", published: true)); + + iv11.SetValue("ip", "changed"); + iv11.SetValue("vp", "changed.de", "de"); + iv11.SetValue("vp", "changed.ru", "ru"); + ServiceContext.ContentService.Save(iv11); + } + + [Test] + public void Can_Publish_Mixed_Branch_1() + { + Can_Publish_Mixed_Branch(out var iRoot, out var ii1, out var iv11); + + var r = ServiceContext.ContentService.SaveAndPublishBranch(iRoot, false, "de").ToArray(); + AssertPublishResults(r, x => x.Content.Name, + "iroot", "ii1", "iv11.de"); + AssertPublishResults(r, x => x.Result, + PublishResultType.SuccessPublishAlready, + PublishResultType.SuccessPublish, + PublishResultType.SuccessPublishCulture); + + // reload - SaveAndPublishBranch has modified other instances + Reload(ref ii1); + Reload(ref iv11); + + // the invariant child has been published + // the variant child has been published for 'de' only + + Assert.AreEqual("changed", ii1.GetValue("ip", published: true)); + Assert.AreEqual("changed", iv11.GetValue("ip", published: true)); + Assert.AreEqual("changed.de", iv11.GetValue("vp", "de", published: true)); + Assert.AreEqual("iv11.ru", iv11.GetValue("vp", "ru", published: true)); + } + + [Test] + public void Can_Publish_MixedBranch_2() + { + Can_Publish_Mixed_Branch(out var iRoot, out var ii1, out var iv11); + + var r = ServiceContext.ContentService.SaveAndPublishBranch(iRoot, false, new[] { "de", "ru" }).ToArray(); + AssertPublishResults(r, x => x.Content.Name, + "iroot", "ii1", "iv11.de"); + AssertPublishResults(r, x => x.Result, + PublishResultType.SuccessPublishAlready, + PublishResultType.SuccessPublish, + PublishResultType.SuccessPublishCulture); + + // reload - SaveAndPublishBranch has modified other instances + Reload(ref ii1); + Reload(ref iv11); + + // the invariant child has been published + // the variant child has been published for 'de' and 'ru' + + Assert.AreEqual("changed", ii1.GetValue("ip", published: true)); + Assert.AreEqual("changed", iv11.GetValue("ip", published: true)); + Assert.AreEqual("changed.de", iv11.GetValue("vp", "de", published: true)); + Assert.AreEqual("changed.ru", iv11.GetValue("vp", "ru", published: true)); + } + + private void AssertPublishResults(PublishResult[] values, Func getter, params T[] expected) + { + if (expected.Length != values.Length) + Console.WriteLine(string.Join(", ", values.Select(x => getter(x).ToString()))); + Assert.AreEqual(expected.Length, values.Length); + + for (var i = 0; i < values.Length; i++) + { + var value = getter(values[i]); + Assert.AreEqual(expected[i], value, $"Expected {expected[i]} at {i} but got {value}."); + } + } + + private void Reload(ref IContent document) + => document = ServiceContext.ContentService.GetById(document.Id); + + private void CreateTypes(out IContentType iContentType, out IContentType vContentType) + { + var langDe = new Language("de") { IsDefault = true }; + ServiceContext.LocalizationService.Save(langDe); + var langRu = new Language("ru"); + ServiceContext.LocalizationService.Save(langRu); + var langEs = new Language("es"); + ServiceContext.LocalizationService.Save(langEs); + + iContentType = new ContentType(-1) + { + Alias = "ict", + Name = "Invariant Content Type", + Variations = ContentVariation.Nothing + }; + iContentType.AddPropertyType(new PropertyType(Constants.PropertyEditors.Aliases.TextBox, ValueStorageType.Nvarchar, "ip") { Variations = ContentVariation.Nothing }); + ServiceContext.ContentTypeService.Save(iContentType); + + vContentType = new ContentType(-1) + { + Alias = "vct", + Name = "Variant Content Type", + Variations = ContentVariation.Culture + }; + vContentType.AddPropertyType(new PropertyType(Constants.PropertyEditors.Aliases.TextBox, ValueStorageType.Nvarchar, "ip") { Variations = ContentVariation.Nothing }); + vContentType.AddPropertyType(new PropertyType(Constants.PropertyEditors.Aliases.TextBox, ValueStorageType.Nvarchar, "vp") { Variations = ContentVariation.Culture }); + ServiceContext.ContentTypeService.Save(vContentType); + } + + private IEnumerable SaveAndPublishInvariantBranch(IContent content, bool force, int method) + { + // ReSharper disable RedundantArgumentDefaultValue + // ReSharper disable ArgumentsStyleOther + switch (method) + { + case 1: + return ServiceContext.ContentService.SaveAndPublishBranch(content, force, culture: "*"); + case 2: + return ServiceContext.ContentService.SaveAndPublishBranch(content, force, cultures: new [] { "*" }); + default: + throw new ArgumentOutOfRangeException(nameof(method)); + } + // ReSharper restore RedundantArgumentDefaultValue + // ReSharper restore ArgumentsStyleOther + } + } +} diff --git a/src/Umbraco.Tests/Services/ContentServiceTests.cs b/src/Umbraco.Tests/Services/ContentServiceTests.cs index f187b8b70d..2c90ce90c5 100644 --- a/src/Umbraco.Tests/Services/ContentServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentServiceTests.cs @@ -1,5 +1,4 @@ using System; -using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Linq; @@ -12,19 +11,17 @@ using Umbraco.Core.IO; using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; using LightInject; -using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Services; using Umbraco.Tests.TestHelpers.Entities; using Umbraco.Core.Events; -using Umbraco.Core.Logging; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Repositories.Implement; using Umbraco.Core.PropertyEditors; using Umbraco.Core.Scoping; using Umbraco.Core.Services.Implement; using Umbraco.Tests.Testing; -using Umbraco.Web.PropertyEditors; using System.Reflection; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; namespace Umbraco.Tests.Services { @@ -34,7 +31,10 @@ namespace Umbraco.Tests.Services /// as well as configuration. /// [TestFixture] - [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest, PublishedRepositoryEvents = true, WithApplication = true)] + [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest, + PublishedRepositoryEvents = true, + WithApplication = true, + Logger = UmbracoTestOptions.Logger.Console)] public class ContentServiceTests : TestWithSomeContentBase { //TODO Add test to verify there is only ONE newest document/content in {Constants.DatabaseSchema.Tables.Document} table after updating. @@ -211,11 +211,11 @@ namespace Umbraco.Tests.Services var root = ServiceContext.ContentService.GetById(NodeDto.NodeIdSeed + 1); root.PublishCulture(); Assert.IsTrue(contentService.SaveAndPublish(root).Success); - var content = contentService.CreateAndSave("Test", -1, "umbTextpage", 0); + var content = contentService.CreateAndSave("Test", -1, "umbTextpage", Constants.Security.SuperUserId); content.PublishCulture(); Assert.IsTrue(contentService.SaveAndPublish(content).Success); var hierarchy = CreateContentHierarchy().OrderBy(x => x.Level).ToArray(); - contentService.Save(hierarchy, 0); + contentService.Save(hierarchy, Constants.Security.SuperUserId); foreach (var c in hierarchy) { c.PublishCulture(); @@ -250,6 +250,96 @@ namespace Umbraco.Tests.Services Assert.That(hierarchy.All(c => c.Path.StartsWith("-1,-20") == false), Is.True); } + [Test] + public void Perform_Scheduled_Publishing() + { + var langUk = new Language("en-GB") { IsDefault = true }; + var langFr = new Language("fr-FR"); + + ServiceContext.LocalizationService.Save(langFr); + ServiceContext.LocalizationService.Save(langUk); + + var ctInvariant = MockedContentTypes.CreateBasicContentType("invariantPage"); + ServiceContext.ContentTypeService.Save(ctInvariant); + + var ctVariant = MockedContentTypes.CreateBasicContentType("variantPage"); + ctVariant.Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(ctVariant); + + var now = DateTime.Now; + + //10x invariant content, half is scheduled to be published in 5 seconds, the other half is scheduled to be unpublished in 5 seconds + var invariant = new List(); + for (var i = 0; i < 10; i++) + { + var c = MockedContent.CreateBasicContent(ctInvariant); + c.Name = "name" + i; + if (i % 2 == 0) + { + c.ContentSchedule.Add(now.AddSeconds(5), null); //release in 5 seconds + var r = ServiceContext.ContentService.Save(c); + Assert.IsTrue(r.Success, r.Result.ToString()); + } + else + { + c.ContentSchedule.Add(null, now.AddSeconds(5)); //expire in 5 seconds + var r = ServiceContext.ContentService.SaveAndPublish(c); + Assert.IsTrue(r.Success, r.Result.ToString()); + } + invariant.Add(c); + } + + //10x variant content, half is scheduled to be published in 5 seconds, the other half is scheduled to be unpublished in 5 seconds + var variant = new List(); + var alternatingCulture = langFr.IsoCode; + for (var i = 0; i < 10; i++) + { + var c = MockedContent.CreateBasicContent(ctVariant); + c.SetCultureName("name-uk" + i, langUk.IsoCode); + c.SetCultureName("name-fr" + i, langFr.IsoCode); + + if (i % 2 == 0) + { + c.ContentSchedule.Add(alternatingCulture, now.AddSeconds(5), null); //release in 5 seconds + var r = ServiceContext.ContentService.Save(c); + Assert.IsTrue(r.Success, r.Result.ToString()); + + alternatingCulture = alternatingCulture == langFr.IsoCode ? langUk.IsoCode : langFr.IsoCode; + } + else + { + c.ContentSchedule.Add(alternatingCulture, null, now.AddSeconds(5)); //expire in 5 seconds + var r = ServiceContext.ContentService.SaveAndPublish(c); + Assert.IsTrue(r.Success, r.Result.ToString()); + } + variant.Add(c); + } + + + var runSched = ServiceContext.ContentService.PerformScheduledPublish( + now.AddMinutes(1)).ToList(); //process anything scheduled before a minute from now + + //this is 21 because the test data installed before this test runs has a scheduled item! + Assert.AreEqual(21, runSched.Count); + Assert.AreEqual(20, runSched.Count(x => x.Success), + string.Join(Environment.NewLine, runSched.Select(x => $"{x.Entity.Name} - {x.Result}"))); + + Assert.AreEqual(5, runSched.Count(x => x.Result == PublishResultType.SuccessPublish), + string.Join(Environment.NewLine, runSched.Select(x => $"{x.Entity.Name} - {x.Result}"))); + Assert.AreEqual(5, runSched.Count(x => x.Result == PublishResultType.SuccessUnpublish), + string.Join(Environment.NewLine, runSched.Select(x => $"{x.Entity.Name} - {x.Result}"))); + Assert.AreEqual(5, runSched.Count(x => x.Result == PublishResultType.SuccessPublishCulture), + string.Join(Environment.NewLine, runSched.Select(x => $"{x.Entity.Name} - {x.Result}"))); + Assert.AreEqual(5, runSched.Count(x => x.Result == PublishResultType.SuccessUnpublishCulture), + string.Join(Environment.NewLine, runSched.Select(x => $"{x.Entity.Name} - {x.Result}"))); + + //re-run the scheduled publishing, there should be no results + runSched = ServiceContext.ContentService.PerformScheduledPublish( + now.AddMinutes(1)).ToList(); + + Assert.AreEqual(0, runSched.Count); + } + [Test] public void Remove_Scheduled_Publishing_Date() { @@ -257,17 +347,24 @@ namespace Umbraco.Tests.Services var contentService = ServiceContext.ContentService; // Act - var content = contentService.CreateAndSave("Test", -1, "umbTextpage", 0); + var content = contentService.CreateAndSave("Test", -1, "umbTextpage", Constants.Security.SuperUserId); - content.ReleaseDate = DateTime.Now.AddHours(2); - contentService.Save(content, 0); + content.ContentSchedule.Add(null, DateTime.Now.AddHours(2)); + contentService.Save(content, Constants.Security.SuperUserId); + Assert.AreEqual(1, content.ContentSchedule.FullSchedule.Count); content = contentService.GetById(content.Id); - content.ReleaseDate = null; - contentService.Save(content, 0); + var sched = content.ContentSchedule.FullSchedule; + Assert.AreEqual(1, sched.Count); + Assert.AreEqual(1, sched.Count(x => x.Culture == string.Empty)); + content.ContentSchedule.Clear(ContentScheduleAction.Expire); + contentService.Save(content, Constants.Security.SuperUserId); // Assert + content = contentService.GetById(content.Id); + sched = content.ContentSchedule.FullSchedule; + Assert.AreEqual(0, sched.Count); Assert.IsTrue(contentService.SaveAndPublish(content).Success); } @@ -278,7 +375,7 @@ namespace Umbraco.Tests.Services var contentService = ServiceContext.ContentService; // Act - var content = contentService.CreateAndSave("Test", -1, "umbTextpage", 0); + var content = contentService.CreateAndSave("Test", -1, "umbTextpage", Constants.Security.SuperUserId); for (var i = 0; i < 20; i++) { content.SetValue("bodyText", "hello world " + Guid.NewGuid()); @@ -323,7 +420,7 @@ namespace Umbraco.Tests.Services // Act for (int i = 0; i < 20; i++) { - contentService.CreateAndSave("Test", -1, "umbTextpage", 0); + contentService.CreateAndSave("Test", -1, "umbTextpage", Constants.Security.SuperUserId); } // Assert @@ -342,7 +439,7 @@ namespace Umbraco.Tests.Services // Act for (int i = 0; i < 20; i++) { - contentService.CreateAndSave("Test", -1, "umbBlah", 0); + contentService.CreateAndSave("Test", -1, "umbBlah", Constants.Security.SuperUserId); } // Assert @@ -357,7 +454,7 @@ namespace Umbraco.Tests.Services var contentTypeService = ServiceContext.ContentTypeService; var contentType = MockedContentTypes.CreateSimpleContentType("umbBlah", "test Doc Type"); contentTypeService.Save(contentType); - var parent = contentService.CreateAndSave("Test", -1, "umbBlah", 0); + var parent = contentService.CreateAndSave("Test", -1, "umbBlah", Constants.Security.SuperUserId); // Act for (int i = 0; i < 20; i++) @@ -377,7 +474,7 @@ namespace Umbraco.Tests.Services var contentTypeService = ServiceContext.ContentTypeService; var contentType = MockedContentTypes.CreateSimpleContentType("umbBlah", "test Doc Type"); contentTypeService.Save(contentType); - var parent = contentService.CreateAndSave("Test", -1, "umbBlah", 0); + var parent = contentService.CreateAndSave("Test", -1, "umbBlah", Constants.Security.SuperUserId); // Act IContent current = parent; @@ -916,7 +1013,7 @@ namespace Umbraco.Tests.Services var contentService = ServiceContext.ContentService; // Act - var content = contentService.Create("Test", -1, "umbTextpage", 0); + var content = contentService.Create("Test", -1, "umbTextpage", Constants.Security.SuperUserId); // Assert Assert.That(content, Is.Not.Null); @@ -930,7 +1027,7 @@ namespace Umbraco.Tests.Services var contentService = ServiceContext.ContentService; // Act - var content = contentService.Create("Test", -1, "umbTextpage", 0); + var content = contentService.Create("Test", -1, "umbTextpage", Constants.Security.SuperUserId); // Assert Assert.That(content, Is.Not.Null); @@ -1037,37 +1134,6 @@ namespace Umbraco.Tests.Services Assert.That(contents.Count(), Is.GreaterThanOrEqualTo(2)); } - [Test] - public void Can_Get_Children_Of_Content_Id() - { - // Arrange - var contentService = ServiceContext.ContentService; - - // Act - var contents = contentService.GetChildren(NodeDto.NodeIdSeed + 2).ToList(); - - // Assert - Assert.That(contents, Is.Not.Null); - Assert.That(contents.Any(), Is.True); - Assert.That(contents.Count(), Is.GreaterThanOrEqualTo(2)); - } - - [Test] - public void Can_Get_Descendents_Of_Content() - { - // Arrange - var contentService = ServiceContext.ContentService; - var hierarchy = CreateContentHierarchy(); - contentService.Save(hierarchy, 0); - - // Act - var contents = contentService.GetDescendants(NodeDto.NodeIdSeed + 2).ToList(); - - // Assert - Assert.That(contents, Is.Not.Null); - Assert.That(contents.Any(), Is.True); - Assert.That(contents.Count(), Is.EqualTo(52)); - } [Test] public void Can_Get_All_Versions_Of_Content() @@ -1163,12 +1229,12 @@ namespace Umbraco.Tests.Services var root = contentService.GetById(NodeDto.NodeIdSeed + 2); contentService.SaveAndPublish(root); var content = contentService.GetById(NodeDto.NodeIdSeed + 4); - content.ExpireDate = DateTime.Now.AddSeconds(1); + content.ContentSchedule.Add(null, DateTime.Now.AddSeconds(1)); contentService.SaveAndPublish(content); // Act Thread.Sleep(new TimeSpan(0, 0, 0, 2)); - var contents = contentService.GetContentForExpiration().ToList(); + var contents = contentService.GetContentForExpiration(DateTime.Now).ToList(); // Assert Assert.That(contents, Is.Not.Null); @@ -1183,7 +1249,7 @@ namespace Umbraco.Tests.Services var contentService = ServiceContext.ContentService; // Act - var contents = contentService.GetContentForRelease().ToList(); + var contents = contentService.GetContentForRelease(DateTime.Now).ToList(); // Assert Assert.That(DateTime.Now.AddMinutes(-5) <= DateTime.Now); @@ -1199,7 +1265,7 @@ namespace Umbraco.Tests.Services var contentService = ServiceContext.ContentService; // Act - var contents = contentService.GetContentInRecycleBin().ToList(); + var contents = contentService.GetPagedContentInRecycleBin(0, int.MaxValue, out var _).ToList(); // Assert Assert.That(contents, Is.Not.Null); @@ -1227,6 +1293,7 @@ namespace Umbraco.Tests.Services Assert.That(published.Success, Is.True); Assert.That(unpublished.Success, Is.True); Assert.That(content.Published, Is.False); + Assert.AreEqual(PublishResultType.SuccessUnpublish, unpublished.Result); using (var scope = ScopeProvider.CreateScope()) { @@ -1234,6 +1301,135 @@ namespace Umbraco.Tests.Services } } + [Test] + public void Can_Unpublish_Content_Variation() + { + // Arrange + + var langUk = new Language("en-GB") { IsDefault = true }; + var langFr = new Language("fr-FR"); + + ServiceContext.LocalizationService.Save(langFr); + ServiceContext.LocalizationService.Save(langUk); + + var contentType = MockedContentTypes.CreateBasicContentType(); + contentType.Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(contentType); + + IContent content = new Content("content", -1, contentType); + content.SetCultureName("content-fr", langFr.IsoCode); + content.SetCultureName("content-en", langUk.IsoCode); + content.PublishCulture(langFr.IsoCode); + content.PublishCulture(langUk.IsoCode); + Assert.IsTrue(content.IsCulturePublished(langFr.IsoCode)); + Assert.IsFalse(content.WasCulturePublished(langFr.IsoCode)); //not persisted yet, will be false + Assert.IsTrue(content.IsCulturePublished(langUk.IsoCode)); + Assert.IsFalse(content.WasCulturePublished(langUk.IsoCode)); //not persisted yet, will be false + + var published = ServiceContext.ContentService.SavePublishing(content); + //re-get + content = ServiceContext.ContentService.GetById(content.Id); + Assert.IsTrue(published.Success); + Assert.IsTrue(content.IsCulturePublished(langFr.IsoCode)); + Assert.IsTrue(content.WasCulturePublished(langFr.IsoCode)); + Assert.IsTrue(content.IsCulturePublished(langUk.IsoCode)); + Assert.IsTrue(content.WasCulturePublished(langUk.IsoCode)); + + var unpublished = ServiceContext.ContentService.Unpublish(content, langFr.IsoCode); + Assert.IsTrue(unpublished.Success); + Assert.AreEqual(PublishResultType.SuccessUnpublishCulture, unpublished.Result); + + Assert.IsFalse(content.IsCulturePublished(langFr.IsoCode)); + //this is slightly confusing but this will be false because this method is used for checking the state of the current model, + //but the state on the model has changed with the above Unpublish call + Assert.IsFalse(content.WasCulturePublished(langFr.IsoCode)); + + //re-get + content = ServiceContext.ContentService.GetById(content.Id); + Assert.IsFalse(content.IsCulturePublished(langFr.IsoCode)); + //this is slightly confusing but this will be false because this method is used for checking the state of a current model, + //but we've re-fetched from the database + Assert.IsFalse(content.WasCulturePublished(langFr.IsoCode)); + Assert.IsTrue(content.IsCulturePublished(langUk.IsoCode)); + Assert.IsTrue(content.WasCulturePublished(langUk.IsoCode)); + + } + + [Test] + public void Can_Publish_Content_Variation_And_Detect_Changed_Cultures() + { + // Arrange + + var langGB = new Language("en-GB") { IsDefault = true }; + var langFr = new Language("fr-FR"); + + ServiceContext.LocalizationService.Save(langFr); + ServiceContext.LocalizationService.Save(langGB); + + var contentType = MockedContentTypes.CreateBasicContentType(); + contentType.Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(contentType); + + IContent content = new Content("content", -1, contentType); + content.SetCultureName("content-fr", langFr.IsoCode); + content.PublishCulture(langFr.IsoCode); + var published = ServiceContext.ContentService.SavePublishing(content); + //audit log will only show that french was published + var lastLog = ServiceContext.AuditService.GetLogs(content.Id).Last(); + Assert.AreEqual($"Published languages: French (France)", lastLog.Comment); + + //re-get + content = ServiceContext.ContentService.GetById(content.Id); + content.SetCultureName("content-en", langGB.IsoCode); + content.PublishCulture(langGB.IsoCode); + published = ServiceContext.ContentService.SavePublishing(content); + //audit log will only show that english was published + lastLog = ServiceContext.AuditService.GetLogs(content.Id).Last(); + Assert.AreEqual($"Published languages: English (United Kingdom)", lastLog.Comment); + } + + [Test] + public void Can_Unpublish_Content_Variation_And_Detect_Changed_Cultures() + { + // Arrange + + var langGB = new Language("en-GB") { IsDefault = true, IsMandatory = true }; + var langFr = new Language("fr-FR"); + + ServiceContext.LocalizationService.Save(langFr); + ServiceContext.LocalizationService.Save(langGB); + + var contentType = MockedContentTypes.CreateBasicContentType(); + contentType.Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(contentType); + + IContent content = new Content("content", -1, contentType); + content.SetCultureName("content-fr", langFr.IsoCode); + content.SetCultureName("content-gb", langGB.IsoCode); + content.PublishCulture(langGB.IsoCode); + content.PublishCulture(langFr.IsoCode); + var published = ServiceContext.ContentService.SavePublishing(content); + Assert.IsTrue(published.Success); + + //re-get + content = ServiceContext.ContentService.GetById(content.Id); + content.UnpublishCulture(langFr.IsoCode); //unpublish non-mandatory lang + var unpublished = ServiceContext.ContentService.SavePublishing(content); + //audit log will only show that french was unpublished + var lastLog = ServiceContext.AuditService.GetLogs(content.Id).Last(); + Assert.AreEqual($"Unpublished languages: French (France)", lastLog.Comment); + + //re-get + content = ServiceContext.ContentService.GetById(content.Id); + content.SetCultureName("content-en", langGB.IsoCode); + content.UnpublishCulture(langGB.IsoCode); //unpublish mandatory lang + unpublished = ServiceContext.ContentService.SavePublishing(content); + //audit log will only show that english was published + var logs = ServiceContext.AuditService.GetLogs(content.Id).ToList(); + Assert.AreEqual($"Unpublished languages: English (United Kingdom)", logs[logs.Count - 2].Comment); + Assert.AreEqual($"Unpublished (mandatory language unpublished)", logs[logs.Count - 1].Comment); + } + [Test] public void Can_Publish_Content_1() { @@ -1243,7 +1439,7 @@ namespace Umbraco.Tests.Services // Act content.PublishCulture(); - var published = contentService.SavePublishing(content, 0); + var published = contentService.SavePublishing(content, Constants.Security.SuperUserId); // Assert Assert.That(published.Success, Is.True); @@ -1298,7 +1494,7 @@ namespace Umbraco.Tests.Services content.Name = "foo"; content.PublishCulture(); - var published = contentService.SavePublishing(content, 0); + var published = contentService.SavePublishing(content, Constants.Security.SuperUserId); Assert.That(published.Success, Is.True); Assert.That(content.Published, Is.True); @@ -1355,10 +1551,45 @@ namespace Umbraco.Tests.Services // because it did not have a published version at all var contentPublished = contentService.SaveAndPublish(content); Assert.IsFalse(contentPublished.Success); - Assert.AreEqual(PublishResultType.FailedContentInvalid, contentPublished.Result); + Assert.AreEqual(PublishResultType.FailedPublishContentInvalid, contentPublished.Result); Assert.IsFalse(content.Published); } + [Test] + public void Can_Publish_And_Unpublish_Cultures_In_Single_Operation() + { + var langFr = new Language("fr"); + var langDa = new Language("da"); + ServiceContext.LocalizationService.Save(langFr); + ServiceContext.LocalizationService.Save(langDa); + + var ct = MockedContentTypes.CreateBasicContentType(); + ct.Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(ct); + + IContent content = MockedContent.CreateBasicContent(ct); + content.SetCultureName("name-fr", langFr.IsoCode); + content.SetCultureName("name-da", langDa.IsoCode); + + content.PublishCulture(langFr.IsoCode); + var result = ServiceContext.ContentService.SavePublishing(content); + Assert.IsTrue(result.Success); + content = ServiceContext.ContentService.GetById(content.Id); + Assert.IsTrue(content.IsCulturePublished(langFr.IsoCode)); + Assert.IsFalse(content.IsCulturePublished(langDa.IsoCode)); + + content.UnpublishCulture(langFr.IsoCode); + content.PublishCulture(langDa.IsoCode); + + result = ServiceContext.ContentService.SavePublishing(content); + Assert.IsTrue(result.Success); + Assert.AreEqual(PublishResultType.SuccessMixedCulture, result.Result); + + content = ServiceContext.ContentService.GetById(content.Id); + Assert.IsFalse(content.IsCulturePublished(langFr.IsoCode)); + Assert.IsTrue(content.IsCulturePublished(langDa.IsoCode)); + } + // documents: an enumeration of documents, in tree order // map: applies (if needed) PublishValue, returns a value indicating whether to proceed with the branch private IEnumerable MapPublishValues(IEnumerable documents, Func map) @@ -1389,8 +1620,16 @@ namespace Umbraco.Tests.Services var parent = contentService.GetById(parentId); Console.WriteLine(" " + parent.Id); - foreach (var x in contentService.GetDescendants(parent)) - Console.WriteLine(" ".Substring(0, x.Level) + x.Id); + const int pageSize = 500; + var page = 0; + var total = long.MaxValue; + while(page * pageSize < total) + { + var descendants = contentService.GetPagedDescendants(parent.Id, page++, pageSize, out total); + foreach (var x in descendants) + Console.WriteLine(" ".Substring(0, x.Level) + x.Id); + } + Console.WriteLine(); // publish parent & its branch @@ -1405,7 +1644,7 @@ namespace Umbraco.Tests.Services Assert.IsTrue(parentPublished.All(x => x.Success)); Assert.IsTrue(parent.Published); - var children = contentService.GetChildren(parentId); + var children = contentService.GetPagedChildren(parentId, 0, 500, out var totalChildren); //we only want the first so page size, etc.. is abitrary // children are published including ... that was released 5 mins ago Assert.IsTrue(children.First(x => x.Id == NodeDto.NodeIdSeed + 4).Published); @@ -1417,21 +1656,41 @@ namespace Umbraco.Tests.Services // Arrange var contentService = ServiceContext.ContentService; var content = contentService.GetById(NodeDto.NodeIdSeed + 4); //This Content expired 5min ago - content.ExpireDate = DateTime.Now.AddMinutes(-5); + content.ContentSchedule.Add(null, DateTime.Now.AddMinutes(-5)); contentService.Save(content); var parent = contentService.GetById(NodeDto.NodeIdSeed + 2); parent.PublishCulture(); - var parentPublished = contentService.SavePublishing(parent, 0);//Publish root Home node to enable publishing of 'NodeDto.NodeIdSeed + 3' + var parentPublished = contentService.SavePublishing(parent, Constants.Security.SuperUserId);//Publish root Home node to enable publishing of 'NodeDto.NodeIdSeed + 3' // Act content.PublishCulture(); - var published = contentService.SavePublishing(content, 0); + var published = contentService.SavePublishing(content, Constants.Security.SuperUserId); // Assert Assert.That(parentPublished.Success, Is.True); Assert.That(published.Success, Is.False); Assert.That(content.Published, Is.False); + Assert.AreEqual(PublishResultType.FailedPublishHasExpired, published.Result); + } + + [Test] + public void Cannot_Publish_Expired_Culture() + { + var contentType = MockedContentTypes.CreateBasicContentType(); + contentType.Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(contentType); + + var content = MockedContent.CreateBasicContent(contentType); + content.SetCultureName("Hello", "en-US"); + content.ContentSchedule.Add("en-US", null, DateTime.Now.AddMinutes(-5)); + ServiceContext.ContentService.Save(content); + + var published = ServiceContext.ContentService.SaveAndPublish(content, "en-US", Constants.Security.SuperUserId); + + Assert.IsFalse(published.Success); + Assert.AreEqual(PublishResultType.FailedPublishCultureHasExpired, published.Result); + Assert.That(content.Published, Is.False); } [Test] @@ -1440,21 +1699,41 @@ namespace Umbraco.Tests.Services // Arrange var contentService = ServiceContext.ContentService; var content = contentService.GetById(NodeDto.NodeIdSeed + 3); - content.ReleaseDate = DateTime.Now.AddHours(2); - contentService.Save(content, 0); + content.ContentSchedule.Add(DateTime.Now.AddHours(2), null); + contentService.Save(content, Constants.Security.SuperUserId); var parent = contentService.GetById(NodeDto.NodeIdSeed + 2); parent.PublishCulture(); - var parentPublished = contentService.SavePublishing(parent, 0);//Publish root Home node to enable publishing of 'NodeDto.NodeIdSeed + 3' + var parentPublished = contentService.SavePublishing(parent, Constants.Security.SuperUserId);//Publish root Home node to enable publishing of 'NodeDto.NodeIdSeed + 3' // Act content.PublishCulture(); - var published = contentService.SavePublishing(content, 0); + var published = contentService.SavePublishing(content, Constants.Security.SuperUserId); // Assert Assert.That(parentPublished.Success, Is.True); Assert.That(published.Success, Is.False); Assert.That(content.Published, Is.False); + Assert.AreEqual(PublishResultType.FailedPublishAwaitingRelease, published.Result); + } + + [Test] + public void Cannot_Publish_Culture_Awaiting_Release() + { + var contentType = MockedContentTypes.CreateBasicContentType(); + contentType.Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(contentType); + + var content = MockedContent.CreateBasicContent(contentType); + content.SetCultureName("Hello", "en-US"); + content.ContentSchedule.Add("en-US", DateTime.Now.AddHours(2), null); + ServiceContext.ContentService.Save(content); + + var published = ServiceContext.ContentService.SaveAndPublish(content, "en-US", Constants.Security.SuperUserId); + + Assert.IsFalse(published.Success); + Assert.AreEqual(PublishResultType.FailedPublishCultureAwaitingRelease, published.Result); + Assert.That(content.Published, Is.False); } [Test] @@ -1462,8 +1741,8 @@ namespace Umbraco.Tests.Services { // Arrange var contentService = ServiceContext.ContentService; - var content = contentService.Create("Subpage with Unpublisehed Parent", NodeDto.NodeIdSeed + 2, "umbTextpage", 0); - contentService.Save(content, 0); + var content = contentService.Create("Subpage with Unpublished Parent", NodeDto.NodeIdSeed + 2, "umbTextpage", Constants.Security.SuperUserId); + contentService.Save(content, Constants.Security.SuperUserId); // Act var published = contentService.SaveAndPublishBranch(content, true); @@ -1482,7 +1761,7 @@ namespace Umbraco.Tests.Services // Act content.PublishCulture(); - var published = contentService.SavePublishing(content, 0); + var published = contentService.SavePublishing(content, Constants.Security.SuperUserId); // Assert Assert.That(published.Success, Is.False); @@ -1495,12 +1774,12 @@ namespace Umbraco.Tests.Services { // Arrange var contentService = ServiceContext.ContentService; - var content = contentService.Create("Home US", - 1, "umbTextpage", 0); + var content = contentService.Create("Home US", - 1, "umbTextpage", Constants.Security.SuperUserId); content.SetValue("author", "Barack Obama"); // Act content.PublishCulture(); - var published = contentService.SavePublishing(content, 0); + var published = contentService.SavePublishing(content, Constants.Security.SuperUserId); // Assert Assert.That(content.HasIdentity, Is.True); @@ -1520,19 +1799,19 @@ namespace Umbraco.Tests.Services { // Arrange var contentService = ServiceContext.ContentService; - var content = contentService.Create("Home US", -1, "umbTextpage", 0); + var content = contentService.Create("Home US", -1, "umbTextpage", Constants.Security.SuperUserId); content.SetValue("author", "Barack Obama"); // Act content.PublishCulture(); - var published = contentService.SavePublishing(content, 0); - var childContent = contentService.Create("Child", content.Id, "umbTextpage", 0); + var published = contentService.SavePublishing(content, Constants.Security.SuperUserId); + var childContent = contentService.Create("Child", content.Id, "umbTextpage", Constants.Security.SuperUserId); // Reset all identity properties childContent.Id = 0; childContent.Path = null; ((Content)childContent).ResetIdentity(); childContent.PublishCulture(); - var childPublished = contentService.SavePublishing(childContent, 0); + var childPublished = contentService.SavePublishing(childContent, Constants.Security.SuperUserId); // Assert Assert.That(content.HasIdentity, Is.True); @@ -1594,11 +1873,11 @@ namespace Umbraco.Tests.Services { // Arrange var contentService = ServiceContext.ContentService; - var content = contentService.Create("Home US", - 1, "umbTextpage", 0); + var content = contentService.Create("Home US", - 1, "umbTextpage", Constants.Security.SuperUserId); content.SetValue("author", "Barack Obama"); // Act - contentService.Save(content, 0); + contentService.Save(content, Constants.Security.SuperUserId); // Assert Assert.That(content.HasIdentity, Is.True); @@ -1617,7 +1896,7 @@ namespace Umbraco.Tests.Services var list = new List {subpage, subpage2}; // Act - contentService.Save(list, 0); + contentService.Save(list, Constants.Security.SuperUserId); // Assert Assert.That(list.Any(x => !x.HasIdentity), Is.False); @@ -1631,7 +1910,7 @@ namespace Umbraco.Tests.Services var hierarchy = CreateContentHierarchy().ToList(); // Act - contentService.Save(hierarchy, 0); + contentService.Save(hierarchy, Constants.Security.SuperUserId); Assert.That(hierarchy.Any(), Is.True); Assert.That(hierarchy.Any(x => x.HasIdentity == false), Is.False); @@ -1651,7 +1930,7 @@ namespace Umbraco.Tests.Services // Act contentService.DeleteOfType(contentType.Id); var rootContent = contentService.GetRootContent(); - var contents = contentService.GetByType(contentType.Id); + var contents = contentService.GetPagedOfType(contentType.Id, 0, int.MaxValue, out var _, null); // Assert Assert.That(rootContent.Any(), Is.False); @@ -1666,7 +1945,7 @@ namespace Umbraco.Tests.Services var content = contentService.GetById(NodeDto.NodeIdSeed + 4); // Act - contentService.Delete(content, 0); + contentService.Delete(content, Constants.Security.SuperUserId); var deleted = contentService.GetById(NodeDto.NodeIdSeed + 4); // Assert @@ -1681,7 +1960,7 @@ namespace Umbraco.Tests.Services var content = contentService.GetById(NodeDto.NodeIdSeed + 3); // Act - contentService.MoveToRecycleBin(content, 0); + contentService.MoveToRecycleBin(content, Constants.Security.SuperUserId); // Assert Assert.That(content.ParentId, Is.EqualTo(-20)); @@ -1695,18 +1974,28 @@ namespace Umbraco.Tests.Services var contentType = ServiceContext.ContentTypeService.Get("umbTextpage"); var subsubpage = MockedContent.CreateSimpleContent(contentType, "Text Page 3", NodeDto.NodeIdSeed + 3); - contentService.Save(subsubpage, 0); + contentService.Save(subsubpage, Constants.Security.SuperUserId); var content = contentService.GetById(NodeDto.NodeIdSeed + 2); - var descendants = contentService.GetDescendants(content).ToList(); + const int pageSize = 500; + var page = 0; + var total = long.MaxValue; + var descendants = new List(); + while(page * pageSize < total) + descendants.AddRange(contentService.GetPagedDescendants(content.Id, page++, pageSize, out total)); + Assert.AreNotEqual(-20, content.ParentId); Assert.IsFalse(content.Trashed); Assert.AreEqual(3, descendants.Count); Assert.IsFalse(descendants.Any(x => x.Path.StartsWith("-1,-20,"))); Assert.IsFalse(descendants.Any(x => x.Trashed)); - contentService.MoveToRecycleBin(content, 0); - descendants = contentService.GetDescendants(content).ToList(); + contentService.MoveToRecycleBin(content, Constants.Security.SuperUserId); + + descendants.Clear(); + page = 0; + while (page * pageSize < total) + descendants.AddRange(contentService.GetPagedDescendants(content.Id, page++, pageSize, out total)); Assert.AreEqual(-20, content.ParentId); Assert.IsTrue(content.Trashed); @@ -1715,7 +2004,7 @@ namespace Umbraco.Tests.Services Assert.True(descendants.All(x => x.Trashed)); contentService.EmptyRecycleBin(); - var trashed = contentService.GetContentInRecycleBin(); + var trashed = contentService.GetPagedContentInRecycleBin(0, int.MaxValue, out var _).ToList(); Assert.IsEmpty(trashed); } @@ -1727,7 +2016,7 @@ namespace Umbraco.Tests.Services // Act contentService.EmptyRecycleBin(); - var contents = contentService.GetContentInRecycleBin(); + var contents = contentService.GetPagedContentInRecycleBin(0, int.MaxValue, out var _).ToList(); // Assert Assert.That(contents.Any(), Is.False); @@ -1796,8 +2085,13 @@ namespace Umbraco.Tests.Services ServiceContext.ContentService.Save(childPage3); //Verify that the children have the inherited permissions - var descendants = ServiceContext.ContentService.GetDescendants(parentPage).ToArray(); - Assert.AreEqual(3, descendants.Length); + var descendants = new List(); + const int pageSize = 500; + var page = 0; + var total = long.MaxValue; + while(page * pageSize < total) + descendants.AddRange(ServiceContext.ContentService.GetPagedDescendants(parentPage.Id, page++, pageSize, out total)); + Assert.AreEqual(3, descendants.Count); foreach (var descendant in descendants) { @@ -1815,8 +2109,11 @@ namespace Umbraco.Tests.Services //Now copy, what should happen is the child pages will now have permissions inherited from the new parent var copy = ServiceContext.ContentService.Copy(childPage1, parentPage2.Id, false, true); - descendants = ServiceContext.ContentService.GetDescendants(parentPage2).ToArray(); - Assert.AreEqual(3, descendants.Length); + descendants.Clear(); + page = 0; + while (page * pageSize < total) + descendants.AddRange(ServiceContext.ContentService.GetPagedDescendants(parentPage2.Id, page++, pageSize, out total)); + Assert.AreEqual(3, descendants.Count); foreach (var descendant in descendants) { @@ -1842,7 +2139,7 @@ namespace Umbraco.Tests.Services // * multiple versions var contentType = MockedContentTypes.CreateAllTypesContentType("test", "test"); - ServiceContext.ContentTypeService.Save(contentType, 0); + ServiceContext.ContentTypeService.Save(contentType, Constants.Security.SuperUserId); object obj = new @@ -1852,13 +2149,13 @@ namespace Umbraco.Tests.Services var content1 = MockedContent.CreateBasicContent(contentType); content1.PropertyValues(obj); content1.ResetDirtyProperties(false); - ServiceContext.ContentService.Save(content1, 0); + ServiceContext.ContentService.Save(content1, Constants.Security.SuperUserId); content1.PublishCulture(); Assert.IsTrue(ServiceContext.ContentService.SavePublishing(content1, 0).Success); var content2 = MockedContent.CreateBasicContent(contentType); content2.PropertyValues(obj); content2.ResetDirtyProperties(false); - ServiceContext.ContentService.Save(content2, 0); + ServiceContext.ContentService.Save(content2, Constants.Security.SuperUserId); content2.PublishCulture(); Assert.IsTrue(ServiceContext.ContentService.SavePublishing(content2, 0).Success); @@ -1897,7 +2194,7 @@ namespace Umbraco.Tests.Services // Act ServiceContext.ContentService.MoveToRecycleBin(content1); ServiceContext.ContentService.EmptyRecycleBin(); - var contents = ServiceContext.ContentService.GetContentInRecycleBin(); + var contents = ServiceContext.ContentService.GetPagedContentInRecycleBin(0, int.MaxValue, out var _).ToList(); // Assert Assert.That(contents.Any(), Is.False); @@ -1927,7 +2224,7 @@ namespace Umbraco.Tests.Services var temp = contentService.GetById(NodeDto.NodeIdSeed + 4); // Act - var copy = contentService.Copy(temp, temp.ParentId, false, 0); + var copy = contentService.Copy(temp, temp.ParentId, false, Constants.Security.SuperUserId); var content = contentService.GetById(NodeDto.NodeIdSeed + 4); // Assert @@ -1948,20 +2245,20 @@ namespace Umbraco.Tests.Services var contentService = ServiceContext.ContentService; var temp = contentService.GetById(NodeDto.NodeIdSeed + 2); Assert.AreEqual("Home", temp.Name); - Assert.AreEqual(2, temp.Children(contentService).Count()); + Assert.AreEqual(2, contentService.CountChildren(temp.Id)); // Act - var copy = contentService.Copy(temp, temp.ParentId, false, true, 0); + var copy = contentService.Copy(temp, temp.ParentId, false, true, Constants.Security.SuperUserId); var content = contentService.GetById(NodeDto.NodeIdSeed + 2); // Assert Assert.That(copy, Is.Not.Null); Assert.That(copy.Id, Is.Not.EqualTo(content.Id)); Assert.AreNotSame(content, copy); - Assert.AreEqual(2, copy.Children(contentService).Count()); + Assert.AreEqual(2, contentService.CountChildren(copy.Id)); var child = contentService.GetById(NodeDto.NodeIdSeed + 3); - var childCopy = copy.Children(contentService).First(); + var childCopy = contentService.GetPagedChildren(copy.Id, 0, 500, out var total).First(); Assert.AreEqual(childCopy.Name, child.Name); Assert.AreNotEqual(childCopy.Id, child.Id); Assert.AreNotEqual(childCopy.Key, child.Key); @@ -1974,17 +2271,17 @@ namespace Umbraco.Tests.Services var contentService = ServiceContext.ContentService; var temp = contentService.GetById(NodeDto.NodeIdSeed + 2); Assert.AreEqual("Home", temp.Name); - Assert.AreEqual(2, temp.Children(contentService).Count()); + Assert.AreEqual(2, contentService.CountChildren(temp.Id)); // Act - var copy = contentService.Copy(temp, temp.ParentId, false, false, 0); + var copy = contentService.Copy(temp, temp.ParentId, false, false, Constants.Security.SuperUserId); var content = contentService.GetById(NodeDto.NodeIdSeed + 2); // Assert Assert.That(copy, Is.Not.Null); Assert.That(copy.Id, Is.Not.EqualTo(content.Id)); Assert.AreNotSame(content, copy); - Assert.AreEqual(0, copy.Children(contentService).Count()); + Assert.AreEqual(0, contentService.CountChildren(copy.Id)); } [Test] @@ -2111,7 +2408,7 @@ namespace Umbraco.Tests.Services var rollback = contentService.GetById(NodeDto.NodeIdSeed + 4); var rollto = contentService.GetVersion(version1); rollback.CopyFrom(rollto); - rollback.Name = rollto.Name; // must do it explicitely + rollback.Name = rollto.Name; // must do it explicitly contentService.Save(rollback); Assert.IsNotNull(rollback); @@ -2161,6 +2458,210 @@ namespace Umbraco.Tests.Services Assert.AreEqual("Jane Doe", content.GetValue("author")); } + [Test] + public void Can_Rollback_Version_On_Multilingual() + { + var langFr = new Language("fr"); + var langDa = new Language("da"); + ServiceContext.LocalizationService.Save(langFr); + ServiceContext.LocalizationService.Save(langDa); + + var contentType = MockedContentTypes.CreateSimpleContentType("multi", "Multi"); + contentType.Key = new Guid("45FF9A70-9C5F-448D-A476-DCD23566BBF8"); + contentType.Variations = ContentVariation.Culture; + var p1 = contentType.PropertyTypes.First(); + p1.Variations = ContentVariation.Culture; + ServiceContext.FileService.SaveTemplate(contentType.DefaultTemplate); // else, FK violation on contentType! + ServiceContext.ContentTypeService.Save(contentType); + + var page = new Content("Page", -1, contentType) + { + Level = 1, + SortOrder = 1, + CreatorId = 0, + WriterId = 0, + Key = new Guid("D7B84CC9-14AE-4D92-A042-023767AD3304") + }; + + page.SetCultureName("fr1", "fr"); + page.SetCultureName("da1", "da"); + ServiceContext.ContentService.Save(page); + var versionId0 = page.VersionId; + + page.SetValue(p1.Alias, "v1fr", "fr"); + page.SetValue(p1.Alias, "v1da", "da"); + ServiceContext.ContentService.SaveAndPublish(page); + var versionId1 = page.VersionId; + + Thread.Sleep(250); + + page.SetCultureName("fr2", "fr"); + page.SetValue(p1.Alias, "v2fr", "fr"); + ServiceContext.ContentService.SaveAndPublish(page, "fr"); + var versionId2 = page.VersionId; + + Thread.Sleep(250); + + page.SetCultureName("da2", "da"); + page.SetValue(p1.Alias, "v2da", "da"); + ServiceContext.ContentService.SaveAndPublish(page, "da"); + var versionId3 = page.VersionId; + + Thread.Sleep(250); + + page.SetCultureName("fr3", "fr"); + page.SetCultureName("da3", "da"); + page.SetValue(p1.Alias, "v3fr", "fr"); + page.SetValue(p1.Alias, "v3da", "da"); + ServiceContext.ContentService.SaveAndPublish(page); + var versionId4 = page.VersionId; + + // now get all versions + + var versions = ServiceContext.ContentService.GetVersions(page.Id).ToArray(); + + Assert.AreEqual(5, versions.Length); + + // current version + Assert.AreEqual(versionId4, versions[0].VersionId); + Assert.AreEqual(versionId3, versions[0].PublishedVersionId); + // published version + Assert.AreEqual(versionId3, versions[1].VersionId); + Assert.AreEqual(versionId3, versions[1].PublishedVersionId); + // previous version + Assert.AreEqual(versionId2, versions[2].VersionId); + Assert.AreEqual(versionId3, versions[2].PublishedVersionId); + // previous version + Assert.AreEqual(versionId1, versions[3].VersionId); + Assert.AreEqual(versionId3, versions[3].PublishedVersionId); + // previous version + Assert.AreEqual(versionId0, versions[4].VersionId); + Assert.AreEqual(versionId3, versions[4].PublishedVersionId); + + Assert.AreEqual("fr3", versions[4].GetPublishName("fr")); + Assert.AreEqual("fr3", versions[3].GetPublishName("fr")); + Assert.AreEqual("fr3", versions[2].GetPublishName("fr")); + Assert.AreEqual("fr3", versions[1].GetPublishName("fr")); + Assert.AreEqual("fr3", versions[0].GetPublishName("fr")); + + Assert.AreEqual("fr1", versions[4].GetCultureName("fr")); + Assert.AreEqual("fr2", versions[3].GetCultureName("fr")); + Assert.AreEqual("fr2", versions[2].GetCultureName("fr")); + Assert.AreEqual("fr3", versions[1].GetCultureName("fr")); + Assert.AreEqual("fr3", versions[0].GetCultureName("fr")); + + Assert.AreEqual("da3", versions[4].GetPublishName("da")); + Assert.AreEqual("da3", versions[3].GetPublishName("da")); + Assert.AreEqual("da3", versions[2].GetPublishName("da")); + Assert.AreEqual("da3", versions[1].GetPublishName("da")); + Assert.AreEqual("da3", versions[0].GetPublishName("da")); + + Assert.AreEqual("da1", versions[4].GetCultureName("da")); + Assert.AreEqual("da1", versions[3].GetCultureName("da")); + Assert.AreEqual("da2", versions[2].GetCultureName("da")); + Assert.AreEqual("da3", versions[1].GetCultureName("da")); + Assert.AreEqual("da3", versions[0].GetCultureName("da")); + + // all versions have the same publish infos + for (var i = 0; i < 5; i++) + { + Assert.AreEqual(versions[0].PublishDate, versions[i].PublishDate); + Assert.AreEqual(versions[0].GetPublishDate("fr"), versions[i].GetPublishDate("fr")); + Assert.AreEqual(versions[0].GetPublishDate("da"), versions[i].GetPublishDate("da")); + } + + for (var i = 0; i < 5; i++) + { + Console.Write("[{0}] ", i); + Console.WriteLine(versions[i].UpdateDate.ToString("O").Substring(11)); + Console.WriteLine(" fr: {0}", versions[i].GetUpdateDate("fr")?.ToString("O").Substring(11)); + Console.WriteLine(" da: {0}", versions[i].GetUpdateDate("da")?.ToString("O").Substring(11)); + } + Console.WriteLine("-"); + + // for all previous versions, UpdateDate is the published date + + Assert.AreEqual(versions[4].UpdateDate, versions[4].GetUpdateDate("fr")); + Assert.AreEqual(versions[4].UpdateDate, versions[4].GetUpdateDate("da")); + + Assert.AreEqual(versions[3].UpdateDate, versions[3].GetUpdateDate("fr")); + Assert.AreEqual(versions[4].UpdateDate, versions[3].GetUpdateDate("da")); + + Assert.AreEqual(versions[3].UpdateDate, versions[2].GetUpdateDate("fr")); + Assert.AreEqual(versions[2].UpdateDate, versions[2].GetUpdateDate("da")); + + // for the published version, UpdateDate is the published date + + Assert.AreEqual(versions[1].UpdateDate, versions[1].GetUpdateDate("fr")); + Assert.AreEqual(versions[1].UpdateDate, versions[1].GetUpdateDate("da")); + Assert.AreEqual(versions[1].PublishDate, versions[1].UpdateDate); + + // for the current version, things are different + // UpdateDate is the date it was last saved + + Assert.AreEqual(versions[0].UpdateDate, versions[0].GetUpdateDate("fr")); + Assert.AreEqual(versions[0].UpdateDate, versions[0].GetUpdateDate("da")); + + // so if we save again... + + page.SetCultureName("fr4", "fr"); + //page.SetCultureName("da4", "da"); + page.SetValue(p1.Alias, "v4fr", "fr"); + page.SetValue(p1.Alias, "v4da", "da"); + ServiceContext.ContentService.Save(page); + var versionId5 = page.VersionId; + + versions = ServiceContext.ContentService.GetVersions(page.Id).ToArray(); + + // we just update the current version + Assert.AreEqual(5, versions.Length); + Assert.AreEqual(versionId4, versionId5); + + for (var i = 0; i < 5; i++) + { + Console.Write("[{0}] ", i); + Console.WriteLine(versions[i].UpdateDate.ToString("O").Substring(11)); + Console.WriteLine(" fr: {0}", versions[i].GetUpdateDate("fr")?.ToString("O").Substring(11)); + Console.WriteLine(" da: {0}", versions[i].GetUpdateDate("da")?.ToString("O").Substring(11)); + } + Console.WriteLine("-"); + + var versionsSlim = ServiceContext.ContentService.GetVersionsSlim(page.Id, 0, 50).ToArray(); + Assert.AreEqual(5, versionsSlim.Length); + + for (var i = 0; i < 5; i++) + { + Console.Write("[{0}] ", i); + Console.WriteLine(versionsSlim[i].UpdateDate.ToString("O").Substring(11)); + Console.WriteLine(" fr: {0}", versionsSlim[i].GetUpdateDate("fr")?.ToString("O").Substring(11)); + Console.WriteLine(" da: {0}", versionsSlim[i].GetUpdateDate("da")?.ToString("O").Substring(11)); + } + Console.WriteLine("-"); + + // what we do in the controller to get rollback versions + var versionsSlimFr = versionsSlim.Where(x => x.UpdateDate == x.GetUpdateDate("fr")).ToArray(); + Assert.AreEqual(3, versionsSlimFr.Length); + + // alas, at the moment we do *not* properly track 'dirty' for cultures, meaning + // that we cannot synchronize dates the way we do with publish dates - and so this + // would fail - the version UpdateDate is greater than the cultures'. + //Assert.AreEqual(versions[0].UpdateDate, versions[0].GetUpdateDate("fr")); + //Assert.AreEqual(versions[0].UpdateDate, versions[0].GetUpdateDate("da")); + + // now roll french back to its very first version + page.CopyFrom(versions[4], "fr"); // only the pure FR values + page.CopyFrom(versions[4], null); // so, must explicitly do the INVARIANT values too + page.SetCultureName(versions[4].GetPublishName("fr"), "fr"); + ServiceContext.ContentService.Save(page); + + // and voila, rolled back! + Assert.AreEqual(versions[4].GetPublishName("fr"), page.GetCultureName("fr")); + Assert.AreEqual(versions[4].GetValue(p1.Alias, "fr"), page.GetValue(p1.Alias, "fr")); + + // note that rolling back invariant values means we also rolled back... DA... at least partially + // bah? + } + [Test] public void Can_Save_Lazy_Content() { @@ -2242,7 +2743,7 @@ namespace Umbraco.Tests.Services var version = content.VersionId; // Act - contentService.DeleteVersion(NodeDto.NodeIdSeed + 5, version, true, 0); + contentService.DeleteVersion(NodeDto.NodeIdSeed + 5, version, true, Constants.Security.SuperUserId); var sut = contentService.GetById(NodeDto.NodeIdSeed + 5); // Assert @@ -2253,7 +2754,7 @@ namespace Umbraco.Tests.Services public void Ensure_Content_Xml_Created() { var contentService = ServiceContext.ContentService; - var content = contentService.Create("Home US", -1, "umbTextpage", 0); + var content = contentService.Create("Home US", -1, "umbTextpage", Constants.Security.SuperUserId); content.SetValue("author", "Barack Obama"); contentService.Save(content); @@ -2275,7 +2776,7 @@ namespace Umbraco.Tests.Services public void Ensure_Preview_Xml_Created() { var contentService = ServiceContext.ContentService; - var content = contentService.Create("Home US", -1, "umbTextpage", 0); + var content = contentService.Create("Home US", -1, "umbTextpage", Constants.Security.SuperUserId); content.SetValue("author", "Barack Obama"); contentService.Save(content); @@ -2489,7 +2990,7 @@ namespace Umbraco.Tests.Services { var languageService = ServiceContext.LocalizationService; - var langUk = new Language("en-UK") { IsDefault = true }; + var langUk = new Language("en-GB") { IsDefault = true }; var langFr = new Language("fr-FR"); languageService.Save(langFr); @@ -2524,7 +3025,7 @@ namespace Umbraco.Tests.Services { var languageService = ServiceContext.LocalizationService; - var langUk = new Language("en-UK") { IsDefault = true }; + var langUk = new Language("en-GB") { IsDefault = true }; var langFr = new Language("fr-FR"); languageService.Save(langFr); @@ -2556,6 +3057,107 @@ namespace Umbraco.Tests.Services } } + [Test] + public void Can_Get_Paged_Children_WithFilterAndOrder() + { + var languageService = ServiceContext.LocalizationService; + + var langUk = new Language("en-GB") { IsDefault = true }; + var langFr = new Language("fr-FR"); + var langDa = new Language("da-DK"); + + languageService.Save(langFr); + languageService.Save(langUk); + languageService.Save(langDa); + + var contentTypeService = ServiceContext.ContentTypeService; + + var contentType = contentTypeService.Get("umbTextpage"); + contentType.Variations = ContentVariation.Culture; + contentTypeService.Save(contentType); + + var contentService = ServiceContext.ContentService; + + var o = new[] { 2, 1, 3, 0, 4 }; // randomly different + for (var i = 0; i < 5; i++) + { + var contentA = new Content(null, -1, contentType); + contentA.SetCultureName("contentA" + i + "uk", langUk.IsoCode); + contentA.SetCultureName("contentA" + o[i] + "fr", langFr.IsoCode); + contentA.SetCultureName("contentX" + i + "da", langDa.IsoCode); + contentService.Save(contentA); + + var contentB = new Content(null, -1, contentType); + contentB.SetCultureName("contentB" + i + "uk", langUk.IsoCode); + contentB.SetCultureName("contentB" + o[i] + "fr", langFr.IsoCode); + contentB.SetCultureName("contentX" + i + "da", langDa.IsoCode); + contentService.Save(contentB); + } + + // get all + var list = contentService.GetPagedChildren(-1, 0, 100, out var total).ToList(); + + Console.WriteLine("ALL"); + WriteList(list); + + // 10 items (there's already a Home content in there...) + Assert.AreEqual(11, total); + Assert.AreEqual(11, list.Count); + + // filter + list = contentService.GetPagedChildren(-1, 0, 100, out total, + SqlContext.Query().Where(x => x.Name.Contains("contentX")), + Ordering.By("name", culture: langFr.IsoCode)).ToList(); + + Assert.AreEqual(0, total); + Assert.AreEqual(0, list.Count); + + // filter + list = contentService.GetPagedChildren(-1, 0, 100, out total, + SqlContext.Query().Where(x => x.Name.Contains("contentX")), + Ordering.By("name", culture: langDa.IsoCode)).ToList(); + + Console.WriteLine("FILTER BY NAME da:'contentX'"); + WriteList(list); + + Assert.AreEqual(10, total); + Assert.AreEqual(10, list.Count); + + // filter + list = contentService.GetPagedChildren(-1, 0, 100, out total, + SqlContext.Query().Where(x => x.Name.Contains("contentA")), + Ordering.By("name", culture: langFr.IsoCode)).ToList(); + + Console.WriteLine("FILTER BY NAME fr:'contentA', ORDER ASC"); + WriteList(list); + + Assert.AreEqual(5, total); + Assert.AreEqual(5, list.Count); + + for (var i = 0; i < 5; i++) + Assert.AreEqual("contentA" + i + "fr", list[i].GetCultureName(langFr.IsoCode)); + + list = contentService.GetPagedChildren(-1, 0, 100, out total, + SqlContext.Query().Where(x => x.Name.Contains("contentA")), + Ordering.By("name", direction: Direction.Descending, culture: langFr.IsoCode)).ToList(); + + Console.WriteLine("FILTER BY NAME fr:'contentA', ORDER DESC"); + WriteList(list); + + Assert.AreEqual(5, total); + Assert.AreEqual(5, list.Count); + + for (var i = 0; i < 5; i++) + Assert.AreEqual("contentA" + (4-i) + "fr", list[i].GetCultureName(langFr.IsoCode)); + } + + private void WriteList(List list) + { + foreach (var content in list) + Console.WriteLine("[{0}] {1} {2} {3} {4}", content.Id, content.Name, content.GetCultureName("en-GB"), content.GetCultureName("fr-FR"), content.GetCultureName("da-DK")); + Console.WriteLine("-"); + } + [Test] public void Can_SaveRead_Variations() { @@ -2564,7 +3166,7 @@ namespace Umbraco.Tests.Services //var langFr = new Language("fr-FR") { IsDefaultVariantLanguage = true }; var langXx = new Language("pt-PT") { IsDefault = true }; var langFr = new Language("fr-FR"); - var langUk = new Language("en-UK"); + var langUk = new Language("en-GB"); var langDe = new Language("de-DE"); languageService.Save(langFr); diff --git a/src/Umbraco.Tests/Services/ContentTypeServiceTests.cs b/src/Umbraco.Tests/Services/ContentTypeServiceTests.cs index f25382d557..f186ae8e83 100644 --- a/src/Umbraco.Tests/Services/ContentTypeServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentTypeServiceTests.cs @@ -1,10 +1,8 @@ -using System.Runtime.Remoting; -using NUnit.Framework; +using NUnit.Framework; using System; using System.Collections.Generic; using System.Linq; using System.Threading; -using NPoco; using Umbraco.Core; using Umbraco.Core.Events; using Umbraco.Core.Exceptions; @@ -12,10 +10,9 @@ using Umbraco.Core.Models; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Services; using Umbraco.Core.Services.Implement; -using Umbraco.Tests.TestHelpers; using Umbraco.Tests.TestHelpers.Entities; using Umbraco.Tests.Testing; -using Umbraco.Core.Components; +using Umbraco.Tests.Scoping; namespace Umbraco.Tests.Services { diff --git a/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs b/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs new file mode 100644 index 0000000000..c28d4f7955 --- /dev/null +++ b/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs @@ -0,0 +1,773 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using LightInject; +using Moq; +using NUnit.Framework; +using Umbraco.Core; +using Umbraco.Core.Cache; +using Umbraco.Core.Composing; +using Umbraco.Core.Configuration; +using Umbraco.Core.Models; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.Dtos; +using Umbraco.Core.Persistence.Repositories; +using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Services; +using Umbraco.Core.Sync; +using Umbraco.Tests.Testing; +using Umbraco.Web.PublishedCache; +using Umbraco.Web.PublishedCache.NuCache; +using Umbraco.Web.PublishedCache.NuCache.DataSource; +using Umbraco.Web.Routing; + +namespace Umbraco.Tests.Services +{ + [TestFixture] + [Apartment(ApartmentState.STA)] + [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest, PublishedRepositoryEvents = true, WithApplication = true)] + public class ContentTypeServiceVariantsTests : TestWithSomeContentBase + { + protected override void Compose() + { + base.Compose(); + + // pfew - see note in ScopedNuCacheTests? + Container.RegisterSingleton(); + Container.RegisterSingleton(f => Mock.Of()); + Container.RegisterCollectionBuilder() + .Add(f => f.TryGetInstance().GetCacheRefreshers()); + } + + protected override IPublishedSnapshotService CreatePublishedSnapshotService() + { + var options = new PublishedSnapshotService.Options { IgnoreLocalDb = true }; + var publishedSnapshotAccessor = new UmbracoContextPublishedSnapshotAccessor(Umbraco.Web.Composing.Current.UmbracoContextAccessor); + var runtimeStateMock = new Mock(); + runtimeStateMock.Setup(x => x.Level).Returns(() => RuntimeLevel.Run); + + var contentTypeFactory = new PublishedContentTypeFactory(Mock.Of(), new PropertyValueConverterCollection(Array.Empty()), Mock.Of()); + //var documentRepository = Mock.Of(); + var documentRepository = Container.GetInstance(); + var mediaRepository = Mock.Of(); + var memberRepository = Mock.Of(); + + return new PublishedSnapshotService( + options, + null, + runtimeStateMock.Object, + ServiceContext, + contentTypeFactory, + null, + publishedSnapshotAccessor, + Mock.Of(), + Logger, + ScopeProvider, + documentRepository, mediaRepository, memberRepository, + DefaultCultureAccessor, + new DatabaseDataSource(), + Container.GetInstance(), new SiteDomainHelper()); + } + + public class LocalServerMessenger : ServerMessengerBase + { + public LocalServerMessenger() + : base(false) + { } + + protected override void DeliverRemote(ICacheRefresher refresher, MessageType messageType, IEnumerable ids = null, string json = null) + { + throw new NotImplementedException(); + } + } + + private void AssertJsonStartsWith(int id, string expected) + { + var json = GetJson(id).Replace('"', '\''); + var pos = json.IndexOf("'cultureData':", StringComparison.InvariantCultureIgnoreCase); + json = json.Substring(0, pos + "'cultureData':".Length); + Assert.AreEqual(expected, json); + } + + private string GetJson(int id) + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + var selectJson = SqlContext.Sql().Select().From().Where(x => x.NodeId == id && !x.Published); + var dto = scope.Database.Fetch(selectJson).FirstOrDefault(); + Assert.IsNotNull(dto); + var json = dto.Data; + return json; + } + } + + [Test] + public void Change_Variations_SimpleContentType_VariantToInvariantAndBack() + { + // one simple content type, variant, with both variant and invariant properties + // can change it to invariant and back + + var languageEn = new Language("en") { IsDefault = true }; + ServiceContext.LocalizationService.Save(languageEn); + var languageFr = new Language("fr"); + ServiceContext.LocalizationService.Save(languageFr); + + var contentType = new ContentType(-1) + { + Alias = "contentType", + Name = "contentType", + Variations = ContentVariation.Culture + }; + + var properties = new PropertyTypeCollection(true) + { + new PropertyType("value1", ValueStorageType.Ntext) + { + Alias = "value1", + DataTypeId = -88, + Variations = ContentVariation.Culture + }, + new PropertyType("value2", ValueStorageType.Ntext) + { + Alias = "value2", + DataTypeId = -88, + Variations = ContentVariation.Nothing + } + }; + + contentType.PropertyGroups.Add(new PropertyGroup(properties) { Name = "Content" }); + ServiceContext.ContentTypeService.Save(contentType); + + var document = (IContent) new Content("document", -1, contentType); + document.SetCultureName("doc1en", "en"); + document.SetCultureName("doc1fr", "fr"); + document.SetValue("value1", "v1en", "en"); + document.SetValue("value1", "v1fr", "fr"); + document.SetValue("value2", "v2"); + ServiceContext.ContentService.Save(document); + + document = ServiceContext.ContentService.GetById(document.Id); + Assert.AreEqual("doc1en", document.Name); + Assert.AreEqual("doc1en", document.GetCultureName("en")); + Assert.AreEqual("doc1fr", document.GetCultureName("fr")); + Assert.AreEqual("v1en", document.GetValue("value1", "en")); + Assert.AreEqual("v1fr", document.GetValue("value1", "fr")); + Assert.AreEqual("v2", document.GetValue("value2")); + + Console.WriteLine(GetJson(document.Id)); + AssertJsonStartsWith(document.Id, + "{'properties':{'value1':[{'culture':'en','seg':'','val':'v1en'},{'culture':'fr','seg':'','val':'v1fr'}],'value2':[{'culture':'','seg':'','val':'v2'}]},'cultureData':"); + + // switch content type to Nothing + contentType.Variations = ContentVariation.Nothing; + ServiceContext.ContentTypeService.Save(contentType); + + document = ServiceContext.ContentService.GetById(document.Id); + Assert.AreEqual("doc1en", document.Name); + Assert.IsNull(document.GetCultureName("en")); + Assert.IsNull(document.GetCultureName("fr")); + Assert.IsNull(document.GetValue("value1", "en")); + Assert.IsNull(document.GetValue("value1", "fr")); + Assert.AreEqual("v1en", document.GetValue("value1")); + Assert.AreEqual("v2", document.GetValue("value2")); + + Assert.IsFalse(document.ContentType.PropertyTypes.First(x => x.Alias == "value1").VariesByCulture()); + + Console.WriteLine(GetJson(document.Id)); + AssertJsonStartsWith(document.Id, + "{'properties':{'value1':[{'culture':'','seg':'','val':'v1en'}],'value2':[{'culture':'','seg':'','val':'v2'}]},'cultureData':"); + + // switch content back to Culture + contentType.Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(contentType); + + document = ServiceContext.ContentService.GetById(document.Id); + Assert.AreEqual("doc1en", document.Name); + Assert.AreEqual("doc1en", document.GetCultureName("en")); + Assert.AreEqual("doc1fr", document.GetCultureName("fr")); + Assert.IsNull(document.GetValue("value1", "en")); + Assert.IsNull(document.GetValue("value1", "fr")); + Assert.AreEqual("v1en", document.GetValue("value1")); + Assert.AreEqual("v2", document.GetValue("value2")); + + Assert.IsFalse(document.ContentType.PropertyTypes.First(x => x.Alias == "value1").VariesByCulture()); + + Console.WriteLine(GetJson(document.Id)); + AssertJsonStartsWith(document.Id, + "{'properties':{'value1':[{'culture':'','seg':'','val':'v1en'}],'value2':[{'culture':'','seg':'','val':'v2'}]},'cultureData':"); + + // switch property back to Culture + contentType.PropertyTypes.First(x => x.Alias == "value1").Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(contentType); + + document = ServiceContext.ContentService.GetById(document.Id); + Assert.AreEqual("doc1en", document.Name); + Assert.AreEqual("doc1en", document.GetCultureName("en")); + Assert.AreEqual("doc1fr", document.GetCultureName("fr")); + Assert.AreEqual("v1en", document.GetValue("value1", "en")); + Assert.AreEqual("v1fr", document.GetValue("value1", "fr")); + Assert.AreEqual("v2", document.GetValue("value2")); + + Assert.IsTrue(document.ContentType.PropertyTypes.First(x => x.Alias == "value1").VariesByCulture()); + + Console.WriteLine(GetJson(document.Id)); + AssertJsonStartsWith(document.Id, + "{'properties':{'value1':[{'culture':'en','seg':'','val':'v1en'},{'culture':'fr','seg':'','val':'v1fr'}],'value2':[{'culture':'','seg':'','val':'v2'}]},'cultureData':"); + } + + [Test] + public void Change_Variations_SimpleContentType_InvariantToVariantAndBack() + { + // one simple content type, invariant + // can change it to variant and back + // can then switch one property to variant + + var languageEn = new Language("en") { IsDefault = true }; + ServiceContext.LocalizationService.Save(languageEn); + var languageFr = new Language("fr"); + ServiceContext.LocalizationService.Save(languageFr); + + var contentType = new ContentType(-1) + { + Alias = "contentType", + Name = "contentType", + Variations = ContentVariation.Nothing + }; + + var properties = new PropertyTypeCollection(true) + { + new PropertyType("value1", ValueStorageType.Ntext) + { + Alias = "value1", + DataTypeId = -88, + Variations = ContentVariation.Nothing + }, + new PropertyType("value2", ValueStorageType.Ntext) + { + Alias = "value2", + DataTypeId = -88, + Variations = ContentVariation.Nothing + } + }; + + contentType.PropertyGroups.Add(new PropertyGroup(properties) { Name = "Content" }); + ServiceContext.ContentTypeService.Save(contentType); + + var document = (IContent) new Content("document", -1, contentType); + document.Name = "doc1"; + document.SetValue("value1", "v1"); + document.SetValue("value2", "v2"); + ServiceContext.ContentService.Save(document); + + document = ServiceContext.ContentService.GetById(document.Id); + Assert.AreEqual("doc1", document.Name); + Assert.IsNull(document.GetCultureName("en")); + Assert.IsNull(document.GetCultureName("fr")); + Assert.IsNull(document.GetValue("value1", "en")); + Assert.IsNull(document.GetValue("value1", "fr")); + Assert.AreEqual("v1", document.GetValue("value1")); + Assert.AreEqual("v2", document.GetValue("value2")); + + Console.WriteLine(GetJson(document.Id)); + AssertJsonStartsWith(document.Id, + "{'properties':{'value1':[{'culture':'','seg':'','val':'v1'}],'value2':[{'culture':'','seg':'','val':'v2'}]},'cultureData':"); + + // switch content type to Culture + contentType.Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(contentType); + + document = ServiceContext.ContentService.GetById(document.Id); + Assert.AreEqual("doc1", document.GetCultureName("en")); + Assert.IsNull(document.GetCultureName("fr")); + Assert.IsNull(document.GetValue("value1", "en")); + Assert.IsNull(document.GetValue("value1", "fr")); + Assert.AreEqual("v1", document.GetValue("value1")); + Assert.AreEqual("v2", document.GetValue("value2")); + + Assert.IsFalse(document.ContentType.PropertyTypes.First(x => x.Alias == "value1").VariesByCulture()); + + Console.WriteLine(GetJson(document.Id)); + AssertJsonStartsWith(document.Id, + "{'properties':{'value1':[{'culture':'','seg':'','val':'v1'}],'value2':[{'culture':'','seg':'','val':'v2'}]},'cultureData':"); + + // switch property to Culture + contentType.PropertyTypes.First(x => x.Alias == "value1").Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(contentType); + + document = ServiceContext.ContentService.GetById(document.Id); + Assert.AreEqual("doc1", document.GetCultureName("en")); + Assert.IsNull(document.GetCultureName("fr")); + Assert.AreEqual("v1", document.GetValue("value1", "en")); + Assert.IsNull(document.GetValue("value1", "fr")); + Assert.AreEqual("v2", document.GetValue("value2")); + + Assert.IsTrue(document.ContentType.PropertyTypes.First(x => x.Alias == "value1").VariesByCulture()); + + Console.WriteLine(GetJson(document.Id)); + AssertJsonStartsWith(document.Id, + "{'properties':{'value1':[{'culture':'en','seg':'','val':'v1'}],'value2':[{'culture':'','seg':'','val':'v2'}]},'cultureData':"); + + // switch content back to Nothing + contentType.Variations = ContentVariation.Nothing; + ServiceContext.ContentTypeService.Save(contentType); + + document = ServiceContext.ContentService.GetById(document.Id); + Assert.AreEqual("doc1", document.Name); + Assert.IsNull(document.GetCultureName("en")); + Assert.IsNull(document.GetCultureName("fr")); + Assert.IsNull(document.GetValue("value1", "en")); + Assert.IsNull(document.GetValue("value1", "fr")); + Assert.AreEqual("v1", document.GetValue("value1")); + Assert.AreEqual("v2", document.GetValue("value2")); + + Assert.IsFalse(document.ContentType.PropertyTypes.First(x => x.Alias == "value1").VariesByCulture()); + + Console.WriteLine(GetJson(document.Id)); + AssertJsonStartsWith(document.Id, + "{'properties':{'value1':[{'culture':'','seg':'','val':'v1'}],'value2':[{'culture':'','seg':'','val':'v2'}]},'cultureData':"); + } + + [Test] + public void Change_Variations_SimpleContentType_VariantPropertyToInvariantAndBack() + { + // one simple content type, variant, with both variant and invariant properties + // can change an invariant property to variant and back + + var languageEn = new Language("en") { IsDefault = true }; + ServiceContext.LocalizationService.Save(languageEn); + var languageFr = new Language("fr"); + ServiceContext.LocalizationService.Save(languageFr); + + var contentType = new ContentType(-1) + { + Alias = "contentType", + Name = "contentType", + Variations = ContentVariation.Culture + }; + + var properties = new PropertyTypeCollection(true) + { + new PropertyType("value1", ValueStorageType.Ntext) + { + Alias = "value1", + DataTypeId = -88, + Variations = ContentVariation.Culture + }, + new PropertyType("value2", ValueStorageType.Ntext) + { + Alias = "value2", + DataTypeId = -88, + Variations = ContentVariation.Nothing + } + }; + + contentType.PropertyGroups.Add(new PropertyGroup(properties) { Name = "Content" }); + ServiceContext.ContentTypeService.Save(contentType); + + var document = (IContent)new Content("document", -1, contentType); + document.SetCultureName("doc1en", "en"); + document.SetCultureName("doc1fr", "fr"); + document.SetValue("value1", "v1en", "en"); + document.SetValue("value1", "v1fr", "fr"); + document.SetValue("value2", "v2"); + ServiceContext.ContentService.Save(document); + + document = ServiceContext.ContentService.GetById(document.Id); + Assert.AreEqual("doc1en", document.Name); + Assert.AreEqual("doc1en", document.GetCultureName("en")); + Assert.AreEqual("doc1fr", document.GetCultureName("fr")); + Assert.AreEqual("v1en", document.GetValue("value1", "en")); + Assert.AreEqual("v1fr", document.GetValue("value1", "fr")); + Assert.AreEqual("v2", document.GetValue("value2")); + + Console.WriteLine(GetJson(document.Id)); + AssertJsonStartsWith(document.Id, + "{'properties':{'value1':[{'culture':'en','seg':'','val':'v1en'},{'culture':'fr','seg':'','val':'v1fr'}],'value2':[{'culture':'','seg':'','val':'v2'}]},'cultureData':"); + + // switch property type to Nothing + contentType.PropertyTypes.First(x => x.Alias == "value1").Variations = ContentVariation.Nothing; + ServiceContext.ContentTypeService.Save(contentType); + + document = ServiceContext.ContentService.GetById(document.Id); + Assert.AreEqual("doc1en", document.Name); + Assert.AreEqual("doc1en", document.GetCultureName("en")); + Assert.AreEqual("doc1fr", document.GetCultureName("fr")); + Assert.IsNull(document.GetValue("value1", "en")); + Assert.IsNull(document.GetValue("value1", "fr")); + Assert.AreEqual("v1en", document.GetValue("value1")); + Assert.AreEqual("v2", document.GetValue("value2")); + + Assert.IsFalse(document.ContentType.PropertyTypes.First(x => x.Alias == "value1").VariesByCulture()); + + Console.WriteLine(GetJson(document.Id)); + AssertJsonStartsWith(document.Id, + "{'properties':{'value1':[{'culture':'','seg':'','val':'v1en'}],'value2':[{'culture':'','seg':'','val':'v2'}]},'cultureData':"); + + // switch property back to Culture + contentType.PropertyTypes.First(x => x.Alias == "value1").Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(contentType); + + document = ServiceContext.ContentService.GetById(document.Id); + Assert.AreEqual("doc1en", document.Name); + Assert.AreEqual("doc1en", document.GetCultureName("en")); + Assert.AreEqual("doc1fr", document.GetCultureName("fr")); + Assert.AreEqual("v1en", document.GetValue("value1", "en")); + Assert.AreEqual("v1fr", document.GetValue("value1", "fr")); + Assert.AreEqual("v2", document.GetValue("value2")); + + Assert.IsTrue(document.ContentType.PropertyTypes.First(x => x.Alias == "value1").VariesByCulture()); + + Console.WriteLine(GetJson(document.Id)); + AssertJsonStartsWith(document.Id, + "{'properties':{'value1':[{'culture':'en','seg':'','val':'v1en'},{'culture':'fr','seg':'','val':'v1fr'}],'value2':[{'culture':'','seg':'','val':'v2'}]},'cultureData':"); + + // switch other property to Culture + contentType.PropertyTypes.First(x => x.Alias == "value2").Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(contentType); + + document = ServiceContext.ContentService.GetById(document.Id); + Assert.AreEqual("doc1en", document.Name); + Assert.AreEqual("doc1en", document.GetCultureName("en")); + Assert.AreEqual("doc1fr", document.GetCultureName("fr")); + Assert.AreEqual("v1en", document.GetValue("value1", "en")); + Assert.AreEqual("v1fr", document.GetValue("value1", "fr")); + Assert.AreEqual("v2", document.GetValue("value2", "en")); + Assert.IsNull(document.GetValue("value2", "fr")); + Assert.IsNull(document.GetValue("value2")); + + Assert.IsTrue(document.ContentType.PropertyTypes.First(x => x.Alias == "value2").VariesByCulture()); + + Console.WriteLine(GetJson(document.Id)); + AssertJsonStartsWith(document.Id, + "{'properties':{'value1':[{'culture':'en','seg':'','val':'v1en'},{'culture':'fr','seg':'','val':'v1fr'}],'value2':[{'culture':'en','seg':'','val':'v2'}]},'cultureData':"); + } + + [Test] + public void Change_Variations_ComposedContentType_1() + { + // one composing content type, variant, with both variant and invariant properties + // one composed content type, variant, with both variant and invariant properties + // can change the composing content type to invariant and back + // can change the composed content type to invariant and back + + var languageEn = new Language("en") { IsDefault = true }; + ServiceContext.LocalizationService.Save(languageEn); + var languageFr = new Language("fr"); + ServiceContext.LocalizationService.Save(languageFr); + + var composing = new ContentType(-1) + { + Alias = "composing", + Name = "composing", + Variations = ContentVariation.Culture + }; + + var properties1 = new PropertyTypeCollection(true) + { + new PropertyType("value11", ValueStorageType.Ntext) + { + Alias = "value11", + DataTypeId = -88, + Variations = ContentVariation.Culture + }, + new PropertyType("value12", ValueStorageType.Ntext) + { + Alias = "value12", + DataTypeId = -88, + Variations = ContentVariation.Nothing + } + }; + + composing.PropertyGroups.Add(new PropertyGroup(properties1) { Name = "Content" }); + ServiceContext.ContentTypeService.Save(composing); + + var composed = new ContentType(-1) + { + Alias = "composed", + Name = "composed", + Variations = ContentVariation.Culture + }; + + var properties2 = new PropertyTypeCollection(true) + { + new PropertyType("value21", ValueStorageType.Ntext) + { + Alias = "value21", + DataTypeId = -88, + Variations = ContentVariation.Culture + }, + new PropertyType("value22", ValueStorageType.Ntext) + { + Alias = "value22", + DataTypeId = -88, + Variations = ContentVariation.Nothing + } + }; + + composed.PropertyGroups.Add(new PropertyGroup(properties2) { Name = "Content" }); + composed.AddContentType(composing); + ServiceContext.ContentTypeService.Save(composed); + + var document = (IContent) new Content("document", -1, composed); + document.SetCultureName("doc1en", "en"); + document.SetCultureName("doc1fr", "fr"); + document.SetValue("value11", "v11en", "en"); + document.SetValue("value11", "v11fr", "fr"); + document.SetValue("value12", "v12"); + document.SetValue("value21", "v21en", "en"); + document.SetValue("value21", "v21fr", "fr"); + document.SetValue("value22", "v22"); + ServiceContext.ContentService.Save(document); + + // both value11 and value21 are variant + Console.WriteLine(GetJson(document.Id)); + AssertJsonStartsWith(document.Id, + "{'properties':{'value11':[{'culture':'en','seg':'','val':'v11en'},{'culture':'fr','seg':'','val':'v11fr'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'en','seg':'','val':'v21en'},{'culture':'fr','seg':'','val':'v21fr'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':"); + + composed.Variations = ContentVariation.Nothing; + ServiceContext.ContentTypeService.Save(composed); + + // both value11 and value21 are invariant + Console.WriteLine(GetJson(document.Id)); + AssertJsonStartsWith(document.Id, + "{'properties':{'value11':[{'culture':'','seg':'','val':'v11en'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'','seg':'','val':'v21en'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':"); + + composed.Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(composed); + + // value11 is variant again, but value21 is still invariant + Console.WriteLine(GetJson(document.Id)); + AssertJsonStartsWith(document.Id, + "{'properties':{'value11':[{'culture':'en','seg':'','val':'v11en'},{'culture':'fr','seg':'','val':'v11fr'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'','seg':'','val':'v21en'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':"); + + composed.PropertyTypes.First(x => x.Alias == "value21").Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(composed); + + // we can make it variant again + Console.WriteLine(GetJson(document.Id)); + AssertJsonStartsWith(document.Id, + "{'properties':{'value11':[{'culture':'en','seg':'','val':'v11en'},{'culture':'fr','seg':'','val':'v11fr'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'en','seg':'','val':'v21en'},{'culture':'fr','seg':'','val':'v21fr'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':"); + + composing.Variations = ContentVariation.Nothing; + ServiceContext.ContentTypeService.Save(composing); + + // value11 is invariant + Console.WriteLine(GetJson(document.Id)); + AssertJsonStartsWith(document.Id, + "{'properties':{'value11':[{'culture':'','seg':'','val':'v11en'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'en','seg':'','val':'v21en'},{'culture':'fr','seg':'','val':'v21fr'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':"); + + composing.Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(composing); + + // value11 is still invariant + Console.WriteLine(GetJson(document.Id)); + AssertJsonStartsWith(document.Id, + "{'properties':{'value11':[{'culture':'','seg':'','val':'v11en'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'en','seg':'','val':'v21en'},{'culture':'fr','seg':'','val':'v21fr'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':"); + + composing.PropertyTypes.First(x => x.Alias == "value11").Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(composing); + + // we can make it variant again + Console.WriteLine(GetJson(document.Id)); + AssertJsonStartsWith(document.Id, + "{'properties':{'value11':[{'culture':'en','seg':'','val':'v11en'},{'culture':'fr','seg':'','val':'v11fr'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'en','seg':'','val':'v21en'},{'culture':'fr','seg':'','val':'v21fr'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':"); + } + + [Test] + public void Change_Variations_ComposedContentType_2() + { + // one composing content type, variant, with both variant and invariant properties + // one composed content type, variant, with both variant and invariant properties + // one composed content type, invariant + // can change the composing content type to invariant and back + // can change the variant composed content type to invariant and back + + var languageEn = new Language("en") { IsDefault = true }; + ServiceContext.LocalizationService.Save(languageEn); + var languageFr = new Language("fr"); + ServiceContext.LocalizationService.Save(languageFr); + + var composing = new ContentType(-1) + { + Alias = "composing", + Name = "composing", + Variations = ContentVariation.Culture + }; + + var properties1 = new PropertyTypeCollection(true) + { + new PropertyType("value11", ValueStorageType.Ntext) + { + Alias = "value11", + DataTypeId = -88, + Variations = ContentVariation.Culture + }, + new PropertyType("value12", ValueStorageType.Ntext) + { + Alias = "value12", + DataTypeId = -88, + Variations = ContentVariation.Nothing + } + }; + + composing.PropertyGroups.Add(new PropertyGroup(properties1) { Name = "Content" }); + ServiceContext.ContentTypeService.Save(composing); + + var composed1 = new ContentType(-1) + { + Alias = "composed1", + Name = "composed1", + Variations = ContentVariation.Culture + }; + + var properties2 = new PropertyTypeCollection(true) + { + new PropertyType("value21", ValueStorageType.Ntext) + { + Alias = "value21", + DataTypeId = -88, + Variations = ContentVariation.Culture + }, + new PropertyType("value22", ValueStorageType.Ntext) + { + Alias = "value22", + DataTypeId = -88, + Variations = ContentVariation.Nothing + } + }; + + composed1.PropertyGroups.Add(new PropertyGroup(properties2) { Name = "Content" }); + composed1.AddContentType(composing); + ServiceContext.ContentTypeService.Save(composed1); + + var composed2 = new ContentType(-1) + { + Alias = "composed2", + Name = "composed2", + Variations = ContentVariation.Nothing + }; + + var properties3 = new PropertyTypeCollection(true) + { + new PropertyType("value31", ValueStorageType.Ntext) + { + Alias = "value31", + DataTypeId = -88, + Variations = ContentVariation.Nothing + }, + new PropertyType("value32", ValueStorageType.Ntext) + { + Alias = "value32", + DataTypeId = -88, + Variations = ContentVariation.Nothing + } + }; + + composed2.PropertyGroups.Add(new PropertyGroup(properties3) { Name = "Content" }); + composed2.AddContentType(composing); + ServiceContext.ContentTypeService.Save(composed2); + + var document1 = (IContent) new Content ("document1", -1, composed1); + document1.SetCultureName("doc1en", "en"); + document1.SetCultureName("doc1fr", "fr"); + document1.SetValue("value11", "v11en", "en"); + document1.SetValue("value11", "v11fr", "fr"); + document1.SetValue("value12", "v12"); + document1.SetValue("value21", "v21en", "en"); + document1.SetValue("value21", "v21fr", "fr"); + document1.SetValue("value22", "v22"); + ServiceContext.ContentService.Save(document1); + + var document2 = (IContent)new Content("document2", -1, composed2); + document2.Name = "doc2"; + document2.SetValue("value11", "v11"); + document2.SetValue("value12", "v12"); + document2.SetValue("value31", "v31"); + document2.SetValue("value32", "v32"); + ServiceContext.ContentService.Save(document2); + + // both value11 and value21 are variant + Console.WriteLine(GetJson(document1.Id)); + AssertJsonStartsWith(document1.Id, + "{'properties':{'value11':[{'culture':'en','seg':'','val':'v11en'},{'culture':'fr','seg':'','val':'v11fr'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'en','seg':'','val':'v21en'},{'culture':'fr','seg':'','val':'v21fr'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':"); + + Console.WriteLine(GetJson(document2.Id)); + AssertJsonStartsWith(document2.Id, + "{'properties':{'value11':[{'culture':'','seg':'','val':'v11'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value31':[{'culture':'','seg':'','val':'v31'}],'value32':[{'culture':'','seg':'','val':'v32'}]},'cultureData':"); + + composed1.Variations = ContentVariation.Nothing; + ServiceContext.ContentTypeService.Save(composed1); + + // both value11 and value21 are invariant + Console.WriteLine(GetJson(document1.Id)); + AssertJsonStartsWith(document1.Id, + "{'properties':{'value11':[{'culture':'','seg':'','val':'v11en'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'','seg':'','val':'v21en'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':"); + + Console.WriteLine(GetJson(document2.Id)); + AssertJsonStartsWith(document2.Id, + "{'properties':{'value11':[{'culture':'','seg':'','val':'v11'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value31':[{'culture':'','seg':'','val':'v31'}],'value32':[{'culture':'','seg':'','val':'v32'}]},'cultureData':"); + + composed1.Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(composed1); + + // value11 is variant again, but value21 is still invariant + Console.WriteLine(GetJson(document1.Id)); + AssertJsonStartsWith(document1.Id, + "{'properties':{'value11':[{'culture':'en','seg':'','val':'v11en'},{'culture':'fr','seg':'','val':'v11fr'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'','seg':'','val':'v21en'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':"); + + Console.WriteLine(GetJson(document2.Id)); + AssertJsonStartsWith(document2.Id, + "{'properties':{'value11':[{'culture':'','seg':'','val':'v11'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value31':[{'culture':'','seg':'','val':'v31'}],'value32':[{'culture':'','seg':'','val':'v32'}]},'cultureData':"); + + composed1.PropertyTypes.First(x => x.Alias == "value21").Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(composed1); + + // we can make it variant again + Console.WriteLine(GetJson(document1.Id)); + AssertJsonStartsWith(document1.Id, + "{'properties':{'value11':[{'culture':'en','seg':'','val':'v11en'},{'culture':'fr','seg':'','val':'v11fr'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'en','seg':'','val':'v21en'},{'culture':'fr','seg':'','val':'v21fr'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':"); + + Console.WriteLine(GetJson(document2.Id)); + AssertJsonStartsWith(document2.Id, + "{'properties':{'value11':[{'culture':'','seg':'','val':'v11'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value31':[{'culture':'','seg':'','val':'v31'}],'value32':[{'culture':'','seg':'','val':'v32'}]},'cultureData':"); + + composing.Variations = ContentVariation.Nothing; + ServiceContext.ContentTypeService.Save(composing); + + // value11 is invariant + Console.WriteLine(GetJson(document1.Id)); + AssertJsonStartsWith(document1.Id, + "{'properties':{'value11':[{'culture':'','seg':'','val':'v11en'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'en','seg':'','val':'v21en'},{'culture':'fr','seg':'','val':'v21fr'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':"); + + Console.WriteLine(GetJson(document2.Id)); + AssertJsonStartsWith(document2.Id, + "{'properties':{'value11':[{'culture':'','seg':'','val':'v11'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value31':[{'culture':'','seg':'','val':'v31'}],'value32':[{'culture':'','seg':'','val':'v32'}]},'cultureData':"); + + composing.Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(composing); + + // value11 is still invariant + Console.WriteLine(GetJson(document1.Id)); + AssertJsonStartsWith(document1.Id, + "{'properties':{'value11':[{'culture':'','seg':'','val':'v11en'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'en','seg':'','val':'v21en'},{'culture':'fr','seg':'','val':'v21fr'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':"); + + Console.WriteLine(GetJson(document2.Id)); + AssertJsonStartsWith(document2.Id, + "{'properties':{'value11':[{'culture':'','seg':'','val':'v11'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value31':[{'culture':'','seg':'','val':'v31'}],'value32':[{'culture':'','seg':'','val':'v32'}]},'cultureData':"); + + composing.PropertyTypes.First(x => x.Alias == "value11").Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(composing); + + // we can make it variant again + Console.WriteLine(GetJson(document1.Id)); + AssertJsonStartsWith(document1.Id, + "{'properties':{'value11':[{'culture':'en','seg':'','val':'v11en'},{'culture':'fr','seg':'','val':'v11fr'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'en','seg':'','val':'v21en'},{'culture':'fr','seg':'','val':'v21fr'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':"); + + Console.WriteLine(GetJson(document2.Id)); + AssertJsonStartsWith(document2.Id, + "{'properties':{'value11':[{'culture':'','seg':'','val':'v11'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value31':[{'culture':'','seg':'','val':'v31'}],'value32':[{'culture':'','seg':'','val':'v32'}]},'cultureData':"); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Tests/Services/Importing/PackageImportTests.cs b/src/Umbraco.Tests/Services/Importing/PackageImportTests.cs index 767ffd4fc2..b33ff83c4a 100644 --- a/src/Umbraco.Tests/Services/Importing/PackageImportTests.cs +++ b/src/Umbraco.Tests/Services/Importing/PackageImportTests.cs @@ -628,7 +628,7 @@ namespace Umbraco.Tests.Services.Importing // Assert Assert.That(macros.Any(), Is.True); - Assert.That(macros.First().Properties.Any(), Is.True); + Assert.That(macros.First().Properties.Values.Any(), Is.True); var allMacros = ServiceContext.MacroService.GetAll().ToList(); foreach (var macro in macros) diff --git a/src/Umbraco.Tests/Services/MacroServiceTests.cs b/src/Umbraco.Tests/Services/MacroServiceTests.cs index fa86f4baab..6539a37114 100644 --- a/src/Umbraco.Tests/Services/MacroServiceTests.cs +++ b/src/Umbraco.Tests/Services/MacroServiceTests.cs @@ -195,7 +195,7 @@ namespace Umbraco.Tests.Services macro.Properties["blah1"].EditorAlias = "new"; macro.Properties.Remove("blah3"); - var allPropKeys = macro.Properties.Select(x => new { x.Alias, x.Key }).ToArray(); + var allPropKeys = macro.Properties.Values.Select(x => new { x.Alias, x.Key }).ToArray(); macroService.Save(macro); @@ -228,10 +228,10 @@ namespace Umbraco.Tests.Services macroService.Save(macro); var result1 = macroService.GetById(macro.Id); - Assert.AreEqual(4, result1.Properties.Count()); + Assert.AreEqual(4, result1.Properties.Values.Count()); //simulate clearing the sections - foreach (var s in result1.Properties.ToArray()) + foreach (var s in result1.Properties.Values.ToArray()) { result1.Properties.Remove(s.Alias); } @@ -244,7 +244,7 @@ namespace Umbraco.Tests.Services //re-get result1 = macroService.GetById(result1.Id); - Assert.AreEqual(2, result1.Properties.Count()); + Assert.AreEqual(2, result1.Properties.Values.Count()); } diff --git a/src/Umbraco.Tests/Services/MediaServiceTests.cs b/src/Umbraco.Tests/Services/MediaServiceTests.cs index 68fd2c3e11..b9e1fee0db 100644 --- a/src/Umbraco.Tests/Services/MediaServiceTests.cs +++ b/src/Umbraco.Tests/Services/MediaServiceTests.cs @@ -69,11 +69,15 @@ namespace Umbraco.Tests.Services } long total; - var result = ServiceContext.MediaService.GetPagedChildren(-1, 0, 11, out total, "SortOrder", Direction.Ascending, true, null, new[] { mediaType1.Id, mediaType2.Id }); + var result = ServiceContext.MediaService.GetPagedChildren(-1, 0, 11, out total, + SqlContext.Query().Where(x => new[] { mediaType1.Id, mediaType2.Id }.Contains(x.ContentTypeId)), + Ordering.By("SortOrder", Direction.Ascending)); Assert.AreEqual(11, result.Count()); Assert.AreEqual(20, total); - result = ServiceContext.MediaService.GetPagedChildren(-1, 1, 11, out total, "SortOrder", Direction.Ascending, true, null, new[] { mediaType1.Id, mediaType2.Id }); + result = ServiceContext.MediaService.GetPagedChildren(-1, 1, 11, out total, + SqlContext.Query().Where(x => new[] { mediaType1.Id, mediaType2.Id }.Contains(x.ContentTypeId)), + Ordering.By("SortOrder", Direction.Ascending)); Assert.AreEqual(9, result.Count()); Assert.AreEqual(20, total); } diff --git a/src/Umbraco.Tests/Services/MemberServiceTests.cs b/src/Umbraco.Tests/Services/MemberServiceTests.cs index 968b27a4f9..078336262f 100644 --- a/src/Umbraco.Tests/Services/MemberServiceTests.cs +++ b/src/Umbraco.Tests/Services/MemberServiceTests.cs @@ -292,6 +292,29 @@ namespace Umbraco.Tests.Services Assert.AreEqual(2, membersInRole.Count()); } + [Test] + public void Associate_Members_To_Roles_With_Member_Id_Casing() + { + ServiceContext.MemberService.AddRole("MyTestRole1"); + + IMemberType memberType = MockedContentTypes.CreateSimpleMemberType(); + ServiceContext.MemberTypeService.Save(memberType); + var member1 = MockedMember.CreateSimpleMember(memberType, "test1", "test1@test.com", "pass", "test1"); + ServiceContext.MemberService.Save(member1); + var member2 = MockedMember.CreateSimpleMember(memberType, "test2", "test2@test.com", "pass", "test2"); + ServiceContext.MemberService.Save(member2); + + // temp make sure they exist + Assert.IsNotNull(ServiceContext.MemberService.GetById(member1.Id)); + Assert.IsNotNull(ServiceContext.MemberService.GetById(member2.Id)); + + ServiceContext.MemberService.AssignRoles(new[] { member1.Id, member2.Id }, new[] { "mytestrole1" }); + + var membersInRole = ServiceContext.MemberService.GetMembersInRole("MyTestRole1"); + + Assert.AreEqual(2, membersInRole.Count()); + } + [Test] public void Associate_Members_To_Roles_With_Member_Username() { diff --git a/src/Umbraco.Tests/Services/PerformanceTests.cs b/src/Umbraco.Tests/Services/PerformanceTests.cs index 9b0117c266..900a466a1d 100644 --- a/src/Umbraco.Tests/Services/PerformanceTests.cs +++ b/src/Umbraco.Tests/Services/PerformanceTests.cs @@ -108,40 +108,6 @@ namespace Umbraco.Tests.Services } } - [Test] - public void Get_All_Published_Content_Of_Type() - { - var result = PrimeDbWithLotsOfContent(); - var contentSvc = (ContentService)ServiceContext.ContentService; - - var countOfPublished = result.Count(x => x.Published); - var contentTypeId = result.First().ContentTypeId; - - var proflog = GetTestProfilingLogger(); - using (proflog.DebugDuration("Getting published content of type normally")) - { - //do this 10x! - for (var i = 0; i < 10; i++) - { - - //get all content items that are published of this type - var published = contentSvc.GetByType(contentTypeId).Where(content => content.Published); - Assert.AreEqual(countOfPublished, published.Count(x => x.ContentTypeId == contentTypeId)); - } - } - - using (proflog.DebugDuration("Getting published content of type optimized")) - { - - //do this 10x! - for (var i = 0; i < 10; i++) - { - //get all content items that are published of this type - var published = contentSvc.GetPublishedContentOfContentType(contentTypeId); - Assert.AreEqual(countOfPublished, published.Count(x => x.ContentTypeId == contentTypeId)); - } - } - } [Test] public void Truncate_Insert_Vs_Update_Insert() diff --git a/src/Umbraco.Tests/Services/TestWithSomeContentBase.cs b/src/Umbraco.Tests/Services/TestWithSomeContentBase.cs index b821e5a516..2b313afc5c 100644 --- a/src/Umbraco.Tests/Services/TestWithSomeContentBase.cs +++ b/src/Umbraco.Tests/Services/TestWithSomeContentBase.cs @@ -34,7 +34,7 @@ namespace Umbraco.Tests.Services //Create and Save Content "Text Page 1" based on "umbTextpage" -> 1062 Content subpage = MockedContent.CreateSimpleContent(contentType, "Text Page 1", textpage.Id); - subpage.ReleaseDate = DateTime.Now.AddMinutes(-5); + subpage.ContentSchedule.Add(DateTime.Now.AddMinutes(-5), null); ServiceContext.ContentService.Save(subpage, 0); //Create and Save Content "Text Page 1" based on "umbTextpage" -> 1063 diff --git a/src/Umbraco.Tests/Services/UserServiceTests.cs b/src/Umbraco.Tests/Services/UserServiceTests.cs index 117b0f9103..cce54c81a4 100644 --- a/src/Umbraco.Tests/Services/UserServiceTests.cs +++ b/src/Umbraco.Tests/Services/UserServiceTests.cs @@ -14,7 +14,8 @@ using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Services; using Umbraco.Core.Services.Implement; using Umbraco.Tests.Testing; -using Umbraco.Web._Legacy.Actions; +using Umbraco.Web.Actions; + namespace Umbraco.Tests.Services { @@ -70,12 +71,12 @@ namespace Umbraco.Tests.Services MockedContent.CreateSimpleContent(contentType) }; ServiceContext.ContentService.Save(content); - ServiceContext.ContentService.SetPermission(content[0], ActionBrowse.Instance.Letter, new int[] { userGroup.Id }); - ServiceContext.ContentService.SetPermission(content[0], ActionDelete.Instance.Letter, new int[] { userGroup.Id }); - ServiceContext.ContentService.SetPermission(content[0], ActionMove.Instance.Letter, new int[] { userGroup.Id }); - ServiceContext.ContentService.SetPermission(content[1], ActionBrowse.Instance.Letter, new int[] { userGroup.Id }); - ServiceContext.ContentService.SetPermission(content[1], ActionDelete.Instance.Letter, new int[] { userGroup.Id }); - ServiceContext.ContentService.SetPermission(content[2], ActionBrowse.Instance.Letter, new int[] { userGroup.Id }); + ServiceContext.ContentService.SetPermission(content[0], ActionBrowse.ActionLetter, new int[] { userGroup.Id }); + ServiceContext.ContentService.SetPermission(content[0], ActionDelete.ActionLetter, new int[] { userGroup.Id }); + ServiceContext.ContentService.SetPermission(content[0], ActionMove.ActionLetter, new int[] { userGroup.Id }); + ServiceContext.ContentService.SetPermission(content[1], ActionBrowse.ActionLetter, new int[] { userGroup.Id }); + ServiceContext.ContentService.SetPermission(content[1], ActionDelete.ActionLetter, new int[] { userGroup.Id }); + ServiceContext.ContentService.SetPermission(content[2], ActionBrowse.ActionLetter, new int[] { userGroup.Id }); // Act var permissions = userService.GetPermissions(user, content[0].Id, content[1].Id, content[2].Id).ToArray(); @@ -103,12 +104,12 @@ namespace Umbraco.Tests.Services MockedContent.CreateSimpleContent(contentType) }; ServiceContext.ContentService.Save(content); - ServiceContext.ContentService.SetPermission(content.ElementAt(0), ActionBrowse.Instance.Letter, new int[] { userGroup.Id }); - ServiceContext.ContentService.SetPermission(content.ElementAt(0), ActionDelete.Instance.Letter, new int[] { userGroup.Id }); - ServiceContext.ContentService.SetPermission(content.ElementAt(0), ActionMove.Instance.Letter, new int[] { userGroup.Id }); - ServiceContext.ContentService.SetPermission(content.ElementAt(1), ActionBrowse.Instance.Letter, new int[] { userGroup.Id }); - ServiceContext.ContentService.SetPermission(content.ElementAt(1), ActionDelete.Instance.Letter, new int[] { userGroup.Id }); - ServiceContext.ContentService.SetPermission(content.ElementAt(2), ActionBrowse.Instance.Letter, new int[] { userGroup.Id }); + ServiceContext.ContentService.SetPermission(content.ElementAt(0), ActionBrowse.ActionLetter, new int[] { userGroup.Id }); + ServiceContext.ContentService.SetPermission(content.ElementAt(0), ActionDelete.ActionLetter, new int[] { userGroup.Id }); + ServiceContext.ContentService.SetPermission(content.ElementAt(0), ActionMove.ActionLetter, new int[] { userGroup.Id }); + ServiceContext.ContentService.SetPermission(content.ElementAt(1), ActionBrowse.ActionLetter, new int[] { userGroup.Id }); + ServiceContext.ContentService.SetPermission(content.ElementAt(1), ActionDelete.ActionLetter, new int[] { userGroup.Id }); + ServiceContext.ContentService.SetPermission(content.ElementAt(2), ActionBrowse.ActionLetter, new int[] { userGroup.Id }); // Act var permissions = userService.GetPermissions(userGroup, false, content[0].Id, content[1].Id, content[2].Id).ToArray(); @@ -136,11 +137,11 @@ namespace Umbraco.Tests.Services MockedContent.CreateSimpleContent(contentType) }; ServiceContext.ContentService.Save(content); - ServiceContext.ContentService.SetPermission(content[0], ActionBrowse.Instance.Letter, new int[] { userGroup.Id }); - ServiceContext.ContentService.SetPermission(content[0], ActionDelete.Instance.Letter, new int[] { userGroup.Id }); - ServiceContext.ContentService.SetPermission(content[0], ActionMove.Instance.Letter, new int[] { userGroup.Id }); - ServiceContext.ContentService.SetPermission(content[1], ActionBrowse.Instance.Letter, new int[] { userGroup.Id }); - ServiceContext.ContentService.SetPermission(content[1], ActionDelete.Instance.Letter, new int[] { userGroup.Id }); + ServiceContext.ContentService.SetPermission(content[0], ActionBrowse.ActionLetter, new int[] { userGroup.Id }); + ServiceContext.ContentService.SetPermission(content[0], ActionDelete.ActionLetter, new int[] { userGroup.Id }); + ServiceContext.ContentService.SetPermission(content[0], ActionMove.ActionLetter, new int[] { userGroup.Id }); + ServiceContext.ContentService.SetPermission(content[1], ActionBrowse.ActionLetter, new int[] { userGroup.Id }); + ServiceContext.ContentService.SetPermission(content[1], ActionDelete.ActionLetter, new int[] { userGroup.Id }); // Act var permissions = userService.GetPermissions(userGroup, true, content[0].Id, content[1].Id, content[2].Id) @@ -180,12 +181,12 @@ namespace Umbraco.Tests.Services }; ServiceContext.ContentService.Save(content); //assign permissions - we aren't assigning anything explicit for group3 and nothing explicit for content[2] /w group2 - ServiceContext.ContentService.SetPermission(content[0], ActionBrowse.Instance.Letter, new int[] { userGroup1.Id }); - ServiceContext.ContentService.SetPermission(content[0], ActionDelete.Instance.Letter, new int[] { userGroup1.Id }); - ServiceContext.ContentService.SetPermission(content[0], ActionMove.Instance.Letter, new int[] { userGroup2.Id }); - ServiceContext.ContentService.SetPermission(content[1], ActionBrowse.Instance.Letter, new int[] { userGroup1.Id }); - ServiceContext.ContentService.SetPermission(content[1], ActionDelete.Instance.Letter, new int[] { userGroup2.Id }); - ServiceContext.ContentService.SetPermission(content[2], ActionDelete.Instance.Letter, new int[] { userGroup1.Id }); + ServiceContext.ContentService.SetPermission(content[0], ActionBrowse.ActionLetter, new int[] { userGroup1.Id }); + ServiceContext.ContentService.SetPermission(content[0], ActionDelete.ActionLetter, new int[] { userGroup1.Id }); + ServiceContext.ContentService.SetPermission(content[0], ActionMove.ActionLetter, new int[] { userGroup2.Id }); + ServiceContext.ContentService.SetPermission(content[1], ActionBrowse.ActionLetter, new int[] { userGroup1.Id }); + ServiceContext.ContentService.SetPermission(content[1], ActionDelete.ActionLetter, new int[] { userGroup2.Id }); + ServiceContext.ContentService.SetPermission(content[2], ActionDelete.ActionLetter, new int[] { userGroup1.Id }); // Act //we don't pass in any nodes so it will return all of them @@ -249,12 +250,12 @@ namespace Umbraco.Tests.Services MockedContent.CreateSimpleContent(contentType) }; ServiceContext.ContentService.Save(content); - ServiceContext.ContentService.SetPermission(content[0], ActionBrowse.Instance.Letter, new int[] { userGroup.Id }); - ServiceContext.ContentService.SetPermission(content[0], ActionDelete.Instance.Letter, new int[] { userGroup.Id }); - ServiceContext.ContentService.SetPermission(content[0], ActionMove.Instance.Letter, new int[] { userGroup.Id }); - ServiceContext.ContentService.SetPermission(content[1], ActionBrowse.Instance.Letter, new int[] { userGroup.Id }); - ServiceContext.ContentService.SetPermission(content[1], ActionDelete.Instance.Letter, new int[] { userGroup.Id }); - ServiceContext.ContentService.SetPermission(content[2], ActionDelete.Instance.Letter, new int[] { userGroup.Id }); + ServiceContext.ContentService.SetPermission(content[0], ActionBrowse.ActionLetter, new int[] { userGroup.Id }); + ServiceContext.ContentService.SetPermission(content[0], ActionDelete.ActionLetter, new int[] { userGroup.Id }); + ServiceContext.ContentService.SetPermission(content[0], ActionMove.ActionLetter, new int[] { userGroup.Id }); + ServiceContext.ContentService.SetPermission(content[1], ActionBrowse.ActionLetter, new int[] { userGroup.Id }); + ServiceContext.ContentService.SetPermission(content[1], ActionDelete.ActionLetter, new int[] { userGroup.Id }); + ServiceContext.ContentService.SetPermission(content[2], ActionDelete.ActionLetter, new int[] { userGroup.Id }); // Act //we don't pass in any nodes so it will return all of them @@ -412,11 +413,11 @@ namespace Umbraco.Tests.Services var child2 = MockedContent.CreateSimpleContent(contentType, "child2", child1); ServiceContext.ContentService.Save(child2); - ServiceContext.ContentService.SetPermission(parent, ActionBrowse.Instance.Letter, new int[] { userGroup.Id }); - ServiceContext.ContentService.SetPermission(parent, ActionDelete.Instance.Letter, new int[] { userGroup.Id }); - ServiceContext.ContentService.SetPermission(parent, ActionMove.Instance.Letter, new int[] { userGroup.Id }); - ServiceContext.ContentService.SetPermission(parent, ActionBrowse.Instance.Letter, new int[] { userGroup.Id }); - ServiceContext.ContentService.SetPermission(parent, ActionDelete.Instance.Letter, new int[] { userGroup.Id }); + ServiceContext.ContentService.SetPermission(parent, ActionBrowse.ActionLetter, new int[] { userGroup.Id }); + ServiceContext.ContentService.SetPermission(parent, ActionDelete.ActionLetter, new int[] { userGroup.Id }); + ServiceContext.ContentService.SetPermission(parent, ActionMove.ActionLetter, new int[] { userGroup.Id }); + ServiceContext.ContentService.SetPermission(parent, ActionBrowse.ActionLetter, new int[] { userGroup.Id }); + ServiceContext.ContentService.SetPermission(parent, ActionDelete.ActionLetter, new int[] { userGroup.Id }); // Act var permissions = userService.GetPermissionsForPath(userGroup, child2.Path); diff --git a/src/Umbraco.Tests/Strings/StylesheetHelperTests.cs b/src/Umbraco.Tests/Strings/StylesheetHelperTests.cs index c2e17c7755..5ae4c0511f 100644 --- a/src/Umbraco.Tests/Strings/StylesheetHelperTests.cs +++ b/src/Umbraco.Tests/Strings/StylesheetHelperTests.cs @@ -131,5 +131,84 @@ world */p{font-size: 1em;}")] // Assert Assert.IsTrue(results.Any() == false); } + + [Test] + public void AppendRules_IsFormatted() + { + // base CSS + var css = Tabbed( +@"body { +#font-family:Arial; +}"); + // add a couple of rules + var result = StylesheetHelper.AppendRule(css, new StylesheetRule + { + Name = "Test", + Selector = ".test", + Styles = "font-color: red;margin: 1rem;" + }); + result = StylesheetHelper.AppendRule(result, new StylesheetRule + { + Name = "Test2", + Selector = ".test2", + Styles = "font-color: green;" + }); + + // verify the CSS formatting including the indents + Assert.AreEqual(Tabbed( +@"body { +#font-family:Arial; +} + +/**umb_name:Test*/ +.test { +#font-color: red; +#margin: 1rem; +} + +/**umb_name:Test2*/ +.test2 { +#font-color: green; +}"), result + ); + } + + [Test] + public void ParseFormattedRules_CanParse() + { + // base CSS + var css = Tabbed( +@"body { +#font-family:Arial; +} + +/**umb_name:Test*/ +.test { +#font-color: red; +#margin: 1rem; +} + +/**umb_name:Test2*/ +.test2 { +#font-color: green; +}"); + var rules = StylesheetHelper.ParseRules(css); + Assert.AreEqual(2, rules.Count()); + + Assert.AreEqual("Test", rules.First().Name); + Assert.AreEqual(".test", rules.First().Selector); + Assert.AreEqual( +@"font-color: red; +margin: 1rem;", rules.First().Styles); + + Assert.AreEqual("Test2", rules.Last().Name); + Assert.AreEqual(".test2", rules.Last().Selector); + Assert.AreEqual("font-color: green;", rules.Last().Styles); + } + + // can't put tabs in verbatim strings, so this will replace # with \t to test the CSS indents + // - and it's tabs because the editor uses tabs, not spaces... + private static string Tabbed(string input) => input.Replace("#", "\t"); + } } diff --git a/src/Umbraco.Tests/TestHelpers/Entities/MockedContentTypes.cs b/src/Umbraco.Tests/TestHelpers/Entities/MockedContentTypes.cs index 752d0ac97e..42beea7df3 100644 --- a/src/Umbraco.Tests/TestHelpers/Entities/MockedContentTypes.cs +++ b/src/Umbraco.Tests/TestHelpers/Entities/MockedContentTypes.cs @@ -362,12 +362,10 @@ namespace Umbraco.Tests.TestHelpers.Entities contentCollection.Add(new PropertyType(Constants.PropertyEditors.Aliases.NoEdit, ValueStorageType.Nvarchar) { Alias = "label", Name = "Label", Mandatory = false, SortOrder = 7, DataTypeId = -92 }); contentCollection.Add(new PropertyType(Constants.PropertyEditors.Aliases.DateTime, ValueStorageType.Date) { Alias = "dateTime", Name = "Date Time", Mandatory = false, SortOrder = 8, DataTypeId = -36 }); contentCollection.Add(new PropertyType(Constants.PropertyEditors.Aliases.ColorPicker, ValueStorageType.Nvarchar) { Alias = "colorPicker", Name = "Color Picker", Mandatory = false, SortOrder = 9, DataTypeId = -37 }); - //that one is gone in 7.4 - //contentCollection.Add(new PropertyType(Constants.PropertyEditors.FolderBrowserAlias, DataTypeDatabaseType.Nvarchar) { Alias = "folderBrowser", Name = "Folder Browser", Mandatory = false, SortOrder = 10, DataTypeDefinitionId = -38 }); - contentCollection.Add(new PropertyType(Constants.PropertyEditors.Aliases.DropDownListMultiple, ValueStorageType.Nvarchar) { Alias = "ddlMultiple", Name = "Dropdown List Multiple", Mandatory = false, SortOrder = 11, DataTypeId = -39 }); + contentCollection.Add(new PropertyType(Constants.PropertyEditors.Aliases.DropDownListFlexible, ValueStorageType.Nvarchar) { Alias = "ddlMultiple", Name = "Dropdown List Multiple", Mandatory = false, SortOrder = 11, DataTypeId = -39 }); contentCollection.Add(new PropertyType(Constants.PropertyEditors.Aliases.RadioButtonList, ValueStorageType.Nvarchar) { Alias = "rbList", Name = "Radio Button List", Mandatory = false, SortOrder = 12, DataTypeId = -40 }); contentCollection.Add(new PropertyType(Constants.PropertyEditors.Aliases.Date, ValueStorageType.Date) { Alias = "date", Name = "Date", Mandatory = false, SortOrder = 13, DataTypeId = -41 }); - contentCollection.Add(new PropertyType(Constants.PropertyEditors.Aliases.DropDownList, ValueStorageType.Integer) { Alias = "ddl", Name = "Dropdown List", Mandatory = false, SortOrder = 14, DataTypeId = -42 }); + contentCollection.Add(new PropertyType(Constants.PropertyEditors.Aliases.DropDownListFlexible, ValueStorageType.Integer) { Alias = "ddl", Name = "Dropdown List", Mandatory = false, SortOrder = 14, DataTypeId = -42 }); contentCollection.Add(new PropertyType(Constants.PropertyEditors.Aliases.CheckBoxList, ValueStorageType.Nvarchar) { Alias = "chklist", Name = "Checkbox List", Mandatory = false, SortOrder = 15, DataTypeId = -43 }); contentCollection.Add(new PropertyType(Constants.PropertyEditors.Aliases.ContentPicker, ValueStorageType.Integer) { Alias = "contentPicker", Name = "Content Picker", Mandatory = false, SortOrder = 16, DataTypeId = 1046 }); contentCollection.Add(new PropertyType(Constants.PropertyEditors.Aliases.MediaPicker, ValueStorageType.Integer) { Alias = "mediaPicker", Name = "Media Picker", Mandatory = false, SortOrder = 17, DataTypeId = 1048 }); @@ -375,11 +373,6 @@ namespace Umbraco.Tests.TestHelpers.Entities contentCollection.Add(new PropertyType(Constants.PropertyEditors.Aliases.RelatedLinks, ValueStorageType.Ntext) { Alias = "relatedLinks", Name = "Related Links", Mandatory = false, SortOrder = 21, DataTypeId = 1050 }); contentCollection.Add(new PropertyType(Constants.PropertyEditors.Aliases.Tags, ValueStorageType.Ntext) { Alias = "tags", Name = "Tags", Mandatory = false, SortOrder = 22, DataTypeId = 1041 }); - //contentCollection.Add(new PropertyType(Constants.PropertyEditors.UltraSimpleEditorAlias, DataTypeDatabaseType.Ntext) { Alias = "simpleEditor", Name = "Ultra Simple Editor", Mandatory = false, SortOrder = 19, DataTypeDefinitionId = 1038 }); - //contentCollection.Add(new PropertyType(Constants.PropertyEditors.UltimatePickerAlias, DataTypeDatabaseType.Ntext) { Alias = "ultimatePicker", Name = "Ultimate Picker", Mandatory = false, SortOrder = 20, DataTypeDefinitionId = 1039 }); - //contentCollection.Add(new PropertyType(Constants.PropertyEditors.MacroContainerAlias, DataTypeDatabaseType.Ntext) { Alias = "macroContainer", Name = "Macro Container", Mandatory = false, SortOrder = 23, DataTypeDefinitionId = 1042 }); - //contentCollection.Add(new PropertyType(Constants.PropertyEditors.ImageCropperAlias, DataTypeDatabaseType.Ntext) { Alias = "imgCropper", Name = "Image Cropper", Mandatory = false, SortOrder = 24, DataTypeDefinitionId = 1043 }); - contentType.PropertyGroups.Add(new PropertyGroup(contentCollection) { Name = "Content", SortOrder = 1 }); return contentType; diff --git a/src/Umbraco.Tests/TestHelpers/TestObjects.cs b/src/Umbraco.Tests/TestHelpers/TestObjects.cs index 2707c73607..4529c4f1ef 100644 --- a/src/Umbraco.Tests/TestHelpers/TestObjects.cs +++ b/src/Umbraco.Tests/TestHelpers/TestObjects.cs @@ -10,6 +10,7 @@ using Umbraco.Core; using Umbraco.Core.Cache; using Umbraco.Core.Composing; using Umbraco.Core.Configuration; +using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Events; using Umbraco.Core.IO; using Umbraco.Core.Logging; @@ -106,8 +107,10 @@ namespace Umbraco.Tests.TestHelpers CacheHelper cache, ILogger logger, IGlobalSettings globalSettings, + IUmbracoSettingsSection umbracoSettings, IEventMessagesFactory eventMessagesFactory, IEnumerable urlSegmentProviders, + TypeLoader typeLoader, IServiceFactory container = null) { if (scopeProvider == null) throw new ArgumentNullException(nameof(scopeProvider)); @@ -158,10 +161,11 @@ namespace Umbraco.Tests.TestHelpers var runtimeState = Mock.Of(); var idkMap = new IdkMap(scopeProvider); + var localizationService = GetLazyService(container, c => new LocalizationService(scopeProvider, logger, eventMessagesFactory, GetRepo(c), GetRepo(c), GetRepo(c))); var userService = GetLazyService(container, c => new UserService(scopeProvider, logger, eventMessagesFactory, runtimeState, GetRepo(c), GetRepo(c),globalSettings)); var dataTypeService = GetLazyService(container, c => new DataTypeService(scopeProvider, logger, eventMessagesFactory, GetRepo(c), GetRepo(c), GetRepo(c), GetRepo(c), GetRepo(c))); var contentService = GetLazyService(container, c => new ContentService(scopeProvider, logger, eventMessagesFactory, mediaFileSystem, GetRepo(c), GetRepo(c), GetRepo(c), GetRepo(c), GetRepo(c), GetRepo(c))); - var notificationService = GetLazyService(container, c => new NotificationService(scopeProvider, userService.Value, contentService.Value, logger, GetRepo(c),globalSettings)); + var notificationService = GetLazyService(container, c => new NotificationService(scopeProvider, userService.Value, contentService.Value, localizationService.Value, logger, GetRepo(c), globalSettings, umbracoSettings.Content)); var serverRegistrationService = GetLazyService(container, c => new ServerRegistrationService(scopeProvider, logger, eventMessagesFactory, GetRepo(c))); var memberGroupService = GetLazyService(container, c => new MemberGroupService(scopeProvider, logger, eventMessagesFactory, GetRepo(c))); var memberService = GetLazyService(container, c => new MemberService(scopeProvider, logger, eventMessagesFactory, memberGroupService.Value, mediaFileSystem, GetRepo(c), GetRepo(c), GetRepo(c), GetRepo(c))); @@ -169,7 +173,6 @@ namespace Umbraco.Tests.TestHelpers var contentTypeService = GetLazyService(container, c => new ContentTypeService(scopeProvider, logger, eventMessagesFactory, contentService.Value, GetRepo(c), GetRepo(c), GetRepo(c), GetRepo(c))); var mediaTypeService = GetLazyService(container, c => new MediaTypeService(scopeProvider, logger, eventMessagesFactory, mediaService.Value, GetRepo(c), GetRepo(c), GetRepo(c), GetRepo(c))); var fileService = GetLazyService(container, c => new FileService(scopeProvider, logger, eventMessagesFactory, GetRepo(c), GetRepo(c), GetRepo(c), GetRepo(c), GetRepo(c), GetRepo(c))); - var localizationService = GetLazyService(container, c => new LocalizationService(scopeProvider, logger, eventMessagesFactory, GetRepo(c), GetRepo(c), GetRepo(c))); var memberTypeService = GetLazyService(container, c => new MemberTypeService(scopeProvider, logger, eventMessagesFactory, memberService.Value, GetRepo(c), GetRepo(c), GetRepo(c))); var entityService = GetLazyService(container, c => new EntityService( @@ -181,7 +184,7 @@ namespace Umbraco.Tests.TestHelpers var macroService = GetLazyService(container, c => new MacroService(scopeProvider, logger, eventMessagesFactory, GetRepo(c), GetRepo(c))); var packagingService = GetLazyService(container, c => new PackagingService(logger, contentService.Value, contentTypeService.Value, mediaService.Value, macroService.Value, dataTypeService.Value, fileService.Value, localizationService.Value, entityService.Value, userService.Value, scopeProvider, urlSegmentProviders, GetRepo(c), GetRepo(c), new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty())))); var relationService = GetLazyService(container, c => new RelationService(scopeProvider, logger, eventMessagesFactory, entityService.Value, GetRepo(c), GetRepo(c))); - var treeService = GetLazyService(container, c => new ApplicationTreeService(logger, cache)); + var treeService = GetLazyService(container, c => new ApplicationTreeService(logger, cache, typeLoader)); var tagService = GetLazyService(container, c => new TagService(scopeProvider, logger, eventMessagesFactory, GetRepo(c))); var sectionService = GetLazyService(container, c => new SectionService(userService.Value, treeService.Value, scopeProvider, cache)); var redirectUrlService = GetLazyService(container, c => new RedirectUrlService(scopeProvider, logger, eventMessagesFactory, GetRepo(c))); diff --git a/src/Umbraco.Tests/TestHelpers/TestWithDatabaseBase.cs b/src/Umbraco.Tests/TestHelpers/TestWithDatabaseBase.cs index 21f1ce82b2..6b52137542 100644 --- a/src/Umbraco.Tests/TestHelpers/TestWithDatabaseBase.cs +++ b/src/Umbraco.Tests/TestHelpers/TestWithDatabaseBase.cs @@ -64,6 +64,8 @@ namespace Umbraco.Tests.TestHelpers internal ScopeProvider ScopeProvider => Current.ScopeProvider as ScopeProvider; + protected ISqlContext SqlContext => Container.GetInstance(); + public override void SetUp() { base.SetUp(); diff --git a/src/Umbraco.Tests/Testing/UmbracoTestBase.cs b/src/Umbraco.Tests/Testing/UmbracoTestBase.cs index 7f68f22a27..d80802b1cf 100644 --- a/src/Umbraco.Tests/Testing/UmbracoTestBase.cs +++ b/src/Umbraco.Tests/Testing/UmbracoTestBase.cs @@ -36,9 +36,10 @@ using Umbraco.Web; using Umbraco.Web.Services; using Umbraco.Examine; using Umbraco.Tests.Testing.Objects.Accessors; +using Umbraco.Web.Actions; using Umbraco.Web.Composing.CompositionRoots; using Umbraco.Web.ContentApps; -using Umbraco.Web._Legacy.Actions; + using Current = Umbraco.Core.Composing.Current; using Umbraco.Web.Routing; @@ -205,9 +206,7 @@ namespace Umbraco.Tests.Testing Container.RegisterSingleton(f => runtimeStateMock.Object); // ah... - Container.RegisterCollectionBuilder() - .SetProducer(Enumerable.Empty); - + Container.RegisterCollectionBuilder(); Container.RegisterCollectionBuilder(); Container.RegisterSingleton(); diff --git a/src/Umbraco.Tests/UI/LegacyDialogTests.cs b/src/Umbraco.Tests/UI/LegacyDialogTests.cs index ba7c4f0e66..be9b0d4d7e 100644 --- a/src/Umbraco.Tests/UI/LegacyDialogTests.cs +++ b/src/Umbraco.Tests/UI/LegacyDialogTests.cs @@ -23,9 +23,7 @@ namespace Umbraco.Tests.UI } } - [TestCase(typeof(MemberGroupTasks), Constants.Applications.Members)] - [TestCase(typeof(dictionaryTasks), Constants.Applications.Settings)] - [TestCase(typeof(macroTasks), Constants.Applications.Packages)] + [TestCase(typeof(macroTasks), Constants.Applications.Settings)] [TestCase(typeof(CreatedPackageTasks), Constants.Applications.Packages)] public void Check_Assigned_Apps_For_Tasks(Type taskType, string app) { diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 61ae537529..322be82ca7 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -119,8 +119,10 @@ + + @@ -128,6 +130,8 @@ + + @@ -404,7 +408,6 @@ - diff --git a/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs b/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs index dedf04488c..8b57e10849 100644 --- a/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs +++ b/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs @@ -71,11 +71,7 @@ namespace Umbraco.Tests.UmbracoExamine contentService = Mock.Of( x => x.GetPagedDescendants( - It.IsAny(), It.IsAny(), It.IsAny(), out longTotalRecs, It.IsAny(), It.IsAny(), It.IsAny()) - == - allRecs - && x.GetPagedDescendants( - It.IsAny(), It.IsAny(), It.IsAny(), out longTotalRecs, It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>()) + It.IsAny(), It.IsAny(), It.IsAny(), out longTotalRecs, It.IsAny>(), It.IsAny()) == allRecs); } @@ -116,12 +112,7 @@ namespace Umbraco.Tests.UmbracoExamine mediaServiceMock .Setup(x => x.GetPagedDescendants( - It.IsAny(), It.IsAny(), It.IsAny(), out totalRecs, It.IsAny(), It.IsAny(), It.IsAny()) - ).Returns(() => allRecs); - - mediaServiceMock - .Setup(x => x.GetPagedDescendants( - It.IsAny(), It.IsAny(), It.IsAny(), out totalRecs, It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>()) + It.IsAny(), It.IsAny(), It.IsAny(), out totalRecs, It.IsAny>(), It.IsAny()) ).Returns(() => allRecs); //mediaServiceMock.Setup(service => service.GetPagedXmlEntries(It.IsAny(), It.IsAny(), It.IsAny(), out longTotalRecs)) diff --git a/src/Umbraco.Tests/UmbracoExamine/SearchTests.cs b/src/Umbraco.Tests/UmbracoExamine/SearchTests.cs index 2d440b8453..768d1c735c 100644 --- a/src/Umbraco.Tests/UmbracoExamine/SearchTests.cs +++ b/src/Umbraco.Tests/UmbracoExamine/SearchTests.cs @@ -49,7 +49,7 @@ namespace Umbraco.Tests.UmbracoExamine .ToArray(); var contentService = Mock.Of( x => x.GetPagedDescendants( - It.IsAny(), It.IsAny(), It.IsAny(), out totalRecs, It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>()) + It.IsAny(), It.IsAny(), It.IsAny(), out totalRecs, It.IsAny>(), It.IsAny()) == allRecs); diff --git a/src/Umbraco.Tests/Web/Controllers/BackOfficeControllerUnitTests.cs b/src/Umbraco.Tests/Web/Controllers/BackOfficeControllerUnitTests.cs index 850870f395..fa9335bc3f 100644 --- a/src/Umbraco.Tests/Web/Controllers/BackOfficeControllerUnitTests.cs +++ b/src/Umbraco.Tests/Web/Controllers/BackOfficeControllerUnitTests.cs @@ -22,20 +22,5 @@ namespace Umbraco.Tests.Web.Controllers }; - [TestCaseSource("TestLegacyJsActionPaths")] - public void Separates_Legacy_JsActions_By_Block_Or_Url(object[] jsActions) - { - var jsBlocks = - BackOfficeController.GetLegacyActionJsForActions(BackOfficeController.LegacyJsActionType.JsBlock, - jsActions.Select(n => n.ToString())); - - var jsUrls = - BackOfficeController.GetLegacyActionJsForActions(BackOfficeController.LegacyJsActionType.JsUrl, - jsActions.Select(n => n.ToString())); - - Assert.That(jsBlocks.Count() == 4); - Assert.That(jsUrls.Count() == 3); - Assert.That(jsUrls.Last().StartsWith("~/") == false); - } } } diff --git a/src/Umbraco.Tests/Web/Controllers/ContentControllerTests.cs b/src/Umbraco.Tests/Web/Controllers/ContentControllerTests.cs index f75371d203..26a7403dac 100644 --- a/src/Umbraco.Tests/Web/Controllers/ContentControllerTests.cs +++ b/src/Umbraco.Tests/Web/Controllers/ContentControllerTests.cs @@ -22,7 +22,7 @@ using Umbraco.Web; using Umbraco.Web.Editors; using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.PublishedCache; -using Umbraco.Web._Legacy.Actions; + using Task = System.Threading.Tasks.Task; using Umbraco.Core.Dictionary; using Umbraco.Web.PropertyEditors; @@ -30,6 +30,7 @@ using System; using Umbraco.Web.WebApi; using Umbraco.Web.Trees; using System.Globalization; +using Umbraco.Web.Actions; namespace Umbraco.Tests.Web.Controllers { @@ -53,10 +54,10 @@ namespace Umbraco.Tests.Web.Controllers { new EntityPermission(0, 123, new[] { - ActionBrowse.Instance.Letter.ToString(), - ActionUpdate.Instance.Letter.ToString(), - ActionPublish.Instance.Letter.ToString(), - ActionNew.Instance.Letter.ToString() + ActionBrowse.ActionLetter.ToString(), + ActionUpdate.ActionLetter.ToString(), + ActionPublish.ActionLetter.ToString(), + ActionNew.ActionLetter.ToString() }), }))); diff --git a/src/Umbraco.Tests/Web/Controllers/ContentControllerUnitTests.cs b/src/Umbraco.Tests/Web/Controllers/ContentControllerUnitTests.cs index 21aa739e23..f55a4e593b 100644 --- a/src/Umbraco.Tests/Web/Controllers/ContentControllerUnitTests.cs +++ b/src/Umbraco.Tests/Web/Controllers/ContentControllerUnitTests.cs @@ -5,6 +5,7 @@ using NUnit.Framework; using Umbraco.Core.Models; using Umbraco.Core.Models.Entities; using Umbraco.Core.Models.Membership; +using Umbraco.Core.Security; using Umbraco.Core.Services; using Umbraco.Web.Editors; @@ -33,14 +34,14 @@ namespace Umbraco.Tests.Web.Controllers var userService = userServiceMock.Object; //act - var result = ContentController.CheckPermissions(new Dictionary(), user, userService, contentService, entityService, 1234); + var result = ContentPermissionsHelper.CheckPermissions(1234, user, userService, contentService, entityService, out var foundContent); //assert - Assert.IsTrue(result); + Assert.AreEqual(ContentPermissionsHelper.ContentAccess.Granted, result); } [Test] - public void Throws_Exception_When_No_Content_Found() + public void No_Content_Found() { //arrange var userMock = new Mock(); @@ -60,8 +61,11 @@ namespace Umbraco.Tests.Web.Controllers var entityServiceMock = new Mock(); var entityService = entityServiceMock.Object; - //act/assert - Assert.Throws(() => ContentController.CheckPermissions(new Dictionary(), user, userService, contentService, entityService, 1234, new[] { 'F' })); + //act + var result = ContentPermissionsHelper.CheckPermissions(1234, user, userService, contentService, entityService, out var foundContent, new[] { 'F' }); + + //assert + Assert.AreEqual(ContentPermissionsHelper.ContentAccess.NotFound, result); } [Test] @@ -89,10 +93,10 @@ namespace Umbraco.Tests.Web.Controllers var entityService = entityServiceMock.Object; //act - var result = ContentController.CheckPermissions(new Dictionary(), user, userService, contentService, entityService, 1234, new[] { 'F' }); + var result = ContentPermissionsHelper.CheckPermissions(1234, user, userService, contentService, entityService, out var foundContent, new[] { 'F' }); //assert - Assert.IsFalse(result); + Assert.AreEqual(ContentPermissionsHelper.ContentAccess.Denied, result); } [Test] @@ -120,10 +124,10 @@ namespace Umbraco.Tests.Web.Controllers var entityService = entityServiceMock.Object; //act - var result = ContentController.CheckPermissions(new Dictionary(), user, userService, contentService, entityService, 1234, new[] { 'F' }); + var result = ContentPermissionsHelper.CheckPermissions(1234, user, userService, contentService, entityService, out var foundContent, new[] { 'F' }); //assert - Assert.IsFalse(result); + Assert.AreEqual(ContentPermissionsHelper.ContentAccess.Denied, result); } [Test] @@ -152,10 +156,10 @@ namespace Umbraco.Tests.Web.Controllers var entityService = entityServiceMock.Object; //act - var result = ContentController.CheckPermissions(new Dictionary(), user, userService, contentService, entityService, 1234, new[] { 'F' }); + var result = ContentPermissionsHelper.CheckPermissions(1234, user, userService, contentService, entityService, out var foundContent, new[] { 'F' }); //assert - Assert.IsTrue(result); + Assert.AreEqual(ContentPermissionsHelper.ContentAccess.Granted, result); } [Test] @@ -174,10 +178,10 @@ namespace Umbraco.Tests.Web.Controllers var entityService = entityServiceMock.Object; //act - var result = ContentController.CheckPermissions(new Dictionary(), user, userService, contentService, entityService, -1); + var result = ContentPermissionsHelper.CheckPermissions(-1, user, userService, contentService, entityService, out var foundContent); //assert - Assert.IsTrue(result); + Assert.AreEqual(ContentPermissionsHelper.ContentAccess.Granted, result); } [Test] @@ -196,10 +200,10 @@ namespace Umbraco.Tests.Web.Controllers var entityService = entityServiceMock.Object; //act - var result = ContentController.CheckPermissions(new Dictionary(), user, userService, contentService, entityService, -20); + var result = ContentPermissionsHelper.CheckPermissions(-20, user, userService, contentService, entityService, out var foundContent); //assert - Assert.IsTrue(result); + Assert.AreEqual(ContentPermissionsHelper.ContentAccess.Granted, result); } [Test] @@ -220,10 +224,10 @@ namespace Umbraco.Tests.Web.Controllers var entityService = entityServiceMock.Object; //act - var result = ContentController.CheckPermissions(new Dictionary(), user, userService, contentService, entityService, -20); + var result = ContentPermissionsHelper.CheckPermissions(-20, user, userService, contentService, entityService, out var foundContent); //assert - Assert.IsFalse(result); + Assert.AreEqual(ContentPermissionsHelper.ContentAccess.Denied, result); } [Test] @@ -244,10 +248,10 @@ namespace Umbraco.Tests.Web.Controllers var entityService = entityServiceMock.Object; //act - var result = ContentController.CheckPermissions(new Dictionary(), user, userService, contentService, entityService, -1); + var result = ContentPermissionsHelper.CheckPermissions(-1, user, userService, contentService, entityService, out var foundContent); //assert - Assert.IsFalse(result); + Assert.AreEqual(ContentPermissionsHelper.ContentAccess.Denied, result); } [Test] @@ -274,10 +278,10 @@ namespace Umbraco.Tests.Web.Controllers //act - var result = ContentController.CheckPermissions(new Dictionary(), user, userService, contentService, entityService, -1, new[] { 'A' }); + var result = ContentPermissionsHelper.CheckPermissions(-1, user, userService, contentService, entityService, out var foundContent, new[] { 'A' }); //assert - Assert.IsTrue(result); + Assert.AreEqual(ContentPermissionsHelper.ContentAccess.Granted, result); } [Test] @@ -302,10 +306,10 @@ namespace Umbraco.Tests.Web.Controllers var contentService = contentServiceMock.Object; //act - var result = ContentController.CheckPermissions(new Dictionary(), user, userService, contentService, entityService, -1, new[] { 'B' }); + var result = ContentPermissionsHelper.CheckPermissions(-1, user, userService, contentService, entityService, out var foundContent, new[] { 'B' }); //assert - Assert.IsFalse(result); + Assert.AreEqual(ContentPermissionsHelper.ContentAccess.Denied, result); } [Test] @@ -332,10 +336,10 @@ namespace Umbraco.Tests.Web.Controllers var contentService = contentServiceMock.Object; //act - var result = ContentController.CheckPermissions(new Dictionary(), user, userService, contentService, entityService, -20, new[] { 'A' }); + var result = ContentPermissionsHelper.CheckPermissions(-20, user, userService, contentService, entityService, out var foundContent, new[] { 'A' }); //assert - Assert.IsTrue(result); + Assert.AreEqual(ContentPermissionsHelper.ContentAccess.Granted, result); } [Test] @@ -360,10 +364,10 @@ namespace Umbraco.Tests.Web.Controllers var contentService = contentServiceMock.Object; //act - var result = ContentController.CheckPermissions(new Dictionary(), user, userService, contentService, entityService, -20, new[] { 'B' }); + var result = ContentPermissionsHelper.CheckPermissions(-20, user, userService, contentService, entityService, out var foundContent, new[] { 'B' }); //assert - Assert.IsFalse(result); + Assert.AreEqual(ContentPermissionsHelper.ContentAccess.Denied, result); } } diff --git a/src/Umbraco.Tests/Web/TemplateUtilitiesTests.cs b/src/Umbraco.Tests/Web/TemplateUtilitiesTests.cs index df21739c0b..1e6229fa4c 100644 --- a/src/Umbraco.Tests/Web/TemplateUtilitiesTests.cs +++ b/src/Umbraco.Tests/Web/TemplateUtilitiesTests.cs @@ -1,8 +1,7 @@ using System; -using System.Globalization; +using System.Linq; using System.Web; using LightInject; -using HtmlAgilityPack; using Moq; using NUnit.Framework; using Umbraco.Core; @@ -12,16 +11,15 @@ using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.Services; using Umbraco.Tests.TestHelpers; -using Umbraco.Tests.TestHelpers.Stubs; using Umbraco.Tests.Testing.Objects.Accessors; using Umbraco.Web; using Umbraco.Web.PublishedCache; using Umbraco.Web.Routing; using Umbraco.Web.Security; using Umbraco.Web.Templates; -using System.Linq; -using Umbraco.Core.Services; +using Umbraco.Core.Configuration; namespace Umbraco.Tests.Web { @@ -49,6 +47,8 @@ namespace Umbraco.Tests.Web Umbraco.Web.Composing.Current.UmbracoContextAccessor = new TestUmbracoContextAccessor(); Udi.ResetUdiTypes(); + + UmbracoConfig.For.SetUmbracoSettings(SettingsForTests.GetDefaultUmbracoSettings()); } [TearDown] @@ -63,14 +63,6 @@ namespace Umbraco.Tests.Web [TestCase("hello href=\"{localLink:umb://document-type/9931BDE0AAC34BABB838909A7B47570E}\" world ", "hello href=\"/my-test-url\" world ")] //this one has an invalid char so won't match [TestCase("hello href=\"{localLink:umb^://document-type/9931BDE0-AAC3-4BAB-B838-909A7B47570E}\" world ", "hello href=\"{localLink:umb^://document-type/9931BDE0-AAC3-4BAB-B838-909A7B47570E}\" world ")] - // with a-tag with data-udi attribute, that needs to be stripped - [TestCase("hello world ", "hello world ")] - // with a-tag with data-udi attribute spelled wrong, so don't need stripping - [TestCase("hello world ", "hello world ")] - // with a img-tag with data-udi id, that needs to be strippde - [TestCase("hello world ", "hello world ")] - // with a img-tag with data-udi id spelled wrong, so don't need stripping - [TestCase("hello world ", "hello world ")] public void ParseLocalLinks(string input, string result) { var serviceCtxMock = new TestObjects(null).GetServiceContextMock(); @@ -90,7 +82,7 @@ namespace Umbraco.Tests.Web .Returns((UmbracoContext umbCtx, IPublishedContent content, UrlProviderMode mode, string culture, Uri url) => "/my-test-url"); var globalSettings = SettingsForTests.GenerateMockGlobalSettings(); - + var contentType = new PublishedContentType(666, "alias", PublishedItemType.Content, Enumerable.Empty(), Enumerable.Empty(), ContentVariation.Nothing); var publishedContent = Mock.Of(); Mock.Get(publishedContent).Setup(x => x.Id).Returns(1234); @@ -111,7 +103,7 @@ namespace Umbraco.Tests.Web //setup a quick mock of the WebRouting section Mock.Of(section => section.WebRouting == Mock.Of(routingSection => routingSection.UrlProviderMode == "AutoLegacy")), //pass in the custom url provider - new[] { testUrlProvider.Object }, + new[]{ testUrlProvider.Object }, globalSettings, new TestVariationContextAccessor(), true)) @@ -121,27 +113,5 @@ namespace Umbraco.Tests.Web Assert.AreEqual(result, output); } } - - [Test] - public void StripDataUdiAttributesUsingSrtringOnLinks() - { - var input = "hello world "; - var expected = "hello world "; - - var result = TemplateUtilities.StripUdiDataAttributes(input); - - Assert.AreEqual(expected, result); - } - - [Test] - public void StripDataUdiAttributesUsingStringOnImages() - { - var input = "hello world "; - var expected = "hello world "; - - var result = TemplateUtilities.StripUdiDataAttributes(input); - - Assert.AreEqual(expected, result); - } } } diff --git a/src/Umbraco.Web.UI.Client/gulpfile.js b/src/Umbraco.Web.UI.Client/gulpfile.js index f614f24b53..cd7f6f4bef 100644 --- a/src/Umbraco.Web.UI.Client/gulpfile.js +++ b/src/Umbraco.Web.UI.Client/gulpfile.js @@ -6,21 +6,22 @@ var wrap = require("gulp-wrap-js"); var sort = require('gulp-sort'); var connect = require('gulp-connect'); var open = require('gulp-open'); -const babel = require("gulp-babel"); +var babel = require("gulp-babel"); var runSequence = require('run-sequence'); -const imagemin = require('gulp-imagemin'); +var imagemin = require('gulp-imagemin'); var _ = require('lodash'); var MergeStream = require('merge-stream'); // js -const eslint = require('gulp-eslint'); +var eslint = require('gulp-eslint'); //Less + css var postcss = require('gulp-postcss'); var less = require('gulp-less'); var autoprefixer = require('autoprefixer'); var cssnano = require('cssnano'); +var cleanCss = require("gulp-clean-css"); // Documentation var gulpDocs = require('gulp-ngdocs'); @@ -49,14 +50,14 @@ function processJs(files, out) { } function processLess(files, out) { - var processors = [ autoprefixer, - cssnano({zindex: false}), + cssnano({zindex: false}) ]; return gulp.src(files) .pipe(less()) + .pipe(cleanCss()) .pipe(postcss(processors)) .pipe(rename(out)) .pipe(gulp.dest(root + targets.css)); @@ -158,10 +159,13 @@ gulp.task('dependencies', function () { "./node_modules/ace-builds/src-min-noconflict/ext-settings_menu.js", "./node_modules/ace-builds/src-min-noconflict/snippets/text.js", "./node_modules/ace-builds/src-min-noconflict/snippets/javascript.js", + "./node_modules/ace-builds/src-min-noconflict/snippets/css.js", "./node_modules/ace-builds/src-min-noconflict/theme-chrome.js", "./node_modules/ace-builds/src-min-noconflict/mode-razor.js", "./node_modules/ace-builds/src-min-noconflict/mode-javascript.js", - "./node_modules/ace-builds/src-min-noconflict/worker-javascript.js" + "./node_modules/ace-builds/src-min-noconflict/mode-css.js", + "./node_modules/ace-builds/src-min-noconflict/worker-javascript.js", + "./node_modules/ace-builds/src-min-noconflict/worker-css.js" ], "base": "./node_modules/ace-builds" }, @@ -311,7 +315,7 @@ gulp.task('dependencies', function () { "src": [ "./node_modules/moment/min/moment.min.js", "./node_modules/moment/min/moment-with-locales.js", - "./node_modules/moment/min/moment-with-locales.min.js", + "./node_modules/moment/min/moment-with-locales.min.js" ], "base": "./node_modules/moment/min" }, diff --git a/src/Umbraco.Web.UI.Client/lib/angular-bootstrap/ui-bootstrap-tpls-0.10.0.min.js b/src/Umbraco.Web.UI.Client/lib/angular-bootstrap/ui-bootstrap-tpls-0.10.0.min.js deleted file mode 100644 index 0830f35b07..0000000000 --- a/src/Umbraco.Web.UI.Client/lib/angular-bootstrap/ui-bootstrap-tpls-0.10.0.min.js +++ /dev/null @@ -1,9 +0,0 @@ -/* - * angular-ui-bootstrap - * http://angular-ui.github.io/bootstrap/ - - * Version: 0.10.0 - 2014-01-13 - * License: MIT - */ -angular.module("ui.bootstrap",["ui.bootstrap.tpls","ui.bootstrap.transition","ui.bootstrap.collapse","ui.bootstrap.accordion","ui.bootstrap.alert","ui.bootstrap.bindHtml","ui.bootstrap.buttons","ui.bootstrap.carousel","ui.bootstrap.position","ui.bootstrap.datepicker","ui.bootstrap.dropdownToggle","ui.bootstrap.modal","ui.bootstrap.pagination","ui.bootstrap.tooltip","ui.bootstrap.popover","ui.bootstrap.progressbar","ui.bootstrap.rating","ui.bootstrap.tabs","ui.bootstrap.timepicker","ui.bootstrap.typeahead"]),angular.module("ui.bootstrap.tpls",["template/accordion/accordion-group.html","template/accordion/accordion.html","template/alert/alert.html","template/carousel/carousel.html","template/carousel/slide.html","template/datepicker/datepicker.html","template/datepicker/popup.html","template/modal/backdrop.html","template/modal/window.html","template/pagination/pager.html","template/pagination/pagination.html","template/tooltip/tooltip-html-unsafe-popup.html","template/tooltip/tooltip-popup.html","template/popover/popover.html","template/progressbar/bar.html","template/progressbar/progress.html","template/progressbar/progressbar.html","template/rating/rating.html","template/tabs/tab.html","template/tabs/tabset.html","template/timepicker/timepicker.html","template/typeahead/typeahead-match.html","template/typeahead/typeahead-popup.html"]),angular.module("ui.bootstrap.transition",[]).factory("$transition",["$q","$timeout","$rootScope",function(a,b,c){function d(a){for(var b in a)if(void 0!==f.style[b])return a[b]}var e=function(d,f,g){g=g||{};var h=a.defer(),i=e[g.animation?"animationEndEventName":"transitionEndEventName"],j=function(){c.$apply(function(){d.unbind(i,j),h.resolve(d)})};return i&&d.bind(i,j),b(function(){angular.isString(f)?d.addClass(f):angular.isFunction(f)?f(d):angular.isObject(f)&&d.css(f),i||h.resolve(d)}),h.promise.cancel=function(){i&&d.unbind(i,j),h.reject("Transition cancelled")},h.promise},f=document.createElement("trans"),g={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd",transition:"transitionend"},h={WebkitTransition:"webkitAnimationEnd",MozTransition:"animationend",OTransition:"oAnimationEnd",transition:"animationend"};return e.transitionEndEventName=d(g),e.animationEndEventName=d(h),e}]),angular.module("ui.bootstrap.collapse",["ui.bootstrap.transition"]).directive("collapse",["$transition",function(a){return{link:function(b,c,d){function e(b){function d(){j===e&&(j=void 0)}var e=a(c,b);return j&&j.cancel(),j=e,e.then(d,d),e}function f(){k?(k=!1,g()):(c.removeClass("collapse").addClass("collapsing"),e({height:c[0].scrollHeight+"px"}).then(g))}function g(){c.removeClass("collapsing"),c.addClass("collapse in"),c.css({height:"auto"})}function h(){if(k)k=!1,i(),c.css({height:0});else{c.css({height:c[0].scrollHeight+"px"});{c[0].offsetWidth}c.removeClass("collapse in").addClass("collapsing"),e({height:0}).then(i)}}function i(){c.removeClass("collapsing"),c.addClass("collapse")}var j,k=!0;b.$watch(d.collapse,function(a){a?h():f()})}}}]),angular.module("ui.bootstrap.accordion",["ui.bootstrap.collapse"]).constant("accordionConfig",{closeOthers:!0}).controller("AccordionController",["$scope","$attrs","accordionConfig",function(a,b,c){this.groups=[],this.closeOthers=function(d){var e=angular.isDefined(b.closeOthers)?a.$eval(b.closeOthers):c.closeOthers;e&&angular.forEach(this.groups,function(a){a!==d&&(a.isOpen=!1)})},this.addGroup=function(a){var b=this;this.groups.push(a),a.$on("$destroy",function(){b.removeGroup(a)})},this.removeGroup=function(a){var b=this.groups.indexOf(a);-1!==b&&this.groups.splice(this.groups.indexOf(a),1)}}]).directive("accordion",function(){return{restrict:"EA",controller:"AccordionController",transclude:!0,replace:!1,templateUrl:"template/accordion/accordion.html"}}).directive("accordionGroup",["$parse",function(a){return{require:"^accordion",restrict:"EA",transclude:!0,replace:!0,templateUrl:"template/accordion/accordion-group.html",scope:{heading:"@"},controller:function(){this.setHeading=function(a){this.heading=a}},link:function(b,c,d,e){var f,g;e.addGroup(b),b.isOpen=!1,d.isOpen&&(f=a(d.isOpen),g=f.assign,b.$parent.$watch(f,function(a){b.isOpen=!!a})),b.$watch("isOpen",function(a){a&&e.closeOthers(b),g&&g(b.$parent,a)})}}}]).directive("accordionHeading",function(){return{restrict:"EA",transclude:!0,template:"",replace:!0,require:"^accordionGroup",compile:function(a,b,c){return function(a,b,d,e){e.setHeading(c(a,function(){}))}}}}).directive("accordionTransclude",function(){return{require:"^accordionGroup",link:function(a,b,c,d){a.$watch(function(){return d[c.accordionTransclude]},function(a){a&&(b.html(""),b.append(a))})}}}),angular.module("ui.bootstrap.alert",[]).controller("AlertController",["$scope","$attrs",function(a,b){a.closeable="close"in b}]).directive("alert",function(){return{restrict:"EA",controller:"AlertController",templateUrl:"template/alert/alert.html",transclude:!0,replace:!0,scope:{type:"=",close:"&"}}}),angular.module("ui.bootstrap.bindHtml",[]).directive("bindHtmlUnsafe",function(){return function(a,b,c){b.addClass("ng-binding").data("$binding",c.bindHtmlUnsafe),a.$watch(c.bindHtmlUnsafe,function(a){b.html(a||"")})}}),angular.module("ui.bootstrap.buttons",[]).constant("buttonConfig",{activeClass:"active",toggleEvent:"click"}).controller("ButtonsController",["buttonConfig",function(a){this.activeClass=a.activeClass||"active",this.toggleEvent=a.toggleEvent||"click"}]).directive("btnRadio",function(){return{require:["btnRadio","ngModel"],controller:"ButtonsController",link:function(a,b,c,d){var e=d[0],f=d[1];f.$render=function(){b.toggleClass(e.activeClass,angular.equals(f.$modelValue,a.$eval(c.btnRadio)))},b.bind(e.toggleEvent,function(){b.hasClass(e.activeClass)||a.$apply(function(){f.$setViewValue(a.$eval(c.btnRadio)),f.$render()})})}}}).directive("btnCheckbox",function(){return{require:["btnCheckbox","ngModel"],controller:"ButtonsController",link:function(a,b,c,d){function e(){return g(c.btnCheckboxTrue,!0)}function f(){return g(c.btnCheckboxFalse,!1)}function g(b,c){var d=a.$eval(b);return angular.isDefined(d)?d:c}var h=d[0],i=d[1];i.$render=function(){b.toggleClass(h.activeClass,angular.equals(i.$modelValue,e()))},b.bind(h.toggleEvent,function(){a.$apply(function(){i.$setViewValue(b.hasClass(h.activeClass)?f():e()),i.$render()})})}}}),angular.module("ui.bootstrap.carousel",["ui.bootstrap.transition"]).controller("CarouselController",["$scope","$timeout","$transition","$q",function(a,b,c){function d(){e();var c=+a.interval;!isNaN(c)&&c>=0&&(g=b(f,c))}function e(){g&&(b.cancel(g),g=null)}function f(){h?(a.next(),d()):a.pause()}var g,h,i=this,j=i.slides=[],k=-1;i.currentSlide=null;var l=!1;i.select=function(e,f){function g(){if(!l){if(i.currentSlide&&angular.isString(f)&&!a.noTransition&&e.$element){e.$element.addClass(f);{e.$element[0].offsetWidth}angular.forEach(j,function(a){angular.extend(a,{direction:"",entering:!1,leaving:!1,active:!1})}),angular.extend(e,{direction:f,active:!0,entering:!0}),angular.extend(i.currentSlide||{},{direction:f,leaving:!0}),a.$currentTransition=c(e.$element,{}),function(b,c){a.$currentTransition.then(function(){h(b,c)},function(){h(b,c)})}(e,i.currentSlide)}else h(e,i.currentSlide);i.currentSlide=e,k=m,d()}}function h(b,c){angular.extend(b,{direction:"",active:!0,leaving:!1,entering:!1}),angular.extend(c||{},{direction:"",active:!1,leaving:!1,entering:!1}),a.$currentTransition=null}var m=j.indexOf(e);void 0===f&&(f=m>k?"next":"prev"),e&&e!==i.currentSlide&&(a.$currentTransition?(a.$currentTransition.cancel(),b(g)):g())},a.$on("$destroy",function(){l=!0}),i.indexOfSlide=function(a){return j.indexOf(a)},a.next=function(){var b=(k+1)%j.length;return a.$currentTransition?void 0:i.select(j[b],"next")},a.prev=function(){var b=0>k-1?j.length-1:k-1;return a.$currentTransition?void 0:i.select(j[b],"prev")},a.select=function(a){i.select(a)},a.isActive=function(a){return i.currentSlide===a},a.slides=function(){return j},a.$watch("interval",d),a.$on("$destroy",e),a.play=function(){h||(h=!0,d())},a.pause=function(){a.noPause||(h=!1,e())},i.addSlide=function(b,c){b.$element=c,j.push(b),1===j.length||b.active?(i.select(j[j.length-1]),1==j.length&&a.play()):b.active=!1},i.removeSlide=function(a){var b=j.indexOf(a);j.splice(b,1),j.length>0&&a.active?b>=j.length?i.select(j[b-1]):i.select(j[b]):k>b&&k--}}]).directive("carousel",[function(){return{restrict:"EA",transclude:!0,replace:!0,controller:"CarouselController",require:"carousel",templateUrl:"template/carousel/carousel.html",scope:{interval:"=",noTransition:"=",noPause:"="}}}]).directive("slide",["$parse",function(a){return{require:"^carousel",restrict:"EA",transclude:!0,replace:!0,templateUrl:"template/carousel/slide.html",scope:{},link:function(b,c,d,e){if(d.active){var f=a(d.active),g=f.assign,h=b.active=f(b.$parent);b.$watch(function(){var a=f(b.$parent);return a!==b.active&&(a!==h?h=b.active=a:g(b.$parent,a=h=b.active)),a})}e.addSlide(b,c),b.$on("$destroy",function(){e.removeSlide(b)}),b.$watch("active",function(a){a&&e.select(b)})}}}]),angular.module("ui.bootstrap.position",[]).factory("$position",["$document","$window",function(a,b){function c(a,c){return a.currentStyle?a.currentStyle[c]:b.getComputedStyle?b.getComputedStyle(a)[c]:a.style[c]}function d(a){return"static"===(c(a,"position")||"static")}var e=function(b){for(var c=a[0],e=b.offsetParent||c;e&&e!==c&&d(e);)e=e.offsetParent;return e||c};return{position:function(b){var c=this.offset(b),d={top:0,left:0},f=e(b[0]);f!=a[0]&&(d=this.offset(angular.element(f)),d.top+=f.clientTop-f.scrollTop,d.left+=f.clientLeft-f.scrollLeft);var g=b[0].getBoundingClientRect();return{width:g.width||b.prop("offsetWidth"),height:g.height||b.prop("offsetHeight"),top:c.top-d.top,left:c.left-d.left}},offset:function(c){var d=c[0].getBoundingClientRect();return{width:d.width||c.prop("offsetWidth"),height:d.height||c.prop("offsetHeight"),top:d.top+(b.pageYOffset||a[0].body.scrollTop||a[0].documentElement.scrollTop),left:d.left+(b.pageXOffset||a[0].body.scrollLeft||a[0].documentElement.scrollLeft)}}}}]),angular.module("ui.bootstrap.datepicker",["ui.bootstrap.position"]).constant("datepickerConfig",{dayFormat:"dd",monthFormat:"MMMM",yearFormat:"yyyy",dayHeaderFormat:"EEE",dayTitleFormat:"MMMM yyyy",monthTitleFormat:"yyyy",showWeeks:!0,startingDay:0,yearRange:20,minDate:null,maxDate:null}).controller("DatepickerController",["$scope","$attrs","dateFilter","datepickerConfig",function(a,b,c,d){function e(b,c){return angular.isDefined(b)?a.$parent.$eval(b):c}function f(a,b){return new Date(a,b,0).getDate()}function g(a,b){for(var c=new Array(b),d=a,e=0;b>e;)c[e++]=new Date(d),d.setDate(d.getDate()+1);return c}function h(a,b,d,e){return{date:a,label:c(a,b),selected:!!d,secondary:!!e}}var i={day:e(b.dayFormat,d.dayFormat),month:e(b.monthFormat,d.monthFormat),year:e(b.yearFormat,d.yearFormat),dayHeader:e(b.dayHeaderFormat,d.dayHeaderFormat),dayTitle:e(b.dayTitleFormat,d.dayTitleFormat),monthTitle:e(b.monthTitleFormat,d.monthTitleFormat)},j=e(b.startingDay,d.startingDay),k=e(b.yearRange,d.yearRange);this.minDate=d.minDate?new Date(d.minDate):null,this.maxDate=d.maxDate?new Date(d.maxDate):null,this.modes=[{name:"day",getVisibleDates:function(a,b){var d=a.getFullYear(),e=a.getMonth(),k=new Date(d,e,1),l=j-k.getDay(),m=l>0?7-l:-l,n=new Date(k),o=0;m>0&&(n.setDate(-m+1),o+=m),o+=f(d,e+1),o+=(7-o%7)%7;for(var p=g(n,o),q=new Array(7),r=0;o>r;r++){var s=new Date(p[r]);p[r]=h(s,i.day,b&&b.getDate()===s.getDate()&&b.getMonth()===s.getMonth()&&b.getFullYear()===s.getFullYear(),s.getMonth()!==e)}for(var t=0;7>t;t++)q[t]=c(p[t].date,i.dayHeader);return{objects:p,title:c(a,i.dayTitle),labels:q}},compare:function(a,b){return new Date(a.getFullYear(),a.getMonth(),a.getDate())-new Date(b.getFullYear(),b.getMonth(),b.getDate())},split:7,step:{months:1}},{name:"month",getVisibleDates:function(a,b){for(var d=new Array(12),e=a.getFullYear(),f=0;12>f;f++){var g=new Date(e,f,1);d[f]=h(g,i.month,b&&b.getMonth()===f&&b.getFullYear()===e)}return{objects:d,title:c(a,i.monthTitle)}},compare:function(a,b){return new Date(a.getFullYear(),a.getMonth())-new Date(b.getFullYear(),b.getMonth())},split:3,step:{years:1}},{name:"year",getVisibleDates:function(a,b){for(var c=new Array(k),d=a.getFullYear(),e=parseInt((d-1)/k,10)*k+1,f=0;k>f;f++){var g=new Date(e+f,0,1);c[f]=h(g,i.year,b&&b.getFullYear()===g.getFullYear())}return{objects:c,title:[c[0].label,c[k-1].label].join(" - ")}},compare:function(a,b){return a.getFullYear()-b.getFullYear()},split:5,step:{years:k}}],this.isDisabled=function(b,c){var d=this.modes[c||0];return this.minDate&&d.compare(b,this.minDate)<0||this.maxDate&&d.compare(b,this.maxDate)>0||a.dateDisabled&&a.dateDisabled({date:b,mode:d.name})}}]).directive("datepicker",["dateFilter","$parse","datepickerConfig","$log",function(a,b,c,d){return{restrict:"EA",replace:!0,templateUrl:"template/datepicker/datepicker.html",scope:{dateDisabled:"&"},require:["datepicker","?^ngModel"],controller:"DatepickerController",link:function(a,e,f,g){function h(){a.showWeekNumbers=0===o&&q}function i(a,b){for(var c=[];a.length>0;)c.push(a.splice(0,b));return c}function j(b){var c=null,e=!0;n.$modelValue&&(c=new Date(n.$modelValue),isNaN(c)?(e=!1,d.error('Datepicker directive: "ng-model" value must be a Date object, a number of milliseconds since 01.01.1970 or a string representing an RFC2822 or ISO 8601 date.')):b&&(p=c)),n.$setValidity("date",e);var f=m.modes[o],g=f.getVisibleDates(p,c);angular.forEach(g.objects,function(a){a.disabled=m.isDisabled(a.date,o)}),n.$setValidity("date-disabled",!c||!m.isDisabled(c)),a.rows=i(g.objects,f.split),a.labels=g.labels||[],a.title=g.title}function k(a){o=a,h(),j()}function l(a){var b=new Date(a);b.setDate(b.getDate()+4-(b.getDay()||7));var c=b.getTime();return b.setMonth(0),b.setDate(1),Math.floor(Math.round((c-b)/864e5)/7)+1}var m=g[0],n=g[1];if(n){var o=0,p=new Date,q=c.showWeeks;f.showWeeks?a.$parent.$watch(b(f.showWeeks),function(a){q=!!a,h()}):h(),f.min&&a.$parent.$watch(b(f.min),function(a){m.minDate=a?new Date(a):null,j()}),f.max&&a.$parent.$watch(b(f.max),function(a){m.maxDate=a?new Date(a):null,j()}),n.$render=function(){j(!0)},a.select=function(a){if(0===o){var b=n.$modelValue?new Date(n.$modelValue):new Date(0,0,0,0,0,0,0);b.setFullYear(a.getFullYear(),a.getMonth(),a.getDate()),n.$setViewValue(b),j(!0)}else p=a,k(o-1)},a.move=function(a){var b=m.modes[o].step;p.setMonth(p.getMonth()+a*(b.months||0)),p.setFullYear(p.getFullYear()+a*(b.years||0)),j()},a.toggleMode=function(){k((o+1)%m.modes.length)},a.getWeekNumber=function(b){return 0===o&&a.showWeekNumbers&&7===b.length?l(b[0].date):null}}}}}]).constant("datepickerPopupConfig",{dateFormat:"yyyy-MM-dd",currentText:"Today",toggleWeeksText:"Weeks",clearText:"Clear",closeText:"Done",closeOnDateSelection:!0,appendToBody:!1,showButtonBar:!0}).directive("datepickerPopup",["$compile","$parse","$document","$position","dateFilter","datepickerPopupConfig","datepickerConfig",function(a,b,c,d,e,f,g){return{restrict:"EA",require:"ngModel",link:function(h,i,j,k){function l(a){u?u(h,!!a):q.isOpen=!!a}function m(a){if(a){if(angular.isDate(a))return k.$setValidity("date",!0),a;if(angular.isString(a)){var b=new Date(a);return isNaN(b)?(k.$setValidity("date",!1),void 0):(k.$setValidity("date",!0),b)}return k.$setValidity("date",!1),void 0}return k.$setValidity("date",!0),null}function n(a,c,d){a&&(h.$watch(b(a),function(a){q[c]=a}),y.attr(d||c,c))}function o(){q.position=s?d.offset(i):d.position(i),q.position.top=q.position.top+i.prop("offsetHeight")}var p,q=h.$new(),r=angular.isDefined(j.closeOnDateSelection)?h.$eval(j.closeOnDateSelection):f.closeOnDateSelection,s=angular.isDefined(j.datepickerAppendToBody)?h.$eval(j.datepickerAppendToBody):f.appendToBody;j.$observe("datepickerPopup",function(a){p=a||f.dateFormat,k.$render()}),q.showButtonBar=angular.isDefined(j.showButtonBar)?h.$eval(j.showButtonBar):f.showButtonBar,h.$on("$destroy",function(){C.remove(),q.$destroy()}),j.$observe("currentText",function(a){q.currentText=angular.isDefined(a)?a:f.currentText}),j.$observe("toggleWeeksText",function(a){q.toggleWeeksText=angular.isDefined(a)?a:f.toggleWeeksText}),j.$observe("clearText",function(a){q.clearText=angular.isDefined(a)?a:f.clearText}),j.$observe("closeText",function(a){q.closeText=angular.isDefined(a)?a:f.closeText});var t,u;j.isOpen&&(t=b(j.isOpen),u=t.assign,h.$watch(t,function(a){q.isOpen=!!a})),q.isOpen=t?t(h):!1;var v=function(a){q.isOpen&&a.target!==i[0]&&q.$apply(function(){l(!1)})},w=function(){q.$apply(function(){l(!0)})},x=angular.element("
");x.attr({"ng-model":"date","ng-change":"dateSelection()"});var y=angular.element(x.children()[0]),z={};j.datepickerOptions&&(z=h.$eval(j.datepickerOptions),y.attr(angular.extend({},z))),k.$parsers.unshift(m),q.dateSelection=function(a){angular.isDefined(a)&&(q.date=a),k.$setViewValue(q.date),k.$render(),r&&l(!1)},i.bind("input change keyup",function(){q.$apply(function(){q.date=k.$modelValue})}),k.$render=function(){var a=k.$viewValue?e(k.$viewValue,p):"";i.val(a),q.date=k.$modelValue},n(j.min,"min"),n(j.max,"max"),j.showWeeks?n(j.showWeeks,"showWeeks","show-weeks"):(q.showWeeks="show-weeks"in z?z["show-weeks"]:g.showWeeks,y.attr("show-weeks","showWeeks")),j.dateDisabled&&y.attr("date-disabled",j.dateDisabled);var A=!1,B=!1;q.$watch("isOpen",function(a){a?(o(),c.bind("click",v),B&&i.unbind("focus",w),i[0].focus(),A=!0):(A&&c.unbind("click",v),i.bind("focus",w),B=!0),u&&u(h,a)}),q.today=function(){q.dateSelection(new Date)},q.clear=function(){q.dateSelection(null)};var C=a(x)(q);s?c.find("body").append(C):i.after(C)}}}]).directive("datepickerPopupWrap",function(){return{restrict:"EA",replace:!0,transclude:!0,templateUrl:"template/datepicker/popup.html",link:function(a,b){b.bind("click",function(a){a.preventDefault(),a.stopPropagation()})}}}),angular.module("ui.bootstrap.dropdownToggle",[]).directive("dropdownToggle",["$document","$location",function(a){var b=null,c=angular.noop;return{restrict:"CA",link:function(d,e){d.$watch("$location.path",function(){c()}),e.parent().bind("click",function(){c()}),e.bind("click",function(d){var f=e===b;d.preventDefault(),d.stopPropagation(),b&&c(),f||e.hasClass("disabled")||e.prop("disabled")||(e.parent().addClass("open"),b=e,c=function(d){d&&(d.preventDefault(),d.stopPropagation()),a.unbind("click",c),e.parent().removeClass("open"),c=angular.noop,b=null},a.bind("click",c))})}}}]),angular.module("ui.bootstrap.modal",["ui.bootstrap.transition"]).factory("$$stackedMap",function(){return{createNew:function(){var a=[];return{add:function(b,c){a.push({key:b,value:c})},get:function(b){for(var c=0;c0)}function i(){if(k&&-1==g()){var a=l;j(k,l,150,function(){a.$destroy(),a=null}),k=void 0,l=void 0}}function j(c,d,e,f){function g(){g.done||(g.done=!0,c.remove(),f&&f())}d.animate=!1;var h=a.transitionEndEventName;if(h){var i=b(g,e);c.bind(h,function(){b.cancel(i),g(),d.$apply()})}else b(g,0)}var k,l,m="modal-open",n=f.createNew(),o={};return e.$watch(g,function(a){l&&(l.index=a)}),c.bind("keydown",function(a){var b;27===a.which&&(b=n.top(),b&&b.value.keyboard&&e.$apply(function(){o.dismiss(b.key)}))}),o.open=function(a,b){n.add(a,{deferred:b.deferred,modalScope:b.scope,backdrop:b.backdrop,keyboard:b.keyboard});var f=c.find("body").eq(0),h=g();h>=0&&!k&&(l=e.$new(!0),l.index=h,k=d("
")(l),f.append(k));var i=angular.element("
");i.attr("window-class",b.windowClass),i.attr("index",n.length()-1),i.attr("animate","animate"),i.html(b.content);var j=d(i)(b.scope);n.top().value.modalDomEl=j,f.append(j),f.addClass(m)},o.close=function(a,b){var c=n.get(a).value;c&&(c.deferred.resolve(b),h(a))},o.dismiss=function(a,b){var c=n.get(a).value;c&&(c.deferred.reject(b),h(a))},o.dismissAll=function(a){for(var b=this.getTop();b;)this.dismiss(b.key,a),b=this.getTop()},o.getTop=function(){return n.top()},o}]).provider("$modal",function(){var a={options:{backdrop:!0,keyboard:!0},$get:["$injector","$rootScope","$q","$http","$templateCache","$controller","$modalStack",function(b,c,d,e,f,g,h){function i(a){return a.template?d.when(a.template):e.get(a.templateUrl,{cache:f}).then(function(a){return a.data})}function j(a){var c=[];return angular.forEach(a,function(a){(angular.isFunction(a)||angular.isArray(a))&&c.push(d.when(b.invoke(a)))}),c}var k={};return k.open=function(b){var e=d.defer(),f=d.defer(),k={result:e.promise,opened:f.promise,close:function(a){h.close(k,a)},dismiss:function(a){h.dismiss(k,a)}};if(b=angular.extend({},a.options,b),b.resolve=b.resolve||{},!b.template&&!b.templateUrl)throw new Error("One of template or templateUrl options is required.");var l=d.all([i(b)].concat(j(b.resolve)));return l.then(function(a){var d=(b.scope||c).$new();d.$close=k.close,d.$dismiss=k.dismiss;var f,i={},j=1;b.controller&&(i.$scope=d,i.$modalInstance=k,angular.forEach(b.resolve,function(b,c){i[c]=a[j++]}),f=g(b.controller,i)),h.open(k,{scope:d,deferred:e,content:a[0],backdrop:b.backdrop,keyboard:b.keyboard,windowClass:b.windowClass})},function(a){e.reject(a)}),l.then(function(){f.resolve(!0)},function(){f.reject(!1)}),k},k}]};return a}),angular.module("ui.bootstrap.pagination",[]).controller("PaginationController",["$scope","$attrs","$parse","$interpolate",function(a,b,c,d){var e=this,f=b.numPages?c(b.numPages).assign:angular.noop;this.init=function(d){b.itemsPerPage?a.$parent.$watch(c(b.itemsPerPage),function(b){e.itemsPerPage=parseInt(b,10),a.totalPages=e.calculateTotalPages()}):this.itemsPerPage=d},this.noPrevious=function(){return 1===this.page},this.noNext=function(){return this.page===a.totalPages},this.isActive=function(a){return this.page===a},this.calculateTotalPages=function(){var b=this.itemsPerPage<1?1:Math.ceil(a.totalItems/this.itemsPerPage);return Math.max(b||0,1)},this.getAttributeValue=function(b,c,e){return angular.isDefined(b)?e?d(b)(a.$parent):a.$parent.$eval(b):c},this.render=function(){this.page=parseInt(a.page,10)||1,this.page>0&&this.page<=a.totalPages&&(a.pages=this.getPages(this.page,a.totalPages))},a.selectPage=function(b){!e.isActive(b)&&b>0&&b<=a.totalPages&&(a.page=b,a.onSelectPage({page:b}))},a.$watch("page",function(){e.render()}),a.$watch("totalItems",function(){a.totalPages=e.calculateTotalPages()}),a.$watch("totalPages",function(b){f(a.$parent,b),e.page>b?a.selectPage(b):e.render()})}]).constant("paginationConfig",{itemsPerPage:10,boundaryLinks:!1,directionLinks:!0,firstText:"First",previousText:"Previous",nextText:"Next",lastText:"Last",rotate:!0}).directive("pagination",["$parse","paginationConfig",function(a,b){return{restrict:"EA",scope:{page:"=",totalItems:"=",onSelectPage:" &"},controller:"PaginationController",templateUrl:"template/pagination/pagination.html",replace:!0,link:function(c,d,e,f){function g(a,b,c,d){return{number:a,text:b,active:c,disabled:d}}var h,i=f.getAttributeValue(e.boundaryLinks,b.boundaryLinks),j=f.getAttributeValue(e.directionLinks,b.directionLinks),k=f.getAttributeValue(e.firstText,b.firstText,!0),l=f.getAttributeValue(e.previousText,b.previousText,!0),m=f.getAttributeValue(e.nextText,b.nextText,!0),n=f.getAttributeValue(e.lastText,b.lastText,!0),o=f.getAttributeValue(e.rotate,b.rotate);f.init(b.itemsPerPage),e.maxSize&&c.$parent.$watch(a(e.maxSize),function(a){h=parseInt(a,10),f.render()}),f.getPages=function(a,b){var c=[],d=1,e=b,p=angular.isDefined(h)&&b>h;p&&(o?(d=Math.max(a-Math.floor(h/2),1),e=d+h-1,e>b&&(e=b,d=e-h+1)):(d=(Math.ceil(a/h)-1)*h+1,e=Math.min(d+h-1,b)));for(var q=d;e>=q;q++){var r=g(q,q,f.isActive(q),!1);c.push(r)}if(p&&!o){if(d>1){var s=g(d-1,"...",!1,!1);c.unshift(s)}if(b>e){var t=g(e+1,"...",!1,!1);c.push(t)}}if(j){var u=g(a-1,l,!1,f.noPrevious());c.unshift(u);var v=g(a+1,m,!1,f.noNext());c.push(v)}if(i){var w=g(1,k,!1,f.noPrevious());c.unshift(w);var x=g(b,n,!1,f.noNext());c.push(x)}return c}}}}]).constant("pagerConfig",{itemsPerPage:10,previousText:"« Previous",nextText:"Next »",align:!0}).directive("pager",["pagerConfig",function(a){return{restrict:"EA",scope:{page:"=",totalItems:"=",onSelectPage:" &"},controller:"PaginationController",templateUrl:"template/pagination/pager.html",replace:!0,link:function(b,c,d,e){function f(a,b,c,d,e){return{number:a,text:b,disabled:c,previous:i&&d,next:i&&e}}var g=e.getAttributeValue(d.previousText,a.previousText,!0),h=e.getAttributeValue(d.nextText,a.nextText,!0),i=e.getAttributeValue(d.align,a.align);e.init(a.itemsPerPage),e.getPages=function(a){return[f(a-1,g,e.noPrevious(),!0,!1),f(a+1,h,e.noNext(),!1,!0)]}}}}]),angular.module("ui.bootstrap.tooltip",["ui.bootstrap.position","ui.bootstrap.bindHtml"]).provider("$tooltip",function(){function a(a){var b=/[A-Z]/g,c="-";return a.replace(b,function(a,b){return(b?c:"")+a.toLowerCase()})}var b={placement:"top",animation:!0,popupDelay:0},c={mouseenter:"mouseleave",click:"click",focus:"blur"},d={};this.options=function(a){angular.extend(d,a)},this.setTriggers=function(a){angular.extend(c,a)},this.$get=["$window","$compile","$timeout","$parse","$document","$position","$interpolate",function(e,f,g,h,i,j,k){return function(e,l,m){function n(a){var b=a||o.trigger||m,d=c[b]||b;return{show:b,hide:d}}var o=angular.extend({},b,d),p=a(e),q=k.startSymbol(),r=k.endSymbol(),s="
';return{restrict:"EA",scope:!0,compile:function(){var a=f(s);return function(b,c,d){function f(){b.tt_isOpen?m():k()}function k(){(!z||b.$eval(d[l+"Enable"]))&&(b.tt_popupDelay?(v=g(p,b.tt_popupDelay,!1),v.then(function(a){a()})):p()())}function m(){b.$apply(function(){q()})}function p(){return b.tt_content?(r(),u&&g.cancel(u),t.css({top:0,left:0,display:"block"}),w?i.find("body").append(t):c.after(t),A(),b.tt_isOpen=!0,b.$digest(),A):angular.noop}function q(){b.tt_isOpen=!1,g.cancel(v),b.tt_animation?u=g(s,500):s()}function r(){t&&s(),t=a(b,function(){}),b.$digest()}function s(){t&&(t.remove(),t=null)}var t,u,v,w=angular.isDefined(o.appendToBody)?o.appendToBody:!1,x=n(void 0),y=!1,z=angular.isDefined(d[l+"Enable"]),A=function(){var a,d,e,f;switch(a=w?j.offset(c):j.position(c),d=t.prop("offsetWidth"),e=t.prop("offsetHeight"),b.tt_placement){case"right":f={top:a.top+a.height/2-e/2,left:a.left+a.width};break;case"bottom":f={top:a.top+a.height,left:a.left+a.width/2-d/2};break;case"left":f={top:a.top+a.height/2-e/2,left:a.left-d};break;default:f={top:a.top-e,left:a.left+a.width/2-d/2}}f.top+="px",f.left+="px",t.css(f)};b.tt_isOpen=!1,d.$observe(e,function(a){b.tt_content=a,!a&&b.tt_isOpen&&q()}),d.$observe(l+"Title",function(a){b.tt_title=a}),d.$observe(l+"Placement",function(a){b.tt_placement=angular.isDefined(a)?a:o.placement}),d.$observe(l+"PopupDelay",function(a){var c=parseInt(a,10);b.tt_popupDelay=isNaN(c)?o.popupDelay:c});var B=function(){y&&(c.unbind(x.show,k),c.unbind(x.hide,m))};d.$observe(l+"Trigger",function(a){B(),x=n(a),x.show===x.hide?c.bind(x.show,f):(c.bind(x.show,k),c.bind(x.hide,m)),y=!0});var C=b.$eval(d[l+"Animation"]);b.tt_animation=angular.isDefined(C)?!!C:o.animation,d.$observe(l+"AppendToBody",function(a){w=angular.isDefined(a)?h(a)(b):w}),w&&b.$on("$locationChangeSuccess",function(){b.tt_isOpen&&q()}),b.$on("$destroy",function(){g.cancel(u),g.cancel(v),B(),s()})}}}}}]}).directive("tooltipPopup",function(){return{restrict:"EA",replace:!0,scope:{content:"@",placement:"@",animation:"&",isOpen:"&"},templateUrl:"template/tooltip/tooltip-popup.html"}}).directive("tooltip",["$tooltip",function(a){return a("tooltip","tooltip","mouseenter")}]).directive("tooltipHtmlUnsafePopup",function(){return{restrict:"EA",replace:!0,scope:{content:"@",placement:"@",animation:"&",isOpen:"&"},templateUrl:"template/tooltip/tooltip-html-unsafe-popup.html"}}).directive("tooltipHtmlUnsafe",["$tooltip",function(a){return a("tooltipHtmlUnsafe","tooltip","mouseenter")}]),angular.module("ui.bootstrap.popover",["ui.bootstrap.tooltip"]).directive("popoverPopup",function(){return{restrict:"EA",replace:!0,scope:{title:"@",content:"@",placement:"@",animation:"&",isOpen:"&"},templateUrl:"template/popover/popover.html"}}).directive("popover",["$tooltip",function(a){return a("popover","popover","click")}]),angular.module("ui.bootstrap.progressbar",["ui.bootstrap.transition"]).constant("progressConfig",{animate:!0,max:100}).controller("ProgressController",["$scope","$attrs","progressConfig","$transition",function(a,b,c,d){var e=this,f=[],g=angular.isDefined(b.max)?a.$parent.$eval(b.max):c.max,h=angular.isDefined(b.animate)?a.$parent.$eval(b.animate):c.animate;this.addBar=function(a,b){var c=0,d=a.$parent.$index;angular.isDefined(d)&&f[d]&&(c=f[d].value),f.push(a),this.update(b,a.value,c),a.$watch("value",function(a,c){a!==c&&e.update(b,a,c)}),a.$on("$destroy",function(){e.removeBar(a)})},this.update=function(a,b,c){var e=this.getPercentage(b);h?(a.css("width",this.getPercentage(c)+"%"),d(a,{width:e+"%"})):a.css({transition:"none",width:e+"%"})},this.removeBar=function(a){f.splice(f.indexOf(a),1)},this.getPercentage=function(a){return Math.round(100*a/g)}}]).directive("progress",function(){return{restrict:"EA",replace:!0,transclude:!0,controller:"ProgressController",require:"progress",scope:{},template:'
'}}).directive("bar",function(){return{restrict:"EA",replace:!0,transclude:!0,require:"^progress",scope:{value:"=",type:"@"},templateUrl:"template/progressbar/bar.html",link:function(a,b,c,d){d.addBar(a,b)}}}).directive("progressbar",function(){return{restrict:"EA",replace:!0,transclude:!0,controller:"ProgressController",scope:{value:"=",type:"@"},templateUrl:"template/progressbar/progressbar.html",link:function(a,b,c,d){d.addBar(a,angular.element(b.children()[0]))}}}),angular.module("ui.bootstrap.rating",[]).constant("ratingConfig",{max:5,stateOn:null,stateOff:null}).controller("RatingController",["$scope","$attrs","$parse","ratingConfig",function(a,b,c,d){this.maxRange=angular.isDefined(b.max)?a.$parent.$eval(b.max):d.max,this.stateOn=angular.isDefined(b.stateOn)?a.$parent.$eval(b.stateOn):d.stateOn,this.stateOff=angular.isDefined(b.stateOff)?a.$parent.$eval(b.stateOff):d.stateOff,this.createRateObjects=function(a){for(var b={stateOn:this.stateOn,stateOff:this.stateOff},c=0,d=a.length;d>c;c++)a[c]=angular.extend({index:c},b,a[c]);return a},a.range=angular.isDefined(b.ratingStates)?this.createRateObjects(angular.copy(a.$parent.$eval(b.ratingStates))):this.createRateObjects(new Array(this.maxRange)),a.rate=function(b){a.value===b||a.readonly||(a.value=b) -},a.enter=function(b){a.readonly||(a.val=b),a.onHover({value:b})},a.reset=function(){a.val=angular.copy(a.value),a.onLeave()},a.$watch("value",function(b){a.val=b}),a.readonly=!1,b.readonly&&a.$parent.$watch(c(b.readonly),function(b){a.readonly=!!b})}]).directive("rating",function(){return{restrict:"EA",scope:{value:"=",onHover:"&",onLeave:"&"},controller:"RatingController",templateUrl:"template/rating/rating.html",replace:!0}}),angular.module("ui.bootstrap.tabs",[]).controller("TabsetController",["$scope",function(a){var b=this,c=b.tabs=a.tabs=[];b.select=function(a){angular.forEach(c,function(a){a.active=!1}),a.active=!0},b.addTab=function(a){c.push(a),(1===c.length||a.active)&&b.select(a)},b.removeTab=function(a){var d=c.indexOf(a);if(a.active&&c.length>1){var e=d==c.length-1?d-1:d+1;b.select(c[e])}c.splice(d,1)}}]).directive("tabset",function(){return{restrict:"EA",transclude:!0,replace:!0,scope:{},controller:"TabsetController",templateUrl:"template/tabs/tabset.html",link:function(a,b,c){a.vertical=angular.isDefined(c.vertical)?a.$parent.$eval(c.vertical):!1,a.justified=angular.isDefined(c.justified)?a.$parent.$eval(c.justified):!1,a.type=angular.isDefined(c.type)?a.$parent.$eval(c.type):"tabs"}}}).directive("tab",["$parse",function(a){return{require:"^tabset",restrict:"EA",replace:!0,templateUrl:"template/tabs/tab.html",transclude:!0,scope:{heading:"@",onSelect:"&select",onDeselect:"&deselect"},controller:function(){},compile:function(b,c,d){return function(b,c,e,f){var g,h;e.active?(g=a(e.active),h=g.assign,b.$parent.$watch(g,function(a,c){a!==c&&(b.active=!!a)}),b.active=g(b.$parent)):h=g=angular.noop,b.$watch("active",function(a){h(b.$parent,a),a?(f.select(b),b.onSelect()):b.onDeselect()}),b.disabled=!1,e.disabled&&b.$parent.$watch(a(e.disabled),function(a){b.disabled=!!a}),b.select=function(){b.disabled||(b.active=!0)},f.addTab(b),b.$on("$destroy",function(){f.removeTab(b)}),b.$transcludeFn=d}}}}]).directive("tabHeadingTransclude",[function(){return{restrict:"A",require:"^tab",link:function(a,b){a.$watch("headingElement",function(a){a&&(b.html(""),b.append(a))})}}}]).directive("tabContentTransclude",function(){function a(a){return a.tagName&&(a.hasAttribute("tab-heading")||a.hasAttribute("data-tab-heading")||"tab-heading"===a.tagName.toLowerCase()||"data-tab-heading"===a.tagName.toLowerCase())}return{restrict:"A",require:"^tabset",link:function(b,c,d){var e=b.$eval(d.tabContentTransclude);e.$transcludeFn(e.$parent,function(b){angular.forEach(b,function(b){a(b)?e.headingElement=b:c.append(b)})})}}}),angular.module("ui.bootstrap.timepicker",[]).constant("timepickerConfig",{hourStep:1,minuteStep:1,showMeridian:!0,meridians:null,readonlyInput:!1,mousewheel:!0}).directive("timepicker",["$parse","$log","timepickerConfig","$locale",function(a,b,c,d){return{restrict:"EA",require:"?^ngModel",replace:!0,scope:{},templateUrl:"template/timepicker/timepicker.html",link:function(e,f,g,h){function i(){var a=parseInt(e.hours,10),b=e.showMeridian?a>0&&13>a:a>=0&&24>a;return b?(e.showMeridian&&(12===a&&(a=0),e.meridian===q[1]&&(a+=12)),a):void 0}function j(){var a=parseInt(e.minutes,10);return a>=0&&60>a?a:void 0}function k(a){return angular.isDefined(a)&&a.toString().length<2?"0"+a:a}function l(a){m(),h.$setViewValue(new Date(p)),n(a)}function m(){h.$setValidity("time",!0),e.invalidHours=!1,e.invalidMinutes=!1}function n(a){var b=p.getHours(),c=p.getMinutes();e.showMeridian&&(b=0===b||12===b?12:b%12),e.hours="h"===a?b:k(b),e.minutes="m"===a?c:k(c),e.meridian=p.getHours()<12?q[0]:q[1]}function o(a){var b=new Date(p.getTime()+6e4*a);p.setHours(b.getHours(),b.getMinutes()),l()}if(h){var p=new Date,q=angular.isDefined(g.meridians)?e.$parent.$eval(g.meridians):c.meridians||d.DATETIME_FORMATS.AMPMS,r=c.hourStep;g.hourStep&&e.$parent.$watch(a(g.hourStep),function(a){r=parseInt(a,10)});var s=c.minuteStep;g.minuteStep&&e.$parent.$watch(a(g.minuteStep),function(a){s=parseInt(a,10)}),e.showMeridian=c.showMeridian,g.showMeridian&&e.$parent.$watch(a(g.showMeridian),function(a){if(e.showMeridian=!!a,h.$error.time){var b=i(),c=j();angular.isDefined(b)&&angular.isDefined(c)&&(p.setHours(b),l())}else n()});var t=f.find("input"),u=t.eq(0),v=t.eq(1),w=angular.isDefined(g.mousewheel)?e.$eval(g.mousewheel):c.mousewheel;if(w){var x=function(a){a.originalEvent&&(a=a.originalEvent);var b=a.wheelDelta?a.wheelDelta:-a.deltaY;return a.detail||b>0};u.bind("mousewheel wheel",function(a){e.$apply(x(a)?e.incrementHours():e.decrementHours()),a.preventDefault()}),v.bind("mousewheel wheel",function(a){e.$apply(x(a)?e.incrementMinutes():e.decrementMinutes()),a.preventDefault()})}if(e.readonlyInput=angular.isDefined(g.readonlyInput)?e.$eval(g.readonlyInput):c.readonlyInput,e.readonlyInput)e.updateHours=angular.noop,e.updateMinutes=angular.noop;else{var y=function(a,b){h.$setViewValue(null),h.$setValidity("time",!1),angular.isDefined(a)&&(e.invalidHours=a),angular.isDefined(b)&&(e.invalidMinutes=b)};e.updateHours=function(){var a=i();angular.isDefined(a)?(p.setHours(a),l("h")):y(!0)},u.bind("blur",function(){!e.validHours&&e.hours<10&&e.$apply(function(){e.hours=k(e.hours)})}),e.updateMinutes=function(){var a=j();angular.isDefined(a)?(p.setMinutes(a),l("m")):y(void 0,!0)},v.bind("blur",function(){!e.invalidMinutes&&e.minutes<10&&e.$apply(function(){e.minutes=k(e.minutes)})})}h.$render=function(){var a=h.$modelValue?new Date(h.$modelValue):null;isNaN(a)?(h.$setValidity("time",!1),b.error('Timepicker directive: "ng-model" value must be a Date object, a number of milliseconds since 01.01.1970 or a string representing an RFC2822 or ISO 8601 date.')):(a&&(p=a),m(),n())},e.incrementHours=function(){o(60*r)},e.decrementHours=function(){o(60*-r)},e.incrementMinutes=function(){o(s)},e.decrementMinutes=function(){o(-s)},e.toggleMeridian=function(){o(720*(p.getHours()<12?1:-1))}}}}}]),angular.module("ui.bootstrap.typeahead",["ui.bootstrap.position","ui.bootstrap.bindHtml"]).factory("typeaheadParser",["$parse",function(a){var b=/^\s*(.*?)(?:\s+as\s+(.*?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+(.*)$/;return{parse:function(c){var d=c.match(b);if(!d)throw new Error("Expected typeahead specification in form of '_modelValue_ (as _label_)? for _item_ in _collection_' but got '"+c+"'.");return{itemName:d[3],source:a(d[4]),viewMapper:a(d[2]||d[1]),modelMapper:a(d[1])}}}}]).directive("typeahead",["$compile","$parse","$q","$timeout","$document","$position","typeaheadParser",function(a,b,c,d,e,f,g){var h=[9,13,27,38,40];return{require:"ngModel",link:function(i,j,k,l){var m,n=i.$eval(k.typeaheadMinLength)||1,o=i.$eval(k.typeaheadWaitMs)||0,p=i.$eval(k.typeaheadEditable)!==!1,q=b(k.typeaheadLoading).assign||angular.noop,r=b(k.typeaheadOnSelect),s=k.typeaheadInputFormatter?b(k.typeaheadInputFormatter):void 0,t=k.typeaheadAppendToBody?b(k.typeaheadAppendToBody):!1,u=b(k.ngModel).assign,v=g.parse(k.typeahead),w=angular.element("
");w.attr({matches:"matches",active:"activeIdx",select:"select(activeIdx)",query:"query",position:"position"}),angular.isDefined(k.typeaheadTemplateUrl)&&w.attr("template-url",k.typeaheadTemplateUrl);var x=i.$new();i.$on("$destroy",function(){x.$destroy()});var y=function(){x.matches=[],x.activeIdx=-1},z=function(a){var b={$viewValue:a};q(i,!0),c.when(v.source(i,b)).then(function(c){if(a===l.$viewValue&&m){if(c.length>0){x.activeIdx=0,x.matches.length=0;for(var d=0;d=n?o>0?(A&&d.cancel(A),A=d(function(){z(a)},o)):z(a):(q(i,!1),y()),p?a:a?(l.$setValidity("editable",!1),void 0):(l.$setValidity("editable",!0),a)}),l.$formatters.push(function(a){var b,c,d={};return s?(d.$model=a,s(i,d)):(d[v.itemName]=a,b=v.viewMapper(i,d),d[v.itemName]=void 0,c=v.viewMapper(i,d),b!==c?b:a)}),x.select=function(a){var b,c,d={};d[v.itemName]=c=x.matches[a].model,b=v.modelMapper(i,d),u(i,b),l.$setValidity("editable",!0),r(i,{$item:c,$model:b,$label:v.viewMapper(i,d)}),y(),j[0].focus()},j.bind("keydown",function(a){0!==x.matches.length&&-1!==h.indexOf(a.which)&&(a.preventDefault(),40===a.which?(x.activeIdx=(x.activeIdx+1)%x.matches.length,x.$digest()):38===a.which?(x.activeIdx=(x.activeIdx?x.activeIdx:x.matches.length)-1,x.$digest()):13===a.which||9===a.which?x.$apply(function(){x.select(x.activeIdx)}):27===a.which&&(a.stopPropagation(),y(),x.$digest()))}),j.bind("blur",function(){m=!1});var B=function(a){j[0]!==a.target&&(y(),x.$digest())};e.bind("click",B),i.$on("$destroy",function(){e.unbind("click",B)});var C=a(w)(x);t?e.find("body").append(C):j.after(C)}}}]).directive("typeaheadPopup",function(){return{restrict:"EA",scope:{matches:"=",query:"=",active:"=",position:"=",select:"&"},replace:!0,templateUrl:"template/typeahead/typeahead-popup.html",link:function(a,b,c){a.templateUrl=c.templateUrl,a.isOpen=function(){return a.matches.length>0},a.isActive=function(b){return a.active==b},a.selectActive=function(b){a.active=b},a.selectMatch=function(b){a.select({activeIdx:b})}}}}).directive("typeaheadMatch",["$http","$templateCache","$compile","$parse",function(a,b,c,d){return{restrict:"EA",scope:{index:"=",match:"=",query:"="},link:function(e,f,g){var h=d(g.templateUrl)(e.$parent)||"template/typeahead/typeahead-match.html";a.get(h,{cache:b}).success(function(a){f.replaceWith(c(a.trim())(e))})}}}]).filter("typeaheadHighlight",function(){function a(a){return a.replace(/([.?*+^$[\]\\(){}|-])/g,"\\$1")}return function(b,c){return c?b.replace(new RegExp(a(c),"gi"),"$&"):b}}),angular.module("template/accordion/accordion-group.html",[]).run(["$templateCache",function(a){a.put("template/accordion/accordion-group.html",'
\n
\n

\n {{heading}}\n

\n
\n
\n
\n
\n
')}]),angular.module("template/accordion/accordion.html",[]).run(["$templateCache",function(a){a.put("template/accordion/accordion.html",'
')}]),angular.module("template/alert/alert.html",[]).run(["$templateCache",function(a){a.put("template/alert/alert.html","
\n \n
\n
\n")}]),angular.module("template/carousel/carousel.html",[]).run(["$templateCache",function(a){a.put("template/carousel/carousel.html",'\n')}]),angular.module("template/carousel/slide.html",[]).run(["$templateCache",function(a){a.put("template/carousel/slide.html","
\n")}]),angular.module("template/datepicker/datepicker.html",[]).run(["$templateCache",function(a){a.put("template/datepicker/datepicker.html",'\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
#{{label}}
{{ getWeekNumber(row) }}\n \n
\n')}]),angular.module("template/datepicker/popup.html",[]).run(["$templateCache",function(a){a.put("template/datepicker/popup.html","
    \n
  • \n"+'
  • \n \n \n \n \n \n \n
  • \n
\n')}]),angular.module("template/modal/backdrop.html",[]).run(["$templateCache",function(a){a.put("template/modal/backdrop.html",'')}]),angular.module("template/modal/window.html",[]).run(["$templateCache",function(a){a.put("template/modal/window.html",'')}]),angular.module("template/pagination/pager.html",[]).run(["$templateCache",function(a){a.put("template/pagination/pager.html",'')}]),angular.module("template/pagination/pagination.html",[]).run(["$templateCache",function(a){a.put("template/pagination/pagination.html",'')}]),angular.module("template/tooltip/tooltip-html-unsafe-popup.html",[]).run(["$templateCache",function(a){a.put("template/tooltip/tooltip-html-unsafe-popup.html",'
\n
\n
\n
\n')}]),angular.module("template/tooltip/tooltip-popup.html",[]).run(["$templateCache",function(a){a.put("template/tooltip/tooltip-popup.html",'
\n
\n
\n
\n')}]),angular.module("template/popover/popover.html",[]).run(["$templateCache",function(a){a.put("template/popover/popover.html",'
\n
\n\n
\n

\n
\n
\n
\n')}]),angular.module("template/progressbar/bar.html",[]).run(["$templateCache",function(a){a.put("template/progressbar/bar.html",'
')}]),angular.module("template/progressbar/progress.html",[]).run(["$templateCache",function(a){a.put("template/progressbar/progress.html",'
')}]),angular.module("template/progressbar/progressbar.html",[]).run(["$templateCache",function(a){a.put("template/progressbar/progressbar.html",'
')}]),angular.module("template/rating/rating.html",[]).run(["$templateCache",function(a){a.put("template/rating/rating.html",'\n \n')}]),angular.module("template/tabs/tab.html",[]).run(["$templateCache",function(a){a.put("template/tabs/tab.html",'
  • \n {{heading}}\n
  • \n')}]),angular.module("template/tabs/tabset-titles.html",[]).run(["$templateCache",function(a){a.put("template/tabs/tabset-titles.html","
      \n
    \n")}]),angular.module("template/tabs/tabset.html",[]).run(["$templateCache",function(a){a.put("template/tabs/tabset.html",'\n
    \n \n
    \n
    \n
    \n
    \n
    \n')}]),angular.module("template/timepicker/timepicker.html",[]).run(["$templateCache",function(a){a.put("template/timepicker/timepicker.html",'\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
     
    \n \n :\n \n
     
    \n')}]),angular.module("template/typeahead/typeahead-match.html",[]).run(["$templateCache",function(a){a.put("template/typeahead/typeahead-match.html",'')}]),angular.module("template/typeahead/typeahead-popup.html",[]).run(["$templateCache",function(a){a.put("template/typeahead/typeahead-popup.html","
      \n"+'
    • \n
      \n
    • \n
    ')}]); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/lib/bootstrap/less/dropdowns.less b/src/Umbraco.Web.UI.Client/lib/bootstrap/less/dropdowns.less index bbfe3fd3e3..35730a6bba 100644 --- a/src/Umbraco.Web.UI.Client/lib/bootstrap/less/dropdowns.less +++ b/src/Umbraco.Web.UI.Client/lib/bootstrap/less/dropdowns.less @@ -80,6 +80,7 @@ line-height: @baseLineHeight; color: @dropdownLinkColor; white-space: nowrap; + cursor:pointer; } } diff --git a/src/Umbraco.Web.UI.Client/lib/umbraco/LegacyUmbClientMgr.js b/src/Umbraco.Web.UI.Client/lib/umbraco/LegacyUmbClientMgr.js deleted file mode 100644 index 5fad4b944e..0000000000 --- a/src/Umbraco.Web.UI.Client/lib/umbraco/LegacyUmbClientMgr.js +++ /dev/null @@ -1,427 +0,0 @@ - -//TODO: WE NEED TO CONVERT ALL OF THESE METHODS TO PROXY TO OUR APPLICATION SINCE MANY CUSTOM APPS USE THIS! - -//TEST to mock iframe, this intercepts calls directly -//to the old iframe, and funnels requests to angular directly -//var right = {document: {location: {}}}; -/**/ - -Umbraco.Sys.registerNamespace("Umbraco.Application"); - -(function($) { - Umbraco.Application.ClientManager = function() { - - //to support those trying to call right.document.etc - var fakeFrame = {}; - Object.defineProperty(fakeFrame, "href", { - get: function() { - return this._href ? this._href : ""; - }, - set: function(value) { - this._href = value; - UmbClientMgr.contentFrame(value); - }, - }); - - /** - * @ngdoc function - * @name getRootScope - * @methodOf UmbClientMgr - * @function - * - * @description - * Returns the root angular scope - */ - function getRootScope() { - return angular.element(document.getElementById("umbracoMainPageBody")).scope(); - } - - /** - * @ngdoc function - * @name getRootInjector - * @methodOf UmbClientMgr - * @function - * - * @description - * Returns the root angular injector - */ - function getRootInjector() { - return angular.element(document.getElementById("umbracoMainPageBody")).injector(); - } - - - return { - _isDirty: false, - _isDebug: false, - _mainTree: null, - _appActions: null, - _historyMgr: null, - _rootPath: "/umbraco", //this is the default - _modal: new Array(), //track all modal window objects (they get stacked) - - historyManager: function () { - - throw "Not implemented!"; - - //if (!this._historyMgr) { - // this._historyMgr = new Umbraco.Controls.HistoryManager(); - //} - //return this._historyMgr; - }, - - setUmbracoPath: function(strPath) { - /// - /// sets the Umbraco root path folder - /// - this._debug("setUmbracoPath: " + strPath); - this._rootPath = strPath; - }, - - mainWindow: function() { - return top; - }, - mainTree: function () { - - var injector = getRootInjector(); - var navService = injector.get("navigationService"); - var appState = injector.get("appState"); - var angularHelper = injector.get("angularHelper"); - var $rootScope = injector.get("$rootScope"); - - //mimic the API of the legacy tree - var tree = { - syncTree: function (path, forceReload) { - angularHelper.safeApply($rootScope, function() { - navService._syncPath(path, forceReload); - }); - }, - clearTreeCache: function(){ - var treeService = injector.get("treeService"); - angularHelper.safeApply($rootScope, function() { - treeService.clearCache(); - }); - }, - childNodeCreated: function() { - //no-op, just needs to be here for legacy reasons - }, - reloadActionNode: function () { - angularHelper.safeApply($rootScope, function() { - var currentMenuNode = appState.getMenuState("currentNode"); - if (currentMenuNode) { - navService.reloadNode(currentMenuNode); - } - }); - }, - refreshTree: function (treeAlias) { - //no-op, just needs to be here for legacy reasons - }, - moveNode: function (id, path) { - angularHelper.safeApply($rootScope, function() { - var currentMenuNode = appState.getMenuState("currentNode"); - if (currentMenuNode) { - var treeService = injector.get("treeService"); - var treeRoot = treeService.getTreeRoot(currentMenuNode); - if (treeRoot) { - var found = treeService.getDescendantNode(treeRoot, id); - if (found) { - treeService.removeNode(found); - } - } - } - navService._syncPath(path, true); - }); - }, - getActionNode: function () { - //need to replicate the legacy tree node - var currentMenuNode = appState.getMenuState("currentNode"); - if (!currentMenuNode) { - return null; - } - - var legacyNode = { - nodeId: currentMenuNode.id, - nodeName: currentMenuNode.name, - nodeType: currentMenuNode.nodeType, - treeType: currentMenuNode.nodeType, - sourceUrl: currentMenuNode.childNodesUrl, - updateDefinition: function() { - throw "'updateDefinition' method is not supported in Umbraco 7, consider upgrading to the new v7 APIs"; - } - }; - //defined getters that will throw a not implemented/supported exception - Object.defineProperty(legacyNode, "menu", { - get: function () { - throw "'menu' property is not supported in Umbraco 7, consider upgrading to the new v7 APIs"; - } - }); - Object.defineProperty(legacyNode, "jsNode", { - get: function () { - throw "'jsNode' property is not supported in Umbraco 7, consider upgrading to the new v7 APIs"; - } - }); - Object.defineProperty(legacyNode, "jsTree", { - get: function () { - throw "'jsTree' property is not supported in Umbraco 7, consider upgrading to the new v7 APIs"; - } - }); - - return legacyNode; - } - }; - - return tree; - }, - appActions: function() { - var injector = getRootInjector(); - var navService = injector.get("navigationService"); - var localizationService = injector.get("localizationService"); - var usersResource = injector.get("usersResource"); - //var appState = injector.get("appState"); - var angularHelper = injector.get("angularHelper"); - var $rootScope = injector.get("$rootScope"); - - var actions = { - openDashboard : function(section){ - navService.changeSection(section); - } - }; - - return actions; - }, - uiKeys: function() { - - throw "Not implemented!"; - - ////TODO: If there is no main window, we need to go retrieve the appActions from the server! - //return this.mainWindow().uiKeys; - }, - contentFrameAndSection: function(app, rightFrameUrl) { - - throw "Not implemented!"; - - ////this.appActions().shiftApp(app, this.uiKeys()['sections_' + app]); - //var self = this; - //self.mainWindow().UmbClientMgr.historyManager().addHistory(app, true); - //window.setTimeout(function() { - // self.mainWindow().UmbClientMgr.contentFrame(rightFrameUrl); - //}, 200); - }, - - /** - * @ngdoc function - * @name contentFrame - * @methodOf UmbClientMgr - * @function - * - * @description - * This will tell our angular app to create and load in an iframe at the specified location - * @param strLocation {String} The URL to load the iframe in - */ - contentFrame: function (strLocation) { - - if (!strLocation || strLocation == "") { - //SD: NOTE: We used to return the content iframe object but now I'm not sure we should do that ?! - - if (typeof top.right != "undefined") { - return top.right; - } - else { - return top; //return the current window if the content frame doesn't exist in the current context - } - //return; - } - - this._debug("contentFrame: " + strLocation); - - //get our angular navigation service - var injector = getRootInjector(); - - var rootScope = injector.get("$rootScope"); - var angularHelper = injector.get("angularHelper"); - var navService = injector.get("navigationService"); - var locationService = injector.get("$location"); - - var self = this; - - angularHelper.safeApply(rootScope, function() { - if (strLocation.startsWith("#")) { - locationService.path(strLocation.trimStart("#")).search(""); - } - else { - //if the path doesn't start with "/" or with the root path then - //prepend the root path - if (!strLocation.startsWith("/")) { - strLocation = self._rootPath + "/" + strLocation; - } - else if (strLocation.length >= self._rootPath.length - && strLocation.substr(0, self._rootPath.length) != self._rootPath) { - strLocation = self._rootPath + "/" + strLocation; - } - - navService.loadLegacyIFrame(strLocation); - } - }); - }, - - getFakeFrame : function() { - return fakeFrame; - }, - - /** This is used to launch an angular based modal window instead of the legacy window */ - openAngularModalWindow: function (options) { - - //get our angular navigation service - var injector = getRootInjector(); - var dialogService = injector.get("dialogService"); - - var dialog = dialogService.open(options); - - ////add the callback to the jquery data for the modal so we can call it on close to support the legacy way dialogs worked. - //dialog.element.data("modalCb", onCloseCallback); - - //this._modal.push(dialog); - //return dialog; - }, - - openModalWindow: function(url, name, showHeader, width, height, top, leftOffset, closeTriggers, onCloseCallback) { - //need to create the modal on the top window if the top window has a client manager, if not, create it on the current window - - //get our angular navigation service - var injector = getRootInjector(); - var navService = injector.get("navigationService"); - var dialogService = injector.get("dialogService"); - - var self = this; - - //based on what state the nav ui is in, depends on how we are going to launch a model / dialog. A modal - // will show up on the right hand side and a dialog will show up as if it is in the menu. - // with the legacy API we cannot know what is expected so we can only check if the menu is active, if it is - // we'll launch a dialog, otherwise a modal. - var appState = injector.get("appState"); - var navMode = appState.getGlobalState("navMode"); - var dialog; - if (navMode === "menu") { - dialog = navService.showDialog({ - //create a 'fake' action to passin with the specified actionUrl since it needs to load into an iframe - action: { - name: name, - metaData: { - actionUrl: url - } - }, - node: {} - }); - } - else { - dialog = dialogService.open({ - template: url, - width: width, - height: height, - iframe: true, - show: true - }); - } - - - //add the callback to the jquery data for the modal so we can call it on close to support the legacy way dialogs worked. - dialog.element.data("modalCb", onCloseCallback); - //add the close triggers - if (angular.isArray(closeTriggers)) { - for (var i = 0; i < closeTriggers.length; i++) { - var e = dialog.find(closeTriggers[i]); - if (e.length > 0) { - e.click(function () { - self.closeModalWindow(); - }); - } - } - } - - this._modal.push(dialog); - return dialog; - }, - rootScope : function(){ - return getRootScope(); - }, - - /** - This will reload the content frame based on it's current route, if pathToMatch is specified it will only reload it if the current - location matches the path - */ - reloadLocation: function(pathToMatch) { - - var injector = getRootInjector(); - var doChange = true; - if (pathToMatch) { - var $location = injector.get("$location"); - var path = $location.path(); - if (path != pathToMatch) { - doChange = false; - } - } - - if (doChange) { - var $route = injector.get("$route"); - $route.reload(); - var $rootScope = injector.get("$rootScope"); - $rootScope.$apply(); - } - }, - - closeModalWindow: function(rVal) { - - //get our angular navigation service - var injector = getRootInjector(); - var dialogService = injector.get("dialogService"); - - // all legacy calls to closeModalWindow are expecting to just close the last opened one so we'll ensure - // that this is still the case. - if (this._modal != null && this._modal.length > 0) { - - var lastModal = this._modal.pop(); - - //if we've stored a callback on this modal call it before we close. - var self = this; - //get the compat callback from the modal element - var onCloseCallback = lastModal.element.data("modalCb"); - if (typeof onCloseCallback == "function") { - onCloseCallback.apply(self, [{ outVal: rVal }]); - } - - //just call the native dialog close() method to remove the dialog - lastModal.close(); - - //if it's the last one close them all - if (this._modal.length == 0) { - getRootScope().$emit("app.closeDialogs", undefined); - } - } - else { - //instead of calling just the dialog service we funnel it through the global - //event emitter - getRootScope().$emit("app.closeDialogs", undefined); - } - }, - /* This is used for the package installer to call in order to reload all app assets so we don't have to reload the window */ - _packageInstalled: function() { - var injector = getRootInjector(); - var packageHelper = injector.get("packageHelper"); - packageHelper.packageInstalled(); - }, - _debug: function(strMsg) { - if (this._isDebug) { - Sys.Debug.trace("UmbClientMgr: " + strMsg); - } - }, - get_isDirty: function() { - return this._isDirty; - }, - set_isDirty: function(value) { - this._isDirty = value; - } - }; - }; -})(jQuery); - -//define alias for use throughout application -var UmbClientMgr = new Umbraco.Application.ClientManager(); diff --git a/src/Umbraco.Web.UI.Client/lib/umbraco/compat.js b/src/Umbraco.Web.UI.Client/lib/umbraco/compat.js deleted file mode 100644 index a7c0b32938..0000000000 --- a/src/Umbraco.Web.UI.Client/lib/umbraco/compat.js +++ /dev/null @@ -1,47 +0,0 @@ -/* contains random bits and pieces we neede to make the U6 UI behave */ - -(function ($) { - - $(document).ready(function () { - scaleScrollables("body"); - - $(window).bind("resize", function () { - scaleScrollables("body"); - }); - - $("body").click(function (event) { - var el = event.target.nodeName; - var els = ["INPUT","A","BUTTON"]; - - if(els.indexOf(el) >= 0){return;} - - var parents = $(event.target).parents("a,button"); - if(parents.length > 0){ - return; - } - - var click = $(event.target).attr('onClick'); - if(click){ - return; - } - - - UmbClientMgr.closeModalWindow(undefined); - }); - }); - - function scaleScrollables(selector) { - $(".umb-scrollable").each(function() { - var el = jQuery(this); - var totalOffset = 0; - var offsety = el.data("offset-y"); - - if (offsety != undefined) - totalOffset += offsety; - - el.height($(window).height() - (el.offset().top + totalOffset)); - }); - } - - -})(jQuery); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/package-lock.json b/src/Umbraco.Web.UI.Client/package-lock.json index 7d914ec558..a53244f131 100644 --- a/src/Umbraco.Web.UI.Client/package-lock.json +++ b/src/Umbraco.Web.UI.Client/package-lock.json @@ -1948,7 +1948,7 @@ }, "uuid": { "version": "2.0.3", - "resolved": "http://registry.npmjs.org/uuid/-/uuid-2.0.3.tgz", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-2.0.3.tgz", "integrity": "sha1-Z+LoY3lyFVMN/zGOW/nc6/1Hsho=", "dev": true }, @@ -2245,6 +2245,23 @@ } } }, + "clean-css": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.1.tgz", + "integrity": "sha512-4ZxI6dy4lrY6FHzfiy1aEOXgu4LIsW2MhwG0VBKdcoGoH/XLFgaHSdLTGr4O8Be6A8r3MOphEiI8Gc1n0ecf3g==", + "dev": true, + "requires": { + "source-map": "~0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, "cli-cursor": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", @@ -2453,7 +2470,7 @@ }, "commander": { "version": "2.8.1", - "resolved": "http://registry.npmjs.org/commander/-/commander-2.8.1.tgz", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.8.1.tgz", "integrity": "sha1-Br42f+v9oMMwqh4qBy09yXYkJdQ=", "dev": true, "requires": { @@ -3109,7 +3126,7 @@ }, "readable-stream": { "version": "1.0.34", - "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", "dev": true, "requires": { @@ -3313,7 +3330,7 @@ }, "readable-stream": { "version": "1.0.34", - "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", "dev": true, "requires": { @@ -3374,7 +3391,7 @@ }, "readable-stream": { "version": "1.0.34", - "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", "dev": true, "requires": { @@ -3434,7 +3451,7 @@ }, "readable-stream": { "version": "1.0.34", - "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", "dev": true, "requires": { @@ -3823,7 +3840,7 @@ }, "readable-stream": { "version": "1.0.34", - "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", "dev": true, "requires": { @@ -5334,9 +5351,9 @@ } }, "font-awesome": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/font-awesome/-/font-awesome-4.2.0.tgz", - "integrity": "sha1-RzOKGgF9pr75XOLhsOF2go/Yreo=" + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/font-awesome/-/font-awesome-4.7.0.tgz", + "integrity": "sha1-j6jPBBGhoxr9B7BtKQK7n8gVoTM=" }, "for-in": { "version": "1.0.2", @@ -6037,7 +6054,7 @@ }, "get-stream": { "version": "3.0.0", - "resolved": "http://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", "dev": true, "optional": true @@ -6379,7 +6396,7 @@ }, "got": { "version": "5.7.1", - "resolved": "http://registry.npmjs.org/got/-/got-5.7.1.tgz", + "resolved": "https://registry.npmjs.org/got/-/got-5.7.1.tgz", "integrity": "sha1-X4FjWmHkplifGAVp6k44FoClHzU=", "dev": true, "requires": { @@ -6505,6 +6522,18 @@ } } }, + "gulp-clean-css": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/gulp-clean-css/-/gulp-clean-css-3.10.0.tgz", + "integrity": "sha512-7Isf9Y690o/Q5MVjEylH1H7L8WeZ89woW7DnhD5unTintOdZb67KdOayRgp9trUFo+f9UyJtuatV42e/+kghPg==", + "dev": true, + "requires": { + "clean-css": "4.2.1", + "plugin-error": "1.0.1", + "through2": "2.0.3", + "vinyl-sourcemaps-apply": "0.2.1" + } + }, "gulp-concat": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/gulp-concat/-/gulp-concat-2.6.1.tgz", @@ -8256,7 +8285,7 @@ }, "is-obj": { "version": "1.0.1", - "resolved": "http://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", "dev": true }, @@ -13057,7 +13086,7 @@ }, "strip-dirs": { "version": "1.1.1", - "resolved": "http://registry.npmjs.org/strip-dirs/-/strip-dirs-1.1.1.tgz", + "resolved": "https://registry.npmjs.org/strip-dirs/-/strip-dirs-1.1.1.tgz", "integrity": "sha1-lgu9EoeETzl1pFWKoQOoJV4kVqA=", "dev": true, "requires": { @@ -13086,7 +13115,7 @@ }, "minimist": { "version": "1.2.0", - "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "dev": true } diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index 974a0b017e..c70d3df8e4 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -23,7 +23,7 @@ "clipboard": "2.0.0", "diff": "3.4.0", "flatpickr": "4.5.2", - "font-awesome": "4.2.0", + "font-awesome": "4.7.0", "jquery": "2.2.4", "jquery-migrate": "1.4.0", "jquery-ui-dist": "1.12.1", @@ -42,6 +42,7 @@ "@babel/preset-env": "^7.1.0", "autoprefixer": "^6.5.0", "bower-installer": "^1.2.0", + "gulp-clean-css": "3.10.0", "cssnano": "^3.7.6", "gulp": "^3.9.1", "gulp-babel": "^8.0.0-beta.2", diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbcontextmenu.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbcontextmenu.directive.js index 45c5c200cb..8d1420b73e 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbcontextmenu.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbcontextmenu.directive.js @@ -1,5 +1,5 @@ angular.module("umbraco.directives") -.directive('umbContextMenu', function (navigationService) { +.directive('umbContextMenu', function (navigationService, keyboardService) { return { scope: { menuDialogTitle: "@", @@ -17,6 +17,20 @@ angular.module("umbraco.directives") scope.executeMenuItem = function (action) { navigationService.executeMenuAction(action, scope.currentNode, scope.currentSection); }; + + scope.outSideClick = function() { + navigationService.hideNavigation(); + }; + + keyboardService.bind("esc", function() { + navigationService.hideNavigation(); + }); + + //ensure to unregister from all events! + scope.$on('$destroy', function () { + keyboardService.unbind("esc"); + }); + } }; }); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umblogin.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umblogin.directive.js new file mode 100644 index 0000000000..6b09a8de2f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umblogin.directive.js @@ -0,0 +1,430 @@ +(function () { + 'use strict'; + + angular + .module('umbraco.directives') + .component('umbLogin', { + templateUrl: 'views/components/application/umb-login.html', + controller: UmbLoginController, + controllerAs: 'vm', + bindings: { + isTimedOut: "<", + onLogin: "&" + } + }); + + function UmbLoginController($scope, $location, currentUserResource, formHelper, mediaHelper, umbRequestHelper, Upload, localizationService, userService, externalLoginInfo, resetPasswordCodeInfo, $timeout, authResource, $q) { + + const vm = this; + let twoFactorloginDialog = null; + + vm.invitedUser = null; + + vm.invitedUserPasswordModel = { + password: "", + confirmPassword: "", + buttonState: "", + passwordPolicies: null, + passwordPolicyText: "" + }; + + vm.loginStates = { + submitButton: "init" + }; + + vm.avatarFile = { + filesHolder: null, + uploadStatus: null, + uploadProgress: 0, + maxFileSize: Umbraco.Sys.ServerVariables.umbracoSettings.maxFileSize + "KB", + acceptedFileTypes: mediaHelper.formatFileTypes(Umbraco.Sys.ServerVariables.umbracoSettings.imageFileTypes), + uploaded: false + }; + + vm.allowPasswordReset = Umbraco.Sys.ServerVariables.umbracoSettings.canSendRequiredEmail && Umbraco.Sys.ServerVariables.umbracoSettings.allowPasswordReset; + vm.errorMsg = ""; + vm.externalLoginFormAction = Umbraco.Sys.ServerVariables.umbracoUrls.externalLoginsUrl; + vm.externalLoginProviders = externalLoginInfo.providers; + vm.externalLoginInfo = externalLoginInfo; + vm.resetPasswordCodeInfo = resetPasswordCodeInfo; + vm.backgroundImage = Umbraco.Sys.ServerVariables.umbracoSettings.loginBackgroundImage; + + vm.$onInit = onInit; + vm.togglePassword = togglePassword; + vm.changeAvatar = changeAvatar; + vm.getStarted = getStarted; + vm.inviteSavePassword = inviteSavePassword; + vm.showLogin = showLogin; + vm.showRequestPasswordReset = showRequestPasswordReset; + vm.showSetPassword = showSetPassword; + vm.loginSubmit = loginSubmit; + vm.requestPasswordResetSubmit = requestPasswordResetSubmit; + vm.setPasswordSubmit = setPasswordSubmit; + + function onInit() { + + // Check if it is a new user + const inviteVal = $location.search().invite; + //1 = enter password, 2 = password set, 3 = invalid token + if (inviteVal && (inviteVal === "1" || inviteVal === "2")) { + + $q.all([ + //get the current invite user + authResource.getCurrentInvitedUser().then(function (data) { + vm.invitedUser = data; + }, + function () { + //it failed so we should remove the search + $location.search('invite', null); + }), + //get the membership provider config for password policies + authResource.getMembershipProviderConfig().then(function (data) { + vm.invitedUserPasswordModel.passwordPolicies = data; + + //localize the text + localizationService.localize("errorHandling_errorInPasswordFormat", [ + vm.invitedUserPasswordModel.passwordPolicies.minPasswordLength, + vm.invitedUserPasswordModel.passwordPolicies.minNonAlphaNumericChars + ]).then(function (data) { + vm.invitedUserPasswordModel.passwordPolicyText = data; + }); + }) + ]).then(function () { + vm.inviteStep = Number(inviteVal); + }); + + } else if (inviteVal && inviteVal === "3") { + vm.inviteStep = Number(inviteVal); + } + + // set the welcome greeting + setGreeting(); + + // show the correct panel + if (vm.resetPasswordCodeInfo.resetCodeModel) { + vm.showSetPassword(); + } + else if (vm.resetPasswordCodeInfo.errors.length > 0) { + vm.view = "password-reset-code-expired"; + } + else { + vm.showLogin(); + } + + } + + function togglePassword() { + var elem = $("form[name='vm.loginForm'] input[name='password']"); + elem.attr("type", (elem.attr("type") === "text" ? "password" : "text")); + $(".password-text.show, .password-text.hide").toggle(); + } + + function changeAvatar(files, event) { + if (files && files.length > 0) { + upload(files[0]); + } + } + + function getStarted() { + $location.search('invite', null); + if(vm.onLogin) { + vm.onLogin(); + } + } + + function inviteSavePassword () { + + if (formHelper.submitForm({ scope: $scope })) { + + vm.invitedUserPasswordModel.buttonState = "busy"; + + currentUserResource.performSetInvitedUserPassword(vm.invitedUserPasswordModel.password) + .then(function (data) { + + //success + formHelper.resetForm({ scope: $scope }); + vm.invitedUserPasswordModel.buttonState = "success"; + //set the user and set them as logged in + vm.invitedUser = data; + userService.setAuthenticationSuccessful(data); + + vm.inviteStep = 2; + + }, function (err) { + formHelper.handleError(err); + vm.invitedUserPasswordModel.buttonState = "error"; + }); + } + } + + function showLogin() { + vm.errorMsg = ""; + resetInputValidation(); + vm.view = "login"; + setFieldFocus("loginForm", "username"); + } + + function showRequestPasswordReset() { + vm.errorMsg = ""; + resetInputValidation(); + vm.view = "request-password-reset"; + vm.showEmailResetConfirmation = false; + setFieldFocus("requestPasswordResetForm", "email"); + } + + function showSetPassword() { + vm.errorMsg = ""; + resetInputValidation(); + vm.view = "set-password"; + setFieldFocus("setPasswordForm", "password"); + } + + function loginSubmit(login, password) { + + //TODO: Do validation properly like in the invite password update + + //if the login and password are not empty we need to automatically + // validate them - this is because if there are validation errors on the server + // then the user has to change both username & password to resubmit which isn't ideal, + // so if they're not empty, we'll just make sure to set them to valid. + if (login && password && login.length > 0 && password.length > 0) { + vm.loginForm.username.$setValidity('auth', true); + vm.loginForm.password.$setValidity('auth', true); + } + + if (vm.loginForm.$invalid) { + return; + } + + vm.loginStates.submitButton = "busy"; + + userService.authenticate(login, password) + .then(function (data) { + vm.loginStates.submitButton = "success"; + userService._retryRequestQueue(true); + if(vm.onLogin) { + vm.onLogin(); + } + }, + function (reason) { + + //is Two Factor required? + if (reason.status === 402) { + vm.errorMsg = "Additional authentication required"; + show2FALoginDialog(reason.data.twoFactorView, submit); + } + else { + vm.loginStates.submitButton = "error"; + vm.errorMsg = reason.errorMsg; + + //set the form inputs to invalid + vm.loginForm.username.$setValidity("auth", false); + vm.loginForm.password.$setValidity("auth", false); + } + + userService._retryRequestQueue(); + + }); + + //setup a watch for both of the model values changing, if they change + // while the form is invalid, then revalidate them so that the form can + // be submitted again. + vm.loginForm.username.$viewChangeListeners.push(function () { + if (vm.loginForm.$invalid) { + vm.loginForm.username.$setValidity('auth', true); + vm.loginForm.password.$setValidity('auth', true); + } + }); + vm.loginForm.password.$viewChangeListeners.push(function () { + if (vm.loginForm.$invalid) { + vm.loginForm.username.$setValidity('auth', true); + vm.loginForm.password.$setValidity('auth', true); + } + }); + } + + function requestPasswordResetSubmit(email) { + + //TODO: Do validation properly like in the invite password update + + if (email && email.length > 0) { + vm.requestPasswordResetForm.email.$setValidity('auth', true); + } + + vm.showEmailResetConfirmation = false; + + if (vm.requestPasswordResetForm.$invalid) { + return; + } + + vm.errorMsg = ""; + + authResource.performRequestPasswordReset(email) + .then(function () { + //remove the email entered + vm.email = ""; + vm.showEmailResetConfirmation = true; + }, function (reason) { + vm.errorMsg = reason.errorMsg; + vm.requestPasswordResetForm.email.$setValidity("auth", false); + }); + + vm.requestPasswordResetForm.email.$viewChangeListeners.push(function () { + if (vm.requestPasswordResetForm.email.$invalid) { + vm.requestPasswordResetForm.email.$setValidity('auth', true); + } + }); + } + + function setPasswordSubmit(password, confirmPassword) { + + vm.showSetPasswordConfirmation = false; + + if (password && confirmPassword && password.length > 0 && confirmPassword.length > 0) { + vm.setPasswordForm.password.$setValidity('auth', true); + vm.setPasswordForm.confirmPassword.$setValidity('auth', true); + } + + if (vm.setPasswordForm.$invalid) { + return; + } + + //TODO: All of this logic can/should be shared! We should do validation the nice way instead of all of this manual stuff, see: inviteSavePassword + authResource.performSetPassword(vm.resetPasswordCodeInfo.resetCodeModel.userId, password, confirmPassword, vm.resetPasswordCodeInfo.resetCodeModel.resetCode) + .then(function () { + vm.showSetPasswordConfirmation = true; + vm.resetComplete = true; + + //reset the values in the resetPasswordCodeInfo angular so if someone logs out the change password isn't shown again + resetPasswordCodeInfo.resetCodeModel = null; + + }, function (reason) { + if (reason.data && reason.data.Message) { + vm.errorMsg = reason.data.Message; + } + else { + vm.errorMsg = reason.errorMsg; + } + vm.setPasswordForm.password.$setValidity("auth", false); + vm.setPasswordForm.confirmPassword.$setValidity("auth", false); + }); + + vm.setPasswordForm.password.$viewChangeListeners.push(function () { + if (vm.setPasswordForm.password.$invalid) { + vm.setPasswordForm.password.$setValidity('auth', true); + } + }); + + vm.setPasswordForm.confirmPassword.$viewChangeListeners.push(function () { + if (vm.setPasswordForm.confirmPassword.$invalid) { + vm.setPasswordForm.confirmPassword.$setValidity('auth', true); + } + }); + } + + + //// + + function setGreeting() { + const date = new Date(); + localizationService.localize("login_greeting" + date.getDay()).then(function (label) { + $scope.greeting = label; + }); + } + + function upload(file) { + + vm.avatarFile.uploadProgress = 0; + + Upload.upload({ + url: umbRequestHelper.getApiUrl("currentUserApiBaseUrl", "PostSetAvatar"), + fields: {}, + file: file + }).progress(function (evt) { + + if (vm.avatarFile.uploadStatus !== "done" && vm.avatarFile.uploadStatus !== "error") { + // set uploading status on file + vm.avatarFile.uploadStatus = "uploading"; + + // calculate progress in percentage + var progressPercentage = parseInt(100.0 * evt.loaded / evt.total, 10); + + // set percentage property on file + vm.avatarFile.uploadProgress = progressPercentage; + } + + }).success(function (data, status, headers, config) { + + vm.avatarFile.uploadProgress = 100; + + // set done status on file + vm.avatarFile.uploadStatus = "done"; + + vm.invitedUser.avatars = data; + + vm.avatarFile.uploaded = true; + + }).error(function (evt, status, headers, config) { + + // set status done + vm.avatarFile.uploadStatus = "error"; + + // If file not found, server will return a 404 and display this message + if (status === 404) { + vm.avatarFile.serverErrorMessage = "File not found"; + } + else if (status == 400) { + //it's a validation error + vm.avatarFile.serverErrorMessage = evt.message; + } + else { + //it's an unhandled error + //if the service returns a detailed error + if (evt.InnerException) { + vm.avatarFile.serverErrorMessage = evt.InnerException.ExceptionMessage; + + //Check if its the common "too large file" exception + if (evt.InnerException.StackTrace && evt.InnerException.StackTrace.indexOf("ValidateRequestEntityLength") > 0) { + vm.avatarFile.serverErrorMessage = "File too large to upload"; + } + + } else if (evt.Message) { + vm.avatarFile.serverErrorMessage = evt.Message; + } + } + }); + } + + function setFieldFocus(form, field) { + $timeout(function () { + $("form[name='" + form + "'] input[name='" + field + "']").focus(); + }); + } + + function show2FALoginDialog(view, callback) { + // TODO: show 2FA window + } + + function resetInputValidation() { + vm.confirmPassword = ""; + vm.password = ""; + vm.login = ""; + if (vm.loginForm) { + vm.loginForm.username.$setValidity('auth', true); + vm.loginForm.password.$setValidity('auth', true); + } + if (vm.requestPasswordResetForm) { + vm.requestPasswordResetForm.email.$setValidity("auth", true); + } + if (vm.setPasswordForm) { + vm.setPasswordForm.password.$setValidity('auth', true); + vm.setPasswordForm.confirmPassword.$setValidity('auth', true); + } + } + + + + + } + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbsections.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbsections.directive.js index 7f906ddcc0..5006087ca5 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbsections.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbsections.directive.js @@ -12,7 +12,7 @@ function sectionsDirective($timeout, $window, navigationService, treeService, se var sectionItemsWidth = []; var evts = []; - var maxSections = 7; + var maxSections = 8; //setup scope vars scope.maxSections = maxSections; @@ -46,8 +46,8 @@ function sectionsDirective($timeout, $window, navigationService, treeService, se var sectionsWidth = 0; scope.totalSections = scope.sections.length; scope.maxSections = maxSections; - scope.overflowingSections = 0; - scope.needTray = false; + scope.overflowingSections = scope.maxSections - scope.totalSections; + scope.needTray = scope.sections.length > scope.maxSections; // detect how many sections we can show on the screen for (var i = 0; i < sectionItemsWidth.length; i++) { diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour/umbtourstep.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour/umbtourstep.directive.js index 08e0a44c0b..2dc0ebdf93 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour/umbtourstep.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour/umbtourstep.directive.js @@ -1,4 +1,17 @@ -(function() { +/** +@ngdoc directive +@name umbraco.directives.directive:umbTourStep +@restrict E +@scope + +@description +Added in Umbraco 7.8. The tour step component is a component that can be used in custom views for tour steps. + +@param {callback} onClose The callback which should be performened when the close button of the tour step is clicked +@param {boolean=} hideClose A boolean indicating if the close button needs to be shown + +**/ +(function () { 'use strict'; function TourStepDirective() { diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour/umbtourstepcontent.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour/umbtourstepcontent.directive.js index 52ed358b61..909f87eac5 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour/umbtourstepcontent.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour/umbtourstepcontent.directive.js @@ -1,4 +1,17 @@ -(function() { +/** +@ngdoc directive +@name umbraco.directives.directive:umbTourStepContent +@restrict E +@scope + +@description +Added in Umbraco 7.8. The tour step content component is a component that can be used in custom views for tour steps. +It's meant to be used in the umb-tour-step directive. +All markup in the body of the directive will be shown after the content attribute + +@param {string} content The content that needs to be shown +**/ +(function () { 'use strict'; function TourStepContentDirective() { diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour/umbtourstepcounter.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour/umbtourstepcounter.directive.js index 7e04ef5d00..2477953580 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour/umbtourstepcounter.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour/umbtourstepcounter.directive.js @@ -1,4 +1,18 @@ -(function() { +/** +@ngdoc directive +@name umbraco.directives.directive:umbTourStepCounter +@restrict E +@scope + +@description +Added in Umbraco 7.8. The tour step counter component is a component that can be used in custom views for tour steps. +It's meant to be used in the umb-tour-step-footer directive. It will show the progress you have made in a tour eg. step 2/12 + + +@param {int} currentStep The current step the tour is on +@param {int} totalSteps The current step the tour is on +**/ +(function () { 'use strict'; function TourStepCounterDirective() { diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour/umbtourstepfooter.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour/umbtourstepfooter.directive.js index fedb527972..7cfb498394 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour/umbtourstepfooter.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour/umbtourstepfooter.directive.js @@ -1,4 +1,16 @@ -(function() { +/** +@ngdoc directive +@name umbraco.directives.directive:umbTourStepFooter +@restrict E +@scope + +@description +Added in Umbraco 7.8. The tour step footer component is a component that can be used in custom views for tour steps. It's meant to be used in the umb-tour-step directive. +All markup in the body of the directive will be shown as the footer of the tour step + + +**/ +(function () { 'use strict'; function TourStepFooterDirective() { diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour/umbtourstepheader.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour/umbtourstepheader.directive.js index 9d32ad87a4..ec6768928f 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour/umbtourstepheader.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour/umbtourstepheader.directive.js @@ -1,4 +1,16 @@ -(function() { +/** +@ngdoc directive +@name umbraco.directives.directive:umbTourStepHeader +@restrict E +@scope + +@description +Added in Umbraco 7.8. The tour step header component is a component that can be used in custom views for tour steps. It's meant to be used in the umb-tour-step directive. + + +@param {string} title The title that needs to be shown +**/ +(function () { 'use strict'; function TourStepHeaderDirective() { diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js index efb63fe1ac..422c02c87c 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js @@ -46,7 +46,7 @@ $scope.ancestors = anc; }); $scope.$watch('culture', - function(value, oldValue) { + function (value, oldValue) { entityResource.getAncestors(content.id, "document", value) .then(function (anc) { $scope.ancestors = anc; @@ -86,8 +86,8 @@ } } - /** Returns true if the save/publish dialog should be shown when pressing the button */ - function showSaveOrPublishDialog() { + /** Returns true if the content item varies by culture */ + function isContentCultureVariant() { return $scope.content.variants.length > 1; } @@ -103,6 +103,17 @@ loadContent(); } })); + + evts.push(eventsService.on("editors.content.reload", function (name, args) { + // if this content item uses the updated doc type we need to reload the content item + if(args && args.node && args.node.key === $scope.content.key) { + $scope.page.loading = true; + loadContent().then(function() { + $scope.page.loading = false; + }); + } + })); + } /** @@ -146,7 +157,7 @@ // only create the save/publish/preview buttons if the // content app is "Conent" - if(app && app.alias !== "umbContent" && app.alias !== "umbInfo") { + if (app && app.alias !== "umbContent" && app.alias !== "umbInfo") { $scope.defaultButton = null; $scope.subButtons = null; $scope.page.showSaveButton = false; @@ -155,7 +166,7 @@ } // create the save button - if(_.contains($scope.content.allowedActions, "A")) { + if (_.contains($scope.content.allowedActions, "A")) { $scope.page.showSaveButton = true; // add ellipsis to the save button if it opens the variant overlay $scope.page.saveButtonEllipsis = content.variants && content.variants.length > 1 ? "true" : "false"; @@ -170,7 +181,8 @@ saveAndPublish: $scope.saveAndPublish, sendToPublish: $scope.sendToPublish, unpublish: $scope.unpublish, - schedulePublish: $scope.schedule + schedulePublish: $scope.schedule, + publishDescendants: $scope.publishDescendants } }); @@ -224,7 +236,7 @@ } } - function checkValidility(){ + function checkValidility() { //Get all controls from the 'contentForm' var allControls = $scope.contentForm.$getControls(); @@ -233,7 +245,7 @@ //Exclude known formControls 'contentHeaderForm' and 'tabbedContentForm' //Check property - $name === "contentHeaderForm" - allControls = _.filter(allControls, function(obj){ + allControls = _.filter(allControls, function (obj) { return obj.$name !== 'contentHeaderForm' && obj.$name !== 'tabbedContentForm' && obj.hasOwnProperty('$submitted'); }); @@ -251,26 +263,26 @@ } //Controls is the - function recurseFormControls(controls, array){ + function recurseFormControls(controls, array) { //Loop over the controls for (var i = 0; i < controls.length; i++) { var controlItem = controls[i]; //Check if the controlItem has a property '' - if(controlItem.hasOwnProperty('$submitted')){ + if (controlItem.hasOwnProperty('$submitted')) { //This item is a form - so lets get the child controls of it & recurse again var childFormControls = controlItem.$getControls(); recurseFormControls(childFormControls, array); } else { //We can assume its a field on a form - if(controlItem.hasOwnProperty('$error')){ + if (controlItem.hasOwnProperty('$error')) { //Set the validlity of the error/s to be valid //String of keys of error invalid messages var errorKeys = []; - for(var key in controlItem.$error){ + for (var key in controlItem.$error) { errorKeys.push(key); controlItem.$setValidity(key, true); } @@ -286,7 +298,7 @@ return array; } - function resetNestedFieldValiation(array){ + function resetNestedFieldValiation(array) { for (var i = 0; i < array.length; i++) { var item = array[i]; //Item is an object containing two props @@ -294,7 +306,7 @@ var fieldControl = item.control; var fieldErrorKeys = item.errorKeys; - for(var i = 0; i < fieldErrorKeys.length; i++) { + for (var i = 0; i < fieldErrorKeys.length; i++) { fieldControl.$setValidity(fieldErrorKeys[i], false); } } @@ -302,7 +314,7 @@ // This is a helper method to reduce the amount of code repitition for actions: Save, Publish, SendToPublish function performSave(args) { - + //Used to check validility of nested form - coming from Content Apps mostly //Set them all to be invalid @@ -380,6 +392,34 @@ } } + function moveNode(node, target) { + + contentResource.move({ "parentId": target.id, "id": node.id }) + .then(function (path) { + + // remove the node that we're working on + if ($scope.page.menu.currentNode) { + treeService.removeNode($scope.page.menu.currentNode); + } + + // sync the destination node + if (!infiniteMode) { + navigationService.syncTree({ tree: "content", path: path, forceReload: true, activate: false }); + } + + $scope.page.buttonRestore = "success"; + notificationsService.success("Successfully restored " + node.name + " to " + target.name); + + // reload the node + loadContent(); + + }, function (err) { + $scope.page.buttonRestore = "error"; + notificationsService.error("Cannot automatically restore this item", err); + }); + + } + if ($scope.page.isNew) { $scope.page.loading = true; @@ -409,7 +449,7 @@ }); } - $scope.unpublish = function() { + $scope.unpublish = function () { clearNotifications($scope.content); if (formHelper.submitForm({ scope: $scope, action: "unpublish", skipValidation: true })) { var dialog = { @@ -421,9 +461,9 @@ submit: function (model) { model.submitButtonState = "busy"; - - var selectedVariants = _.filter(model.variants, function(variant) { return variant.save; }); - var culturesForUnpublishing = _.map(selectedVariants, function(variant) { return variant.language.culture; }); + + var selectedVariants = _.filter(model.variants, v => v.save && v.language); //ignore invariant + var culturesForUnpublishing = _.map(selectedVariants, v => v.language.culture); contentResource.unpublish($scope.content.id, culturesForUnpublishing) .then(function (data) { @@ -437,8 +477,8 @@ }, function (err) { $scope.page.buttonGroupState = 'error'; }); - - + + }, close: function () { overlayService.close(); @@ -447,10 +487,10 @@ overlayService.open(dialog); } }; - + $scope.sendToPublish = function () { clearNotifications($scope.content); - if (showSaveOrPublishDialog()) { + if (isContentCultureVariant()) { //before we launch the dialog we want to execute all client side validations first if (formHelper.submitForm({ scope: $scope, action: "publish" })) { @@ -464,7 +504,24 @@ model.submitButtonState = "busy"; clearNotifications($scope.content); //we need to return this promise so that the dialog can handle the result and wire up the validation response - console.log("saving need to happen here"); + return performSave({ + saveMethod: contentResource.sendToPublish, + action: "sendToPublish", + showNotifications: false + }).then(function (data) { + //show all notifications manually here since we disabled showing them automatically in the save method + formHelper.showNotifications(data); + clearNotifications($scope.content); + overlayService.close(); + return $q.when(data); + }, function (err) { + clearDirtyState($scope.content.variants); + model.submitButtonState = "error"; + //re-map the dialog model since we've re-bound the properties + dialog.variants = $scope.content.variants; + //don't reject, we've handled the error + return $q.when(err); + }); }, close: function () { overlayService.close(); @@ -476,10 +533,10 @@ } else { $scope.page.buttonGroupState = "busy"; - return performSave({ - saveMethod: contentResource.sendToPublish, - action: "sendToPublish" - }).then(function(){ + return performSave({ + saveMethod: contentResource.sendToPublish, + action: "sendToPublish" + }).then(function () { $scope.page.buttonGroupState = "success"; }, function () { $scope.page.buttonGroupState = "error"; @@ -489,7 +546,7 @@ $scope.saveAndPublish = function () { clearNotifications($scope.content); - if (showSaveOrPublishDialog()) { + if (isContentCultureVariant()) { //before we launch the dialog we want to execute all client side validations first if (formHelper.submitForm({ scope: $scope, action: "publish" })) { @@ -532,13 +589,14 @@ } } else { - //ensure the publish flag is set + //ensure the flags are set + $scope.content.variants[0].save = true; $scope.content.variants[0].publish = true; $scope.page.buttonGroupState = "busy"; - return performSave({ - saveMethod: contentResource.publish, - action: "publish" - }).then(function(){ + return performSave({ + saveMethod: contentResource.publish, + action: "publish" + }).then(function () { $scope.page.buttonGroupState = "success"; }, function () { $scope.page.buttonGroupState = "error"; @@ -549,7 +607,7 @@ $scope.save = function () { clearNotifications($scope.content); // TODO: Add "..." to save button label if there are more than one variant to publish - currently it just adds the elipses if there's more than 1 variant - if (showSaveOrPublishDialog()) { + if (isContentCultureVariant()) { //before we launch the dialog we want to execute all client side validations first if (formHelper.submitForm({ scope: $scope, action: "save" })) { @@ -591,15 +649,17 @@ } } else { + //ensure the flags are set + $scope.content.variants[0].save = true; $scope.page.saveButtonState = "busy"; return performSave({ - saveMethod: $scope.saveMethod(), - action: "save" - }).then(function(){ - $scope.page.saveButtonState = "success"; - }, function () { - $scope.page.saveButtonState = "error"; - }); + saveMethod: $scope.saveMethod(), + action: "save" + }).then(function () { + $scope.page.saveButtonState = "success"; + }, function () { + $scope.page.saveButtonState = "error"; + }); } }; @@ -609,6 +669,20 @@ //before we launch the dialog we want to execute all client side validations first if (formHelper.submitForm({ scope: $scope, action: "schedule" })) { + //used to track the original values so if the user doesn't save the schedule and they close the dialog we reset the dates back to what they were. + let origDates = []; + for (let i = 0; i < $scope.content.variants.length; i++) { + origDates.push({ + releaseDate: $scope.content.variants[i].releaseDate, + expireDate: $scope.content.variants[i].expireDate + }); + } + + if (!isContentCultureVariant()) { + //ensure the flags are set + $scope.content.variants[0].save = true; + } + var dialog = { parentScope: $scope, view: "views/content/overlays/schedule.html", @@ -618,7 +692,92 @@ submit: function (model) { model.submitButtonState = "busy"; clearNotifications($scope.content); - model.submitButtonState = "success"; + + //we need to return this promise so that the dialog can handle the result and wire up the validation response + return performSave({ + saveMethod: contentResource.saveSchedule, + action: "schedule", + showNotifications: false + }).then(function (data) { + //show all notifications manually here since we disabled showing them automatically in the save method + formHelper.showNotifications(data); + clearNotifications($scope.content); + overlayService.close(); + return $q.when(data); + }, function (err) { + clearDirtyState($scope.content.variants); + //if this is invariant, show the notification errors, else they'll be shown inline with the variant + if (!isContentCultureVariant()) { + formHelper.showNotifications(err.data); + } + model.submitButtonState = "error"; + //re-map the dialog model since we've re-bound the properties + dialog.variants = $scope.content.variants; + //don't reject, we've handled the error + return $q.when(err); + }); + + }, + close: function () { + overlayService.close(); + //restore the dates + for (let i = 0; i < $scope.content.variants.length; i++) { + $scope.content.variants[i].releaseDate = origDates[i].releaseDate; + $scope.content.variants[i].expireDate = origDates[i].expireDate; + } + } + }; + overlayService.open(dialog); + } + }; + + $scope.publishDescendants = function() { + clearNotifications($scope.content); + //before we launch the dialog we want to execute all client side validations first + if (formHelper.submitForm({ scope: $scope, action: "publishDescendants" })) { + + if (!isContentCultureVariant()) { + //ensure the flags are set + $scope.content.variants[0].save = true; + $scope.content.variants[0].publish = true; + } + + var dialog = { + parentScope: $scope, + view: "views/content/overlays/publishdescendants.html", + variants: $scope.content.variants, //set a model property for the dialog + skipFormValidation: true, //when submitting the overlay form, skip any client side validation + submitButtonLabelKey: "buttons_publishDescendants", + submit: function (model) { + model.submitButtonState = "busy"; + clearNotifications($scope.content); + + //we need to return this promise so that the dialog can handle the result and wire up the validation response + return performSave({ + saveMethod: function (content, create, files, showNotifications) { + return contentResource.publishWithDescendants(content, create, model.includeUnpublished, files, showNotifications); + }, + action: "publishDescendants", + showNotifications: false + }).then(function (data) { + //show all notifications manually here since we disabled showing them automatically in the save method + formHelper.showNotifications(data); + clearNotifications($scope.content); + overlayService.close(); + return $q.when(data); + }, function (err) { + clearDirtyState($scope.content.variants); + //if this is invariant, show the notification errors, else they'll be shown inline with the variant + if (!isContentCultureVariant()) { + formHelper.showNotifications(err.data); + } + model.submitButtonState = "error"; + //re-map the dialog model since we've re-bound the properties + dialog.variants = $scope.content.variants; + //don't reject, we've handled the error + return $q.when(err); + }); + }, close: function () { overlayService.close(); @@ -746,38 +905,10 @@ * Call back when a content app changes * @param {any} app */ - $scope.appChanged = function(app) { + $scope.appChanged = function (app) { createButtons($scope.content, app); }; - function moveNode(node, target) { - - contentResource.move({ "parentId": target.id, "id": node.id }) - .then(function (path) { - - // remove the node that we're working on - if ($scope.page.menu.currentNode) { - treeService.removeNode($scope.page.menu.currentNode); - } - - // sync the destination node - if (!infiniteMode) { - navigationService.syncTree({ tree: "content", path: path, forceReload: true, activate: false }); - } - - $scope.page.buttonRestore = "success"; - notificationsService.success("Successfully restored " + node.name + " to " + target.name); - - // reload the node - loadContent(); - - }, function (err) { - $scope.page.buttonRestore = "error"; - notificationsService.error("Cannot automatically restore this item", err); - }); - - } - // methods for infinite editing $scope.close = function () { if ($scope.infiniteModel.close) { diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbcontentnodeinfo.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbcontentnodeinfo.directive.js index b2e64983d6..78efc8f789 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbcontentnodeinfo.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbcontentnodeinfo.directive.js @@ -1,12 +1,13 @@ (function () { 'use strict'; - function ContentNodeInfoDirective($timeout, $location, logResource, eventsService, userService, localizationService, dateHelper, editorService, redirectUrlsResource) { + function ContentNodeInfoDirective($timeout, $routeParams, logResource, eventsService, userService, localizationService, dateHelper, editorService, redirectUrlsResource) { - function link(scope, element, attrs, ctrl) { + function link(scope, element, attrs, umbVariantContentCtrl) { var evts = []; var isInfoTab = false; + var auditTrailLoaded = false; var labels = {}; scope.publishStatus = []; @@ -63,10 +64,22 @@ if (scope.documentType !== null) { scope.previewOpenUrl = '#/settings/documenttypes/edit/' + scope.documentType.id; } + + //load in the audit trail if we are currently looking at the INFO tab + if (umbVariantContentCtrl) { + var activeApp = _.find(umbVariantContentCtrl.editor.content.apps, a => a.active); + if (activeApp.alias === "umbInfo") { + isInfoTab = true; + loadAuditTrail(); + loadRedirectUrls(); + } + } + } scope.auditTrailPageChange = function (pageNumber) { scope.auditTrailOptions.pageNumber = pageNumber; + auditTrailLoaded = false; loadAuditTrail(); }; @@ -101,8 +114,27 @@ scope.node.template = templateAlias; }; + scope.openRollback = function() { + + var rollback = { + node: scope.node, + submit: function(model) { + const args = { node: scope.node }; + eventsService.emit("editors.content.reload", args); + editorService.close(); + }, + close: function() { + editorService.close(); + } + }; + editorService.rollback(rollback); + }; + function loadAuditTrail() { + //don't load this if it's already done + if (auditTrailLoaded) { return; }; + scope.loadingAuditTrail = true; logResource.getPagedEntityLog(scope.auditTrailOptions) @@ -124,6 +156,8 @@ setAuditTrailLogTypeColor(scope.auditTrail); scope.loadingAuditTrail = false; + + auditTrailLoaded = true; }); } @@ -230,7 +264,8 @@ if (!newValue) { return; } if (newValue === oldValue) { return; } - if(isInfoTab) { + if (isInfoTab) { + auditTrailLoaded = false; loadAuditTrail(); loadRedirectUrls(); setNodePublishStatus(scope.node); @@ -249,6 +284,7 @@ } var directive = { + require: '^^umbVariantContent', restrict: 'E', replace: true, templateUrl: 'views/components/content/umb-content-node-info.html', diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbtabbedcontent.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbtabbedcontent.directive.js index 015255c577..f83f441d66 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbtabbedcontent.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbtabbedcontent.directive.js @@ -13,6 +13,18 @@ //expose the property/methods for other directives to use this.content = $scope.content; + $scope.activeVariant = _.find(this.content.variants, variant => { + return variant.active; + }); + + $scope.defaultVariant = _.find(this.content.variants, variant => { + return variant.language.isDefault; + }); + + $scope.unlockInvariantValue = function(property) { + property.unlockInvariantValue = !property.unlockInvariantValue; + }; + $scope.$watch("tabbedContentForm.$dirty", function (newValue, oldValue) { if (newValue === true) { @@ -22,14 +34,6 @@ }, link: function(scope) { - function onInit() { - angular.forEach(scope.content.tabs, function (group) { - group.open = true; - }); - } - - onInit(); - }, scope: { content: "=" diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontenteditors.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontenteditors.directive.js index a3a212a603..1987c897f0 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontenteditors.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontenteditors.directive.js @@ -194,7 +194,9 @@ return a.alias === "umbContent"; }); - contentApp.viewModel = _.omit(variant, 'apps'); + //The view model for the content app is simply the index of the variant being edited + var variantIndex = vm.content.variants.indexOf(variant); + contentApp.viewModel = variantIndex; // make sure the same app it set to active in the new variant if(activeAppAlias) { diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/grid/grid.rte.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/grid/grid.rte.directive.js index 882a808ae4..8b0e2f053d 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/grid/grid.rte.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/grid/grid.rte.directive.js @@ -1,5 +1,5 @@ angular.module("umbraco.directives") - .directive('gridRte', function (tinyMceService, angularHelper, assetsService, $q, $timeout) { + .directive('gridRte', function (tinyMceService, angularHelper, assetsService, $q, $timeout, eventsService) { return { scope: { uniqueId: '=', @@ -188,8 +188,23 @@ angular.module("umbraco.directives") // tinyMceEditor.fire('LoadContent', null); //}; + + var tabShownListener = eventsService.on("app.tabChange", function (e, args) { + + var tabId = args.id; + var myTabId = element.closest(".umb-tab-pane").attr("rel"); + + if (String(tabId) === myTabId) { + //the tab has been shown, trigger the mceAutoResize (as it could have timed out before the tab was shown) + if (tinyMceEditor !== undefined && tinyMceEditor != null) { + tinyMceEditor.execCommand('mceAutoResize', false, null, null); + } + } + + }); + //listen for formSubmitting event (the result is callback used to remove the event subscription) - var unsubscribe = scope.$on("formSubmitting", function () { + var formSubmittingListener = scope.$on("formSubmitting", function () { //TODO: Here we should parse out the macro rendered content so we can save on a lot of bytes in data xfer // we do parse it out on the server side but would be nice to do that on the client side before as well. scope.value = tinyMceEditor ? tinyMceEditor.getContent() : null; @@ -199,7 +214,8 @@ angular.module("umbraco.directives") // NOTE: this is very important otherwise if this is part of a modal, the listener still exists because the dom // element might still be there even after the modal has been hidden. scope.$on('$destroy', function () { - unsubscribe(); + formSubmittingListener(); + eventsService.unsubscribe(tabShownListener); //ensure we unbind this in case the blur doesn't fire above $('.umb-panel-body').off('scroll', pinToolbar); if (tinyMceEditor !== undefined && tinyMceEditor != null) { diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/imaging/umbimagegravity.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/imaging/umbimagegravity.directive.js index d56a4d2439..a70b8ca33e 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/imaging/umbimagegravity.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/imaging/umbimagegravity.directive.js @@ -97,10 +97,32 @@ //listen for the image DOM element loading htmlImage.on("load", function () { $timeout(function () { + + vm.isCroppable = true; + vm.hasDimensions = true; + + if (vm.src) { + if (vm.src.endsWith(".svg")) { + vm.isCroppable = false; + vm.hasDimensions = false; + } + else { + // From: https://stackoverflow.com/a/51789597/5018 + var type = vm.src.substring(vm.src.indexOf("/") + 1, vm.src.indexOf(";base64")); + if (type.startsWith("svg")) { + vm.isCroppable = false; + vm.hasDimensions = false; + } + } + } + setDimensions(); vm.loaded = true; if (vm.onImageLoaded) { - vm.onImageLoaded(); + vm.onImageLoaded({ + "isCroppable": vm.isCroppable, + "hasDimensions": vm.hasDimensions + }); } }, 100); }); @@ -147,19 +169,7 @@ /** Sets the width/height/left/top dimentions based on the image size and the "center" value */ function setDimensions() { - if (vm.src.endsWith(".svg")) { - // svg files don't automatically get a size by - // loading them set a default size for now - vm.dimensions.width = 200; - vm.dimensions.height = 200; - vm.dimensions.left = vm.center.left * vm.dimensions.width - 10; - vm.dimensions.top = vm.center.top * vm.dimensions.height - 10; - // can't crop an svg file, don't show the focal point - if (htmlOverlay) { - htmlOverlay.remove(); - } - } - else if (htmlImage && vm.center) { + if (vm.isCroppable && htmlImage && vm.center) { vm.dimensions.width = htmlImage.width(); vm.dimensions.height = htmlImage.height(); vm.dimensions.left = vm.center.left * vm.dimensions.width - 10; diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/localization/localize.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/localization/localize.directive.js index 3833dc50b9..c3093eee9e 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/localization/localize.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/localization/localize.directive.js @@ -31,13 +31,15 @@ angular.module("umbraco.directives") return { restrict: 'E', scope:{ - key: '@' + key: '@', + tokens: '=' }, replace: true, link: function (scope, element, attrs) { var key = scope.key; - localizationService.localize(key).then(function(value){ + var tokens = scope.tokens ? scope.tokens : null; + localizationService.localize(key, tokens).then(function(value){ element.html(value); }); } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbproperty.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbproperty.directive.js index 69457a6f10..302378b8c0 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbproperty.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbproperty.directive.js @@ -7,7 +7,9 @@ angular.module("umbraco.directives") .directive('umbProperty', function (umbPropEditorHelper, userService) { return { scope: { - property: "=" + property: "=", + showInherit: "<", + inheritsFrom: "<" }, transclude: true, restrict: 'E', diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbpropertyeditor.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbpropertyeditor.directive.js index 0aa2dc02c3..32cbbb31ec 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbpropertyeditor.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbpropertyeditor.directive.js @@ -12,7 +12,7 @@ function umbPropEditor(umbPropEditorHelper) { scope: { model: "=", isPreValue: "@", - preview: "@" + preview: "<" }, require: "^^form", diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbcontextdialog/umbcontextdialog.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbcontextdialog/umbcontextdialog.directive.js new file mode 100644 index 0000000000..926c59144d --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbcontextdialog/umbcontextdialog.directive.js @@ -0,0 +1,39 @@ +(function() { + 'use strict'; + + function UmbContextDialog(navigationService, keyboardService) { + + function link($scope) { + + $scope.outSideClick = function() { + navigationService.hideNavigation(); + } + + keyboardService.bind("esc", function() { + navigationService.hideNavigation(); + }); + + //ensure to unregister from all events! + $scope.$on('$destroy', function () { + keyboardService.unbind("esc"); + }); + + } + + var directive = { + restrict: 'E', + transclude: true, + templateUrl: "views/components/tree/umbcontextdialog/umb-context-dialog.html", + scope: { + title: "<", + currentNode: "<", + view: "<" + }, + link: link + }; + return directive; + } + + angular.module('umbraco.directives').directive('umbContextDialog', UmbContextDialog); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtree.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtree.directive.js index 251683edc8..836a5af2a6 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtree.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtree.directive.js @@ -80,6 +80,7 @@ function umbTreeDirective($q, $rootScope, treeService, notificationsService, use // entire tree again since we already still have it in memory. Of course if the section is different we will // reload it. This saves a lot on processing if someone is navigating in and out of the same section many times // since it saves on data retreival and DOM processing. + //TODO: This isn't used!? var lastSection = ""; /** Helper function to emit tree events */ @@ -91,6 +92,7 @@ function umbTreeDirective($q, $rootScope, treeService, notificationsService, use } } + //TODO: This isn't used!? function clearCache(section) { treeService.clearCache({ section: section }); } @@ -174,6 +176,29 @@ function umbTreeDirective($q, $rootScope, treeService, notificationsService, use } + /** This will check the section tree loaded and return all actual root nodes based on a tree type (non group nodes, non section groups) */ + function getTreeRootNodes() { + var roots; + if ($scope.tree.root.containsGroups) { + //all children in this case are group nodes, so we want the children of these children + roots = _.reduce( + //get the array of array of children + _.map($scope.tree.root.children, function (n) { + return n.children + }), function (m, p) { + //combine the arrays to one array + return m.concat(p) + }); + } + else { + roots = [$scope.tree.root].concat($scope.tree.root.children); + } + + return _.filter(roots, function (node) { + return node && node.metaData && node.metaData.treeAlias; + }); + } + //given a tree alias, this will search the current section tree for the specified tree alias and set the current active tree to it's root node function loadActiveTree(treeAlias) { @@ -189,12 +214,9 @@ function umbTreeDirective($q, $rootScope, treeService, notificationsService, use return $scope.activeTree; } - var childrenAndSelf = [$scope.tree.root].concat($scope.tree.root.children); - $scope.activeTree = _.find(childrenAndSelf, function (node) { - if (node && node.metaData && node.metaData.treeAlias) { - return node.metaData.treeAlias.toUpperCase() === treeAlias.toUpperCase(); - } - return false; + var treeRoots = getTreeRootNodes(); + $scope.activeTree = _.find(treeRoots, function (node) { + return node.metaData.treeAlias.toUpperCase() === treeAlias.toUpperCase(); }); if (!$scope.activeTree) { diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreeitem.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreeitem.directive.js index 2c8887ef3f..47ec4a83cc 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreeitem.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreeitem.directive.js @@ -77,7 +77,13 @@ angular.module("umbraco.directives") //is this the current action node (this is not the same as the current selected node!) var actionNode = appState.getMenuState("currentNode"); if (actionNode) { - if (actionNode.id === node.id) { + if (actionNode.id === node.id && String(actionNode.id) !== "-1") { + css.push("active"); + } + + // special handling of root nodes with id -1 + // as there can be many nodes with id -1 in a tree we need to check the treeAlias instead + if (String(actionNode.id) === "-1" && actionNode.metaData.treeAlias === node.metaData.treeAlias) { css.push("active"); } } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbGenerateAlias.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbGenerateAlias.directive.js index 4bc6f08eb9..47d1431e13 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbGenerateAlias.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbGenerateAlias.directive.js @@ -48,7 +48,7 @@ the directive will use {@link umbraco.directives.directive:umbLockedField umbLoc **/ angular.module("umbraco.directives") - .directive('umbGenerateAlias', function ($timeout, entityResource) { + .directive('umbGenerateAlias', function ($timeout, entityResource, localizationService) { return { restrict: 'E', templateUrl: 'views/components/umb-generate-alias.html', @@ -67,7 +67,21 @@ angular.module("umbraco.directives") var updateAlias = false; scope.locked = true; - scope.placeholderText = "Enter alias..."; + + scope.labels = { + idle: "Enter alias...", + busy: "Generating alias..." + }; + + scope.placeholderText = scope.labels.idle; + + localizationService.localize('placeholders_enterAlias').then(function (value) { + scope.labels.idle = scope.placeholderText = value; + }); + + localizationService.localize('placeholders_generatingAlias').then(function (value) { + scope.labels.busy = value; + }); function generateAlias(value) { @@ -78,7 +92,7 @@ angular.module("umbraco.directives") if( value !== undefined && value !== "" && value !== null) { scope.alias = ""; - scope.placeholderText = "Generating Alias..."; + scope.placeholderText = scope.labels.busy; generateAliasTimeout = $timeout(function () { updateAlias = true; @@ -92,7 +106,7 @@ angular.module("umbraco.directives") } else { updateAlias = true; scope.alias = ""; - scope.placeholderText = "Enter alias..."; + scope.placeholderText = scope.labels.idle; } } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbmediagrid.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbmediagrid.directive.js index b14f8418c5..308ffbf00f 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbmediagrid.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbmediagrid.directive.js @@ -125,13 +125,19 @@ Use this directive to generate a thumbnail grid of media items. i--; } - if (scope.includeSubFolders !== 'true') { - if (item.parentId !== parseInt(scope.currentFolderId)) { - scope.items.splice(i, 1); - i--; + + // If subfolder search is not enabled remove the media items that's not needed + // Make sure that includeSubFolder is not undefined since the directive is used + // in contexts where it should not be used. Currently only used when we trigger + // a media picker + if(scope.includeSubFolders !== undefined){ + if (scope.includeSubFolders !== 'true') { + if (item.parentId !== parseInt(scope.currentFolderId)) { + scope.items.splice(i, 1); + i--; + } } } - } @@ -152,7 +158,7 @@ Use this directive to generate a thumbnail grid of media items. } if (!item.isFolder) { - + // handle entity if(item.image) { item.thumbnail = mediaHelper.resolveFileFromEntity(item, true); @@ -161,7 +167,7 @@ Use this directive to generate a thumbnail grid of media items. } else { item.thumbnail = mediaHelper.resolveFile(item, true); item.image = mediaHelper.resolveFile(item, false); - + var fileProp = _.find(item.properties, function (v) { return (v.alias === "umbracoFile"); }); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/upload/umbpropertyfileupload.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/upload/umbpropertyfileupload.directive.js index 0dec3f6e0b..7ace23a988 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/upload/umbpropertyfileupload.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/upload/umbpropertyfileupload.directive.js @@ -222,7 +222,7 @@ }); //special check for a comma in the name - newVal += files[i].name.replace(',', '-') + ","; + newVal += files[i].name.split(',').join('-') + ","; if (isImage) { diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/util/disabletabindex.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/util/disabletabindex.directive.js new file mode 100644 index 0000000000..800efb8c28 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/util/disabletabindex.directive.js @@ -0,0 +1,46 @@ +angular.module("umbraco.directives") + .directive('disableTabindex', function (tabbableService) { + + return { + restrict: 'A', //Can only be used as an attribute, + scope: { + "disableTabindex": "<" + }, + link: function (scope, element, attrs) { + + if(scope.disableTabindex) { + //Select the node that will be observed for mutations (native DOM element not jQLite version) + var targetNode = element[0]; + + //Watch for DOM changes - so when the property editor subview loads in + //We can be notified its updated the child elements inside the DIV we are watching + var observer = new MutationObserver(domChange); + + // Options for the observer (which mutations to observe) + var config = { attributes: true, childList: true, subtree: false }; + + function domChange(mutationsList, observer){ + for(var mutation of mutationsList) { + + //DOM items have been added or removed + if (mutation.type == 'childList') { + + //Check if any child items in mutation.target contain an input + var childInputs = tabbableService.tabbable(mutation.target); + + //For each item in childInputs - override or set HTML attribute tabindex="-1" + angular.forEach(childInputs, function(element){ + angular.element(element).attr('tabindex', '-1'); + }); + } + } + } + + // Start observing the target node for configured mutations + //GO GO GO + observer.observe(targetNode, config); + } + + } + }; +}); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valsubview.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valsubview.directive.js index 097602fe20..d5a21e0ba6 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valsubview.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valsubview.directive.js @@ -5,74 +5,78 @@ * @description Used to show validation warnings for a editor sub view to indicate that the section content has validation errors in its data. * In order for this directive to work, the valFormManager directive must be placed on the containing form. **/ -(function() { - 'use strict'; +(function () { + 'use strict'; - function valSubViewDirective() { + function valSubViewDirective() { - function controller($scope, $element) { - //expose api - return { - valStatusChanged: function(args) { - if (!args.form.$valid) { - var subViewContent = $element.find(".ng-invalid"); + function controller($scope, $element) { + //expose api + return { + valStatusChanged: function (args) { - if (subViewContent.length > 0) { - $scope.model.hasError = true; - $scope.model.errorClass = args.showValidation ? 'show-validation' : null; - } else { - $scope.model.hasError = false; - $scope.model.errorClass = null; + //TODO: Verify this is correct, does $scope.model ever exist? + if ($scope.model) { + if (!args.form.$valid) { + var subViewContent = $element.find(".ng-invalid"); + + if (subViewContent.length > 0) { + $scope.model.hasError = true; + $scope.model.errorClass = args.showValidation ? 'show-validation' : null; + } else { + $scope.model.hasError = false; + $scope.model.errorClass = null; + } + } + else { + $scope.model.hasError = false; + $scope.model.errorClass = null; + } + } + } } - } - else { - $scope.model.hasError = false; - $scope.model.errorClass = null; - } } - } + + function link(scope, el, attr, ctrl) { + + //if there are no containing form or valFormManager controllers, then we do nothing + if (!ctrl || !angular.isArray(ctrl) || ctrl.length !== 2 || !ctrl[0] || !ctrl[1]) { + return; + } + + var valFormManager = ctrl[1]; + scope.model.hasError = false; + + //listen for form validation changes + valFormManager.onValidationStatusChanged(function (evt, args) { + if (!args.form.$valid) { + + var subViewContent = el.find(".ng-invalid"); + + if (subViewContent.length > 0) { + scope.model.hasError = true; + } else { + scope.model.hasError = false; + } + + } + else { + scope.model.hasError = false; + } + }); + + } + + var directive = { + require: ['?^^form', '?^^valFormManager'], + restrict: "A", + link: link, + controller: controller + }; + + return directive; } - function link(scope, el, attr, ctrl) { - - //if there are no containing form or valFormManager controllers, then we do nothing - if (!ctrl || !angular.isArray(ctrl) || ctrl.length !== 2 || !ctrl[0] || !ctrl[1]) { - return; - } - - var valFormManager = ctrl[1]; - scope.model.hasError = false; - - //listen for form validation changes - valFormManager.onValidationStatusChanged(function (evt, args) { - if (!args.form.$valid) { - - var subViewContent = el.find(".ng-invalid"); - - if (subViewContent.length > 0) { - scope.model.hasError = true; - } else { - scope.model.hasError = false; - } - - } - else { - scope.model.hasError = false; - } - }); - - } - - var directive = { - require: ['?^^form', '?^^valFormManager'], - restrict: "A", - link: link, - controller: controller - }; - - return directive; - } - - angular.module('umbraco.directives').directive('valSubView', valSubViewDirective); + angular.module('umbraco.directives').directive('valSubView', valSubViewDirective); })(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valtab.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valtab.directive.js index 512b040ce3..b73aa0f29c 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valtab.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valtab.directive.js @@ -14,7 +14,7 @@ function valTab() { var valFormManager = ctrs[1]; var tabAlias = scope.tab.alias; - scope.tabHasError = false; + scope.tabHasError = false; //listen for form validation changes valFormManager.onValidationStatusChanged(function (evt, args) { @@ -31,7 +31,6 @@ function valTab() { scope.tabHasError = false; } }); - } }; } diff --git a/src/Umbraco.Web.UI.Client/src/common/interceptors/security.interceptor.js b/src/Umbraco.Web.UI.Client/src/common/interceptors/security.interceptor.js index b636a0a51c..d187714c62 100644 --- a/src/Umbraco.Web.UI.Client/src/common/interceptors/security.interceptor.js +++ b/src/Umbraco.Web.UI.Client/src/common/interceptors/security.interceptor.js @@ -83,10 +83,7 @@ if (rejection.data && rejection.data.ExceptionMessage) { errMsg += "
    with error:
    " + rejection.data.ExceptionMessage + ""; } - if (rejection.config.data) { - errMsg += "
    with data:
    " + angular.toJson(rejection.config.data) + "
    Contact your administrator for information."; - } - + notificationsService.error( "Request error", errMsg); @@ -102,11 +99,8 @@ //It was decided to just put these messages into the normal status messages. - var msg = "Unauthorized access to URL:
    " + rejection.config.url.split('?')[0] + ""; - if (rejection.config.data) { - msg += "
    with data:
    " + angular.toJson(rejection.config.data) + "
    Contact your administrator for information."; - } - + var msg = "Unauthorized access to URL:
    " + rejection.config.url.split('?')[0] + "
    Contact your administrator for information."; + notificationsService.error("Authorization error", msg); } diff --git a/src/Umbraco.Web.UI.Client/src/common/mocks/services/localization.mocks.js b/src/Umbraco.Web.UI.Client/src/common/mocks/services/localization.mocks.js index 28aae834b3..8b1faab448 100644 --- a/src/Umbraco.Web.UI.Client/src/common/mocks/services/localization.mocks.js +++ b/src/Umbraco.Web.UI.Client/src/common/mocks/services/localization.mocks.js @@ -36,7 +36,6 @@ angular.module('umbraco.mocks'). "actions_sendtopublish": "Send To Publish", "actions_sendToTranslate": "Send To Translation", "actions_sort": "Sort", - "actions_toPublish": "Send to publication", "actions_translate": "Translate", "actions_update": "Update", "actions_exportContourForm": "Export form", diff --git a/src/Umbraco.Web.UI.Client/src/common/mocks/umbraco.servervariables.js b/src/Umbraco.Web.UI.Client/src/common/mocks/umbraco.servervariables.js index 7ba14485d4..da6f78a6a5 100644 --- a/src/Umbraco.Web.UI.Client/src/common/mocks/umbraco.servervariables.js +++ b/src/Umbraco.Web.UI.Client/src/common/mocks/umbraco.servervariables.js @@ -13,8 +13,6 @@ Umbraco.Sys.ServerVariables = { "mediaTypeApiBaseUrl": "/umbraco/Api/MediaType/", "macroApiBaseUrl": "/umbraco/Api/Macro/", "authenticationApiBaseUrl": "/umbraco/UmbracoApi/Authentication/", - //For this we'll just provide a file that exists during the mock session since we don't really have legay js tree stuff - "legacyTreeJs": "/belle/lib/lazyload/empty.js", "serverVarsJs": "/belle/lib/lazyload/empty.js", "imagesApiBaseUrl": "/umbraco/UmbracoApi/Images/", "entityApiBaseUrl": "/umbraco/UmbracoApi/Entity/", @@ -39,4 +37,4 @@ Umbraco.Sys.ServerVariables = { assemblyVersion: "1", version: "7" } -}; \ No newline at end of file +}; diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/codefile.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/codefile.resource.js index 7cd55b268a..bb1dad1dbd 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/codefile.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/codefile.resource.js @@ -243,6 +243,77 @@ function codefileResource($q, $http, umbDataFormatter, umbRequestHelper) { "PostCreateContainer", { type: type, parentId: parentId, name: encodeURIComponent(name) })), 'Failed to create a folder under parent id ' + parentId); + }, + + /** + * @ngdoc method + * @name umbraco.resources.codefileResource#interpolateStylesheetRules + * @methodOf umbraco.resources.codefileResource + * + * @description + * Takes all rich text editor styling rules and turns them into css + * + * ##usage + *
    +         * codefileResource.interpolateStylesheetRules(".box{background:purple;}", "[{name: "heading", selector: "h1", styles: "color: red"}]")
    +         *    .then(function(data) {
    +         *        alert('its here!');
    +         *    });
    +         * 
    + * + * @param {string} content The style sheet content. + * @param {string} rules The rich text editor rules + * @returns {Promise} resourcePromise object. + * + */ + interpolateStylesheetRules: function (content, rules) { + var payload = { + content: content, + rules: rules + }; + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "codeFileApiBaseUrl", + "PostInterpolateStylesheetRules"), + payload), + "Failed to interpolate sheet rules"); + }, + + /** + * @ngdoc method + * @name umbraco.resources.codefileResource#extractStylesheetRules + * @methodOf umbraco.resources.codefileResource + * + * @description + * Find all rich text editor styles in the style sheets and turns them into "rules" + * + * ##usage + *
    +         * 
    +         * var conntent
    +         * codefileResource.extractStylesheetRules(".box{background:purple;}")
    +         *    .then(function(data) {
    +         *        alert('its here!');
    +         *    });
    +         * 
    + * + * @param {string} content The style sheet content. + * @returns {Promise} resourcePromise object. + * + */ + extractStylesheetRules: function(content) { + var payload = { + content: content, + rules: null + }; + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "codeFileApiBaseUrl", + "PostExtractStylesheetRules"), + payload), + "Failed to extract style sheet rules"); } }; diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js index 721cd4da57..06ef8748a0 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js @@ -607,7 +607,7 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { else if (options.orderDirection === "desc") { options.orderDirection = "Descending"; } - + //converts the value to a js bool function toBool(v) { if (angular.isNumber(v)) { @@ -729,6 +729,18 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { return saveContentItem(content, "publish" + (isNew ? "New" : ""), files, endpoint, showNotifications); }, + publishWithDescendants: function (content, isNew, force, files, showNotifications) { + var endpoint = umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "PostSave"); + + var action = "publishWithDescendants"; + if (force === true) { + action += "Force"; + } + + return saveContentItem(content, action + (isNew ? "New" : ""), files, endpoint, showNotifications); + }, /** * @ngdoc method @@ -756,11 +768,32 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { * @returns {Promise} resourcePromise object containing the saved content item. * */ - sendToPublish: function (content, isNew, files) { + sendToPublish: function (content, isNew, files, showNotifications) { var endpoint = umbRequestHelper.getApiUrl( "contentApiBaseUrl", "PostSave"); - return saveContentItem(content, "sendPublish" + (isNew ? "New" : ""), files, endpoint); + return saveContentItem(content, "sendPublish" + (isNew ? "New" : ""), files, endpoint, showNotifications); + }, + + /** + * @ngdoc method + * @name umbraco.resources.contentResource#saveSchedule + * @methodOf umbraco.resources.contentResource + * + * @description + * Saves changes made to a content item, and saves the publishing schedule + * + * @param {Object} content The content item object with changes applied + * @param {Bool} isNew set to true to create a new item or to update an existing + * @param {Array} files collection of files for the document + * @returns {Promise} resourcePromise object containing the saved content item. + * + */ + saveSchedule: function (content, isNew, files, showNotifications) { + var endpoint = umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "PostSave"); + return saveContentItem(content, "schedule" + (isNew ? "New" : ""), files, endpoint, showNotifications); }, /** @@ -808,8 +841,106 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { ), "Failed to create blueprint from content with id " + contentId ); - } + }, + /** + * @ngdoc method + * @name umbraco.resources.contentResource#getRollbackVersions + * @methodOf umbraco.resources.contentResource + * + * @description + * Returns an array of previous version id's, given a node id and a culture + * + * ##usage + *
    +          * contentResource.getRollbackVersions(id, culture)
    +          *    .then(function(versions) {
    +          *        alert('its here!');
    +          *    });
    +          * 
    + * + * @param {Int} id Id of node + * @param {Int} culture if provided, the results will be for this specific culture/variant + * @returns {Promise} resourcePromise object containing the versions + * + */ + getRollbackVersions: function (contentId, culture) { + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl("contentApiBaseUrl", "GetRollbackVersions", { + contentId: contentId, + culture: culture + }) + ), + "Failed to get rollback versions for content item with id " + contentId + ); + }, + + /** + * @ngdoc method + * @name umbraco.resources.contentResource#getRollbackVersion + * @methodOf umbraco.resources.contentResource + * + * @description + * Returns a previous version of a content item + * + * ##usage + *
    +          * contentResource.getRollbackVersion(versionId, culture)
    +          *    .then(function(version) {
    +          *        alert('its here!');
    +          *    });
    +          * 
    + * + * @param {Int} versionId The version Id + * @param {Int} culture if provided, the results will be for this specific culture/variant + * @returns {Promise} resourcePromise object containing the version + * + */ + getRollbackVersion: function (versionId, culture) { + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl("contentApiBaseUrl", "GetRollbackVersion", { + versionId: versionId, + culture: culture + }) + ), + "Failed to get version for content item with id " + versionId + ); + }, + + /** + * @ngdoc method + * @name umbraco.resources.contentResource#rollback + * @methodOf umbraco.resources.contentResource + * + * @description + * Roll backs a content item to a previous version + * + * ##usage + *
    +          * contentResource.rollback(contentId, versionId, culture)
    +          *    .then(function() {
    +          *        alert('its here!');
    +          *    });
    +          * 
    + * + * @param {Int} id Id of node + * @param {Int} versionId The version Id + * @param {Int} culture if provided, the results will be for this specific culture/variant + * @returns {Promise} resourcePromise object + * + */ + rollback: function (contentId, versionId, culture) { + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl("contentApiBaseUrl", "PostRollbackContent", { + contentId: contentId, versionId:versionId, culture:culture + }) + ), + "Failed to roll back content item with id " + contentId + ); + } }; } diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/media.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/media.resource.js index 5d2ac1e8b9..1d6d5171a1 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/media.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/media.resource.js @@ -109,11 +109,34 @@ function mediaResource($q, $http, umbDataFormatter, umbRequestHelper) { return umbRequestHelper.resourcePromise( $http.post(umbRequestHelper.getApiUrl("mediaApiBaseUrl", "PostMove"), - { - parentId: args.parentId, - id: args.id + { + parentId: args.parentId, + id: args.id }, {responseType: 'text'}), - 'Failed to move media'); + { + error: function(data){ + var errorMsg = 'Failed to move media'; + if (data.id !== undefined && data.parentId !== undefined) { + if (data.id === data.parentId) { + errorMsg = 'Media can\'t be moved into itself'; + } + } + else if (data.notifications !== undefined) { + if (data.notifications.length > 0) { + if (data.notifications[0].header.length > 0) { + errorMsg = data.notifications[0].header; + } + if (data.notifications[0].message.length > 0) { + errorMsg = errorMsg + ": " + data.notifications[0].message; + } + } + } + + return { + errorMsg: errorMsg + }; + } + }); }, diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/mediatype.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/mediatype.resource.js index 8dc64e4cac..d194ae2c73 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/mediatype.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/mediatype.resource.js @@ -3,7 +3,7 @@ * @name umbraco.resources.mediaTypeResource * @description Loads in data for media types **/ -function mediaTypeResource($q, $http, umbRequestHelper, umbDataFormatter) { +function mediaTypeResource($q, $http, umbRequestHelper, umbDataFormatter, localizationService) { return { @@ -208,13 +208,15 @@ function mediaTypeResource($q, $http, umbRequestHelper, umbDataFormatter) { throw "args.id cannot be null"; } + var promise = localizationService.localize("media_moveFailed"); + return umbRequestHelper.resourcePromise( $http.post(umbRequestHelper.getApiUrl("mediaTypeApiBaseUrl", "PostMove"), { parentId: args.parentId, id: args.id }, { responseType: 'text' }), - 'Failed to move content'); + promise); }, copy: function (args) { @@ -228,33 +230,39 @@ function mediaTypeResource($q, $http, umbRequestHelper, umbDataFormatter) { throw "args.id cannot be null"; } + var promise = localizationService.localize("media_copyFailed"); + return umbRequestHelper.resourcePromise( $http.post(umbRequestHelper.getApiUrl("mediaTypeApiBaseUrl", "PostCopy"), { parentId: args.parentId, id: args.id }, { responseType: 'text' }), - 'Failed to copy content'); + promise); }, createContainer: function(parentId, name) { + var promise = localizationService.localize("media_createFolderFailed", [parentId]); + return umbRequestHelper.resourcePromise( $http.post( umbRequestHelper.getApiUrl( "mediaTypeApiBaseUrl", "PostCreateContainer", { parentId: parentId, name: encodeURIComponent(name) })), - 'Failed to create a folder under parent id ' + parentId); + promise); }, renameContainer: function (id, name) { + var promise = localizationService.localize("media_renameFolderFailed", [id]); + return umbRequestHelper.resourcePromise( $http.post(umbRequestHelper.getApiUrl("mediaTypeApiBaseUrl", "PostRenameContainer", { id: id, name: name })), - "Failed to rename the folder with id " + id + promise ); } diff --git a/src/Umbraco.Web.UI.Client/src/common/services/appstate.service.js b/src/Umbraco.Web.UI.Client/src/common/services/appstate.service.js index d1ac3d39cf..de53f7ecea 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/appstate.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/appstate.service.js @@ -70,6 +70,8 @@ function appState(eventsService) { currentNode: null, //Whether the menu's dialog is being shown or not showMenuDialog: null, + // The dialogs template + dialogTemplateUrl: null, //Whether the context menu is being shown or not showMenu: null }; diff --git a/src/Umbraco.Web.UI.Client/src/common/services/assets.service.js b/src/Umbraco.Web.UI.Client/src/common/services/assets.service.js index a331af899b..e7f40f4814 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/assets.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/assets.service.js @@ -141,10 +141,6 @@ angular.module('umbraco.services') var self = this; return self.loadJs(umbRequestHelper.getApiUrl("serverVarsJs", "", ""), $rootScope).then(function () { initAssetsLoaded = true; - - //now we need to go get the legacyTreeJs - but this can be done async without waiting. - self.loadJs(umbRequestHelper.getApiUrl("legacyTreeJs", "", ""), $rootScope); - return loadMomentLocaleForCurrentUser(); }); } diff --git a/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js index 777b336447..47e5a24180 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js @@ -5,7 +5,7 @@ * @description A helper service for most editors, some methods are specific to content/media/member model types but most are used by * all editors to share logic and reduce the amount of replicated code among editors. **/ -function contentEditingHelper(fileManager, $q, $location, $routeParams, notificationsService, navigationService, localizationService, serverValidationManager, dialogService, formHelper, appState) { +function contentEditingHelper(fileManager, $q, $location, $routeParams, notificationsService, navigationService, localizationService, serverValidationManager, formHelper) { function isValidIdentifier(id) { @@ -146,7 +146,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica if (!args.methods) { throw "args.methods is not defined"; } - if (!args.methods.saveAndPublish || !args.methods.sendToPublish || !args.methods.unpublish || !args.methods.schedulePublish) { + if (!args.methods.saveAndPublish || !args.methods.sendToPublish || !args.methods.unpublish || !args.methods.schedulePublish || !args.methods.publishDescendants) { throw "args.methods does not contain all required defined methods"; } @@ -188,7 +188,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica hotKey: "ctrl+u", hotKeyWhenHidden: true, alias: "unpublish", - addEllipsis: args.content.variants && args.content.variants.length > 1 ? "true" : "false" + addEllipsis: "true" }; case "SCHEDULE": //schedule publish - schedule doesn't have a permission letter so @@ -200,6 +200,16 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica alias: "schedulePublish", addEllipsis: "true" }; + case "PUBLISH_DESCENDANTS": + // Publish descendants - it doesn't have a permission letter so + // the button letter is made unique so it doesn't collide with anything else + return { + letter: ch, + labelKey: "buttons_publishDescendants", + handler: args.methods.publishDescendants, + alias: "publishDescendant", + addEllipsis: "true" + }; default: return null; } @@ -210,7 +220,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica //This is the ideal button order but depends on circumstance, we'll use this array to create the button list // Publish, SendToPublish - var buttonOrder = ["U", "H", "SCHEDULE"]; + var buttonOrder = ["U", "H", "SCHEDULE", "PUBLISH_DESCENDANTS"]; //Create the first button (primary button) //We cannot have the Save or SaveAndPublish buttons if they don't have create permissions when we are creating a new item. @@ -253,6 +263,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica // get picked up by the loop through permissions if( _.contains(args.content.allowedActions, "U")) { buttons.subButtons.push(createButtonDefinition("SCHEDULE")); + buttons.subButtons.push(createButtonDefinition("PUBLISH_DESCENDANTS")); } // if we are not creating, then we should add unpublish too, @@ -421,16 +432,8 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica "properties", "apps", "createDateFormatted", - "releaseDateYear", - "releaseDateMonth", - "releaseDateDayNumber", - "releaseDateDay", - "releaseDateTime", - "removeDateYear", - "removeDateMonth", - "removeDateDayNumber", - "removeDateDay", - "removeDateTime" + "releaseDate", + "expireDate" ], function (i) { return i === propName; }); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/dialog.service.js b/src/Umbraco.Web.UI.Client/src/common/services/dialog.service.js deleted file mode 100644 index ccf198bcbf..0000000000 --- a/src/Umbraco.Web.UI.Client/src/common/services/dialog.service.js +++ /dev/null @@ -1,539 +0,0 @@ -/** - * @ngdoc service - * @name umbraco.services.dialogService - * - * @requires $rootScope - * @requires $compile - * @requires $http - * @requires $log - * @requires $q - * @requires $templateCache - * - * @description - * Application-wide service for handling modals, overlays and dialogs - * By default it injects the passed template url into a div to body of the document - * And renders it, but does also support rendering items in an iframe, incase - * serverside processing is needed, or its a non-angular page - * - * ##usage - * To use, simply inject the dialogService into any controller that needs it, and make - * sure the umbraco.services module is accesible - which it should be by default. - * - *
    - *    var dialog = dialogService.open({template: 'path/to/page.html', show: true, callback: done});
    - *    functon done(data){
    - *      //The dialog has been submitted
    - *      //data contains whatever the dialog has selected / attached
    - *    }
    - * 
    - */ - -angular.module('umbraco.services') -.factory('dialogService', function ($rootScope, $compile, $http, $timeout, $q, $templateCache, appState, eventsService) { - - var dialogs = []; - - /** Internal method that removes all dialogs */ - function removeAllDialogs(args) { - for (var i = 0; i < dialogs.length; i++) { - var dialog = dialogs[i]; - - //very special flag which means that global events cannot close this dialog - currently only used on the login - // dialog since it's special and cannot be closed without logging in. - if (!dialog.manualClose) { - dialog.close(args); - } - - } - } - - /** Internal method that closes the dialog properly and cleans up resources */ - function closeDialog(dialog) { - - if (dialog.element) { - dialog.element.modal('hide'); - - //this is not entirely enough since the damn webforms scriploader still complains - if (dialog.iframe) { - dialog.element.find("iframe").attr("src", "about:blank"); - } - - dialog.scope.$destroy(); - - //we need to do more than just remove the element, this will not destroy the - // scope in angular 1.1x, in angular 1.2x this is taken care of but if we dont - // take care of this ourselves we have memory leaks. - dialog.element.remove(); - - //remove 'this' dialog from the dialogs array - dialogs = _.reject(dialogs, function (i) { return i === dialog; }); - } - } - - /** Internal method that handles opening all dialogs */ - function openDialog(options) { - var defaults = { - container: $("body"), - animation: "fade", - modalClass: "umb-modal", - width: "100%", - inline: false, - iframe: false, - show: true, - template: "views/common/notfound.html", - callback: undefined, - closeCallback: undefined, - element: undefined, - // It will set this value as a property on the dialog controller's scope as dialogData, - // used to pass in custom data to the dialog controller's $scope. Though this is near identical to - // the dialogOptions property that is also set the the dialog controller's $scope object. - // So there's basically 2 ways of doing the same thing which we're now stuck with and in fact - // dialogData has another specially attached property called .selection which gets used. - dialogData: undefined - }; - - var dialog = angular.extend(defaults, options); - - //NOTE: People should NOT pass in a scope object that is legacy functoinality and causes problems. We will ALWAYS - // destroy the scope when the dialog is closed regardless if it is in use elsewhere which is why it shouldn't be done. - var scope = options.scope || $rootScope.$new(); - - //Modal dom obj and set id to old-dialog-service - used until we get all dialogs moved the the new overlay directive - dialog.element = $('
    '); - var id = "old-dialog-service"; - - if (options.inline) { - dialog.animation = ""; - } - else { - dialog.element.addClass("modal"); - dialog.element.addClass("hide"); - } - - //set the id and add classes - dialog.element - .attr('id', id) - .addClass(dialog.animation) - .addClass(dialog.modalClass); - - //push the modal into the global modal collection - //we halt the .push because a link click will trigger a closeAll right away - $timeout(function () { - dialogs.push(dialog); - }, 500); - - - dialog.close = function (data) { - if (dialog.closeCallback) { - dialog.closeCallback(data); - } - - closeDialog(dialog); - }; - - //if iframe is enabled, inject that instead of a template - if (dialog.iframe) { - var html = $(""); - dialog.element.html(html); - - //append to body or whatever element is passed in as options.containerElement - dialog.container.append(dialog.element); - - // Compile modal content - $timeout(function () { - $compile(dialog.element)(dialog.scope); - }); - - dialog.element.css("width", dialog.width); - - //Autoshow - if (dialog.show) { - dialog.element.modal('show'); - } - - dialog.scope = scope; - return dialog; - } - else { - - //We need to load the template with an httpget and once it's loaded we'll compile and assign the result to the container - // object. However since the result could be a promise or just data we need to use a $q.when. We still need to return the - // $modal object so we'll actually return the modal object synchronously without waiting for the promise. Otherwise this openDialog - // method will always need to return a promise which gets nasty because of promises in promises plus the result just needs a reference - // to the $modal object which will not change (only it's contents will change). - $q.when($templateCache.get(dialog.template) || $http.get(dialog.template, { cache: true }).then(function (res) { return res.data; })) - .then(function onSuccess(template) { - - // Build modal object - dialog.element.html(template); - - //append to body or other container element - dialog.container.append(dialog.element); - - // Compile modal content - $timeout(function () { - $compile(dialog.element)(scope); - }); - - scope.dialogOptions = dialog; - - //Scope to handle data from the modal form - scope.dialogData = dialog.dialogData ? dialog.dialogData : {}; - scope.dialogData.selection = []; - - // Provide scope display functions - //this passes the modal to the current scope - scope.$modal = function (name) { - dialog.element.modal(name); - }; - - scope.swipeHide = function (e) { - - if (appState.getGlobalState("touchDevice")) { - var selection = window.getSelection(); - if (selection.type !== "Range") { - scope.hide(); - } - } - }; - - //NOTE: Same as 'close' without the callbacks - scope.hide = function () { - closeDialog(dialog); - }; - - //basic events for submitting and closing - scope.submit = function (data) { - if (dialog.callback) { - dialog.callback(data); - } - - closeDialog(dialog); - }; - - scope.close = function (data) { - dialog.close(data); - }; - - //NOTE: This can ONLY ever be used to show the dialog if dialog.show is false (autoshow). - // You CANNOT call show() after you call hide(). hide = close, they are the same thing and once - // a dialog is closed it's resources are disposed of. - scope.show = function () { - if (dialog.manualClose === true) { - //show and configure that the keyboard events are not enabled on this modal - dialog.element.modal({ keyboard: false }); - } - else { - //just show normally - dialog.element.modal('show'); - } - - }; - - scope.select = function (item) { - var i = scope.dialogData.selection.indexOf(item); - if (i < 0) { - scope.dialogData.selection.push(item); - } else { - scope.dialogData.selection.splice(i, 1); - } - }; - - //NOTE: Same as 'close' without the callbacks - scope.dismiss = scope.hide; - - // Emit modal events - angular.forEach(['show', 'shown', 'hide', 'hidden'], function (name) { - dialog.element.on(name, function (ev) { - scope.$emit('modal-' + name, ev); - }); - }); - - // Support autofocus attribute - dialog.element.on('shown', function (event) { - $('input[autofocus]', dialog.element).first().trigger('focus'); - }); - - dialog.scope = scope; - - //Autoshow - if (dialog.show) { - scope.show(); - } - - }); - - //Return the modal object outside of the promise! - return dialog; - } - } - - /** Handles the closeDialogs event */ - eventsService.on("app.closeDialogs", function (evt, args) { - removeAllDialogs(args); - }); - - return { - /** - * @ngdoc method - * @name umbraco.services.dialogService#open - * @methodOf umbraco.services.dialogService - * - * @description - * Opens a modal rendering a given template url. - * - * @param {Object} options rendering options - * @param {DomElement} options.container the DOM element to inject the modal into, by default set to body - * @param {Function} options.callback function called when the modal is submitted - * @param {String} options.template the url of the template - * @param {String} options.animation animation csss class, by default set to "fade" - * @param {String} options.modalClass modal css class, by default "umb-modal" - * @param {Bool} options.show show the modal instantly - * @param {Bool} options.iframe load template in an iframe, only needed for serverside templates - * @param {Int} options.width set a width on the modal, only needed for iframes - * @param {Bool} options.inline strips the modal from any animation and wrappers, used when you want to inject a dialog into an existing container - * @returns {Object} modal object - */ - open: function (options) { - return openDialog(options); - }, - - /** - * @ngdoc method - * @name umbraco.services.dialogService#close - * @methodOf umbraco.services.dialogService - * - * @description - * Closes a specific dialog - * @param {Object} dialog the dialog object to close - * @param {Object} args if specified this object will be sent to any callbacks registered on the dialogs. - */ - close: function (dialog, args) { - if (dialog) { - dialog.close(args); - } - }, - - /** - * @ngdoc method - * @name umbraco.services.dialogService#closeAll - * @methodOf umbraco.services.dialogService - * - * @description - * Closes all dialogs - * @param {Object} args if specified this object will be sent to any callbacks registered on the dialogs. - */ - closeAll: function (args) { - removeAllDialogs(args); - }, - - /** - * @ngdoc method - * @name umbraco.services.dialogService#mediaPicker - * @methodOf umbraco.services.dialogService - * - * @description - * Opens a media picker in a modal, the callback returns an array of selected media items - * @param {Object} options mediapicker dialog options object - * @param {Boolean} options.onlyImages Only display files that have an image file-extension - * @param {Function} options.callback callback function - * @returns {Object} modal object - */ - mediaPicker: function (options) { - options.template = 'views/common/dialogs/mediaPicker.html'; - options.show = true; - return openDialog(options); - }, - - - /** - * @ngdoc method - * @name umbraco.services.dialogService#contentPicker - * @methodOf umbraco.services.dialogService - * - * @description - * Opens a content picker tree in a modal, the callback returns an array of selected documents - * @param {Object} options content picker dialog options object - * @param {Boolean} options.multiPicker should the picker return one or multiple items - * @param {Function} options.callback callback function - * @returns {Object} modal object - */ - contentPicker: function (options) { - - options.treeAlias = "content"; - options.section = "content"; - - return this.treePicker(options); - }, - - /** - * @ngdoc method - * @name umbraco.services.dialogService#linkPicker - * @methodOf umbraco.services.dialogService - * - * @description - * Opens a link picker tree in a modal, the callback returns a single link - * @param {Object} options content picker dialog options object - * @param {Function} options.callback callback function - * @returns {Object} modal object - */ - linkPicker: function (options) { - options.template = 'views/common/dialogs/linkPicker.html'; - options.show = true; - return openDialog(options); - }, - - /** - * @ngdoc method - * @name umbraco.services.dialogService#macroPicker - * @methodOf umbraco.services.dialogService - * - * @description - * Opens a mcaro picker in a modal, the callback returns a object representing the macro and it's parameters - * @param {Object} options macropicker dialog options object - * @param {Function} options.callback callback function - * @returns {Object} modal object - */ - macroPicker: function (options) { - options.template = 'views/common/dialogs/insertmacro.html'; - options.show = true; - options.modalClass = "span7 umb-modal"; - return openDialog(options); - }, - - /** - * @ngdoc method - * @name umbraco.services.dialogService#memberPicker - * @methodOf umbraco.services.dialogService - * - * @description - * Opens a member picker in a modal, the callback returns a object representing the selected member - * @param {Object} options member picker dialog options object - * @param {Boolean} options.multiPicker should the tree pick one or multiple members before returning - * @param {Function} options.callback callback function - * @returns {Object} modal object - */ - memberPicker: function (options) { - - options.treeAlias = "member"; - options.section = "member"; - - return this.treePicker(options); - }, - - /** - * @ngdoc method - * @name umbraco.services.dialogService#memberGroupPicker - * @methodOf umbraco.services.dialogService - * - * @description - * Opens a member group picker in a modal, the callback returns a object representing the selected member - * @param {Object} options member group picker dialog options object - * @param {Boolean} options.multiPicker should the tree pick one or multiple members before returning - * @param {Function} options.callback callback function - * @returns {Object} modal object - */ - memberGroupPicker: function (options) { - options.template = 'views/common/dialogs/memberGroupPicker.html'; - options.show = true; - return openDialog(options); - }, - - /** - * @ngdoc method - * @name umbraco.services.dialogService#iconPicker - * @methodOf umbraco.services.dialogService - * - * @description - * Opens a icon picker in a modal, the callback returns a object representing the selected icon - * @param {Object} options iconpicker dialog options object - * @param {Function} options.callback callback function - * @returns {Object} modal object - */ - iconPicker: function (options) { - options.template = 'views/common/dialogs/iconPicker.html'; - options.show = true; - return openDialog(options); - }, - - /** - * @ngdoc method - * @name umbraco.services.dialogService#treePicker - * @methodOf umbraco.services.dialogService - * - * @description - * Opens a tree picker in a modal, the callback returns a object representing the selected tree item - * @param {Object} options iconpicker dialog options object - * @param {String} options.section tree section to display - * @param {String} options.treeAlias specific tree to display - * @param {Boolean} options.multiPicker should the tree pick one or multiple items before returning - * @param {Function} options.callback callback function - * @returns {Object} modal object - */ - treePicker: function (options) { - options.template = 'views/common/dialogs/treePicker.html'; - options.show = true; - return openDialog(options); - }, - - /** - * @ngdoc method - * @name umbraco.services.dialogService#propertyDialog - * @methodOf umbraco.services.dialogService - * - * @description - * Opens a dialog with a chosen property editor in, a value can be passed to the modal, and this value is returned in the callback - * @param {Object} options mediapicker dialog options object - * @param {Function} options.callback callback function - * @param {String} editor editor to use to edit a given value and return on callback - * @param {Object} value value sent to the property editor - * @returns {Object} modal object - */ - //TODO: Wtf does this do? I don't think anything! - propertyDialog: function (options) { - options.template = 'views/common/dialogs/property.html'; - options.show = true; - return openDialog(options); - }, - - /** - * @ngdoc method - * @name umbraco.services.dialogService#embedDialog - * @methodOf umbraco.services.dialogService - * @description - * Opens a dialog to an embed dialog - */ - embedDialog: function (options) { - options.template = 'views/common/dialogs/rteembed.html'; - options.show = true; - return openDialog(options); - }, - /** - * @ngdoc method - * @name umbraco.services.dialogService#ysodDialog - * @methodOf umbraco.services.dialogService - * - * @description - * Opens a dialog to show a custom YSOD - */ - ysodDialog: function (ysodError) { - - var newScope = $rootScope.$new(); - newScope.error = ysodError; - return openDialog({ - modalClass: "umb-modal wide ysod", - scope: newScope, - //callback: options.callback, - template: 'views/common/dialogs/ysod.html', - show: true - }); - }, - - confirmDialog: function (ysodError) { - - options.template = 'views/common/dialogs/confirm.html'; - options.show = true; - return openDialog(options); - } - }; -}); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js b/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js index eab167c2ec..449470f54c 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js @@ -24,6 +24,18 @@ return editors; }; + /** + * @ngdoc method + * @name umbraco.services.editorService#getNumberOfEditors + * @methodOf umbraco.services.editorService + * + * @description + * Method to return the number of open editors + */ + function getNumberOfEditors() { + return editors.length; + }; + /** * @ngdoc method * @name umbraco.services.editorService#open @@ -180,6 +192,25 @@ open(editor); } + /** + * @ngdoc method + * @name umbraco.services.editorService#rollback + * @methodOf umbraco.services.editorService + * + * @description + * Opens a rollback editor in infinite editing. + * @param {String} editor.node The node to rollback + * @param {Callback} editor.submit Saves, submits, and closes the editor + * @param {Callback} editor.close Closes the editor + * @returns {Object} editor object + */ + + function rollback(editor) { + editor.view = "views/common/infiniteeditors/rollback/rollback.html"; + editor.size = "small"; + open(editor); + } + /** * @ngdoc method * @name umbraco.services.editorService#linkPicker @@ -472,6 +503,7 @@ var service = { getEditors: getEditors, + getNumberOfEditors: getNumberOfEditors, open: open, close: close, closeAll: closeAll, @@ -481,6 +513,7 @@ copy: copy, move: move, embed: embed, + rollback: rollback, linkPicker: linkPicker, mediaPicker: mediaPicker, iconPicker: iconPicker, diff --git a/src/Umbraco.Web.UI.Client/src/common/services/events.service.js b/src/Umbraco.Web.UI.Client/src/common/services/events.service.js index 5f45a2d3e5..6bab8fda81 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/events.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/events.service.js @@ -6,7 +6,6 @@ app.ready app.authenticated app.notAuthenticated - app.closeDialogs app.ysod app.reInitialize app.userRefresh diff --git a/src/Umbraco.Web.UI.Client/src/common/services/formhelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/formhelper.service.js index c019258a02..f3b64e0c28 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/formhelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/formhelper.service.js @@ -7,7 +7,7 @@ * A utility class used to streamline how forms are developed, to ensure that validation is check and displayed consistently and to ensure that the correct events * fire when they need to. */ -function formHelper(angularHelper, serverValidationManager, $timeout, notificationsService, dialogService) { +function formHelper(angularHelper, serverValidationManager, notificationsService, overlayService) { return { /** @@ -119,7 +119,7 @@ function formHelper(angularHelper, serverValidationManager, $timeout, notificati serverValidationManager.notifyAndClearAllSubscriptions(); } else { - dialogService.ysodDialog(err); + overlayService.ysod(err); } } diff --git a/src/Umbraco.Web.UI.Client/src/common/services/navigation.service.js b/src/Umbraco.Web.UI.Client/src/common/services/navigation.service.js index cb416e3974..0ecfa375d4 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/navigation.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/navigation.service.js @@ -4,9 +4,7 @@ * * @requires $rootScope * @requires $routeParams - * @requires $log * @requires $location - * @requires dialogService * @requires treeService * @requires sectionResource * @@ -15,7 +13,7 @@ * Section navigation and search, and maintain their state for the entire application lifetime * */ -function navigationService($rootScope, $route, $routeParams, $log, $location, $q, $timeout, $injector, urlHelper, eventsService, dialogService, umbModelMapper, treeService, notificationsService, historyService, appState, angularHelper) { +function navigationService($routeParams, $location, $q, $timeout, $injector, eventsService, umbModelMapper, treeService, appState) { //the promise that will be resolved when the navigation is ready var navReadyPromise = $q.defer(); @@ -313,7 +311,6 @@ function navigationService($rootScope, $route, $routeParams, $log, $location, $q * @param {String} args.tree the tree alias to sync to * @param {Array} args.path the path to sync the tree to * @param {Boolean} args.forceReload optional, specifies whether to force reload the node data from the server even if it already exists in the tree currently - * @param {Boolean} args.activate optional, specifies whether to set the synced node to be the active node, this will default to true if not specified */ syncTree: function (args) { if (!args) { @@ -334,6 +331,8 @@ function navigationService($rootScope, $route, $routeParams, $log, $location, $q /** Internal method that should ONLY be used by the legacy API wrapper, the legacy API used to have to set an active tree and then sync, the new API does this in one method by using syncTree + + TODO: Delete this if not required */ _syncPath: function(path, forceReload) { return navReadyPromise.promise.then(function () { @@ -405,19 +404,13 @@ function navigationService($rootScope, $route, $routeParams, $log, $location, $q //NOTE: This is assigning the current action node - this is not the same as the currently selected node! appState.setMenuState("currentNode", args.node); - //ensure the current dialog is cleared before creating another! - if (currentDialog) { - dialogService.close(currentDialog); - } - - var dialog = self.showDialog({ + self.showDialog({ node: args.node, action: found, section: appState.getSectionState("currentSection") }); - //return the dialog this is opening. - return $q.resolve(dialog); + return $q.resolve(); } } @@ -429,8 +422,7 @@ function navigationService($rootScope, $route, $routeParams, $log, $location, $q appState.setMenuState("menuActions", data.menuItems); appState.setMenuState("dialogTitle", args.node.name); - //we're not opening a dialog, return null. - return $q.resolve(null); + return $q.resolve(); }); }, @@ -478,7 +470,7 @@ function navigationService($rootScope, $route, $routeParams, $log, $location, $q //if it is not two parts long then this most likely means that it's a legacy action var js = action.metaData["jsAction"].replace("javascript:", ""); - //there's not really a different way to acheive this except for eval + //there's not really a different way to achieve this except for eval eval(js); } else { @@ -521,14 +513,13 @@ function navigationService($rootScope, $route, $routeParams, $log, $location, $q * * @description * Opens a dialog, for a given action on a given tree node - * uses the dialogService to inject the selected action dialog - * into #dialog div.umb-panel-body * the path to the dialog view is determined by: * "views/" + current tree + "/" + action alias + ".html" * The dialog controller will get passed a scope object that is created here with the properties: - * scope.currentNode = the selected tree node - * scope.currentAction = the selected menu item - * so that the dialog controllers can use these properties + * scope.currentNode = the selected tree node + * scope.title = the title of the menu item + * scope.view = the path to the view html file + * so that the dialog controllers can use these properties * * @param {Object} args arguments passed to the function * @param {Scope} args.scope current scope passed to the dialog @@ -546,22 +537,6 @@ function navigationService($rootScope, $route, $routeParams, $log, $location, $q throw "The args parameter must have a 'node' as the active tree node"; } - //ensure the current dialog is cleared before creating another! - if (currentDialog) { - dialogService.close(currentDialog); - currentDialog = null; - } - - setMode("dialog"); - - //NOTE: Set up the scope object and assign properties, this is legacy functionality but we have to live with it now. - // we should be passing in currentNode and currentAction using 'dialogData' for the dialog, not attaching it to a scope. - // This scope instance will be destroyed by the dialog so it cannot be a scope that exists outside of the dialog. - // If a scope instance has been passed in, we'll have to create a child scope of it, otherwise a new root scope. - var dialogScope = args.scope ? args.scope.$new() : $rootScope.$new(); - dialogScope.currentNode = args.node; - dialogScope.currentAction = args.action; - //the title might be in the meta data, check there first if (args.action.metaData["dialogTitle"]) { appState.setMenuState("dialogTitle", args.action.metaData["dialogTitle"]); @@ -571,15 +546,9 @@ function navigationService($rootScope, $route, $routeParams, $log, $location, $q } var templateUrl; - var iframe; - if (args.action.metaData["actionUrl"]) { - templateUrl = args.action.metaData["actionUrl"]; - iframe = true; - } - else if (args.action.metaData["actionView"]) { + if (args.action.metaData["actionView"]) { templateUrl = args.action.metaData["actionView"]; - iframe = false; } else { @@ -605,35 +574,14 @@ function navigationService($rootScope, $route, $routeParams, $log, $location, $q templateUrl = "views/" + treeAlias + "/" + args.action.alias + ".html"; } - iframe = false; } - //TODO: some action's want to launch a new window like live editing, we support this in the menu item's metadata with - // a key called: "actionUrlMethod" which can be set to either: Dialog, BlankWindow. Normally this is always set to Dialog - // if a URL is specified in the "actionUrl" metadata. For now I'm not going to implement launching in a blank window, - // though would be v-easy, just not sure we want to ever support that? + setMode("dialog"); - var dialog = dialogService.open( - { - container: $("#dialog div.umb-modalcolumn-body"), - //The ONLY reason we're passing in scope to the dialogService (which is legacy functionality) is - // for backwards compatibility since many dialogs require $scope.currentNode or $scope.currentAction - // to exist - scope: dialogScope, - inline: true, - show: true, - iframe: iframe, - modalClass: "umb-dialog", - template: templateUrl, - - //These will show up on the dialog controller's $scope under dialogOptions - currentNode: args.node, - currentAction: args.action - }); - - //save the currently assigned dialog so it can be removed before a new one is created - currentDialog = dialog; - return dialog; + if(templateUrl) { + appState.setMenuState("dialogTemplateUrl", templateUrl); + } + }, /** @@ -646,10 +594,10 @@ function navigationService($rootScope, $route, $routeParams, $log, $location, $q */ hideDialog: function (showMenu) { - setMode("default"); - - if(showMenu){ + if (showMenu) { this.showMenu({ skipDefault: true, node: appState.getMenuState("currentNode") }); + } else { + setMode("default"); } }, /** diff --git a/src/Umbraco.Web.UI.Client/src/common/services/overlay.service.js b/src/Umbraco.Web.UI.Client/src/common/services/overlay.service.js index 16d51add92..6c50e58490 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/overlay.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/overlay.service.js @@ -16,7 +16,7 @@ // prevent two open overlays at the same time if(currentOverlay) { - return; + close(); } var backdropOptions = {}; @@ -44,9 +44,21 @@ eventsService.emit("appState.overlay", null); } + function ysod(error) { + const overlay = { + view: "views/common/overlays/ysod/ysod.html", + error: error, + close: function() { + close(); + } + }; + open(overlay); + } + var service = { open: open, - close: close + close: close, + ysod: ysod }; return service; diff --git a/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js b/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js index 11e54c0fdb..43022d0e86 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js @@ -116,7 +116,6 @@ function serverValidationManager($timeout) { * @name subscribe * @methodOf umbraco.services.serverValidationManager * @function - * @returns {} a method to unsubscribe this callback * @description * Adds a callback method that is executed whenever validation changes for the field name + property specified. * This is generally used for server side validation in order to match up a server side validation error with diff --git a/src/Umbraco.Web.UI.Client/src/common/services/tabbable.service.js b/src/Umbraco.Web.UI.Client/src/common/services/tabbable.service.js new file mode 100644 index 0000000000..4d8d5f68f3 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/services/tabbable.service.js @@ -0,0 +1,223 @@ +//tabbable JS Lib (Wrapped in angular service) +//https://github.com/davidtheclark/tabbable + +(function() { + 'use strict'; + + function tabbableService() { + + var candidateSelectors = [ + 'input', + 'select', + 'textarea', + 'a[href]', + 'button', + '[tabindex]', + 'audio[controls]', + 'video[controls]', + '[contenteditable]:not([contenteditable="false"])' + ]; + var candidateSelector = candidateSelectors.join(','); + + var matches = typeof Element === 'undefined' + ? function () {} + : Element.prototype.matches || Element.prototype.msMatchesSelector || Element.prototype.webkitMatchesSelector; + + function tabbable(el, options) { + options = options || {}; + + var elementDocument = el.ownerDocument || el; + var regularTabbables = []; + var orderedTabbables = []; + + var untouchabilityChecker = new UntouchabilityChecker(elementDocument); + var candidates = el.querySelectorAll(candidateSelector); + + if (options.includeContainer) { + if (matches.call(el, candidateSelector)) { + candidates = Array.prototype.slice.apply(candidates); + candidates.unshift(el); + } + } + + var i, candidate, candidateTabindex; + for (i = 0; i < candidates.length; i++) { + candidate = candidates[i]; + + if (!isNodeMatchingSelectorTabbable(candidate, untouchabilityChecker)) continue; + + candidateTabindex = getTabindex(candidate); + if (candidateTabindex === 0) { + regularTabbables.push(candidate); + } else { + orderedTabbables.push({ + documentOrder: i, + tabIndex: candidateTabindex, + node: candidate + }); + } + } + + var tabbableNodes = orderedTabbables + .sort(sortOrderedTabbables) + .map(function(a) { return a.node }) + .concat(regularTabbables); + + return tabbableNodes; + } + + tabbable.isTabbable = isTabbable; + tabbable.isFocusable = isFocusable; + + function isNodeMatchingSelectorTabbable(node, untouchabilityChecker) { + if ( + !isNodeMatchingSelectorFocusable(node, untouchabilityChecker) + || isNonTabbableRadio(node) + || getTabindex(node) < 0 + ) { + return false; + } + return true; + } + + function isTabbable(node, untouchabilityChecker) { + if (!node) throw new Error('No node provided'); + if (matches.call(node, candidateSelector) === false) return false; + return isNodeMatchingSelectorTabbable(node, untouchabilityChecker); + } + + function isNodeMatchingSelectorFocusable(node, untouchabilityChecker) { + untouchabilityChecker = untouchabilityChecker || new UntouchabilityChecker(node.ownerDocument || node); + if ( + node.disabled + || isHiddenInput(node) + || untouchabilityChecker.isUntouchable(node) + ) { + return false; + } + return true; + } + + var focusableCandidateSelector = candidateSelectors.concat('iframe').join(','); + function isFocusable(node, untouchabilityChecker) { + if (!node) throw new Error('No node provided'); + if (matches.call(node, focusableCandidateSelector) === false) return false; + return isNodeMatchingSelectorFocusable(node, untouchabilityChecker); + } + + function getTabindex(node) { + var tabindexAttr = parseInt(node.getAttribute('tabindex'), 10); + if (!isNaN(tabindexAttr)) return tabindexAttr; + // Browsers do not return `tabIndex` correctly for contentEditable nodes; + // so if they don't have a tabindex attribute specifically set, assume it's 0. + if (isContentEditable(node)) return 0; + return node.tabIndex; + } + + function sortOrderedTabbables(a, b) { + return a.tabIndex === b.tabIndex ? a.documentOrder - b.documentOrder : a.tabIndex - b.tabIndex; + } + + // Array.prototype.find not available in IE. + function find(list, predicate) { + for (var i = 0, length = list.length; i < length; i++) { + if (predicate(list[i])) return list[i]; + } + } + + function isContentEditable(node) { + return node.contentEditable === 'true'; + } + + function isInput(node) { + return node.tagName === 'INPUT'; + } + + function isHiddenInput(node) { + return isInput(node) && node.type === 'hidden'; + } + + function isRadio(node) { + return isInput(node) && node.type === 'radio'; + } + + function isNonTabbableRadio(node) { + return isRadio(node) && !isTabbableRadio(node); + } + + function getCheckedRadio(nodes) { + for (var i = 0; i < nodes.length; i++) { + if (nodes[i].checked) { + return nodes[i]; + } + } + } + + function isTabbableRadio(node) { + if (!node.name) return true; + // This won't account for the edge case where you have radio groups with the same + // in separate forms on the same page. + var radioSet = node.ownerDocument.querySelectorAll('input[type="radio"][name="' + node.name + '"]'); + var checked = getCheckedRadio(radioSet); + return !checked || checked === node; + } + + // An element is "untouchable" if *it or one of its ancestors* has + // `visibility: hidden` or `display: none`. + function UntouchabilityChecker(elementDocument) { + this.doc = elementDocument; + // Node cache must be refreshed on every check, in case + // the content of the element has changed. The cache contains tuples + // mapping nodes to their boolean result. + this.cache = []; + } + + // getComputedStyle accurately reflects `visibility: hidden` of ancestors + // but not `display: none`, so we need to recursively check parents. + UntouchabilityChecker.prototype.hasDisplayNone = function hasDisplayNone(node, nodeComputedStyle) { + if (node === this.doc.documentElement) return false; + + // Search for a cached result. + var cached = find(this.cache, function(item) { + return item === node; + }); + if (cached) return cached[1]; + + nodeComputedStyle = nodeComputedStyle || this.doc.defaultView.getComputedStyle(node); + + var result = false; + + if (nodeComputedStyle.display === 'none') { + result = true; + } else if (node.parentNode) { + result = this.hasDisplayNone(node.parentNode); + } + + this.cache.push([node, result]); + + return result; + } + + UntouchabilityChecker.prototype.isUntouchable = function isUntouchable(node) { + if (node === this.doc.documentElement) return false; + var computedStyle = this.doc.defaultView.getComputedStyle(node); + if (this.hasDisplayNone(node, computedStyle)) return true; + return computedStyle.visibility === 'hidden'; + } + + //module.exports = tabbable; + + //////////// + + var service = { + tabbable: tabbable, + isTabbable: isTabbable, + isFocusable: isFocusable + }; + + return service; + } + + angular.module('umbraco.services').factory('tabbableService', tabbableService); + + })(); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js b/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js index fd82b663d5..6263e40711 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js @@ -990,8 +990,8 @@ function tinyMceService($log, $q, imageHelper, $locale, $http, $timeout, stylesh currentTarget.anchor = anchorVal.substring(1); } - //locallink detection, we do this here, to avoid poluting the dialogservice - //so the dialog service can just expect to get a node-like structure + //locallink detection, we do this here, to avoid poluting the editorService + //so the editor service can just expect to get a node-like structure if (currentTarget.url.indexOf("localLink:") > 0) { // if the current link has an anchor, it needs to be considered when getting the udi/id // if an anchor exists, reduce the substring max by its length plus two to offset the removed prefix and trailing curly brace diff --git a/src/Umbraco.Web.UI.Client/src/common/services/tree.service.js b/src/Umbraco.Web.UI.Client/src/common/services/tree.service.js index d5d2093d3b..aae6e43650 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/tree.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/tree.service.js @@ -15,21 +15,21 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS // a tab and have the trees where they used to be - supposed that is kind of nice but would mean we'd have to store the parent // as a nodeid reference instead of a variable with a getParent() method. var treeCache = {}; - + var standardCssClass = 'icon umb-tree-icon sprTree'; function getCacheKey(args) { //if there is no cache key they return null - it won't be cached. if (!args || !args.cacheKey) { return null; - } + } var cacheKey = args.cacheKey; cacheKey += "_" + args.section; return cacheKey; } - return { + return { /** Internal method to return the tree cache */ _getTreeCache: function() { @@ -70,10 +70,10 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS } }); }, - + /** Internal method that ensures there's a routePath, parent and level property on each tree node and adds some icon specific properties so that the nodes display properly */ _formatNodeDataForUseInUI: function (parentNode, treeNodes, section, level) { - //if no level is set, then we make it 1 + //if no level is set, then we make it 1 var childLevel = (level ? level : 1); //set the section if it's not already set if (!parentNode.section) { @@ -91,13 +91,14 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS var funcParent = function() { return parentNode; }; + for (var i = 0; i < treeNodes.length; i++) { var treeNode = treeNodes[i]; treeNode.level = childLevel; - //create a function to get the parent node, we could assign the parent node but + //create a function to get the parent node, we could assign the parent node but // then we cannot serialize this entity because we have a cyclical reference. // Instead we just make a function to return the parentNode. treeNode.parent = funcParent; @@ -108,17 +109,17 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS //if there is not route path specified, then set it automatically, //if this is a tree root node then we want to route to the section's dashboard if (!treeNode.routePath) { - + if (treeNode.metaData && treeNode.metaData["treeAlias"]) { //this is a root node - treeNode.routePath = section; + treeNode.routePath = section; } else { var treeAlias = this.getTreeAlias(treeNode); treeNode.routePath = section + "/" + treeAlias + "/edit/" + treeNode.id; } } - + //now, format the icon data if (treeNode.iconIsClass === undefined || treeNode.iconIsClass) { var converted = iconHelper.convertFromLegacyTreeNodeIcon(treeNode); @@ -155,10 +156,10 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS * @description * Determines if the current tree is a plugin tree and if so returns the package folder it has declared * so we know where to find it's views, otherwise it will just return undefined. - * + * * @param {String} treeAlias The tree alias to check */ - getTreePackageFolder: function(treeAlias) { + getTreePackageFolder: function(treeAlias) { //we determine this based on the server variables if (Umbraco.Sys.ServerVariables.umbracoPlugins && Umbraco.Sys.ServerVariables.umbracoPlugins.trees && @@ -167,7 +168,7 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS var found = _.find(Umbraco.Sys.ServerVariables.umbracoPlugins.trees, function(item) { return item.alias === treeAlias; }); - + return found ? found.packageFolder : undefined; } return undefined; @@ -181,7 +182,7 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS * * @description * Clears the tree cache - with optional cacheKey, optional section or optional filter. - * + * * @param {Object} args arguments * @param {String} args.cacheKey optional cachekey - this is used to clear specific trees in dialogs * @param {String} args.section optional section alias - clear tree for a given section @@ -205,7 +206,7 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS if (!args.cacheKey) { throw "args.cacheKey is required if args.childrenOf is supplied"; } - //this will clear out all children for the parentId passed in to this parameter, we'll + //this will clear out all children for the parentId passed in to this parameter, we'll // do this by recursing and specifying a filter var self = this; this.clearCache({ @@ -238,7 +239,7 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS //set the result to the filtered data treeCache[args.cacheKey] = result; } - else { + else { //remove the cache treeCache = _.omit(treeCache, args.cacheKey); } @@ -261,7 +262,7 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS return k.endsWith("_" + args.section); }); treeCache = _.omit(treeCache, toRemove2); - } + } } }, @@ -285,7 +286,7 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS if (!args.node) { throw "No node defined on args object for loadNodeChildren"; } - + this.removeChildNodes(args.node); args.node.loading = true; @@ -312,7 +313,7 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS //in case of error, emit event eventsService.emit("treeService.treeNodeLoadError", {error: reason } ); - //stop show the loading indicator + //stop show the loading indicator args.node.loading = false; //tell notications about the error @@ -342,9 +343,12 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS throw "Cannot remove a node that doesn't have a parent"; } //remove the current item from it's siblings - treeNode.parent().children.splice(treeNode.parent().children.indexOf(treeNode), 1); + var parent = treeNode.parent(); + parent.children.splice(parent.children.indexOf(treeNode), 1); + + parent.hasChildren = parent.children.length !== 0; }, - + /** * @ngdoc method * @name umbraco.services.treeService#removeChildNodes @@ -352,7 +356,7 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS * @function * * @description - * Removes all child nodes from a given tree node + * Removes all child nodes from a given tree node * @param {object} treeNode the node to remove children from */ removeChildNodes : function(treeNode) { @@ -401,21 +405,39 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS throw "Cannot get a descendant node from a section container node without a treeAlias specified"; } - //if it is a section container, we need to find the tree to be searched - if (treeNode.isContainer) { - var foundRoot = null; - for (var c = 0; c < treeNode.children.length; c++) { - if (this.getTreeAlias(treeNode.children[c]) === treeAlias) { - foundRoot = treeNode.children[c]; - break; + //the treeNode passed in could be a section container, or it could be a section group + //in either case we need to go through the children until we can find the actual tree root with the treeAlias + var self = this; + function getTreeRoot(tn) { + //if it is a section container, we need to find the tree to be searched + if (tn.isContainer) { + for (var c = 0; c < tn.children.length; c++) { + if (tn.children[c].isContainer) { + //recurse + var root = getTreeRoot(tn.children[c]); + + //only return if we found the root in this child, otherwise continue. + if(root){ + return root; + } + } + else if (self.getTreeAlias(tn.children[c]) === treeAlias) { + return tn.children[c]; + } } + return null; } - if (!foundRoot) { - throw "Could not find a tree in the current section with alias " + treeAlias; + else { + return tn; } - treeNode = foundRoot; } + var foundRoot = getTreeRoot(treeNode); + if (!foundRoot) { + throw "Could not find a tree in the current section with alias " + treeAlias; + } + treeNode = foundRoot; + //check this node if (treeNode.id === id) { return treeNode; @@ -426,7 +448,7 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS if (found) { return found; } - + //check each child of this node if (!treeNode.children) { return null; @@ -442,7 +464,7 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS } } } - + //not found return found === undefined ? null : found; }, @@ -464,9 +486,9 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS //all root nodes have metadata key 'treeAlias' var root = null; - var current = treeNode; + var current = treeNode; while (root === null && current) { - + if (current.metaData && current.metaData["treeAlias"]) { root = current; } @@ -491,7 +513,7 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS * @function * * @description - * Gets the node's tree alias, this is done by looking up the meta-data of the current node's root node + * Gets the node's tree alias, this is done by looking up the meta-data of the current node's root node * @param {object} treeNode to retrive tree alias from */ getTreeAlias : function(treeNode) { @@ -509,7 +531,7 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS * @function * * @description - * gets the tree, returns a promise + * gets the tree, returns a promise * @param {object} args Arguments * @param {string} args.section Section alias * @param {string} args.cacheKey Optional cachekey @@ -525,7 +547,7 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS } var cacheKey = getCacheKey(args); - + //return the cache if it exists if (cacheKey && treeCache[cacheKey] !== undefined) { return $q.when(treeCache[cacheKey]); @@ -540,9 +562,20 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS alias: args.section, root: data }; - //we need to format/modify some of the node data to be used in our app. + + //format the root self._formatNodeDataForUseInUI(result.root, result.root.children, args.section); + //if this is a root that contains group nodes, we need to format those manually too + if (result.root.containsGroups) { + for (var i = 0; i < result.root.children.length; i++) { + var group = result.root.children[i]; + + //we need to format/modify some of the node data to be used in our app. + self._formatNodeDataForUseInUI(group, group.children, args.section); + } + } + //cache this result if a cache key is specified - generally a cache key should ONLY // be specified for application trees, dialog trees should not be cached. if (cacheKey) { @@ -584,7 +617,7 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS return data; }); }, - + /** * @ngdoc method * @name umbraco.services.treeService#getChildren @@ -592,7 +625,7 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS * @function * * @description - * Gets the children from the server for a given node + * Gets the children from the server for a given node * @param {object} args Arguments * @param {object} args.node tree node object to retrieve the children for * @param {string} args.section current section alias @@ -618,7 +651,7 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS return $q.when(data); }); }, - + /** * @ngdoc method * @name umbraco.services.treeService#reloadNode @@ -639,7 +672,7 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS if (!node.section) { throw "cannot reload a single node without an assigned node.section"; } - + //set the node to loading node.loading = true; @@ -663,7 +696,7 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS //just update as per normal - this means styles, etc.. won't be applied _.extend(node.parent().children[index], found); } - + //set the node loading node.parent().children[index].loading = false; //return @@ -684,22 +717,25 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS * @function * * @description - * This will return the current node's path by walking up the tree + * This will return the current node's path by walking up the tree * @param {object} node Tree node to retrieve path for */ getPath: function(node) { if (!node) { - throw "node cannot be null"; + throw "node cannot be null"; } if (!angular.isFunction(node.parent)) { throw "node.parent is not a function, the path cannot be resolved"; } - //all root nodes have metadata key 'treeAlias' + var reversePath = []; var current = node; while (current != null) { - reversePath.push(current.id); - if (current.metaData && current.metaData["treeAlias"]) { + reversePath.push(current.id); + + //all tree root nodes (non group, not section root) have a treeAlias so exit if that is the case + //or exit if we cannot traverse further up + if ((current.metaData && current.metaData["treeAlias"]) || !current.parent) { current = null; } else { @@ -710,7 +746,7 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS }, syncTree: function(args) { - + if (!args) { throw "No args object defined for syncTree"; } @@ -733,7 +769,7 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS if (!root) { throw "Could not get the root tree node based on the node passed in"; } - + //now we want to loop through the ids in the path, first we'll check if the first part //of the path is the root node, otherwise we'll search it's children. var currPathIndex = 0; @@ -748,7 +784,7 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS currPathIndex = 1; } } - + //now that we have the first id to lookup, we can start the process var self = this; @@ -778,7 +814,7 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS } } else { - //couldn't find it in the + //couldn't find it in the return self.loadNodeChildren({ node: node, section: node.section }).then(function (children) { //ok, got the children, let's find it var found = self.getChildNode(node, args.path[currPathIndex]); @@ -810,7 +846,7 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS return doSync(); } - + }; } diff --git a/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js b/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js index b18cb73eae..668509cdf3 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js @@ -369,7 +369,9 @@ properties: getContentProperties(v.tabs), culture: v.language ? v.language.culture : null, publish: v.publish, - save: v.save + save: v.save, + releaseDate: v.releaseDate, + expireDate: v.expireDate }; }) }; diff --git a/src/Umbraco.Web.UI.Client/src/common/services/umbrequesthelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/umbrequesthelper.service.js index 1619ca0623..fcb5585d5d 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/umbrequesthelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/umbrequesthelper.service.js @@ -3,7 +3,7 @@ * @name umbraco.services.umbRequestHelper * @description A helper object used for sending requests to the server **/ -function umbRequestHelper($http, $q, umbDataFormatter, angularHelper, dialogService, notificationsService, eventsService, formHelper) { +function umbRequestHelper($http, $q, notificationsService, eventsService, formHelper) { return { @@ -126,12 +126,21 @@ function umbRequestHelper($http, $q, umbDataFormatter, angularHelper, dialogServ /** The default error callback used if one is not supplied in the opts */ function defaultError(data, status, headers, config) { - return { + + var err = { //NOTE: the default error message here should never be used based on the above docs! errorMsg: (angular.isString(opts) ? opts : 'An error occurred!'), data: data, status: status }; + + // if "opts" is a promise, we set "err.errorMsg" to be that promise + if (typeof(opts) == "object" && typeof(opts.then) == "function") { + err.errorMsg = opts; + } + + return err; + } //create the callbacs based on whats been passed in. diff --git a/src/Umbraco.Web.UI.Client/src/common/services/user.service.js b/src/Umbraco.Web.UI.Client/src/common/services/user.service.js index ac35790127..7723c8f4bb 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/user.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/user.service.js @@ -1,36 +1,20 @@ angular.module('umbraco.services') - .factory('userService', function ($rootScope, eventsService, $q, $location, $log, requestRetryQueue, authResource, dialogService, $timeout, angularHelper, $http) { + .factory('userService', function ($rootScope, eventsService, $q, $location, requestRetryQueue, authResource, $timeout, angularHelper) { var currentUser = null; var lastUserId = null; - var loginDialog = null; //this tracks the last date/time that the user's remainingAuthSeconds was updated from the server // this is used so that we know when to go and get the user's remaining seconds directly. var lastServerTimeoutSet = null; function openLoginDialog(isTimedOut) { - if (!loginDialog) { - loginDialog = dialogService.open({ - - //very special flag which means that global events cannot close this dialog - manualClose: true, - - template: 'views/common/dialogs/login.html', - modalClass: "login-overlay", - animation: "slide", - show: true, - callback: onLoginDialogClose, - dialogData: { - isTimedOut: isTimedOut - } - }); - } + //broadcast a global event that the user is no longer logged in + const args = { isTimedOut: isTimedOut }; + eventsService.emit("app.notAuthenticated", args); } - function onLoginDialogClose(success) { - loginDialog = null; - + function retryRequestQueue(success) { if (success) { requestRetryQueue.retryAll(currentUser.name); } @@ -164,9 +148,6 @@ angular.module('umbraco.services') lastServerTimeoutSet = null; currentUser = null; - //broadcast a global event that the user is no longer logged in - eventsService.emit("app.notAuthenticated"); - openLoginDialog(isLogout === undefined ? true : !isLogout); } @@ -183,6 +164,12 @@ angular.module('umbraco.services') _showLoginDialog: function () { openLoginDialog(); }, + + /** Internal method to retry all request after sucessfull login */ + _retryRequestQueue: function(success) { + retryRequestQueue(success) + }, + /** Returns a promise, sends a request to the server to check if the current cookie is authorized */ isAuthenticated: function () { //if we've got a current user then just return true diff --git a/src/Umbraco.Web.UI.Client/src/controllers/main.controller.js b/src/Umbraco.Web.UI.Client/src/controllers/main.controller.js index a10943c17e..6f3f0bff52 100644 --- a/src/Umbraco.Web.UI.Client/src/controllers/main.controller.js +++ b/src/Umbraco.Web.UI.Client/src/controllers/main.controller.js @@ -17,42 +17,33 @@ function MainController($scope, $location, appState, treeService, notificationsS $scope.overlay = {}; $scope.drawer = {}; $scope.search = {}; + $scope.login = {}; $scope.removeNotification = function (index) { notificationsService.remove(index); }; - $scope.closeDialogs = function (event) { - //only close dialogs if non-link and non-buttons are clicked - var el = event.target.nodeName; - var els = ["INPUT", "A", "BUTTON"]; - - if (els.indexOf(el) >= 0) { return; } - - var parents = $(event.target).parents("a,button"); - if (parents.length > 0) { - return; - } - - //SD: I've updated this so that we don't close the dialog when clicking inside of the dialog - var nav = $(event.target).parents("#dialog"); - if (nav.length === 1) { - return; - } - - eventsService.emit("app.closeDialogs", event); - }; - $scope.closeSearch = function() { appState.setSearchState("show", false); }; - var evts = []; + $scope.showLoginScreen = function(isTimedOut) { + $scope.login.isTimedOut = isTimedOut; + $scope.login.show = true; + }; + $scope.hideLoginScreen = function() { + $scope.login.show = false; + }; + + var evts = []; + //when a user logs out or timesout - evts.push(eventsService.on("app.notAuthenticated", function () { + evts.push(eventsService.on("app.notAuthenticated", function (evt, data) { $scope.authenticated = null; $scope.user = null; + const isTimedOut = data && data.isTimedOut ? true : false; + $scope.showLoginScreen(isTimedOut); })); evts.push(eventsService.on("app.userRefresh", function(evt) { diff --git a/src/Umbraco.Web.UI.Client/src/controllers/navigation.controller.js b/src/Umbraco.Web.UI.Client/src/controllers/navigation.controller.js index 1f6f2c75b8..8c521ca35e 100644 --- a/src/Umbraco.Web.UI.Client/src/controllers/navigation.controller.js +++ b/src/Umbraco.Web.UI.Client/src/controllers/navigation.controller.js @@ -9,7 +9,7 @@ * * @param {navigationService} navigationService A reference to the navigationService */ -function NavigationController($scope, $rootScope, $location, $log, $q, $routeParams, $timeout, treeService, appState, navigationService, keyboardService, dialogService, historyService, eventsService, sectionResource, angularHelper, languageResource, contentResource) { +function NavigationController($scope, $rootScope, $location, $log, $q, $routeParams, $timeout, treeService, appState, navigationService, keyboardService, historyService, eventsService, angularHelper, languageResource, contentResource) { //this is used to trigger the tree to start loading once everything is ready var treeInitPromise = $q.defer(); @@ -113,16 +113,6 @@ function NavigationController($scope, $rootScope, $location, $log, $q, $routePar return treeInitPromise.promise; } - //TODO: Remove this, this is not healthy - // Put the navigation service on this scope so we can use it's methods/properties in the view. - // IMPORTANT: all properties assigned to this scope are generally available on the scope object on dialogs since - // when we create a dialog we pass in this scope to be used for the dialog's scope instead of creating a new one. - $scope.nav = navigationService; - // TODO: Remove this, this is not healthy - // it is less than ideal to be passing in the navigationController scope to something else to be used as it's scope, - // this is going to lead to problems/confusion. I really don't think passing scope's around is very good practice. - $rootScope.nav = navigationService; - //set up our scope vars $scope.showContextMenuDialog = false; $scope.showContextMenu = false; @@ -147,12 +137,8 @@ function NavigationController($scope, $rootScope, $location, $log, $q, $routePar navigationService.showSearch(); }); - //trigger dialods with a hotkey: - keyboardService.bind("esc", function () { - eventsService.emit("app.closeDialogs"); - }); - - $scope.selectedId = navigationService.currentId; + ////TODO: remove this it's not a thing + //$scope.selectedId = navigationService.currentId; var evts = []; @@ -168,6 +154,9 @@ function NavigationController($scope, $rootScope, $location, $log, $q, $routePar if (args.key === "showMenuDialog") { $scope.showContextMenuDialog = args.value; } + if (args.key === "dialogTemplateUrl") { + $scope.dialogTemplateUrl = args.value; + } if (args.key === "showMenu") { $scope.showContextMenu = args.value; } @@ -182,28 +171,35 @@ function NavigationController($scope, $rootScope, $location, $log, $q, $routePar } })); - //Listen for section state changes + //Listen for tree state changes evts.push(eventsService.on("appState.treeState.changed", function (e, args) { - var f = args; - if (args.value.root && args.value.root.metaData.containsTrees === false) { - $rootScope.emptySection = true; - } - else { - $rootScope.emptySection = false; + if (args.key === "currentRootNode") { + + //if the changed state is the currentRootNode, determine if this is a full screen app + if (args.value.root && args.value.root.containsTrees === false) { + $rootScope.emptySection = true; + } + else { + $rootScope.emptySection = false; + } } + })); //Listen for section state changes evts.push(eventsService.on("appState.sectionState.changed", function (e, args) { + //section changed if (args.key === "currentSection" && $scope.currentSection != args.value) { - $scope.currentSection = args.value; - - //load the tree - configureTreeAndLanguages(); - $scope.treeApi.load({ section: $scope.currentSection, customTreeParams: $scope.customTreeParams, cacheKey: $scope.treeCacheKey }); - + //before loading the main tree we need to ensure that the nav is ready + navigationService.waitForNavReady().then(() => { + $scope.currentSection = args.value; + //load the tree + configureTreeAndLanguages(); + $scope.treeApi.load({ section: $scope.currentSection, customTreeParams: $scope.customTreeParams, cacheKey: $scope.treeCacheKey }); + }); } + //show/hide search results if (args.key === "showSearchResults") { $scope.showSearchResults = args.value; @@ -218,18 +214,20 @@ function NavigationController($scope, $rootScope, $location, $log, $q, $routePar }); })); - evts.push(eventsService.on("editors.languages.languageCreated", function (e, args) { - loadLanguages().then(function (languages) { - $scope.languages = languages; - }); - })); - - //This reacts to clicks passed to the body element which emits a global call to close all dialogs - evts.push(eventsService.on("app.closeDialogs", function (event) { - if (appState.getGlobalState("stickyNavigation")) { - navigationService.hideNavigation(); - //TODO: don't know why we need this? - we are inside of an angular event listener. - angularHelper.safeApply($scope); + //Emitted when a language is created or an existing one saved/edited + evts.push(eventsService.on("editors.languages.languageSaved", function (e, args) { + console.log('lang event listen args', args); + if(args.isNew){ + //A new language has been created - reload languages for tree + loadLanguages().then(function (languages) { + $scope.languages = languages; + }); + } + else if(args.language.isDefault){ + //A language was saved and was set to be the new default (refresh the tree, so its at the top) + loadLanguages().then(function (languages) { + $scope.languages = languages; + }); } })); @@ -238,7 +236,7 @@ function NavigationController($scope, $rootScope, $location, $log, $q, $routePar $scope.authenticated = false; })); - //when the application is ready and the user is authorized setup the data + //when the application is ready and the user is authorized, setup the data evts.push(eventsService.on("app.ready", function (evt, data) { init(); })); @@ -303,12 +301,12 @@ function NavigationController($scope, $rootScope, $location, $log, $q, $routePar navInit = true; initNav(); } - else { - //keep track of the current section, when it changes change the state, and we listen for that event change above - if ($scope.currentSection != $routeParams.section) { - appState.setSectionState("currentSection", $routeParams.section); - } + + //keep track of the current section when it changes + if ($scope.currentSection != $routeParams.section) { + appState.setSectionState("currentSection", $routeParams.section); } + } }); } @@ -369,8 +367,7 @@ function NavigationController($scope, $rootScope, $location, $log, $q, $routePar //the nav is ready, let the app know eventsService.emit("app.navigationReady", { treeApi: $scope.treeApi }); - //finally set the section state - appState.setSectionState("currentSection", $routeParams.section); + } }); }); @@ -422,7 +419,7 @@ function NavigationController($scope, $rootScope, $location, $log, $q, $routePar //this reacts to the options item in the tree //TODO: migrate to nav service - //TODO: is this used? + //TODO: is this used? $scope.searchShowMenu = function (ev, args) { //always skip default args.skipDefault = true; diff --git a/src/Umbraco.Web.UI.Client/src/index.html b/src/Umbraco.Web.UI.Client/src/index.html index bd354efc90..e15cf0ab62 100644 --- a/src/Umbraco.Web.UI.Client/src/index.html +++ b/src/Umbraco.Web.UI.Client/src/index.html @@ -21,7 +21,7 @@ - + diff --git a/src/Umbraco.Web.UI.Client/src/installer/steps/user.html b/src/Umbraco.Web.UI.Client/src/installer/steps/user.html index 1f916c6c00..765eb3952a 100644 --- a/src/Umbraco.Web.UI.Client/src/installer/steps/user.html +++ b/src/Umbraco.Web.UI.Client/src/installer/steps/user.html @@ -32,6 +32,7 @@ ng-pattern="passwordPattern" autocorrect="off" autocapitalize="off" + autocomplete="off" required ng-model="installer.current.model.password" id="password" /> At least {{installer.current.model.minCharLength}} characters long diff --git a/src/Umbraco.Web.UI.Client/src/less/application/grid.less b/src/Umbraco.Web.UI.Client/src/less/application/grid.less index 7ed2abc898..a7b4bd0011 100644 --- a/src/Umbraco.Web.UI.Client/src/less/application/grid.less +++ b/src/Umbraco.Web.UI.Client/src/less/application/grid.less @@ -167,6 +167,7 @@ body.umb-drawer-is-visible #mainwrapper{ @media (min-width: 1101px) { #contentwrapper, #umb-notifications-wrapper {left: 360px;} #speechbubble {left: 360px;} + .emptySection #contentwrapper {left:0px;} } //empty section modification diff --git a/src/Umbraco.Web.UI.Client/src/less/belle.less b/src/Umbraco.Web.UI.Client/src/less/belle.less index 7cff469c28..5218a9b59c 100644 --- a/src/Umbraco.Web.UI.Client/src/less/belle.less +++ b/src/Umbraco.Web.UI.Client/src/less/belle.less @@ -67,6 +67,7 @@ // Belle styles @import "buttons.less"; @import "forms.less"; +@import "legacydialog.less"; @import "modals.less"; @import "panel.less"; @import "sections.less"; @@ -115,6 +116,7 @@ @import "components/umb-confirm-action.less"; @import "components/umb-keyboard-shortcuts-overview.less"; @import "components/umb-checkbox-list.less"; +@import "components/umb-radiobuttons-list.less"; @import "components/umb-locked-field.less"; @import "components/umb-tabs.less"; @import "components/umb-load-indicator.less"; @@ -149,6 +151,7 @@ @import "components/umb-box.less"; @import "components/umb-number-badge.less"; @import "components/umb-progress-circle.less"; +@import "components/umb-stylesheet.less"; @import "components/buttons/umb-button.less"; @import "components/buttons/umb-button-group.less"; @@ -160,6 +163,7 @@ @import "components/umb-mini-editor.less"; @import "components/users/umb-user-cards.less"; +@import "components/users/umb-user-details.less"; @import "components/users/umb-user-group-picker-list.less"; @import "components/users/umb-user-group-preview.less"; @import "components/users/umb-user-preview.less"; @@ -169,6 +173,7 @@ // Utilities @import "utilities/layout/_display.less"; +@import "utilities/theme/_opacity.less"; @import "utilities/typography/_text-decoration.less"; @import "utilities/typography/_white-space.less"; @import "utilities/_flexbox.less"; diff --git a/src/Umbraco.Web.UI.Client/src/less/buttons.less b/src/Umbraco.Web.UI.Client/src/less/buttons.less index b1e9671de3..0b21864127 100644 --- a/src/Umbraco.Web.UI.Client/src/less/buttons.less +++ b/src/Umbraco.Web.UI.Client/src/less/buttons.less @@ -63,9 +63,6 @@ .btn-group>.btn+.dropdown-toggle { box-shadow: none; -webkit-box-shadow:none; -} - -.btn-group .btn.dropdown-toggle { border-left-width: 1px; border-left-style: solid; border-color: rgba(0,0,0,0.09); diff --git a/src/Umbraco.Web.UI.Client/src/less/canvas-designer.less b/src/Umbraco.Web.UI.Client/src/less/canvas-designer.less index 78c1616bc6..cbb38a23b1 100644 --- a/src/Umbraco.Web.UI.Client/src/less/canvas-designer.less +++ b/src/Umbraco.Web.UI.Client/src/less/canvas-designer.less @@ -81,9 +81,9 @@ a, a:hover{ .wait { display: block; - height: 280px; + height: 100%; width: 100%; - background: center center url(../img/loader.gif) no-repeat; + background:#fff center center url(../img/loader.gif) no-repeat; } /****************************/ diff --git a/src/Umbraco.Web.UI.Client/src/less/components/application/umb-drawer.less b/src/Umbraco.Web.UI.Client/src/less/components/application/umb-drawer.less index cc024ca5bb..2c7f07ef26 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/application/umb-drawer.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/application/umb-drawer.less @@ -159,6 +159,7 @@ margin-bottom: 1px; border-radius: 0; border-bottom: 1px solid @gray-9; + padding: 10px; } .umb-help-list-item:last-child { @@ -193,6 +194,7 @@ .umb-help-list-item__title { font-size: 14px; display: block; + margin-left: 26px; } .umb-help-list-item__description { @@ -205,6 +207,7 @@ margin-right: 8px; color: @gray-4; font-size: 18px; + float: left; } .umb-help-list-item__open-icon { @@ -219,4 +222,4 @@ [data-element*="tour-"].umb-help-list-item:hover .umb-help-list-item__title { text-decoration:none; -} \ No newline at end of file +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/buttons/umb-button.less b/src/Umbraco.Web.UI.Client/src/less/components/buttons/umb-button.less index 4b670ab781..d3f2fee5d5 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/buttons/umb-button.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/buttons/umb-button.less @@ -104,7 +104,7 @@ } .umb-button--xs { - padding: 5px 16px; + padding: 5px 13px; font-size: 14px; } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/buttons/umb-toggle.less b/src/Umbraco.Web.UI.Client/src/less/components/buttons/umb-toggle.less index 73f059b4ee..150963cbb2 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/buttons/umb-toggle.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/buttons/umb-toggle.less @@ -10,6 +10,10 @@ } } +.umb-toggle:focus .umb-toggle__toggle{ + box-shadow: 0 1px 3px fade(@black, 12%), 0 1px 2px fade(@black, 24%); +} + .umb-toggle__handler { position: absolute; top: 0; @@ -30,6 +34,7 @@ background: @gray-8; border-radius: 90px; position: relative; + transition: box-shadow .3s; } .umb-toggle--checked .umb-toggle__toggle { @@ -43,7 +48,6 @@ /* Labels */ .umb-toggle__label { - font-size: 12px; color: @gray-2; } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/card.less b/src/Umbraco.Web.UI.Client/src/less/components/card.less index e8b8325183..a302ba71b8 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/card.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/card.less @@ -101,11 +101,13 @@ } .umb-card-grid li.-four-in-row { - flex: 0 0 25%; + flex: 0 0 25%; + max-width: 25%; } .umb-card-grid li.-three-in-row { flex: 0 0 33.33%; + max-width:33.33%; } .umb-card-grid .umb-card-grid-item { diff --git a/src/Umbraco.Web.UI.Client/src/less/components/editor.less b/src/Umbraco.Web.UI.Client/src/less/components/editor.less index f045b0adca..d699193c24 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/editor.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/editor.less @@ -201,7 +201,6 @@ a.umb-variant-switcher__toggle { .umb-variant-switcher__name { display: block; - font-weight: bold; } .umb-variant-switcher__state { diff --git a/src/Umbraco.Web.UI.Client/src/less/components/prevalues/multivalues.less b/src/Umbraco.Web.UI.Client/src/less/components/prevalues/multivalues.less index 43f6697eb1..0dbc4c381c 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/prevalues/multivalues.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/prevalues/multivalues.less @@ -5,6 +5,10 @@ .umb-overlay & { width: 500px; } + + p{ + margin: 7px 0; + } } .umb-prevalues-multivalues__left { diff --git a/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-actions.less b/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-actions.less index f52258333d..15296a6aaa 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-actions.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-actions.less @@ -51,6 +51,15 @@ text-decoration: none; } +.umb-action { + &.-opens-dialog { + .menu-label:after { + // adds an ellipsis (...) after the menu label for actions that open a dialog + content: '\2026'; + } + } +} + .umb-actions-child { .umb-action { 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 f2cacc26b3..fb83504a1f 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 @@ -8,6 +8,9 @@ .umb-box-header { padding: 10px 20px; border-bottom: 1px solid @gray-9; + display: flex; + align-items: center; + justify-content: space-between; } .umb-box-header-title { diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-color-swatches.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-color-swatches.less index a987c5daa3..6dc2dd6ff3 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-color-swatches.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-color-swatches.less @@ -3,8 +3,8 @@ flex-flow: row wrap; .umb-color-box { - border: none; - color: white; + border: 1px solid rgba(0,0,0,0.15); + color: @white; cursor: pointer; padding: 1px; text-align: center; @@ -19,7 +19,7 @@ justify-content: center; &:hover, &:focus { - box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24); + box-shadow: 0 1px 3px fade(@black, 12%), 0 1px 2px fade(@black, 24%); } &.umb-color-box--m { @@ -36,10 +36,11 @@ &.with-labels { .umb-color-box { - width: 120px; - height: 100%; + width: 130px; + height: auto; display: flex; flex-flow: row wrap; + border: 1px solid @gray-8; .umb-color-box-inner { display: flex; @@ -47,15 +48,21 @@ flex: 0 0 100%; max-width: 100%; min-height: 80px; - padding-top: 10px; + padding: 0; + + .check_circle { + margin: 15px auto; + } .umb-color-box__label { - background: #fff; + background: @white; font-size: 14px; display: flex; flex-flow: column wrap; - flex: 0 0 100%; + flex: 1 0 100%; + justify-content: flex-end; padding: 1px 5px; + min-height: 45px; max-width: 100%; margin-top: auto; margin-bottom: -3px; @@ -63,7 +70,8 @@ margin-right: -1px; text-indent: 0; text-align: left; - border: 1px solid @gray-8; + border-top: 1px solid @gray-8; + border-bottom: 1px solid @gray-8; border-bottom-left-radius: 3px; border-bottom-right-radius: 3px; overflow: hidden; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-content-grid.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-content-grid.less index b0c90483f7..63a0856c84 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-content-grid.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-content-grid.less @@ -1,6 +1,6 @@ .umb-content-grid { display: grid; - grid-template-columns: 1fr 1fr 1fr; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); grid-template-rows: auto; grid-gap: 15px; } @@ -79,6 +79,7 @@ .umb-content-grid__details-value { display: inline-block; + word-break: break-word; } .umb-content-grid__checkmark { diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-editor-navigation.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-editor-navigation.less index 407cfaf6a7..3b49dceeb2 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-editor-navigation.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-editor-navigation.less @@ -43,7 +43,7 @@ font-size: 24px; display: block; text-align: center; - margin-bottom: 5px; + margin-bottom: 7px; } .umb-sub-views-nav-item-text { diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-multiple-textbox.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-multiple-textbox.less index 21f59a3e2d..52cc7a9aaf 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-multiple-textbox.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-multiple-textbox.less @@ -1,4 +1,17 @@ -.umb-multiple-textbox .textbox-wrapper { +.umb-multiple-textbox{ + &__confirm{ + position: relative; + + &-action{ + margin: 0; + padding: 2px; + background: transparent; + border: 0 none; + } + } +} + +.umb-multiple-textbox .textbox-wrapper { align-items: center; margin-bottom: 15px; } @@ -7,7 +20,7 @@ margin-bottom: 0; } -.umb-multiple-textbox .textbox-wrapper i { +.umb-multiple-textbox .textbox-wrapper i:not(.icon-delete, .icon-check) { margin-right: 5px; } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-nested-content.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-nested-content.less index 514a73407c..df8977a2bf 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-nested-content.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-nested-content.less @@ -1,5 +1,6 @@ .umb-nested-content { text-align: center; + position: relative; } .umb-nested-content--not-supported { @@ -208,12 +209,6 @@ width: 99%; } -.usky-grid.umb-nested-content__node-type-picker .cell-tools-menu { - position: relative; - transform: translate(-50%, -25%); -} - - // this resolves the layout issue introduced in nested content in 7.12 with the addition of the input for link anchors // the attribute selector ensures the change only applies to the linkpicker overlay .form-horizontal .umb-nested-content--narrow [ng-controller*="Umbraco.Overlays.LinkPickerController"] .controls-row { diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-property-editor.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-property-editor.less index cbea6987e7..8a43c95e6e 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-property-editor.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-property-editor.less @@ -1,3 +1,4 @@ -.umb-property-editor.-not-clickable { +.umb-property-editor--preview { pointer-events: none; -} + user-select: none; +} \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-radiobuttons-list.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-radiobuttons-list.less new file mode 100644 index 0000000000..2fe3487a8f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-radiobuttons-list.less @@ -0,0 +1,80 @@ +.umb-radiobuttons{ + &__label{ + position: relative; + padding: 0; + + &-text{ + margin: 0 0 0 32px; + position: relative; + top: 1px; + } + } + + &__input{ + position: absolute; + top: 0; + left: 0; + opacity: 0; + + &:focus ~ .umb-radiobuttons__state{ + box-shadow: 0 1px 3px fade(@black, 12%), 0 1px 2px fade(@black, 24%); + } + + &:focus:checked ~ .umb-radiobuttons__state{ + box-shadow: none; + } + + &:checked ~ .umb-radiobuttons__state{ + &:before{ + width: 100%; + height: 100%; + } + } + + &:checked ~ .umb-radiobuttons__state .umb-radiobuttons__icon{ + opacity: 1; + } + } + + &__state{ + display: flex; + flex-wrap: wrap; + border: 1px solid @gray-8; + border-radius: 100%; + width: 22px; + height: 22px; + position: relative; + + &:before{ + content: ""; + background: @green; + width: 0; + height: 0; + transition: .1s ease-out; + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; + margin: auto; + border-radius: 100%; + } + } + + &__icon{ + color: @white; + text-align: center; + font-size: 15px; + opacity: 0; + transition: .3s ease-out; + + &:before{ + position: absolute; + top: 2px; + right: 0; + left: 0; + bottom: 0; + margin: auto; + } + } +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-stylesheet.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-stylesheet.less new file mode 100644 index 0000000000..59ded555a6 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-stylesheet.less @@ -0,0 +1,47 @@ +.umb-stylesheet-rules { + max-width: 600px; +} + +.umb-stylesheet-rules__listitem { + display: flex; + padding: 6px; + margin: 10px 0 !important; + background: @gray-10; + cursor: pointer; + border-radius: @baseBorderRadius; +} + +.umb-stylesheet-rules__listitem i { + display: flex; + align-items: center; + margin-right: 5px; + cursor: move; +} + +.umb-stylesheet-rules__listitem a { + margin-left: auto; +} + +.umb-stylesheet-rules__listitem input { + width: 295px; +} + +.umb-stylesheet-rules__left { + display: flex; + flex: 1 1 auto; + overflow: hidden; +} + +.umb-stylesheet-rules__right { + display: flex; + flex: 0 0 auto; + align-items: center; +} + +.umb-stylesheet-rule-overlay { + textarea { + width: 300px; + height: 120px; + resize: none; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-table.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-table.less index b729aa92bc..c1687636d3 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-table.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-table.less @@ -150,38 +150,15 @@ input.umb-table__input { } // Show checkmark when checked, hide file icon -.-selected { +.umb-table-row--selected { .umb-table-body__fileicon { display: none; } - .umb-table-body__checkicon { display: inline-block; } } - -// Styling for when item is published or not -.-published { - -} - -.-content :not(.with-unpublished-version).-unpublished { - .umb-table__name > * { - opacity: .4; - } - - .umb-table-body__icon { - opacity: .4; - } -} - -.-selected.-unpublished { - .umb-table-body__icon { - opacity: 1; - } -} - // Table Row Styles .umb-table-row { display: flex; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-cards.less b/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-cards.less index 7acf47d22e..de2dca5f91 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-cards.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-cards.less @@ -1,18 +1,13 @@ .umb-user-cards { - display: flex; - flex-direction: row; - flex-wrap: wrap; - margin: -10px; + display: grid; + grid-gap: 20px; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); } .umb-user-card { - padding: 10px; box-sizing: border-box; - flex: 0 0 100%; max-width: 100%; display: flex; - flex-wrap: wrap; - flex-direction: column; } .umb-user-card:hover, @@ -21,49 +16,6 @@ text-decoration: none !important; } -@media (min-width: 768px) { - .umb-user-card { - flex: 0 0 50%; - max-width: 50%; - } -} - -@media (min-width: 1200px) { - .umb-user-card { - flex: 0 0 33.33%; - max-width: 33.33%; - } -} - -@media (min-width: 1400px) { - .umb-user-card { - flex: 0 0 25%; - max-width: 25%; - } -} - -@media (min-width: 1700px) { - .umb-user-card { - flex: 0 0 20%; - max-width: 20%; - } -} - - -@media (min-width: 1900px) { - .umb-user-card { - flex: 0 0 16.66%; - max-width: 16.66%; - } -} - -@media (min-width: 2200px) { - .umb-user-card { - flex: 0 0 14.28%; - max-width: 14.28%; - } -} - .umb-user-card__content { position: relative; padding: 15px; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-details.less b/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-details.less new file mode 100644 index 0000000000..9ddad03b48 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-details.less @@ -0,0 +1,114 @@ +.umb-user-details-avatar { + margin-bottom: 20px; + padding-bottom: 20px; + border-bottom: 1px solid #d8d7d9; +} + +div.umb-user-details-actions > div { + margin-bottom: 20px; +} + +.umb-user-details-actions .umb-button { + margin-bottom: 20px; +} + +.umb-user-details-view-title { + font-size: 20px; + font-weight: bold; + color: @black; + margin-bottom: 30px; +} + +.umb-user-details-view-wrapper { + padding: 20px 60px; +} + +@media (max-width: 768px) { + + .umb-user-details-view-wrapper { + padding: 0; + } +} + +.umb-user-details-section { + margin-bottom: 40px; +} +.umb-user-details-details { + display: flex; +} + +a.umb-user-details-details__back-link { + font-weight: bold; + color: @black; +} + +.umb-user-details-details__back-link:hover { + color: @gray-4; + text-decoration: none; +} + + +@sidebarwidth: 350px; // Width of sidebar. Ugly hack because of old version of Less + +.umb-user-details-details__main-content { + flex: 1 1 auto; + margin-right: 30px; + width: calc(~'100%' - ~'@{sidebarwidth}' - ~'30px'); // Make sure that the main content area doesn't gets affected by inline styling +} + +.umb-user-details-details__main-content .umb-node-preview-add { + max-width: 100%; +} + + +.umb-user-details-details__sidebar { + flex: 0 0 @sidebarwidth; +} + +@media (max-width: 768px) { + + .umb-user-details-details { + flex-direction: column; + } + + .umb-user-details-details__main-content { + flex: 1 1 auto; + width: 100%; + margin-bottom: 30px; + margin-right: 0; + } + + .umb-user-details-details__sidebar { + flex: 1 1 auto; + width: 100%; + } +} + +.umb-user-details-details__section-title { + font-size: 17px; + font-weight: bold; + color: @black; + margin-top: 0; + margin-bottom: 15px; +} + +.umb-user-details-details__section-description { + font-size: 12px; + line-height: 1.6em; + margin-bottom: 15px; +} + +.umb-user-details-details__information-item { + margin-bottom: 10px; + font-size: 13px; +} + +.umb-user-details-details__information-item-label { + color: @black; + font-weight: bold; +} + +.umb-user-details-details__information-item-content { + word-break: break-word; +} + diff --git a/src/Umbraco.Web.UI.Client/src/less/forms.less b/src/Umbraco.Web.UI.Client/src/less/forms.less index 6940987290..e3350b4956 100644 --- a/src/Umbraco.Web.UI.Client/src/less/forms.less +++ b/src/Umbraco.Web.UI.Client/src/less/forms.less @@ -580,7 +580,7 @@ div.help { text-align: center; text-shadow: 0 1px 0 @white; background-color: @gray-10; - border: 1px solid @gray-8; + border: 1px solid @purple-l3; } .add-on, .btn, diff --git a/src/Umbraco.Web.UI.Client/src/less/forms/umb-validation-label.less b/src/Umbraco.Web.UI.Client/src/less/forms/umb-validation-label.less index a57748c35e..417447c3dc 100644 --- a/src/Umbraco.Web.UI.Client/src/less/forms/umb-validation-label.less +++ b/src/Umbraco.Web.UI.Client/src/less/forms/umb-validation-label.less @@ -1,5 +1,7 @@ .umb-validation-label { - position: relative; + position: absolute; + top: 27px; + width: 200px; padding: 1px 5px; background: @red; color: @white; diff --git a/src/Umbraco.Web.UI.Client/src/less/hacks.less b/src/Umbraco.Web.UI.Client/src/less/hacks.less index 22e2eb4566..cd32c64782 100644 --- a/src/Umbraco.Web.UI.Client/src/less/hacks.less +++ b/src/Umbraco.Web.UI.Client/src/less/hacks.less @@ -202,7 +202,7 @@ pre { //font-size: @baseFontSize - 1; // 14px to 13px color: @gray-2; line-height: @baseLineHeight; - white-space: pre-line; // 1 + white-space: pre-wrap; // 1 overflow-x: auto; // 1 background-color: @gray-10; border: 1px solid @gray-8; @@ -222,4 +222,4 @@ pre { background-color: transparent; border: 0; } -} \ No newline at end of file +} diff --git a/src/Umbraco.Web.UI.Client/src/less/legacydialog.less b/src/Umbraco.Web.UI.Client/src/less/legacydialog.less new file mode 100644 index 0000000000..ca920d22c2 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/legacydialog.less @@ -0,0 +1,58 @@ + +.umb-dialog .propertyItemheader { + width: 140px !Important; +} + +.umb-dialog .diffDropdown { + width: 400px; +} + +.umb-dialog .diffPanel { + height: 400px; +} + + +.umb-dialog .diff { + margin-top: 10px; + height: 100%; + overflow: auto; + border-top: 1px solid #ccc; + border-top: 1px solid #ccc; + padding: 5px; +} + +.umb-dialog .diff table { + width: 95%; + max-width: 95%; + margin: 0 3px; +} + +.umb-dialog .diff table th { + padding: 5px; + width: 25%; + border-bottom: 1px solid #ccc; +} + +.umb-dialog .diff table td { + border-bottom: 1px solid #ccc; + padding: 3px; +} + +.umb-dialog .diff del { + background: rgb(255, 230, 230) none repeat scroll 0%; + -moz-background-clip: -moz-initial; + -moz-background-origin: -moz-initial; + -moz-background-inline-policy: -moz-initial; +} + +.umb-dialog .diff ins { + background: rgb(230, 255, 230) none repeat scroll 0%; + -moz-background-clip: -moz-initial; + -moz-background-origin: -moz-initial; + -moz-background-inline-policy: -moz-initial; +} + +.umb-dialog .diff .diffnotice { + text-align: center; + margin-bottom: 10px; +} diff --git a/src/Umbraco.Web.UI.Client/src/less/main.less b/src/Umbraco.Web.UI.Client/src/less/main.less index 0284a79865..12a13e11ed 100644 --- a/src/Umbraco.Web.UI.Client/src/less/main.less +++ b/src/Umbraco.Web.UI.Client/src/less/main.less @@ -23,7 +23,7 @@ margin-top: 0px; margin-bottom: 15px; font-size: 14px; - color: @gray-7; + color: @gray-7; } h5{ @@ -141,7 +141,7 @@ h5.-black { padding-bottom: 0; } -.block-form .umb-control-group label .help-block, +.block-form .umb-control-group label .help-block, .block-form .umb-control-group label small { font-size: 13px; padding-top: 2px; @@ -250,9 +250,9 @@ label:not([for]) { } .umb-version { - color: @gray-7; - position: absolute; - bottom: 5px; + color: @gray-7; + position: absolute; + bottom: 5px; right: 20px; } @@ -660,3 +660,15 @@ input[type=checkbox]:checked + .input-label--small { background-color: @green-l3; text-decoration: none; } + +.visuallyhidden{ + position: absolute !important; + clip: rect(1px 1px 1px 1px); /* IE6, IE7 */ + clip: rect(1px, 1px, 1px, 1px); + padding:0 !important; + border:0 !important; + height: 1px !important; + width: 1px !important; + overflow: hidden; +} + diff --git a/src/Umbraco.Web.UI.Client/src/less/panel.less b/src/Umbraco.Web.UI.Client/src/less/panel.less index def6d114ff..3d9df196d7 100644 --- a/src/Umbraco.Web.UI.Client/src/less/panel.less +++ b/src/Umbraco.Web.UI.Client/src/less/panel.less @@ -16,7 +16,7 @@ .umb-panel-header { background: @gray-10; - border-bottom: 1px solid @gray-8; + border-bottom: 1px solid @purple-l3; position: absolute; height: 99px; top: 0px; @@ -43,15 +43,25 @@ } .umb-mediapicker-upload { - display: -ms-flexbox; - display: -webkit-box; - display: -webkit-flex; display: flex; .form-search { - -webkit-flex: 1; - -ms-flex: 1; flex: 1; + + &__toggle{ + margin: 10px 0; + display: flex; + align-items: center; + + input { + margin: 0; + } + + label { + margin-left: 5px; + margin-bottom: 0; + } + } } .upload-button { diff --git a/src/Umbraco.Web.UI.Client/src/less/property-editors.less b/src/Umbraco.Web.UI.Client/src/less/property-editors.less index c94db10d06..5c436bd27c 100644 --- a/src/Umbraco.Web.UI.Client/src/less/property-editors.less +++ b/src/Umbraco.Web.UI.Client/src/less/property-editors.less @@ -2,7 +2,12 @@ // Container styles // -------------------------------------------------- .umb-property-editor { - min-width:66.6%; + @media (max-width: 800px) { + width: 100%; + } + @media (min-width: 800px) { + min-width:66.6%; + } &-pull { float:left; @@ -98,6 +103,18 @@ } +// +// RTE +// -------------------------------------------------- +.umb-rte { + position: relative; + + .-loading { + position: absolute; + } +} + + /* pre-value editor */ .rte-editor-preval .control-group .controls > div > label .mce-ico { line-height: 20px; } @@ -109,11 +126,15 @@ /* pre-value editor */ .control-group.color-picker-preval { .thumbnail { - width: 36px; + width: 34px; + height: 34px; min-width: auto; border: none; cursor: move; border-radius: 3px; + margin-top: auto; + margin-bottom: auto; + flex: 0 0 auto; } .handle { @@ -125,19 +146,19 @@ div.color-picker-prediv { display: inline-flex; align-items: center; - max-width: 85%; + max-width: 100%; + flex: 1; pre { display: inline-flex; font-family: monospace; - margin-right: 10px; - margin-left: 10px; + margin-left: 15px; + margin-right: 15px; white-space: nowrap; overflow: hidden; margin-bottom: 0; vertical-align: middle; - padding-top: 7px; - padding-bottom: 7px; + padding: 6px 10px; background: #f7f7f7; flex: 0 0 auto; } @@ -165,16 +186,59 @@ } label { - border: 1px solid #fff; - padding: 7px 10px; + border: 1px solid @white; + padding: 6px 10px; font-family: monospace; border: 1px solid #dfdfe1; background: #f7f7f7; - margin: 0 15px 0 0; + margin: 0 15px 0 3px; border-radius: 3px; } } +// +// Image Cropper +// -------------------------------------------------- + +.umb-prevalues-multivalues.umb-cropsizes{ + + max-width: 500px; + width: 100%; + min-width: 66.6%; + + @media (min-width: 1101px) and (max-width: 1300px), (max-width: 930px) { + max-width: none; + } + + .umb-overlay__form & { + width: 100%; + } +} + +.umb-cropsizes { + + &__add { + display: inline-flex; + align-items: center; + } + + &__controls { + margin: 24px 0 0; + display: flex; + } + + &__input { + width: 100%; + &-wrap{ + flex: 1 1 auto; + margin-right: 10px; + &--narrow { + flex: 0 1 100px; + } + } + } +} + // // Media picker @@ -197,6 +261,18 @@ } } +.umb-mediapicker .label{ + &__trashed{ + background-color: @red; + position: absolute; + top: 50%; + left: 50%; + z-index: 1; + transform: translate3d(-50%,-50%,0); + margin: 0; + } +} + .umb-mediapicker .picked-image { position: absolute; bottom: 10px; @@ -277,7 +353,7 @@ .umb-mediapicker .umb-sortable-thumbnails li { flex-direction: column; - margin: 0 5px 5px 0; + margin: 0 0 5px 5px; padding: 5px; } @@ -295,7 +371,7 @@ background-image: url(../img/checkered-background.png); } -.umb-sortable-thumbnails li img.trashed { +.umb-sortable-thumbnails li .trashed { opacity:0.3; } @@ -546,6 +622,16 @@ } } + .imagecropper .umb-cropper__container .button-drawer { + display: flex; + justify-content: flex-end; + padding: 10px; + + button { + margin-left: 4px; + } + } + .umb-close-cropper { position: absolute; top: 3px; @@ -652,7 +738,7 @@ .umb-fileupload ul { list-style: none; vertical-align: middle; - margin-bottom: 0px; + margin-bottom: 0; } .umb-fileupload label { @@ -810,7 +896,20 @@ // // Nested boolean (e.g. list view bulk action permissions) -// ---------------------=====----------------------------- +// ------------------------------------------------------- .umb-nested-boolean label {margin-bottom: 8px; float: left; width: 320px;} .umb-nested-boolean label span {float: left; width: 80%;} .umb-nested-boolean label input[type='checkbox'] {margin-right: 10px; float: left;} + +// +// Custom styles of property editors in property preview in document type editor +// ----------------------------------------------------------------------------- +.umb-group-builder__property-preview { + .umb-property-editor { + .slider { + .tooltip { + display: none; + } + } + } +} diff --git a/src/Umbraco.Web.UI.Client/src/less/utilities/theme/_opacity.less b/src/Umbraco.Web.UI.Client/src/less/utilities/theme/_opacity.less new file mode 100644 index 0000000000..4550827cdc --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/utilities/theme/_opacity.less @@ -0,0 +1,19 @@ +/* + + Opacity + +*/ + +.o-100 { opacity: 1; } +.o-90 { opacity: 0.9; } +.o-80 { opacity: 0.8; } +.o-70 { opacity: 0.7; } +.o-60 { opacity: 0.6; } +.o-50 { opacity: 0.5; } +.o-40 { opacity: 0.4; } +.o-30 { opacity: 0.3; } +.o-20 { opacity: 0.2; } +.o-10 { opacity: 0.1; } +.o-05 { opacity: 0.05; } +.o-025 { opacity: 0.025; } +.o-0 { opacity: 0; } \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/less/utilities/typography/_white-space.less b/src/Umbraco.Web.UI.Client/src/less/utilities/typography/_white-space.less index b8fb5ca5db..5767b0b474 100644 --- a/src/Umbraco.Web.UI.Client/src/less/utilities/typography/_white-space.less +++ b/src/Umbraco.Web.UI.Client/src/less/utilities/typography/_white-space.less @@ -7,6 +7,8 @@ .ws-normal { white-space: normal; } .nowrap { white-space: nowrap; } .pre { white-space: pre; } +.pre-wrap { white-space: pre-wrap; } +.pre-line { white-space: pre-line; } .truncate { white-space: nowrap; diff --git a/src/Umbraco.Web.UI.Client/src/preview/preview.controller.js b/src/Umbraco.Web.UI.Client/src/preview/preview.controller.js index beb6353bde..d93814d1a7 100644 --- a/src/Umbraco.Web.UI.Client/src/preview/preview.controller.js +++ b/src/Umbraco.Web.UI.Client/src/preview/preview.controller.js @@ -5,7 +5,7 @@ var app = angular.module("umbraco.preview", ['umbraco.resources', 'umbraco.services']) - .controller("previewController", function ($scope, $http, $window, $timeout, $location, dialogService) { + .controller("previewController", function ($scope, $window, $location) { //gets a real query string value function getParameterByName(name, url) { diff --git a/src/Umbraco.Web.UI.Client/src/views/common/authorizeupgrade.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/authorizeupgrade.controller.js index 76a53929de..12803dfd51 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/authorizeupgrade.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/authorizeupgrade.controller.js @@ -10,9 +10,7 @@ */ function AuthorizeUpgradeController($scope, $window) { - //Add this method to the scope - this method will be called by the login dialog controller when the login is successful - // then we'll handle the redirect. - $scope.submit = function (event) { + $scope.loginAndRedirect = function (event) { var qry = $window.location.search.trimStart("?").split("&"); var redir = _.find(qry, function(item) { @@ -24,7 +22,7 @@ function AuthorizeUpgradeController($scope, $window) { else { $window.location = "/"; } - + }; } diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.controller.js deleted file mode 100644 index c965d16c0d..0000000000 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.controller.js +++ /dev/null @@ -1,416 +0,0 @@ -angular.module("umbraco").controller("Umbraco.Dialogs.LoginController", - function ($scope, $cookies, $location, currentUserResource, formHelper, mediaHelper, umbRequestHelper, Upload, localizationService, userService, externalLoginInfo, resetPasswordCodeInfo, $timeout, authResource, dialogService, $q) { - - $scope.invitedUser = null; - $scope.invitedUserPasswordModel = { - password: "", - confirmPassword: "", - buttonState: "", - passwordPolicies: null, - passwordPolicyText: "" - } - $scope.loginStates = { - submitButton: "init" - } - $scope.avatarFile = { - filesHolder: null, - uploadStatus: null, - uploadProgress: 0, - maxFileSize: Umbraco.Sys.ServerVariables.umbracoSettings.maxFileSize + "KB", - acceptedFileTypes: mediaHelper.formatFileTypes(Umbraco.Sys.ServerVariables.umbracoSettings.imageFileTypes), - uploaded: false - } - $scope.togglePassword = function () { - var elem = $("form[name='loginForm'] input[name='password']"); - elem.attr("type", (elem.attr("type") === "text" ? "password" : "text")); - $(".password-text.show, .password-text.hide").toggle(); - } - - function init() { - // Check if it is a new user - var inviteVal = $location.search().invite; - //1 = enter password, 2 = password set, 3 = invalid token - if (inviteVal && (inviteVal === "1" || inviteVal === "2")) { - - $q.all([ - //get the current invite user - authResource.getCurrentInvitedUser().then(function (data) { - $scope.invitedUser = data; - }, - function () { - //it failed so we should remove the search - $location.search('invite', null); - }), - //get the membership provider config for password policies - authResource.getMembershipProviderConfig().then(function (data) { - $scope.invitedUserPasswordModel.passwordPolicies = data; - - //localize the text - localizationService.localize("errorHandling_errorInPasswordFormat", - [ - $scope.invitedUserPasswordModel.passwordPolicies.minPasswordLength, - $scope.invitedUserPasswordModel.passwordPolicies.minNonAlphaNumericChars - ]).then(function (data) { - $scope.invitedUserPasswordModel.passwordPolicyText = data; - }); - }) - ]).then(function () { - - $scope.inviteStep = Number(inviteVal); - - }); - } else if (inviteVal && inviteVal === "3") { - $scope.inviteStep = Number(inviteVal); - } - } - - $scope.changeAvatar = function (files, event) { - if (files && files.length > 0) { - upload(files[0]); - } - }; - - $scope.getStarted = function () { - $location.search('invite', null); - $scope.submit(true); - } - - function upload(file) { - - $scope.avatarFile.uploadProgress = 0; - - Upload.upload({ - url: umbRequestHelper.getApiUrl("currentUserApiBaseUrl", "PostSetAvatar"), - fields: {}, - file: file - }).progress(function (evt) { - - if ($scope.avatarFile.uploadStatus !== "done" && $scope.avatarFile.uploadStatus !== "error") { - // set uploading status on file - $scope.avatarFile.uploadStatus = "uploading"; - - // calculate progress in percentage - var progressPercentage = parseInt(100.0 * evt.loaded / evt.total, 10); - - // set percentage property on file - $scope.avatarFile.uploadProgress = progressPercentage; - } - - }).success(function (data, status, headers, config) { - - $scope.avatarFile.uploadProgress = 100; - - // set done status on file - $scope.avatarFile.uploadStatus = "done"; - - $scope.invitedUser.avatars = data; - - $scope.avatarFile.uploaded = true; - - }).error(function (evt, status, headers, config) { - - // set status done - $scope.avatarFile.uploadStatus = "error"; - - // If file not found, server will return a 404 and display this message - if (status === 404) { - $scope.avatarFile.serverErrorMessage = "File not found"; - } - else if (status == 400) { - //it's a validation error - $scope.avatarFile.serverErrorMessage = evt.message; - } - else { - //it's an unhandled error - //if the service returns a detailed error - if (evt.InnerException) { - $scope.avatarFile.serverErrorMessage = evt.InnerException.ExceptionMessage; - - //Check if its the common "too large file" exception - if (evt.InnerException.StackTrace && evt.InnerException.StackTrace.indexOf("ValidateRequestEntityLength") > 0) { - $scope.avatarFile.serverErrorMessage = "File too large to upload"; - } - - } else if (evt.Message) { - $scope.avatarFile.serverErrorMessage = evt.Message; - } - } - }); - } - - $scope.inviteSavePassword = function () { - - if (formHelper.submitForm({ scope: $scope })) { - - $scope.invitedUserPasswordModel.buttonState = "busy"; - - currentUserResource.performSetInvitedUserPassword($scope.invitedUserPasswordModel.password) - .then(function (data) { - - //success - formHelper.resetForm({ scope: $scope }); - $scope.invitedUserPasswordModel.buttonState = "success"; - //set the user and set them as logged in - $scope.invitedUser = data; - userService.setAuthenticationSuccessful(data); - - $scope.inviteStep = 2; - - }, function (err) { - - //error - formHelper.handleError(err); - - $scope.invitedUserPasswordModel.buttonState = "error"; - - }); - } - }; - - var setFieldFocus = function (form, field) { - $timeout(function () { - $("form[name='" + form + "'] input[name='" + field + "']").focus(); - }); - } - - var twoFactorloginDialog = null; - function show2FALoginDialog(view, callback) { - if (!twoFactorloginDialog) { - twoFactorloginDialog = dialogService.open({ - - //very special flag which means that global events cannot close this dialog - manualClose: true, - template: view, - modalClass: "login-overlay", - animation: "slide", - show: true, - callback: callback - - }); - } - } - - function resetInputValidation() { - $scope.confirmPassword = ""; - $scope.password = ""; - $scope.login = ""; - if ($scope.loginForm) { - $scope.loginForm.username.$setValidity('auth', true); - $scope.loginForm.password.$setValidity('auth', true); - } - if ($scope.requestPasswordResetForm) { - $scope.requestPasswordResetForm.email.$setValidity("auth", true); - } - if ($scope.setPasswordForm) { - $scope.setPasswordForm.password.$setValidity('auth', true); - $scope.setPasswordForm.confirmPassword.$setValidity('auth', true); - } - } - - $scope.allowPasswordReset = Umbraco.Sys.ServerVariables.umbracoSettings.allowPasswordReset; - - $scope.showLogin = function () { - $scope.errorMsg = ""; - resetInputValidation(); - $scope.view = "login"; - setFieldFocus("loginForm", "username"); - } - - $scope.showRequestPasswordReset = function () { - $scope.errorMsg = ""; - resetInputValidation(); - $scope.view = "request-password-reset"; - $scope.showEmailResetConfirmation = false; - setFieldFocus("requestPasswordResetForm", "email"); - } - - $scope.showSetPassword = function () { - $scope.errorMsg = ""; - resetInputValidation(); - $scope.view = "set-password"; - setFieldFocus("setPasswordForm", "password"); - } - - var d = new Date(); - var konamiGreetings = new Array("Suze Sunday", "Malibu Monday", "Tequila Tuesday", "Whiskey Wednesday", "Negroni Day", "Fernet Friday", "Sancerre Saturday"); - var konamiMode = $cookies.konamiLogin; - if (konamiMode == "1") { - $scope.greeting = "Happy " + konamiGreetings[d.getDay()]; - } else { - localizationService.localize("login_greeting" + d.getDay()).then(function (label) { - $scope.greeting = label; - }); // weekday[d.getDay()]; - } - $scope.errorMsg = ""; - - $scope.externalLoginFormAction = Umbraco.Sys.ServerVariables.umbracoUrls.externalLoginsUrl; - $scope.externalLoginProviders = externalLoginInfo.providers; - $scope.externalLoginInfo = externalLoginInfo; - $scope.resetPasswordCodeInfo = resetPasswordCodeInfo; - $scope.backgroundImage = Umbraco.Sys.ServerVariables.umbracoSettings.loginBackgroundImage; - - $scope.activateKonamiMode = function () { - if ($cookies.konamiLogin == "1") { - // somehow I can't update the cookie value using $cookies, so going native - document.cookie = "konamiLogin=; expires=Thu, 01 Jan 1970 00:00:01 GMT;"; - document.location.reload(); - } else { - document.cookie = "konamiLogin=1; expires=Tue, 01 Jan 2030 00:00:01 GMT;"; - $scope.$apply(function () { - $scope.greeting = "Happy " + konamiGreetings[d.getDay()]; - }); - } - } - - $scope.loginSubmit = function (login, password) { - - //TODO: Do validation properly like in the invite password update - - //if the login and password are not empty we need to automatically - // validate them - this is because if there are validation errors on the server - // then the user has to change both username & password to resubmit which isn't ideal, - // so if they're not empty, we'll just make sure to set them to valid. - if (login && password && login.length > 0 && password.length > 0) { - $scope.loginForm.username.$setValidity('auth', true); - $scope.loginForm.password.$setValidity('auth', true); - } - - if ($scope.loginForm.$invalid) { - return; - } - - $scope.loginStates.submitButton = "busy"; - - userService.authenticate(login, password) - .then(function (data) { - $scope.loginStates.submitButton = "success"; - $scope.submit(true); - }, - function (reason) { - - //is Two Factor required? - if (reason.status === 402) { - $scope.errorMsg = "Additional authentication required"; - show2FALoginDialog(reason.data.twoFactorView, $scope.submit); - } - else { - $scope.loginStates.submitButton = "error"; - $scope.errorMsg = reason.errorMsg; - - //set the form inputs to invalid - $scope.loginForm.username.$setValidity("auth", false); - $scope.loginForm.password.$setValidity("auth", false); - } - }); - - //setup a watch for both of the model values changing, if they change - // while the form is invalid, then revalidate them so that the form can - // be submitted again. - $scope.loginForm.username.$viewChangeListeners.push(function () { - if ($scope.loginForm.$invalid) { - $scope.loginForm.username.$setValidity('auth', true); - $scope.loginForm.password.$setValidity('auth', true); - } - }); - $scope.loginForm.password.$viewChangeListeners.push(function () { - if ($scope.loginForm.$invalid) { - $scope.loginForm.username.$setValidity('auth', true); - $scope.loginForm.password.$setValidity('auth', true); - } - }); - }; - - $scope.requestPasswordResetSubmit = function (email) { - - //TODO: Do validation properly like in the invite password update - - if (email && email.length > 0) { - $scope.requestPasswordResetForm.email.$setValidity('auth', true); - } - - $scope.showEmailResetConfirmation = false; - - if ($scope.requestPasswordResetForm.$invalid) { - return; - } - - $scope.errorMsg = ""; - - authResource.performRequestPasswordReset(email) - .then(function () { - //remove the email entered - $scope.email = ""; - $scope.showEmailResetConfirmation = true; - }, function (reason) { - $scope.errorMsg = reason.errorMsg; - $scope.requestPasswordResetForm.email.$setValidity("auth", false); - }); - - $scope.requestPasswordResetForm.email.$viewChangeListeners.push(function () { - if ($scope.requestPasswordResetForm.email.$invalid) { - $scope.requestPasswordResetForm.email.$setValidity('auth', true); - } - }); - }; - - $scope.setPasswordSubmit = function (password, confirmPassword) { - - $scope.showSetPasswordConfirmation = false; - - if (password && confirmPassword && password.length > 0 && confirmPassword.length > 0) { - $scope.setPasswordForm.password.$setValidity('auth', true); - $scope.setPasswordForm.confirmPassword.$setValidity('auth', true); - } - - if ($scope.setPasswordForm.$invalid) { - return; - } - - //TODO: All of this logic can/should be shared! We should do validation the nice way instead of all of this manual stuff, see: inviteSavePassword - authResource.performSetPassword($scope.resetPasswordCodeInfo.resetCodeModel.userId, password, confirmPassword, $scope.resetPasswordCodeInfo.resetCodeModel.resetCode) - .then(function () { - $scope.showSetPasswordConfirmation = true; - $scope.resetComplete = true; - - //reset the values in the resetPasswordCodeInfo angular so if someone logs out the change password isn't shown again - resetPasswordCodeInfo.resetCodeModel = null; - - }, function (reason) { - if (reason.data && reason.data.Message) { - $scope.errorMsg = reason.data.Message; - } - else { - $scope.errorMsg = reason.errorMsg; - } - $scope.setPasswordForm.password.$setValidity("auth", false); - $scope.setPasswordForm.confirmPassword.$setValidity("auth", false); - }); - - $scope.setPasswordForm.password.$viewChangeListeners.push(function () { - if ($scope.setPasswordForm.password.$invalid) { - $scope.setPasswordForm.password.$setValidity('auth', true); - } - }); - $scope.setPasswordForm.confirmPassword.$viewChangeListeners.push(function () { - if ($scope.setPasswordForm.confirmPassword.$invalid) { - $scope.setPasswordForm.confirmPassword.$setValidity('auth', true); - } - }); - } - - - //Now, show the correct panel: - - if ($scope.resetPasswordCodeInfo.resetCodeModel) { - $scope.showSetPassword(); - } - else if ($scope.resetPasswordCodeInfo.errors.length > 0) { - $scope.view = "password-reset-code-expired"; - } - else { - $scope.showLogin(); - } - - init(); - - }); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/ysod.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/ysod.controller.js deleted file mode 100644 index 46abf5c88a..0000000000 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/ysod.controller.js +++ /dev/null @@ -1,22 +0,0 @@ -/** - * @ngdoc controller - * @name Umbraco.Dialogs.LegacyDeleteController - * @function - * - * @description - * The controller for deleting content - */ -function YsodController($scope, legacyResource, treeService, navigationService) { - - if ($scope.error && $scope.error.data && $scope.error.data.StackTrace) { - //trim whitespace - $scope.error.data.StackTrace = $scope.error.data.StackTrace.trim(); - } - - $scope.closeDialog = function() { - $scope.dismiss(); - }; - -} - -angular.module("umbraco").controller("Umbraco.Dialogs.YsodController", YsodController); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/ysod.html b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/ysod.html deleted file mode 100644 index 1734867945..0000000000 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/ysod.html +++ /dev/null @@ -1,28 +0,0 @@ -
    - - - - - \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/iconpicker/iconpicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/iconpicker/iconpicker.html index 8a7358116a..c0ca65de9c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/iconpicker/iconpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/iconpicker/iconpicker.html @@ -1,6 +1,6 @@ -
    @@ -30,17 +30,18 @@ no-dirty-check />
    - +
    -
    - + - +
    • @@ -50,7 +51,7 @@
    - + diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/insertcodesnippet/insertcodesnippet.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/insertcodesnippet/insertcodesnippet.html index 58b422ceb2..2ccbf11cc1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/insertcodesnippet/insertcodesnippet.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/insertcodesnippet/insertcodesnippet.html @@ -16,24 +16,23 @@
    -
    +
    ...
    -
    +
    ...
    -
    +
    ...
    -
    -
    +
    ...
    diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.controller.js index 9b563be2df..fde8d23187 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.controller.js @@ -151,7 +151,7 @@ angular.module("umbraco") if (folder.id > 0) { entityResource.getAncestors(folder.id, "media") - .then(function(anc) { + .then(function(anc) { $scope.path = _.filter(anc, function(f) { return f.path.indexOf($scope.startNodeId) !== -1; @@ -236,7 +236,10 @@ angular.module("umbraco") $scope.onUploadComplete = function(files) { $scope.gotoFolder($scope.currentFolder).then(function() { if (files.length === 1 && $scope.model.selectedImages.length === 0) { - selectImage($scope.images[$scope.images.length - 1]); + var image = $scope.images[$scope.images.length - 1]; + $scope.target = image; + $scope.target.url = mediaHelper.resolveFile(image); + selectImage(image); } }); }; @@ -305,6 +308,11 @@ angular.module("umbraco") debounceSearchMedia(); }; + $scope.toggle = function() { + // Make sure to activate the changeSearch function everytime the toggle is clicked + $scope.changeSearch(); + } + $scope.changePagination = function(pageNumber) { $scope.loading = true; $scope.searchOptions.pageNumber = pageNumber; diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.html index 5da564d41f..5046088d28 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.html @@ -30,12 +30,13 @@ ng-change="changeSearch()" type="text" no-dirty-check /> -
    - + +
    + + +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/rollback/rollback.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/rollback/rollback.controller.js new file mode 100644 index 0000000000..6b8462b583 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/rollback/rollback.controller.js @@ -0,0 +1,177 @@ +(function () { + "use strict"; + + function RollbackController($scope, contentResource, localizationService, assetsService) { + + var vm = this; + + vm.rollback = rollback; + vm.changeLanguage = changeLanguage; + vm.changeVersion = changeVersion; + vm.submit = submit; + vm.close = close; + + ////////// + + function onInit() { + + vm.loading = true; + vm.variantVersions = []; + vm.diff = null; + vm.currentVersion = null; + vm.rollbackButtonDisabled = true; + + // find the current version for invariant nodes + if($scope.model.node.variants.length === 1) { + vm.currentVersion = $scope.model.node.variants[0]; + } + + // find the current version for nodes with variants + if($scope.model.node.variants.length > 1) { + var active = _.find($scope.model.node.variants, function (v) { + return v.active; + }); + + // preselect the language in the dropdown + if(active) { + vm.selectedLanguage = active; + vm.currentVersion = active; + } + } + + // set default title + if(!$scope.model.title) { + localizationService.localize("actions_rollback").then(function(value){ + $scope.model.title = value; + }); + } + + // Load in diff library + assetsService.loadJs('lib/jsdiff/diff.min.js', $scope).then(function () { + + getVersions().then(function(){ + vm.loading = false; + }); + + }); + + } + + function changeLanguage(language) { + vm.currentVersion = language; + getVersions(); + } + + function changeVersion(version) { + + if(version && version.versionId) { + + const culture = $scope.model.node.variants.length > 1 ? vm.currentVersion.language.culture : null; + + contentResource.getRollbackVersion(version.versionId, culture) + .then(function(data){ + vm.previousVersion = data; + vm.previousVersion.versionId = version.versionId; + createDiff(vm.currentVersion, vm.previousVersion); + vm.rollbackButtonDisabled = false; + }); + + } else { + vm.diff = null; + vm.rollbackButtonDisabled = true; + } + } + + function getVersions() { + + const nodeId = $scope.model.node.id; + const culture = $scope.model.node.variants.length > 1 ? vm.currentVersion.language.culture : null; + + return contentResource.getRollbackVersions(nodeId, culture) + .then(function(data){ + vm.previousVersions = data.map(version => { + version.displayValue = version.versionDate + " - " + version.versionAuthorName; + return version; + }); + }); + } + + /** + * This will load in a new version + */ + function createDiff(currentVersion, previousVersion) { + + vm.diff = {}; + vm.diff.properties = []; + + // find diff in name + vm.diff.name = JsDiff.diffWords(currentVersion.name, previousVersion.name); + + // extract all properties from the tabs and create new object for the diff + currentVersion.tabs.forEach((tab, tabIndex) => { + tab.properties.forEach((property, propertyIndex) => { + var oldProperty = previousVersion.tabs[tabIndex].properties[propertyIndex]; + + // we have to make properties storing values as object into strings (Grid, nested content, etc.) + if(property.value instanceof Object) { + property.value = JSON.stringify(property.value, null, 1); + property.isObject = true; + } + + if(oldProperty.value instanceof Object) { + oldProperty.value = JSON.stringify(oldProperty.value, null, 1); + oldProperty.isObject = true; + } + + // create new property object used in the diff table + var diffProperty = { + "alias": property.alias, + "label": property.label, + "diff": (property.value || oldProperty.value) ? JsDiff.diffWords(property.value, oldProperty.value) : "", + "isObject": (property.isObject || oldProperty.isObject) ? true : false + }; + + vm.diff.properties.push(diffProperty); + + }); + }); + + } + + function rollback() { + + vm.rollbackButtonState = "busy"; + + const nodeId = $scope.model.node.id; + const versionId = vm.previousVersion.versionId; + const culture = $scope.model.node.variants.length > 1 ? vm.currentVersion.language.culture : null; + + return contentResource.rollback(nodeId, versionId, culture) + .then(data => { + vm.rollbackButtonState = "success"; + submit(); + }, error => { + vm.rollbackButtonState = "error"; + }); + + } + + function submit() { + if($scope.model.submit) { + $scope.model.submit($scope.model.submit); + } + } + + function close() { + if($scope.model.close) { + $scope.model.close(); + } + } + + onInit(); + + } + + angular.module("umbraco").controller("Umbraco.Editors.RollbackController", RollbackController); + +})(); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/rollback/rollback.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/rollback/rollback.html new file mode 100644 index 0000000000..f7ab78b7a0 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/rollback/rollback.html @@ -0,0 +1,104 @@ +
    + + + + + + + + + + + + + + +
    +
    + +
    + +
    + +
    +

    {{vm.currentVersion.name}} (Created: {{vm.currentVersion.createDate}})

    + +
    + +
    + +
    + +
    Changes
    + + + + + + + + + + + + + +
    Name + + {{part.value}} + {{part.value}} + {{part.value}} + +
    {{property.label}} + + {{part.value}} + {{part.value}} + {{part.value}} + +
    + +
    + +
    +
    +
    + + + + + + + + + + +
    + +
    \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/treepicker/treepicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/treepicker/treepicker.controller.js index 5b5de1b393..002b617f84 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/treepicker/treepicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/treepicker/treepicker.controller.js @@ -376,7 +376,7 @@ angular.module("umbraco").controller("Umbraco.Editors.TreePickerController", var foundIndex = 0; if ($scope.model.selection.length > 0) { - for (i = 0; $scope.model.selection.length > i; i++) { + for (var i = 0; $scope.model.selection.length > i; i++) { var selectedItem = $scope.model.selection[i]; if (selectedItem.id === item.id) { found = true; diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.controller.js index fa7a797125..827b2ad4e0 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.controller.js @@ -285,7 +285,7 @@ angular.module("umbraco").controller("Umbraco.Overlays.TreePickerController", var foundIndex = 0; if ($scope.model.selection.length > 0) { - for (i = 0; $scope.model.selection.length > i; i++) { + for (var i = 0; $scope.model.selection.length > i; i++) { var selectedItem = $scope.model.selection[i]; if (selectedItem.id === item.id) { found = true; diff --git a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-contextmenu.html b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-contextmenu.html index 32dd57ade3..2e7048eefa 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-contextmenu.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-contextmenu.html @@ -1,11 +1,11 @@ - diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-login.html similarity index 54% rename from src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html rename to src/Umbraco.Web.UI.Client/src/views/components/application/umb-login.html index eab74994e6..c5167ba964 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-login.html @@ -1,38 +1,38 @@ -
    +