Merge branch 'v8/dev' into Clean-up-members-reset-password

This commit is contained in:
elitsa
2019-10-25 13:30:57 +02:00
337 changed files with 8114 additions and 5763 deletions
+2 -2
View File
@@ -1,4 +1,4 @@
# Umbraco Cms Build
# Umbraco CMS Build
## Are you sure?
@@ -66,7 +66,7 @@ The Visual Studio object is `null` when Visual Studio has not been detected (eg
* `Path`: Visual Studio installation path (eg some place under `Program Files`)
* `Major`: Visual Studio major version (eg `15` for VS 2017)
* `Minor`: Visual Studio minor version
* `MsBUild`: the absolute path to the MsBuild executable
* `MsBuild`: the absolute path to the MsBuild executable
#### GetUmbracoVersion
+1 -1
View File
@@ -29,4 +29,4 @@ Don't rest on your laurels and never accept the status quo. Contribute and give
## Friendly
Dont judge upon mistakes made but rather upon the speed and quality with which mistakes are corrected. Friendly posts and contributions generate smiles and builds long lasting relationships.
Dont judge upon mistakes made but rather upon the speed and quality with which mistakes are corrected. Friendly posts and contributions generate smiles and build long lasting relationships.
+33 -25
View File
@@ -38,11 +38,11 @@ This document gives you a quick overview on how to get started.
### 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 valuable time.
Not all changes are wanted, so on occasion 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 valuable 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, so we can ensure that you don't put all your hard work into something we would not be able to merge.
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.
Remember, it is always worth working on an issue from the `Up for grabs` list or even asking for some feedback before you send us a PR. This way, your PR will not be closed as unwanted.
### What can I start with?
@@ -64,32 +64,32 @@ Great question! The short version goes like this:
* **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! 🎉 **Important:** 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`. When you have a branch, commit your changes. Don't commit to `v8/dev`, create a new branch first.
* **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.
* **Create pull request** - exciting! You're ready to show us your changes (or not quite ready, you just need some feedback to progress - you can now make use of GitHub's draft pull request status, detailed [here] (https://github.blog/2019-02-14-introducing-draft-pull-requests/)). 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.
![Create a pull request](img/createpullrequest.png)
### Pull requests
The most successful pull requests usually look a like this:
* Fill in the required template
* Fill in the required template, linking your pull request to an issue on the [issue tracker,](https://github.com/umbraco/Umbraco-CMS/issues) if applicable.
* 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.com/documentation/Reference/) is generated
* Unit tests, while optional, are awesome. Thank you!
* 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.
Again, these are guidelines, not strict requirements. However, the more information that you give to us, the more we have to work with when considering your contributions. Good documentation of a pull request can really speed up the time it takes to review and merge your work!
## Reviews
You've sent us your first contribution, congratulations! Now what?
You've sent us your first contribution - congratulations! Now what?
The [pull request team](#the-pr-team) can now start reviewing your proposed changes and give you feedback on them. If it's not perfect, we'll either fix up what we need or we can request you to make some additional changes.
The [pull request team](#the-pr-team) can now start reviewing your proposed changes and give you feedback on them. If it's not perfect, we'll either fix up what we need or we can request that you make some additional changes.
We have [a process in place which you can read all about](REVIEW_PROCESS.md). The very abbreviated version is:
- Your PR will get a reply within 48 hours
- An in-depth reply will be added within at most 2 weeks
- The PR will be either merged or rejected within at most 4 weeks
- Sometimes it is difficult to meet these timelines and we'll talk to you
- Sometimes it is difficult to meet these timelines and we'll talk to you if this is the case.
### Styleguides
@@ -99,21 +99,21 @@ That said, the Umbraco development team likes to follow the hints that ReSharper
### The PR team
The pull request team consists of a member of Umbraco HQ, [Sebastiaan](https://github.com/nul800sebastiaan), who gets assistance from the following community members
The pull request team consists of one member of Umbraco HQ, [Sebastiaan](https://github.com/nul800sebastiaan), who gets assistance from the following community members who have comitted to volunteering their free time:
- [Anders Bjerner](https://github.com/abjerner)
- [Dave Woestenborghs](https://github.com/dawoe)
- [Emma Burstow](https://github.com/emmaburstow)
- [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.
These wonderful people aim to provide you with a first reply to your PR, review and test out your changes and on occasions, they might ask more questions. If they are happy with your work, they'll let Umbraco HQ know by approving the PR. Hq will have final sign-off and will check the work again before it is merged.
### Questions?
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:
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 question 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.com/forum/contributing-to-umbraco-cms/) forum, the team monitors that one closely
- 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.com/forum/contributing-to-umbraco-cms/) forum. The team monitors that one closely, so one of us will be on hand and ready to point you in the right direction.
## Working with the code
@@ -125,19 +125,19 @@ In order to build the Umbraco source code locally, first make sure you have the
* Node v10+
* npm v6.4.1+
The easiest way to get started is to run `build.ps1` which will build both the backoffice (also known as "Belle") and the Umbraco core. You can then easily start debugging from Visual Studio, or if you need to debug Belle you can run `gulp dev` in `src\Umbraco.Web.UI.Client`. See [this page](BUILD.md) for more details.
The easiest way to get started is to open `src\umbraco.sln` in Visual Studio 2017 (version 15.9.7 or higher, [the community edition is free](https://www.visualstudio.com/thank-you-downloading-visual-studio/?sku=Community&rel=15) for you to use to contribute to Open Source projects). In Visual Studio, find the Task Runner Explorer (in the View menu under Other Windows) and run the build task under the gulpfile.
Alternatively, you can open `src\umbraco.sln` in Visual Studio 2017 (version 15.9.7 or higher, [the community edition is free](https://www.visualstudio.com/thank-you-downloading-visual-studio/?sku=Community&rel=15) for you to use to contribute to Open Source projects). In Visual Studio, find the Task Runner Explorer (in the View menu under Other Windows) and run the build task under the gulpfile.
Alternatively, you can run `build.ps1` from the Powershell command line, which will build both the backoffice (also known as "Belle") and the Umbraco core. You can then easily start debugging from Visual Studio, or if you need to debug Belle you can run `gulp dev` in `src\Umbraco.Web.UI.Client`. See [this page](BUILD.md) for more details.
![Gulp build in Visual Studio](img/gulpbuild.png)
After this build completes, you should be able to hit `F5` in Visual Studio to build and run the project. A IISExpress webserver will start and the Umbraco installer will pop up in your browser, follow the directions there to get a working Umbraco install up and running.
After this build completes, you should be able to hit `F5` in Visual Studio to build and run the project. A IISExpress webserver will start and the Umbraco installer will pop up in your browser. Follow the directions there to get a working Umbraco install up and running.
### Working with the source code
Some parts of our source code are over 10 years old now. And when we say "old", we mean "mature" of course!
There's two big areas that you should know about:
There are two big areas that you should know about:
1. The Umbraco backoffice is a extensible AngularJS app and requires you to run a `gulp dev` command while you're working with it, so changes are copied over to the appropriate directories and you can refresh your browser to view the results of your changes.
You may need to run the following commands to set up gulp properly:
@@ -146,20 +146,24 @@ There's two big areas that you should know about:
npm install
npm run build
```
The caching for the back office has been described as 'aggressive' so we often find it's best when making back office changes to disable caching in the browser to help you to see the changes you're making.
2. "The rest" is a C# based codebase, which is mostly ASP.NET MVC based. You can make changes, build them in Visual Studio, and hit `F5` to see the result.
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.
To find the general areas for 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.com/apidocs/ui/#/api) (to be found in `src\Umbraco.Web.UI.Client\src`)
* [The C# application](https://our.umbraco.com/apidocs/csharp/)
### Which branch should I target for my contributions?
We like to use [Gitflow as much as possible](https://jeffkreeftmeijer.com/git-flow/), don't worry if you are not familiar with it. The most important thing you need to know is that when you fork the Umbraco repository, the default branch is set to something, usually `v8/dev`. Whatever the default is, that's where we'd like you to target your contributions.
We like to use [Gitflow as much as possible](https://jeffkreeftmeijer.com/git-flow/), but don't worry if you are not familiar with it. The most important thing you need to know is that when you fork the Umbraco repository, the default branch is set to something, usually `v8/dev`. If you are working on v8, this is the branch you should be targetting. For v7 contributions, please target 'v7/dev'.
Please note: we are no longer accepting features for v7 but will continue to merge bug fixes as and when they arise.
![Which branch should I target?](img/defaultbranch.png)
### Making changes after the PR was opened
### Making changes after the PR is open
If you make the corrections we ask for in the same branch and push them to your fork again, the pull request automatically updates with the additional commit(s) so we can review it again. If all is well, we'll merge the code and your commits are forever part of Umbraco!
@@ -167,9 +171,9 @@ If you make the corrections we ask for in the same branch and push them to your
We recommend you sync with our repository before you submit your pull request. That way, you can fix any potential merge conflicts and make our lives a little bit easier.
Also, if you've submitted a pull request three weeks ago and want to work on something new, you'll want to get the latest code to build against of course.
Also, if you submitted a pull request three weeks ago and want to work on something new, you'll want to get the latest code to build against of course.
To sync your fork with this original one, you'll have to add the upstream url, you only have to do this once:
To sync your fork with this original one, you'll have to add the upstream url. You only have to do this once:
```
git remote add upstream https://github.com/umbraco/Umbraco-CMS.git
@@ -185,3 +189,7 @@ git rebase upstream/v8/dev
In this command we're syncing with the `v8/dev` branch, but you can of course choose another one if needed.
(More info on how this works: [http://robots.thoughtbot.com/post/5133345960/keeping-a-git-fork-updated](http://robots.thoughtbot.com/post/5133345960/keeping-a-git-fork-updated))
### And finally
We welcome all kinds of contributions to this repository. If you don't feel you'd like to make code changes here, you can visit our [documentation repository](https://github.com/umbraco/UmbracoDocs) and use your experience to contribute to making the docs we have, even better. We also encourage community members to feel free to comment on others' pull requests and issues - the expertise we have is not limited to the PR team and HQ. So, if you see something on the issue tracker or pull requests you feel you can add to, please don't be shy.
+2 -2
View File
@@ -13,7 +13,7 @@ Were usually able to handle small PRs pretty quickly. A community volunteer w
Umbraco HQ will regularly mark newly created issues on the issue tracker with the `Up for grabs` tag. This means that the proposed changes are wanted in Umbraco but the HQ does not have the time to make them at this time. We encourage anyone to pick them up and help out.
If you do start working on something, make sure leave a small comment on the issue saying something like: "I'm working on this". That way other people stumbling upon the issue know they don't need to pick it up, someone already has.
If you do start working on something, make sure to leave a small comment on the issue saying something like: "I'm working on this". That way other people stumbling upon the issue know they don't need to pick it up, someone already has.
## Large PRs
New features and large refactorings - can be recognized by seeing a large number of changes, plenty of new files, updates to package manager files (NuGets packages.config, NPMs packages.json, etc.).
@@ -30,6 +30,6 @@ It is highly recommended that you speak to the HQ before making large, complex c
### Pull request or package?
If it doesnt fit in CMS right now, we will likely encourage you to make it into a package instead. A package is a great way to check out popularity of a feature, learn how people use it, validate good usability and to fix bugs.
If it doesnt fit in CMS right now, we will likely encourage you to make it into a package instead. A package is a great way to check out popularity of a feature, learn how people use it, validate good usability and fix bugs.
Eventually, a package could "graduate" to be included in the CMS.
+1 -1
View File
@@ -1,4 +1,4 @@
# [Umbraco CMS](https://umbraco.com) · [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](../LICENSE.md) [![Build status](https://umbraco.visualstudio.com/Umbraco%20Cms/_apis/build/status/Cms%208%20Continuous?branchName=v8/dev)](https://umbraco.visualstudio.com/Umbraco%20Cms/_build?definitionId=75) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](CONTRIBUTING.md) [![pullreminders](https://pullreminders.com/badge.svg)](https://pullreminders.com?ref=badge)
# [Umbraco CMS](https://umbraco.com) · [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](../LICENSE.md) [![Build status](https://umbraco.visualstudio.com/Umbraco%20Cms/_apis/build/status/Cms%208%20Continuous?branchName=v8/dev)](https://umbraco.visualstudio.com/Umbraco%20Cms/_build?definitionId=75) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](CONTRIBUTING.md) [![Twitter](https://img.shields.io/twitter/follow/umbraco.svg?style=social&label=Follow)](https://twitter.com/intent/follow?screen_name=umbraco)
Umbraco is the friendliest, most flexible and fastest growing ASP.NET CMS, and used by more than 500,000 websites worldwide. Our mission is to help you deliver delightful digital experiences by making Umbraco friendly, simpler and social.
-1
View File
@@ -30,7 +30,6 @@
<dependency id="ImageProcessor.Web" version="[4.10.0.100,4.999999)" />
<dependency id="ImageProcessor.Web.Config" version="[2.5.0.100,2.999999)" />
<dependency id="Microsoft.CodeDom.Providers.DotNetCompilerPlatform" version="[2.0.1,2.999999)" />
<dependency id="Microsoft.Net.Compilers" version="[2.10.0,2.999999)" />
</group>
+1 -1
View File
@@ -26,6 +26,6 @@ The following items will now be automatically included when creating a deploy pa
system: umbraco, config\splashes and global.asax.
Please read the release notes on our.umbraco.com:
http://our.umbraco.com/contribute/releases
https://our.umbraco.com/contribute/releases
- Umbraco
+2 -2
View File
@@ -53,7 +53,7 @@
<add name="umbracoBaseRequestModule" xdt:Locator="Match(name)" xdt:Transform="Remove" />
<add name="viewstateMoverModule" xdt:Locator="Match(name)" xdt:Transform="Remove" />
<add name=" UmbracoModule" xdt:Locator="Match(name)" xdt:Transform="Remove" />
<add name="UmbracoModule" type="Umbraco.Web.UmbracoModule,umbraco" xdt:Locator="Match(name)" xdt:Transform="InsertIfMissing" />
<add name="UmbracoModule" type="Umbraco.Web.UmbracoModule,Umbraco.Web" xdt:Locator="Match(name)" xdt:Transform="InsertIfMissing" />
</httpModules>
<httpHandlers xdt:Transform="InsertIfMissing" />
@@ -76,7 +76,7 @@
<!-- Note, we're removing the one that starts with a space here, don't correct it -->
<!-- This to fix a quirk we for a lot of releases where we added it with the space by default -->
<add name=" UmbracoModule" xdt:Locator="Match(name)" xdt:Transform="Remove" />
<add name="UmbracoModule" type="Umbraco.Web.UmbracoModule,umbraco" xdt:Locator="Match(name)" xdt:Transform="InsertIfMissing" />
<add name="UmbracoModule" type="Umbraco.Web.UmbracoModule,Umbraco.Web" xdt:Locator="Match(name)" xdt:Transform="InsertIfMissing" />
</modules>
<staticContent xdt:Transform="InsertIfMissing" />
@@ -6,6 +6,7 @@ namespace Umbraco.Core.Configuration.Grid
public interface IGridEditorConfig
{
string Name { get; }
string NameTemplate { get; }
string Alias { get; }
string View { get; }
string Render { get; }
@@ -6,7 +6,7 @@ namespace Umbraco.Core.Configuration.UmbracoSettings
{
internal class ContentElement : UmbracoConfigurationElement, IContentSection
{
private const string DefaultPreviewBadge = @"<a id=""umbracoPreviewBadge"" style=""z-index:99999; position: absolute; top: 0; right: 0; border: 0; width: 149px; height: 149px; background: url('{0}/assets/img/preview-mode-badge.png') no-repeat;"" href=""#"" OnClick=""javascript:window.top.location.href = '{0}/preview/end?redir={1}'""><span style=""display:none;"">In Preview Mode - click to end</span></a>";
private const string DefaultPreviewBadge = @"<div id=""umbracoPreviewBadge"" class=""umbraco-preview-badge""><span class=""umbraco-preview-badge__header"">Preview mode</span><a href=""{0}/preview/end?redir={1}"" class=""umbraco-preview-badge__end""><svg viewBox=""0 0 100 100"" xmlns=""http://www.w3.org/2000/svg""><title>Click to end</title><path d=""M5273.1 2400.1v-2c0-2.8-5-4-9.7-4s-9.7 1.3-9.7 4v2a7 7 0 002 4.9l5 4.9c.3.3.4.6.4 1v6.4c0 .4.2.7.6.8l2.9.9c.5.1 1-.2 1-.8v-7.2c0-.4.2-.7.4-1l5.1-5a7 7 0 002-4.9zm-9.7-.1c-4.8 0-7.4-1.3-7.5-1.8.1-.5 2.7-1.8 7.5-1.8s7.3 1.3 7.5 1.8c-.2.5-2.7 1.8-7.5 1.8z""/><path d=""M5268.4 2410.3c-.6 0-1 .4-1 1s.4 1 1 1h4.3c.6 0 1-.4 1-1s-.4-1-1-1h-4.3zM5272.7 2413.7h-4.3c-.6 0-1 .4-1 1s.4 1 1 1h4.3c.6 0 1-.4 1-1s-.4-1-1-1zM5272.7 2417h-4.3c-.6 0-1 .4-1 1s.4 1 1 1h4.3c.6 0 1-.4 1-1 0-.5-.4-1-1-1z""/><path d=""M78.2 13l-8.7 11.7a32.5 32.5 0 11-51.9 25.8c0-10.3 4.7-19.7 12.9-25.8L21.8 13a47 47 0 1056.4 0z""/><path d=""M42.7 2.5h14.6v49.4H42.7z""/></svg></a></div><style type=""text/css"">.umbraco-preview-badge {{position: absolute;top: 1em;right: 1em;display: inline-flex;background: #1b264f;color: #fff;padding: 1em;font-size: 12px;z-index: 99999999;justify-content: center;align-items: center;box-shadow: 0 10px 50px rgba(0, 0, 0, .1), 0 6px 20px rgba(0, 0, 0, .16);line-height: 1;}}.umbraco-preview-badge__header {{font-weight: bold;}}.umbraco-preview-badge__end {{width: 3em;padding: 1em;margin: -1em -1em -1em 2em;display: flex;flex-shrink: 0;align-items: center;align-self: stretch;}}.umbraco-preview-badge__end:hover,.umbraco-preview-badge__end:focus {{background: #f5c1bc;}}.umbraco-preview-badge__end svg {{fill: #fff;width:1em;}}</style>";
[ConfigurationProperty("imaging")]
internal ContentImagingElement Imaging => (ContentImagingElement) this["imaging"];
@@ -85,12 +85,7 @@ namespace Umbraco.Core
/// ListView.
/// </summary>
public const string ListView = "Umbraco.ListView";
/// <summary>
/// Macro Container.
/// </summary>
public const string MacroContainer = "Umbraco.MacroContainer";
/// <summary>
/// Media Picker.
/// </summary>
+2 -2
View File
@@ -1,7 +1,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Security.Cryptography;
using System.Threading;
using System.Web.Hosting;
using Umbraco.Core.Logging;
@@ -65,7 +65,7 @@ namespace Umbraco.Core
// a new process for the same application path
var appPath = HostingEnvironment.ApplicationPhysicalPath;
var hash = (appId + ":::" + appPath).ToSHA1();
var hash = (appId + ":::" + appPath).GenerateHash<SHA1>();
var lockName = "UMBRACO-" + hash + "-MAINDOM-LCK";
_asyncLock = new AsyncLock(lockName);
+11 -7
View File
@@ -343,16 +343,20 @@ namespace Umbraco.Core.Mapping
if (ctor == null) return null;
if (_ctors.ContainsKey(sourceType))
_ctors.AddOrUpdate(sourceType, sourceCtor, (k, v) =>
{
// Add missing constructors
foreach (var c in sourceCtor)
{
if (!_ctors[sourceType].TryGetValue(c.Key, out _))
_ctors[sourceType].Add(c.Key, c.Value);
}
}
else
_ctors[sourceType] = sourceCtor;
if (!v.ContainsKey(c.Key))
{
v.Add(c.Key, c.Value);
}
}
return v;
});
return ctor;
}
@@ -227,8 +227,8 @@ namespace Umbraco.Core.Migrations.Install
_database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 32, UniqueId = 32.ToGuid(), DataTypeId = Constants.DataTypes.LabelDateTime, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.LastLockoutDate, Name = Constants.Conventions.Member.LastLockoutDateLabel, SortOrder = 4, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing });
_database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 33, UniqueId = 33.ToGuid(), DataTypeId = Constants.DataTypes.LabelDateTime, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.LastLoginDate, Name = Constants.Conventions.Member.LastLoginDateLabel, SortOrder = 5, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing });
_database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 34, UniqueId = 34.ToGuid(), DataTypeId = Constants.DataTypes.LabelDateTime, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.LastPasswordChangeDate, Name = Constants.Conventions.Member.LastPasswordChangeDateLabel, SortOrder = 6, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing });
_database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 35, UniqueId = 35.ToGuid(), DataTypeId = Constants.DataTypes.LabelDateTime, ContentTypeId = 1044, PropertyTypeGroupId = null, Alias = Constants.Conventions.Member.PasswordQuestion, Name = Constants.Conventions.Member.PasswordQuestionLabel, SortOrder = 7, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte)ContentVariation.Nothing });
_database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 36, UniqueId = 36.ToGuid(), DataTypeId = Constants.DataTypes.LabelDateTime, ContentTypeId = 1044, PropertyTypeGroupId = null, Alias = Constants.Conventions.Member.PasswordAnswer, Name = Constants.Conventions.Member.PasswordAnswerLabel, SortOrder = 8, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte)ContentVariation.Nothing });
_database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 35, UniqueId = 35.ToGuid(), DataTypeId = Constants.DataTypes.LabelDateTime, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.PasswordQuestion, Name = Constants.Conventions.Member.PasswordQuestionLabel, SortOrder = 7, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte)ContentVariation.Nothing });
_database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 36, UniqueId = 36.ToGuid(), DataTypeId = Constants.DataTypes.LabelDateTime, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.PasswordAnswer, Name = Constants.Conventions.Member.PasswordAnswerLabel, SortOrder = 8, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte)ContentVariation.Nothing });
}
@@ -74,9 +74,18 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0
.From<DataTypeDto>()
.Where<DataTypeDto>(x => x.NodeId == group.Key)).First();
// check for duplicate aliases
var aliases = group.Select(x => x.Alias).Where(x => !string.IsNullOrWhiteSpace(x)).ToArray();
if (aliases.Distinct().Count() != aliases.Length)
throw new InvalidOperationException($"Cannot migrate prevalues for datatype id={dataType.NodeId}, editor={dataType.EditorAlias}: duplicate alias.");
// handle null/empty aliases
int index = 0;
var dictionary = group.ToDictionary(x => string.IsNullOrWhiteSpace(x.Alias) ? index++.ToString() : x.Alias);
// migrate the preValues to configuration
var migrator = _preValueMigrators.GetMigrator(dataType.EditorAlias) ?? new DefaultPreValueMigrator();
var config = migrator.GetConfiguration(dataType.NodeId, dataType.EditorAlias, group.ToDictionary(x => x.Alias, x => x));
var config = migrator.GetConfiguration(dataType.NodeId, dataType.EditorAlias, dictionary);
var json = JsonConvert.SerializeObject(config);
// validate - and kill the migration if it fails
@@ -24,8 +24,8 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0.DataTypes
}
// assuming we don't want to fall back to array
if (aliases.Length != preValuesA.Count || aliases.Any(string.IsNullOrWhiteSpace))
throw new InvalidOperationException($"Cannot migrate datatype w/ id={dataTypeId} preValues: duplicate or null/empty alias.");
if (aliases.Any(string.IsNullOrWhiteSpace))
throw new InvalidOperationException($"Cannot migrate prevalues for datatype id={dataTypeId}, editor={editorAlias}: null/empty alias.");
// dictionary-base prevalues
return GetPreValues(preValuesA).ToDictionary(x => x.Alias, GetPreValueValue);
@@ -67,6 +67,12 @@ namespace Umbraco.Core.Models.ContentEditing
/// </remarks>
[DataMember(Name = "active")]
public bool Active { get; set; }
/// <summary>
/// Gets or sets the content app badge.
/// </summary>
[DataMember(Name = "badge")]
public ContentAppBadge Badge { get; set; }
}
}
@@ -0,0 +1,39 @@
namespace Umbraco.Core.Models.ContentEditing
{
using System.Runtime.Serialization;
using Umbraco.Core.Events;
/// <summary>
/// Represents a content app badge
/// </summary>
[DataContract(Name = "badge", Namespace = "")]
public class ContentAppBadge
{
/// <summary>
/// Initializes a new instance of the <see cref="ContentAppBadge"/> class.
/// </summary>
public ContentAppBadge()
{
this.Type = ContentAppBadgeType.Default;
}
/// <summary>
/// Gets or sets the number displayed in the badge
/// </summary>
[DataMember(Name = "count")]
public int Count { get; set; }
/// <summary>
/// Gets or sets the type of badge to display
/// </summary>
/// <remarks>
/// <para>This controls the background color of the badge.</para>
/// <para>Warning will display a dark yellow badge</para>
/// <para>Alert will display a red badge</para>
/// <para>Default will display a turquoise badge</para>
/// </remarks>
[DataMember(Name = "type")]
public ContentAppBadgeType Type { get; set; }
}
}
@@ -0,0 +1,24 @@
namespace Umbraco.Core.Models.ContentEditing
{
using System.Runtime.Serialization;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
/// <summary>
/// Represent the content app badge types
/// </summary>
[DataContract(Name = "contentAppBadgeType")]
[JsonConverter(typeof(StringEnumConverter))]
public enum ContentAppBadgeType
{
[EnumMember(Value = "default")]
Default = 0,
[EnumMember(Value = "warning")]
Warning = 1,
[EnumMember(Value = "alert")]
Alert = 2
}
}
@@ -0,0 +1,7 @@
namespace Umbraco.Core.Models.Entities
{
public interface IMemberEntitySlim : IContentEntitySlim
{
}
}
@@ -0,0 +1,13 @@
namespace Umbraco.Core.Models.Entities
{
public class MemberEntitySlim : EntitySlim, IMemberEntitySlim
{
public string ContentTypeAlias { get; set; }
/// <inheritdoc />
public string ContentTypeIcon { get; set; }
/// <inheritdoc />
public string ContentTypeThumbnail { get; set; }
}
}
+1 -1
View File
@@ -67,7 +67,7 @@ namespace Umbraco.Core.Models
if (user.Avatar.IsNullOrWhiteSpace())
{
var gravatarHash = user.Email.ToMd5();
var gravatarHash = user.Email.GenerateHash<MD5>();
var gravatarUrl = "https://www.gravatar.com/avatar/" + gravatarHash + "?d=404";
//try Gravatar
@@ -1,43 +0,0 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.Runtime.CompilerServices;
namespace Umbraco.Core.Persistence
{
internal static class DatabaseNodeLockExtensions
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void ValidateDatabase(IUmbracoDatabase database)
{
if (database == null)
throw new ArgumentNullException("database");
if (database.GetCurrentTransactionIsolationLevel() < IsolationLevel.RepeatableRead)
throw new InvalidOperationException("A transaction with minimum RepeatableRead isolation level is required.");
}
// updating a record within a repeatable-read transaction gets an exclusive lock on
// that record which will be kept until the transaction is ended, effectively locking
// out all other accesses to that record - thus obtaining an exclusive lock over the
// protected resources.
public static void AcquireLockNodeWriteLock(this IUmbracoDatabase database, int nodeId)
{
ValidateDatabase(database);
database.Execute("UPDATE umbracoLock SET value = (CASE WHEN (value=1) THEN -1 ELSE 1 END) WHERE id=@id",
new { @id = nodeId });
}
// reading a record within a repeatable-read transaction gets a shared lock on
// that record which will be kept until the transaction is ended, effectively preventing
// other write accesses to that record - thus obtaining a shared lock over the protected
// resources.
public static void AcquireLockNodeReadLock(this IUmbracoDatabase database, int nodeId)
{
ValidateDatabase(database);
database.ExecuteScalar<int>("SELECT value FROM umbracoLock WHERE id=@id",
new { @id = nodeId });
}
}
}
@@ -7,5 +7,12 @@ namespace Umbraco.Core.Persistence.Repositories
public interface IDataTypeRepository : IReadWriteQueryRepository<int, IDataType>
{
IEnumerable<MoveEventInfo<IDataType>> Move(IDataType toMove, EntityContainer container);
/// <summary>
/// Returns a dictionary of content type <see cref="Udi"/>s and the property type aliases that use a <see cref="IDataType"/>
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
IReadOnlyDictionary<Udi, IEnumerable<string>> FindUsages(int id);
}
}
@@ -1,4 +1,5 @@
using System;

using System;
using System.Collections.Generic;
using System.Data;
using System.Globalization;
@@ -218,6 +219,7 @@ AND umbracoNode.nodeObjectType = @objectType",
protected void PersistUpdatedBaseContentType(IContentTypeComposition entity)
{
CorrectPropertyTypeVariations(entity);
ValidateVariations(entity);
var dto = ContentTypeFactory.BuildContentTypeDto(entity);
@@ -410,26 +412,7 @@ AND umbracoNode.id <> @id",
// note: this only deals with *local* property types, we're dealing w/compositions later below
foreach (var propertyType in entity.PropertyTypes)
{
if (contentTypeVariationChanging)
{
// content type is changing
switch (newContentTypeVariation)
{
case ContentVariation.Nothing: // changing to Nothing
// all property types must change to Nothing
propertyType.Variations = ContentVariation.Nothing;
break;
case ContentVariation.Culture: // changing to Culture
// all property types can remain Nothing
break;
case ContentVariation.CultureAndSegment:
case ContentVariation.Segment:
default:
throw new NotSupportedException(); // TODO: Support this
}
}
// then, track each property individually
// track each property individually
if (propertyType.IsPropertyDirty("Variations"))
{
// allocate the list only when needed
@@ -455,23 +438,19 @@ AND umbracoNode.id <> @id",
// 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)
propertyTypeVariationChanges = propertyTypeVariationChanges ?? new Dictionary<int, (ContentVariation, ContentVariation)>();
foreach (var composedPropertyType in ((ContentTypeCompositionBase)entity).RawComposedPropertyTypes)
{
if (propertyType.VariesBySegment() || newContentTypeVariation.VariesBySegment())
throw new NotSupportedException(); // TODO: support this
if (composedPropertyType.Variations == ContentVariation.Nothing) continue;
if (propertyType.Variations == ContentVariation.Culture)
{
if (propertyTypeVariationChanges == null)
propertyTypeVariationChanges = new Dictionary<int, (ContentVariation, ContentVariation)>();
// Determine target variation of the composed property type.
// The composed property is only considered culture variant when the base content type is also culture variant.
// The composed property is only considered segment variant when the base content type is also segment variant.
// Example: Culture variant content type with a Culture+Segment variant property type will become ContentVariation.Culture
var target = newContentTypeVariation & composedPropertyType.Variations;
// 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);
}
propertyTypeVariationChanges[composedPropertyType.Id] = (composedPropertyType.Variations, target);
}
}
@@ -512,7 +491,7 @@ AND umbracoNode.id <> @id",
var impacted = GetImpactedContentTypes(entity, all);
// if some property types have actually changed, move their variant data
if (propertyTypeVariationChanges != null)
if (propertyTypeVariationChanges?.Count > 0)
MovePropertyTypeVariantData(propertyTypeVariationChanges, impacted);
// deal with orphan properties: those that were in a deleted tab,
@@ -524,23 +503,40 @@ AND umbracoNode.id <> @id",
CommonRepository.ClearCache(); // always
}
/// <summary>
/// Corrects the property type variations for the given entity
/// to make sure the property type variation is compatible with the
/// variation set on the entity itself.
/// </summary>
/// <param name="entity">Entity to correct properties for</param>
private void CorrectPropertyTypeVariations(IContentTypeComposition entity)
{
// Update property variations based on the content type variation
foreach (var propertyType in entity.PropertyTypes)
{
// Determine variation for the property type.
// The property is only considered culture variant when the base content type is also culture variant.
// The property is only considered segment variant when the base content type is also segment variant.
// Example: Culture variant content type with a Culture+Segment variant property type will become ContentVariation.Culture
propertyType.Variations = entity.Variations & propertyType.Variations;
}
}
/// <summary>
/// Ensures that no property types are flagged for a variance that is not supported by the content type itself
/// </summary>
/// <param name="entity"></param>
/// <param name="entity">The entity for which the property types will be validated</param>
private void ValidateVariations(IContentTypeComposition entity)
{
//if the entity does not vary at all, then the property cannot have a variance value greater than it
if (entity.Variations == ContentVariation.Nothing)
foreach (var prop in entity.PropertyTypes)
{
foreach (var prop in entity.PropertyTypes)
{
if (prop.IsPropertyDirty(nameof(prop.Variations)) && prop.Variations > entity.Variations)
throw new InvalidOperationException($"The property {prop.Alias} cannot have variations of {prop.Variations} with the content type variations of {entity.Variations}");
}
// The variation of a property is only allowed if all its variation flags
// are also set on the entity itself. It cannot set anything that is not also set by the content type.
// For example, when entity.Variations is set to Culture a property cannot be set to Segment.
var isValid = entity.Variations.HasFlag(prop.Variations);
if (!isValid)
throw new InvalidOperationException($"The property {prop.Alias} cannot have variations of {prop.Variations} with the content type variations of {entity.Variations}");
}
}
private IEnumerable<IContentTypeComposition> GetImpactedContentTypes(IContentTypeComposition contentType, IEnumerable<IContentTypeComposition> all)
@@ -661,27 +657,27 @@ AND umbracoNode.id <> @id",
var impactedL = impacted.Select(x => x.Id).ToList();
//Group by the "To" variation so we can bulk update in the correct batches
foreach (var grouping in propertyTypeChanges.GroupBy(x => x.Value.ToVariation))
foreach (var grouping in propertyTypeChanges.GroupBy(x => x.Value))
{
var propertyTypeIds = grouping.Select(x => x.Key).ToList();
var toVariation = grouping.Key;
var (FromVariation, ToVariation) = grouping.Key;
switch (toVariation)
var fromCultureEnabled = FromVariation.HasFlag(ContentVariation.Culture);
var toCultureEnabled = ToVariation.HasFlag(ContentVariation.Culture);
if (!fromCultureEnabled && toCultureEnabled)
{
case ContentVariation.Culture:
CopyPropertyData(null, defaultLanguageId, propertyTypeIds, impactedL);
CopyTagData(null, defaultLanguageId, propertyTypeIds, impactedL);
RenormalizeDocumentEditedFlags(propertyTypeIds, impactedL);
break;
case ContentVariation.Nothing:
CopyPropertyData(defaultLanguageId, null, propertyTypeIds, impactedL);
CopyTagData(defaultLanguageId, null, propertyTypeIds, impactedL);
RenormalizeDocumentEditedFlags(propertyTypeIds, impactedL);
break;
case ContentVariation.CultureAndSegment:
case ContentVariation.Segment:
default:
throw new NotSupportedException(); // TODO: Support this
// Culture has been enabled
CopyPropertyData(null, defaultLanguageId, propertyTypeIds, impactedL);
CopyTagData(null, defaultLanguageId, propertyTypeIds, impactedL);
RenormalizeDocumentEditedFlags(propertyTypeIds, impactedL);
}
else if (fromCultureEnabled && !toCultureEnabled)
{
// Culture has been disabled
CopyPropertyData(defaultLanguageId, null, propertyTypeIds, impactedL);
CopyTagData(defaultLanguageId, null, propertyTypeIds, impactedL);
RenormalizeDocumentEditedFlags(propertyTypeIds, impactedL);
}
}
}
@@ -693,78 +689,72 @@ AND umbracoNode.id <> @id",
{
var defaultLanguageId = GetDefaultLanguageId();
switch (toVariation)
var cultureIsNotEnabled = !fromVariation.HasFlag(ContentVariation.Culture);
var cultureWillBeEnabled = toVariation.HasFlag(ContentVariation.Culture);
if (cultureIsNotEnabled && cultureWillBeEnabled)
{
case ContentVariation.Culture:
//move the names
//first clear out any existing names that might already exists under the default lang
//there's 2x tables to update
//move the names
//first clear out any existing names that might already exists under the default lang
//there's 2x tables to update
//clear out the versionCultureVariation table
var sqlSelect = Sql().Select<ContentVersionCultureVariationDto>(x => x.Id)
.From<ContentVersionCultureVariationDto>()
.InnerJoin<ContentVersionDto>().On<ContentVersionDto, ContentVersionCultureVariationDto>(x => x.Id, x => x.VersionId)
.InnerJoin<ContentDto>().On<ContentDto, ContentVersionDto>(x => x.NodeId, x => x.NodeId)
.Where<ContentDto>(x => x.ContentTypeId == contentType.Id)
.Where<ContentVersionCultureVariationDto>(x => x.LanguageId == defaultLanguageId);
var sqlDelete = Sql()
.Delete<ContentVersionCultureVariationDto>()
.WhereIn<ContentVersionCultureVariationDto>(x => x.Id, sqlSelect);
//clear out the versionCultureVariation table
var sqlSelect = Sql().Select<ContentVersionCultureVariationDto>(x => x.Id)
.From<ContentVersionCultureVariationDto>()
.InnerJoin<ContentVersionDto>().On<ContentVersionDto, ContentVersionCultureVariationDto>(x => x.Id, x => x.VersionId)
.InnerJoin<ContentDto>().On<ContentDto, ContentVersionDto>(x => x.NodeId, x => x.NodeId)
.Where<ContentDto>(x => x.ContentTypeId == contentType.Id)
.Where<ContentVersionCultureVariationDto>(x => x.LanguageId == defaultLanguageId);
var sqlDelete = Sql()
.Delete<ContentVersionCultureVariationDto>()
.WhereIn<ContentVersionCultureVariationDto>(x => x.Id, sqlSelect);
Database.Execute(sqlDelete);
Database.Execute(sqlDelete);
//clear out the documentCultureVariation table
sqlSelect = Sql().Select<DocumentCultureVariationDto>(x => x.Id)
.From<DocumentCultureVariationDto>()
.InnerJoin<ContentDto>().On<ContentDto, DocumentCultureVariationDto>(x => x.NodeId, x => x.NodeId)
.Where<ContentDto>(x => x.ContentTypeId == contentType.Id)
.Where<DocumentCultureVariationDto>(x => x.LanguageId == defaultLanguageId);
sqlDelete = Sql()
.Delete<DocumentCultureVariationDto>()
.WhereIn<DocumentCultureVariationDto>(x => x.Id, sqlSelect);
//clear out the documentCultureVariation table
sqlSelect = Sql().Select<DocumentCultureVariationDto>(x => x.Id)
.From<DocumentCultureVariationDto>()
.InnerJoin<ContentDto>().On<ContentDto, DocumentCultureVariationDto>(x => x.NodeId, x => x.NodeId)
.Where<ContentDto>(x => x.ContentTypeId == contentType.Id)
.Where<DocumentCultureVariationDto>(x => x.LanguageId == defaultLanguageId);
sqlDelete = Sql()
.Delete<DocumentCultureVariationDto>()
.WhereIn<DocumentCultureVariationDto>(x => x.Id, sqlSelect);
Database.Execute(sqlDelete);
Database.Execute(sqlDelete);
//now we need to insert names into these 2 tables based on the invariant data
//now we need to insert names into these 2 tables based on the invariant data
//insert rows into the versionCultureVariationDto table based on the data from contentVersionDto for the default lang
var cols = Sql().Columns<ContentVersionCultureVariationDto>(x => x.VersionId, x => x.Name, x => x.UpdateUserId, x => x.UpdateDate, x => x.LanguageId);
sqlSelect = Sql().Select<ContentVersionDto>(x => x.Id, x => x.Text, x => x.UserId, x => x.VersionDate)
.Append($", {defaultLanguageId}") //default language ID
.From<ContentVersionDto>()
.InnerJoin<ContentDto>().On<ContentDto, ContentVersionDto>(x => x.NodeId, x => x.NodeId)
.Where<ContentDto>(x => x.ContentTypeId == contentType.Id);
var sqlInsert = Sql($"INSERT INTO {ContentVersionCultureVariationDto.TableName} ({cols})").Append(sqlSelect);
//insert rows into the versionCultureVariationDto table based on the data from contentVersionDto for the default lang
var cols = Sql().Columns<ContentVersionCultureVariationDto>(x => x.VersionId, x => x.Name, x => x.UpdateUserId, x => x.UpdateDate, x => x.LanguageId);
sqlSelect = Sql().Select<ContentVersionDto>(x => x.Id, x => x.Text, x => x.UserId, x => x.VersionDate)
.Append($", {defaultLanguageId}") //default language ID
.From<ContentVersionDto>()
.InnerJoin<ContentDto>().On<ContentDto, ContentVersionDto>(x => x.NodeId, x => x.NodeId)
.Where<ContentDto>(x => x.ContentTypeId == contentType.Id);
var sqlInsert = Sql($"INSERT INTO {ContentVersionCultureVariationDto.TableName} ({cols})").Append(sqlSelect);
Database.Execute(sqlInsert);
Database.Execute(sqlInsert);
//insert rows into the documentCultureVariation table
cols = Sql().Columns<DocumentCultureVariationDto>(x => x.NodeId, x => x.Edited, x => x.Published, x => x.Name, x => x.Available, x => x.LanguageId);
sqlSelect = Sql().Select<DocumentDto>(x => x.NodeId, x => x.Edited, x => x.Published)
.AndSelect<NodeDto>(x => x.Text)
.Append($", 1, {defaultLanguageId}") //make Available + default language ID
.From<DocumentDto>()
.InnerJoin<NodeDto>().On<NodeDto, DocumentDto>(x => x.NodeId, x => x.NodeId)
.InnerJoin<ContentDto>().On<ContentDto, NodeDto>(x => x.NodeId, x => x.NodeId)
.Where<ContentDto>(x => x.ContentTypeId == contentType.Id);
sqlInsert = Sql($"INSERT INTO {DocumentCultureVariationDto.TableName} ({cols})").Append(sqlSelect);
//insert rows into the documentCultureVariation table
cols = Sql().Columns<DocumentCultureVariationDto>(x => x.NodeId, x => x.Edited, x => x.Published, x => x.Name, x => x.Available, x => x.LanguageId);
sqlSelect = Sql().Select<DocumentDto>(x => x.NodeId, x => x.Edited, x => x.Published)
.AndSelect<NodeDto>(x => x.Text)
.Append($", 1, {defaultLanguageId}") //make Available + default language ID
.From<DocumentDto>()
.InnerJoin<NodeDto>().On<NodeDto, DocumentDto>(x => x.NodeId, x => x.NodeId)
.InnerJoin<ContentDto>().On<ContentDto, NodeDto>(x => x.NodeId, x => x.NodeId)
.Where<ContentDto>(x => x.ContentTypeId == contentType.Id);
sqlInsert = Sql($"INSERT INTO {DocumentCultureVariationDto.TableName} ({cols})").Append(sqlSelect);
Database.Execute(sqlInsert);
}
else
{
//we don't need to move the names! this is because we always keep the invariant names with the name of the default language.
Database.Execute(sqlInsert);
break;
case ContentVariation.Nothing:
//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
break;
case ContentVariation.CultureAndSegment:
case ContentVariation.Segment:
default:
throw new NotSupportedException(); // TODO: Support this
//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
}
}
@@ -279,6 +279,28 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
return moveInfo;
}
public IReadOnlyDictionary<Udi, IEnumerable<string>> FindUsages(int id)
{
if (id == default)
return new Dictionary<Udi, IEnumerable<string>>();
var sql = Sql()
.Select<ContentTypeDto>(ct => ct.Select(node => node.NodeDto))
.AndSelect<PropertyTypeDto>(pt => Alias(pt.Alias, "ptAlias"), pt => Alias(pt.Name, "ptName"))
.From<PropertyTypeDto>()
.InnerJoin<ContentTypeDto>().On<ContentTypeDto, PropertyTypeDto>(ct => ct.NodeId, pt => pt.ContentTypeId)
.InnerJoin<NodeDto>().On<NodeDto, ContentTypeDto>(n => n.NodeId, ct => ct.NodeId)
.Where<PropertyTypeDto>(pt => pt.DataTypeId == id)
.OrderBy<NodeDto>(node => node.NodeId)
.AndBy<PropertyTypeDto>(pt => pt.Alias);
var dtos = Database.FetchOneToMany<ContentTypeReferenceDto>(ct => ct.PropertyTypes, sql);
return dtos.ToDictionary(
x => (Udi)new GuidUdi(ObjectTypes.GetUdiType(x.NodeDto.NodeObjectType.Value), x.NodeDto.UniqueId).EnsureClosed(),
x => (IEnumerable<string>)x.PropertyTypes.Select(p => p.Alias).ToList());
}
private string EnsureUniqueNodeName(string nodeName, int id = 0)
{
var template = SqlContext.Templates.Get("Umbraco.Core.DataTypeDefinitionRepository.EnsureUniqueNodeName", tsql => tsql
@@ -291,5 +313,24 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
return SimilarNodeName.GetUniqueName(names, id, nodeName);
}
[TableName(Constants.DatabaseSchema.Tables.ContentType)]
private class ContentTypeReferenceDto : ContentTypeDto
{
[ResultColumn]
[Reference(ReferenceType.Many)]
public List<PropertyTypeReferenceDto> PropertyTypes { get; set; }
}
[TableName(Constants.DatabaseSchema.Tables.PropertyType)]
private class PropertyTypeReferenceDto
{
[Column("ptAlias")]
public string Alias { get; set; }
[Column("ptName")]
public string Name { get; set; }
}
}
}
@@ -248,14 +248,63 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
return dto == null ? null : MapDtoToContent(dto);
}
// deletes a specific version
public override void DeleteVersion(int versionId)
{
// TODO: test object node type?
// get the version we want to delete
var template = SqlContext.Templates.Get("Umbraco.Core.DocumentRepository.GetVersion", tsql =>
tsql.Select<ContentVersionDto>()
.AndSelect<DocumentVersionDto>()
.From<ContentVersionDto>()
.InnerJoin<DocumentVersionDto>()
.On<ContentVersionDto, DocumentVersionDto>((c, d) => c.Id == d.Id)
.Where<ContentVersionDto>(x => x.Id == SqlTemplate.Arg<int>("versionId"))
);
var versionDto = Database.Fetch<DocumentVersionDto>(template.Sql(new { versionId })).FirstOrDefault();
// nothing to delete
if (versionDto == null)
return;
// don't delete the current or published version
if (versionDto.ContentVersionDto.Current)
throw new InvalidOperationException("Cannot delete the current version.");
else if (versionDto.Published)
throw new InvalidOperationException("Cannot delete the published version.");
PerformDeleteVersion(versionDto.ContentVersionDto.NodeId, versionId);
}
// deletes all versions of an entity, older than a date.
public override void DeleteVersions(int nodeId, DateTime versionDate)
{
// TODO: test object node type?
// get the versions we want to delete, excluding the current one
var template = SqlContext.Templates.Get("Umbraco.Core.DocumentRepository.GetVersions", tsql =>
tsql.Select<ContentVersionDto>()
.From<ContentVersionDto>()
.InnerJoin<DocumentVersionDto>()
.On<ContentVersionDto, DocumentVersionDto>((c, d) => c.Id == d.Id)
.Where<ContentVersionDto>(x => x.NodeId == SqlTemplate.Arg<int>("nodeId") && !x.Current && x.VersionDate < SqlTemplate.Arg<DateTime>("versionDate"))
.Where<DocumentVersionDto>( x => !x.Published)
);
var versionDtos = Database.Fetch<ContentVersionDto>(template.Sql(new { nodeId, versionDate }));
foreach (var versionDto in versionDtos)
PerformDeleteVersion(versionDto.NodeId, versionDto.Id);
}
protected override void PerformDeleteVersion(int id, int versionId)
{
// raise event first else potential FK issues
OnUowRemovingVersion(new ScopedVersionEventArgs(AmbientScope, id, versionId));
Database.Delete<PropertyDataDto>("WHERE versionId = @versionId", new { versionId });
Database.Delete<ContentVersionDto>("WHERE id = @versionId", new { versionId });
Database.Delete<ContentVersionCultureVariationDto>("WHERE versionId = @versionId", new { versionId });
Database.Delete<DocumentVersionDto>("WHERE id = @versionId", new { versionId });
Database.Delete<ContentVersionDto>("WHERE id = @versionId", new { versionId });
}
#endregion
@@ -42,8 +42,9 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
{
var isContent = objectType == Constants.ObjectTypes.Document || objectType == Constants.ObjectTypes.DocumentBlueprint;
var isMedia = objectType == Constants.ObjectTypes.Media;
var isMember = objectType == Constants.ObjectTypes.Member;
var sql = GetBaseWhere(isContent, isMedia, false, x =>
var sql = GetBaseWhere(isContent, isMedia, isMember, false, x =>
{
if (filter == null) return;
foreach (var filterClause in filter.GetWhereClauses())
@@ -54,7 +55,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
var translator = new SqlTranslator<IUmbracoEntity>(sql, query);
sql = translator.Translate();
sql = AddGroupBy(isContent, isMedia, sql, ordering.IsEmpty);
sql = AddGroupBy(isContent, isMedia, isMember, sql, ordering.IsEmpty);
if (!ordering.IsEmpty)
{
@@ -81,6 +82,12 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
dtos = page.Items;
totalRecords = page.TotalItems;
}
else if (isMember)
{
var page = Database.Page<MemberEntityDto>(pageIndexToFetch, pageSize, sql);
dtos = page.Items;
totalRecords = page.TotalItems;
}
else
{
var page = Database.Page<BaseDto>(pageIndexToFetch, pageSize, sql);
@@ -88,7 +95,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
totalRecords = page.TotalItems;
}
var entities = dtos.Select(x => BuildEntity(isContent, isMedia, x)).ToArray();
var entities = dtos.Select(x => BuildEntity(isContent, isMedia, isMember, x)).ToArray();
if (isContent)
BuildVariants(entities.Cast<DocumentEntitySlim>());
@@ -98,13 +105,13 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
public IEntitySlim Get(Guid key)
{
var sql = GetBaseWhere(false, false, false, key);
var sql = GetBaseWhere(false, false, false, false, key);
var dto = Database.FirstOrDefault<BaseDto>(sql);
return dto == null ? null : BuildEntity(false, false, dto);
return dto == null ? null : BuildEntity(false, false, false, dto);
}
private IEntitySlim GetEntity(Sql<ISqlContext> sql, bool isContent, bool isMedia)
private IEntitySlim GetEntity(Sql<ISqlContext> sql, bool isContent, bool isMedia, bool isMember)
{
//isContent is going to return a 1:M result now with the variants so we need to do different things
if (isContent)
@@ -120,7 +127,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
if (dto == null) return null;
var entity = BuildEntity(false, isMedia, dto);
var entity = BuildEntity(false, isMedia, isMember, dto);
return entity;
}
@@ -129,25 +136,27 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
{
var isContent = objectTypeId == Constants.ObjectTypes.Document || objectTypeId == Constants.ObjectTypes.DocumentBlueprint;
var isMedia = objectTypeId == Constants.ObjectTypes.Media;
var isMember = objectTypeId == Constants.ObjectTypes.Member;
var sql = GetFullSqlForEntityType(isContent, isMedia, objectTypeId, key);
return GetEntity(sql, isContent, isMedia);
var sql = GetFullSqlForEntityType(isContent, isMedia, isMember, objectTypeId, key);
return GetEntity(sql, isContent, isMedia, isMember);
}
public IEntitySlim Get(int id)
{
var sql = GetBaseWhere(false, false, false, id);
var sql = GetBaseWhere(false, false, false, false, id);
var dto = Database.FirstOrDefault<BaseDto>(sql);
return dto == null ? null : BuildEntity(false, false, dto);
return dto == null ? null : BuildEntity(false, false, false, dto);
}
public IEntitySlim Get(int id, Guid objectTypeId)
{
var isContent = objectTypeId == Constants.ObjectTypes.Document || objectTypeId == Constants.ObjectTypes.DocumentBlueprint;
var isMedia = objectTypeId == Constants.ObjectTypes.Media;
var isMember = objectTypeId == Constants.ObjectTypes.Member;
var sql = GetFullSqlForEntityType(isContent, isMedia, objectTypeId, id);
return GetEntity(sql, isContent, isMedia);
var sql = GetFullSqlForEntityType(isContent, isMedia, isMember, objectTypeId, id);
return GetEntity(sql, isContent, isMedia, isMember);
}
public IEnumerable<IEntitySlim> GetAll(Guid objectType, params int[] ids)
@@ -164,7 +173,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
: PerformGetAll(objectType);
}
private IEnumerable<IEntitySlim> GetEntities(Sql<ISqlContext> sql, bool isContent, bool isMedia)
private IEnumerable<IEntitySlim> GetEntities(Sql<ISqlContext> sql, bool isContent, bool isMedia, bool isMember)
{
//isContent is going to return a 1:M result now with the variants so we need to do different things
if (isContent)
@@ -180,7 +189,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
? (IEnumerable<BaseDto>)Database.Fetch<MediaEntityDto>(sql)
: Database.Fetch<BaseDto>(sql);
var entities = dtos.Select(x => BuildEntity(false, isMedia, x)).ToArray();
var entities = dtos.Select(x => BuildEntity(false, isMedia, isMember, x)).ToArray();
return entities;
}
@@ -189,9 +198,10 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
{
var isContent = objectType == Constants.ObjectTypes.Document || objectType == Constants.ObjectTypes.DocumentBlueprint;
var isMedia = objectType == Constants.ObjectTypes.Media;
var isMember = objectType == Constants.ObjectTypes.Member;
var sql = GetFullSqlForEntityType(isContent, isMedia, objectType, filter);
return GetEntities(sql, isContent, isMedia);
var sql = GetFullSqlForEntityType(isContent, isMedia, isMember, objectType, filter);
return GetEntities(sql, isContent, isMedia, isMember);
}
public IEnumerable<TreeEntityPath> GetAllPaths(Guid objectType, params int[] ids)
@@ -218,26 +228,27 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
public IEnumerable<IEntitySlim> GetByQuery(IQuery<IUmbracoEntity> query)
{
var sqlClause = GetBase(false, false, null);
var sqlClause = GetBase(false, false, false, null);
var translator = new SqlTranslator<IUmbracoEntity>(sqlClause, query);
var sql = translator.Translate();
sql = AddGroupBy(false, false, sql, true);
sql = AddGroupBy(false, false, false, sql, true);
var dtos = Database.Fetch<BaseDto>(sql);
return dtos.Select(x => BuildEntity(false, false, x)).ToList();
return dtos.Select(x => BuildEntity(false, false, false, x)).ToList();
}
public IEnumerable<IEntitySlim> GetByQuery(IQuery<IUmbracoEntity> query, Guid objectType)
{
var isContent = objectType == Constants.ObjectTypes.Document || objectType == Constants.ObjectTypes.DocumentBlueprint;
var isMedia = objectType == Constants.ObjectTypes.Media;
var isMember = objectType == Constants.ObjectTypes.Member;
var sql = GetBaseWhere(isContent, isMedia, false, null, objectType);
var sql = GetBaseWhere(isContent, isMedia, isMember, false, null, objectType);
var translator = new SqlTranslator<IUmbracoEntity>(sql, query);
sql = translator.Translate();
sql = AddGroupBy(isContent, isMedia, sql, true);
sql = AddGroupBy(isContent, isMedia, isMember, sql, true);
return GetEntities(sql, isContent, isMedia);
return GetEntities(sql, isContent, isMedia, isMember);
}
public UmbracoObjectTypes GetObjectType(int id)
@@ -329,29 +340,29 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
}
// gets the full sql for a given object type and a given unique id
protected Sql<ISqlContext> GetFullSqlForEntityType(bool isContent, bool isMedia, Guid objectType, Guid uniqueId)
protected Sql<ISqlContext> GetFullSqlForEntityType(bool isContent, bool isMedia, bool isMember, Guid objectType, Guid uniqueId)
{
var sql = GetBaseWhere(isContent, isMedia, false, objectType, uniqueId);
return AddGroupBy(isContent, isMedia, sql, true);
var sql = GetBaseWhere(isContent, isMedia, isMember, false, objectType, uniqueId);
return AddGroupBy(isContent, isMedia, isMember, sql, true);
}
// gets the full sql for a given object type and a given node id
protected Sql<ISqlContext> GetFullSqlForEntityType(bool isContent, bool isMedia, Guid objectType, int nodeId)
protected Sql<ISqlContext> GetFullSqlForEntityType(bool isContent, bool isMedia, bool isMember, Guid objectType, int nodeId)
{
var sql = GetBaseWhere(isContent, isMedia, false, objectType, nodeId);
return AddGroupBy(isContent, isMedia, sql, true);
var sql = GetBaseWhere(isContent, isMedia, isMember, false, objectType, nodeId);
return AddGroupBy(isContent, isMedia, isMember, sql, true);
}
// gets the full sql for a given object type, with a given filter
protected Sql<ISqlContext> GetFullSqlForEntityType(bool isContent, bool isMedia, Guid objectType, Action<Sql<ISqlContext>> filter)
protected Sql<ISqlContext> GetFullSqlForEntityType(bool isContent, bool isMedia, bool isMember, Guid objectType, Action<Sql<ISqlContext>> filter)
{
var sql = GetBaseWhere(isContent, isMedia, false, filter, objectType);
return AddGroupBy(isContent, isMedia, sql, true);
var sql = GetBaseWhere(isContent, isMedia, isMember, false, filter, objectType);
return AddGroupBy(isContent, isMedia, isMember, sql, true);
}
// gets the base SELECT + FROM [+ filter] sql
// always from the 'current' content version
protected Sql<ISqlContext> GetBase(bool isContent, bool isMedia, Action<Sql<ISqlContext>> filter, bool isCount = false)
protected Sql<ISqlContext> GetBase(bool isContent, bool isMedia, bool isMember, Action<Sql<ISqlContext>> filter, bool isCount = false)
{
var sql = Sql();
@@ -366,7 +377,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
.AndSelect<NodeDto>(x => x.SortOrder, x => x.UniqueId, x => x.Text, x => x.NodeObjectType, x => x.CreateDate)
.Append(", COUNT(child.id) AS children");
if (isContent || isMedia)
if (isContent || isMedia || isMember)
sql
.AndSelect<ContentVersionDto>(x => Alias(x.Id, "versionId"))
.AndSelect<ContentTypeDto>(x => x.Alias, x => x.Icon, x => x.Thumbnail, x => x.IsContainer, x => x.Variations);
@@ -387,7 +398,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
sql
.From<NodeDto>();
if (isContent || isMedia)
if (isContent || isMedia || isMember)
{
sql
.InnerJoin<ContentVersionDto>().On<NodeDto, ContentVersionDto>((left, right) => left.NodeId == right.NodeId && right.Current)
@@ -422,49 +433,49 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
// gets the base SELECT + FROM [+ filter] + WHERE sql
// for a given object type, with a given filter
protected Sql<ISqlContext> GetBaseWhere(bool isContent, bool isMedia, bool isCount, Action<Sql<ISqlContext>> filter, Guid objectType)
protected Sql<ISqlContext> GetBaseWhere(bool isContent, bool isMedia, bool isMember, bool isCount, Action<Sql<ISqlContext>> filter, Guid objectType)
{
return GetBase(isContent, isMedia, filter, isCount)
return GetBase(isContent, isMedia, isMember, filter, isCount)
.Where<NodeDto>(x => x.NodeObjectType == objectType);
}
// gets the base SELECT + FROM + WHERE sql
// for a given node id
protected Sql<ISqlContext> GetBaseWhere(bool isContent, bool isMedia, bool isCount, int id)
protected Sql<ISqlContext> GetBaseWhere(bool isContent, bool isMedia, bool isMember, bool isCount, int id)
{
var sql = GetBase(isContent, isMedia, null, isCount)
var sql = GetBase(isContent, isMedia, isMember, null, isCount)
.Where<NodeDto>(x => x.NodeId == id);
return AddGroupBy(isContent, isMedia, sql, true);
return AddGroupBy(isContent, isMedia, isMember, sql, true);
}
// gets the base SELECT + FROM + WHERE sql
// for a given unique id
protected Sql<ISqlContext> GetBaseWhere(bool isContent, bool isMedia, bool isCount, Guid uniqueId)
protected Sql<ISqlContext> GetBaseWhere(bool isContent, bool isMedia, bool isMember, bool isCount, Guid uniqueId)
{
var sql = GetBase(isContent, isMedia, null, isCount)
var sql = GetBase(isContent, isMedia, isMember, null, isCount)
.Where<NodeDto>(x => x.UniqueId == uniqueId);
return AddGroupBy(isContent, isMedia, sql, true);
return AddGroupBy(isContent, isMedia, isMember, sql, true);
}
// gets the base SELECT + FROM + WHERE sql
// for a given object type and node id
protected Sql<ISqlContext> GetBaseWhere(bool isContent, bool isMedia, bool isCount, Guid objectType, int nodeId)
protected Sql<ISqlContext> GetBaseWhere(bool isContent, bool isMedia, bool isMember, bool isCount, Guid objectType, int nodeId)
{
return GetBase(isContent, isMedia, null, isCount)
return GetBase(isContent, isMedia, isMember, null, isCount)
.Where<NodeDto>(x => x.NodeId == nodeId && x.NodeObjectType == objectType);
}
// gets the base SELECT + FROM + WHERE sql
// for a given object type and unique id
protected Sql<ISqlContext> GetBaseWhere(bool isContent, bool isMedia, bool isCount, Guid objectType, Guid uniqueId)
protected Sql<ISqlContext> GetBaseWhere(bool isContent, bool isMedia, bool isMember, bool isCount, Guid objectType, Guid uniqueId)
{
return GetBase(isContent, isMedia, null, isCount)
return GetBase(isContent, isMedia, isMember, null, isCount)
.Where<NodeDto>(x => x.UniqueId == uniqueId && x.NodeObjectType == objectType);
}
// gets the GROUP BY / ORDER BY sql
// required in order to count children
protected Sql<ISqlContext> AddGroupBy(bool isContent, bool isMedia, Sql<ISqlContext> sql, bool defaultSort)
protected Sql<ISqlContext> AddGroupBy(bool isContent, bool isMedia, bool isMember, Sql<ISqlContext> sql, bool defaultSort)
{
sql
.GroupBy<NodeDto>(x => x.NodeId, x => x.Trashed, x => x.ParentId, x => x.UserId, x => x.Level, x => x.Path)
@@ -483,7 +494,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
}
if (isContent || isMedia)
if (isContent || isMedia || isMember)
sql
.AndBy<ContentVersionDto>(x => x.Id)
.AndBy<ContentTypeDto>(x => x.Alias, x => x.Icon, x => x.Thumbnail, x => x.IsContainer, x => x.Variations);
@@ -528,6 +539,10 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
public string MediaPath { get; set; }
}
private class MemberEntityDto : BaseDto
{
}
public class VariantInfoDto
{
public int NodeId { get; set; }
@@ -574,12 +589,14 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
#region Factory
private EntitySlim BuildEntity(bool isContent, bool isMedia, BaseDto dto)
private EntitySlim BuildEntity(bool isContent, bool isMedia, bool isMember, BaseDto dto)
{
if (isContent)
return BuildDocumentEntity(dto);
if (isMedia)
return BuildMediaEntity(dto);
if (isMember)
return BuildMemberEntity(dto);
// EntitySlim does not track changes
var entity = new EntitySlim();
@@ -644,6 +661,19 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
return entity;
}
private MemberEntitySlim BuildMemberEntity(BaseDto dto)
{
// EntitySlim does not track changes
var entity = new MemberEntitySlim();
BuildEntity(entity, dto);
entity.ContentTypeAlias = dto.Alias;
entity.ContentTypeIcon = dto.Icon;
entity.ContentTypeThumbnail = dto.Thumbnail;
return entity;
}
#endregion
}
}
@@ -182,6 +182,19 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
throw new InvalidOperationException($"Cannot save the default language ({entity.IsoCode}) as non-default. Make another language the default language instead.");
}
if (entity.IsPropertyDirty(nameof(ILanguage.IsoCode)))
{
//if the iso code is changing, ensure there's not another lang with the same code already assigned
var sameCode = Sql()
.SelectCount()
.From<LanguageDto>()
.Where<LanguageDto>(x => x.IsoCode == entity.IsoCode && x.Id != entity.Id);
var countOfSameCode = Database.ExecuteScalar<int>(sameCode);
if (countOfSameCode > 0)
throw new InvalidOperationException($"Cannot update the language to a new culture: {entity.IsoCode} since that culture is already assigned to another language entity.");
}
// fallback cycles are detected at service level
// update
@@ -133,7 +133,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
// joining the type so we can do a query against the member type - not sure if this adds much overhead or not?
// the execution plan says it doesn't so we'll go with that and in that case, it might be worth joining the content
// types by default on the document and media repo's so we can query by content type there too.
// types by default on the document and media repos so we can query by content type there too.
.InnerJoin<ContentTypeDto>().On<ContentDto, ContentTypeDto>(left => left.ContentTypeId, right => right.NodeId);
sql.Where<NodeDto>(x => x.NodeObjectType == NodeObjectTypeId);
@@ -546,6 +546,15 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
if (ordering.OrderBy.InvariantEquals("userName"))
return SqlSyntax.GetFieldName<MemberDto>(x => x.LoginName);
if (ordering.OrderBy.InvariantEquals("updateDate"))
return SqlSyntax.GetFieldName<ContentVersionDto>(x => x.VersionDate);
if (ordering.OrderBy.InvariantEquals("createDate"))
return SqlSyntax.GetFieldName<NodeDto>(x => x.CreateDate);
if (ordering.OrderBy.InvariantEquals("contentTypeAlias"))
return SqlSyntax.GetFieldName<ContentTypeDto>(x => x.Alias);
return base.ApplySystemOrdering(ref sql, ordering);
}
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using NPoco;
using Umbraco.Core.Cache;
using Umbraco.Core.Logging;
@@ -105,7 +106,7 @@ JOIN umbracoNode ON umbracoRedirectUrl.contentKey=umbracoNode.uniqueID");
CreateDateUtc = redirectUrl.CreateDateUtc,
Url = redirectUrl.Url,
Culture = redirectUrl.Culture,
UrlHash = redirectUrl.Url.ToSHA1()
UrlHash = redirectUrl.Url.GenerateHash<SHA1>()
};
}
@@ -134,7 +135,7 @@ JOIN umbracoNode ON umbracoRedirectUrl.contentKey=umbracoNode.uniqueID");
public IRedirectUrl Get(string url, Guid contentKey, string culture)
{
var urlHash = url.ToSHA1();
var urlHash = url.GenerateHash<SHA1>();
var sql = GetBaseQuery(false).Where<RedirectUrlDto>(x => x.Url == url && x.UrlHash == urlHash && x.ContentKey == contentKey && x.Culture == culture);
var dto = Database.Fetch<RedirectUrlDto>(sql).FirstOrDefault();
return dto == null ? null : Map(dto);
@@ -157,7 +158,7 @@ JOIN umbracoNode ON umbracoRedirectUrl.contentKey=umbracoNode.uniqueID");
public IRedirectUrl GetMostRecentUrl(string url)
{
var urlHash = url.ToSHA1();
var urlHash = url.GenerateHash<SHA1>();
var sql = GetBaseQuery(false)
.Where<RedirectUrlDto>(x => x.Url == url && x.UrlHash == urlHash)
.OrderByDescending<RedirectUrlDto>(x => x.CreateDateUtc);
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.Text.RegularExpressions;
using NPoco;
using Umbraco.Core.Persistence.DatabaseAnnotations;
@@ -76,6 +77,11 @@ namespace Umbraco.Core.Persistence.SqlSyntax
string ConvertIntegerToOrderableString { get; }
string ConvertDateToOrderableString { get; }
string ConvertDecimalToOrderableString { get; }
/// <summary>
/// Returns the default isolation level for the database
/// </summary>
IsolationLevel DefaultIsolationLevel { get; }
IEnumerable<string> GetTablesInSchema(IDatabase db);
IEnumerable<ColumnInfo> GetColumnsInSchema(IDatabase db);
@@ -121,5 +127,8 @@ namespace Umbraco.Core.Persistence.SqlSyntax
/// unspecified.</para>
/// </remarks>
bool TryGetDefaultConstraint(IDatabase db, string tableName, string columnName, out string constraintName);
void ReadLock(IDatabase db, params int[] lockIds);
void WriteLock(IDatabase db, params int[] lockIds);
}
}
@@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlServerCe;
using System.Linq;
using NPoco;
using Umbraco.Core.Persistence.DatabaseAnnotations;
@@ -52,6 +54,8 @@ namespace Umbraco.Core.Persistence.SqlSyntax
return "(" + string.Join("+", args) + ")";
}
public override System.Data.IsolationLevel DefaultIsolationLevel => System.Data.IsolationLevel.RepeatableRead;
public override string FormatColumnRename(string tableName, string oldName, string newName)
{
//NOTE Sql CE doesn't support renaming a column, so a new column needs to be created, then copy data and finally remove old column
@@ -152,6 +156,39 @@ where table_name=@0 and column_name=@1", tableName, columnName).FirstOrDefault()
return result > 0;
}
public override void WriteLock(IDatabase db, params int[] lockIds)
{
// soon as we get Database, a transaction is started
if (db.Transaction.IsolationLevel < IsolationLevel.RepeatableRead)
throw new InvalidOperationException("A transaction with minimum RepeatableRead isolation level is required.");
db.Execute(@"SET LOCK_TIMEOUT 1800;");
// *not* using a unique 'WHERE IN' query here because the *order* of lockIds is important to avoid deadlocks
foreach (var lockId in lockIds)
{
var i = db.Execute(@"UPDATE umbracoLock SET value = (CASE WHEN (value=1) THEN -1 ELSE 1 END) WHERE id=@id", new { id = lockId });
if (i == 0) // ensure we are actually locking!
throw new ArgumentException($"LockObject with id={lockId} does not exist.");
}
}
public override void ReadLock(IDatabase db, params int[] lockIds)
{
// soon as we get Database, a transaction is started
if (db.Transaction.IsolationLevel < IsolationLevel.RepeatableRead)
throw new InvalidOperationException("A transaction with minimum RepeatableRead isolation level is required.");
// *not* using a unique 'WHERE IN' query here because the *order* of lockIds is important to avoid deadlocks
foreach (var lockId in lockIds)
{
var i = db.ExecuteScalar<int?>("SELECT value FROM umbracoLock WHERE id=@id", new { id = lockId });
if (i == null) // ensure we are actually locking!
throw new ArgumentException($"LockObject with id={lockId} does not exist.");
}
}
protected override string FormatIdentity(ColumnDefinition column)
{
return column.IsIdentity ? GetIdentityString(column) : string.Empty;
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Data;
using System.Data.Common;
using System.Data.SqlClient;
using System.Linq;
using NPoco;
using Umbraco.Core.Logging;
@@ -179,6 +180,8 @@ namespace Umbraco.Core.Persistence.SqlSyntax
return items.Select(x => x.TABLE_NAME).Cast<string>().ToList();
}
public override IsolationLevel DefaultIsolationLevel => IsolationLevel.ReadCommitted;
public override IEnumerable<ColumnInfo> GetColumnsInSchema(IDatabase db)
{
var items = db.Fetch<dynamic>("SELECT TABLE_NAME, COLUMN_NAME, ORDINAL_POSITION, COLUMN_DEFAULT, IS_NULLABLE, DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = (SELECT SCHEMA_NAME())");
@@ -246,6 +249,41 @@ where tbl.[name]=@0 and col.[name]=@1;", tableName, columnName)
return result > 0;
}
public override void WriteLock(IDatabase db, params int[] lockIds)
{
// soon as we get Database, a transaction is started
if (db.Transaction.IsolationLevel < IsolationLevel.ReadCommitted)
throw new InvalidOperationException("A transaction with minimum ReadCommitted isolation level is required.");
// *not* using a unique 'WHERE IN' query here because the *order* of lockIds is important to avoid deadlocks
foreach (var lockId in lockIds)
{
db.Execute(@"SET LOCK_TIMEOUT 1800;");
var i = db.Execute(@"UPDATE umbracoLock WITH (REPEATABLEREAD) SET value = (CASE WHEN (value=1) THEN -1 ELSE 1 END) WHERE id=@id", new { id = lockId });
if (i == 0) // ensure we are actually locking!
throw new ArgumentException($"LockObject with id={lockId} does not exist.");
}
}
public override void ReadLock(IDatabase db, params int[] lockIds)
{
// soon as we get Database, a transaction is started
if (db.Transaction.IsolationLevel < IsolationLevel.ReadCommitted)
throw new InvalidOperationException("A transaction with minimum ReadCommitted isolation level is required.");
// *not* using a unique 'WHERE IN' query here because the *order* of lockIds is important to avoid deadlocks
foreach (var lockId in lockIds)
{
var i = db.ExecuteScalar<int?>("SELECT value FROM umbracoLock WITH (REPEATABLEREAD) WHERE id=@id", new { id = lockId });
if (i == null) // ensure we are actually locking!
throw new ArgumentException($"LockObject with id={lockId} does not exist.", nameof(lockIds));
}
}
public override string FormatColumnRename(string tableName, string oldName, string newName)
{
return string.Format(RenameColumn, tableName, oldName, newName);
@@ -200,7 +200,9 @@ namespace Umbraco.Core.Persistence.SqlSyntax
return "NVARCHAR";
}
public abstract IsolationLevel DefaultIsolationLevel { get; }
public virtual IEnumerable<string> GetTablesInSchema(IDatabase db)
{
return new List<string>();
@@ -225,6 +227,9 @@ namespace Umbraco.Core.Persistence.SqlSyntax
public abstract bool TryGetDefaultConstraint(IDatabase db, string tableName, string columnName, out string constraintName);
public abstract void ReadLock(IDatabase db, params int[] lockIds);
public abstract void WriteLock(IDatabase db, params int[] lockIds);
public virtual bool DoesTableExist(IDatabase db, string tableName)
{
return false;
@@ -20,9 +20,6 @@ namespace Umbraco.Core.Persistence
/// </remarks>
public class UmbracoDatabase : Database, IUmbracoDatabase
{
// Umbraco's default isolation level is RepeatableRead
private const IsolationLevel DefaultIsolationLevel = IsolationLevel.RepeatableRead;
private readonly ILogger _logger;
private readonly RetryPolicy _connectionRetryPolicy;
private readonly RetryPolicy _commandRetryPolicy;
@@ -38,7 +35,7 @@ namespace Umbraco.Core.Persistence
/// <para>Also used by DatabaseBuilder for creating databases and installing/upgrading.</para>
/// </remarks>
public UmbracoDatabase(string connectionString, ISqlContext sqlContext, DbProviderFactory provider, ILogger logger, RetryPolicy connectionRetryPolicy = null, RetryPolicy commandRetryPolicy = null)
: base(connectionString, sqlContext.DatabaseType, provider, DefaultIsolationLevel)
: base(connectionString, sqlContext.DatabaseType, provider, sqlContext.SqlSyntax.DefaultIsolationLevel)
{
SqlContext = sqlContext;
@@ -54,7 +51,7 @@ namespace Umbraco.Core.Persistence
/// </summary>
/// <remarks>Internal for unit tests only.</remarks>
internal UmbracoDatabase(DbConnection connection, ISqlContext sqlContext, ILogger logger)
: base(connection, sqlContext.DatabaseType, DefaultIsolationLevel)
: base(connection, sqlContext.DatabaseType, sqlContext.SqlSyntax.DefaultIsolationLevel)
{
SqlContext = sqlContext;
_logger = logger;
@@ -18,6 +18,9 @@ namespace Umbraco.Core.PropertyEditors
[JsonProperty("name", Required = Required.Always)]
public string Name { get; set; }
[JsonProperty("nameTemplate")]
public string NameTemplate { get; set; }
[JsonProperty("alias", Required = Required.Always)]
public string Alias { get; set; }
+3 -33
View File
@@ -33,8 +33,6 @@ namespace Umbraco.Core.Scoping
private ICompletable _fscope;
private IEventDispatcher _eventDispatcher;
private const IsolationLevel DefaultIsolationLevel = IsolationLevel.RepeatableRead;
// initializes a new scope
private Scope(ScopeProvider scopeProvider,
ILogger logger, FileSystems fileSystems, Scope parent, ScopeContext scopeContext, bool detachable,
@@ -205,7 +203,7 @@ namespace Umbraco.Core.Scoping
{
if (_isolationLevel != IsolationLevel.Unspecified) return _isolationLevel;
if (ParentScope != null) return ParentScope.IsolationLevel;
return DefaultIsolationLevel;
return Database.SqlContext.SqlSyntax.DefaultIsolationLevel;
}
}
@@ -488,37 +486,9 @@ namespace Umbraco.Core.Scoping
?? (_logUncompletedScopes = Current.Configs.CoreDebug().LogUncompletedScopes)).Value;
/// <inheritdoc />
public void ReadLock(params int[] lockIds)
{
// soon as we get Database, a transaction is started
if (Database.Transaction.IsolationLevel < IsolationLevel.RepeatableRead)
throw new InvalidOperationException("A transaction with minimum RepeatableRead isolation level is required.");
// *not* using a unique 'WHERE IN' query here because the *order* of lockIds is important to avoid deadlocks
foreach (var lockId in lockIds)
{
var i = Database.ExecuteScalar<int?>("SELECT value FROM umbracoLock WHERE id=@id", new { id = lockId });
if (i == null) // ensure we are actually locking!
throw new Exception($"LockObject with id={lockId} does not exist.");
}
}
public void ReadLock(params int[] lockIds) => Database.SqlContext.SqlSyntax.ReadLock(Database, lockIds);
/// <inheritdoc />
public void WriteLock(params int[] lockIds)
{
// soon as we get Database, a transaction is started
if (Database.Transaction.IsolationLevel < IsolationLevel.RepeatableRead)
throw new InvalidOperationException("A transaction with minimum RepeatableRead isolation level is required.");
// *not* using a unique 'WHERE IN' query here because the *order* of lockIds is important to avoid deadlocks
foreach (var lockId in lockIds)
{
var i = Database.Execute("UPDATE umbracoLock SET value = (CASE WHEN (value=1) THEN -1 ELSE 1 END) WHERE id=@id", new { id = lockId });
if (i == 0) // ensure we are actually locking!
throw new Exception($"LockObject with id={lockId} does not exist.");
}
}
public void WriteLock(params int[] lockIds) => Database.SqlContext.SqlSyntax.WriteLock(Database, lockIds);
}
}
@@ -25,7 +25,7 @@ namespace Umbraco.Core.Services
/// <summary>
/// Gets a content type.
/// </summary>
TItem Get(int id);
new TItem Get(int id);
/// <summary>
/// Gets a content type.
@@ -40,6 +40,7 @@ namespace Umbraco.Core.Services
int Count();
IEnumerable<TItem> GetAll(params int[] ids);
IEnumerable<TItem> GetAll(IEnumerable<Guid> ids);
IEnumerable<TItem> GetDescendants(int id, bool andSelf); // parent-child axis
IEnumerable<TItem> GetComposedOf(int id); // composition axis
@@ -10,6 +10,13 @@ namespace Umbraco.Core.Services
/// </summary>
public interface IDataTypeService : IService
{
/// <summary>
/// Returns a dictionary of content type <see cref="Udi"/>s and the property type aliases that use a <see cref="IDataType"/>
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
IReadOnlyDictionary<Udi, IEnumerable<string>> GetReferences(int id);
Attempt<OperationResult<OperationResultType, EntityContainer>> CreateContainer(int parentId, string name, int userId = Constants.Security.SuperUserId);
Attempt<OperationResult> SaveContainer(EntityContainer container, int userId = Constants.Security.SuperUserId);
EntityContainer GetContainer(int containerId);
@@ -1848,7 +1848,7 @@ namespace Umbraco.Core.Services.Implement
scope.WriteLock(Constants.Locks.ContentTree);
var c = _documentRepository.Get(id);
if (c.VersionId != versionId) // don't delete the current version
if (c.VersionId != versionId && c.PublishedVersionId != versionId) // don't delete the current or published version
_documentRepository.DeleteVersion(versionId);
scope.Events.Dispatch(DeletedVersions, this, new DeleteRevisionsEventArgs(id, false,/* specificVersion:*/ versionId));
@@ -252,12 +252,12 @@ namespace Umbraco.Core.Services.Implement
}
}
public IEnumerable<TItem> GetAll(params Guid[] ids)
public IEnumerable<TItem> GetAll(IEnumerable<Guid> ids)
{
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
{
scope.ReadLock(ReadLockIds);
return Repository.GetMany(ids);
return Repository.GetMany(ids.ToArray());
}
}
@@ -466,6 +466,14 @@ namespace Umbraco.Core.Services.Implement
}
}
public IReadOnlyDictionary<Udi, IEnumerable<string>> GetReferences(int id)
{
using (var scope = ScopeProvider.CreateScope(autoComplete:true))
{
return _dataTypeRepository.FindUsages(id);
}
}
private void Audit(AuditType type, int userId, int objectId)
{
_auditRepository.Save(new AuditItem(objectId, type, userId, ObjectTypes.GetName(UmbracoObjectTypes.DataType)));
@@ -131,6 +131,12 @@ namespace Umbraco.Core.Services
private bool IsPropertyValueValid(PropertyType propertyType, object value)
{
var editor = _propertyEditors[propertyType.PropertyEditorAlias];
if (editor == null)
{
// nothing much we can do validation wise if the property editor has been removed.
// the property will be displayed as a label, so flagging it as invalid would be pointless.
return true;
}
var configuration = _dataTypeService.GetDataType(propertyType.DataTypeId).Configuration;
var valueEditor = editor.GetValueEditor(configuration);
return !valueEditor.Validate(value, propertyType.Mandatory, propertyType.ValidationRegExp).Any();
+28 -39
View File
@@ -724,67 +724,56 @@ namespace Umbraco.Core
/// <summary>
/// Generates a hash of a string based on the FIPS compliance setting.
/// </summary>
/// <param name="str">Refers to itself</param>
/// <returns>The hashed string</returns>
/// <param name="str">The <see cref="string" /> to hash.</param>
/// <returns>
/// The hashed string.
/// </returns>
public static string GenerateHash(this string str)
{
return CryptoConfig.AllowOnlyFipsAlgorithms
? str.ToSHA1()
: str.ToMd5();
return str.GenerateHash(CryptoConfig.AllowOnlyFipsAlgorithms ? "SHA1" : "MD5");
}
/// <summary>
/// Converts the string to MD5
/// Generate a hash of a string based on the specified hash algorithm.
/// </summary>
/// <param name="stringToConvert">Refers to itself</param>
/// <returns>The MD5 hashed string</returns>
[Obsolete("Please use the GenerateHash method instead. This may be removed in future versions")]
internal static string ToMd5(this string stringToConvert)
/// <typeparam name="T">The hash algorithm implementation to use.</typeparam>
/// <param name="str">The <see cref="string" /> to hash.</param>
/// <returns>
/// The hashed string.
/// </returns>
internal static string GenerateHash<T>(this string str)
where T : HashAlgorithm
{
return stringToConvert.GenerateHash("MD5");
return str.GenerateHash(typeof(T).FullName);
}
/// <summary>
/// Converts the string to SHA1
/// Generate a hash of a string based on the specified <paramref name="hashType" />.
/// </summary>
/// <param name="stringToConvert">refers to itself</param>
/// <returns>The SHA1 hashed string</returns>
[Obsolete("Please use the GenerateHash method instead. This may be removed in future versions")]
internal static string ToSHA1(this string stringToConvert)
/// <param name="str">The <see cref="string" /> to hash.</param>
/// <param name="hashType">The hash algorithm implementation to use.</param>
/// <returns>
/// The hashed string.
/// </returns>
/// <exception cref="System.InvalidOperationException">No hashing type found by name <paramref name="hashType" />.</exception>
/// <seealso cref="https://docs.microsoft.com/en-us/dotnet/api/system.security.cryptography.hashalgorithm.create#System_Security_Cryptography_HashAlgorithm_Create_System_String_" />
internal static string GenerateHash(this string str, string hashType)
{
return stringToConvert.GenerateHash("SHA1");
}
/// <summary>Generate a hash of a string based on the hashType passed in
/// </summary>
/// <param name="str">Refers to itself</param>
/// <param name="hashType">String with the hash type. See remarks section of the CryptoConfig Class in MSDN docs for a list of possible values.</param>
/// <returns>The hashed string</returns>
private static string GenerateHash(this string str, string hashType)
{
//create an instance of the correct hashing provider based on the type passed in
var hasher = HashAlgorithm.Create(hashType);
if (hasher == null) throw new InvalidOperationException("No hashing type found by name " + hashType);
if (hasher == null) throw new InvalidOperationException($"No hashing type found by name {hashType}.");
using (hasher)
{
//convert our string into byte array
var byteArray = Encoding.UTF8.GetBytes(str);
//get the hashed values created by our selected provider
var hashedByteArray = hasher.ComputeHash(byteArray);
//create a StringBuilder object
var stringBuilder = new StringBuilder();
//loop to each byte
var sb = new StringBuilder();
foreach (var b in hashedByteArray)
{
//append it to our StringBuilder
stringBuilder.Append(b.ToString("x2"));
sb.Append(b.ToString("x2"));
}
//return the hashed value
return stringBuilder.ToString();
return sb.ToString();
}
}
+4 -1
View File
@@ -245,6 +245,8 @@
<Compile Include="Migrations\Upgrade\V_8_0_0\DataTypes\RichTextPreValueMigrator.cs" />
<Compile Include="Migrations\Upgrade\V_8_0_0\DataTypes\DropDownFlexiblePreValueMigrator.cs" />
<Compile Include="Migrations\Upgrade\V_8_0_0\MergeDateAndDateTimePropertyEditor.cs" />
<Compile Include="Models\ContentEditing\ContentAppBadge.cs" />
<Compile Include="Models\ContentEditing\ContentAppBadgeType.cs" />
<Compile Include="Models\Entities\EntityExtensions.cs" />
<Compile Include="Migrations\Upgrade\V_8_0_0\DataTypes\PreValueMigratorBase.cs" />
<Compile Include="Migrations\Upgrade\V_8_0_0\DataTypes\PreValueDto.cs" />
@@ -263,7 +265,9 @@
<Compile Include="Migrations\Upgrade\V_8_0_0\FixLanguageIsoCodeLength.cs" />
<Compile Include="Models\CultureImpact.cs" />
<Compile Include="Models\Entities\IMediaEntitySlim.cs" />
<Compile Include="Models\Entities\IMemberEntitySlim.cs" />
<Compile Include="Models\Entities\MediaEntitySlim.cs" />
<Compile Include="Models\Entities\MemberEntitySlim.cs" />
<Compile Include="Models\PublishedContent\ILivePublishedModelFactory.cs" />
<Compile Include="Models\PublishedContent\IPublishedContentType.cs" />
<Compile Include="Models\PublishedContent\IPublishedPropertyType.cs" />
@@ -982,7 +986,6 @@
<Compile Include="Persistence\DatabaseModelDefinitions\ModificationType.cs" />
<Compile Include="Persistence\DatabaseModelDefinitions\SystemMethods.cs" />
<Compile Include="Persistence\DatabaseModelDefinitions\TableDefinition.cs" />
<Compile Include="Persistence\DatabaseNodeLockExtensions.cs" />
<Compile Include="Persistence\DbCommandExtensions.cs" />
<Compile Include="Persistence\DbConnectionExtensions.cs" />
<Compile Include="Persistence\EntityNotFoundException.cs" />
+1 -1
View File
@@ -52,7 +52,7 @@ namespace Umbraco.Examine
if (sqlContext == null) throw new ArgumentNullException(nameof(sqlContext));
_contentService = contentService ?? throw new ArgumentNullException(nameof(contentService));
_contentValueSetBuilder = contentValueSetBuilder ?? throw new ArgumentNullException(nameof(contentValueSetBuilder));
if (_publishedQuery != null)
if (_publishedQuery == null)
_publishedQuery = sqlContext.Query<IContent>().Where(x => x.Published);
_publishedValuesOnly = publishedValuesOnly;
_parentId = parentId;
@@ -0,0 +1,12 @@
using Examine;
namespace Umbraco.Examine
{
public interface IUmbracoIndexConfig
{
IContentValueSetValidator GetContentValueSetValidator();
IContentValueSetValidator GetPublishedContentValueSetValidator();
IValueSetValidator GetMemberValueSetValidator();
}
}
@@ -64,9 +64,11 @@
<Compile Include="ExamineExtensions.cs" />
<Compile Include="IContentValueSetBuilder.cs" />
<Compile Include="IContentValueSetValidator.cs" />
<Compile Include="IUmbracoIndexConfig.cs" />
<Compile Include="IIndexCreator.cs" />
<Compile Include="IIndexDiagnostics.cs" />
<Compile Include="IIndexPopulator.cs" />
<Compile Include="UmbracoIndexConfig.cs" />
<Compile Include="IndexPopulator.cs" />
<Compile Include="IndexRebuilder.cs" />
<Compile Include="IPublishedContentValueSetBuilder.cs" />
+34
View File
@@ -0,0 +1,34 @@
using Examine;
using Umbraco.Core.Services;
using Umbraco.Core.Services.Implement;
namespace Umbraco.Examine
{
public class UmbracoIndexConfig : IUmbracoIndexConfig
{
public UmbracoIndexConfig(IPublicAccessService publicAccessService)
{
PublicAccessService = publicAccessService;
}
protected IPublicAccessService PublicAccessService { get; }
public IContentValueSetValidator GetContentValueSetValidator()
{
return new ContentValueSetValidator(false, true, PublicAccessService);
}
public IContentValueSetValidator GetPublishedContentValueSetValidator()
{
return new ContentValueSetValidator(true, false, PublicAccessService);
}
/// <summary>
/// Returns the <see cref="IValueSetValidator"/> for the member indexer
/// </summary>
/// <returns></returns>
public IValueSetValidator GetMemberValueSetValidator()
{
return new MemberValueSetValidator();
}
}
}
@@ -4,6 +4,7 @@ using NUnit.Framework;
using Umbraco.Core;
using Umbraco.Core.Cache;
using Umbraco.Core.Composing;
using Umbraco.Core.IO;
using Umbraco.Core.Logging;
namespace Umbraco.Tests.Composing
@@ -35,7 +36,7 @@ namespace Umbraco.Tests.Composing
.Returns(() => factoryFactory?.Invoke(mockedFactory));
var logger = new ProfilingLogger(Mock.Of<ILogger>(), Mock.Of<IProfiler>());
var typeLoader = new TypeLoader(Mock.Of<IAppPolicyCache>(), "", logger);
var typeLoader = new TypeLoader(Mock.Of<IAppPolicyCache>(), IOHelper.MapPath("~/App_Data/TEMP"), logger);
var composition = new Composition(mockedRegister, typeLoader, logger, Mock.Of<IRuntimeState>());
// create the factory, ensure it is the mocked factory
@@ -268,7 +268,7 @@ AnotherContentFinder
public void GetDataEditors()
{
var types = _typeLoader.GetDataEditors();
Assert.AreEqual(39, types.Count());
Assert.AreEqual(38, types.Count());
}
/// <summary>
@@ -67,7 +67,7 @@ namespace Umbraco.Tests.Configurations.UmbracoSettings
[Test]
public void PreviewBadge()
{
Assert.AreEqual(SettingsSection.Content.PreviewBadge, @"<a id=""umbracoPreviewBadge"" style=""z-index:99999; position: absolute; top: 0; right: 0; border: 0; width: 149px; height: 149px; background: url('{0}/assets/img/preview-mode-badge.png') no-repeat;"" href=""#"" OnClick=""javascript:window.top.location.href = '{0}/preview/end?redir={1}'""><span style=""display:none;"">In Preview Mode - click to end</span></a>");
Assert.AreEqual(SettingsSection.Content.PreviewBadge, @"<div id=""umbracoPreviewBadge"" class=""umbraco-preview-badge""><span class=""umbraco-preview-badge__header"">Preview mode</span><a href=""{0}/preview/end?redir={1}"" class=""umbraco-preview-badge__end""><svg viewBox=""0 0 100 100"" xmlns=""http://www.w3.org/2000/svg""><title>Click to end</title><path d=""M5273.1 2400.1v-2c0-2.8-5-4-9.7-4s-9.7 1.3-9.7 4v2a7 7 0 002 4.9l5 4.9c.3.3.4.6.4 1v6.4c0 .4.2.7.6.8l2.9.9c.5.1 1-.2 1-.8v-7.2c0-.4.2-.7.4-1l5.1-5a7 7 0 002-4.9zm-9.7-.1c-4.8 0-7.4-1.3-7.5-1.8.1-.5 2.7-1.8 7.5-1.8s7.3 1.3 7.5 1.8c-.2.5-2.7 1.8-7.5 1.8z""/><path d=""M5268.4 2410.3c-.6 0-1 .4-1 1s.4 1 1 1h4.3c.6 0 1-.4 1-1s-.4-1-1-1h-4.3zM5272.7 2413.7h-4.3c-.6 0-1 .4-1 1s.4 1 1 1h4.3c.6 0 1-.4 1-1s-.4-1-1-1zM5272.7 2417h-4.3c-.6 0-1 .4-1 1s.4 1 1 1h4.3c.6 0 1-.4 1-1 0-.5-.4-1-1-1z""/><path d=""M78.2 13l-8.7 11.7a32.5 32.5 0 11-51.9 25.8c0-10.3 4.7-19.7 12.9-25.8L21.8 13a47 47 0 1056.4 0z""/><path d=""M42.7 2.5h14.6v49.4H42.7z""/></svg></a></div><style type=""text/css"">.umbraco-preview-badge {{position: absolute;top: 1em;right: 1em;display: inline-flex;background: #1b264f;color: #fff;padding: 1em;font-size: 12px;z-index: 99999999;justify-content: center;align-items: center;box-shadow: 0 10px 50px rgba(0, 0, 0, .1), 0 6px 20px rgba(0, 0, 0, .16);line-height: 1;}}.umbraco-preview-badge__header {{font-weight: bold;}}.umbraco-preview-badge__end {{width: 3em;padding: 1em;margin: -1em -1em -1em 2em;display: flex;flex-shrink: 0;align-items: center;align-self: stretch;}}.umbraco-preview-badge__end:hover,.umbraco-preview-badge__end:focus {{background: #f5c1bc;}}.umbraco-preview-badge__end svg {{fill: #fff;width:1em;}}</style>");
}
[Test]
public void ResolveUrlsFromTextString()
@@ -41,9 +41,7 @@
</notifications>
<PreviewBadge>
<![CDATA[
<a id="umbracoPreviewBadge" style="z-index:99999; position: absolute; top: 0; right: 0; border: 0; width: 149px; height: 149px; background: url('{0}/assets/img/preview-mode-badge.png') no-repeat;" href="#" OnClick="javascript:window.top.location.href = '{0}/preview/end?redir={1}'"><span style="display:none;">In Preview Mode - click to end</span></a>
]]></PreviewBadge>
<![CDATA[<div id="umbracoPreviewBadge" class="umbraco-preview-badge"><span class="umbraco-preview-badge__header">Preview mode</span><a href="{0}/preview/end?redir={1}" class="umbraco-preview-badge__end"><svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><title>Click to end</title><path d="M5273.1 2400.1v-2c0-2.8-5-4-9.7-4s-9.7 1.3-9.7 4v2a7 7 0 002 4.9l5 4.9c.3.3.4.6.4 1v6.4c0 .4.2.7.6.8l2.9.9c.5.1 1-.2 1-.8v-7.2c0-.4.2-.7.4-1l5.1-5a7 7 0 002-4.9zm-9.7-.1c-4.8 0-7.4-1.3-7.5-1.8.1-.5 2.7-1.8 7.5-1.8s7.3 1.3 7.5 1.8c-.2.5-2.7 1.8-7.5 1.8z"/><path d="M5268.4 2410.3c-.6 0-1 .4-1 1s.4 1 1 1h4.3c.6 0 1-.4 1-1s-.4-1-1-1h-4.3zM5272.7 2413.7h-4.3c-.6 0-1 .4-1 1s.4 1 1 1h4.3c.6 0 1-.4 1-1s-.4-1-1-1zM5272.7 2417h-4.3c-.6 0-1 .4-1 1s.4 1 1 1h4.3c.6 0 1-.4 1-1 0-.5-.4-1-1-1z"/><path d="M78.2 13l-8.7 11.7a32.5 32.5 0 11-51.9 25.8c0-10.3 4.7-19.7 12.9-25.8L21.8 13a47 47 0 1056.4 0z"/><path d="M42.7 2.5h14.6v49.4H42.7z"/></svg></a></div><style type="text/css">.umbraco-preview-badge {{position: absolute;top: 1em;right: 1em;display: inline-flex;background: #1b264f;color: #fff;padding: 1em;font-size: 12px;z-index: 99999999;justify-content: center;align-items: center;box-shadow: 0 10px 50px rgba(0, 0, 0, .1), 0 6px 20px rgba(0, 0, 0, .16);line-height: 1;}}.umbraco-preview-badge__header {{font-weight: bold;}}.umbraco-preview-badge__end {{width: 3em;padding: 1em;margin: -1em -1em -1em 2em;display: flex;flex-shrink: 0;align-items: center;align-self: stretch;}}.umbraco-preview-badge__end:hover,.umbraco-preview-badge__end:focus {{background: #f5c1bc;}}.umbraco-preview-badge__end svg {{fill: #fff;width:1em;}}</style>]]></PreviewBadge>
<!-- How Umbraco should handle errors during macro execution. Can be one of the following values:
- inline - show an inline error within the macro but allow the page to continue rendering. Historial Umbraco behaviour.
@@ -21,7 +21,7 @@ namespace Umbraco.Tests.LegacyXmlPublishedCache
/// <summary>
/// Implements a published snapshot service.
/// </summary>
internal class PublishedSnapshotService : PublishedSnapshotServiceBase
internal class XmlPublishedSnapshotService : PublishedSnapshotServiceBase
{
private readonly XmlStore _xmlStore;
private readonly RoutesCache _routesCache;
@@ -41,7 +41,7 @@ namespace Umbraco.Tests.LegacyXmlPublishedCache
#region Constructors
// used in WebBootManager + tests
public PublishedSnapshotService(ServiceContext serviceContext,
public XmlPublishedSnapshotService(ServiceContext serviceContext,
IPublishedContentTypeFactory publishedContentTypeFactory,
IScopeProvider scopeProvider,
IAppCache requestCache,
@@ -65,7 +65,7 @@ namespace Umbraco.Tests.LegacyXmlPublishedCache
}
// used in some tests
internal PublishedSnapshotService(ServiceContext serviceContext,
internal XmlPublishedSnapshotService(ServiceContext serviceContext,
IPublishedContentTypeFactory publishedContentTypeFactory,
IScopeProvider scopeProvider,
IAppCache requestCache,
@@ -32,7 +32,7 @@ namespace Umbraco.Tests.LegacyXmlPublishedCache
/// Represents the Xml storage for the Xml published cache.
/// </summary>
/// <remarks>
/// <para>One instance of <see cref="XmlStore"/> is instantiated by the <see cref="PublishedSnapshotService"/> and
/// <para>One instance of <see cref="XmlStore"/> is instantiated by the <see cref="XmlPublishedSnapshotService"/> and
/// then passed to all <see cref="PublishedContentCache"/> instances that are created (one per request).</para>
/// <para>This class should *not* be public.</para>
/// </remarks>
@@ -57,6 +57,9 @@ namespace Umbraco.Tests.Models
[TestCase("1,-1", "1", "1")] // was an issue
[TestCase("-1,1", "1", "1")] // was an issue
[TestCase("-1", "", "-1")]
[TestCase("", "-1", "-1")]
public void CombineStartNodes(string groupSn, string userSn, string expected)
{
// 1
+6 -7
View File
@@ -5,7 +5,6 @@ using System.Threading;
using NPoco;
using NUnit.Framework;
using Umbraco.Core;
using Umbraco.Core.Persistence;
using Umbraco.Core.Persistence.Dtos;
using Umbraco.Tests.TestHelpers;
using Umbraco.Tests.Testing;
@@ -37,7 +36,7 @@ namespace Umbraco.Tests.Persistence
{
using (var scope = ScopeProvider.CreateScope())
{
scope.Database.AcquireLockNodeReadLock(Constants.Locks.Servers);
scope.ReadLock(Constants.Locks.Servers);
scope.Complete();
}
}
@@ -62,7 +61,7 @@ namespace Umbraco.Tests.Persistence
{
try
{
scope.Database.AcquireLockNodeReadLock(Constants.Locks.Servers);
scope.ReadLock(Constants.Locks.Servers);
lock (locker)
{
acquired++;
@@ -131,7 +130,7 @@ namespace Umbraco.Tests.Persistence
if (entered == threadCount) m1.Set();
}
ms[ic].WaitOne();
scope.Database.AcquireLockNodeWriteLock(Constants.Locks.Servers);
scope.WriteLock(Constants.Locks.Servers);
lock (locker)
{
acquired++;
@@ -221,7 +220,7 @@ namespace Umbraco.Tests.Persistence
{
otherEv.WaitOne();
Console.WriteLine($"[{id1}] WAIT {id1}");
scope.Database.AcquireLockNodeWriteLock(id1);
scope.WriteLock(id1);
Console.WriteLine($"[{id1}] GRANT {id1}");
WriteLocks(scope.Database);
myEv.Set();
@@ -232,7 +231,7 @@ namespace Umbraco.Tests.Persistence
Thread.Sleep(200); // cannot wait due to deadlock... just give it a bit of time
Console.WriteLine($"[{id1}] WAIT {id2}");
scope.Database.AcquireLockNodeWriteLock(id2);
scope.WriteLock(id2);
Console.WriteLine($"[{id1}] GRANT {id2}");
WriteLocks(scope.Database);
}
@@ -284,7 +283,7 @@ namespace Umbraco.Tests.Persistence
{
otherEv.WaitOne();
Console.WriteLine($"[{id}] WAIT {id}");
scope.Database.AcquireLockNodeWriteLock(id);
scope.WriteLock(id);
Console.WriteLine($"[{id}] GRANT {id}");
WriteLocks(scope.Database);
myEv.Set();
@@ -28,6 +28,68 @@ namespace Umbraco.Tests.Persistence.Repositories
return new EntityContainerRepository(scopeAccessor, AppCaches.Disabled, Logger, Constants.ObjectTypes.DataTypeContainer);
}
[Test]
public void Can_Find_Usages()
{
var provider = TestObjects.GetScopeProvider(Logger);
using (provider.CreateScope())
{
var dtRepo = CreateRepository();
IDataType dataType1 = new DataType(new RadioButtonsPropertyEditor(Logger, ServiceContext.TextService)) { Name = "dt1" };
dtRepo.Save(dataType1);
IDataType dataType2 = new DataType(new RadioButtonsPropertyEditor(Logger, ServiceContext.TextService)) { Name = "dt2" };
dtRepo.Save(dataType2);
var ctRepo = Factory.GetInstance<IContentTypeRepository>();
IContentType ct = new ContentType(-1)
{
Alias = "ct1",
Name = "CT1",
AllowedAsRoot = true,
Icon = "icon-home",
PropertyGroups = new PropertyGroupCollection
{
new PropertyGroup(true)
{
Name = "PG1",
PropertyTypes = new PropertyTypeCollection(true)
{
new PropertyType(dataType1, "pt1")
{
Name = "PT1"
},
new PropertyType(dataType1, "pt2")
{
Name = "PT2"
},
new PropertyType(dataType2, "pt3")
{
Name = "PT3"
}
}
}
}
};
ctRepo.Save(ct);
var usages = dtRepo.FindUsages(dataType1.Id);
var key = usages.First().Key;
Assert.AreEqual(ct.Key, ((GuidUdi)key).Guid);
Assert.AreEqual(2, usages[key].Count());
Assert.AreEqual("pt1", usages[key].ElementAt(0));
Assert.AreEqual("pt2", usages[key].ElementAt(1));
usages = dtRepo.FindUsages(dataType2.Id);
key = usages.First().Key;
Assert.AreEqual(ct.Key, ((GuidUdi)key).Guid);
Assert.AreEqual(1, usages[key].Count());
Assert.AreEqual("pt3", usages[key].ElementAt(0));
}
}
[Test]
public void Can_Move()
{
@@ -1,4 +1,5 @@
using System.Globalization;
using System;
using System.Globalization;
using System.Linq;
using Moq;
using NUnit.Framework;
@@ -297,6 +298,24 @@ namespace Umbraco.Tests.Persistence.Repositories
}
}
[Test]
public void Perform_Update_With_Existing_Culture()
{
// Arrange
var provider = TestObjects.GetScopeProvider(Logger);
using (var scope = provider.CreateScope())
{
var repository = CreateRepository(provider);
// Act
var language = repository.Get(5);
language.IsoCode = "da-DK";
language.CultureName = "da-DK";
Assert.Throws<InvalidOperationException>(() => repository.Save(language));
}
}
[Test]
public void Can_Perform_Delete_On_LanguageRepository()
{
@@ -14,7 +14,7 @@ namespace Umbraco.Tests.Persistence
public void ReadLockNonExisting()
{
var provider = TestObjects.GetScopeProvider(Logger);
Assert.Throws<Exception>(() =>
Assert.Throws<ArgumentException>(() =>
{
using (var scope = provider.CreateScope())
{
@@ -39,7 +39,7 @@ namespace Umbraco.Tests.Persistence
public void WriteLockNonExisting()
{
var provider = TestObjects.GetScopeProvider(Logger);
Assert.Throws<Exception>(() =>
Assert.Throws<ArgumentException>(() =>
{
using (var scope = provider.CreateScope())
{
@@ -41,6 +41,12 @@ namespace Umbraco.Tests.PublishedContent
private ContentType _contentTypeVariant;
private TestDataSource _source;
[TearDown]
public void Teardown()
{
_snapshotService?.Dispose();
}
private void Init(IEnumerable<ContentNodeKit> kits)
{
Current.Reset();
@@ -213,22 +219,24 @@ namespace Umbraco.Tests.PublishedContent
var level = path.Count(x => x == ',');
var now = DateTime.Now;
var contentData = new ContentData
{
Name = "N" + id,
Published = true,
TemplateId = 0,
VersionId = 1,
VersionDate = now,
WriterId = 0,
Properties = new Dictionary<string, PropertyData[]>(),
CultureInfos = new Dictionary<string, CultureVariation>()
};
return new ContentNodeKit
{
ContentTypeId = _contentTypeInvariant.Id,
Node = new ContentNode(id, Guid.NewGuid(), level, path, sortOrder, parentId, DateTime.Now, 0),
DraftData = null,
PublishedData = new ContentData
{
Name = "N" + id,
Published = true,
TemplateId = 0,
VersionId = 1,
VersionDate = now,
WriterId = 0,
Properties = new Dictionary<string, PropertyData[]>(),
CultureInfos = new Dictionary<string, CultureVariation>()
}
PublishedData = contentData
};
}
@@ -333,7 +341,7 @@ namespace Umbraco.Tests.PublishedContent
CultureInfos = GetCultureInfos(id, now)
};
var withDraft = id%2==0;
var withDraft = id % 2 == 0;
var withPublished = !withDraft;
return new ContentNodeKit
@@ -1063,6 +1071,153 @@ namespace Umbraco.Tests.PublishedContent
AssertLinkedNode(child3.contentNode, 1, 2, -1, -1, -1);
}
[Test]
public void Refresh_Node_Ensures_Linked_list()
{
// NOTE: these tests are not using real scopes, in which case a Scope does not control
// how the snapshots generations work. We are forcing new snapshot generations manually.
IEnumerable<ContentNodeKit> GetKits()
{
var paths = new Dictionary<int, string> { { -1, "-1" } };
//root
yield return CreateInvariantKit(100, -1, 1, paths);
//site
yield return CreateInvariantKit(2, 100, 1, paths);
yield return CreateInvariantKit(1, 100, 2, paths); //middle child
yield return CreateInvariantKit(3, 100, 3, paths);
//children of 1
yield return CreateInvariantKit(20, 1, 1, paths);
yield return CreateInvariantKit(30, 1, 2, paths);
yield return CreateInvariantKit(40, 1, 3, paths);
}
Init(GetKits());
var snapshotService = (PublishedSnapshotService)_snapshotService;
var contentStore = snapshotService.GetContentStore();
Assert.AreEqual(1, contentStore.Test.LiveGen);
Assert.IsTrue(contentStore.Test.NextGen);
var middleNode = contentStore.Test.GetValues(1)[0];
Assert.AreEqual(1, middleNode.gen);
AssertLinkedNode(middleNode.contentNode, 100, 2, 3, 20, 40);
//This will set a flag to force creating a new Gen next time the store is locked (i.e. In Notify)
contentStore.CreateSnapshot();
Assert.IsFalse(contentStore.Test.NextGen);
_snapshotService.Notify(new[]
{
new ContentCacheRefresher.JsonPayload(1, Guid.Empty, TreeChangeTypes.RefreshNode)
}, out _, out _);
Assert.AreEqual(2, contentStore.Test.LiveGen);
Assert.IsTrue(contentStore.Test.NextGen);
middleNode = contentStore.Test.GetValues(1)[0];
Assert.AreEqual(2, middleNode.gen);
AssertLinkedNode(middleNode.contentNode, 100, 2, 3, 20, 40);
}
/// <summary>
/// This addresses issue: https://github.com/umbraco/Umbraco-CMS/issues/6698
/// </summary>
/// <remarks>
/// This test mimics if someone were to:
/// 1) Unpublish a "middle child"
/// 2) Save and publish it
/// 3) Publish it with descendants
/// 4) Repeat steps 2 and 3
///
/// Which has caused an exception. To replicate this test:
/// 1) RefreshBranch with kits for a branch where the top most node is unpublished
/// 2) RefreshBranch with kits for the branch where the top most node is published
/// 3) RefreshBranch with kits for the branch where the top most node is published
/// 4) RefreshNode
/// 5) RefreshBranch with kits for the branch where the top most node is published
/// </remarks>
[Test]
public void Refresh_Branch_With_Alternating_Publish_Flags()
{
// NOTE: these tests are not using real scopes, in which case a Scope does not control
// how the snapshots generations work. We are forcing new snapshot generations manually.
IEnumerable<ContentNodeKit> GetKits()
{
var paths = new Dictionary<int, string> { { -1, "-1" } };
//root
yield return CreateInvariantKit(100, -1, 1, paths);
//site
yield return CreateInvariantKit(2, 100, 1, paths);
yield return CreateInvariantKit(1, 100, 2, paths); //middle child
yield return CreateInvariantKit(3, 100, 3, paths);
//children of 1
yield return CreateInvariantKit(20, 1, 1, paths);
yield return CreateInvariantKit(30, 1, 2, paths);
yield return CreateInvariantKit(40, 1, 3, paths);
}
//init with all published
Init(GetKits());
var snapshotService = (PublishedSnapshotService)_snapshotService;
var contentStore = snapshotService.GetContentStore();
var rootKit = _source.Kits[1].Clone();
void ChangePublishFlagOfRoot(bool published, int assertGen, TreeChangeTypes changeType)
{
//This will set a flag to force creating a new Gen next time the store is locked (i.e. In Notify)
contentStore.CreateSnapshot();
Assert.IsFalse(contentStore.Test.NextGen);
//Change the root publish flag
var kit = rootKit.Clone();
kit.DraftData = published ? null : kit.PublishedData;
kit.PublishedData = published? kit.PublishedData : null;
_source.Kits[1] = kit;
_snapshotService.Notify(new[]
{
new ContentCacheRefresher.JsonPayload(1, Guid.Empty, changeType)
}, out _, out _);
Assert.AreEqual(assertGen, contentStore.Test.LiveGen);
Assert.IsTrue(contentStore.Test.NextGen);
//get the latest gen for content Id 1
var (gen, contentNode) = contentStore.Test.GetValues(1)[0];
Assert.AreEqual(assertGen, gen);
//even when unpublishing/re-publishing/etc... the linked list is always maintained
AssertLinkedNode(contentNode, 100, 2, 3, 20, 40);
}
//unpublish the root
ChangePublishFlagOfRoot(false, 2, TreeChangeTypes.RefreshBranch);
//publish the root (since it's not published, it will cause a RefreshBranch)
ChangePublishFlagOfRoot(true, 3, TreeChangeTypes.RefreshBranch);
//publish root + descendants
ChangePublishFlagOfRoot(true, 4, TreeChangeTypes.RefreshBranch);
//save/publish the root (since it's already published, it will just cause a RefreshNode
ChangePublishFlagOfRoot(true, 5, TreeChangeTypes.RefreshNode);
//publish root + descendants
ChangePublishFlagOfRoot(true, 6, TreeChangeTypes.RefreshBranch);
}
[Test]
public void Refresh_Branch_Ensures_Linked_List()
{
@@ -36,6 +36,12 @@ namespace Umbraco.Tests.PublishedContent
private ContentType _contentType;
private PropertyType _propertyType;
[TearDown]
public void Teardown()
{
_snapshotService?.Dispose();
}
private void Init()
{
Current.Reset();
@@ -303,5 +309,6 @@ namespace Umbraco.Tests.PublishedContent
Assert.IsFalse(c2.IsPublished("dk-DA"));
Assert.IsTrue(c2.IsPublished("de-DE"));
}
}
}
+1 -1
View File
@@ -73,7 +73,7 @@ namespace Umbraco.Tests.Scoping
// xmlStore.Xml - the actual main xml document
// publishedContentCache.GetXml() - the captured xml
private static XmlStore XmlStore => (Current.Factory.GetInstance<IPublishedSnapshotService>() as PublishedSnapshotService).XmlStore;
private static XmlStore XmlStore => (Current.Factory.GetInstance<IPublishedSnapshotService>() as XmlPublishedSnapshotService).XmlStore;
private static XmlDocument XmlMaster => XmlStore.Xml;
private static XmlDocument XmlInContext => ((PublishedContentCache) Umbraco.Web.Composing.Current.UmbracoContext.Content).GetXml(false);
@@ -105,13 +105,26 @@ namespace Umbraco.Tests.Services
}
}
[Test]
public void Change_Content_Type_Variation_Clears_Redirects()
[TestCase(ContentVariation.Nothing, ContentVariation.Nothing, false)]
[TestCase(ContentVariation.Nothing, ContentVariation.Culture, true)]
[TestCase(ContentVariation.Nothing, ContentVariation.CultureAndSegment, true)]
[TestCase(ContentVariation.Nothing, ContentVariation.Segment, true)]
[TestCase(ContentVariation.Culture, ContentVariation.Nothing, true)]
[TestCase(ContentVariation.Culture, ContentVariation.Culture, false)]
[TestCase(ContentVariation.Culture, ContentVariation.Segment, true)]
[TestCase(ContentVariation.Culture, ContentVariation.CultureAndSegment, true)]
[TestCase(ContentVariation.Segment, ContentVariation.Nothing, true)]
[TestCase(ContentVariation.Segment, ContentVariation.Culture, true)]
[TestCase(ContentVariation.Segment, ContentVariation.Segment, false)]
[TestCase(ContentVariation.Segment, ContentVariation.CultureAndSegment, true)]
[TestCase(ContentVariation.CultureAndSegment, ContentVariation.Nothing, true)]
[TestCase(ContentVariation.CultureAndSegment, ContentVariation.Culture, true)]
[TestCase(ContentVariation.CultureAndSegment, ContentVariation.Segment, true)]
[TestCase(ContentVariation.CultureAndSegment, ContentVariation.CultureAndSegment, false)]
public void Change_Content_Type_Variation_Clears_Redirects(ContentVariation startingContentTypeVariation, ContentVariation changedContentTypeVariation, bool shouldUrlRedirectsBeCleared)
{
var contentType = MockedContentTypes.CreateBasicContentType();
contentType.Variations = ContentVariation.Nothing;
var properties = CreatePropertyCollection(("title", ContentVariation.Nothing));
contentType.PropertyGroups.Add(new PropertyGroup(properties) { Name = "Content" });
var contentType = MockedContentTypes.CreateBasicContentType();
contentType.Variations = startingContentTypeVariation;
ServiceContext.ContentTypeService.Save(contentType);
var contentType2 = MockedContentTypes.CreateBasicContentType("test");
ServiceContext.ContentTypeService.Save(contentType2);
@@ -119,6 +132,11 @@ namespace Umbraco.Tests.Services
//create some content of this content type
IContent doc = MockedContent.CreateBasicContent(contentType);
doc.Name = "Hello1";
if(startingContentTypeVariation.HasFlag(ContentVariation.Culture))
{
doc.SetCultureName(doc.Name, "en-US");
}
ServiceContext.ContentService.Save(doc);
IContent doc2 = MockedContent.CreateBasicContent(contentType2);
@@ -127,24 +145,27 @@ namespace Umbraco.Tests.Services
ServiceContext.RedirectUrlService.Register("hello/world", doc.Key);
ServiceContext.RedirectUrlService.Register("hello2/world2", doc2.Key);
// These 2 assertions should probably be moved to a test for the Register() method?
Assert.AreEqual(1, ServiceContext.RedirectUrlService.GetContentRedirectUrls(doc.Key).Count());
Assert.AreEqual(1, ServiceContext.RedirectUrlService.GetContentRedirectUrls(doc2.Key).Count());
//change variation
contentType.Variations = ContentVariation.Culture;
contentType.Variations = changedContentTypeVariation;
ServiceContext.ContentTypeService.Save(contentType);
Assert.AreEqual(0, ServiceContext.RedirectUrlService.GetContentRedirectUrls(doc.Key).Count());
var expectedRedirectUrlCount = shouldUrlRedirectsBeCleared ? 0 : 1;
Assert.AreEqual(expectedRedirectUrlCount, ServiceContext.RedirectUrlService.GetContentRedirectUrls(doc.Key).Count());
Assert.AreEqual(1, ServiceContext.RedirectUrlService.GetContentRedirectUrls(doc2.Key).Count());
}
[Test]
public void Change_Content_Type_From_Invariant_Variant()
{
[TestCase(ContentVariation.Nothing, ContentVariation.Culture)]
[TestCase(ContentVariation.Nothing, ContentVariation.CultureAndSegment)]
[TestCase(ContentVariation.Segment, ContentVariation.Culture)]
[TestCase(ContentVariation.Segment, ContentVariation.CultureAndSegment)]
public void Change_Content_Type_From_No_Culture_To_Culture(ContentVariation from, ContentVariation to)
{
var contentType = MockedContentTypes.CreateBasicContentType();
contentType.Variations = ContentVariation.Nothing;
var properties = CreatePropertyCollection(("title", ContentVariation.Nothing));
contentType.Variations = from;
var properties = CreatePropertyCollection(("title", from));
contentType.PropertyGroups.Add(new PropertyGroup(properties) { Name = "Content" });
ServiceContext.ContentTypeService.Save(contentType);
@@ -159,12 +180,12 @@ namespace Umbraco.Tests.Services
Assert.AreEqual("Hello1", doc.Name);
Assert.AreEqual("hello world", doc.GetValue("title"));
Assert.IsTrue(doc.Edited);
Assert.IsFalse (doc.IsCultureEdited("en-US"));
Assert.IsFalse(doc.IsCultureEdited("en-US"));
//change the content type to be variant, we will also update the name here to detect the copy changes
doc.Name = "Hello2";
ServiceContext.ContentService.Save(doc);
contentType.Variations = ContentVariation.Culture;
contentType.Variations = to;
ServiceContext.ContentTypeService.Save(contentType);
doc = ServiceContext.ContentService.GetById(doc.Id); //re-get
@@ -176,7 +197,7 @@ namespace Umbraco.Tests.Services
//change back property type to be invariant, we will also update the name here to detect the copy changes
doc.SetCultureName("Hello3", "en-US");
ServiceContext.ContentService.Save(doc);
contentType.Variations = ContentVariation.Nothing;
contentType.Variations = from;
ServiceContext.ContentTypeService.Save(contentType);
doc = ServiceContext.ContentService.GetById(doc.Id); //re-get
@@ -186,12 +207,15 @@ namespace Umbraco.Tests.Services
Assert.IsFalse(doc.IsCultureEdited("en-US"));
}
[Test]
public void Change_Content_Type_From_Variant_Invariant()
[TestCase(ContentVariation.Culture, ContentVariation.Nothing)]
[TestCase(ContentVariation.Culture, ContentVariation.Segment)]
[TestCase(ContentVariation.CultureAndSegment, ContentVariation.Nothing)]
[TestCase(ContentVariation.CultureAndSegment, ContentVariation.Segment)]
public void Change_Content_Type_From_Culture_To_No_Culture(ContentVariation startingContentTypeVariation, ContentVariation changeContentTypeVariationTo)
{
var contentType = MockedContentTypes.CreateBasicContentType();
contentType.Variations = ContentVariation.Culture;
var properties = CreatePropertyCollection(("title", ContentVariation.Culture));
contentType.Variations = startingContentTypeVariation;
var properties = CreatePropertyCollection(("title", startingContentTypeVariation));
contentType.PropertyGroups.Add(new PropertyGroup(properties) { Name = "Content" });
ServiceContext.ContentTypeService.Save(contentType);
@@ -210,7 +234,7 @@ namespace Umbraco.Tests.Services
//change the content type to be invariant, we will also update the name here to detect the copy changes
doc.SetCultureName("Hello2", "en-US");
ServiceContext.ContentService.Save(doc);
contentType.Variations = ContentVariation.Nothing;
contentType.Variations = changeContentTypeVariationTo;
ServiceContext.ContentTypeService.Save(contentType);
doc = ServiceContext.ContentService.GetById(doc.Id); //re-get
@@ -222,19 +246,20 @@ namespace Umbraco.Tests.Services
//change back property type to be variant, we will also update the name here to detect the copy changes
doc.Name = "Hello3";
ServiceContext.ContentService.Save(doc);
contentType.Variations = ContentVariation.Culture;
contentType.Variations = startingContentTypeVariation;
ServiceContext.ContentTypeService.Save(contentType);
doc = ServiceContext.ContentService.GetById(doc.Id); //re-get
//at this stage all property types were switched to invariant so even though the variant value
//exists it will not be returned because the property type is invariant,
//so this check proves that null will be returned
Assert.AreEqual("Hello3", doc.Name);
Assert.IsNull(doc.GetValue("title", "en-US"));
Assert.IsTrue(doc.Edited);
Assert.IsTrue(doc.IsCultureEdited("en-US")); // this is true because the name change is copied to the default language
//we can now switch the property type to be variant and the value can be returned again
contentType.PropertyTypes.First().Variations = ContentVariation.Culture;
contentType.PropertyTypes.First().Variations = startingContentTypeVariation;
ServiceContext.ContentTypeService.Save(contentType);
doc = ServiceContext.ContentService.GetById(doc.Id); //re-get
@@ -242,32 +267,129 @@ namespace Umbraco.Tests.Services
Assert.AreEqual("hello world", doc.GetValue("title", "en-US"));
Assert.IsTrue(doc.Edited);
Assert.IsTrue(doc.IsCultureEdited("en-US"));
}
[Test]
public void Change_Property_Type_From_To_Variant_On_Invariant_Content_Type()
[TestCase(ContentVariation.Nothing, ContentVariation.Nothing)]
[TestCase(ContentVariation.Nothing, ContentVariation.Culture)]
[TestCase(ContentVariation.Nothing, ContentVariation.Segment)]
[TestCase(ContentVariation.Nothing, ContentVariation.CultureAndSegment)]
[TestCase(ContentVariation.Culture, ContentVariation.Nothing)]
[TestCase(ContentVariation.Culture, ContentVariation.Culture)]
[TestCase(ContentVariation.Culture, ContentVariation.Segment)]
[TestCase(ContentVariation.Culture, ContentVariation.CultureAndSegment)]
[TestCase(ContentVariation.Segment, ContentVariation.Nothing)]
[TestCase(ContentVariation.Segment, ContentVariation.Culture)]
[TestCase(ContentVariation.Segment, ContentVariation.Segment)]
[TestCase(ContentVariation.Segment, ContentVariation.CultureAndSegment)]
[TestCase(ContentVariation.CultureAndSegment, ContentVariation.Nothing)]
[TestCase(ContentVariation.CultureAndSegment, ContentVariation.Culture)]
[TestCase(ContentVariation.CultureAndSegment, ContentVariation.Segment)]
[TestCase(ContentVariation.CultureAndSegment, ContentVariation.CultureAndSegment)]
public void Preserve_Content_Name_After_Content_Type_Variation_Change(ContentVariation contentTypeVariationFrom, ContentVariation contentTypeVariationTo)
{
var contentType = MockedContentTypes.CreateBasicContentType();
contentType.Variations = ContentVariation.Nothing;
var properties = CreatePropertyCollection(("title", ContentVariation.Nothing));
contentType.Variations = contentTypeVariationFrom;
ServiceContext.ContentTypeService.Save(contentType);
var invariantContentName = "Content Invariant";
var defaultCultureContentName = "Content en-US";
var defaultCulture = "en-US";
var nlContentName = "Content nl-NL";
var nlCulture = "nl-NL";
ServiceContext.LocalizationService.Save(new Language(nlCulture));
var includeCultureNames = contentType.Variations.HasFlag(ContentVariation.Culture);
// Create some content of this content type
IContent doc = MockedContent.CreateBasicContent(contentType);
doc.Name = invariantContentName;
if (includeCultureNames)
{
Assert.DoesNotThrow(() => doc.SetCultureName(defaultCultureContentName, defaultCulture));
Assert.DoesNotThrow(() => doc.SetCultureName(nlContentName, nlCulture));
} else
{
Assert.Throws<NotSupportedException>(() => doc.SetCultureName(defaultCultureContentName, defaultCulture));
Assert.Throws<NotSupportedException>(() => doc.SetCultureName(nlContentName, nlCulture));
}
ServiceContext.ContentService.Save(doc);
doc = ServiceContext.ContentService.GetById(doc.Id);
AssertAll();
// Change variation
contentType.Variations = contentTypeVariationTo;
ServiceContext.ContentService.Save(doc);
doc = ServiceContext.ContentService.GetById(doc.Id);
AssertAll();
void AssertAll()
{
if (includeCultureNames)
{
// Invariant content name is not preserved when content type is set to culture
Assert.AreEqual(defaultCultureContentName, doc.Name);
Assert.AreEqual(doc.Name, doc.GetCultureName(defaultCulture));
Assert.AreEqual(nlContentName, doc.GetCultureName(nlCulture));
}
else
{
Assert.AreEqual(invariantContentName, doc.Name);
Assert.AreEqual(null, doc.GetCultureName(defaultCulture));
Assert.AreEqual(null, doc.GetCultureName(nlCulture));
}
}
}
[TestCase(ContentVariation.Nothing, ContentVariation.Nothing)]
[TestCase(ContentVariation.Nothing, ContentVariation.Culture)]
[TestCase(ContentVariation.Nothing, ContentVariation.Segment)]
[TestCase(ContentVariation.Nothing, ContentVariation.CultureAndSegment)]
[TestCase(ContentVariation.Culture, ContentVariation.Nothing)]
[TestCase(ContentVariation.Culture, ContentVariation.Culture)]
[TestCase(ContentVariation.Culture, ContentVariation.Segment)]
[TestCase(ContentVariation.Culture, ContentVariation.CultureAndSegment)]
[TestCase(ContentVariation.Segment, ContentVariation.Nothing)]
[TestCase(ContentVariation.Segment, ContentVariation.Culture)]
[TestCase(ContentVariation.Segment, ContentVariation.Segment)]
[TestCase(ContentVariation.Segment, ContentVariation.CultureAndSegment)]
[TestCase(ContentVariation.CultureAndSegment, ContentVariation.Nothing)]
[TestCase(ContentVariation.CultureAndSegment, ContentVariation.Culture)]
[TestCase(ContentVariation.CultureAndSegment, ContentVariation.Segment)]
[TestCase(ContentVariation.CultureAndSegment, ContentVariation.CultureAndSegment)]
public void Verify_If_Property_Type_Variation_Is_Correctly_Corrected_When_Content_Type_Is_Updated(ContentVariation contentTypeVariation, ContentVariation propertyTypeVariation)
{
var contentType = MockedContentTypes.CreateBasicContentType();
// We test an updated content type so it has to be saved first.
ServiceContext.ContentTypeService.Save(contentType);
// Update it
contentType.Variations = contentTypeVariation;
var properties = CreatePropertyCollection(("title", propertyTypeVariation));
contentType.PropertyGroups.Add(new PropertyGroup(properties) { Name = "Content" });
ServiceContext.ContentTypeService.Save(contentType);
//change the property type to be variant
contentType.PropertyTypes.First().Variations = ContentVariation.Culture;
//Cannot change a property type to be variant if the content type itself is not variant
Assert.Throws<InvalidOperationException>(() => ServiceContext.ContentTypeService.Save(contentType));
// Check if property type variations have been updated correctly
Assert.AreEqual(properties.First().Variations, contentTypeVariation & propertyTypeVariation);
}
[Test]
public void Change_Property_Type_From_Invariant_Variant()
[TestCase(ContentVariation.Nothing, ContentVariation.Culture)]
[TestCase(ContentVariation.Nothing, ContentVariation.CultureAndSegment)]
[TestCase(ContentVariation.Segment, ContentVariation.Culture)]
[TestCase(ContentVariation.Segment, ContentVariation.CultureAndSegment)]
public void Change_Property_Type_From_Invariant_Variant(ContentVariation invariant, ContentVariation variant)
{
var contentType = MockedContentTypes.CreateBasicContentType();
contentType.Variations = ContentVariation.Culture;
var properties = CreatePropertyCollection(("title", ContentVariation.Nothing));
// content type supports all variations
contentType.Variations = ContentVariation.Culture | ContentVariation.Segment;
var properties = CreatePropertyCollection(("title", invariant));
contentType.PropertyGroups.Add(new PropertyGroup(properties) { Name = "Content" });
ServiceContext.ContentTypeService.Save(contentType);
@@ -283,7 +405,7 @@ namespace Umbraco.Tests.Services
Assert.IsTrue(doc.Edited);
//change the property type to be variant
contentType.PropertyTypes.First().Variations = ContentVariation.Culture;
contentType.PropertyTypes.First().Variations = variant;
ServiceContext.ContentTypeService.Save(contentType);
doc = ServiceContext.ContentService.GetById(doc.Id); //re-get
@@ -292,7 +414,7 @@ namespace Umbraco.Tests.Services
Assert.IsTrue(doc.Edited);
//change back property type to be invariant
contentType.PropertyTypes.First().Variations = ContentVariation.Nothing;
contentType.PropertyTypes.First().Variations = invariant;
ServiceContext.ContentTypeService.Save(contentType);
doc = ServiceContext.ContentService.GetById(doc.Id); //re-get
@@ -301,13 +423,17 @@ namespace Umbraco.Tests.Services
Assert.IsTrue(doc.Edited);
}
[Test]
public void Change_Property_Type_From_Variant_Invariant()
[TestCase(ContentVariation.Culture, ContentVariation.Nothing)]
[TestCase(ContentVariation.Culture, ContentVariation.Segment)]
[TestCase(ContentVariation.CultureAndSegment, ContentVariation.Nothing)]
[TestCase(ContentVariation.CultureAndSegment, ContentVariation.Segment)]
public void Change_Property_Type_From_Variant_Invariant(ContentVariation variant, ContentVariation invariant)
{
//create content type with a property type that varies by culture
var contentType = MockedContentTypes.CreateBasicContentType();
contentType.Variations = ContentVariation.Culture;
var properties = CreatePropertyCollection(("title", ContentVariation.Culture));
// content type supports all variations
contentType.Variations = ContentVariation.Culture | ContentVariation.Segment;
var properties = CreatePropertyCollection(("title", variant));
contentType.PropertyGroups.Add(new PropertyGroup(properties) { Name = "Content" });
ServiceContext.ContentTypeService.Save(contentType);
@@ -320,33 +446,37 @@ namespace Umbraco.Tests.Services
Assert.AreEqual("hello world", doc.GetValue("title", "en-US"));
//change the property type to be invariant
contentType.PropertyTypes.First().Variations = ContentVariation.Nothing;
contentType.PropertyTypes.First().Variations = invariant;
ServiceContext.ContentTypeService.Save(contentType);
doc = ServiceContext.ContentService.GetById(doc.Id); //re-get
Assert.AreEqual("hello world", doc.GetValue("title"));
//change back property type to be variant
contentType.PropertyTypes.First().Variations = ContentVariation.Culture;
contentType.PropertyTypes.First().Variations = variant;
ServiceContext.ContentTypeService.Save(contentType);
doc = ServiceContext.ContentService.GetById(doc.Id); //re-get
Assert.AreEqual("hello world", doc.GetValue("title", "en-US"));
}
[Test]
public void Change_Property_Type_From_Variant_Invariant_On_A_Composition()
[TestCase(ContentVariation.Culture, ContentVariation.Nothing)]
[TestCase(ContentVariation.Culture, ContentVariation.Segment)]
[TestCase(ContentVariation.CultureAndSegment, ContentVariation.Nothing)]
[TestCase(ContentVariation.CultureAndSegment, ContentVariation.Segment)]
public void Change_Property_Type_From_Variant_Invariant_On_A_Composition(ContentVariation variant, ContentVariation invariant)
{
//create content type with a property type that varies by culture
var contentType = MockedContentTypes.CreateBasicContentType();
contentType.Variations = ContentVariation.Culture;
var properties = CreatePropertyCollection(("title", ContentVariation.Culture));
// content type supports all variations
contentType.Variations = ContentVariation.Culture | ContentVariation.Segment;
var properties = CreatePropertyCollection(("title", variant));
contentType.PropertyGroups.Add(new PropertyGroup(properties) { Name = "Content" });
ServiceContext.ContentTypeService.Save(contentType);
//compose this from the other one
var contentType2 = MockedContentTypes.CreateBasicContentType("test");
contentType2.Variations = ContentVariation.Culture;
contentType2.Variations = contentType.Variations;
contentType2.AddContentType(contentType);
ServiceContext.ContentTypeService.Save(contentType2);
@@ -362,7 +492,7 @@ namespace Umbraco.Tests.Services
ServiceContext.ContentService.Save(doc2);
//change the property type to be invariant
contentType.PropertyTypes.First().Variations = ContentVariation.Nothing;
contentType.PropertyTypes.First().Variations = invariant;
ServiceContext.ContentTypeService.Save(contentType);
doc = ServiceContext.ContentService.GetById(doc.Id); //re-get
doc2 = ServiceContext.ContentService.GetById(doc2.Id); //re-get
@@ -371,7 +501,7 @@ namespace Umbraco.Tests.Services
Assert.AreEqual("hello world", doc2.GetValue("title"));
//change back property type to be variant
contentType.PropertyTypes.First().Variations = ContentVariation.Culture;
contentType.PropertyTypes.First().Variations = variant;
ServiceContext.ContentTypeService.Save(contentType);
doc = ServiceContext.ContentService.GetById(doc.Id); //re-get
doc2 = ServiceContext.ContentService.GetById(doc2.Id); //re-get
@@ -380,19 +510,22 @@ namespace Umbraco.Tests.Services
Assert.AreEqual("hello world", doc2.GetValue("title", "en-US"));
}
[Test]
public void Change_Content_Type_From_Variant_Invariant_On_A_Composition()
[TestCase(ContentVariation.Culture, ContentVariation.Nothing)]
[TestCase(ContentVariation.Culture, ContentVariation.Segment)]
[TestCase(ContentVariation.CultureAndSegment, ContentVariation.Nothing)]
[TestCase(ContentVariation.CultureAndSegment, ContentVariation.Segment)]
public void Change_Content_Type_From_Variant_Invariant_On_A_Composition(ContentVariation variant, ContentVariation invariant)
{
//create content type with a property type that varies by culture
var contentType = MockedContentTypes.CreateBasicContentType();
contentType.Variations = ContentVariation.Culture;
contentType.Variations = variant;
var properties = CreatePropertyCollection(("title", ContentVariation.Culture));
contentType.PropertyGroups.Add(new PropertyGroup(properties) { Name = "Content" });
ServiceContext.ContentTypeService.Save(contentType);
//compose this from the other one
var contentType2 = MockedContentTypes.CreateBasicContentType("test");
contentType2.Variations = ContentVariation.Culture;
contentType2.Variations = contentType.Variations;
contentType2.AddContentType(contentType);
ServiceContext.ContentTypeService.Save(contentType2);
@@ -408,7 +541,7 @@ namespace Umbraco.Tests.Services
ServiceContext.ContentService.Save(doc2);
//change the content type to be invariant
contentType.Variations = ContentVariation.Nothing;
contentType.Variations = invariant;
ServiceContext.ContentTypeService.Save(contentType);
doc = ServiceContext.ContentService.GetById(doc.Id); //re-get
doc2 = ServiceContext.ContentService.GetById(doc2.Id); //re-get
@@ -417,7 +550,7 @@ namespace Umbraco.Tests.Services
Assert.AreEqual("hello world", doc2.GetValue("title"));
//change back content type to be variant
contentType.Variations = ContentVariation.Culture;
contentType.Variations = variant;
ServiceContext.ContentTypeService.Save(contentType);
doc = ServiceContext.ContentService.GetById(doc.Id); //re-get
doc2 = ServiceContext.ContentService.GetById(doc2.Id); //re-get
@@ -693,22 +826,25 @@ namespace Umbraco.Tests.Services
"{'properties':{'value1':[{'culture':'en','seg':'','val':'v1en'},{'culture':'fr','seg':'','val':'v1fr'}],'value2':[{'culture':'en','seg':'','val':'v2'}]},'cultureData':");
}
[Test]
public void Change_Property_Variations_From_Variant_To_Invariant_And_Ensure_Edited_Values_Are_Renormalized()
[TestCase(ContentVariation.Culture, ContentVariation.Nothing)]
[TestCase(ContentVariation.Culture, ContentVariation.Segment)]
[TestCase(ContentVariation.CultureAndSegment, ContentVariation.Nothing)]
[TestCase(ContentVariation.CultureAndSegment, ContentVariation.Segment)]
public void Change_Property_Variations_From_Variant_To_Invariant_And_Ensure_Edited_Values_Are_Renormalized(ContentVariation variant, ContentVariation invariant)
{
// one simple content type, variant, with both variant and invariant properties
// can change an invariant property to variant and back
CreateFrenchAndEnglishLangs();
var contentType = CreateContentType(ContentVariation.Culture);
var contentType = CreateContentType(ContentVariation.Culture | ContentVariation.Segment);
var properties = CreatePropertyCollection(("value1", ContentVariation.Culture));
var properties = CreatePropertyCollection(("value1", variant));
contentType.PropertyGroups.Add(new PropertyGroup(properties) { Name = "Content" });
ServiceContext.ContentTypeService.Save(contentType);
var document = (IContent)new Content("document", -1, contentType);
IContent document = new Content("document", -1, contentType);
document.SetCultureName("doc1en", "en");
document.SetCultureName("doc1fr", "fr");
document.SetValue("value1", "v1en-init", "en");
@@ -737,7 +873,7 @@ namespace Umbraco.Tests.Services
Assert.IsTrue(document.Edited);
// switch property type to Invariant
contentType.PropertyTypes.First(x => x.Alias == "value1").Variations = ContentVariation.Nothing;
contentType.PropertyTypes.First(x => x.Alias == "value1").Variations = invariant;
ServiceContext.ContentTypeService.Save(contentType); //This is going to have to re-normalize the "Edited" flag
document = ServiceContext.ContentService.GetById(document.Id);
@@ -764,7 +900,7 @@ namespace Umbraco.Tests.Services
Assert.IsFalse(document.Edited);
// switch property back to Culture
contentType.PropertyTypes.First(x => x.Alias == "value1").Variations = ContentVariation.Culture;
contentType.PropertyTypes.First(x => x.Alias == "value1").Variations = variant;
ServiceContext.ContentTypeService.Save(contentType);
document = ServiceContext.ContentService.GetById(document.Id);
@@ -793,17 +929,20 @@ namespace Umbraco.Tests.Services
Assert.IsFalse(document.Edited);
}
[Test]
public void Change_Property_Variations_From_Invariant_To_Variant_And_Ensure_Edited_Values_Are_Renormalized()
[TestCase(ContentVariation.Nothing, ContentVariation.Culture)]
[TestCase(ContentVariation.Nothing, ContentVariation.CultureAndSegment)]
[TestCase(ContentVariation.Segment, ContentVariation.Culture)]
[TestCase(ContentVariation.Segment, ContentVariation.CultureAndSegment)]
public void Change_Property_Variations_From_Invariant_To_Variant_And_Ensure_Edited_Values_Are_Renormalized(ContentVariation invariant, ContentVariation variant)
{
// one simple content type, variant, with both variant and invariant properties
// can change an invariant property to variant and back
CreateFrenchAndEnglishLangs();
var contentType = CreateContentType(ContentVariation.Culture);
var contentType = CreateContentType(ContentVariation.Culture | ContentVariation.Segment);
var properties = CreatePropertyCollection(("value1", ContentVariation.Nothing));
var properties = CreatePropertyCollection(("value1", invariant));
contentType.PropertyGroups.Add(new PropertyGroup(properties) { Name = "Content" });
ServiceContext.ContentTypeService.Save(contentType);
@@ -833,7 +972,7 @@ namespace Umbraco.Tests.Services
Assert.IsTrue(document.Edited);
// switch property type to Culture
contentType.PropertyTypes.First(x => x.Alias == "value1").Variations = ContentVariation.Culture;
contentType.PropertyTypes.First(x => x.Alias == "value1").Variations = variant;
ServiceContext.ContentTypeService.Save(contentType); //This is going to have to re-normalize the "Edited" flag
document = ServiceContext.ContentService.GetById(document.Id);
@@ -858,7 +997,7 @@ namespace Umbraco.Tests.Services
Assert.IsFalse(document.Edited);
// switch property back to Invariant
contentType.PropertyTypes.First(x => x.Alias == "value1").Variations = ContentVariation.Nothing;
contentType.PropertyTypes.First(x => x.Alias == "value1").Variations = invariant;
ServiceContext.ContentTypeService.Save(contentType);
document = ServiceContext.ContentService.GetById(document.Id);
@@ -96,8 +96,7 @@ namespace Umbraco.Tests.Services
var properties = pmember.Properties.ToList();
for (var i = 0; i < aliases.Length; i++)
Assert.AreEqual(properties[i].Alias, aliases[i]);
Assert.IsTrue(properties.Select(x => x.Alias).ContainsAll(aliases));
var email = properties[aliases.IndexOf("Email")];
Assert.AreEqual("xemail", email.GetSourceValue());
@@ -329,6 +329,11 @@ namespace Umbraco.Tests.TestHelpers
{
throw new NotImplementedException();
}
public IReadOnlyDictionary<Udi, IEnumerable<string>> GetReferences(int id)
{
throw new NotImplementedException();
}
}
#endregion
@@ -259,7 +259,7 @@ namespace Umbraco.Tests.TestHelpers
var publishedSnapshotAccessor = new UmbracoContextPublishedSnapshotAccessor(Umbraco.Web.Composing.Current.UmbracoContextAccessor);
var variationContextAccessor = new TestVariationContextAccessor();
var service = new PublishedSnapshotService(
var service = new XmlPublishedSnapshotService(
ServiceContext,
Factory.GetInstance<IPublishedContentTypeFactory>(),
ScopeProvider,
@@ -357,14 +357,14 @@ namespace Umbraco.Tests.TestHelpers
protected UmbracoContext GetUmbracoContext(string url, int templateId = 1234, RouteData routeData = null, bool setSingleton = false, IUmbracoSettingsSection umbracoSettings = null, IEnumerable<IUrlProvider> urlProviders = null, IEnumerable<IMediaUrlProvider> mediaUrlProviders = null, IGlobalSettings globalSettings = null, IPublishedSnapshotService snapshotService = null)
{
// ensure we have a PublishedCachesService
var service = snapshotService ?? PublishedSnapshotService as PublishedSnapshotService;
var service = snapshotService ?? PublishedSnapshotService as XmlPublishedSnapshotService;
if (service == null)
throw new Exception("Not a proper XmlPublishedCache.PublishedCachesService.");
if (service is PublishedSnapshotService)
if (service is XmlPublishedSnapshotService)
{
// re-initialize PublishedCacheService content with an Xml source with proper template id
((PublishedSnapshotService)service).XmlStore.GetXmlDocument = () =>
((XmlPublishedSnapshotService)service).XmlStore.GetXmlDocument = () =>
{
var doc = new XmlDocument();
doc.LoadXml(GetXmlContent(templateId));
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using NUnit.Framework;
@@ -8,6 +9,8 @@ namespace Umbraco.Tests.Testing
{
public abstract class TestOptionAttributeBase : Attribute
{
public static readonly List<Assembly> ScanAssemblies = new List<Assembly>();
public static TOptions GetTestOptions<TOptions>(MethodInfo method)
where TOptions : TestOptionAttributeBase, new()
{
@@ -28,9 +31,18 @@ namespace Umbraco.Tests.Testing
var test = TestContext.CurrentContext.Test;
var typeName = test.ClassName;
var methodName = test.MethodName;
var type = Type.GetType(typeName, true);
var type = Type.GetType(typeName, false);
if (type == null)
throw new PanicException($"Could not resolve the type from type name {typeName}"); // makes no sense
{
type = ScanAssemblies
.Select(assembly => assembly.GetType(typeName, false))
.FirstOrDefault(x => x != null);
if (type == null)
{
throw new PanicException($"Could not resolve the running test fixture from type name {typeName}.\n" +
$"To use base classes from Umbraco.Tests, add your test assembly to TestOptionAttributeBase.ScanAssemblies");
}
}
var methodInfo = type.GetMethod(methodName); // what about overloads?
var options = GetTestOptions<TOptions>(methodInfo);
return options;
@@ -7,10 +7,13 @@ using NUnit.Framework;
using Umbraco.Core;
using Umbraco.Core.Cache;
using Umbraco.Core.Composing;
using Umbraco.Core.Configuration;
using Umbraco.Core.Dictionary;
using Umbraco.Core.Logging;
using Umbraco.Core.Mapping;
using Umbraco.Core.Models;
using Umbraco.Core.Models.PublishedContent;
using Umbraco.Core.Persistence;
using Umbraco.Core.Services;
using Umbraco.Tests.TestHelpers;
using Umbraco.Tests.TestHelpers.Stubs;
@@ -19,6 +22,7 @@ using Umbraco.Web;
using Umbraco.Web.PublishedCache;
using Umbraco.Web.Routing;
using Umbraco.Web.Security;
using Umbraco.Web.WebApi;
using Current = Umbraco.Web.Composing.Current;
namespace Umbraco.Tests.Testing.TestingTests
@@ -88,5 +92,26 @@ namespace Umbraco.Tests.Testing.TestingTests
Assert.AreEqual("/hello/world/1234", theUrlProvider.GetUrl(publishedContent));
}
[Test]
public void Can_Mock_UmbracoApiController_Dependencies_With_Injected_UmbracoMapper()
{
var umbracoContext = TestObjects.GetUmbracoContextMock();
var membershipHelper = new MembershipHelper(umbracoContext.HttpContext, Mock.Of<IPublishedMemberCache>(), Mock.Of<MembershipProvider>(), Mock.Of<RoleProvider>(), Mock.Of<IMemberService>(), Mock.Of<IMemberTypeService>(), Mock.Of<IUserService>(), Mock.Of<IPublicAccessService>(), Mock.Of<AppCaches>(), Mock.Of<ILogger>());
var umbracoHelper = new UmbracoHelper(Mock.Of<IPublishedContent>(), Mock.Of<ITagQuery>(), Mock.Of<ICultureDictionaryFactory>(), Mock.Of<IUmbracoComponentRenderer>(), Mock.Of<IPublishedContentQuery>(), membershipHelper);
var umbracoMapper = new UmbracoMapper(new MapDefinitionCollection(new[] { Mock.Of<IMapDefinition>() }));
// ReSharper disable once UnusedVariable
var umbracoApiController = new FakeUmbracoApiController(Mock.Of<IGlobalSettings>(), Mock.Of<IUmbracoContextAccessor>(), Mock.Of<ISqlContext>(), ServiceContext.CreatePartial(), AppCaches.NoCache, Mock.Of<IProfilingLogger>(), Mock.Of<IRuntimeState>(), umbracoHelper, umbracoMapper);
Assert.Pass();
}
}
internal class FakeUmbracoApiController : UmbracoApiController
{
public FakeUmbracoApiController(IGlobalSettings globalSettings, IUmbracoContextAccessor umbracoContextAccessor, ISqlContext sqlContext, ServiceContext services, AppCaches appCaches, IProfilingLogger logger, IRuntimeState runtimeState, UmbracoHelper umbracoHelper) : base(globalSettings, umbracoContextAccessor, sqlContext, services, appCaches, logger, runtimeState, umbracoHelper) { }
public FakeUmbracoApiController(IGlobalSettings globalSettings, IUmbracoContextAccessor umbracoContextAccessor, ISqlContext sqlContext, ServiceContext services, AppCaches appCaches, IProfilingLogger logger, IRuntimeState runtimeState, UmbracoHelper umbracoHelper, UmbracoMapper umbracoMapper) : base(globalSettings, umbracoContextAccessor, sqlContext, services, appCaches, logger, runtimeState, umbracoHelper, umbracoMapper) { }
}
}
+1 -1
View File
@@ -511,7 +511,7 @@
<Compile Include="LegacyXmlPublishedCache\PublishedMediaCache.cs" />
<Compile Include="LegacyXmlPublishedCache\PublishedMemberCache.cs" />
<Compile Include="LegacyXmlPublishedCache\PublishedSnapshot.cs" />
<Compile Include="LegacyXmlPublishedCache\PublishedSnapshotService.cs" />
<Compile Include="LegacyXmlPublishedCache\XmlPublishedSnapshotService.cs" />
<Compile Include="LegacyXmlPublishedCache\RoutesCache.cs" />
<Compile Include="LegacyXmlPublishedCache\SafeXmlReaderWriter.cs" />
<Compile Include="LegacyXmlPublishedCache\UmbracoContextCache.cs" />
@@ -29,7 +29,7 @@ namespace Umbraco.Tests.Web.Mvc
[UmbracoTest(WithApplication = true)]
public class UmbracoViewPageTests : UmbracoTestBase
{
private PublishedSnapshotService _service;
private XmlPublishedSnapshotService _service;
[TearDown]
public override void TearDown()
@@ -421,7 +421,7 @@ namespace Umbraco.Tests.Web.Mvc
var scopeProvider = TestObjects.GetScopeProvider(Mock.Of<ILogger>());
var factory = Mock.Of<IPublishedContentTypeFactory>();
var umbracoContextAccessor = Mock.Of<IUmbracoContextAccessor>();
_service = new PublishedSnapshotService(svcCtx, factory, scopeProvider, cache,
_service = new XmlPublishedSnapshotService(svcCtx, factory, scopeProvider, cache,
null, null,
umbracoContextAccessor, null, null, null,
new TestDefaultCultureAccessor(),
+21 -18
View File
@@ -3,26 +3,26 @@
module.exports = {
sources: {
//less files used by backoffice and preview
//processed in the less task
// less files used by backoffice and preview
// processed in the less task
less: {
installer: { files: ["./src/less/installer.less"], out: "installer.css" },
nonodes: { files: ["./src/less/pages/nonodes.less"], out: "nonodes.style.min.css"},
preview: { files: ["./src/less/canvas-designer.less"], out: "canvasdesigner.css" },
umbraco: { files: ["./src/less/belle.less"], out: "umbraco.css" },
rteContent: { files: ["./src/less/rte-content.less"], out: "rte-content.css" }
installer: { files: "./src/less/installer.less", watch: "./src/less/**/*.less", out: "installer.css" },
nonodes: { files: "./src/less/pages/nonodes.less", watch: "./src/less/**/*.less", out: "nonodes.style.min.css"},
preview: { files: "./src/less/canvas-designer.less", watch: "./src/less/**/*.less", out: "canvasdesigner.css" },
umbraco: { files: "./src/less/belle.less", watch: "./src/less/**/*.less", out: "umbraco.css" },
rteContent: { files: "./src/less/rte-content.less", watch: "./src/less/**/*.less", out: "rte-content.css" }
},
//js files for backoffie
//processed in the js task
// js files for backoffice
// processed in the js task
js: {
preview: { files: ["./src/preview/**/*.js"], out: "umbraco.preview.js" },
installer: { files: ["./src/installer/**/*.js"], out: "umbraco.installer.js" },
filters: { files: ["./src/common/filters/**/*.js"], out: "umbraco.filters.js" },
resources: { files: ["./src/common/resources/**/*.js"], out: "umbraco.resources.js" },
services: { files: ["./src/common/services/**/*.js"], out: "umbraco.services.js" },
security: { files: ["./src/common/interceptors/**/*.js"], out: "umbraco.interceptors.js" },
preview: { files: "./src/preview/**/*.js", out: "umbraco.preview.js" },
installer: { files: "./src/installer/**/*.js", out: "umbraco.installer.js" },
filters: { files: "./src/common/filters/**/*.js", out: "umbraco.filters.js" },
resources: { files: "./src/common/resources/**/*.js", out: "umbraco.resources.js" },
services: { files: "./src/common/services/**/*.js", out: "umbraco.services.js" },
security: { files: "./src/common/interceptors/**/*.js", out: "umbraco.interceptors.js" },
//the controllers for views
controllers: {
files: [
@@ -42,13 +42,16 @@ module.exports = {
],
out: "umbraco.directives.js"
}
},
//selectors for copying all views into the build
//processed in the views task
views:{
umbraco: {files: ["./src/views/**/*.html"], folder: ""},
installer: {files: ["./src/installer/steps/*.html"], folder: "install/"}
views: {files: "./src/views/**/*.html", folder: ""},
directives: {files: "./src/common/directives/**/*.html", folder: ""},
components: {files: "./src/common/components/**/*.html", folder: ""},
installer: {files: "./src/installer/steps/*.html", folder: "install/"}
},
//globs for file-watching
-10
View File
@@ -1,10 +0,0 @@
'use strict';
var fs = require('fs');
var onlyScripts = require('./util/scriptFilter');
var tasks = fs.readdirSync('./gulp/tasks/').filter(onlyScripts);
tasks.forEach(function(task) {
require('./tasks/' + task);
});
@@ -1,10 +0,0 @@
'use strict';
var config = require('../config');
var gulp = require('gulp');
var runSequence = require('run-sequence');
// Build - build the files ready for production
gulp.task('build', function(cb) {
runSequence(["js", "dependencies", "less", "views"], "test:unit", cb);
});
@@ -10,7 +10,7 @@ var imagemin = require('gulp-imagemin');
/**************************
* Task processes and copies all dependencies, either installed by npm or stored locally in the project
**************************/
gulp.task('dependencies', function () {
function dependencies() {
//as we do multiple things in this task, we merge the multiple streams
var stream = new MergeStream();
@@ -42,6 +42,11 @@ gulp.task('dependencies', function () {
"src": ["./node_modules/angular/angular.js"],
"base": "./node_modules/angular"
},
{
"name": "angular-aria",
"src": ["./node_modules/angular-aria/angular-aria.min.js"],
"base": "./node_modules/angular-aria"
},
{
"name": "angular-cookies",
"src": ["./node_modules/angular-cookies/angular-cookies.js"],
@@ -100,7 +105,7 @@ gulp.task('dependencies', function () {
"name": "angular-messages",
"src": ["./node_modules/angular-messages/angular-messages.js"],
"base": "./node_modules/angular-messages"
},
},
{
"name": "angular-mocks",
"src": ["./node_modules/angular-mocks/angular-mocks.js"],
@@ -239,21 +244,21 @@ gulp.task('dependencies', function () {
nodeModules.forEach(module => {
stream.add(
gulp.src(module.src,
{ base: module.base })
{ base: module.base, allowEmpty: true })
.pipe(gulp.dest(config.root + config.targets.lib + "/" + module.name))
);
});
//copy over libs which are not on npm (/lib)
stream.add(
gulp.src(config.sources.globs.lib)
gulp.src(config.sources.globs.lib, { allowEmpty: true })
.pipe(gulp.dest(config.root + config.targets.lib))
);
//Copies all static assets into /root / assets folder
//css, fonts and image files
var assetsTask = gulp.src(config.sources.globs.assets);
var assetsTask = gulp.src(config.sources.globs.assets, { allowEmpty: true });
if (global.isProd === true) {
assetsTask = assetsTask.pipe(imagemin([
imagemin.gifsicle({interlaced: true}),
@@ -272,21 +277,23 @@ gulp.task('dependencies', function () {
stream.add(assetsTask);
// Copies all the less files related to the preview into their folder
//these are not pre-processed as preview has its own less combiler client side
//these are not pre-processed as preview has its own less compiler client side
stream.add(
gulp.src("src/canvasdesigner/editors/*.less")
gulp.src("src/canvasdesigner/editors/*.less", { allowEmpty: true })
.pipe(gulp.dest(config.root + config.targets.assets + "/less"))
);
// Todo: check if we need these fileSize
// TODO: check if we need these fileSize
stream.add(
gulp.src("src/views/propertyeditors/grid/config/*.*")
gulp.src("src/views/propertyeditors/grid/config/*.*", { allowEmpty: true })
.pipe(gulp.dest(config.root + config.targets.views + "/propertyeditors/grid/config"))
);
stream.add(
gulp.src("src/views/dashboard/default/*.jpg")
gulp.src("src/views/dashboard/default/*.jpg", { allowEmpty: true })
.pipe(gulp.dest(config.root + config.targets.views + "/dashboard/default"))
);
return stream;
});
};
module.exports = { dependencies: dependencies };
@@ -1,13 +0,0 @@
'use strict';
var config = require('../config');
var gulp = require('gulp');
var runSequence = require('run-sequence');
// Dev - build the files ready for development and start watchers
gulp.task('dev', function (cb) {
global.isProd = false;
runSequence(["dependencies", "js", "less", "views"], "watch", cb);
});
+12 -9
View File
@@ -9,21 +9,24 @@ var MergeStream = require('merge-stream');
var processJs = require('../util/processJs');
/**************************
* Copies all angular JS files into their seperate umbraco.*.js file
* Copies all angular JS files into their separate umbraco.*.js file
**************************/
gulp.task('js', function () {
function js() {
//we run multiple streams, so merge them all together
var stream = new MergeStream();
stream.add(
gulp.src(config.sources.globs.js)
.pipe(gulp.dest(config.root + config.targets.js))
);
gulp.src(config.sources.globs.js).pipe( gulp.dest(config.root + config.targets.js) )
);
_.forEach(config.sources.js, function (group) {
stream.add (processJs(group.files, group.out) );
});
_.forEach(config.sources.js, function (group) {
stream.add(
processJs(group.files, group.out)
);
});
return stream;
});
};
module.exports = { js: js };
+4 -3
View File
@@ -1,14 +1,13 @@
'use strict';
var config = require('../config');
var gulp = require('gulp');
var _ = require('lodash');
var MergeStream = require('merge-stream');
var processLess = require('../util/processLess');
gulp.task('less', function () {
function less() {
var stream = new MergeStream();
@@ -17,4 +16,6 @@ gulp.task('less', function () {
});
return stream;
});
};
module.exports = { less: less };
@@ -0,0 +1,22 @@
'use strict';
var gulp = require('gulp');
var through2 = require('through2');
function createEmptyStream() {
var pass = through2.obj();
process.nextTick(pass.end.bind(pass));
return pass;
}
/**************************
* Copies all angular JS files into their separate umbraco.*.js file
**************************/
function removeProductionMode() {
global.isProd = false;
return createEmptyStream();
};
module.exports = { removeProductionMode: removeProductionMode };
+8 -13
View File
@@ -1,32 +1,27 @@
'use strict';
var config = require('../config');
var gulp = require('gulp');
var karmaServer = require('karma').Server;
var runSequence = require('run-sequence');
/**************************
* Build tests
**************************/
// Karma test
gulp.task('runTests', function(cb) {
runSequence("js", "test:unit", cb);
});
function testUnit() {
gulp.task('test:unit', function () {
new karmaServer({
return new karmaServer({
configFile: __dirname + "/../../test/config/karma.conf.js",
keepalive: true
})
.start();
});
};
gulp.task('test:e2e', function() {
new karmaServer({
function testE2e() {
return new karmaServer({
configFile: __dirname + "/../../test/config/e2e.js",
keepalive: true
})
.start();
});
};
module.exports = { testUnit: testUnit, testE2e: testE2e };
@@ -6,7 +6,7 @@ var gulp = require('gulp');
var _ = require('lodash');
var MergeStream = require('merge-stream');
gulp.task('views', function () {
function views() {
var stream = new MergeStream();
@@ -22,4 +22,7 @@ gulp.task('views', function () {
});
return stream;
});
};
module.exports = { views: views };
@@ -1,58 +0,0 @@
'use strict';
var config = require('../config');
var gulp = require('gulp');
var _ = require('lodash');
var MergeStream = require('merge-stream');
var processJs = require('../util/processJs');
var watch = require('gulp-watch');
gulp.task('watch', function () {
var stream = new MergeStream();
var watchInterval = 500;
//Setup a watcher for all groups of javascript files
_.forEach(config.sources.js, function (group) {
if(group.watch !== false){
stream.add(
watch(group.files, { ignoreInitial: true, interval: watchInterval }, function (file) {
console.info(file.path + " has changed, added to: " + group.out);
processJs(group.files, group.out);
})
);
}
});
stream.add(
//watch all less files and trigger the less task
watch(config.sources.globs.less, { ignoreInitial: true, interval: watchInterval }, function () {
gulp.run(['less']);
})
);
//watch all views - copy single file changes
stream.add(
watch(config.sources.globs.views, { interval: watchInterval })
.pipe(gulp.dest(config.root + config.targets.views))
);
//watch all app js files that will not be merged - copy single file changes
stream.add(
watch(config.sources.globs.js, { interval: watchInterval })
.pipe(gulp.dest(config.root + config.targets.js))
);
return stream;
});
@@ -0,0 +1,49 @@
'use strict';
const config = require('../config');
const {watch, parallel, dest, src} = require('gulp');
var _ = require('lodash');
var MergeStream = require('merge-stream');
var processJs = require('../util/processJs');
var processLess = require('../util/processLess');
//const { less } = require('./less');
//const { views } = require('./views');
function watchTask(cb) {
var watchInterval = 500;
//Setup a watcher for all groups of JS files
_.forEach(config.sources.js, function (group) {
if(group.watch !== false) {
watch(group.files, { ignoreInitial: true, interval: watchInterval }, function JS_Group_Compile() { return processJs(group.files, group.out) });
}
});
//Setup a watcher for all groups of LESS files
_.forEach(config.sources.less, function (group) {
if(group.watch !== false) {
watch(group.watch, { ignoreInitial: true, interval: watchInterval }, function Less_Group_Compile() { processLess(group.files, group.out) });
}
});
//Setup a watcher for all groups of view files
var viewWatcher;
_.forEach(config.sources.views, function (group) {
if(group.watch !== false) {
viewWatcher = watch(group.files, { ignoreInitial: true, interval: watchInterval });
viewWatcher.on('change', function(path, stats) {
console.log("copying " + group.files + " to " + config.root + config.targets.views + group.folder);
src(group.files).pipe( dest(config.root + config.targets.views + group.folder) )
});
}
});
return cb();
};
module.exports = { watchTask: watchTask };
@@ -11,6 +11,8 @@ var embedTemplates = require('gulp-angular-embed-templates');
module.exports = function (files, out) {
console.log("JS: ", files, " -> ", config.root + config.targets.js + out)
var task = gulp.src(files);
// check for js errors
@@ -15,7 +15,9 @@ module.exports = function(files, out) {
autoprefixer,
cssnano({zindex: false})
];
console.log("LESS: ", files, " -> ", config.root + config.targets.js + out)
var task = gulp.src(files)
.pipe(less())
.pipe(cleanCss())
+43 -10
View File
@@ -1,16 +1,49 @@
'use strict';
/*
* gulpfile.js
* ===========
* Rather than manage one giant configuration file responsible
* for creating multiple tasks, each task has been broken out into
* its own file in gulp/tasks. Any file in that folder gets automatically
* required by the loop in ./gulp/index.js (required below).
*
* To add a new task, simply add a new task file to gulp/tasks.
*/
* gulpfile.js
* ===========
* This is now using Gulp 4, each child task is now a child function in its own corresponding file.
*
* To add a new task, simply add a new task file to gulp/tasks folder, add a require statement below to include the one or more methods
* and then add the exports command to add the new item into the task menu.
*/
global.isProd = true;
require('./gulp');
const { src, dest, series, parallel, lastRun } = require('gulp');
const { dependencies } = require('./gulp/tasks/dependencies');
const { js } = require('./gulp/tasks/js');
const { less } = require('./gulp/tasks/less');
const { testE2e, testUnit } = require('./gulp/tasks/test');
const { views } = require('./gulp/tasks/views');
const { watchTask } = require('./gulp/tasks/watchTask');
const { removeProductionMode } = require('./gulp/tasks/removeProductionMode');
// Load local overwrites, can be used to overwrite paths in your local setup.
var fs = require('fs');
var onlyScripts = require('./gulp/util/scriptFilter');
try {
if (fs.existsSync('./gulp/overwrites/')) {
var overwrites = fs.readdirSync('./gulp/overwrites/').filter(onlyScripts);
overwrites.forEach(function(overwrite) {
require('./gulp/overwrites/' + overwrite);
});
}
} catch (err) {
console.error(err)
}
// ***********************************************************
// These Exports are the new way of defining Tasks in Gulp 4.x
// ***********************************************************
exports.build = series(parallel(dependencies, js, less, views), testUnit);
exports.dev = series(parallel(dependencies, js, less, views), watchTask);
exports.fastdev = series(removeProductionMode, parallel(dependencies, js, less, views), watchTask);
exports.watch = series(watchTask);
//
exports.runTests = series(js, testUnit);
exports.testUnit = series(testUnit);
exports.testE2e = series(testE2e);
@@ -241,3 +241,7 @@ table th[class*="span"],
background-color: darken(@infoBackground, 5%);
}
}
.table .icon {
vertical-align: bottom;
}
+2550 -2766
View File
File diff suppressed because it is too large Load Diff
+84 -79
View File
@@ -1,79 +1,84 @@
{
"private": true,
"scripts": {
"test": "karma start test/config/karma.conf.js --singlerun",
"build": "gulp build",
"dev": "gulp dev",
"fastdev": "gulp fastdev"
},
"dependencies": {
"ace-builds": "1.4.2",
"angular": "1.7.5",
"angular-animate": "1.7.5",
"angular-chart.js": "^1.1.1",
"angular-cookies": "1.7.5",
"angular-dynamic-locale": "0.1.37",
"angular-i18n": "1.7.5",
"angular-local-storage": "0.7.1",
"angular-messages": "1.7.5",
"angular-mocks": "1.7.5",
"angular-route": "1.7.5",
"angular-sanitize": "1.7.5",
"angular-touch": "1.7.5",
"angular-ui-sortable": "0.19.0",
"animejs": "2.2.0",
"bootstrap-social": "5.1.1",
"chart.js": "^2.7.3",
"clipboard": "2.0.4",
"diff": "3.5.0",
"flatpickr": "4.5.2",
"font-awesome": "4.7.0",
"jquery": "^3.4.0",
"jquery-ui-dist": "1.12.1",
"jquery-ui-touch-punch": "0.2.3",
"lazyload-js": "1.0.0",
"moment": "2.22.2",
"ng-file-upload": "12.2.13",
"nouislider": "14.0.2",
"npm": "^6.4.1",
"signalr": "2.4.0",
"spectrum-colorpicker": "1.8.0",
"tinymce": "4.9.2",
"typeahead.js": "0.11.1",
"underscore": "1.9.1"
},
"devDependencies": {
"@babel/core": "7.1.6",
"@babel/preset-env": "7.1.6",
"autoprefixer": "9.3.1",
"cssnano": "4.1.7",
"fs": "0.0.2",
"gulp": "^3.9.1",
"gulp-angular-embed-templates": "^2.3.0",
"gulp-babel": "8.0.0",
"gulp-clean-css": "4.0.0",
"gulp-cli": "^2.0.1",
"gulp-concat": "2.6.1",
"gulp-eslint": "^5.0.0",
"gulp-imagemin": "^4.1.0",
"gulp-less": "4.0.1",
"gulp-notify": "^3.0.0",
"gulp-postcss": "8.0.0",
"gulp-rename": "1.4.0",
"gulp-sort": "2.0.0",
"gulp-watch": "5.0.1",
"gulp-wrap": "0.14.0",
"gulp-wrap-js": "0.4.1",
"jasmine-core": "3.3.0",
"karma": "3.1.1",
"karma-jasmine": "2.0.1",
"karma-junit-reporter": "^1.2.0",
"karma-phantomjs-launcher": "1.0.4",
"less": "3.9.0",
"lodash": "4.17.13",
"marked": "^0.6.1",
"merge-stream": "1.0.1",
"run-sequence": "2.2.1",
"caniuse-lite": "^1.0.30000966"
}
}
{
"private": true,
"scripts": {
"test": "gulp runTests",
"unit": "gulp testUnit",
"e2e": "gulp testE2e",
"build": "gulp build",
"dev": "gulp dev",
"fastdev": "gulp fastdev",
"watch": "gulp watch"
},
"dependencies": {
"ace-builds": "1.4.2",
"angular": "1.7.5",
"angular-animate": "1.7.5",
"angular-aria": "1.7.5",
"angular-chart.js": "^1.1.1",
"angular-cookies": "1.7.5",
"angular-dynamic-locale": "0.1.37",
"angular-i18n": "1.7.5",
"angular-local-storage": "0.7.1",
"angular-messages": "1.7.5",
"angular-mocks": "1.7.5",
"angular-route": "1.7.5",
"angular-sanitize": "1.7.5",
"angular-touch": "1.7.5",
"angular-ui-sortable": "0.19.0",
"animejs": "2.2.0",
"bootstrap-social": "5.1.1",
"chart.js": "^2.8.0",
"clipboard": "2.0.4",
"diff": "3.5.0",
"flatpickr": "4.5.2",
"font-awesome": "4.7.0",
"jquery": "^3.4.1",
"jquery-ui-dist": "1.12.1",
"jquery-ui-touch-punch": "0.2.3",
"lazyload-js": "1.0.0",
"moment": "2.22.2",
"ng-file-upload": "12.2.13",
"nouislider": "14.0.2",
"npm": "6.12.0",
"signalr": "2.4.0",
"spectrum-colorpicker": "1.8.0",
"tinymce": "4.9.2",
"typeahead.js": "0.11.1",
"underscore": "1.9.1"
},
"devDependencies": {
"@babel/core": "7.6.4",
"@babel/preset-env": "7.6.3",
"autoprefixer": "9.6.5",
"caniuse-lite": "^1.0.30001002",
"cssnano": "4.1.10",
"fs": "0.0.2",
"gulp": "4.0.2",
"gulp-angular-embed-templates": "^2.3.0",
"gulp-babel": "8.0.0",
"gulp-clean-css": "4.2.0",
"gulp-cli": "^2.2.0",
"gulp-concat": "2.6.1",
"gulp-eslint": "6.0.0",
"gulp-imagemin": "6.1.1",
"gulp-less": "4.0.1",
"gulp-notify": "^3.0.0",
"gulp-postcss": "8.0.0",
"gulp-rename": "1.4.0",
"gulp-sort": "2.0.0",
"gulp-watch": "5.0.1",
"gulp-wrap": "0.15.0",
"gulp-wrap-js": "0.4.1",
"jasmine-core": "3.5.0",
"karma": "4.4.1",
"karma-jasmine": "2.0.1",
"karma-junit-reporter": "2.0.1",
"karma-phantomjs-launcher": "1.0.4",
"karma-spec-reporter": "0.0.32",
"less": "3.10.3",
"lodash": "4.17.15",
"marked": "^0.7.0",
"merge-stream": "2.0.0",
"run-sequence": "2.2.1"
}
}
+1
View File
@@ -12,6 +12,7 @@ var app = angular.module('umbraco', [
'ngSanitize',
'ngTouch',
'ngMessages',
'ngAria',
'tmh.dynamicLocale',
'ngFileUpload',
'LocalStorageModule',
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

@@ -54,6 +54,7 @@
</pre>
@param {boolean} checked Set to <code>true</code> or <code>false</code> to toggle the switch.
@param {string} inputId Set the <code>id</code> of the toggle.
@param {callback} onClick The function which should be called when the toggle is clicked.
@param {string=} showLabels Set to <code>true</code> or <code>false</code> to show a "On" or "Off" label next to the switch.
@param {string=} labelOn Set a custom label for when the switched is turned on. It will default to "On".
@@ -122,6 +123,7 @@
scope: {
checked: "=",
disabled: "=",
inputId: "@",
onClick: "&",
labelOn: "@?",
labelOff: "@?",
@@ -4,7 +4,7 @@
function ContentEditController($rootScope, $scope, $routeParams, $q, $window,
appState, contentResource, entityResource, navigationService, notificationsService,
serverValidationManager, contentEditingHelper, localizationService, formHelper, umbRequestHelper,
editorState, $http, eventsService, overlayService, $location, localStorageService) {
editorState, $http, eventsService, overlayService, $location, localStorageService, treeService) {
var evts = [];
var infiniteMode = $scope.infiniteModel && $scope.infiniteModel.infiniteMode;
@@ -201,6 +201,12 @@
$scope.page.buttonGroupState = "success";
}));
evts.push(eventsService.on("rte.shortcut.save", function(){
if ($scope.page.showSaveButton) {
$scope.save();
}
}));
evts.push(eventsService.on("content.saved", function(){
// Clear out localstorage keys that start with tinymce__
// When we save/perist a content node
@@ -305,7 +311,7 @@
}
/** Syncs the content item to it's tree node - this occurs on first load and after saving */
function syncTreeNode(content, path, initialLoad) {
function syncTreeNode(content, path, initialLoad, reloadChildren) {
if (infiniteMode || !path) {
return;
@@ -315,6 +321,9 @@
navigationService.syncTree({ tree: $scope.treeAlias, path: path.split(","), forceReload: initialLoad !== true })
.then(function (syncArgs) {
$scope.page.menu.currentNode = syncArgs.node;
if (reloadChildren && syncArgs.node.expanded) {
treeService.loadNodeChildren({node: syncArgs.node});
}
}, function () {
//handle the rejection
console.log("A problem occurred syncing the tree! A path is probably incorrect.")
@@ -446,7 +455,7 @@
//needs to be manually set for infinite editing mode
$scope.page.isNew = false;
syncTreeNode($scope.content, data.path);
syncTreeNode($scope.content, data.path, false, args.reloadChildren);
eventsService.emit("content.saved", { content: $scope.content, action: args.action });
@@ -766,16 +775,6 @@
clearNotifications($scope.content);
//before we launch the dialog we want to execute all client side validations first
if (formHelper.submitForm({ scope: $scope, action: "schedule" })) {
//used to track the original values so if the user doesn't save the schedule and they close the dialog we reset the dates back to what they were.
let origDates = [];
for (let i = 0; i < $scope.content.variants.length; i++) {
origDates.push({
releaseDate: $scope.content.variants[i].releaseDate,
expireDate: $scope.content.variants[i].expireDate
});
}
if (!isContentCultureVariant()) {
//ensure the flags are set
$scope.content.variants[0].save = true;
@@ -784,10 +783,17 @@
var dialog = {
parentScope: $scope,
view: "views/content/overlays/schedule.html",
variants: $scope.content.variants, //set a model property for the dialog
variants: angular.copy($scope.content.variants), //set a model property for the dialog
skipFormValidation: true, //when submitting the overlay form, skip any client side validation
submitButtonLabelKey: "buttons_schedulePublish",
submit: function (model) {
for (let i = 0; i < $scope.content.variants.length; i++) {
$scope.content.variants[i].releaseDate = model.variants[i].releaseDate;
$scope.content.variants[i].expireDate = model.variants[i].expireDate;
$scope.content.variants[i].releaseDateFormatted = model.variants[i].releaseDateFormatted;
$scope.content.variants[i].expireDateFormatted = model.variants[i].expireDateFormatted;
}
model.submitButtonState = "busy";
clearNotifications($scope.content);
@@ -810,7 +816,7 @@
}
model.submitButtonState = "error";
//re-map the dialog model since we've re-bound the properties
dialog.variants = $scope.content.variants;
dialog.variants = angular.copy($scope.content.variants);
//don't reject, we've handled the error
return $q.when(err);
});
@@ -818,11 +824,6 @@
},
close: function () {
overlayService.close();
//restore the dates
for (let i = 0; i < $scope.content.variants.length; i++) {
$scope.content.variants[i].releaseDate = origDates[i].releaseDate;
$scope.content.variants[i].expireDate = origDates[i].expireDate;
}
}
};
overlayService.open(dialog);
@@ -859,7 +860,8 @@
return contentResource.publishWithDescendants(content, create, model.includeUnpublished, files, showNotifications);
},
action: "publishDescendants",
showNotifications: false
showNotifications: false,
reloadChildren: model.includeUnpublished
}).then(function (data) {
//show all notifications manually here since we disabled showing them automatically in the save method
formHelper.showNotifications(data);
@@ -307,6 +307,8 @@
// get current backoffice user and format dates
userService.getCurrentUser().then(function (currentUser) {
scope.currentVariant.createDateFormatted = dateHelper.getLocalDate(scope.currentVariant.createDate, currentUser.locale, 'LLL');
scope.currentVariant.releaseDateFormatted = dateHelper.getLocalDate(scope.currentVariant.releaseDate, currentUser.locale, 'LLL');
scope.currentVariant.expireDateFormatted = dateHelper.getLocalDate(scope.currentVariant.expireDate, currentUser.locale, 'LLL');
});
}
@@ -0,0 +1,45 @@
// A slightly modified version of https://github.com/myplanet/angular-deep-blur/blob/master/angular-deep-blur.js - Kudos to Ufuk Kayserilioglu (paracycle)
(function () {
'use strict';
function DeepBlurDirective($timeout){
function controller($scope, $element, $attrs) {
var leaveExpr = $attrs.deepBlur,
dom = $element[0];
function containsDom(parent, dom) {
while (dom) {
if (dom === parent) {
return true;
}
dom = dom.parentNode;
}
return false;
}
function onBlur(e) {
var targetElement = e.relatedTarget;
if (!containsDom(dom, targetElement)) {
$timeout(function () {
$scope.$apply(leaveExpr);
}, 10);
}
}
dom.addEventListener('blur', onBlur, true);
}
var directive = {
restrict: 'A',
controller: controller
};
return directive;
}
angular.module('umbraco.directives').directive('deepBlur', DeepBlurDirective);
})();
@@ -1,273 +0,0 @@
/**
* @description Utillity directives for key and field events
**/
angular.module('umbraco.directives')
.directive('onDragEnter', function () {
return {
link: function (scope, elm, attrs) {
var f = function () {
scope.$apply(attrs.onDragEnter);
};
elm.on("dragenter", f);
scope.$on("$destroy", function () { elm.off("dragenter", f); });
}
};
})
.directive('onDragLeave', function () {
return function (scope, elm, attrs) {
var f = function (event) {
var rect = this.getBoundingClientRect();
var getXY = function getCursorPosition(event) {
var x, y;
if (typeof event.clientX === 'undefined') {
// try touch screen
x = event.pageX + document.documentElement.scrollLeft;
y = event.pageY + document.documentElement.scrollTop;
} else {
x = event.clientX + document.body.scrollLeft + document.documentElement.scrollLeft;
y = event.clientY + document.body.scrollTop + document.documentElement.scrollTop;
}
return { x: x, y: y };
};
var e = getXY(event.originalEvent);
// Check the mouseEvent coordinates are outside of the rectangle
if (e.x > rect.left + rect.width - 1 || e.x < rect.left || e.y > rect.top + rect.height - 1 || e.y < rect.top) {
scope.$apply(attrs.onDragLeave);
}
};
elm.on("dragleave", f);
scope.$on("$destroy", function () { elm.off("dragleave", f); });
};
})
.directive('onDragOver', function () {
return {
link: function (scope, elm, attrs) {
var f = function () {
scope.$apply(attrs.onDragOver);
};
elm.on("dragover", f);
scope.$on("$destroy", function () { elm.off("dragover", f); });
}
};
})
.directive('onDragStart', function () {
return {
link: function (scope, elm, attrs) {
var f = function () {
scope.$apply(attrs.onDragStart);
};
elm.on("dragstart", f);
scope.$on("$destroy", function () { elm.off("dragstart", f); });
}
};
})
.directive('onDragEnd', function () {
return {
link: function (scope, elm, attrs) {
var f = function () {
scope.$apply(attrs.onDragEnd);
};
elm.on("dragend", f);
scope.$on("$destroy", function () { elm.off("dragend", f); });
}
};
})
.directive('onDrop', function () {
return {
link: function (scope, elm, attrs) {
var f = function () {
scope.$apply(attrs.onDrop);
};
elm.on("drop", f);
scope.$on("$destroy", function () { elm.off("drop", f); });
}
};
})
.directive('onOutsideClick', function ($timeout, angularHelper) {
return function (scope, element, attrs) {
var eventBindings = [];
function oneTimeClick(event) {
var el = event.target.nodeName;
//ignore link and button clicks
var els = ["INPUT", "A", "BUTTON"];
if (els.indexOf(el) >= 0) { return; }
// ignore clicks on new overlay
var parents = $(event.target).parents("a,button,.umb-overlay,.umb-tour");
if (parents.length > 0) {
return;
}
// ignore clicks on dialog from old dialog service
var oldDialog = $(event.target).parents("#old-dialog-service");
if (oldDialog.length === 1) {
return;
}
// ignore clicks in tinyMCE dropdown(floatpanel)
var floatpanel = $(event.target).closest(".mce-floatpanel");
if (floatpanel.length === 1) {
return;
}
// ignore clicks in flatpickr datepicker
var flatpickr = $(event.target).closest(".flatpickr-calendar");
if (flatpickr.length === 1) {
return;
}
//ignore clicks inside this element
if ($(element).has($(event.target)).length > 0) {
return;
}
// please to not use angularHelper.safeApply here, it won't work
scope.$apply(attrs.onOutsideClick);
}
$timeout(function () {
if ("bindClickOn" in attrs) {
eventBindings.push(scope.$watch(function () {
return attrs.bindClickOn;
}, function (newValue) {
if (newValue === "true") {
$(document).on("click", oneTimeClick);
} else {
$(document).off("click", oneTimeClick);
}
}));
} else {
$(document).on("click", oneTimeClick);
}
scope.$on("$destroy", function () {
$(document).off("click", oneTimeClick);
// unbind watchers
for (var e in eventBindings) {
eventBindings[e]();
}
});
}); // Temp removal of 1 sec timeout to prevent bug where overlay does not open. We need to find a better solution.
};
})
.directive('onRightClick', function ($parse) {
document.oncontextmenu = function (e) {
if (e.target.hasAttribute('on-right-click')) {
e.preventDefault();
e.stopPropagation();
return false;
}
};
return function (scope, el, attrs) {
el.on('contextmenu', function (e) {
e.preventDefault();
e.stopPropagation();
var fn = $parse(attrs.onRightClick);
scope.$apply(function () {
fn(scope, { $event: e });
});
return false;
});
};
})
.directive('onDelayedMouseleave', function ($timeout, $parse) {
return {
restrict: 'A',
link: function (scope, element, attrs, ctrl) {
var active = false;
var fn = $parse(attrs.onDelayedMouseleave);
var leave_f = function (event) {
var callback = function () {
fn(scope, { $event: event });
};
active = false;
$timeout(function () {
if (active === false) {
scope.$apply(callback);
}
}, 650);
};
var enter_f = function (event, args) {
active = true;
};
element.on("mouseleave", leave_f);
element.on("mouseenter", enter_f);
//unsub events
scope.$on("$destroy", function () {
element.off("mouseleave", leave_f);
element.off("mouseenter", enter_f);
});
}
};
})
// A slightly modified version of https://github.com/myplanet/angular-deep-blur/blob/master/angular-deep-blur.js - Kudos to Ufuk Kayserilioglu (paracycle)
.directive('deepBlur', function ($timeout) {
return {
restrict: 'A',
controller: function ($scope, $element, $attrs) {
var leaveExpr = $attrs.deepBlur,
dom = $element[0];
function containsDom(parent, dom) {
while (dom) {
if (dom === parent) {
return true;
}
dom = dom.parentNode;
}
return false;
}
function onBlur(e) {
var targetElement = e.relatedTarget;
if (!containsDom(dom, targetElement)) {
$timeout(function () {
$scope.$apply(leaveExpr);
}, 10);
}
}
dom.addEventListener('blur', onBlur, true);
}
};
});
@@ -0,0 +1,49 @@
(function () {
'use strict';
function onDelayedMouseleaveDirective($timeout, $parse){
function link(scope, element, attrs, ctrl) {
var active = false;
var fn = $parse(attrs.onDelayedMouseleave);
var leave_f = function (event) {
var callback = function () {
fn(scope, { $event: event });
};
active = false;
$timeout(function () {
if (active === false) {
scope.$apply(callback);
}
}, 650);
};
var enter_f = function (event, args) {
active = true;
};
element.on("mouseleave", leave_f);
element.on("mouseenter", enter_f);
//unsub events
scope.$on("$destroy", function () {
element.off("mouseleave", leave_f);
element.off("mouseenter", enter_f);
});
}
var directive = {
restrict: 'A',
link: link
};
return directive;
}
angular.module('umbraco.directives').directive('onDelayedMouseleave', onDelayedMouseleaveDirective);
})();
@@ -0,0 +1,24 @@
(function () {
'use strict';
function onDragEndDirective(){
function link(scope, elm, attrs) {
var f = function () {
scope.$apply(attrs.onDragEnd);
};
elm.on("dragend", f);
scope.$on("$destroy", function () { elm.off("dragend", f); });
}
var directive = {
link: link
};
return directive;
}
angular.module('umbraco.directives').directive('onDragEnd', onDragEndDirective);
})();
@@ -0,0 +1,24 @@
(function () {
'use strict';
function onDragEnterDirective(){
function link(scope, elm, attrs) {
var f = function () {
scope.$apply(attrs.onDragEnter);
};
elm.on("dragenter", f);
scope.$on("$destroy", function () { elm.off("dragenter", f); });
}
var directive = {
link: link
};
return directive;
}
angular.module('umbraco.directives').directive('onDragEnter', onDragEnterDirective);
})();

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