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)
[](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
[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