From b931ce01231ea5c1beee762fce0062fcb224506c Mon Sep 17 00:00:00 2001 From: Stephan Date: Tue, 12 Jun 2018 11:03:18 +0200 Subject: [PATCH 01/14] Fix CoreRuntime upgrade detection --- src/Umbraco.Core/Runtime/CoreRuntime.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Core/Runtime/CoreRuntime.cs b/src/Umbraco.Core/Runtime/CoreRuntime.cs index 98767f2aa2..93f475f7a6 100644 --- a/src/Umbraco.Core/Runtime/CoreRuntime.cs +++ b/src/Umbraco.Core/Runtime/CoreRuntime.cs @@ -258,13 +258,21 @@ namespace Umbraco.Core.Runtime logger.Debug("No local version, need to install Umbraco."); _state.Level = RuntimeLevel.Install; } - else if (localVersion != codeVersion) + else if (localVersion < codeVersion) { // there *is* a local version, but it does not match the code version // need to upgrade - logger.Debug(() => $"Local version \"{localVersion}\" != code version \"{codeVersion}\", need to upgrade Umbraco."); + logger.Debug(() => $"Local version \"{localVersion}\" < code version \"{codeVersion}\", need to upgrade Umbraco."); _state.Level = RuntimeLevel.Upgrade; } + else if (localVersion > codeVersion) + { + logger.Warn(() => $"Local version \"{localVersion}\" > code version \"{codeVersion}\", downgrading is not supported."); + _state.Level = RuntimeLevel.BootFailed; + + // in fact, this is bad enough that we want to throw + throw new BootFailedException($"Local version \"{localVersion}\" > code version \"{codeVersion}\", downgrading is not supported."); + } else if (databaseFactory.Configured == false) { // local version *does* match code version, but the database is not configured From 9c3e8b8e3c1de02b84e2a883c09fcba67b3dfb4d Mon Sep 17 00:00:00 2001 From: Stephan Date: Tue, 12 Jun 2018 11:11:16 +0200 Subject: [PATCH 02/14] Fix language inconsistencies management --- .../Implement/LanguageRepository.cs | 7 ++++++- .../Models/Mapping/LanguageMapperProfile.cs | 18 +----------------- 2 files changed, 7 insertions(+), 18 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs index db2e1124a2..740015683a 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs @@ -52,7 +52,12 @@ namespace Umbraco.Core.Persistence.Repositories.Implement sql.OrderBy(dto => dto.Id); // get languages - var languages = Database.Fetch(sql).Select(ConvertFromDto).ToList(); + var languages = Database.Fetch(sql).Select(ConvertFromDto).OrderBy(x => x.Id).ToList(); + + // fix inconsistencies: there has to be a default language, and it has to be mandatory + var defaultLanguage = languages.FirstOrDefault(x => x.IsDefaultVariantLanguage) ?? languages.First(); + defaultLanguage.IsDefaultVariantLanguage = true; + defaultLanguage.Mandatory = true; // initialize the code-id map lock (_codeIdMap) diff --git a/src/Umbraco.Web/Models/Mapping/LanguageMapperProfile.cs b/src/Umbraco.Web/Models/Mapping/LanguageMapperProfile.cs index b00a0949ec..b305ee2824 100644 --- a/src/Umbraco.Web/Models/Mapping/LanguageMapperProfile.cs +++ b/src/Umbraco.Web/Models/Mapping/LanguageMapperProfile.cs @@ -28,23 +28,7 @@ namespace Umbraco.Web.Models.Mapping { public IEnumerable Convert(IEnumerable source, IEnumerable destination, ResolutionContext context) { - var allLanguages = source.OrderBy(x => x.Id).ToList(); - var langs = new List(allLanguages.Select(x => context.Mapper.Map(x, null, context))); - - //if there's only one language, by default it is the default - if (langs.Count == 1) - { - langs[0].IsDefaultVariantLanguage = true; - langs[0].Mandatory = true; - } - else if (allLanguages.All(x => !x.IsDefaultVariantLanguage)) - { - //if no language has the default flag, then the defaul language is the one with the lowest id - langs[0].IsDefaultVariantLanguage = true; - langs[0].Mandatory = true; - } - - return langs.OrderBy(x => x.Name); + return source.Select(x => context.Mapper.Map(x, null, context)).OrderBy(x => x.Name); } } } From a6976085294d08c849674c02bafced5e3c09446c Mon Sep 17 00:00:00 2001 From: Stephan Date: Tue, 12 Jun 2018 17:05:37 +0200 Subject: [PATCH 03/14] Fix un/publishing with variants + urls --- .../Services/Implement/ContentService.cs | 147 ++++++----- src/Umbraco.Web.UI/Umbraco/config/lang/en.xml | 228 +++++++++--------- .../Umbraco/config/lang/en_us.xml | 228 +++++++++--------- src/Umbraco.Web/PublishedContentExtensions.cs | 17 +- src/Umbraco.Web/Routing/DefaultUrlProvider.cs | 27 +-- .../Routing/UrlProviderExtensions.cs | 82 +++++-- 6 files changed, 377 insertions(+), 352 deletions(-) diff --git a/src/Umbraco.Core/Services/Implement/ContentService.cs b/src/Umbraco.Core/Services/Implement/ContentService.cs index 0c0015019a..2e3fcf8522 100644 --- a/src/Umbraco.Core/Services/Implement/ContentService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentService.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; +using Umbraco.Core.Composing; using Umbraco.Core.Events; using Umbraco.Core.Exceptions; using Umbraco.Core.IO; @@ -857,6 +858,14 @@ namespace Umbraco.Core.Services.Implement // fixme - kill all those raiseEvents + // fixme + // the API is not consistent + // SaveAndPublish requires that SetPublishValues / ClearPublishValues is called beforehand + // Unpublish will unpublish one culture - no idea what happens if we SetPublishValues / ClearPublishValues beforehand + // and there's not way to just plainly unpublish the whole thing - refactor! + // + // plus, does it make any sense for the 'default' culture to also be 'mandatory'? + /// public OperationResult Save(IContent content, int userId = 0, bool raiseEvents = true) { @@ -944,6 +953,11 @@ namespace Umbraco.Core.Services.Implement /// public PublishResult SaveAndPublish(IContent content, int userId = 0, bool raiseEvents = true) { + // note: SaveAndPublish does *not* publishes value, it assumes that content.PublishValues() + // has been called for the variations that are to be published - those are are already + // published remain published - can also be used to simply put the content back into its + // previous published state after it had been completely unpublished + var evtMsgs = EventMessagesFactory.Get(); PublishResult result; @@ -1028,118 +1042,97 @@ namespace Umbraco.Core.Services.Implement /// public UnpublishResult Unpublish(IContent content, string culture = null, string segment = null, int userId = 0) { + // unpublish a variation - and that may end up unpublishing the document as a whole, + // if the variation was mandatory (ie for a mandatory language - no idea what would happen w/ segments) + + if (!segment.IsNullOrWhiteSpace()) + throw new NotImplementedException("Segments are not supported."); + var evtMsgs = EventMessagesFactory.Get(); using (var scope = ScopeProvider.CreateScope()) { scope.WriteLock(Constants.Locks.ContentTree); - var tryUnpublishVariation = TryUnpublishVariationInternal(scope, content, evtMsgs, culture, segment, userId); - - if (tryUnpublishVariation) return tryUnpublishVariation.Result; - - //continue the normal Unpublish operation to unpublish the entire content item - var result = UnpublishInternal(scope, content, evtMsgs, userId); - - //not succesful, so return it - if (!result.Success) return result; - - //else check if there was a status returned from TryUnpublishVariationInternal and if so use that - return tryUnpublishVariation.Result ?? result; - } - } - - /// - /// Used to unpublish the requested variant if possible - /// - /// - /// - /// - /// - /// - /// - /// - /// A successful attempt if a variant was unpublished and it wasn't the last, else a failed result if it's an invariant document or if it's the last. - /// A failed result indicates that a normal unpublish operation will need to be performed. - /// - private Attempt TryUnpublishVariationInternal(IScope scope, IContent content, EventMessages evtMsgs, string culture, string segment, int userId) - { - if (!culture.IsNullOrWhiteSpace() || !segment.IsNullOrWhiteSpace()) - { - //now we need to check if this is not the last culture/segment that is published - if (!culture.IsNullOrWhiteSpace()) + // not varying, or invariant culture + // simply unpublish the document + if (!content.ContentType.Variations.DoesSupportCulture() || culture.IsNullOrWhiteSpace()) { - //capture before we clear the values - var publishedCultureCount = content.PublishedCultures.Count(); + var unpublished = UnpublishScoped(scope, content, evtMsgs, userId); + if (unpublished.Success) scope.Complete(); + return unpublished; + } - //we need to unpublish everything if this is a mandatory language - var unpublishAll = _languageRepository.GetMany().Where(x => x.Mandatory).Select(x => x.IsoCode).Contains(culture, StringComparer.InvariantCultureIgnoreCase); + // varying, with culture = deal with cultures - content.ClearPublishedValues(culture, segment); - //now we just publish with the name cleared - SaveAndPublish(content, userId); + // if already unpublished, nothing to do + var publishedCultures = content.PublishedCultures.ToList(); + if (!publishedCultures.Contains(culture, StringComparer.OrdinalIgnoreCase)) + { + scope.Complete(); + return new UnpublishResult(UnpublishResultType.SuccessAlready, evtMsgs, content); + } + + // otherwise, in any case, clear the unpublishing variation + content.ClearPublishedValues(culture, segment); + + // would there be any culture left? + var mandatoryCultures = _languageRepository.GetMany().Where(x => x.Mandatory).Select(x => x.IsoCode); + var isMandatory = mandatoryCultures.Contains(culture, StringComparer.OrdinalIgnoreCase); + if (isMandatory || publishedCultures.Count == 1) + { + // nothing left = unpublish all + var unpublished = UnpublishScoped(scope, content, evtMsgs, userId); + if (unpublished.Success) scope.Complete(); + return unpublished; + } + + // else we need to republish, without that culture + var saved = SaveAndPublish(content, userId); + if (saved.Success) + { Audit(AuditType.UnPublish, $"UnPublish variation culture: {culture ?? string.Empty}, segment: {segment ?? string.Empty} performed by user", userId, content.Id); - - //We don't need to unpublish all and this is not the last one, so complete the scope and return - if (!unpublishAll && publishedCultureCount > 1) - { - scope.Complete(); - return Attempt.Succeed(new UnpublishResult(UnpublishResultType.SuccessVariant, evtMsgs, content)); - } - - if (unpublishAll) - { - //return a fail with a custom status here so the normal unpublish operation takes place but the result will be this flag - return Attempt.Fail(new UnpublishResult(UnpublishResultType.SuccessMandatoryCulture, evtMsgs, content)); - } + scope.Complete(); + return new UnpublishResult(UnpublishResultType.SuccessVariant, evtMsgs, content); } - if (!segment.IsNullOrWhiteSpace()) - { - //TODO: When segments are supported, implement this, a discussion needs to happen about what how a segment is 'published' or not - // since currently this is very different from a culture. - throw new NotImplementedException("Segments are currently not supported in Umbraco"); - } + // failed - map result + var r = saved.Result == PublishResultType.FailedCancelledByEvent + ? UnpublishResultType.FailedCancelledByEvent + : UnpublishResultType.Failed; + return new UnpublishResult(r, evtMsgs, content); } - - //This is either a non variant document or this is the last one or this was a mandatory variant, so return a failed result which indicates that a normal unpublish operation needs to also take place - return Attempt.Fail(); } /// - /// Performs the logic to unpublish an entire document + /// Unpublishes an entire document. /// - /// - /// - /// - /// - /// - private UnpublishResult UnpublishInternal(IScope scope, IContent content, EventMessages evtMsgs, int userId) + private UnpublishResult UnpublishScoped(IScope scope, IContent content, EventMessages evtMsgs, int userId) { var newest = GetById(content.Id); // ensure we have the newest version if (content.VersionId != newest.VersionId) // but use the original object if it's already the newest version content = newest; - if (content.Published == false) - { - scope.Complete(); - return new UnpublishResult(UnpublishResultType.SuccessAlready, evtMsgs, content); // already unpublished - } - // strategy + // if already unpublished, nothing to do + if (!content.Published) + return new UnpublishResult(UnpublishResultType.SuccessAlready, evtMsgs, content); + + // run strategies // fixme should we still complete the uow? don't want to rollback here! var attempt = StrategyCanUnpublish(scope, content, userId, evtMsgs); if (attempt.Success == false) return attempt; // causes rollback attempt = StrategyUnpublish(scope, content, true, userId, evtMsgs); if (attempt.Success == false) return attempt; // causes rollback + // save content.WriterId = userId; _documentRepository.Save(content); + // events and audit scope.Events.Dispatch(UnPublished, this, new PublishEventArgs(content, false, false), "UnPublished"); scope.Events.Dispatch(TreeChanged, this, new TreeChange(content, TreeChangeTypes.RefreshBranch).ToEventArgs()); Audit(AuditType.UnPublish, "UnPublish performed by user", userId, content.Id); - scope.Complete(); return new UnpublishResult(evtMsgs, content); } diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml index 39e578df45..450b33d816 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml @@ -147,6 +147,8 @@ Remove at This item has been changed after publication This item is not published + Culture '%0%' for this item is not published + Culture '%0%' for this item is not available Last published There are no items to show There are no items to show in the list. @@ -770,41 +772,41 @@ To manage your website, simply open the Umbraco back office and start adding con Umbraco: Reset Password - - - - - - + + + + + + + - - + +
+
- - - + + + -
- -
- -
+ +
+ +
-
+ - + - - - + + +
+

- - + + -
+
- - + +
+

Password reset requested

@@ -814,12 +816,12 @@ To manage your website, simply open the Umbraco back office and start adding con

- - + + +
+
Click this link to reset your password - -
@@ -831,22 +833,22 @@ To manage your website, simply open the Umbraco back office and start adding con %1% -

-
-
+ - -


- - - + +


+ + + - + ]]> @@ -889,53 +891,53 @@ To manage your website, simply open the Umbraco back office and start adding con
- - - - - - + + + + + + + - - + +
+
- - + + -
- -
- -
+ +
+ +
-
+ - + - - - + + -
+

- - + - +
+
- - +
+

Hi %0%,

-

+

This is an automated mail to inform you that the task '%1%' has been performed on the page '%2%' by the user '%3%'

- - @@ -945,7 +947,7 @@ To manage your website, simply open the Umbraco back office and start adding con

Update summary:

+ +
EDIT
%6% -
+

Have a nice day!

@@ -957,10 +959,10 @@ To manage your website, simply open the Umbraco back office and start adding con

-


+


@@ -1519,9 +1521,9 @@ To manage your website, simply open the Umbraco back office and start adding con Hide this property value from content editors that don't have access to view sensitive information Show on member profile Allow this property value to be displayed on the member profile page - + tab has no sort order - + Where is this composition used? This composition is currently used in the composition of the following content types: @@ -1775,41 +1777,41 @@ To manage your website, simply open the Umbraco back office and start adding con Umbraco: Invitation - - - - - - + + + + + + + - - + +
+
- - - + + + -
- -
- -
+ +
+ +
-
+
+ - - - + + +
+

- - + + -
+
- - + +
+

Hi %0%,

@@ -1827,12 +1829,12 @@ To manage your website, simply open the Umbraco back office and start adding con
- - + + +
+
Click this link to accept the invite - -
@@ -1847,22 +1849,22 @@ To manage your website, simply open the Umbraco back office and start adding con %3% -

-
-
+ - -


- - - + +


+ + + - + ]]>
diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml index b70cfe58e0..3b3d5b4b6e 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -181,6 +181,8 @@ Remove at This item has been changed after publication This item is not published + Culture '%0%' for this item is not published + Culture '%0%' for this item is not available Last published There are no items to show There are no items to show in the list. @@ -897,41 +899,41 @@ To manage your website, simply open the Umbraco back office and start adding con Umbraco: Reset Password - - - - - - + + + + + + + - - + +
+
- - - + + + -
- -
- -
+ +
+ +
-
+ - + - - - + + +
+

- - + + -
+
- - + +
+

Password reset requested

@@ -941,12 +943,12 @@ To manage your website, simply open the Umbraco back office and start adding con

- - + + +
+
Click this link to reset your password - -
@@ -958,22 +960,22 @@ To manage your website, simply open the Umbraco back office and start adding con %1% -

-
-
+ - -


- - - + +


+ + + - + ]]> @@ -1017,53 +1019,53 @@ To manage your website, simply open the Umbraco back office and start adding con
- - - - - - + + + + + + + - - + +
+
- - + + -
- -
- -
+ +
+ +
-
+ - + - - - + + -
+

- - + - +
+
- - +
+

Hi %0%,

-

+

This is an automated mail to inform you that the task '%1%' has been performed on the page '%2%' by the user '%3%'

- - @@ -1073,7 +1075,7 @@ To manage your website, simply open the Umbraco back office and start adding con

Update summary:

+ +
EDIT
%6% -
+

Have a nice day!

@@ -1085,10 +1087,10 @@ To manage your website, simply open the Umbraco back office and start adding con

-


+


@@ -1646,9 +1648,9 @@ To manage your website, simply open the Umbraco back office and start adding con Hide this property value from content editors that don't have access to view sensitive information Show on member profile Allow this property value to be displayed on the member profile page - + tab has no sort order - + Where is this composition used? This composition is currently used in the composition of the following content types: @@ -1911,41 +1913,41 @@ To manage your website, simply open the Umbraco back office and start adding con Umbraco: Invitation - - - - - - + + + + + + + - - + +
+
- - - + + + -
- -
- -
+ +
+ +
-
+
+ - - - + + +
+

- - + + -
+
- - + +
+

Hi %0%,

@@ -1963,12 +1965,12 @@ To manage your website, simply open the Umbraco back office and start adding con
- - + + +
+
Click this link to accept the invite - -
@@ -1983,22 +1985,22 @@ To manage your website, simply open the Umbraco back office and start adding con %3% -

-
-
+ - -


- - - + +


+ + + - + ]]>
diff --git a/src/Umbraco.Web/PublishedContentExtensions.cs b/src/Umbraco.Web/PublishedContentExtensions.cs index 1adfb55ca9..1d3c747b6e 100644 --- a/src/Umbraco.Web/PublishedContentExtensions.cs +++ b/src/Umbraco.Web/PublishedContentExtensions.cs @@ -37,17 +37,6 @@ namespace Umbraco.Web return content.Url; } - /// - /// Gets the absolute url for the content. - /// - /// The content. - /// The absolute url for the content. - //[Obsolete("UrlWithDomain() is obsolete, use the UrlAbsolute() method instead.")] - public static string UrlWithDomain(this IPublishedContent content) - { - return content.UrlAbsolute(); - } - /// /// Gets the absolute url for the content. /// @@ -85,8 +74,12 @@ namespace Umbraco.Web if (!content.ContentType.Variations.Has(ContentVariation.CultureNeutral)) return content.UrlSegment; + // content.GetCulture(culture) will use the 'current' culture (via accessor) in case 'culture' + // is null (meaning, 'current') - and can return 'null' if that culture is not published - and + // will return 'null' if the content is variant and culture is invariant + // else try and get the culture info - // return the corresponding url segment, or null if none (ie the culture is not published) + // return the corresponding url segment, or null if none var cultureInfo = content.GetCulture(culture); return cultureInfo?.UrlSegment; } diff --git a/src/Umbraco.Web/Routing/DefaultUrlProvider.cs b/src/Umbraco.Web/Routing/DefaultUrlProvider.cs index e40dcd0560..7ea359fa67 100644 --- a/src/Umbraco.Web/Routing/DefaultUrlProvider.cs +++ b/src/Umbraco.Web/Routing/DefaultUrlProvider.cs @@ -79,23 +79,18 @@ namespace Umbraco.Web.Routing /// public virtual IEnumerable GetOtherUrls(UmbracoContext umbracoContext, int id, Uri current) { - //get the invariant route for this item, this will give us the Id of it's domain node if one is assigned - var invariantRoute = umbracoContext.ContentCache.GetRouteById(id); - - if (string.IsNullOrWhiteSpace(invariantRoute)) - { - _logger.Debug(() => $"Couldn't find any page with nodeId={id}. This is most likely caused by the page not being published."); - return null; - } - + var node = umbracoContext.ContentCache.GetById(id); var domainHelper = umbracoContext.GetDomainHelper(_siteDomainHelper); - // extract domainUri and path - // route is / or / - var pos = invariantRoute.IndexOf('/'); - var path = pos == 0 ? invariantRoute : invariantRoute.Substring(pos); - var domainUris = pos == 0 ? null : domainHelper.DomainsForNode(int.Parse(invariantRoute.Substring(0, pos)), current); + var n = node; + var domainUris = domainHelper.DomainsForNode(n.Id, current, false); + while (domainUris == null && n != null) // n is null at root + { + n = n.Parent; // move to parent node + domainUris = n == null ? null : domainHelper.DomainsForNode(n.Id, current, false); + } + // no domains = exit if (domainUris ==null) return Enumerable.Empty(); @@ -107,8 +102,8 @@ namespace Umbraco.Web.Routing if (route == null) continue; //need to strip off the leading ID for the route if it exists (occurs if the route is for a node with a domain assigned) - pos = route.IndexOf('/'); - path = pos == 0 ? route : route.Substring(pos); + var pos = route.IndexOf('/'); + var path = pos == 0 ? route : route.Substring(pos); var uri = new Uri(CombinePaths(d.Uri.GetLeftPart(UriPartial.Path), path)); uri = UriUtility.UriFromUmbraco(uri, _globalSettings, _requestSettings); diff --git a/src/Umbraco.Web/Routing/UrlProviderExtensions.cs b/src/Umbraco.Web/Routing/UrlProviderExtensions.cs index f7a671b1e3..86fc51fe44 100644 --- a/src/Umbraco.Web/Routing/UrlProviderExtensions.cs +++ b/src/Umbraco.Web/Routing/UrlProviderExtensions.cs @@ -1,25 +1,22 @@ using System; using System.Collections.Generic; +using System.Threading; using Umbraco.Core.Models; using Umbraco.Core.Services; using Umbraco.Core; using Umbraco.Core.Logging; using LightInject; -using Umbraco.Web.Composing; namespace Umbraco.Web.Routing { internal static class UrlProviderExtensions { /// - /// Gets the URLs for the content item + /// Gets the Urls of the content item. /// - /// - /// - /// /// - /// Use this when displaying URLs, if there are errors genertaing the urls the urls themselves will - /// contain the errors. + /// Use when displaying Urls. If errors occur when generating the Urls, they will show in the list. + /// Contains all the Urls that we can figure out (based upon domains, etc). /// public static IEnumerable GetContentUrls(this IContent content, UrlProvider urlProvider, ILocalizedTextService textService, IContentService contentService, ILogger logger) { @@ -31,44 +28,87 @@ namespace Umbraco.Web.Routing var urls = new HashSet(); + // going to build a list of urls (essentially for the back-office) + // which will contain + // - the 'main' url, which is what .Url would return, in the current culture + // - the 'other' urls we know (based upon domains, etc) + // + // this essentially happens when producing the urls for the back-office, and then we don't have + // a meaningful 'current culture' - so we need to explicitely pass some culture where required, + // and deal with whatever might happen + // + // if content is variant, go with the current culture - and that is NOT safe, there may be + // no 'main' url for that culture, deal with it later - otherwise, go with the invariant + // culture, and that is safe. + var varies = content.ContentType.Variations.DoesSupportCulture(); + var culture = varies ? Thread.CurrentThread.CurrentUICulture.Name : ""; + if (content.Published == false) { urls.Add(textService.Localize("content/itemNotPublished")); return urls; } - string url; - try + string url = null; + + if (varies) { - url = urlProvider.GetUrl(content.Id); + if (!content.IsCulturePublished(culture)) + { + urls.Add(textService.Localize("content/itemCultureNotPublished", culture)); + // but keep going, we want to add the 'other' urls + url = "#no"; + } + else if (!content.IsCultureAvailable(culture)) + { + urls.Add(textService.Localize("content/itemCultureNotAvailable", culture)); + // but keep going, we want to add the 'other' urls + url = "#no"; + } } - catch (Exception e) + + // get the 'main' url + if (url == null) { - logger.Error("GetUrl exception.", e); - url = "#ex"; + try + { + url = urlProvider.GetUrl(content.Id, culture); + } + catch (Exception e) + { + logger.Error("GetUrl exception.", e); + url = "#ex"; + } } - if (url == "#") + + if (url == "#") // deal with 'could not get the url' { - // document as a published version yet it's url is "#" => a parent must be + // document as a published version yet its url is "#" => a parent must be // unpublished, walk up the tree until we find it, and report. var parent = content; do { parent = parent.ParentId > 0 ? parent.Parent(contentService) : null; } - while (parent != null && parent.Published); + while (parent != null && parent.Published && (!varies || parent.IsCulturePublished(culture))); urls.Add(parent == null ? textService.Localize("content/parentNotPublishedAnomaly") // oops - internal error : textService.Localize("content/parentNotPublished", new[] { parent.Name })); } - else if (url == "#ex") + else if (url == "#ex") // deal with exceptions { urls.Add(textService.Localize("content/getUrlException")); } - else + else if (url == "#no") // deal with 'there is no main url' { - // test for collisions + // get the 'other' urls + foreach(var otherUrl in urlProvider.GetOtherUrls(content.Id)) + urls.Add(otherUrl); + } + else // detect collisions, etc + { + // test for collisions on the 'main' url var uri = new Uri(url.TrimEnd('/'), UriKind.RelativeOrAbsolute); if (uri.IsAbsoluteUri == false) uri = uri.MakeAbsolute(UmbracoContext.Current.CleanedUmbracoUrl); uri = UriUtility.UriToUmbraco(uri); @@ -105,10 +145,10 @@ namespace Umbraco.Web.Routing else { urls.Add(url); + + // get the 'other' urls foreach(var otherUrl in urlProvider.GetOtherUrls(content.Id)) - { urls.Add(otherUrl); - } } } return urls; From ecf9a928d7a91765b68f046f542c25e51d4fafa0 Mon Sep 17 00:00:00 2001 From: Stephan Date: Wed, 20 Jun 2018 14:18:57 +0200 Subject: [PATCH 04/14] Refactor variations --- .../ContentVariationExtensions.cs | 148 ++++---- .../Migrations/Install/DatabaseDataCreator.cs | 40 +-- src/Umbraco.Core/Models/Content.cs | 337 ++++++++---------- src/Umbraco.Core/Models/ContentBase.cs | 185 +++++----- src/Umbraco.Core/Models/ContentTypeBase.cs | 45 +-- src/Umbraco.Core/Models/ContentVariation.cs | 54 ++- src/Umbraco.Core/Models/IContent.cs | 104 +++--- src/Umbraco.Core/Models/IContentBase.cs | 39 +- src/Umbraco.Core/Models/IContentTypeBase.cs | 26 +- src/Umbraco.Core/Models/Property.cs | 218 ++++++++--- src/Umbraco.Core/Models/PropertyType.cs | 37 +- .../PublishedContent/PublishedContentType.cs | 2 +- .../PublishedContentTypeFactory.cs | 8 +- .../PublishedContent/RawValueProperty.cs | 2 +- .../Factories/ContentBaseFactory.cs | 25 +- .../Persistence/Factories/PropertyFactory.cs | 8 +- .../Implement/DocumentRepository.cs | 201 ++++++----- .../Implement/EntityRepository.cs | 2 +- .../Services/Implement/ContentService.cs | 2 +- src/Umbraco.Core/StringExtensions.cs | 2 +- .../Strings/DefaultUrlSegmentProvider.cs | 2 +- src/Umbraco.Tests/Models/VariationTests.cs | 197 ++++++---- .../Repositories/ContentRepositoryTest.cs | 32 +- .../PublishedContent/NuCacheTests.cs | 12 +- .../PublishedContentDataTableTests.cs | 4 +- .../PublishedContent/PublishedRouterTests.cs | 2 +- .../SolidPublishedSnapshot.cs | 4 +- .../Routing/ContentFinderByAliasTests.cs | 4 +- .../ContentFinderByAliasWithDomainsTests.cs | 4 +- src/Umbraco.Tests/Routing/UrlProviderTests.cs | 9 +- .../Routing/UrlsProviderWithDomainsTests.cs | 6 +- .../Services/ContentServiceTests.cs | 134 +++---- .../Services/EntityServiceTests.cs | 19 +- .../Testing/TestingTests/MockTests.cs | 3 +- .../UmbracoExamine/IndexInitializer.cs | 4 +- .../UmbracoExamine/SearchTests.cs | 2 +- .../Web/TemplateUtilitiesTests.cs | 2 +- src/Umbraco.Web/Editors/ContentController.cs | 20 +- .../Mapping/ContentItemDisplayNameResolver.cs | 9 +- .../ContentItemDisplayVariationResolver.cs | 4 +- .../Mapping/ContentPropertyBasicConverter.cs | 4 +- .../Mapping/ContentTypeMapperProfile.cs | 2 +- .../Mapping/ContentTypeVariationsResolver.cs | 6 +- .../Mapping/PropertyTypeGroupResolver.cs | 2 +- .../Mapping/PropertyTypeVariationsResolver.cs | 7 +- .../PublishedCache/NuCache/Property.cs | 5 +- .../NuCache/PublishedContent.cs | 6 +- .../NuCache/PublishedSnapshotService.cs | 4 +- src/Umbraco.Web/PublishedContentExtensions.cs | 2 +- src/Umbraco.Web/Routing/DefaultUrlProvider.cs | 2 +- src/Umbraco.Web/Routing/DomainHelper.cs | 4 +- src/Umbraco.Web/Routing/PublishedRouter.cs | 2 +- .../Routing/UrlProviderExtensions.cs | 2 +- .../Trees/ContentTreeController.cs | 2 +- .../WebApi/Binders/ContentItemBinder.cs | 4 +- src/Umbraco.Web/umbraco.presentation/page.cs | 6 +- 56 files changed, 1121 insertions(+), 897 deletions(-) diff --git a/src/Umbraco.Core/ContentVariationExtensions.cs b/src/Umbraco.Core/ContentVariationExtensions.cs index 275cc61425..03abbcbc9e 100644 --- a/src/Umbraco.Core/ContentVariationExtensions.cs +++ b/src/Umbraco.Core/ContentVariationExtensions.cs @@ -1,82 +1,110 @@ -using Umbraco.Core.Models; +using System; +using Umbraco.Core.Models; +using Umbraco.Core.Models.PublishedContent; namespace Umbraco.Core { /// - /// Provides extension methods for various enumerations. + /// Provides extension methods for content variations. /// public static class ContentVariationExtensions { - /// - /// Determines whether a variation has all flags set. - /// - public static bool Has(this ContentVariation variation, ContentVariation values) - => (variation & values) == values; + // fixme document + public static bool VariesByNothing(this IContentTypeBase contentType) => contentType.Variations.VariesByNothing(); + public static bool VariesByCulture(this IContentTypeBase contentType) => contentType.Variations.VariesByCulture(); + public static bool VariesBySegment(this IContentTypeBase contentType) => contentType.Variations.VariesBySegment(); + public static bool VariesByCultureAndSegment(this IContentTypeBase contentType) => contentType.Variations.VariesByCultureAndSegment(); + + public static bool VariesByNothing(this PropertyType propertyType) => propertyType.Variations.VariesByNothing(); + public static bool VariesByCulture(this PropertyType propertyType) => propertyType.Variations.VariesByCulture(); + public static bool VariesBySegment(this PropertyType propertyType) => propertyType.Variations.VariesBySegment(); + public static bool VariesByCultureAndSegment(this PropertyType propertyType) => propertyType.Variations.VariesByCultureAndSegment(); + + public static bool VariesByNothing(this PublishedContentType contentType) => contentType.Variations.VariesByNothing(); + public static bool VariesByCulture(this PublishedContentType contentType) => contentType.Variations.VariesByCulture(); + public static bool VariesBySegment(this PublishedContentType contentType) => contentType.Variations.VariesBySegment(); + public static bool VariesByCultureAndSegment(this PublishedContentType contentType) => contentType.Variations.VariesByCultureAndSegment(); /// - /// Determines whether a variation has at least a flag set. + /// Determines whether a variation varies by nothing. /// - public static bool HasAny(this ContentVariation variation, ContentVariation values) - => (variation & values) != ContentVariation.Unknown; + public static bool VariesByNothing(this ContentVariation variation) => variation == ContentVariation.Nothing; /// - /// Determines whether a variation does not support culture variations + /// Determines whether a variation varies by culture. /// - /// - /// - public static bool DoesNotSupportCulture(this ContentVariation variation) + public static bool VariesByCulture(this ContentVariation variation) => (variation & ContentVariation.Culture) > 0; + + /// + /// Determines whether a variation varies by segment. + /// + public static bool VariesBySegment(this ContentVariation variation) => (variation & ContentVariation.Segment) > 0; + + /// + /// Determines whether a variation varies by culture and segment. + /// + public static bool VariesByCultureAndSegment(this ContentVariation variation) => (variation & ContentVariation.CultureAndSegment) > 0; + + /// + /// Validates that a combination of culture and segment is valid for the variation. + /// + /// The variation. + /// The culture. + /// The segment. + /// A value indicating whether to perform exact validation. + /// A value indicating whether to support wildcards. + /// A value indicating whether to throw a when the combination is invalid. + /// True if the combination is valid; otherwise false. + /// + /// When validation is exact, the combination must match the variation exactly. For instance, if the variation is Culture, then + /// a culture is required. When validation is not strict, the combination must be equivalent, or more restrictive: if the variation is + /// Culture, an invariant combination is ok. + /// Basically, exact is for one content type, or one property type, and !exact is for "all property types" of one content type. + /// Both and can be "*" to indicate "all of them". + /// + /// Occurs when the combination is invalid, and is true. + public static bool ValidateVariation(this ContentVariation variation, string culture, string segment, bool exact, bool wildcards, bool throwIfInvalid) { - return !variation.HasAny(ContentVariation.CultureNeutral | ContentVariation.CultureSegment); - } + culture = culture.NullOrWhiteSpaceAsNull(); + segment = segment.NullOrWhiteSpaceAsNull(); - /// - /// Determines whether a variation does support culture variations - /// - /// - /// - public static bool DoesSupportCulture(this ContentVariation variation) - { - return variation.HasAny(ContentVariation.CultureNeutral | ContentVariation.CultureSegment); - } + bool Validate(bool variesBy, string value) + { + if (variesBy) + { + // varies by + // in exact mode, the value cannot be null (but it can be a wildcard) + // in !wildcards mode, the value cannot be a wildcard (but it can be null) + if ((exact && value == null) || (!wildcards && value == "*")) + return false; + } + else + { + // does not vary by value + // the value cannot have a value + // unless wildcards and it's "*" + if (value != null && (!wildcards || value != "*")) + return false; + } - /// - /// Determines whether a variation does not support invariant variations - /// - /// - /// - public static bool DoesNotSupportInvariant(this ContentVariation variation) - { - return !variation.HasAny(ContentVariation.InvariantNeutral | ContentVariation.InvariantSegment); - } + return true; + } - /// - /// Determines whether a variation does support invariant variations - /// - /// - /// - public static bool DoesSupportInvariant(this ContentVariation variation) - { - return variation.HasAny(ContentVariation.InvariantNeutral | ContentVariation.InvariantSegment); - } + if (!Validate(variation.VariesByCulture(), culture)) + { + if (throwIfInvalid) + throw new NotSupportedException($"Culture value \"{culture ?? ""}\" is invalid."); + return false; + } - /// - /// Determines whether a variation does not support segment variations - /// - /// - /// - public static bool DoesNotSupportSegment(this ContentVariation variation) - { - return !variation.HasAny(ContentVariation.InvariantSegment | ContentVariation.CultureSegment); - } + if (!Validate(variation.VariesBySegment(), segment)) + { + if (throwIfInvalid) + throw new NotSupportedException($"Segment value \"{segment ?? ""}\" is invalid."); + return false; + } - /// - /// Determines whether a variation does not support neutral variations - /// - /// - /// - public static bool DoesNotSupportNeutral(this ContentVariation variation) - { - return !variation.HasAny(ContentVariation.InvariantNeutral | ContentVariation.CultureNeutral); + return true; } } } diff --git a/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs b/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs index 8063ba9f46..7c02f7ad75 100644 --- a/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs +++ b/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs @@ -156,10 +156,10 @@ namespace Umbraco.Core.Migrations.Install private void CreateContentTypeData() { - _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 532, NodeId = 1031, Alias = Constants.Conventions.MediaTypes.Folder, Icon = "icon-folder", Thumbnail = "icon-folder", IsContainer = false, AllowAtRoot = true, Variations = (byte) ContentVariation.InvariantNeutral }); - _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 533, NodeId = 1032, Alias = Constants.Conventions.MediaTypes.Image, Icon = "icon-picture", Thumbnail = "icon-picture", AllowAtRoot = true, Variations = (byte) ContentVariation.InvariantNeutral }); - _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 534, NodeId = 1033, Alias = Constants.Conventions.MediaTypes.File, Icon = "icon-document", Thumbnail = "icon-document", AllowAtRoot = true, Variations = (byte) ContentVariation.InvariantNeutral }); - _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 531, NodeId = 1044, Alias = Constants.Conventions.MemberTypes.DefaultAlias, Icon = "icon-user", Thumbnail = "icon-user", Variations = (byte) ContentVariation.InvariantNeutral }); + _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 532, NodeId = 1031, Alias = Constants.Conventions.MediaTypes.Folder, Icon = "icon-folder", Thumbnail = "icon-folder", IsContainer = false, AllowAtRoot = true, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 533, NodeId = 1032, Alias = Constants.Conventions.MediaTypes.Image, Icon = "icon-picture", Thumbnail = "icon-picture", AllowAtRoot = true, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 534, NodeId = 1033, Alias = Constants.Conventions.MediaTypes.File, Icon = "icon-document", Thumbnail = "icon-document", AllowAtRoot = true, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 531, NodeId = 1044, Alias = Constants.Conventions.MemberTypes.DefaultAlias, Icon = "icon-user", Thumbnail = "icon-user", Variations = (byte) ContentVariation.Nothing }); } private void CreateUserData() @@ -212,23 +212,23 @@ namespace Umbraco.Core.Migrations.Install private void CreatePropertyTypeData() { - _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 6, UniqueId = 6.ToGuid(), DataTypeId = 1043, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Constants.Conventions.Media.File, Name = "Upload image", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.InvariantNeutral }); - _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 7, UniqueId = 7.ToGuid(), DataTypeId = Constants.DataTypes.LabelInt, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Constants.Conventions.Media.Width, Name = "Width", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.InvariantNeutral }); - _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 8, UniqueId = 8.ToGuid(), DataTypeId = Constants.DataTypes.LabelInt, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Constants.Conventions.Media.Height, Name = "Height", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.InvariantNeutral }); - _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 9, UniqueId = 9.ToGuid(), DataTypeId = Constants.DataTypes.LabelBigint, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Constants.Conventions.Media.Bytes, Name = "Size", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.InvariantNeutral }); - _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 10, UniqueId = 10.ToGuid(), DataTypeId = -92, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Constants.Conventions.Media.Extension, Name = "Type", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.InvariantNeutral }); - _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 24, UniqueId = 24.ToGuid(), DataTypeId = -90, ContentTypeId = 1033, PropertyTypeGroupId = 4, Alias = Constants.Conventions.Media.File, Name = "Upload file", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.InvariantNeutral }); - _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 25, UniqueId = 25.ToGuid(), DataTypeId = -92, ContentTypeId = 1033, PropertyTypeGroupId = 4, Alias = Constants.Conventions.Media.Extension, Name = "Type", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.InvariantNeutral }); - _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 26, UniqueId = 26.ToGuid(), DataTypeId = Constants.DataTypes.LabelBigint, ContentTypeId = 1033, PropertyTypeGroupId = 4, Alias = Constants.Conventions.Media.Bytes, Name = "Size", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.InvariantNeutral }); - _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 27, UniqueId = 27.ToGuid(), DataTypeId = Constants.DataTypes.DefaultMediaListView, ContentTypeId = 1031, PropertyTypeGroupId = 5, Alias = "contents", Name = "Contents:", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.InvariantNeutral }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 6, UniqueId = 6.ToGuid(), DataTypeId = 1043, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Constants.Conventions.Media.File, Name = "Upload image", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 7, UniqueId = 7.ToGuid(), DataTypeId = Constants.DataTypes.LabelInt, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Constants.Conventions.Media.Width, Name = "Width", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 8, UniqueId = 8.ToGuid(), DataTypeId = Constants.DataTypes.LabelInt, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Constants.Conventions.Media.Height, Name = "Height", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 9, UniqueId = 9.ToGuid(), DataTypeId = Constants.DataTypes.LabelBigint, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Constants.Conventions.Media.Bytes, Name = "Size", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 10, UniqueId = 10.ToGuid(), DataTypeId = -92, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Constants.Conventions.Media.Extension, Name = "Type", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 24, UniqueId = 24.ToGuid(), DataTypeId = -90, ContentTypeId = 1033, PropertyTypeGroupId = 4, Alias = Constants.Conventions.Media.File, Name = "Upload file", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 25, UniqueId = 25.ToGuid(), DataTypeId = -92, ContentTypeId = 1033, PropertyTypeGroupId = 4, Alias = Constants.Conventions.Media.Extension, Name = "Type", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 26, UniqueId = 26.ToGuid(), DataTypeId = Constants.DataTypes.LabelBigint, ContentTypeId = 1033, PropertyTypeGroupId = 4, Alias = Constants.Conventions.Media.Bytes, Name = "Size", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 27, UniqueId = 27.ToGuid(), DataTypeId = Constants.DataTypes.DefaultMediaListView, ContentTypeId = 1031, PropertyTypeGroupId = 5, Alias = "contents", Name = "Contents:", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); //membership property types - _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 28, UniqueId = 28.ToGuid(), DataTypeId = -89, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.Comments, Name = Constants.Conventions.Member.CommentsLabel, SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.InvariantNeutral }); - _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 29, UniqueId = 29.ToGuid(), DataTypeId = Constants.DataTypes.LabelInt, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.FailedPasswordAttempts, Name = Constants.Conventions.Member.FailedPasswordAttemptsLabel, SortOrder = 1, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.InvariantNeutral }); - _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 30, UniqueId = 30.ToGuid(), DataTypeId = -49, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.IsApproved, Name = Constants.Conventions.Member.IsApprovedLabel, SortOrder = 2, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.InvariantNeutral }); - _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 31, UniqueId = 31.ToGuid(), DataTypeId = -49, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.IsLockedOut, Name = Constants.Conventions.Member.IsLockedOutLabel, SortOrder = 3, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.InvariantNeutral }); - _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.InvariantNeutral }); - _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.InvariantNeutral }); - _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.InvariantNeutral }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 28, UniqueId = 28.ToGuid(), DataTypeId = -89, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.Comments, Name = Constants.Conventions.Member.CommentsLabel, SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 29, UniqueId = 29.ToGuid(), DataTypeId = Constants.DataTypes.LabelInt, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.FailedPasswordAttempts, Name = Constants.Conventions.Member.FailedPasswordAttemptsLabel, SortOrder = 1, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 30, UniqueId = 30.ToGuid(), DataTypeId = -49, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.IsApproved, Name = Constants.Conventions.Member.IsApprovedLabel, SortOrder = 2, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 31, UniqueId = 31.ToGuid(), DataTypeId = -49, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.IsLockedOut, Name = Constants.Conventions.Member.IsLockedOutLabel, SortOrder = 3, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _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 }); } diff --git a/src/Umbraco.Core/Models/Content.cs b/src/Umbraco.Core/Models/Content.cs index e671b45968..d670d3a588 100644 --- a/src/Umbraco.Core/Models/Content.cs +++ b/src/Umbraco.Core/Models/Content.cs @@ -21,7 +21,7 @@ namespace Umbraco.Core.Models private DateTime? _releaseDate; private DateTime? _expireDate; private Dictionary _publishInfos; - private HashSet _edited; + private HashSet _editedCultures; private static readonly Lazy Ps = new Lazy(); @@ -188,123 +188,136 @@ namespace Umbraco.Core.Models [IgnoreDataMember] public IContentType ContentType => _contentType; + /// [IgnoreDataMember] - public DateTime? PublishDate { get; internal set; } + public DateTime? PublishDate { get; internal set; } // set by persistence + /// [IgnoreDataMember] - public int? PublisherId { get; internal set; } + public int? PublisherId { get; internal set; } // set by persistence + /// [IgnoreDataMember] - public ITemplate PublishTemplate { get; internal set; } + public ITemplate PublishTemplate { get; internal set; } // set by persistence + /// [IgnoreDataMember] - public string PublishName { get; internal set; } + public string PublishName { get; internal set; } // set by persistence - // sets publish infos - // internal for repositories - // clear by clearing name - internal void SetPublishInfos(string culture, string name, DateTime date) - { - if (string.IsNullOrWhiteSpace(name)) - throw new ArgumentNullOrEmptyException(nameof(name)); + /// + [IgnoreDataMember] + public IEnumerable EditedCultures => CultureNames.Keys.Where(IsCultureEdited); - // this is the only place where we set PublishName (apart from factories etc), and we must ensure - // that we do have an invariant name, as soon as we have a variant name, else we would end up not - // being able to publish - and not being able to change the name, as PublishName is readonly. - // see also: DocumentRepository.EnsureInvariantNameValues() - which deals with Name. - // see also: U4-11286 - if (culture == null || string.IsNullOrEmpty(PublishName)) - { - PublishName = name; - PublishDate = date; - } + /// + [IgnoreDataMember] + public IEnumerable PublishedCultures => _publishInfos?.Keys ?? Enumerable.Empty(); - if (culture != null) - { - // private method, assume that culture is valid + /// + public bool IsCulturePublished(string culture) + => _publishInfos != null && _publishInfos.ContainsKey(culture); - if (_publishInfos == null) - _publishInfos = new Dictionary(StringComparer.OrdinalIgnoreCase); - - _publishInfos[culture] = (name, date); - } - } + /// + public bool IsCultureEdited(string culture) + => !IsCulturePublished(culture) || (_editedCultures != null && _editedCultures.Contains(culture)); /// [IgnoreDataMember] - public IReadOnlyDictionary PublishCultureNames => _publishInfos?.ToDictionary(x => x.Key, x => x.Value.Name, StringComparer.OrdinalIgnoreCase) ?? NoNames; + public IReadOnlyDictionary PublishNames => _publishInfos?.ToDictionary(x => x.Key, x => x.Value.Name, StringComparer.OrdinalIgnoreCase) ?? NoNames; /// public string GetPublishName(string culture) { - if (culture == null) return PublishName; + if (culture.IsNullOrWhiteSpace()) return PublishName; + if (!ContentTypeBase.VariesByCulture()) return null; if (_publishInfos == null) return null; return _publishInfos.TryGetValue(culture, out var infos) ? infos.Name : null; } - // clears a publish name - private void ClearPublishName(string culture) + /// + public DateTime? GetPublishDate(string culture) { - if (culture == null) - { - PublishName = null; - return; - } - - if (_publishInfos == null) return; - _publishInfos.Remove(culture); - if (_publishInfos.Count == 0) - _publishInfos = null; + if (culture.IsNullOrWhiteSpace()) return PublishDate; + if (!ContentTypeBase.VariesByCulture()) return null; + if (_publishInfos == null) return null; + return _publishInfos.TryGetValue(culture, out var infos) ? infos.Date : (DateTime?) null; } - // clears all publish names - private void ClearPublishNames() + // internal for repository + internal void SetPublishInfo(string culture, string name, DateTime date) + { + if (string.IsNullOrWhiteSpace(name)) + throw new ArgumentNullOrEmptyException(nameof(name)); + + if (culture.IsNullOrWhiteSpace()) + throw new ArgumentNullOrEmptyException(nameof(culture)); + + if (_publishInfos == null) + _publishInfos = new Dictionary(StringComparer.OrdinalIgnoreCase); + + _publishInfos[culture] = (name, date); + } + + private void ClearPublishInfos() { - PublishName = null; _publishInfos = null; } - /// - public bool IsCulturePublished(string culture) - => !string.IsNullOrWhiteSpace(GetPublishName(culture)); - - /// - public DateTime GetCulturePublishDate(string culture) + private void ClearPublishInfo(string culture) { - if (_publishInfos != null && _publishInfos.TryGetValue(culture, out var infos)) - return infos.Date; - throw new InvalidOperationException($"Culture \"{culture}\" is not published."); + if (culture.IsNullOrWhiteSpace()) + throw new ArgumentNullOrEmptyException(nameof(culture)); + + if (_publishInfos == null) return; + _publishInfos.Remove(culture); + if (_publishInfos.Count == 0) _publishInfos = null; } - /// - public IEnumerable PublishedCultures => _publishInfos?.Keys ?? Enumerable.Empty(); + // fixme DOCUMENT + // sets publish name - used by persistence + //internal void UpdatePublishName(string culture, string name) + //{ + // if (string.IsNullOrWhiteSpace(name)) + // throw new ArgumentNullOrEmptyException(nameof(name)); - /// - public bool IsCultureEdited(string culture) - { - return string.IsNullOrWhiteSpace(GetPublishName(culture)) || (_edited != null && _edited.Contains(culture)); - } + // if (culture == null) // fixme that should NOT happen + // { + // PublishName = name; + // } + // else + // { + // // private method, assume that culture is valid + + // if (_publishInfos == null || !_publishInfos.TryGetValue(culture, out var pi)) + // throw new InvalidOperationException("Culture is not published."); + + // _publishInfos[culture] = (name, pi.Date); + // } + //} // sets a publish edited internal void SetCultureEdited(string culture) { - if (_edited == null) - _edited = new HashSet(StringComparer.OrdinalIgnoreCase); - _edited.Add(culture); + if (culture.IsNullOrWhiteSpace()) + throw new ArgumentNullOrEmptyException(nameof(culture)); + if (_editedCultures == null) + _editedCultures = new HashSet(StringComparer.OrdinalIgnoreCase); + _editedCultures.Add(culture); } // sets all publish edited internal void SetCultureEdited(IEnumerable cultures) { - _edited = new HashSet(cultures, StringComparer.OrdinalIgnoreCase); + if (cultures == null) + { + _editedCultures = null; + } + else + { + var editedCultures = new HashSet(cultures.Where(x => !x.IsNullOrWhiteSpace()), StringComparer.OrdinalIgnoreCase); + _editedCultures = editedCultures.Count > 0 ? editedCultures : null; + } } - /// - public IEnumerable EditedCultures => CultureNames.Keys.Where(IsCultureEdited); - - /// - public IEnumerable AvailableCultures => CultureNames.Keys; - [IgnoreDataMember] public int PublishedVersionId { get; internal set; } @@ -312,128 +325,80 @@ namespace Umbraco.Core.Models public bool Blueprint { get; internal set; } /// - internal virtual bool TryPublishAllValues() + public virtual bool TryPublishValues(string culture = null, string segment = null) // fixme should it default to "*", "*" instead? { - // the values we want to publish should be valid - if (ValidateAllProperties().Any()) - return false; //fixme this should return an attempt with error results + culture = culture.NullOrWhiteSpaceAsNull(); + segment = segment.NullOrWhiteSpaceAsNull(); - // Name and PublishName are managed by the repository, but Names and PublishNames - // must be managed here as they depend on the existing / supported variations. - if (string.IsNullOrWhiteSpace(Name)) - throw new InvalidOperationException($"Cannot publish invariant culture without a name."); - PublishName = Name; - var now = DateTime.Now; - foreach (var (culture, name) in CultureNames) - { - if (string.IsNullOrWhiteSpace(name)) - return false; //fixme this should return an attempt with error results - - SetPublishInfos(culture, name, now); - } - - // property.PublishAllValues only deals with supported variations (if any) - foreach (var property in Properties) - property.PublishAllValues(); - - _publishedState = PublishedState.Publishing; - return true; - } - - /// - public virtual bool TryPublishValues(string culture = null, string segment = null) - { - // the variation should be supported by the content type - ContentType.ValidateVariation(culture, segment, throwIfInvalid: true); + // the variation should be supported by the content type properties + if (!ContentType.SupportsPropertyVariation(culture, segment, true)) + throw new NotSupportedException($"Content type \"{ContentType.Alias}\" with variation \"{ContentType.Variations}\" does not support \"{culture??""},{segment??""}\"."); // the values we want to publish should be valid if (ValidateProperties(culture, segment).Any()) - return false; //fixme this should return an attempt with error results + return false; // fixme - should return an attempt with error results - // Name and PublishName are managed by the repository, but Names and PublishNames - // must be managed here as they depend on the existing / supported variations. - if (segment == null) + // when explicitely publishing values for a given segment, values for the segment are published, + // but that does not change which cultures are published. to publish a culture, publish 'null' or + // '*', and then we need to deal with culture names + if (segment == null || segment == "*") { - var name = GetName(culture); - if (string.IsNullOrWhiteSpace(name)) - return false; //fixme this should return an attempt with error results + // do not deal with invariant culture - it is always published - SetPublishInfos(culture, name, DateTime.Now); + if (culture == "*") // all cultures + { + foreach (var c in AvailableCultures) + { + var name = GetCultureName(c); + if (string.IsNullOrWhiteSpace(name)) + return false; + SetPublishInfo(c, name, DateTime.Now); + } + } + else if (!culture.IsNullOrWhiteSpace()) // one single culture + { + var name = GetCultureName(culture); + if (string.IsNullOrWhiteSpace(name)) + return false; + SetPublishInfo(culture, name, DateTime.Now); + } } - // property.PublishValue throws on invalid variation, so filter them out - foreach (var property in Properties.Where(x => x.PropertyType.ValidateVariation(culture, segment, throwIfInvalid: false))) - property.PublishValue(culture, segment); + // property.PublishValues only publishes what is valid, variation-wise + foreach (var property in Properties) + property.PublishValues(culture, segment); _publishedState = PublishedState.Publishing; return true; } /// - internal virtual bool PublishCultureValues(string culture = null) + public virtual void ClearPublishedValues(string culture = null, string segment = null) // fixme should it default to "*", "*" instead? { - //fixme - needs API review as this is not used apart from in tests + culture = culture.NullOrWhiteSpaceAsNull(); + segment = segment.NullOrWhiteSpaceAsNull(); - // the values we want to publish should be valid - if (ValidatePropertiesForCulture(culture).Any()) - return false; + // the variation should be supported by the content type properties + if (!ContentType.SupportsPropertyVariation(culture, segment, true)) + throw new NotSupportedException($"Content type \"{ContentType.Alias}\" with variation \"{ContentType.Variations}\" does not support \"{culture??""},{segment??""}\"."); - // Name and PublishName are managed by the repository, but Names and PublishNames - // must be managed here as they depend on the existing / supported variations. - var name = GetName(culture); - if (string.IsNullOrWhiteSpace(name)) - throw new InvalidOperationException($"Cannot publish {culture ?? "invariant"} culture without a name."); - SetPublishInfos(culture, name, DateTime.Now); + // when explicitely clearing values for a given segment, values for the segment are cleared, + // but that does not change which cultures are published. to unpublish a culture, clear 'null' or + // '*', and then we need to deal with culture names + if (segment == null || segment == "*") + { + // do not deal with invariant culture - it cannot be unpublished + // fixme - so should we throw instead? - // property.PublishCultureValues only deals with supported variations (if any) + if (culture == "*") // all cultures + ClearPublishInfos(); + else if (!culture.IsNullOrWhiteSpace()) // one single culture + ClearPublishInfo(culture); + } + + // property.PublishValues only publishes what is valid, variation-wise foreach (var property in Properties) - property.PublishCultureValues(culture); - - _publishedState = PublishedState.Publishing; - return true; - } - - /// - public virtual void ClearAllPublishedValues() - { - // property.ClearPublishedAllValues only deals with supported variations (if any) - foreach (var property in Properties) - property.ClearPublishedAllValues(); - - // Name and PublishName are managed by the repository, but Names and PublishNames - // must be managed here as they depend on the existing / supported variations. - ClearPublishNames(); - - _publishedState = PublishedState.Publishing; - } - - /// - public virtual void ClearPublishedValues(string culture = null, string segment = null) - { - // the variation should be supported by the content type - ContentType.ValidateVariation(culture, segment, throwIfInvalid: true); - - // property.ClearPublishedValue throws on invalid variation, so filter them out - foreach (var property in Properties.Where(x => x.PropertyType.ValidateVariation(culture, segment, throwIfInvalid: false))) - property.ClearPublishedValue(culture, segment); - - // Name and PublishName are managed by the repository, but Names and PublishNames - // must be managed here as they depend on the existing / supported variations. - ClearPublishName(culture); - - _publishedState = PublishedState.Publishing; - } - - /// - public virtual void ClearCulturePublishedValues(string culture = null) - { - // property.ClearPublishedCultureValues only deals with supported variations (if any) - foreach (var property in Properties) - property.ClearPublishedCultureValues(culture); - - // Name and PublishName are managed by the repository, but Names and PublishNames - // must be managed here as they depend on the existing / supported variations. - ClearPublishName(culture); + property.ClearPublishedValues(culture, segment); _publishedState = PublishedState.Publishing; } @@ -444,6 +409,8 @@ namespace Umbraco.Core.Models return Id == other.Id && VersionId == other.VersionId; } + // fixme must ALSO refactor copyValues + /// public virtual void CopyAllValues(IContent other) { @@ -460,7 +427,7 @@ namespace Umbraco.Core.Models // clear all existing properties foreach (var property in Properties) foreach (var pvalue in property.Values) - if (property.PropertyType.ValidateVariation(pvalue.Culture, pvalue.Segment, false)) + if (property.PropertyType.SupportsVariation(pvalue.Culture, pvalue.Segment)) property.SetValue(null, pvalue.Culture, pvalue.Segment); // copy other properties @@ -470,7 +437,7 @@ namespace Umbraco.Core.Models var alias = otherProperty.PropertyType.Alias; foreach (var pvalue in otherProperty.Values) { - if (!otherProperty.PropertyType.ValidateVariation(pvalue.Culture, pvalue.Segment, false)) + if (!otherProperty.PropertyType.SupportsVariation(pvalue.Culture, pvalue.Segment)) continue; var value = published ? pvalue.PublishedValue : pvalue.EditedValue; SetValue(alias, value, pvalue.Culture, pvalue.Segment); @@ -478,9 +445,9 @@ namespace Umbraco.Core.Models } // copy names - ClearNames(); + ClearCultureInfos(); foreach (var (culture, name) in other.CultureNames) - SetName(name, culture); + SetCultureName(name, culture); Name = other.Name; } @@ -500,7 +467,7 @@ namespace Umbraco.Core.Models // clear all existing properties foreach (var property in Properties) { - if (!property.PropertyType.ValidateVariation(culture, segment, false)) + if (!property.PropertyType.SupportsVariation(culture, segment)) continue; foreach (var pvalue in property.Values) @@ -512,7 +479,7 @@ namespace Umbraco.Core.Models var otherProperties = other.Properties; foreach (var otherProperty in otherProperties) { - if (!otherProperty.PropertyType.ValidateVariation(culture, segment, false)) + if (!otherProperty.PropertyType.SupportsVariation(culture, segment)) continue; var alias = otherProperty.PropertyType.Alias; @@ -520,7 +487,7 @@ namespace Umbraco.Core.Models } // copy name - SetName(other.GetName(culture), culture); + SetCultureName(other.GetCultureName(culture), culture); } /// @@ -536,7 +503,7 @@ namespace Umbraco.Core.Models // clear all existing properties foreach (var property in Properties) foreach (var pvalue in property.Values) - if (pvalue.Culture.InvariantEquals(culture) && property.PropertyType.ValidateVariation(pvalue.Culture, pvalue.Segment, false)) + if (pvalue.Culture.InvariantEquals(culture) && property.PropertyType.SupportsVariation(pvalue.Culture, pvalue.Segment)) property.SetValue(null, pvalue.Culture, pvalue.Segment); // copy other properties @@ -546,7 +513,7 @@ namespace Umbraco.Core.Models var alias = otherProperty.PropertyType.Alias; foreach (var pvalue in otherProperty.Values) { - if (pvalue.Culture != culture || !otherProperty.PropertyType.ValidateVariation(pvalue.Culture, pvalue.Segment, false)) + if (pvalue.Culture != culture || !otherProperty.PropertyType.SupportsVariation(pvalue.Culture, pvalue.Segment)) continue; var value = published ? pvalue.PublishedValue : pvalue.EditedValue; SetValue(alias, value, pvalue.Culture, pvalue.Segment); @@ -554,7 +521,7 @@ namespace Umbraco.Core.Models } // copy name - SetName(other.GetName(culture), culture); + SetCultureName(other.GetCultureName(culture), culture); } /// diff --git a/src/Umbraco.Core/Models/ContentBase.cs b/src/Umbraco.Core/Models/ContentBase.cs index 04fbe269f8..283054d501 100644 --- a/src/Umbraco.Core/Models/ContentBase.cs +++ b/src/Umbraco.Core/Models/ContentBase.cs @@ -56,7 +56,7 @@ namespace Umbraco.Core.Models Id = 0; // no identity VersionId = 0; // no versions - SetName(name, culture); + SetCultureName(name, culture); _contentTypeId = contentType.Id; _properties = properties ?? throw new ArgumentNullException(nameof(properties)); @@ -139,104 +139,101 @@ namespace Umbraco.Core.Models #region Cultures + // notes - common rules + // - setting a variant value on an invariant content type throws + // - getting a variant value on an invariant content type returns null + // - setting and getting the invariant value is always possible + // - setting a null value clears the value + + /// + public IEnumerable AvailableCultures + => _cultureInfos?.Select(x => x.Key) ?? Enumerable.Empty(); + + /// + public bool IsCultureAvailable(string culture) + => _cultureInfos != null && _cultureInfos.ContainsKey(culture); + /// [DataMember] public virtual IReadOnlyDictionary CultureNames => _cultureInfos?.ToDictionary(x => x.Key, x => x.Value.Name, StringComparer.OrdinalIgnoreCase) ?? NoNames; - // sets culture infos - // internal for repositories - // clear by clearing name - internal void SetCultureInfos(string culture, string name, DateTime date) - { - if (string.IsNullOrWhiteSpace(name)) - throw new ArgumentNullOrEmptyException(nameof(name)); - - if (culture == null) - { - Name = name; - return; - } - - // private method, assume that culture is valid - - if (_cultureInfos == null) - _cultureInfos = new Dictionary(StringComparer.OrdinalIgnoreCase); - - _cultureInfos[culture] = (name, date); - } - /// - public virtual void SetName(string name, string culture) + public virtual string GetCultureName(string culture) { - if (string.IsNullOrWhiteSpace(name)) - { - ClearName(culture); - return; - } - - if (culture == null) - { - Name = name; - return; - } - - if (ContentTypeBase.Variations.DoesNotSupportCulture()) - throw new NotSupportedException("Content type does not support varying name by culture."); - - if (_cultureInfos == null) - _cultureInfos = new Dictionary(StringComparer.OrdinalIgnoreCase); - - _cultureInfos[culture] = (name, DateTime.Now); - OnPropertyChanged(Ps.Value.NamesSelector); - } - - /// - public virtual string GetName(string culture) - { - if (culture == null) return Name; + if (culture.IsNullOrWhiteSpace()) return Name; + if (!ContentTypeBase.VariesByCulture()) return null; if (_cultureInfos == null) return null; return _cultureInfos.TryGetValue(culture, out var infos) ? infos.Name : null; } /// - public bool IsCultureAvailable(string culture) - => !string.IsNullOrWhiteSpace(GetName(culture)); - - private void ClearName(string culture) + public DateTime? GetCultureDate(string culture) { - if (culture == null) - { - Name = null; - return; - } - - if (ContentTypeBase.Variations.DoesNotSupportCulture()) - throw new NotSupportedException("Content type does not support varying name by culture."); - - if (_cultureInfos == null) return; - if (!_cultureInfos.ContainsKey(culture)) - throw new InvalidOperationException($"Cannot unpublish culture {culture}, the document contains only cultures {string.Join(", ", _cultureInfos.Keys)}"); - - _cultureInfos.Remove(culture); - if (_cultureInfos.Count == 0) - _cultureInfos = null; + if (culture.IsNullOrWhiteSpace()) return null; + if (!ContentTypeBase.VariesByCulture()) return null; + if (_cultureInfos == null) return null; + return _cultureInfos.TryGetValue(culture, out var infos) ? infos.Date : (DateTime?) null; } - protected virtual void ClearNames() + /// + public virtual void SetCultureName(string name, string culture) { - if (ContentTypeBase.Variations.DoesNotSupportCulture()) - throw new NotSupportedException("Content type does not support varying name by culture."); + if (ContentTypeBase.VariesByCulture()) // set on variant content type + { + if (culture.IsNullOrWhiteSpace()) // invariant is ok + { + Name = name; // may be null + } + else if (name.IsNullOrWhiteSpace()) // clear + { + ClearCultureInfo(culture); + } + else // set + { + SetCultureInfo(culture, name, DateTime.Now); + } + } + else // set on invariant content type + { + if (!culture.IsNullOrWhiteSpace()) // invariant is NOT ok + throw new NotSupportedException("Content type does not vary by culture."); + Name = name; // may be null + } + } + + protected void ClearCultureInfos() + { _cultureInfos = null; OnPropertyChanged(Ps.Value.NamesSelector); } - /// - public DateTime GetCultureDate(string culture) + protected void ClearCultureInfo(string culture) { - if (_cultureInfos != null && _cultureInfos.TryGetValue(culture, out var infos)) - return infos.Date; - throw new InvalidOperationException($"Culture \"{culture}\" is not available."); + if (culture.IsNullOrWhiteSpace()) + throw new ArgumentNullOrEmptyException(nameof(culture)); + + if (_cultureInfos == null) return; + _cultureInfos.Remove(culture); + if (_cultureInfos.Count == 0) + _cultureInfos = null; + OnPropertyChanged(Ps.Value.NamesSelector); + } + + // internal for repository + internal void SetCultureInfo(string culture, string name, DateTime date) + { + if (name.IsNullOrWhiteSpace()) + throw new ArgumentNullOrEmptyException(nameof(name)); + + if (culture.IsNullOrWhiteSpace()) + throw new ArgumentNullOrEmptyException(nameof(culture)); + + if (_cultureInfos == null) + _cultureInfos = new Dictionary(StringComparer.OrdinalIgnoreCase); + + _cultureInfos[culture.ToLowerInvariant()] = (name, date); + OnPropertyChanged(Ps.Value.NamesSelector); } #endregion @@ -307,17 +304,10 @@ namespace Umbraco.Core.Models #region Validation - internal virtual Property[] ValidateAllProperties() - { - //fixme - needs API review as this is not used apart from in tests - - return Properties.Where(x => !x.IsAllValid()).ToArray(); - } - /// public bool IsValid(string culture = null, string segment = null) { - var name = GetName(culture); + var name = GetCultureName(culture); if (name.IsNullOrWhiteSpace()) return false; return ValidateProperties(culture, segment).Length == 0; } @@ -325,25 +315,10 @@ namespace Umbraco.Core.Models /// public virtual Property[] ValidateProperties(string culture = null, string segment = null) { - return Properties.Where(x => - { - if (!culture.IsNullOrWhiteSpace() && x.PropertyType.Variations.DoesNotSupportCulture()) - return false; //has a culture, this prop is only culture invariant, ignore - if (culture.IsNullOrWhiteSpace() && x.PropertyType.Variations.DoesNotSupportInvariant()) - return false; //no culture, this prop is only culture variant, ignore - if (!segment.IsNullOrWhiteSpace() && x.PropertyType.Variations.DoesNotSupportSegment()) - return false; //has segment, this prop is only segment neutral, ignore - if (segment.IsNullOrWhiteSpace() && x.PropertyType.Variations.DoesNotSupportNeutral()) - return false; //no segment, this prop is only non segment neutral, ignore - return !x.IsValid(culture, segment); - }).ToArray(); - } - - internal virtual Property[] ValidatePropertiesForCulture(string culture = null) - { - //fixme - needs API review as this is not used apart from in tests - - return Properties.Where(x => !x.IsCultureValid(culture)).ToArray(); + return Properties.Where(x => // select properties... + x.PropertyType.SupportsVariation(culture, segment, true) && // that support the variation + !x.IsValid(culture, segment)) // and are not valid + .ToArray(); } #endregion diff --git a/src/Umbraco.Core/Models/ContentTypeBase.cs b/src/Umbraco.Core/Models/ContentTypeBase.cs index f1b61f424a..6f90b5201d 100644 --- a/src/Umbraco.Core/Models/ContentTypeBase.cs +++ b/src/Umbraco.Core/Models/ContentTypeBase.cs @@ -46,7 +46,7 @@ namespace Umbraco.Core.Models _propertyTypes = new PropertyTypeCollection(IsPublishing); _propertyTypes.CollectionChanged += PropertyTypesChanged; - _variations = ContentVariation.InvariantNeutral; + _variations = ContentVariation.Nothing; } protected ContentTypeBase(IContentTypeBase parent) @@ -67,7 +67,7 @@ namespace Umbraco.Core.Models _propertyTypes = new PropertyTypeCollection(IsPublishing); _propertyTypes.CollectionChanged += PropertyTypesChanged; - _variations = ContentVariation.InvariantNeutral; + _variations = ContentVariation.Nothing; } /// @@ -201,33 +201,22 @@ namespace Umbraco.Core.Models set => SetPropertyValueAndDetectChanges(value, ref _variations, Ps.Value.VaryBy); } - /// - /// Validates that a variation is valid for the content type. - /// - public bool ValidateVariation(string culture, string segment, bool throwIfInvalid) + /// + public bool SupportsVariation(string culture, string segment, bool wildcards = false) { - ContentVariation variation; - if (culture != null) - { - variation = segment != null - ? ContentVariation.CultureSegment - : ContentVariation.CultureNeutral; - } - else if (segment != null) - { - variation = ContentVariation.InvariantSegment; - } - else - { - variation = ContentVariation.InvariantNeutral; - } - if (!Variations.Has(variation)) - { - if (throwIfInvalid) - throw new NotSupportedException($"Variation {variation} is invalid for content type \"{Alias}\"."); - return false; - } - return true; + // exact validation: cannot accept a 'null' culture if the property type varies + // by culture, and likewise for segment + // wildcard validation: can accept a '*' culture or segment + return Variations.ValidateVariation(culture, segment, true, wildcards, false); + } + + /// + public bool SupportsPropertyVariation(string culture, string segment, bool wildcards = false) + { + // non-exact validation: can accept a 'null' culture if the property type varies + // by culture, and likewise for segment + // wildcard validation: can accept a '*' culture or segment + return Variations.ValidateVariation(culture, segment, false, true, false); } /// diff --git a/src/Umbraco.Core/Models/ContentVariation.cs b/src/Umbraco.Core/Models/ContentVariation.cs index 5b775b52b9..2759f2e075 100644 --- a/src/Umbraco.Core/Models/ContentVariation.cs +++ b/src/Umbraco.Core/Models/ContentVariation.cs @@ -3,34 +3,62 @@ namespace Umbraco.Core.Models { /// - /// Indicates the values accepted by a property. + /// Indicates how values can vary. /// + /// + /// Values can vary by nothing, or culture, or segment, or both. + /// Varying by culture implies that each culture version of a document can + /// be available or not, and published or not, individually. Varying by segment + /// is a property-level thing. + /// [Flags] public enum ContentVariation : byte { /// - /// Unknown. + /// Values do not vary. /// - Unknown = 0, + Nothing = 0, /// - /// Accepts values for the invariant culture and the neutral segment. + /// Values vary by culture. /// - InvariantNeutral = 1, + Culture = 1, /// - /// Accepts values for a specified culture and the neutral segment. + /// Values vary by segment. /// - CultureNeutral = 2, + Segment = 2, /// - /// Accepts values for the invariant culture and a specified segment. + /// Values vary by culture and segment. /// - InvariantSegment = 4, + CultureAndSegment = Culture | Segment - /// - /// Accepts values for a specified culture and a specified segment. - /// - CultureSegment = 8 + + // fixme - remove once we have a migration for DB values! + ///// + ///// Unknown. + ///// + //Unknown = 0, + + ///// + ///// Accepts values for the invariant culture and the neutral segment. + ///// + //InvariantNeutral = 1, + + ///// + ///// Accepts values for a specified culture and the neutral segment. + ///// + //CultureNeutral = 2, + + ///// + ///// Accepts values for the invariant culture and a specified segment. + ///// + //InvariantSegment = 4, + + ///// + ///// Accepts values for a specified culture and a specified segment. + ///// + //CultureSegment = 8 } } diff --git a/src/Umbraco.Core/Models/IContent.cs b/src/Umbraco.Core/Models/IContent.cs index 13797425ed..929ea990e5 100644 --- a/src/Umbraco.Core/Models/IContent.cs +++ b/src/Umbraco.Core/Models/IContent.cs @@ -93,7 +93,7 @@ namespace Umbraco.Core.Models /// /// Gets the date a culture was published. /// - DateTime GetCulturePublishDate(string culture); + DateTime? GetPublishDate(string culture); /// /// Gets a value indicated whether a given culture is edited. @@ -121,12 +121,7 @@ namespace Umbraco.Core.Models /// Because a dictionary key cannot be null this cannot get the invariant /// name, which must be get via the property. /// - IReadOnlyDictionary PublishCultureNames { get; } - - /// - /// Gets the available cultures. - /// - IEnumerable AvailableCultures { get; } + IReadOnlyDictionary PublishNames { get; } /// /// Gets the published cultures. @@ -161,54 +156,77 @@ namespace Umbraco.Core.Models /// IContent DeepCloneWithResetIdentities(); - /// - /// Publishes all values. - /// - /// A value indicating whether the values could be published. - /// - /// The document must then be published via the content service. - /// Values are not published if they are not valid. - /// - //fixme return an Attempt with some error results if it doesn't work - //fixme - needs API review as this is not used apart from in tests + ///// + ///// Publishes all values. + ///// + ///// A value indicating whether the values could be published. + ///// + ///// Fails if values cannot be published, e.g. if some values are not valid. + ///// Sets the property values for all cultures, including the invariant ones. + ///// Sets the published name for all culture that are available, thus publishing them all. + ///// The document must then be published via the content service SaveAndPublish method. + ///// + //// fixme - should return an attemps with error results + //// fixme - needs API review as this is not used apart from in tests << YES but users could use it //bool TryPublishAllValues(); + ///// + ///// Publishes the values for a specified culture and all segments. + ///// + ///// A value indicating whether the values could be published. + ///// + ///// Fails if values cannot be published, e.g. if some values are not valid. + ///// Sets the property values for the specified culture, and only the specified culture: must + ///// be invoked with a null culture to set the invariant values. + ///// Sets the published name for the specified culture, thus publishing the culture. + ///// The document must then be published via the content service SaveAndPublish method. + ///// + //// fixme - needs API review as this is not used apart from in tests << NO it is THAT one that we should use for now + //// fixme - should return an attemps with error results + //// fixme - should it publish the invariant values too? - NO that's done when SaveAndPublish (is it? don't think so) - BUT could we want to avoid it? + //bool TryPublishCultureValues(string culture); + /// - /// Publishes values. + /// Publishes values for a specific culture and segment. /// /// A value indicating whether the values could be published. /// - /// The document must then be published via the content service. - /// Values are not published if they are not valid. + /// Fails if values cannot be published, e.g. if some values are not valid. + /// Sets the property values but not the published name for the specified culture, + /// thus not explicitely publishing the culture. + /// The document must then be published via the content service SaveAndPublish method. /// - //fixme return an Attempt with some error results if it doesn't work + // fixme - should return an attemps with error results + // fixme - publishing for segments is not supported + // we don't know whether should it also publish the specified culture? + // we don't know how to publish segments but not neutral, etc + // what shall we do then? bool TryPublishValues(string culture = null, string segment = null); + ///// + ///// Clears published values. + ///// + ///// + ///// Clears the published name for all cultures, thus unpublishing all cultures. + ///// + //void ClearAllPublishedValues(); + /// - /// Publishes the culture/any values. + /// Clears published values for a specified culture and segment. /// - /// A value indicating whether the values could be published. /// - /// The document must then be published via the content service. - /// Values are not published if they are not valie. + /// Clears the property values but not the published name for the specified culture, + /// thus leaving the culture published. /// - //fixme - needs API review as this is not used apart from in tests - //bool PublishCultureValues(string culture = null); + void ClearPublishedValues(string culture = null, string segment = null); // fixme should NOT use - /// - /// Clears all published values. - /// - void ClearAllPublishedValues(); - - /// - /// Clears published values. - /// - void ClearPublishedValues(string culture = null, string segment = null); - - /// - /// Clears the culture/any published values. - /// - void ClearCulturePublishedValues(string culture = null); + ///// + ///// Clears published values for a specified culture, all segments. + ///// + ///// + ///// Clears the published name for the specified culture, thus unpublishing the culture. + ///// + //void ClearCulturePublishedValues(string culture = null); // fixme that one should be used! /// /// Copies values from another document. @@ -216,12 +234,12 @@ namespace Umbraco.Core.Models void CopyAllValues(IContent other); /// - /// Copies values from another document. + /// Copies values from another document for a specified culture and segment. /// void CopyValues(IContent other, string culture = null, string segment = null); /// - /// Copies culture/any values from another document. + /// Copies values from another document for a specified culture, all segments. /// void CopyCultureValues(IContent other, string culture = null); } diff --git a/src/Umbraco.Core/Models/IContentBase.cs b/src/Umbraco.Core/Models/IContentBase.cs index 1605c1da01..deda24cea0 100644 --- a/src/Umbraco.Core/Models/IContentBase.cs +++ b/src/Umbraco.Core/Models/IContentBase.cs @@ -29,45 +29,64 @@ namespace Umbraco.Core.Models int VersionId { get; } /// - /// Sets the name of the content item for a specified language. + /// Sets the name of the content item for a specified culture. /// /// - /// When is null, sets the invariant - /// language, which sets the property. + /// When is null, sets the invariant + /// culture name, which sets the property. + /// When is not null, throws if the content + /// type does not vary by culture. /// - void SetName(string value, string culture); + void SetCultureName(string value, string culture); /// /// Gets the name of the content item for a specified language. /// /// - /// When is null, gets the invariant - /// language, which is the value of the property. + /// When is null, gets the invariant + /// culture name, which is the value of the property. + /// When is not null, and the content type + /// does not vary by culture, returns null. /// - string GetName(string culture); + string GetCultureName(string culture); /// /// Gets the names of the content item. /// /// - /// Because a dictionary key cannot be null this cannot get the invariant - /// name, which must be get or set via the property. + /// Because a dictionary key cannot be null this cannot contain the invariant + /// culture name, which must be get or set via the property. /// IReadOnlyDictionary CultureNames { get; } + /// + /// Gets the available cultures. + /// + /// + /// Cannot contain the invariant culture, which is always available. + /// + IEnumerable AvailableCultures { get; } + /// /// Gets a value indicating whether a given culture is available. /// /// /// A culture becomes available whenever the content name for this culture is /// non-null, and it becomes unavailable whenever the content name is null. + /// Returns false for the invariant culture, in order to be consistent + /// with , even though the invariant culture is + /// always available. /// bool IsCultureAvailable(string culture); /// /// Gets the date a culture was created. /// - DateTime GetCultureDate(string culture); + /// + /// When is null, returns null. + /// If the specified culture is not available, returns null. + /// + DateTime? GetCultureDate(string culture); /// /// List of properties, which make up all the data available for this Content object diff --git a/src/Umbraco.Core/Models/IContentTypeBase.cs b/src/Umbraco.Core/Models/IContentTypeBase.cs index df171d5efc..ef5988344e 100644 --- a/src/Umbraco.Core/Models/IContentTypeBase.cs +++ b/src/Umbraco.Core/Models/IContentTypeBase.cs @@ -49,9 +49,31 @@ namespace Umbraco.Core.Models ContentVariation Variations { get; set; } /// - /// Validates that a variation is valid for the content type. + /// Validates that a combination of culture and segment is valid for the content type. /// - bool ValidateVariation(string culture, string segment, bool throwIfInvalid); + /// The culture. + /// The segment. + /// A value indicating whether wilcards are supported. + /// True if the combination is valid; otherwise false. + /// + /// The combination must match the content type variation exactly. For instance, if the content type varies by culture, + /// then an invariant culture would be invalid. + /// + bool SupportsVariation(string culture, string segment, bool wildcards = false); + + /// + /// Validates that a combination of culture and segment is valid for the content type properties. + /// + /// The culture. + /// The segment. + /// A value indicating whether wilcards are supported. + /// True if the combination is valid; otherwise false. + /// + /// The combination must be valid for properties of the content type. For instance, if the content type varies by culture, + /// then an invariant culture is valid, because some properties may be invariant. On the other hand, if the content type is invariant, + /// then a variant culture is invalid, because no property could possibly vary by culture. + /// + bool SupportsPropertyVariation(string culture, string segment, bool wildcards = false); /// /// Gets or Sets a list of integer Ids of the ContentTypes allowed under the ContentType diff --git a/src/Umbraco.Core/Models/Property.cs b/src/Umbraco.Core/Models/Property.cs index ac6a9b09f0..e1f05a4982 100644 --- a/src/Umbraco.Core/Models/Property.cs +++ b/src/Umbraco.Core/Models/Property.cs @@ -16,44 +16,83 @@ namespace Umbraco.Core.Models [DataContract(IsReference = true)] public class Property : EntityBase { + // _values contains all property values, including the invariant-neutral value private List _values = new List(); + + // _pvalue contains the invariant-neutral property value private PropertyValue _pvalue; + + // _vvalues contains the (indexed) variant property values private Dictionary _vvalues; private static readonly Lazy Ps = new Lazy(); + /// + /// Initializes a new instance of the class. + /// protected Property() { } + /// + /// Initializes a new instance of the class. + /// public Property(PropertyType propertyType) { PropertyType = propertyType; } + /// + /// Initializes a new instance of the class. + /// public Property(int id, PropertyType propertyType) { Id = id; PropertyType = propertyType; } + /// + /// Represents a property value. + /// public class PropertyValue { private string _culture; private string _segment; + /// + /// Gets or sets the culture of the property. + /// + /// The culture is either null (invariant) or a non-empty string. If the property is + /// set with an empty or whitespace value, its value is converted to null. public string Culture { get => _culture; - internal set => _culture = value?.ToLowerInvariant(); + internal set => _culture = value.IsNullOrWhiteSpace() ? null : value.ToLowerInvariant(); } + + /// + /// Gets or sets the segment of the property. + /// + /// The segment is either null (neutral) or a non-empty string. If the property is + /// set with an empty or whitespace value, its value is converted to null. public string Segment { get => _segment; internal set => _segment = value?.ToLowerInvariant(); } + + /// + /// Gets or sets the edited value of the property. + /// public object EditedValue { get; internal set; } + + /// + /// Gets or sets the published value of the property. + /// public object PublishedValue { get; internal set; } + /// + /// Clones the property value. + /// public PropertyValue Clone() => new PropertyValue { _culture = _culture, _segment = _segment, PublishedValue = PublishedValue, EditedValue = EditedValue }; } @@ -101,7 +140,7 @@ namespace Umbraco.Core.Models { // make sure we filter out invalid variations // make sure we leave _vvalues null if possible - _values = value.Where(x => PropertyType.ValidateVariation(x.Culture, x.Segment, false)).ToList(); + _values = value.Where(x => PropertyType.SupportsVariation(x.Culture, x.Segment)).ToList(); _pvalue = _values.FirstOrDefault(x => x.Culture == null && x.Segment == null); _vvalues = _values.Count > (_pvalue == null ? 0 : 1) ? _values.Where(x => x != _pvalue).ToDictionary(x => new CompositeNStringNStringKey(x.Culture, x.Segment), x => x) @@ -135,7 +174,11 @@ namespace Umbraco.Core.Models /// public object GetValue(string culture = null, string segment = null, bool published = false) { - if (!PropertyType.ValidateVariation(culture, segment, false)) return null; + // ensure null or whitespace are nulls + culture = culture.NullOrWhiteSpaceAsNull(); + segment = segment.NullOrWhiteSpaceAsNull(); + + if (!PropertyType.SupportsVariation(culture, segment)) return null; if (culture == null && segment == null) return GetPropertyValue(_pvalue, published); if (_vvalues == null) return null; return _vvalues.TryGetValue(new CompositeNStringNStringKey(culture, segment), out var pvalue) @@ -152,6 +195,8 @@ namespace Umbraco.Core.Models : pvalue.EditedValue; } + // fixme clear + /* // internal - must be invoked by the content item // does *not* validate the value - content item must validate first internal void PublishAllValues() @@ -161,14 +206,13 @@ namespace Umbraco.Core.Models PublishPropertyValue(_pvalue); // publish everything not invariant-neutral that is supported - if (_vvalues != null) - { - var pvalues = _vvalues - .Where(x => PropertyType.ValidateVariation(x.Value.Culture, x.Value.Segment, false)) - .Select(x => x.Value); - foreach (var pvalue in pvalues) - PublishPropertyValue(pvalue); - } + if (_vvalues == null) return; + + var pvalues = _vvalues + .Where(x => PropertyType.ValidateVariation(x.Value.Culture, x.Value.Segment, false)) + .Select(x => x.Value); + foreach (var pvalue in pvalues) + PublishPropertyValue(pvalue); } // internal - must be invoked by the content item @@ -201,7 +245,61 @@ namespace Umbraco.Core.Models PublishPropertyValue(pvalue); } } + */ + // internal - must be invoked by the content item + // does *not* validate the value - content item must validate first + internal void PublishValues(string culture = null, string segment = null) + { + culture = culture.NullOrWhiteSpaceAsNull(); + segment = segment.NullOrWhiteSpaceAsNull(); + + // if invariant or all, and invariant-neutral is supported, publish invariant-neutral + if ((culture == null || culture == "*") && (segment == null || segment == "*") && PropertyType.SupportsVariation(null, null)) + PublishPropertyValue(_pvalue); + + // then deal with everything that varies + if (_vvalues == null) return; + + // get the property values that are still relevant (wrt the property type variation), + // and match the specified culture and segment (or anything when '*'). + var pvalues = _vvalues.Where(x => + PropertyType.SupportsVariation(x.Value.Culture, x.Value.Segment) && // the value variation is ok + (culture == "*" || x.Value.Culture.InvariantEquals(culture)) && // the culture matches + (segment == "*" || x.Value.Segment.InvariantEquals(segment))) // the segment matches + .Select(x => x.Value); + + foreach (var pvalue in pvalues) + PublishPropertyValue(pvalue); + } + + // internal - must be invoked by the content item + internal void ClearPublishedValues(string culture = null, string segment = null) + { + culture = culture.NullOrWhiteSpaceAsNull(); + segment = segment.NullOrWhiteSpaceAsNull(); + + // if invariant or all, and invariant-neutral is supported, publish invariant-neutral + if ((culture == null || culture == "*") && (segment == null || segment == "*") && PropertyType.SupportsVariation(null, null)) + ClearPublishedPropertyValue(_pvalue); + + // then deal with everything that varies + if (_vvalues == null) return; + + // get the property values that are still relevant (wrt the property type variation), + // and match the specified culture and segment (or anything when '*'). + var pvalues = _vvalues.Where(x => + PropertyType.SupportsVariation(x.Value.Culture, x.Value.Segment) && // the value variation is ok + (culture == "*" || x.Value.Culture.InvariantEquals(culture)) && // the culture matches + (segment == "*" || x.Value.Segment.InvariantEquals(segment))) // the segment matches + .Select(x => x.Value); + + foreach (var pvalue in pvalues) + ClearPublishedPropertyValue(pvalue); + } + + // fixme clear + /* // internal - must be invoked by the content item internal void ClearPublishedAllValues() { @@ -228,7 +326,7 @@ namespace Umbraco.Core.Models } // internal - must be invoked by the content item - internal void ClearPublishedCultureValues(string culture = null) + internal void ClearCulturePublishedValues(string culture = null) { if (culture == null && PropertyType.ValidateVariation(null, null, false)) ClearPublishedPropertyValue(_pvalue); @@ -243,6 +341,7 @@ namespace Umbraco.Core.Models ClearPublishedPropertyValue(pvalue); } } + */ private void PublishPropertyValue(PropertyValue pvalue) { @@ -271,7 +370,12 @@ namespace Umbraco.Core.Models /// public void SetValue(object value, string culture = null, string segment = null) { - PropertyType.ValidateVariation(culture, segment, true); + culture = culture.NullOrWhiteSpaceAsNull(); + segment = segment.NullOrWhiteSpaceAsNull(); + + if (!PropertyType.SupportsVariation(culture, segment)) + throw new NotSupportedException($"Variation \"{culture??""},{segment??""}\" is not supported by the property type."); + var (pvalue, change) = GetPValue(culture, segment, true); var origValue = pvalue.EditedValue; @@ -335,65 +439,69 @@ namespace Umbraco.Core.Models /// Gets a value indicating whether all properties are valid. /// /// - internal bool IsAllValid() + internal bool IsValid(string culture = null, string segment = null) { - //fixme - needs API review as this is not used apart from in tests + // if validating invariant/neutral, and it is supported, validate + // (including ensuring that the value exists, if mandatory) + if ((culture == null || culture == "*") && (segment == null || segment == "*") && PropertyType.SupportsVariation(null, null)) + if (!IsValidValue(_pvalue?.EditedValue)) + return false; - // invariant-neutral is supported, validate invariant-neutral - // includes mandatory validation - if (PropertyType.ValidateVariation(null, null, false) && !IsValidValue(_pvalue)) return false; + // if validating only invariant/neutral, we are good + if (culture == null && segment == null) return true; - // either invariant-neutral is not supported, or it is valid // for anything else, validate the existing values (including mandatory), // but we cannot validate mandatory globally (we don't know the possible cultures and segments) - if (_vvalues == null) return true; + if (_vvalues == null) return culture == "*" || segment == "*" || IsValidValue(null); - var pvalues = _vvalues - .Where(x => PropertyType.ValidateVariation(x.Value.Culture, x.Value.Segment, false)) + var pvalues = _vvalues.Where(x => + PropertyType.SupportsVariation(x.Value.Culture, x.Value.Segment) && // the value variation is ok + (culture == "*" || x.Value.Culture.InvariantEquals(culture)) && // the culture matches + (segment == "*" || x.Value.Segment.InvariantEquals(segment))) // the segment matches .Select(x => x.Value) - .ToArray(); + .ToList(); - return pvalues.Length == 0 || pvalues.All(x => IsValidValue(x.EditedValue)); + return pvalues.Count == 0 || pvalues.All(x => IsValidValue(x.EditedValue)); } - /// - /// Gets a value indicating whether the culture/any values are valid. - /// - /// An invalid value can be saved, but only valid values can be published. - internal bool IsCultureValid(string culture) - { - //fixme - needs API review as this is not used apart from in tests + // fixme clear + ///// + ///// Gets a value indicating whether the culture/any values are valid. + ///// + ///// An invalid value can be saved, but only valid values can be published. + //internal bool IsCultureValid(string culture) + //{ - // culture-neutral is supported, validate culture-neutral - // includes mandatory validation - if (PropertyType.ValidateVariation(culture, null, false) && !IsValidValue(GetValue(culture))) - return false; + // // culture-neutral is supported, validate culture-neutral + // // includes mandatory validation + // if (PropertyType.ValidateVariation(culture, null, false) && !IsValidValue(GetValue(culture))) + // return false; - // either culture-neutral is not supported, or it is valid - // for anything non-neutral, validate the existing values (including mandatory), - // but we cannot validate mandatory globally (we don't know the possible segments) + // // either culture-neutral is not supported, or it is valid + // // for anything non-neutral, validate the existing values (including mandatory), + // // but we cannot validate mandatory globally (we don't know the possible segments) - if (_vvalues == null) return true; + // if (_vvalues == null) return true; - var pvalues = _vvalues - .Where(x => x.Value.Culture.InvariantEquals(culture)) - .Where(x => PropertyType.ValidateVariation(culture, x.Value.Segment, false)) - .Select(x => x.Value) - .ToArray(); + // var pvalues = _vvalues + // .Where(x => x.Value.Culture.InvariantEquals(culture)) + // .Where(x => PropertyType.ValidateVariation(culture, x.Value.Segment, false)) + // .Select(x => x.Value) + // .ToArray(); - return pvalues.Length == 0 || pvalues.All(x => IsValidValue(x.EditedValue)); - } + // return pvalues.Length == 0 || pvalues.All(x => IsValidValue(x.EditedValue)); + //} - /// - /// Gets a value indicating whether the value is valid. - /// - /// An invalid value can be saved, but only valid values can be published. - public bool IsValid(string culture = null, string segment = null) - { - // single value -> validates mandatory - return IsValidValue(GetValue(culture, segment)); - } + ///// + ///// Gets a value indicating whether the value is valid. + ///// + ///// An invalid value can be saved, but only valid values can be published. + //public bool IsValid(string culture = null, string segment = null) + //{ + // // single value -> validates mandatory + // return IsValidValue(GetValue(culture, segment)); + //} /// /// Boolean indicating whether the passed in value is valid diff --git a/src/Umbraco.Core/Models/PropertyType.cs b/src/Umbraco.Core/Models/PropertyType.cs index 587af74aa2..3d5fac2077 100644 --- a/src/Umbraco.Core/Models/PropertyType.cs +++ b/src/Umbraco.Core/Models/PropertyType.cs @@ -47,7 +47,7 @@ namespace Umbraco.Core.Models _propertyEditorAlias = dataType.EditorAlias; _valueStorageType = dataType.DatabaseType; - _variations = ContentVariation.InvariantNeutral; + _variations = ContentVariation.Nothing; } /// @@ -84,7 +84,7 @@ namespace Umbraco.Core.Models _valueStorageType = valueStorageType; _forceValueStorageType = forceValueStorageType; _alias = propertyTypeAlias == null ? null : SanitizeAlias(propertyTypeAlias); - _variations = ContentVariation.InvariantNeutral; + _variations = ContentVariation.Nothing; } private static PropertySelectors Selectors => _selectors ?? (_selectors = new PropertySelectors()); @@ -224,32 +224,17 @@ namespace Umbraco.Core.Models } /// - /// Validates that a variation is valid for the property type. + /// Determines whether the property type supports a combination of culture and segment. /// - public bool ValidateVariation(string culture, string segment, bool throwIfInvalid) + /// The culture. + /// The segment. + /// A value indicating whether wildcards are valid. + public bool SupportsVariation(string culture, string segment, bool wildcards = false) { - ContentVariation variation; - if (culture != null) - { - variation = segment != null - ? ContentVariation.CultureSegment - : ContentVariation.CultureNeutral; - } - else if (segment != null) - { - variation = ContentVariation.InvariantSegment; - } - else - { - variation = ContentVariation.InvariantNeutral; - } - if (!Variations.Has(variation)) - { - if (throwIfInvalid) - throw new NotSupportedException($"Variation {variation} is invalid for property type \"{Alias}\"."); - return false; - } - return true; + // exact validation: cannot accept a 'null' culture if the property type varies + // by culture, and likewise for segment + // wildcard validation: can accept a '*' culture or segment + return Variations.ValidateVariation(culture, segment, true, wildcards, false); } /// diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs index bc403b904d..e611ded6c8 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs @@ -81,7 +81,7 @@ namespace Umbraco.Core.Models.PublishedContent foreach ((var alias, (var dataTypeId, var editorAlias)) in BuiltinMemberProperties) { if (aliases.Contains(alias)) continue; - propertyTypes.Add(factory.CreatePropertyType(this, alias, dataTypeId, ContentVariation.InvariantNeutral)); + propertyTypes.Add(factory.CreatePropertyType(this, alias, dataTypeId, ContentVariation.Nothing)); } } diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeFactory.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeFactory.cs index 3ba908b9bf..5de5842eda 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeFactory.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeFactory.cs @@ -32,13 +32,13 @@ namespace Umbraco.Core.Models.PublishedContent } // for tests - internal PublishedContentType CreateContentType(int id, string alias, IEnumerable propertyTypes, ContentVariation variations = ContentVariation.InvariantNeutral) + internal PublishedContentType CreateContentType(int id, string alias, IEnumerable propertyTypes, ContentVariation variations = ContentVariation.Nothing) { return new PublishedContentType(id, alias, PublishedItemType.Content, Enumerable.Empty(), propertyTypes, variations); } // for tests - internal PublishedContentType CreateContentType(int id, string alias, IEnumerable compositionAliases, IEnumerable propertyTypes, ContentVariation variations = ContentVariation.InvariantNeutral) + internal PublishedContentType CreateContentType(int id, string alias, IEnumerable compositionAliases, IEnumerable propertyTypes, ContentVariation variations = ContentVariation.Nothing) { return new PublishedContentType(id, alias, PublishedItemType.Content, compositionAliases, propertyTypes, variations); } @@ -50,13 +50,13 @@ namespace Umbraco.Core.Models.PublishedContent } /// - public PublishedPropertyType CreatePropertyType(PublishedContentType contentType, string propertyTypeAlias, int dataTypeId, ContentVariation variations = ContentVariation.InvariantNeutral) + public PublishedPropertyType CreatePropertyType(PublishedContentType contentType, string propertyTypeAlias, int dataTypeId, ContentVariation variations = ContentVariation.Nothing) { return new PublishedPropertyType(contentType, propertyTypeAlias, dataTypeId, true, variations, _propertyValueConverters, _publishedModelFactory, this); } // for tests - internal PublishedPropertyType CreatePropertyType(string propertyTypeAlias, int dataTypeId, bool umbraco = false, ContentVariation variations = ContentVariation.InvariantNeutral) + internal PublishedPropertyType CreatePropertyType(string propertyTypeAlias, int dataTypeId, bool umbraco = false, ContentVariation variations = ContentVariation.Nothing) { return new PublishedPropertyType(propertyTypeAlias, dataTypeId, umbraco, variations, _propertyValueConverters, _publishedModelFactory, this); } diff --git a/src/Umbraco.Core/Models/PublishedContent/RawValueProperty.cs b/src/Umbraco.Core/Models/PublishedContent/RawValueProperty.cs index 5dc4a280e6..7469222ab0 100644 --- a/src/Umbraco.Core/Models/PublishedContent/RawValueProperty.cs +++ b/src/Umbraco.Core/Models/PublishedContent/RawValueProperty.cs @@ -41,7 +41,7 @@ namespace Umbraco.Core.Models.PublishedContent public RawValueProperty(PublishedPropertyType propertyType, IPublishedElement content, object sourceValue, bool isPreviewing = false) : base(propertyType, PropertyCacheLevel.Unknown) // cache level is ignored { - if (propertyType.Variations != ContentVariation.InvariantNeutral) + if (propertyType.Variations != ContentVariation.Nothing) throw new ArgumentException("Property types with variations are not supported here.", nameof(propertyType)); _sourceValue = sourceValue; diff --git a/src/Umbraco.Core/Persistence/Factories/ContentBaseFactory.cs b/src/Umbraco.Core/Persistence/Factories/ContentBaseFactory.cs index c3b4b0d24c..ec364c7c6a 100644 --- a/src/Umbraco.Core/Persistence/Factories/ContentBaseFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/ContentBaseFactory.cs @@ -159,8 +159,7 @@ namespace Umbraco.Core.Persistence.Factories /// public static DocumentDto BuildDto(IContent entity, Guid objectType) { - var contentBase = (Content) entity; - var contentDto = BuildContentDto(contentBase, objectType); + var contentDto = BuildContentDto(entity, objectType); var dto = new DocumentDto { @@ -170,7 +169,7 @@ namespace Umbraco.Core.Persistence.Factories ExpiresDate = entity.ExpireDate, ContentDto = contentDto, - DocumentVersionDto = BuildDocumentVersionDto(contentBase, contentDto) + DocumentVersionDto = BuildDocumentVersionDto(entity, contentDto) }; return dto; @@ -181,14 +180,13 @@ namespace Umbraco.Core.Persistence.Factories /// public static MediaDto BuildDto(IMedia entity) { - var contentBase = (Models.Media) entity; - var contentDto = BuildContentDto(contentBase, Constants.ObjectTypes.Media); + var contentDto = BuildContentDto(entity, Constants.ObjectTypes.Media); var dto = new MediaDto { NodeId = entity.Id, ContentDto = contentDto, - MediaVersionDto = BuildMediaVersionDto(contentBase, contentDto) + MediaVersionDto = BuildMediaVersionDto(entity, contentDto) }; return dto; @@ -199,8 +197,7 @@ namespace Umbraco.Core.Persistence.Factories /// public static MemberDto BuildDto(IMember entity) { - var member = (Member) entity; - var contentDto = BuildContentDto(member, Constants.ObjectTypes.Member); + var contentDto = BuildContentDto(entity, Constants.ObjectTypes.Member); var dto = new MemberDto { @@ -210,12 +207,12 @@ namespace Umbraco.Core.Persistence.Factories Password = entity.RawPasswordValue, ContentDto = contentDto, - ContentVersionDto = BuildContentVersionDto(member, contentDto) + ContentVersionDto = BuildContentVersionDto(entity, contentDto) }; return dto; } - private static ContentDto BuildContentDto(ContentBase entity, Guid objectType) + private static ContentDto BuildContentDto(IContentBase entity, Guid objectType) { var dto = new ContentDto { @@ -228,7 +225,7 @@ namespace Umbraco.Core.Persistence.Factories return dto; } - private static NodeDto BuildNodeDto(ContentBase entity, Guid objectType) + private static NodeDto BuildNodeDto(IContentBase entity, Guid objectType) { var dto = new NodeDto { @@ -250,7 +247,7 @@ namespace Umbraco.Core.Persistence.Factories // always build the current / VersionPk dto // we're never going to build / save old versions (which are immutable) - private static ContentVersionDto BuildContentVersionDto(ContentBase entity, ContentDto contentDto) + private static ContentVersionDto BuildContentVersionDto(IContentBase entity, ContentDto contentDto) { var dto = new ContentVersionDto { @@ -269,7 +266,7 @@ namespace Umbraco.Core.Persistence.Factories // always build the current / VersionPk dto // we're never going to build / save old versions (which are immutable) - private static DocumentVersionDto BuildDocumentVersionDto(Content entity, ContentDto contentDto) + private static DocumentVersionDto BuildDocumentVersionDto(IContent entity, ContentDto contentDto) { var dto = new DocumentVersionDto { @@ -283,7 +280,7 @@ namespace Umbraco.Core.Persistence.Factories return dto; } - private static MediaVersionDto BuildMediaVersionDto(Models.Media entity, ContentDto contentDto) + private static MediaVersionDto BuildMediaVersionDto(IMedia entity, ContentDto contentDto) { // try to get a path from the string being stored for media // fixme - only considering umbracoFile ?! diff --git a/src/Umbraco.Core/Persistence/Factories/PropertyFactory.cs b/src/Umbraco.Core/Persistence/Factories/PropertyFactory.cs index b0e3e2dc7d..3bea84e619 100644 --- a/src/Umbraco.Core/Persistence/Factories/PropertyFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/PropertyFactory.cs @@ -99,13 +99,7 @@ namespace Umbraco.Core.Persistence.Factories { if (property.PropertyType.IsPublishing) { - // fixme - // why only CultureNeutral? - // then, the tree can only show when a CultureNeutral value has been modified, but not when - // a CultureSegment has been modified, so if I edit some french/mobile thing, the tree will - // NOT tell me that I have changes? - - var editingCultures = property.PropertyType.Variations.Has(ContentVariation.CultureNeutral); + var editingCultures = property.PropertyType.VariesByCulture(); if (editingCultures && editedCultures == null) editedCultures = new HashSet(StringComparer.OrdinalIgnoreCase); // publishing = deal with edit and published values diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs index f2a91c11a1..202f51729a 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs @@ -232,7 +232,10 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected override void PersistNewItem(IContent entity) { - //fixme - stop doing this just so we have access to AddingEntity + // fixme - stop doing this - sort out IContent vs Content + // however, it's not just so we have access to AddingEntity + // there are tons of things at the end of the methods, that can only work with a true Content + // and basically, the repository requires a Content, not an IContent var content = (Content) entity; content.AddingEntity(); @@ -242,11 +245,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (entity.Template == null) entity.Template = entity.ContentType.DefaultTemplate; - // sanitize names: ensure we have an invariant name, and names are unique-ish - // (well, only invariant name is unique at the moment) - EnsureUniqueVariationNames(entity); - EnsureInvariantNameValues(entity, publishing); - entity.Name = EnsureUniqueNodeName(entity.ParentId, entity.Name); + // sanitize names + SanitizeNames(content, publishing); // ensure that strings don't contain characters that are invalid in xml // fixme - do we really want to keep doing this here? @@ -295,7 +295,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var contentVersionDto = dto.DocumentVersionDto.ContentVersionDto; contentVersionDto.NodeId = nodeDto.NodeId; contentVersionDto.Current = !publishing; - contentVersionDto.Text = publishing ? content.PublishName : content.Name; + contentVersionDto.Text = content.Name; Database.Insert(contentVersionDto); content.VersionId = contentVersionDto.Id; @@ -312,7 +312,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement content.PublishedVersionId = content.VersionId; contentVersionDto.Id = 0; contentVersionDto.Current = true; - contentVersionDto.Text = content.PublishName; + contentVersionDto.Text = content.Name; Database.Insert(contentVersionDto); content.VersionId = contentVersionDto.Id; @@ -326,7 +326,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement foreach (var propertyDataDto in propertyDataDtos) Database.Insert(propertyDataDto); - // name also impacts 'edited' + // if !publishing, we may have a new name != current publish name, + // also impacts 'edited' if (content.PublishName != content.Name) edited = true; @@ -340,7 +341,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement Database.Insert(dto); // persist the variations - if (content.ContentType.Variations.DoesSupportCulture()) + if (content.ContentType.VariesByCulture()) { // names also impact 'edited' foreach (var (culture, name) in content.CultureNames) @@ -355,8 +356,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement } // refresh content - if (editedCultures != null) - content.SetCultureEdited(editedCultures); + content.SetCultureEdited(editedCultures); // trigger here, before we reset Published etc OnUowRefreshedEntity(new ScopedEntityEventArgs(AmbientScope, entity)); @@ -369,6 +369,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement content.Published = true; content.PublishTemplate = content.Template; content.PublisherId = content.WriterId; + content.PublishName = content.Name; content.PublishDate = content.UpdateDate; SetEntityTags(entity, _tagRepository); @@ -378,6 +379,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement content.Published = false; content.PublishTemplate = null; content.PublisherId = null; + content.PublishName = null; content.PublishDate = null; ClearEntityTags(entity, _tagRepository); @@ -400,6 +402,10 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected override void PersistUpdatedItem(IContent entity) { + // fixme - stop doing this - sort out IContent vs Content + // however, it's not just so we have access to AddingEntity + // there are tons of things at the end of the methods, that can only work with a true Content + // and basically, the repository requires a Content, not an IContent var content = (Content) entity; // check if we need to make any database changes at all @@ -423,11 +429,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement Database.Execute(Sql().Update(u => u.Set(x => x.Published, false)).Where(x => x.Id == content.PublishedVersionId)); } - // sanitize names: ensure we have an invariant name, and names are unique-ish - // (well, only invariant name is unique at the moment) - EnsureUniqueVariationNames(entity); - EnsureInvariantNameValues(entity, publishing); - entity.Name = EnsureUniqueNodeName(entity.ParentId, entity.Name, entity.Id); + // sanitize names + SanitizeNames(content, publishing); // ensure that strings don't contain characters that are invalid in xml // fixme - do we really want to keep doing this here? @@ -460,7 +463,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement { documentVersionDto.Published = true; // now published contentVersionDto.Current = false; // no more current - contentVersionDto.Text = content.PublishName; + contentVersionDto.Text = content.Name; } Database.Update(contentVersionDto); Database.Update(documentVersionDto); @@ -491,11 +494,12 @@ namespace Umbraco.Core.Persistence.Repositories.Implement foreach (var propertyDataDto in propertyDataDtos) Database.Insert(propertyDataDto); - // name also impacts 'edited' - if (content.PublishName != content.Name) + // if !publishing, we may have a new name != current publish name, + // also impacts 'edited' + if (!publishing && content.PublishName != content.Name) edited = true; - if (content.ContentType.Variations.DoesSupportCulture()) + if (content.ContentType.VariesByCulture()) { // names also impact 'edited' foreach (var (culture, name) in content.CultureNames) @@ -511,7 +515,9 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var deleteDocumentVariations = Sql().Delete().Where(x => x.NodeId == content.Id); Database.Execute(deleteDocumentVariations); - // fixme is we'd like to use the native NPoco InsertBulk here but it causes problems (not sure exaclty all scenarios) but by using SQL Server and updating a variants name will cause: Unable to cast object of type 'Umbraco.Core.Persistence.FaultHandling.RetryDbConnection' to type 'System.Data.SqlClient.SqlConnection'. + // fixme is we'd like to use the native NPoco InsertBulk here but it causes problems (not sure exaclty all scenarios) + // but by using SQL Server and updating a variants name will cause: Unable to cast object of type + // 'Umbraco.Core.Persistence.FaultHandling.RetryDbConnection' to type 'System.Data.SqlClient.SqlConnection'. // (same in PersistNewItem above) // insert content variations @@ -522,8 +528,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement } // refresh content - if (editedCultures != null) - content.SetCultureEdited(editedCultures); + content.SetCultureEdited(editedCultures); // update the document dto // at that point, when un/publishing, the entity still has its old Published value @@ -550,6 +555,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement content.Published = true; content.PublishTemplate = content.Template; content.PublisherId = content.WriterId; + content.PublishName = content.Name; content.PublishDate = content.UpdateDate; SetEntityTags(entity, _tagRepository); @@ -559,6 +565,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement content.Published = false; content.PublishTemplate = null; content.PublisherId = null; + content.PublishName = null; content.PublishDate = null; ClearEntityTags(entity, _tagRepository); @@ -904,7 +911,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement } // set variations, if varying - temps = temps.Where(x => x.ContentType.Variations.DoesSupportCulture()).ToList(); + temps = temps.Where(x => x.ContentType.VariesByCulture()).ToList(); if (temps.Count > 0) { // load all variations for all documents from database, in one query @@ -939,7 +946,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement content.Properties = properties[dto.DocumentVersionDto.Id]; // set variations, if varying - if (contentType.Variations.DoesSupportCulture()) + if (contentType.VariesByCulture()) { var contentVariations = GetContentVariations(ltemp); var documentVariations = GetDocumentVariations(ltemp); @@ -955,10 +962,10 @@ namespace Umbraco.Core.Persistence.Repositories.Implement { if (contentVariations.TryGetValue(content.VersionId, out var contentVariation)) foreach (var v in contentVariation) - content.SetCultureInfos(v.Culture, v.Name, v.Date); + content.SetCultureInfo(v.Culture, v.Name, v.Date); if (content.PublishedVersionId > 0 && contentVariations.TryGetValue(content.PublishedVersionId, out contentVariation)) foreach (var v in contentVariation) - content.SetPublishInfos(v.Culture, v.Name, v.Date); + content.SetPublishInfo(v.Culture, v.Name, v.Date); if (documentVariations.TryGetValue(content.Id, out var documentVariation)) foreach (var v in documentVariation.Where(x => x.Edited)) content.SetCultureEdited(v.Culture); @@ -1038,7 +1045,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement LanguageId = LanguageRepository.GetIdByIsoCode(culture) ?? throw new InvalidOperationException("Not a valid culture."), Culture = culture, Name = name, - Date = content.GetCultureDate(culture) + Date = content.GetCultureDate(culture) ?? DateTime.MinValue // we *know* there is a value }; // if not publishing, we're just updating the 'current' (non-published) version, @@ -1046,14 +1053,14 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (!publishing) yield break; // create dtos for the 'published' version, for published cultures (those having a name) - foreach (var (culture, name) in content.PublishCultureNames) + foreach (var (culture, name) in content.PublishNames) yield return new ContentVersionCultureVariationDto { VersionId = content.PublishedVersionId, LanguageId = LanguageRepository.GetIdByIsoCode(culture) ?? throw new InvalidOperationException("Not a valid culture."), Culture = culture, Name = name, - Date = content.GetCulturePublishDate(culture) + Date = content.GetPublishDate(culture) ?? DateTime.MinValue // we *know* there is a value }; } @@ -1084,90 +1091,124 @@ namespace Umbraco.Core.Persistence.Repositories.Implement #region Utilities - /// - /// Ensures that the Name/PublishName properties are filled in and validates if all names are null - /// - private void EnsureInvariantNameValues(IContent content, bool publishing) + private void SanitizeNames(Content content, bool publishing) { - // here we have to ensure we have names and publish names, and to try and fix the situation if we have no name, see also: U4-11286 + // a content item *must* have an invariant name, and invariant published name + // else we just cannot write the invariant rows (node, content version...) to the database - // invariant content must have an invariant name - if (content.ContentType.Variations.DoesNotSupportCulture() && string.IsNullOrWhiteSpace(content.Name)) - throw new InvalidOperationException("Cannot save content with an empty name."); + // ensure that we have an invariant name + // invariant content = must be there already, else throw + // variant content = update with default culture or anything really + EnsureInvariantNameExists(content); - // variant content must have at least one variant name - if (content.ContentType.Variations.DoesSupportCulture()) + // ensure that that invariant name is unique + EnsureInvariantNameIsUnique(content); + + // now that we have an invariant name, which is unique, + // if publishing, ensure that we have an invariant publish name + // invariant content = must be there, else throw - then must follow the invariant name + // variant content = update with invariant name + // fixme wtf is this we never needed it, PublishName derives from Name when publishing! + //if (publishing) EnsureInvariantPublishName(content); + + // and finally, + // ensure that each culture has a unique node name + // no published name = not published + // else, it needs to be unique + EnsureVariantNamesAreUnique(content, publishing); + } + + private void EnsureInvariantNameExists(Content content) + { + if (content.ContentType.VariesByCulture()) { + // content varies by culture + // then it must have at least a variant name, else it makes no sense if (content.CultureNames.Count == 0) throw new InvalidOperationException("Cannot save content with an empty name."); - // cannot save with an empty invariant name, - // if invariant name is missing, derive it from variant names - // fixme should we always sync the invariant name with the default culture name when updating? - if (string.IsNullOrWhiteSpace(content.Name)) - { - var defaultCulture = LanguageRepository.GetDefaultIsoCode(); - if (defaultCulture != null && content.CultureNames.TryGetValue(defaultCulture, out var cultureName)) - content.Name = cultureName; - else - content.Name = content.CultureNames.First().Value; // only option is to take the first - } + // and then, we need to set the invariant name implicitely, + // using the default culture if it has a name, otherwise anything we can + var defaultCulture = LanguageRepository.GetDefaultIsoCode(); + content.Name = defaultCulture != null && content.CultureNames.TryGetValue(defaultCulture, out var cultureName) + ? cultureName + : content.CultureNames.First().Value; } - - // cannot publish without an invariant name - if (publishing && string.IsNullOrWhiteSpace(content.PublishName)) + else { - // no variant name = error - if (content.PublishCultureNames.Count == 0) - throw new InvalidOperationException("Cannot publish content with an empty name."); - - // else... we cannot deal with it here because PublishName is readonly, so in reality, PublishName - // should not be null because it should have been set when preparing the content for publish. - // see also: Content.SetPublishInfos() - it deals with PublishName + // content is invariant, and invariant content must have an explicit invariant name + if (string.IsNullOrWhiteSpace(content.Name)) + throw new InvalidOperationException("Cannot save content with an empty name."); } } + private void EnsureInvariantNameIsUnique(Content content) + { + content.Name = EnsureUniqueNodeName(content.ParentId, content.Name, content.Id); + } + + //private void EnsureInvariantPublishName(Content content) + //{ + // if (content.ContentType.VariesByCulture()) + // { + // // content varies by culture, reuse name as publish name + // content.UpdatePublishName(null, content.Name); + // } + // else + // { + // // content is invariant, and invariant content must have an explicit invariant name + // if (string.IsNullOrWhiteSpace(content.PublishName)) + // throw new InvalidOperationException("Cannot save content with an empty name."); + // // and then, must follow the name itself + // if (content.PublishName != content.Name) + // content.UpdatePublishName(null, content.Name); + // } + //} + protected override string EnsureUniqueNodeName(int parentId, string nodeName, int id = 0) { return EnsureUniqueNaming == false ? nodeName : base.EnsureUniqueNodeName(parentId, nodeName, id); } - private SqlTemplate SqlEnsureUniqueVariationNames => SqlContext.Templates.Get("Umbraco.Core.DomainRepository.EnsureUniqueVariationNames", tsql => tsql + private SqlTemplate SqlEnsureVariantNamesAreUnique => SqlContext.Templates.Get("Umbraco.Core.DomainRepository.EnsureVariantNamesAreUnique", tsql => tsql .Select(x => x.Id, x => x.Name, x => x.LanguageId) .From() - .InnerJoin() - .On(x => x.Id, x => x.VersionId) - .InnerJoin() - .On(x => x.NodeId, x => x.NodeId) + .InnerJoin().On(x => x.Id, x => x.VersionId) + .InnerJoin().On(x => x.NodeId, x => x.NodeId) .Where(x => x.Current == SqlTemplate.Arg("current")) .Where(x => x.NodeObjectType == SqlTemplate.Arg("nodeObjectType") && x.ParentId == SqlTemplate.Arg("parentId") && x.NodeId != SqlTemplate.Arg("id")) .OrderBy(x => x.LanguageId)); - private void EnsureUniqueVariationNames(IContent content) + private void EnsureVariantNamesAreUnique(Content content, bool publishing) { - if (!EnsureUniqueNaming || content.CultureNames.Count == 0) return; + if (!EnsureUniqueNaming || !content.ContentType.VariesByCulture() || content.CultureNames.Count == 0) return; - //Get all culture names at the same level - - var sql = SqlEnsureUniqueVariationNames.Sql(true, NodeObjectTypeId, content.ParentId, content.Id); + // get names per culture, at same level (ie all siblings) + var sql = SqlEnsureVariantNamesAreUnique.Sql(true, NodeObjectTypeId, content.ParentId, content.Id); var names = Database.Fetch(sql) .GroupBy(x => x.LanguageId) .ToDictionary(x => x.Key, x => x); if (names.Count == 0) return; - foreach(var n in content.CultureNames) + foreach(var (culture, name) in content.CultureNames) { - var langId = LanguageRepository.GetIdByIsoCode(n.Key); + var langId = LanguageRepository.GetIdByIsoCode(culture); if (!langId.HasValue) continue; - if (names.TryGetValue(langId.Value, out var cultureNames)) - { - var otherLangNames = cultureNames.Select(x => new SimilarNodeName { Id = x.Id, Name = x.Name }); - var uniqueName = SimilarNodeName.GetUniqueName(otherLangNames, 0, n.Value); - content.SetName(uniqueName, n.Key); - } + if (!names.TryGetValue(langId.Value, out var cultureNames)) continue; + + // get a unique name + var otherNames = cultureNames.Select(x => new SimilarNodeName { Id = x.Id, Name = x.Name }); + var uniqueName = SimilarNodeName.GetUniqueName(otherNames, 0, name); + + if (uniqueName == content.GetCultureName(culture)) continue; + + // update the name, and the publish name if published + content.SetCultureName(uniqueName, culture); + if (publishing && content.PublishNames.ContainsKey(culture)) // fixme but what about those cultures we are NOT publishing NOW?! they shouldn't change their name! + content.SetPublishInfo(culture, uniqueName, DateTime.Now); } } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs index 75dbeca559..340eecb538 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs @@ -905,7 +905,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement entity.Edited = dto.Edited; entity.Published = dto.Published; - if (dto.Variations.Has(ContentVariation.CultureNeutral) && dto.VariationInfo != null && dto.VariationInfo.Count > 0) + if (dto.Variations.VariesByCulture() && dto.VariationInfo != null && dto.VariationInfo.Count > 0) { var variantInfo = new Dictionary(StringComparer.InvariantCultureIgnoreCase); foreach (var info in dto.VariationInfo) diff --git a/src/Umbraco.Core/Services/Implement/ContentService.cs b/src/Umbraco.Core/Services/Implement/ContentService.cs index 2e3fcf8522..4051a26f7f 100644 --- a/src/Umbraco.Core/Services/Implement/ContentService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentService.cs @@ -1056,7 +1056,7 @@ namespace Umbraco.Core.Services.Implement // not varying, or invariant culture // simply unpublish the document - if (!content.ContentType.Variations.DoesSupportCulture() || culture.IsNullOrWhiteSpace()) + if (!content.ContentType.VariesByCulture() || culture.IsNullOrWhiteSpace()) { var unpublished = UnpublishScoped(scope, content, evtMsgs, userId); if (unpublished.Success) scope.Complete(); diff --git a/src/Umbraco.Core/StringExtensions.cs b/src/Umbraco.Core/StringExtensions.cs index a2f5727ae4..0799c2c9a8 100644 --- a/src/Umbraco.Core/StringExtensions.cs +++ b/src/Umbraco.Core/StringExtensions.cs @@ -1504,7 +1504,7 @@ namespace Umbraco.Core /// /// Turns an null-or-whitespace string into a null string. /// - public static string NullEmpty(this string text) + public static string NullOrWhiteSpaceAsNull(this string text) => string.IsNullOrWhiteSpace(text) ? null : text; } } diff --git a/src/Umbraco.Core/Strings/DefaultUrlSegmentProvider.cs b/src/Umbraco.Core/Strings/DefaultUrlSegmentProvider.cs index 09feeff338..9472ff4823 100644 --- a/src/Umbraco.Core/Strings/DefaultUrlSegmentProvider.cs +++ b/src/Umbraco.Core/Strings/DefaultUrlSegmentProvider.cs @@ -24,7 +24,7 @@ namespace Umbraco.Core.Strings if (content.HasProperty(Constants.Conventions.Content.UrlName)) source = (content.GetValue(Constants.Conventions.Content.UrlName, culture) ?? string.Empty).Trim(); if (string.IsNullOrWhiteSpace(source)) - source = content.GetName(culture); + source = content.GetCultureName(culture); return source; } } diff --git a/src/Umbraco.Tests/Models/VariationTests.cs b/src/Umbraco.Tests/Models/VariationTests.cs index 044f8fa0cd..861dee1eca 100644 --- a/src/Umbraco.Tests/Models/VariationTests.cs +++ b/src/Umbraco.Tests/Models/VariationTests.cs @@ -2,12 +2,11 @@ using LightInject; using Moq; using NUnit.Framework; -using Umbraco.Core.Cache; +using Umbraco.Core; using Umbraco.Core.Composing; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.PropertyEditors; -using Umbraco.Core.PropertyEditors.Validators; using Umbraco.Core.Services; using Umbraco.Tests.TestHelpers; @@ -59,6 +58,55 @@ namespace Umbraco.Tests.Models }); } + [Test] + public void ValidateVariationTests() + { + void Assert4A(ContentVariation v, string c, string s, bool xx) + { + Assert4B(v, c, s, xx, xx, xx, xx); + } + + void Assert4B(ContentVariation v, string c, string s, bool ew, bool nn, bool en, bool nw) + { + Assert.AreEqual(ew, v.ValidateVariation(c, s, true, true, false)); + Assert.AreEqual(nn, v.ValidateVariation(c, s, false, false, false)); + Assert.AreEqual(en, v.ValidateVariation(c, s, true, false, false)); + Assert.AreEqual(nw, v.ValidateVariation(c, s, false, true, false)); + } + + // always support invariant,neutral + Assert4A(ContentVariation.Nothing, null, null, true); + + // never support culture and/or segment + Assert4A(ContentVariation.Nothing, "culture", null, false); + Assert4A(ContentVariation.Nothing, null, "segment", false); + Assert4A(ContentVariation.Nothing, "culture", "segment", false); + + // support '*' only when wildcards are supported + Assert4B(ContentVariation.Nothing, "*", null, true, false, false, true); + Assert4B(ContentVariation.Nothing, null, "*", true, false, false, true); + Assert4B(ContentVariation.Nothing, "*", "*", true, false, false, true); + + + // support invariant if not exact + Assert4B(ContentVariation.Culture, null, null, false, true, false, true); + + // support invariant if not exact, '*' when wildcards are supported + Assert4B(ContentVariation.Culture, "*", null, true, false, false, true); + Assert4B(ContentVariation.Culture, null, "*", false, false, false, true); + Assert4B(ContentVariation.Culture, "*", "*", true, false, false, true); + + // never support segment + Assert4A(ContentVariation.Culture, null, "segment", false); + Assert4A(ContentVariation.Culture, "culture", "segment", false); + Assert4A(ContentVariation.Culture, "*", "segment", false); + + Assert4B(ContentVariation.Culture, null, "*", false, false, false, true); + Assert4B(ContentVariation.Culture, "culture", "*", true, false, false, true); + + // could do the same with .Segment, and .CultureAndSegment + } + [Test] public void PropertyTests() { @@ -75,7 +123,7 @@ namespace Umbraco.Tests.Models Assert.AreEqual("a", prop.GetValue(published: true)); // illegal, 'cos non-publishing - Assert.Throws(() => prop.PublishValue()); + Assert.Throws(() => prop.PublishValues()); // change propertyType.IsPublishing = true; @@ -91,7 +139,7 @@ namespace Umbraco.Tests.Models // can publish value // and get edited and published values - prop.PublishValue(); + prop.PublishValues(); Assert.AreEqual("a", prop.GetValue()); Assert.AreEqual("a", prop.GetValue(published: true)); @@ -102,57 +150,57 @@ namespace Umbraco.Tests.Models Assert.AreEqual("a", prop.GetValue(published: true)); // can clear value - prop.ClearPublishedValue(); + prop.ClearPublishedValues(); Assert.AreEqual("b", prop.GetValue()); Assert.IsNull(prop.GetValue(published: true)); - // change - propertyType.Variations |= ContentVariation.CultureNeutral; + // change - now we vary by culture + propertyType.Variations |= ContentVariation.Culture; // can set value // and get values prop.SetValue("c", langFr); - Assert.AreEqual("b", prop.GetValue()); + Assert.IsNull(prop.GetValue()); // there is no invariant value anymore Assert.IsNull(prop.GetValue(published: true)); Assert.AreEqual("c", prop.GetValue(langFr)); Assert.IsNull(prop.GetValue(langFr, published: true)); // can publish value // and get edited and published values - prop.PublishValue(langFr); - Assert.AreEqual("b", prop.GetValue()); + prop.PublishValues(langFr); + Assert.IsNull(prop.GetValue()); Assert.IsNull(prop.GetValue(published: true)); Assert.AreEqual("c", prop.GetValue(langFr)); Assert.AreEqual("c", prop.GetValue(langFr, published: true)); // can clear all - prop.ClearPublishedAllValues(); - Assert.AreEqual("b", prop.GetValue()); + prop.ClearPublishedValues("*"); + Assert.IsNull(prop.GetValue()); Assert.IsNull(prop.GetValue(published: true)); Assert.AreEqual("c", prop.GetValue(langFr)); Assert.IsNull(prop.GetValue(langFr, published: true)); // can publish all - prop.PublishAllValues(); - Assert.AreEqual("b", prop.GetValue()); - Assert.AreEqual("b", prop.GetValue(published: true)); + prop.PublishValues("*"); + Assert.IsNull(prop.GetValue()); + Assert.IsNull(prop.GetValue(published: true)); Assert.AreEqual("c", prop.GetValue(langFr)); Assert.AreEqual("c", prop.GetValue(langFr, published: true)); // same for culture - prop.ClearPublishedCultureValues(langFr); + prop.ClearPublishedValues(langFr); Assert.AreEqual("c", prop.GetValue(langFr)); Assert.IsNull(prop.GetValue(langFr, published: true)); - prop.PublishCultureValues(langFr); + prop.PublishValues(langFr); Assert.AreEqual("c", prop.GetValue(langFr)); Assert.AreEqual("c", prop.GetValue(langFr, published: true)); - prop.ClearPublishedCultureValues(); - Assert.AreEqual("b", prop.GetValue()); + prop.ClearPublishedValues(); // does not throw, internal, content item throws + Assert.IsNull(prop.GetValue()); + Assert.IsNull(prop.GetValue(published: true)); + prop.PublishValues(); // does not throw, internal, content item throws + Assert.IsNull(prop.GetValue()); Assert.IsNull(prop.GetValue(published: true)); - prop.PublishCultureValues(); - Assert.AreEqual("b", prop.GetValue()); - Assert.AreEqual("b", prop.GetValue(published: true)); } [Test] @@ -165,23 +213,23 @@ namespace Umbraco.Tests.Models const string langUk = "en-UK"; // throws if the content type does not support the variation - Assert.Throws(() => content.SetName("name-fr", langFr)); + Assert.Throws(() => content.SetCultureName("name-fr", langFr)); // now it will work - contentType.Variations = ContentVariation.CultureNeutral; + contentType.Variations = ContentVariation.Culture; // invariant name works content.Name = "name"; - Assert.AreEqual("name", content.GetName(null)); - content.SetName("name2", null); + Assert.AreEqual("name", content.GetCultureName(null)); + content.SetCultureName("name2", null); Assert.AreEqual("name2", content.Name); - Assert.AreEqual("name2", content.GetName(null)); + Assert.AreEqual("name2", content.GetCultureName(null)); // variant names work - content.SetName("name-fr", langFr); - content.SetName("name-uk", langUk); - Assert.AreEqual("name-fr", content.GetName(langFr)); - Assert.AreEqual("name-uk", content.GetName(langUk)); + content.SetCultureName("name-fr", langFr); + content.SetCultureName("name-uk", langUk); + Assert.AreEqual("name-fr", content.GetCultureName(langFr)); + Assert.AreEqual("name-uk", content.GetCultureName(langUk)); // variant dictionary of names work Assert.AreEqual(2, content.CultureNames.Count); @@ -230,14 +278,14 @@ namespace Umbraco.Tests.Models Assert.AreEqual("b", content.GetValue("prop")); Assert.IsNull(content.GetValue("prop", published: true)); - // change - contentType.Variations |= ContentVariation.CultureNeutral; - propertyType.Variations |= ContentVariation.CultureNeutral; + // change - now we vary by culture + contentType.Variations |= ContentVariation.Culture; + propertyType.Variations |= ContentVariation.Culture; // can set value // and get values content.SetValue("prop", "c", langFr); - Assert.AreEqual("b", content.GetValue("prop")); + Assert.IsNull(content.GetValue("prop")); // there is no invariant value anymore Assert.IsNull(content.GetValue("prop", published: true)); Assert.AreEqual("c", content.GetValue("prop", langFr)); Assert.IsNull(content.GetValue("prop", langFr, published: true)); @@ -245,57 +293,57 @@ namespace Umbraco.Tests.Models // can publish value // and get edited and published values Assert.IsFalse(content.TryPublishValues(langFr)); // no name - content.SetName("name-fr", langFr); + content.SetCultureName("name-fr", langFr); Assert.IsTrue(content.TryPublishValues(langFr)); - Assert.AreEqual("b", content.GetValue("prop")); + Assert.IsNull(content.GetValue("prop")); Assert.IsNull(content.GetValue("prop", published: true)); Assert.AreEqual("c", content.GetValue("prop", langFr)); Assert.AreEqual("c", content.GetValue("prop", langFr, published: true)); // can clear all - content.ClearAllPublishedValues(); - Assert.AreEqual("b", content.GetValue("prop")); + content.ClearPublishedValues("*"); + Assert.IsNull(content.GetValue("prop")); Assert.IsNull(content.GetValue("prop", published: true)); Assert.AreEqual("c", content.GetValue("prop", langFr)); Assert.IsNull(content.GetValue("prop", langFr, published: true)); // can publish all - Assert.IsTrue(content.TryPublishAllValues()); - Assert.AreEqual("b", content.GetValue("prop")); - Assert.AreEqual("b", content.GetValue("prop", published: true)); + Assert.IsTrue(content.TryPublishValues("*")); + Assert.IsNull(content.GetValue("prop")); + Assert.IsNull(content.GetValue("prop", published: true)); Assert.AreEqual("c", content.GetValue("prop", langFr)); Assert.AreEqual("c", content.GetValue("prop", langFr, published: true)); // same for culture - content.ClearCulturePublishedValues(langFr); + content.ClearPublishedValues(langFr); Assert.AreEqual("c", content.GetValue("prop", langFr)); Assert.IsNull(content.GetValue("prop", langFr, published: true)); - content.PublishCultureValues(langFr); + content.TryPublishValues(langFr); Assert.AreEqual("c", content.GetValue("prop", langFr)); Assert.AreEqual("c", content.GetValue("prop", langFr, published: true)); - content.ClearCulturePublishedValues(); - Assert.AreEqual("b", content.GetValue("prop")); + content.ClearPublishedValues(); // clears invariant props if any + Assert.IsNull(content.GetValue("prop")); + Assert.IsNull(content.GetValue("prop", published: true)); + content.TryPublishValues(); // publishes invariant props if any + Assert.IsNull(content.GetValue("prop")); Assert.IsNull(content.GetValue("prop", published: true)); - content.PublishCultureValues(); - Assert.AreEqual("b", content.GetValue("prop")); - Assert.AreEqual("b", content.GetValue("prop", published: true)); var other = new Content("other", -1, contentType) { Id = 2, VersionId = 1 }; - other.SetValue("prop", "o"); + Assert.Throws(() => other.SetValue("prop", "o")); // don't even try other.SetValue("prop", "o1", langFr); // can copy other's edited value content.CopyAllValues(other); - Assert.AreEqual("o", content.GetValue("prop")); - Assert.AreEqual("b", content.GetValue("prop", published: true)); + Assert.IsNull(content.GetValue("prop")); + Assert.IsNull(content.GetValue("prop", published: true)); Assert.AreEqual("o1", content.GetValue("prop", langFr)); Assert.AreEqual("c", content.GetValue("prop", langFr, published: true)); // can copy self's published value content.CopyAllValues(content); - Assert.AreEqual("b", content.GetValue("prop")); - Assert.AreEqual("b", content.GetValue("prop", published: true)); + Assert.IsNull(content.GetValue("prop")); + Assert.IsNull(content.GetValue("prop", published: true)); Assert.AreEqual("c", content.GetValue("prop", langFr)); Assert.AreEqual("c", content.GetValue("prop", langFr, published: true)); } @@ -305,29 +353,31 @@ namespace Umbraco.Tests.Models { const string langFr = "fr-FR"; - var contentType = new ContentType(-1) { Alias = "contentType" }; - contentType.Variations |= ContentVariation.CultureNeutral; //supports both variant/invariant + // content type varies by Culture + // prop1 varies by Culture + // prop2 is invariant + + var contentType = new ContentType(-1) { Alias = "contentType" }; + contentType.Variations |= ContentVariation.Culture; + + var variantPropType = new PropertyType("editor", ValueStorageType.Nvarchar) { Alias = "prop1", Variations = ContentVariation.Culture, Mandatory = true }; + var invariantPropType = new PropertyType("editor", ValueStorageType.Nvarchar) { Alias = "prop2", Variations = ContentVariation.Nothing, Mandatory = true}; - //In real life, a property cannot be both invariant + variant and be mandatory. If this happens validation will always fail when doing TryPublishValues since the invariant value will always be empty. - //so here we are only setting properties to one or the other, not both - var variantPropType = new PropertyType("editor", ValueStorageType.Nvarchar) { Alias = "prop1", Variations = ContentVariation.CultureNeutral, Mandatory = true }; - var invariantPropType = new PropertyType("editor", ValueStorageType.Nvarchar) { Alias = "prop2", Variations = ContentVariation.InvariantNeutral, Mandatory = true}; - contentType.AddPropertyType(variantPropType); contentType.AddPropertyType(invariantPropType); var content = new Content("content", -1, contentType) { Id = 1, VersionId = 1 }; - content.SetName("hello", langFr); + content.SetCultureName("hello", langFr); - Assert.IsFalse(content.TryPublishValues(langFr)); //will fail because prop1 is mandatory + Assert.IsFalse(content.TryPublishValues(langFr)); // fails because prop1 is mandatory content.SetValue("prop1", "a", langFr); Assert.IsTrue(content.TryPublishValues(langFr)); Assert.AreEqual("a", content.GetValue("prop1", langFr, published: true)); //this will be null because we tried to publish values for a specific culture but this property is invariant Assert.IsNull(content.GetValue("prop2", published: true)); - Assert.IsFalse(content.TryPublishValues()); //will fail because prop2 is mandatory + Assert.IsFalse(content.TryPublishValues()); // fails because prop2 is mandatory content.SetValue("prop2", "b"); Assert.IsTrue(content.TryPublishValues()); Assert.AreEqual("b", content.GetValue("prop2", published: true)); @@ -346,10 +396,11 @@ namespace Umbraco.Tests.Models var content = new Content("content", -1, contentType) { Id = 1, VersionId = 1 }; - contentType.Variations |= ContentVariation.CultureNeutral; - propertyType.Variations |= ContentVariation.CultureNeutral; + // change - now we vary by culture + contentType.Variations |= ContentVariation.Culture; + propertyType.Variations |= ContentVariation.Culture; - content.SetValue("prop", "a"); + Assert.Throws(() => content.SetValue("prop", "a")); // invariant = no content.SetValue("prop", "a-fr", langFr); content.SetValue("prop", "a-uk", langUk); content.SetValue("prop", "a-es", langEs); @@ -359,29 +410,29 @@ namespace Umbraco.Tests.Models // works with a name // and then FR is available, and published - content.SetName("name-fr", langFr); + content.SetCultureName("name-fr", langFr); Assert.IsTrue(content.TryPublishValues(langFr)); // now UK is available too - content.SetName("name-uk", langUk); + content.SetCultureName("name-uk", langUk); // test available, published Assert.IsTrue(content.IsCultureAvailable(langFr)); Assert.IsTrue(content.IsCulturePublished(langFr)); Assert.AreEqual("name-fr", content.GetPublishName(langFr)); - Assert.AreNotEqual(DateTime.MinValue, content.GetCulturePublishDate(langFr)); + Assert.AreNotEqual(DateTime.MinValue, content.GetPublishDate(langFr)); Assert.IsFalse(content.IsCultureEdited(langFr)); // once published, edited is *wrong* until saved Assert.IsTrue(content.IsCultureAvailable(langUk)); Assert.IsFalse(content.IsCulturePublished(langUk)); Assert.IsNull(content.GetPublishName(langUk)); - Assert.Throws(() => content.GetCulturePublishDate(langUk)); // not published! + Assert.IsNull(content.GetPublishDate(langUk)); // not published Assert.IsTrue(content.IsCultureEdited(langEs)); // not published, so... edited Assert.IsFalse(content.IsCultureAvailable(langEs)); Assert.IsFalse(content.IsCulturePublished(langEs)); Assert.IsNull(content.GetPublishName(langEs)); - Assert.Throws(() => content.GetCulturePublishDate(langEs)); // not published! + Assert.IsNull(content.GetPublishDate(langEs)); // not published! Assert.IsTrue(content.IsCultureEdited(langEs)); // not published, so... edited // cannot test IsCultureEdited here - as that requires the content service and repository @@ -432,7 +483,7 @@ namespace Umbraco.Tests.Models Assert.IsFalse(prop.IsValid()); // can publish, even though invalid - prop.PublishValue(); + prop.PublishValues(); } } } diff --git a/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs index 983cccbdb9..805ea2d862 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs @@ -683,22 +683,24 @@ namespace Umbraco.Tests.Persistence.Repositories [Test] public void GetPagedResultsByQuery_With_Variant_Names() { - //2x content types, one invariant, one variant - + // one invariant content type named "umbInvariantTextPage" + // var invariantCt = MockedContentTypes.CreateSimpleContentType("umbInvariantTextpage", "Invariant Textpage"); - invariantCt.Variations = ContentVariation.InvariantNeutral; - foreach (var p in invariantCt.PropertyTypes) p.Variations = ContentVariation.InvariantNeutral; + invariantCt.Variations = ContentVariation.Nothing; + foreach (var p in invariantCt.PropertyTypes) p.Variations = ContentVariation.Nothing; ServiceContext.FileService.SaveTemplate(invariantCt.DefaultTemplate); // else, FK violation on contentType! ServiceContext.ContentTypeService.Save(invariantCt); + // one variant (by culture) content type named "umbVariantTextPage" + // with properties, every 2nd one being variant (by culture), the other being invariant + // var variantCt = MockedContentTypes.CreateSimpleContentType("umbVariantTextpage", "Variant Textpage"); - variantCt.Variations = ContentVariation.CultureNeutral; + variantCt.Variations = ContentVariation.Culture; var propTypes = variantCt.PropertyTypes.ToList(); for (var i = 0; i < propTypes.Count; i++) { var p = propTypes[i]; - //every 2nd one is variant - p.Variations = i % 2 == 0 ? ContentVariation.CultureNeutral : ContentVariation.InvariantNeutral; + p.Variations = i % 2 == 0 ? ContentVariation.Culture : ContentVariation.Nothing; } ServiceContext.FileService.SaveTemplate(variantCt.DefaultTemplate); // else, FK violation on contentType! ServiceContext.ContentTypeService.Save(variantCt); @@ -711,6 +713,8 @@ namespace Umbraco.Tests.Persistence.Repositories var root = MockedContent.CreateSimpleContent(invariantCt); ServiceContext.ContentService.Save(root); + var children = new List(); + for (var i = 0; i < 25; i++) { var isInvariant = i % 2 == 0; @@ -732,20 +736,30 @@ namespace Umbraco.Tests.Persistence.Repositories } ServiceContext.ContentService.Save(child); + children.Add(child); } + var child1 = children[1]; + Assert.IsTrue(child1.ContentType.VariesByCulture()); + Assert.IsTrue(child1.Name.StartsWith("VAR")); + Assert.IsTrue(child1.GetCultureName("en-US").StartsWith("VAR")); + var provider = TestObjects.GetScopeProvider(Logger); using (var scope = provider.CreateScope()) { var repository = CreateRepository((IScopeAccessor)provider, out _); - var query = scope.SqlContext.Query().Where(x => x.ParentId == root.Id); + var child = repository.Get(children[1].Id); // 1 is variant + Assert.IsTrue(child.ContentType.VariesByCulture()); + Assert.IsTrue(child.Name.StartsWith("VAR")); + Assert.IsTrue(child.GetCultureName("en-US").StartsWith("VAR")); try { scope.Database.AsUmbracoDatabase().EnableSqlTrace = true; scope.Database.AsUmbracoDatabase().EnableSqlCount = true; + var query = scope.SqlContext.Query().Where(x => x.ParentId == root.Id); var result = repository.GetPage(query, 0, 20, out var totalRecords, "UpdateDate", Direction.Ascending, true); Assert.AreEqual(25, totalRecords); @@ -761,7 +775,7 @@ namespace Umbraco.Tests.Persistence.Repositories foreach (var p in r.Properties) { //ensure there is a value for the correct variant/invariant property - var value = p.GetValue(p.PropertyType.Variations.Has(ContentVariation.InvariantNeutral) ? null : "en-US"); + var value = p.GetValue(p.PropertyType.Variations.VariesByNothing() ? null : "en-US"); Assert.IsNotNull(value); } } diff --git a/src/Umbraco.Tests/PublishedContent/NuCacheTests.cs b/src/Umbraco.Tests/PublishedContent/NuCacheTests.cs index 2703cd448d..492f1f7dc0 100644 --- a/src/Umbraco.Tests/PublishedContent/NuCacheTests.cs +++ b/src/Umbraco.Tests/PublishedContent/NuCacheTests.cs @@ -84,8 +84,8 @@ namespace Umbraco.Tests.PublishedContent dataType }; - var propertyType = new PropertyType("Umbraco.Void.Editor", ValueStorageType.Nvarchar) { Alias = "prop", DataTypeId = 3, Variations = ContentVariation.InvariantNeutral | ContentVariation.CultureNeutral }; - var contentType = new ContentType(-1) { Id = 2, Alias = "alias-ct", Variations = ContentVariation.InvariantNeutral | ContentVariation.CultureNeutral }; + var propertyType = new PropertyType("Umbraco.Void.Editor", ValueStorageType.Nvarchar) { Alias = "prop", DataTypeId = 3, Variations = ContentVariation.Culture }; + var contentType = new ContentType(-1) { Id = 2, Alias = "alias-ct", Variations = ContentVariation.Culture }; contentType.AddPropertyType(propertyType); var contentTypes = new[] @@ -195,16 +195,16 @@ namespace Umbraco.Tests.PublishedContent // but, // if the content type / property type does not vary, then it's all invariant again // modify the content type and property type, notify the snapshot service - contentType.Variations = ContentVariation.InvariantNeutral; - propertyType.Variations = ContentVariation.InvariantNeutral; + contentType.Variations = ContentVariation.Nothing; + propertyType.Variations = ContentVariation.Nothing; snapshotService.Notify(new[] { new ContentTypeCacheRefresher.JsonPayload("IContentType", publishedContent.ContentType.Id, ContentTypeChangeTypes.RefreshMain) }); // get a new snapshot (nothing changed in the old one), get the published content again var anotherSnapshot = snapshotService.CreatePublishedSnapshot(previewToken: null); var againContent = anotherSnapshot.Content.GetById(1); - Assert.AreEqual(ContentVariation.InvariantNeutral, againContent.ContentType.Variations); - Assert.AreEqual(ContentVariation.InvariantNeutral, againContent.ContentType.GetPropertyType("prop").Variations); + Assert.AreEqual(ContentVariation.Nothing, againContent.ContentType.Variations); + Assert.AreEqual(ContentVariation.Nothing, againContent.ContentType.GetPropertyType("prop").Variations); // now, "no culture" means "invariant" Assert.AreEqual("It Works1!", againContent.Name); diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentDataTableTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentDataTableTests.cs index 4f895243b2..a640423515 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentDataTableTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentDataTableTests.cs @@ -98,7 +98,7 @@ namespace Umbraco.Tests.PublishedContent var doc = GetContent(true, 1); //change a doc type alias var c = (TestPublishedContent) doc.Children.ElementAt(0); - c.ContentType = new PublishedContentType(22, "DontMatch", PublishedItemType.Content, Enumerable.Empty(), Enumerable.Empty(), ContentVariation.InvariantNeutral); + c.ContentType = new PublishedContentType(22, "DontMatch", PublishedItemType.Content, Enumerable.Empty(), Enumerable.Empty(), ContentVariation.Nothing); var dt = doc.ChildrenAsTable(Current.Services, "Child"); @@ -175,7 +175,7 @@ namespace Umbraco.Tests.PublishedContent new RawValueProperty(factory.CreatePropertyType("property3", 1), d, "value" + (indexVals + 2))); } - d.ContentType = new PublishedContentType(22, contentTypeAlias, PublishedItemType.Content, Enumerable.Empty(), Enumerable.Empty(), ContentVariation.InvariantNeutral); + d.ContentType = new PublishedContentType(22, contentTypeAlias, PublishedItemType.Content, Enumerable.Empty(), Enumerable.Empty(), ContentVariation.Nothing); return d; } diff --git a/src/Umbraco.Tests/PublishedContent/PublishedRouterTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedRouterTests.cs index 66f7871a4b..df7e1d9d09 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedRouterTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedRouterTests.cs @@ -81,7 +81,7 @@ namespace Umbraco.Tests.PublishedContent pc.Setup(content => content.Path).Returns("-1,1"); pc.Setup(content => content.Parent).Returns(() => null); pc.Setup(content => content.Properties).Returns(new Collection()); - pc.Setup(content => content.ContentType).Returns(new PublishedContentType(22, "anything", PublishedItemType.Content, Enumerable.Empty(), Enumerable.Empty(), ContentVariation.InvariantNeutral)); + pc.Setup(content => content.ContentType).Returns(new PublishedContentType(22, "anything", PublishedItemType.Content, Enumerable.Empty(), Enumerable.Empty(), ContentVariation.Nothing)); return pc; } } diff --git a/src/Umbraco.Tests/PublishedContent/SolidPublishedSnapshot.cs b/src/Umbraco.Tests/PublishedContent/SolidPublishedSnapshot.cs index 4f63533693..756b775e46 100644 --- a/src/Umbraco.Tests/PublishedContent/SolidPublishedSnapshot.cs +++ b/src/Umbraco.Tests/PublishedContent/SolidPublishedSnapshot.cs @@ -330,11 +330,11 @@ namespace Umbraco.Tests.PublishedContent } public AutoPublishedContentType(int id, string alias, IEnumerable propertyTypes) - : base(id, alias, PublishedItemType.Content, Enumerable.Empty(), propertyTypes, ContentVariation.InvariantNeutral) + : base(id, alias, PublishedItemType.Content, Enumerable.Empty(), propertyTypes, ContentVariation.Nothing) { } public AutoPublishedContentType(int id, string alias, IEnumerable compositionAliases, IEnumerable propertyTypes) - : base(id, alias, PublishedItemType.Content, compositionAliases, propertyTypes, ContentVariation.InvariantNeutral) + : base(id, alias, PublishedItemType.Content, compositionAliases, propertyTypes, ContentVariation.Nothing) { } public override PublishedPropertyType GetPropertyType(string alias) diff --git a/src/Umbraco.Tests/Routing/ContentFinderByAliasTests.cs b/src/Umbraco.Tests/Routing/ContentFinderByAliasTests.cs index 032f15f5d1..4059cb1858 100644 --- a/src/Umbraco.Tests/Routing/ContentFinderByAliasTests.cs +++ b/src/Umbraco.Tests/Routing/ContentFinderByAliasTests.cs @@ -22,12 +22,12 @@ namespace Umbraco.Tests.Routing var properties = new[] { - new PublishedPropertyType("umbracoUrlAlias", Constants.DataTypes.Textbox, false, ContentVariation.InvariantNeutral, + new PublishedPropertyType("umbracoUrlAlias", Constants.DataTypes.Textbox, false, ContentVariation.Nothing, new PropertyValueConverterCollection(Enumerable.Empty()), Mock.Of(), Mock.Of()), }; - _publishedContentType = new PublishedContentType(0, "Doc", PublishedItemType.Content, Enumerable.Empty(), properties, ContentVariation.InvariantNeutral); + _publishedContentType = new PublishedContentType(0, "Doc", PublishedItemType.Content, Enumerable.Empty(), properties, ContentVariation.Nothing); } protected override PublishedContentType GetPublishedContentTypeByAlias(string alias) diff --git a/src/Umbraco.Tests/Routing/ContentFinderByAliasWithDomainsTests.cs b/src/Umbraco.Tests/Routing/ContentFinderByAliasWithDomainsTests.cs index 6b5eedc743..0c1f89f430 100644 --- a/src/Umbraco.Tests/Routing/ContentFinderByAliasWithDomainsTests.cs +++ b/src/Umbraco.Tests/Routing/ContentFinderByAliasWithDomainsTests.cs @@ -20,12 +20,12 @@ namespace Umbraco.Tests.Routing var properties = new[] { - new PublishedPropertyType("umbracoUrlAlias", Constants.DataTypes.Textbox, false, ContentVariation.InvariantNeutral, + new PublishedPropertyType("umbracoUrlAlias", Constants.DataTypes.Textbox, false, ContentVariation.Nothing, new PropertyValueConverterCollection(Enumerable.Empty()), Mock.Of(), Mock.Of()), }; - _publishedContentType = new PublishedContentType(0, "Doc", PublishedItemType.Content, Enumerable.Empty(), properties, ContentVariation.InvariantNeutral); + _publishedContentType = new PublishedContentType(0, "Doc", PublishedItemType.Content, Enumerable.Empty(), properties, ContentVariation.Nothing); } protected override PublishedContentType GetPublishedContentTypeByAlias(string alias) diff --git a/src/Umbraco.Tests/Routing/UrlProviderTests.cs b/src/Umbraco.Tests/Routing/UrlProviderTests.cs index d3cd25ae92..2f1e4e3476 100644 --- a/src/Umbraco.Tests/Routing/UrlProviderTests.cs +++ b/src/Umbraco.Tests/Routing/UrlProviderTests.cs @@ -167,8 +167,7 @@ namespace Umbraco.Tests.Routing var requestMock = Mock.Get(_umbracoSettings.RequestHandler); requestMock.Setup(x => x.UseDomainPrefixes).Returns(false); - var contentType = new PublishedContentType(666, "alias", PublishedItemType.Content, Enumerable.Empty(), Enumerable.Empty(), - ContentVariation.CultureNeutral); + var contentType = new PublishedContentType(666, "alias", PublishedItemType.Content, Enumerable.Empty(), Enumerable.Empty(), ContentVariation.Culture); var publishedContent = new TestPublishedContent(contentType, 1234, Guid.NewGuid(), new Dictionary(), false); var publishedContentCache = new Mock(); @@ -216,8 +215,7 @@ namespace Umbraco.Tests.Routing var requestMock = Mock.Get(_umbracoSettings.RequestHandler); requestMock.Setup(x => x.UseDomainPrefixes).Returns(false); - var contentType = new PublishedContentType(666, "alias", PublishedItemType.Content, Enumerable.Empty(), Enumerable.Empty(), - ContentVariation.CultureNeutral); + var contentType = new PublishedContentType(666, "alias", PublishedItemType.Content, Enumerable.Empty(), Enumerable.Empty(), ContentVariation.Culture); var publishedContent = new TestPublishedContent(contentType, 1234, Guid.NewGuid(), new Dictionary(), false); var publishedContentCache = new Mock(); @@ -274,8 +272,7 @@ namespace Umbraco.Tests.Routing var requestMock = Mock.Get(_umbracoSettings.RequestHandler); requestMock.Setup(x => x.UseDomainPrefixes).Returns(false); - var contentType = new PublishedContentType(666, "alias", PublishedItemType.Content, Enumerable.Empty(), Enumerable.Empty(), - ContentVariation.CultureNeutral); + var contentType = new PublishedContentType(666, "alias", PublishedItemType.Content, Enumerable.Empty(), Enumerable.Empty(), ContentVariation.Culture); var publishedContent = new TestPublishedContent(contentType, 1234, Guid.NewGuid(), new Dictionary(), false); var publishedContentCache = new Mock(); diff --git a/src/Umbraco.Tests/Routing/UrlsProviderWithDomainsTests.cs b/src/Umbraco.Tests/Routing/UrlsProviderWithDomainsTests.cs index 627d95ea29..c6bd0fdb88 100644 --- a/src/Umbraco.Tests/Routing/UrlsProviderWithDomainsTests.cs +++ b/src/Umbraco.Tests/Routing/UrlsProviderWithDomainsTests.cs @@ -186,7 +186,7 @@ namespace Umbraco.Tests.Routing { new DefaultUrlProvider(settings.RequestHandler, Logger, globalSettings.Object, new SiteDomainHelper()) }, globalSettings:globalSettings.Object); - + SetDomains1(); var currentUri = new Uri(currentUrl); @@ -415,7 +415,9 @@ namespace Umbraco.Tests.Routing var result = umbracoContext.UrlProvider.GetOtherUrls(100111).ToArray(); - Assert.AreEqual(2, result.Count()); + foreach (var x in result) Console.WriteLine(x); + + Assert.AreEqual(2, result.Length); Assert.IsTrue(result.Contains("http://domain1a.com/en/1001-1-1/")); Assert.IsTrue(result.Contains("http://domain1b.com/en/1001-1-1/")); } diff --git a/src/Umbraco.Tests/Services/ContentServiceTests.cs b/src/Umbraco.Tests/Services/ContentServiceTests.cs index 50a535d029..92c38cee67 100644 --- a/src/Umbraco.Tests/Services/ContentServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentServiceTests.cs @@ -2507,7 +2507,7 @@ namespace Umbraco.Tests.Services var languageService = ServiceContext.LocalizationService; var langUk = new Language("en-UK") { IsDefaultVariantLanguage = true }; - var langFr = new Language("fr-FR"); + var langFr = new Language("fr-FR"); languageService.Save(langFr); languageService.Save(langUk); @@ -2515,15 +2515,15 @@ namespace Umbraco.Tests.Services var contentTypeService = ServiceContext.ContentTypeService; var contentType = contentTypeService.Get("umbTextpage"); - contentType.Variations = ContentVariation.InvariantNeutral | ContentVariation.CultureNeutral; - contentType.AddPropertyType(new PropertyType(Constants.PropertyEditors.Aliases.TextBox, ValueStorageType.Nvarchar, "prop") { Variations = ContentVariation.CultureNeutral }); + contentType.Variations = ContentVariation.Culture; + contentType.AddPropertyType(new PropertyType(Constants.PropertyEditors.Aliases.TextBox, ValueStorageType.Nvarchar, "prop") { Variations = ContentVariation.Culture }); contentTypeService.Save(contentType); var contentService = ServiceContext.ContentService; var content = new Content(null, -1, contentType); - content.SetName("name-us", langUk.IsoCode); - content.SetName("name-fr", langFr.IsoCode); + content.SetCultureName("name-us", langUk.IsoCode); + content.SetCultureName("name-fr", langFr.IsoCode); contentService.Save(content); //the name will be set to the default culture variant name @@ -2550,26 +2550,26 @@ namespace Umbraco.Tests.Services var contentTypeService = ServiceContext.ContentTypeService; var contentType = contentTypeService.Get("umbTextpage"); - contentType.Variations = ContentVariation.InvariantNeutral | ContentVariation.CultureNeutral; - contentType.AddPropertyType(new PropertyType(Constants.PropertyEditors.Aliases.TextBox, ValueStorageType.Nvarchar, "prop") { Variations = ContentVariation.CultureNeutral }); + contentType.Variations = ContentVariation.Culture; contentTypeService.Save(contentType); var contentService = ServiceContext.ContentService; var content = new Content(null, -1, contentType); - content.SetName("root", langUk.IsoCode); + content.SetCultureName("root", langUk.IsoCode); contentService.Save(content); for (var i = 0; i < 5; i++) { var child = new Content(null, content, contentType); - child.SetName("child", langUk.IsoCode); + child.SetCultureName("child", langUk.IsoCode); contentService.Save(child); - Assert.AreEqual("child" + (i == 0 ? "" : " (" + (i).ToString() + ")"), child.GetName(langUk.IsoCode)); + + Assert.AreEqual("child" + (i == 0 ? "" : " (" + i + ")"), child.GetCultureName(langUk.IsoCode)); //Save it again to ensure that the unique check is not performed again against it's own name contentService.Save(child); - Assert.AreEqual("child" + (i == 0 ? "" : " (" + (i).ToString() + ")"), child.GetName(langUk.IsoCode)); + Assert.AreEqual("child" + (i == 0 ? "" : " (" + i + ")"), child.GetCultureName(langUk.IsoCode)); } } @@ -2578,7 +2578,7 @@ namespace Umbraco.Tests.Services { var languageService = ServiceContext.LocalizationService; - var langFr = new Language("fr-FR"); + var langFr = new Language("fr-FR") { IsDefaultVariantLanguage = true }; var langUk = new Language("en-UK"); var langDe = new Language("de-DE"); @@ -2588,31 +2588,29 @@ namespace Umbraco.Tests.Services var contentTypeService = ServiceContext.ContentTypeService; - // fixme - // contentType.Variations is InvariantNeutral | CultureNeutral - // propertyType.Variations can only be a subset of contentType.Variations - ie cannot *add* anything - // (at least, we should validate this) - // but then, - // if the contentType supports InvariantNeutral | CultureNeutral, - // the propertyType should support InvariantNeutral, or both, but not solely CultureNeutral? - // but does this mean that CultureNeutral implies InvariantNeutral? - // can a contentType *not* support InvariantNeutral? - var contentType = contentTypeService.Get("umbTextpage"); - contentType.Variations = ContentVariation.InvariantNeutral | ContentVariation.CultureNeutral; - contentType.AddPropertyType(new PropertyType(Constants.PropertyEditors.Aliases.TextBox, ValueStorageType.Nvarchar, "prop") { Variations = ContentVariation.CultureNeutral }); + contentType.Variations = ContentVariation.Culture; + contentType.AddPropertyType(new PropertyType(Constants.PropertyEditors.Aliases.TextBox, ValueStorageType.Nvarchar, "prop") { Variations = ContentVariation.Culture }); + // fixme add test w/ an invariant prop contentTypeService.Save(contentType); var contentService = ServiceContext.ContentService; - var content = contentService.Create("Home US", - 1, "umbTextpage"); + var content = contentService.Create("Home US", -1, "umbTextpage"); + + // creating content with a name but no culture - will set the invariant name + // but, because that content is variant, as soon as we save, we'll need to + // replace the invariant name with whatever we have in cultures - always + // + // in fact, that would throw, because there is no name + //contentService.Save(content); // act content.SetValue("author", "Barack Obama"); content.SetValue("prop", "value-fr1", langFr.IsoCode); content.SetValue("prop", "value-uk1", langUk.IsoCode); - content.SetName("name-fr", langFr.IsoCode); - content.SetName("name-uk", langUk.IsoCode); + content.SetCultureName("name-fr", langFr.IsoCode); // and then we can save + content.SetCultureName("name-uk", langUk.IsoCode); contentService.Save(content); // content has been saved, @@ -2620,9 +2618,9 @@ namespace Umbraco.Tests.Services var content2 = contentService.GetById(content.Id); - Assert.AreEqual("Home US", content2.Name); - Assert.AreEqual("name-fr", content2.GetName(langFr.IsoCode)); - Assert.AreEqual("name-uk", content2.GetName(langUk.IsoCode)); + Assert.AreEqual("name-fr", content2.Name); // got the default culture name when saved + Assert.AreEqual("name-fr", content2.GetCultureName(langFr.IsoCode)); + Assert.AreEqual("name-uk", content2.GetCultureName(langUk.IsoCode)); Assert.AreEqual("value-fr1", content2.GetValue("prop", langFr.IsoCode)); Assert.AreEqual("value-uk1", content2.GetValue("prop", langUk.IsoCode)); @@ -2657,9 +2655,9 @@ namespace Umbraco.Tests.Services content2 = contentService.GetById(content.Id); - Assert.AreEqual("Home US", content2.Name); - Assert.AreEqual("name-fr", content2.GetName(langFr.IsoCode)); - Assert.AreEqual("name-uk", content2.GetName(langUk.IsoCode)); + Assert.AreEqual("name-fr", content2.Name); // got the default culture name when saved + Assert.AreEqual("name-fr", content2.GetCultureName(langFr.IsoCode)); + Assert.AreEqual("name-uk", content2.GetCultureName(langUk.IsoCode)); // we haven't published InvariantNeutral, but a document cannot be published without an invariant name, // so when we tried and published for the first time above the french culture, the french name was used @@ -2686,8 +2684,8 @@ namespace Umbraco.Tests.Services AssertPerCulture(content, (x, c) => x.IsCultureEdited(c), (langFr, false), (langUk, false), (langDe, true)); AssertPerCulture(content2, (x, c) => x.IsCultureEdited(c), (langFr, false), (langUk, false), (langDe, true)); - AssertPerCulture(content, (x, c) => x.GetCulturePublishDate(c) == DateTime.MinValue, (langFr, false), (langUk, false)); // DE would throw - AssertPerCulture(content2, (x, c) => x.GetCulturePublishDate(c) == DateTime.MinValue, (langFr, false), (langUk, false)); // DE would throw + AssertPerCulture(content, (x, c) => x.GetPublishDate(c) == DateTime.MinValue, (langFr, false), (langUk, false)); // DE would throw + AssertPerCulture(content2, (x, c) => x.GetPublishDate(c) == DateTime.MinValue, (langFr, false), (langUk, false)); // DE would throw // note that content and content2 culture published dates might be slightly different due to roundtrip to database @@ -2701,14 +2699,14 @@ namespace Umbraco.Tests.Services content2 = contentService.GetById(content.Id); - Assert.AreEqual("Home US", content2.PublishName); + Assert.AreEqual("name-fr", content2.PublishName); // act - content.SetName("Home US2", null); - content.SetName("name-fr2", langFr.IsoCode); - content.SetName("name-uk2", langUk.IsoCode); + content.SetCultureName("Home US2", null); + content.SetCultureName("name-fr2", langFr.IsoCode); + content.SetCultureName("name-uk2", langUk.IsoCode); content.SetValue("author", "Barack Obama2"); content.SetValue("prop", "value-fr2", langFr.IsoCode); content.SetValue("prop", "value-uk2", langUk.IsoCode); @@ -2719,11 +2717,11 @@ namespace Umbraco.Tests.Services content2 = contentService.GetById(content.Id); - Assert.AreEqual("Home US2", content2.Name); - Assert.AreEqual("name-fr2", content2.GetName(langFr.IsoCode)); - Assert.AreEqual("name-uk2", content2.GetName(langUk.IsoCode)); + Assert.AreEqual("name-fr2", content2.Name); // got the default culture name when saved + Assert.AreEqual("name-fr2", content2.GetCultureName(langFr.IsoCode)); + Assert.AreEqual("name-uk2", content2.GetCultureName(langUk.IsoCode)); - Assert.AreEqual("Home US", content2.PublishName); + Assert.AreEqual("name-fr", content2.PublishName); Assert.AreEqual("name-fr", content2.GetPublishName(langFr.IsoCode)); Assert.AreEqual("name-uk", content2.GetPublishName(langUk.IsoCode)); @@ -2747,8 +2745,8 @@ namespace Umbraco.Tests.Services AssertPerCulture(content, (x, c) => x.IsCultureEdited(c), (langFr, true), (langUk, true), (langDe, true)); AssertPerCulture(content2, (x, c) => x.IsCultureEdited(c), (langFr, true), (langUk, true), (langDe, true)); - AssertPerCulture(content, (x, c) => x.GetCulturePublishDate(c) == DateTime.MinValue, (langFr, false), (langUk, false)); // DE would throw - AssertPerCulture(content2, (x, c) => x.GetCulturePublishDate(c) == DateTime.MinValue, (langFr, false), (langUk, false)); // DE would throw + AssertPerCulture(content, (x, c) => x.GetPublishDate(c) == DateTime.MinValue, (langFr, false), (langUk, false)); // DE would throw + AssertPerCulture(content2, (x, c) => x.GetPublishDate(c) == DateTime.MinValue, (langFr, false), (langUk, false)); // DE would throw // act @@ -2762,11 +2760,11 @@ namespace Umbraco.Tests.Services content2 = contentService.GetById(content.Id); - Assert.AreEqual("Home US2", content2.Name); - Assert.AreEqual("name-fr2", content2.GetName(langFr.IsoCode)); - Assert.AreEqual("name-uk2", content2.GetName(langUk.IsoCode)); + Assert.AreEqual("name-fr2", content2.Name); // got the default culture name when saved + Assert.AreEqual("name-fr2", content2.GetCultureName(langFr.IsoCode)); + Assert.AreEqual("name-uk2", content2.GetCultureName(langUk.IsoCode)); - Assert.AreEqual("Home US", content2.PublishName); + Assert.AreEqual("name-fr2", content2.PublishName); Assert.IsNull(content2.GetPublishName(langFr.IsoCode)); Assert.AreEqual("name-uk", content2.GetPublishName(langUk.IsoCode)); @@ -2790,8 +2788,8 @@ namespace Umbraco.Tests.Services AssertPerCulture(content, (x, c) => x.IsCultureEdited(c), (langFr, true), (langUk, true), (langDe, true)); AssertPerCulture(content2, (x, c) => x.IsCultureEdited(c), (langFr, true), (langUk, true), (langDe, true)); - AssertPerCulture(content, (x, c) => x.GetCulturePublishDate(c) == DateTime.MinValue, (langUk, false)); // FR, DE would throw - AssertPerCulture(content2, (x, c) => x.GetCulturePublishDate(c) == DateTime.MinValue, (langUk, false)); // FR, DE would throw + AssertPerCulture(content, (x, c) => x.GetPublishDate(c) == DateTime.MinValue, (langUk, false)); // FR, DE would throw + AssertPerCulture(content2, (x, c) => x.GetPublishDate(c) == DateTime.MinValue, (langUk, false)); // FR, DE would throw // act @@ -2812,11 +2810,11 @@ namespace Umbraco.Tests.Services Assert.IsFalse(content2.Published); - Assert.AreEqual("Home US2", content2.Name); - Assert.AreEqual("name-fr2", content2.GetName(langFr.IsoCode)); - Assert.AreEqual("name-uk2", content2.GetName(langUk.IsoCode)); + Assert.AreEqual("name-fr2", content2.Name); // got the default culture name when saved + Assert.AreEqual("name-fr2", content2.GetCultureName(langFr.IsoCode)); + Assert.AreEqual("name-uk2", content2.GetCultureName(langUk.IsoCode)); - Assert.AreEqual("Home US", content2.PublishName); // not null, see note above + Assert.AreEqual("name-fr2", content2.PublishName); // not null, see note above Assert.IsNull(content2.GetPublishName(langFr.IsoCode)); Assert.AreEqual("name-uk", content2.GetPublishName(langUk.IsoCode)); // not null, see note above @@ -2837,8 +2835,8 @@ namespace Umbraco.Tests.Services AssertPerCulture(content, (x, c) => x.IsCultureEdited(c), (langFr, true), (langUk, true), (langDe, true)); AssertPerCulture(content2, (x, c) => x.IsCultureEdited(c), (langFr, true), (langUk, true), (langDe, true)); - AssertPerCulture(content, (x, c) => x.GetCulturePublishDate(c) == DateTime.MinValue, (langUk, false)); // FR, DE would throw - AssertPerCulture(content2, (x, c) => x.GetCulturePublishDate(c) == DateTime.MinValue, (langUk, false)); // FR, DE would throw + AssertPerCulture(content, (x, c) => x.GetPublishDate(c) == DateTime.MinValue, (langUk, false)); // FR, DE would throw + AssertPerCulture(content2, (x, c) => x.GetPublishDate(c) == DateTime.MinValue, (langUk, false)); // FR, DE would throw // act @@ -2852,11 +2850,11 @@ namespace Umbraco.Tests.Services Assert.IsTrue(content2.Published); - Assert.AreEqual("Home US2", content2.Name); - Assert.AreEqual("name-fr2", content2.GetName(langFr.IsoCode)); - Assert.AreEqual("name-uk2", content2.GetName(langUk.IsoCode)); + Assert.AreEqual("name-fr2", content2.Name); // got the default culture name when saved + Assert.AreEqual("name-fr2", content2.GetCultureName(langFr.IsoCode)); + Assert.AreEqual("name-uk2", content2.GetCultureName(langUk.IsoCode)); - Assert.AreEqual("Home US", content2.PublishName); + Assert.AreEqual("name-fr2", content2.PublishName); Assert.IsNull(content2.GetPublishName(langFr.IsoCode)); Assert.AreEqual("name-uk", content2.GetPublishName(langUk.IsoCode)); @@ -2877,8 +2875,8 @@ namespace Umbraco.Tests.Services AssertPerCulture(content, (x, c) => x.IsCultureEdited(c), (langFr, true), (langUk, true), (langDe, true)); AssertPerCulture(content2, (x, c) => x.IsCultureEdited(c), (langFr, true), (langUk, true), (langDe, true)); - AssertPerCulture(content, (x, c) => x.GetCulturePublishDate(c) == DateTime.MinValue, (langUk, false)); // FR, DE would throw - AssertPerCulture(content2, (x, c) => x.GetCulturePublishDate(c) == DateTime.MinValue, (langUk, false)); // FR, DE would throw + AssertPerCulture(content, (x, c) => x.GetPublishDate(c) == DateTime.MinValue, (langUk, false)); // FR, DE would throw + AssertPerCulture(content2, (x, c) => x.GetPublishDate(c) == DateTime.MinValue, (langUk, false)); // FR, DE would throw // act @@ -2900,18 +2898,22 @@ namespace Umbraco.Tests.Services AssertPerCulture(content, (x, c) => x.IsCultureEdited(c), (langFr, true), (langUk, false), (langDe, true)); AssertPerCulture(content2, (x, c) => x.IsCultureEdited(c), (langFr, true), (langUk, false), (langDe, true)); - AssertPerCulture(content, (x, c) => x.GetCulturePublishDate(c) == DateTime.MinValue, (langUk, false)); // FR, DE would throw - AssertPerCulture(content2, (x, c) => x.GetCulturePublishDate(c) == DateTime.MinValue, (langUk, false)); // FR, DE would throw + AssertPerCulture(content, (x, c) => x.GetPublishDate(c) == DateTime.MinValue, (langUk, false)); // FR, DE would throw + AssertPerCulture(content2, (x, c) => x.GetPublishDate(c) == DateTime.MinValue, (langUk, false)); // FR, DE would throw // act - content.SetName("name-uk3", langUk.IsoCode); + content.SetCultureName("name-uk3", langUk.IsoCode); contentService.Save(content); content2 = contentService.GetById(content.Id); + // note that the 'edited' flags only change once saved - not immediately + // but they change, on what's being saved, and when getting it back + // changing the name = edited! + Assert.IsTrue(content.IsCultureEdited(langUk.IsoCode)); Assert.IsTrue(content2.IsCultureEdited(langUk.IsoCode)); } diff --git a/src/Umbraco.Tests/Services/EntityServiceTests.cs b/src/Umbraco.Tests/Services/EntityServiceTests.cs index 6abe5add94..74401c9318 100644 --- a/src/Umbraco.Tests/Services/EntityServiceTests.cs +++ b/src/Umbraco.Tests/Services/EntityServiceTests.cs @@ -450,19 +450,18 @@ namespace Umbraco.Tests.Services { var service = ServiceContext.EntityService; - var alias = "test" + Guid.NewGuid(); var contentType = MockedContentTypes.CreateSimpleContentType(alias, alias, false); - contentType.Variations = ContentVariation.CultureNeutral; + contentType.Variations = ContentVariation.Culture; ServiceContext.ContentTypeService.Save(contentType); var c1 = MockedContent.CreateSimpleContent(contentType, "Test", -1); - c1.SetName("Test - FR", _langFr.IsoCode); - c1.SetName("Test - ES", _langEs.IsoCode); + c1.SetCultureName("Test - FR", _langFr.IsoCode); + c1.SetCultureName("Test - ES", _langEs.IsoCode); ServiceContext.ContentService.Save(c1); var result = service.Get(c1.Id, UmbracoObjectTypes.Document); - Assert.AreEqual("Test", result.Name); + Assert.AreEqual("Test - FR", result.Name); // got name from default culture Assert.IsNotNull(result as IDocumentEntitySlim); var doc = (IDocumentEntitySlim)result; var cultureNames = doc.CultureNames; @@ -476,11 +475,11 @@ namespace Umbraco.Tests.Services var service = ServiceContext.EntityService; var contentType = MockedContentTypes.CreateSimpleContentType("test1", "Test1", false); - contentType.Variations = ContentVariation.CultureNeutral; + contentType.Variations = ContentVariation.Culture; ServiceContext.ContentTypeService.Save(contentType); var root = MockedContent.CreateSimpleContent(contentType); - root.SetName("Root", _langFr.IsoCode); // else cannot save + root.SetCultureName("Root", _langFr.IsoCode); // else cannot save ServiceContext.ContentService.Save(root); for (int i = 0; i < 10; i++) @@ -488,12 +487,12 @@ namespace Umbraco.Tests.Services var c1 = MockedContent.CreateSimpleContent(contentType, Guid.NewGuid().ToString(), root); if (i % 2 == 0) { - c1.SetName("Test " + i + " - FR", _langFr.IsoCode); - c1.SetName("Test " + i + " - ES", _langEs.IsoCode); + c1.SetCultureName("Test " + i + " - FR", _langFr.IsoCode); + c1.SetCultureName("Test " + i + " - ES", _langEs.IsoCode); } else { - c1.SetName("Test", _langFr.IsoCode); // else cannot save + c1.SetCultureName("Test", _langFr.IsoCode); // else cannot save } ServiceContext.ContentService.Save(c1); } diff --git a/src/Umbraco.Tests/Testing/TestingTests/MockTests.cs b/src/Umbraco.Tests/Testing/TestingTests/MockTests.cs index f21a953ae7..e79e504a69 100644 --- a/src/Umbraco.Tests/Testing/TestingTests/MockTests.cs +++ b/src/Umbraco.Tests/Testing/TestingTests/MockTests.cs @@ -83,8 +83,7 @@ namespace Umbraco.Tests.Testing.TestingTests var theUrlProvider = new UrlProvider(umbracoContext, new [] { urlProvider }, umbracoContext.VariationContextAccessor); - var contentType = new PublishedContentType(666, "alias", PublishedItemType.Content, Enumerable.Empty(), Enumerable.Empty(), - ContentVariation.InvariantNeutral); + var contentType = new PublishedContentType(666, "alias", PublishedItemType.Content, Enumerable.Empty(), Enumerable.Empty(), ContentVariation.Nothing); var publishedContent = Mock.Of(); Mock.Get(publishedContent).Setup(x => x.ContentType).Returns(contentType); diff --git a/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs b/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs index 585e9a17d6..dedf04488c 100644 --- a/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs +++ b/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs @@ -59,7 +59,7 @@ namespace Umbraco.Tests.UmbracoExamine m.CreateDate == (DateTime)x.Attribute("createDate") && m.UpdateDate == (DateTime)x.Attribute("updateDate") && m.Name == (string)x.Attribute("nodeName") && - m.GetName(It.IsAny()) == (string)x.Attribute("nodeName") && + m.GetCultureName(It.IsAny()) == (string)x.Attribute("nodeName") && m.Path == (string)x.Attribute("path") && m.Properties == new PropertyCollection() && m.ContentType == Mock.Of(mt => @@ -103,7 +103,7 @@ namespace Umbraco.Tests.UmbracoExamine m.CreateDate == (DateTime) x.Attribute("createDate") && m.UpdateDate == (DateTime) x.Attribute("updateDate") && m.Name == (string) x.Attribute("nodeName") && - m.GetName(It.IsAny()) == (string)x.Attribute("nodeName") && + m.GetCultureName(It.IsAny()) == (string)x.Attribute("nodeName") && m.Path == (string) x.Attribute("path") && m.Properties == new PropertyCollection() && m.ContentType == Mock.Of(mt => diff --git a/src/Umbraco.Tests/UmbracoExamine/SearchTests.cs b/src/Umbraco.Tests/UmbracoExamine/SearchTests.cs index e08c6c5f05..2d440b8453 100644 --- a/src/Umbraco.Tests/UmbracoExamine/SearchTests.cs +++ b/src/Umbraco.Tests/UmbracoExamine/SearchTests.cs @@ -38,7 +38,7 @@ namespace Umbraco.Tests.UmbracoExamine m.CreateDate == (DateTime)x.Attribute("createDate") && m.UpdateDate == (DateTime)x.Attribute("updateDate") && m.Name == (string)x.Attribute("nodeName") && - m.GetName(It.IsAny()) == (string)x.Attribute("nodeName") && + m.GetCultureName(It.IsAny()) == (string)x.Attribute("nodeName") && m.Path == (string)x.Attribute("path") && m.Properties == new PropertyCollection() && m.Published == true && diff --git a/src/Umbraco.Tests/Web/TemplateUtilitiesTests.cs b/src/Umbraco.Tests/Web/TemplateUtilitiesTests.cs index c6f7d8551e..0a9638fc30 100644 --- a/src/Umbraco.Tests/Web/TemplateUtilitiesTests.cs +++ b/src/Umbraco.Tests/Web/TemplateUtilitiesTests.cs @@ -82,7 +82,7 @@ namespace Umbraco.Tests.Web var globalSettings = SettingsForTests.GenerateMockGlobalSettings(); - var contentType = new PublishedContentType(666, "alias", PublishedItemType.Content, Enumerable.Empty(), Enumerable.Empty(), ContentVariation.InvariantNeutral); + var contentType = new PublishedContentType(666, "alias", PublishedItemType.Content, Enumerable.Empty(), Enumerable.Empty(), ContentVariation.Nothing); var publishedContent = Mock.Of(); Mock.Get(publishedContent).Setup(x => x.Id).Returns(1234); Mock.Get(publishedContent).Setup(x => x.ContentType).Returns(contentType); diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index 184c442568..75b22d5404 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -74,7 +74,7 @@ namespace Umbraco.Web.Editors public bool AllowsCultureVariation() { var contentTypes = Services.ContentTypeService.GetAll(); - return contentTypes.Any(contentType => contentType.Variations.DoesSupportCulture()); + return contentTypes.Any(contentType => contentType.VariesByCulture()); } /// @@ -310,7 +310,7 @@ namespace Umbraco.Web.Editors var containerTab = mapped.Tabs.FirstOrDefault(x => x.Alias == Constants.Conventions.PropertyGroups.ListViewGroupName); mapped.Tabs = mapped.Tabs.Except(new[] { containerTab }); - if (contentType.Variations.Has(ContentVariation.CultureNeutral)) + if (contentType.VariesByCulture()) { //Remove all variants except for the default since currently the default must be saved before other variants can be edited //TODO: Allow for editing all variants at once ... this will be a future task @@ -709,7 +709,7 @@ namespace Umbraco.Web.Editors /// private void PublishInternal(ContentItemSave contentItem, ref PublishResult publishStatus, ref bool wasCancelled) { - if (!contentItem.PersistedContent.ContentType.Variations.Has(ContentVariation.CultureNeutral)) + if (!contentItem.PersistedContent.ContentType.VariesByCulture()) { //its invariant, proceed normally contentItem.PersistedContent.TryPublishValues(); @@ -1037,13 +1037,13 @@ namespace Umbraco.Web.Editors private void MapPropertyValues(ContentItemSave contentItem) { //Don't update the name if it is empty - if (contentItem.Name.IsNullOrWhiteSpace() == false) + if (!contentItem.Name.IsNullOrWhiteSpace()) { - //set the name according to the culture settings - if (contentItem.PersistedContent.ContentType.Variations.HasFlag(ContentVariation.CultureNeutral)) + if (contentItem.PersistedContent.ContentType.VariesByCulture()) { - if (contentItem.Culture.IsNullOrWhiteSpace()) throw new InvalidOperationException($"Cannot save a content item that is {ContentVariation.CultureNeutral} with a culture specified"); - contentItem.PersistedContent.SetName(contentItem.Name, contentItem.Culture); + if (contentItem.Culture.IsNullOrWhiteSpace()) + throw new InvalidOperationException($"Cannot set culture name without a culture."); + contentItem.PersistedContent.SetCultureName(contentItem.Name, contentItem.Culture); } else { @@ -1074,7 +1074,7 @@ namespace Umbraco.Web.Editors } } - bool Varies(Property property) => property.PropertyType.Variations.Has(ContentVariation.CultureNeutral); + bool Varies(Property property) => property.PropertyType.VariesByCulture(); MapPropertyValues( contentItem, @@ -1276,7 +1276,7 @@ namespace Umbraco.Web.Editors { //A culture must exist in the mapping context if this content type is CultureNeutral since for a culture variant to be edited, // the Cuture property of ContentItemDisplay must exist (at least currently). - if (culture == null && content.ContentType.Variations.Has(ContentVariation.CultureNeutral)) + if (culture == null && content.ContentType.VariesByCulture()) { //If a culture is not explicitly sent up, then it means that the user is editing the default variant language. culture = Services.LocalizationService.GetDefaultLanguageIsoCode(); diff --git a/src/Umbraco.Web/Models/Mapping/ContentItemDisplayNameResolver.cs b/src/Umbraco.Web/Models/Mapping/ContentItemDisplayNameResolver.cs index 2e8155e1a7..101fed8a06 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentItemDisplayNameResolver.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentItemDisplayNameResolver.cs @@ -14,12 +14,9 @@ namespace Umbraco.Web.Models.Mapping public string Resolve(IContent source, ContentItemDisplay destination, string destMember, ResolutionContext context) { var culture = context.GetCulture(); - if (culture != null && source.ContentType.Variations.Has(ContentVariation.CultureNeutral)) - { - //return the culture name being requested - return source.GetName(culture); - } - return source.Name; + return source.ContentType.VariesByCulture() && culture != null + ? source.GetCultureName(culture) + : source.Name; } } } diff --git a/src/Umbraco.Web/Models/Mapping/ContentItemDisplayVariationResolver.cs b/src/Umbraco.Web/Models/Mapping/ContentItemDisplayVariationResolver.cs index d1673c2a5b..cb6e2938be 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentItemDisplayVariationResolver.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentItemDisplayVariationResolver.cs @@ -25,7 +25,7 @@ namespace Umbraco.Web.Models.Mapping public IEnumerable Resolve(IContent source, ContentItemDisplay destination, IEnumerable destMember, ResolutionContext context) { - if (!source.ContentType.Variations.Has(Core.Models.ContentVariation.CultureNeutral)) + if (!source.ContentType.VariesByCulture()) return Enumerable.Empty(); var allLanguages = _localizationService.GetAllLanguages().OrderBy(x => x.Id).ToList(); @@ -36,7 +36,7 @@ namespace Umbraco.Web.Models.Mapping { Language = x, Mandatory = x.Mandatory, - Name = source.GetName(x.IsoCode), + Name = source.GetCultureName(x.IsoCode), Exists = source.IsCultureAvailable(x.IsoCode), // segments ?? PublishedState = (source.PublishedState == PublishedState.Unpublished //if the entire document is unpublished, then flag every variant as unpublished ? PublishedState.Unpublished diff --git a/src/Umbraco.Web/Models/Mapping/ContentPropertyBasicConverter.cs b/src/Umbraco.Web/Models/Mapping/ContentPropertyBasicConverter.cs index 5353614033..ffcd39856e 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentPropertyBasicConverter.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentPropertyBasicConverter.cs @@ -68,11 +68,11 @@ namespace Umbraco.Web.Models.Mapping var culture = context.GetCulture(); //a culture needs to be in the context for a property type that can vary - if (culture == null && property.PropertyType.Variations.Has(ContentVariation.CultureNeutral)) + if (culture == null && property.PropertyType.VariesByCulture()) throw new InvalidOperationException($"No languageId found in mapping operation when one is required for the culture neutral property type {property.PropertyType.Alias}"); //set the culture to null if it's an invariant property type - culture = !property.PropertyType.Variations.Has(ContentVariation.CultureNeutral) ? null : culture; + culture = !property.PropertyType.VariesByCulture() ? null : culture; // if no 'IncludeProperties' were specified or this property is set to be included - we will map the value and return. result.Value = editor.GetValueEditor().ToEditor(property, DataTypeService, culture); diff --git a/src/Umbraco.Web/Models/Mapping/ContentTypeMapperProfile.cs b/src/Umbraco.Web/Models/Mapping/ContentTypeMapperProfile.cs index 2ee9e38ff5..407fd64372 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentTypeMapperProfile.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentTypeMapperProfile.cs @@ -112,7 +112,7 @@ namespace Umbraco.Web.Models.Mapping .ForMember(dto => dto.AllowedTemplates, opt => opt.Ignore()) .ForMember(dto => dto.DefaultTemplate, opt => opt.Ignore()) .ForMember(display => display.Notifications, opt => opt.Ignore()) - .ForMember(display => display.AllowCultureVariant, opt => opt.MapFrom(type => type.Variations.HasFlag(ContentVariation.CultureNeutral))) + .ForMember(display => display.AllowCultureVariant, opt => opt.MapFrom(type => type.VariesByCulture())) .AfterMap((source, dest) => { //sync templates diff --git a/src/Umbraco.Web/Models/Mapping/ContentTypeVariationsResolver.cs b/src/Umbraco.Web/Models/Mapping/ContentTypeVariationsResolver.cs index fcfe9a47cc..e25568868f 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentTypeVariationsResolver.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentTypeVariationsResolver.cs @@ -13,12 +13,10 @@ namespace Umbraco.Web.Models.Mapping public ContentVariation Resolve(TSource source, TDestination destination, ContentVariation destMember, ResolutionContext context) { //this will always be the case, a content type will always be allowed to be invariant - var result = ContentVariation.InvariantNeutral; + var result = ContentVariation.Nothing; if (source.AllowCultureVariant) - { - result |= ContentVariation.CultureNeutral; - } + result |= ContentVariation.Culture; return result; } diff --git a/src/Umbraco.Web/Models/Mapping/PropertyTypeGroupResolver.cs b/src/Umbraco.Web/Models/Mapping/PropertyTypeGroupResolver.cs index 63c8c5e97a..fbd7be4ecd 100644 --- a/src/Umbraco.Web/Models/Mapping/PropertyTypeGroupResolver.cs +++ b/src/Umbraco.Web/Models/Mapping/PropertyTypeGroupResolver.cs @@ -221,7 +221,7 @@ namespace Umbraco.Web.Models.Mapping SortOrder = p.SortOrder, ContentTypeId = contentType.Id, ContentTypeName = contentType.Name, - AllowCultureVariant = p.Variations.HasFlag(Core.Models.ContentVariation.CultureNeutral) + AllowCultureVariant = p.VariesByCulture() }); } diff --git a/src/Umbraco.Web/Models/Mapping/PropertyTypeVariationsResolver.cs b/src/Umbraco.Web/Models/Mapping/PropertyTypeVariationsResolver.cs index 6c0fa9915e..00472a291c 100644 --- a/src/Umbraco.Web/Models/Mapping/PropertyTypeVariationsResolver.cs +++ b/src/Umbraco.Web/Models/Mapping/PropertyTypeVariationsResolver.cs @@ -12,12 +12,7 @@ namespace Umbraco.Web.Models.Mapping { public ContentVariation Resolve(PropertyTypeBasic source, PropertyType destination, ContentVariation destMember, ResolutionContext context) { - //A property type should only be one type of culture variation. - //If a property type allows both variant and invariant then it generally won't be able to save because validation - //occurs when performing something like IContent.TryPublishAllValues and it will result in validation problems because - //the invariant value will not be set since in the UI only the variant values are edited if it supports it. - var result = source.AllowCultureVariant ? ContentVariation.CultureNeutral : ContentVariation.InvariantNeutral; - return result; + return source.AllowCultureVariant ? ContentVariation.Culture : ContentVariation.Nothing; } } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/Property.cs b/src/Umbraco.Web/PublishedCache/NuCache/Property.cs index 39fb005ba3..2d24efdd67 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/Property.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/Property.cs @@ -191,10 +191,9 @@ namespace Umbraco.Web.PublishedCache.NuCache if (culture != null && segment != null) return; // use context values - // fixme CultureSegment? var publishedVariationContext = _content.VariationContextAccessor?.VariationContext; - if (culture == null) culture = _variations.Has(ContentVariation.CultureNeutral) ? publishedVariationContext?.Culture : ""; - if (segment == null) segment = _variations.Has(ContentVariation.CultureNeutral) ? publishedVariationContext?.Segment : ""; + if (culture == null) culture = _variations.VariesByCulture() ? publishedVariationContext?.Culture : ""; + if (segment == null) segment = _variations.VariesBySegment() ? publishedVariationContext?.Segment : ""; } public override object GetValue(string culture = null, string segment = null) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedContent.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedContent.cs index 567201ef6f..f8e8cdb974 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedContent.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedContent.cs @@ -178,7 +178,7 @@ namespace Umbraco.Web.PublishedCache.NuCache { get { - if (!ContentType.Variations.Has(ContentVariation.CultureNeutral)) // fixme CultureSegment? + if (!ContentType.VariesByCulture()) return _contentData.Name; var culture = VariationContextAccessor.VariationContext.Culture; @@ -194,7 +194,7 @@ namespace Umbraco.Web.PublishedCache.NuCache { get { - if (!ContentType.Variations.Has(ContentVariation.CultureNeutral)) // fixme CultureSegment? + if (!ContentType.VariesByCulture()) return _urlSegment; var culture = VariationContextAccessor.VariationContext.Culture; @@ -258,7 +258,7 @@ namespace Umbraco.Web.PublishedCache.NuCache { get { - if (!ContentType.Variations.Has(ContentVariation.CultureNeutral)) // fixme CultureSegment? + if (!ContentType.VariesByCulture()) return NoCultureInfos; if (_cultureInfos != null) return _cultureInfos; diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs index fca9458565..34efa7136a 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs @@ -1188,13 +1188,13 @@ namespace Umbraco.Web.PublishedCache.NuCache var names = content is IContent document ? (published - ? document.PublishCultureNames + ? document.PublishNames : document.CultureNames) : content.CultureNames; foreach (var (culture, name) in names) { - cultureData[culture] = new CultureVariation { Name = name, Date = content.GetCultureDate(culture) }; + cultureData[culture] = new CultureVariation { Name = name, Date = content.GetCultureDate(culture) ?? DateTime.MinValue }; } //the dictionary that will be serialized diff --git a/src/Umbraco.Web/PublishedContentExtensions.cs b/src/Umbraco.Web/PublishedContentExtensions.cs index 1d3c747b6e..5e1708f118 100644 --- a/src/Umbraco.Web/PublishedContentExtensions.cs +++ b/src/Umbraco.Web/PublishedContentExtensions.cs @@ -71,7 +71,7 @@ namespace Umbraco.Web public static string GetUrlSegment(this IPublishedContent content, string culture = null) { // for invariant content, return the invariant url segment - if (!content.ContentType.Variations.Has(ContentVariation.CultureNeutral)) + if (!content.ContentType.VariesByCulture()) return content.UrlSegment; // content.GetCulture(culture) will use the 'current' culture (via accessor) in case 'culture' diff --git a/src/Umbraco.Web/Routing/DefaultUrlProvider.cs b/src/Umbraco.Web/Routing/DefaultUrlProvider.cs index 7ea359fa67..9e89459774 100644 --- a/src/Umbraco.Web/Routing/DefaultUrlProvider.cs +++ b/src/Umbraco.Web/Routing/DefaultUrlProvider.cs @@ -87,7 +87,7 @@ namespace Umbraco.Web.Routing while (domainUris == null && n != null) // n is null at root { n = n.Parent; // move to parent node - domainUris = n == null ? null : domainHelper.DomainsForNode(n.Id, current, false); + domainUris = n == null ? null : domainHelper.DomainsForNode(n.Id, current, excludeDefault: true); } // no domains = exit diff --git a/src/Umbraco.Web/Routing/DomainHelper.cs b/src/Umbraco.Web/Routing/DomainHelper.cs index 67bd27f959..b6d79e788a 100644 --- a/src/Umbraco.Web/Routing/DomainHelper.cs +++ b/src/Umbraco.Web/Routing/DomainHelper.cs @@ -135,8 +135,8 @@ namespace Umbraco.Web.Routing return null; // sanitize cultures - culture = culture.NullEmpty(); - defaultCulture = defaultCulture.NullEmpty(); + culture = culture.NullOrWhiteSpaceAsNull(); + defaultCulture = defaultCulture.NullOrWhiteSpaceAsNull(); if (uri == null) { diff --git a/src/Umbraco.Web/Routing/PublishedRouter.cs b/src/Umbraco.Web/Routing/PublishedRouter.cs index 1cc487fd99..4c137b2d81 100644 --- a/src/Umbraco.Web/Routing/PublishedRouter.cs +++ b/src/Umbraco.Web/Routing/PublishedRouter.cs @@ -295,7 +295,7 @@ namespace Umbraco.Web.Routing return false; // invariant - always published - if (!domainDocument.ContentType.Variations.Has(ContentVariation.CultureNeutral)) + if (!domainDocument.ContentType.VariesByCulture()) return true; // variant, ensure that the culture corresponding to the domain's language is published diff --git a/src/Umbraco.Web/Routing/UrlProviderExtensions.cs b/src/Umbraco.Web/Routing/UrlProviderExtensions.cs index 86fc51fe44..b7e26f894e 100644 --- a/src/Umbraco.Web/Routing/UrlProviderExtensions.cs +++ b/src/Umbraco.Web/Routing/UrlProviderExtensions.cs @@ -40,7 +40,7 @@ namespace Umbraco.Web.Routing // if content is variant, go with the current culture - and that is NOT safe, there may be // no 'main' url for that culture, deal with it later - otherwise, go with the invariant // culture, and that is safe. - var varies = content.ContentType.Variations.DoesSupportCulture(); + var varies = content.ContentType.VariesByCulture(); var culture = varies ? Thread.CurrentThread.CurrentUICulture.Name : ""; if (content.Published == false) diff --git a/src/Umbraco.Web/Trees/ContentTreeController.cs b/src/Umbraco.Web/Trees/ContentTreeController.cs index 3e24e037df..4e032b8b25 100644 --- a/src/Umbraco.Web/Trees/ContentTreeController.cs +++ b/src/Umbraco.Web/Trees/ContentTreeController.cs @@ -264,7 +264,7 @@ namespace Umbraco.Web.Trees // for those items that DO support cultures, we need to get the proper name, IF it exists // otherwise, invariant is fine - if (docEntity.Variations.Has(Core.Models.ContentVariation.CultureNeutral) && + if (docEntity.Variations.VariesByCulture() && docEntity.CultureNames.TryGetValue(culture, out var name) && !string.IsNullOrWhiteSpace(name)) { diff --git a/src/Umbraco.Web/WebApi/Binders/ContentItemBinder.cs b/src/Umbraco.Web/WebApi/Binders/ContentItemBinder.cs index da7f723e12..1df1445979 100644 --- a/src/Umbraco.Web/WebApi/Binders/ContentItemBinder.cs +++ b/src/Umbraco.Web/WebApi/Binders/ContentItemBinder.cs @@ -58,10 +58,10 @@ namespace Umbraco.Web.WebApi.Binders protected override bool ValidateCultureVariant(ContentItemSave postedItem, HttpActionContext actionContext) { var contentType = postedItem.PersistedContent.GetContentType(); - if (contentType.Variations.DoesSupportCulture() && postedItem.Culture.IsNullOrWhiteSpace()) + if (contentType.VariesByCulture() && postedItem.Culture.IsNullOrWhiteSpace()) { //we cannot save a content item that is culture variant if no culture was specified in the request! - actionContext.Response = actionContext.Request.CreateValidationErrorResponse($"No 'Culture' found in request. Cannot save a content item that is of a {Core.Models.ContentVariation.CultureNeutral} content type without a specified culture."); + actionContext.Response = actionContext.Request.CreateValidationErrorResponse($"No culture found in request. Cannot save a content item that varies by culture, without a specified culture."); return false; } return true; diff --git a/src/Umbraco.Web/umbraco.presentation/page.cs b/src/Umbraco.Web/umbraco.presentation/page.cs index d61d21700a..a1da34d46e 100644 --- a/src/Umbraco.Web/umbraco.presentation/page.cs +++ b/src/Umbraco.Web/umbraco.presentation/page.cs @@ -478,14 +478,14 @@ namespace umbraco { get { - if (!_inner.ContentType.Variations.HasFlag(ContentVariation.CultureNeutral)) // fixme CultureSegment? + if (!_inner.ContentType.VariesByCulture()) return NoCultureInfos; if (_cultureInfos != null) return _cultureInfos; - return _cultureInfos = _inner.PublishCultureNames - .ToDictionary(x => x.Key, x => new PublishedCultureInfo(x.Key, x.Value, _inner.GetCulturePublishDate(x.Key))); + return _cultureInfos = _inner.PublishNames + .ToDictionary(x => x.Key, x => new PublishedCultureInfo(x.Key, x.Value, _inner.GetPublishDate(x.Key) ?? DateTime.MinValue)); } } From 570ca464e7b2ae42f9605d7b161d28df7edc7e86 Mon Sep 17 00:00:00 2001 From: Stephan Date: Wed, 20 Jun 2018 15:29:44 +0200 Subject: [PATCH 05/14] Migrate variations --- .../Migrations/Upgrade/UmbracoPlan.cs | 8 ++- .../V_8_0_0/ContentVariationMigration.cs | 63 +++++++++++++++++++ src/Umbraco.Core/Umbraco.Core.csproj | 3 +- 3 files changed, 71 insertions(+), 3 deletions(-) create mode 100644 src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/ContentVariationMigration.cs diff --git a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs index b7a77b10ce..698b77f8bf 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs @@ -121,9 +121,10 @@ namespace Umbraco.Core.Migrations.Upgrade Chain("{083A9894-903D-41B7-B6B3-9EAF2D4CCED0}"); Chain("{42097524-0F8C-482C-BD79-AC7407D8A028}"); Chain("{3608CD41-792A-4E9A-A97D-42A5E797EE31}"); + Chain("{608A02B8-B1A1-4C24-8955-0B95DB1F567E}"); // must chain to v8 final state (see at end of file) - Chain("{4CACE351-C6B9-4F0C-A6BA-85A02BBD39E4}"); + Chain("{1350617A-4930-4D61-852F-E3AA9E692173}"); // UPGRADE FROM 7, MORE RECENT @@ -221,10 +222,13 @@ namespace Umbraco.Core.Migrations.Upgrade //Chain("{65D6B71C-BDD5-4A2E-8D35-8896325E9151}"); // stephan added that one - need a path to final state Add("{65D6B71C-BDD5-4A2E-8D35-8896325E9151}", "{4CACE351-C6B9-4F0C-A6BA-85A02BBD39E4}"); + // 8.0.0 + Chain("{1350617A-4930-4D61-852F-E3AA9E692173}"); + // FINAL STATE - MUST MATCH LAST ONE ABOVE ! // whenever this changes, update all references in this file! - Add(string.Empty, "{4CACE351-C6B9-4F0C-A6BA-85A02BBD39E4}"); + Add(string.Empty, "{1350617A-4930-4D61-852F-E3AA9E692173}"); } } } diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/ContentVariationMigration.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/ContentVariationMigration.cs new file mode 100644 index 0000000000..0d64061058 --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/ContentVariationMigration.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.Dtos; + +namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 +{ + public class ContentVariationMigration : MigrationBase + { + public ContentVariationMigration(IMigrationContext context) + : base(context) + { } + + public override void Migrate() + { + byte GetNewValue(byte oldValue) + { + switch (oldValue) + { + case 0: // Unknown + case 1: // InvariantNeutral + return 0; // Unknown + case 2: // CultureNeutral + case 3: // CultureNeutral | InvariantNeutral + return 1; // Culture + case 4: // InvariantSegment + case 5: // InvariantSegment | InvariantNeutral + return 2; // Segment + case 6: // InvariantSegment | CultureNeutral + case 7: // InvariantSegment | CultureNeutral | InvariantNeutral + case 8: // CultureSegment + case 9: // CultureSegment | InvariantNeutral + case 10: // CultureSegment | CultureNeutral + case 11: // CultureSegment | CultureNeutral | InvariantNeutral + case 12: // etc + case 13: + case 14: + case 15: + return 3; // Culture | Segment + default: + throw new NotSupportedException($"Invalid value {oldValue}."); + } + } + + var propertyTypes = Database.Fetch(Sql().Select().From()); + foreach (var dto in propertyTypes) + { + dto.Variations = GetNewValue(dto.Variations); + Database.Update(dto); + } + + var contentTypes = Database.Fetch(Sql().Select().From()); + foreach (var dto in contentTypes) + { + dto.Variations = GetNewValue(dto.Variations); + Database.Update(dto); + } + } + } +} diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 6bd78044f8..f4afd9a65b 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -1097,6 +1097,7 @@ + @@ -1520,4 +1521,4 @@ - \ No newline at end of file + From 8df060fb00181ba34677cef0cac765a1107b91c3 Mon Sep 17 00:00:00 2001 From: Stephan Date: Wed, 20 Jun 2018 15:38:04 +0200 Subject: [PATCH 06/14] Fix migration --- .../Migrations/Upgrade/V_8_0_0/ContentVariationMigration.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/ContentVariationMigration.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/ContentVariationMigration.cs index 0d64061058..23c835a327 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/ContentVariationMigration.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/ContentVariationMigration.cs @@ -45,7 +45,7 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 } } - var propertyTypes = Database.Fetch(Sql().Select().From()); + var propertyTypes = Database.Fetch(Sql().Select().From()); foreach (var dto in propertyTypes) { dto.Variations = GetNewValue(dto.Variations); From b3696c29cc3ea02bd73558a7fb5ad420a961f7be Mon Sep 17 00:00:00 2001 From: Stephan Date: Fri, 22 Jun 2018 21:03:47 +0200 Subject: [PATCH 07/14] Refactor Variants at service level --- src/Umbraco.Core/Models/Content.cs | 229 ++-------- src/Umbraco.Core/Models/ContentBase.cs | 104 ++++- src/Umbraco.Core/Models/EntityExtensions.cs | 9 - src/Umbraco.Core/Models/IContent.cs | 94 +--- src/Umbraco.Core/Models/IContentBase.cs | 41 +- src/Umbraco.Core/Models/Property.cs | 174 +------- .../Implement/DocumentRepository.cs | 2 +- .../Publishing/ScheduledPublisher.cs | 4 +- src/Umbraco.Core/Services/IContentService.cs | 37 +- .../Services/Implement/ContentService.cs | 410 ++++++++++-------- .../Services/Implement/MediaService.cs | 6 +- .../Integration/ContentEventsTests.cs | 247 +++++------ src/Umbraco.Tests/Models/VariationTests.cs | 44 +- .../NPocoTests/PetaPocoCachesTest.cs | 4 +- .../Repositories/ContentRepositoryTest.cs | 8 +- .../Scoping/ScopedNuCacheTests.cs | 2 - src/Umbraco.Tests/Scoping/ScopedXmlTests.cs | 4 - .../Services/ContentServiceTests.cs | 163 ++++--- .../Services/ContentTypeServiceTests.cs | 10 - .../Services/PerformanceTests.cs | 4 +- src/Umbraco.Tests/Services/TagServiceTests.cs | 7 - .../Umbraco/dialogs/ChangeDocType.aspx.cs | 2 +- src/Umbraco.Web/Editors/ContentController.cs | 17 +- .../WebServices/BulkPublishController.cs | 4 +- 24 files changed, 687 insertions(+), 939 deletions(-) diff --git a/src/Umbraco.Core/Models/Content.cs b/src/Umbraco.Core/Models/Content.cs index d670d3a588..47358d39be 100644 --- a/src/Umbraco.Core/Models/Content.cs +++ b/src/Umbraco.Core/Models/Content.cs @@ -21,6 +21,7 @@ namespace Umbraco.Core.Models private DateTime? _releaseDate; private DateTime? _expireDate; private Dictionary _publishInfos; + private Dictionary _publishInfosOrig; private HashSet _editedCultures; private static readonly Lazy Ps = new Lazy(); @@ -216,6 +217,10 @@ namespace Umbraco.Core.Models public bool IsCulturePublished(string culture) => _publishInfos != null && _publishInfos.ContainsKey(culture); + /// + public bool WasCulturePublished(string culture) + => _publishInfosOrig != null && _publishInfosOrig.ContainsKey(culture); + /// public bool IsCultureEdited(string culture) => !IsCulturePublished(culture) || (_editedCultures != null && _editedCultures.Contains(culture)); @@ -272,28 +277,6 @@ namespace Umbraco.Core.Models if (_publishInfos.Count == 0) _publishInfos = null; } - // fixme DOCUMENT - // sets publish name - used by persistence - //internal void UpdatePublishName(string culture, string name) - //{ - // if (string.IsNullOrWhiteSpace(name)) - // throw new ArgumentNullOrEmptyException(nameof(name)); - - // if (culture == null) // fixme that should NOT happen - // { - // PublishName = name; - // } - // else - // { - // // private method, assume that culture is valid - - // if (_publishInfos == null || !_publishInfos.TryGetValue(culture, out var pi)) - // throw new InvalidOperationException("Culture is not published."); - - // _publishInfos[culture] = (name, pi.Date); - // } - //} - // sets a publish edited internal void SetCultureEdited(string culture) { @@ -325,205 +308,73 @@ namespace Umbraco.Core.Models public bool Blueprint { get; internal set; } /// - public virtual bool TryPublishValues(string culture = null, string segment = null) // fixme should it default to "*", "*" instead? + public virtual bool PublishCulture(string culture = "*") { culture = culture.NullOrWhiteSpaceAsNull(); - segment = segment.NullOrWhiteSpaceAsNull(); // the variation should be supported by the content type properties - if (!ContentType.SupportsPropertyVariation(culture, segment, true)) - throw new NotSupportedException($"Content type \"{ContentType.Alias}\" with variation \"{ContentType.Variations}\" does not support \"{culture??""},{segment??""}\"."); + // if the content type is invariant, only '*' and 'null' is ok + // if the content type varies, everything is ok because some properties may be invariant + if (!ContentType.SupportsPropertyVariation(culture, "*", true)) + throw new NotSupportedException($"Culture \"{culture}\" is not supported by content type \"{ContentType.Alias}\" with variation \"{ContentType.Variations}\"."); // the values we want to publish should be valid - if (ValidateProperties(culture, segment).Any()) + if (ValidateProperties(culture).Any()) return false; // fixme - should return an attempt with error results - // when explicitely publishing values for a given segment, values for the segment are published, - // but that does not change which cultures are published. to publish a culture, publish 'null' or - // '*', and then we need to deal with culture names - if (segment == null || segment == "*") + var alsoInvariant = false; + if (culture == "*") // all cultures { - // do not deal with invariant culture - it is always published - - if (culture == "*") // all cultures + foreach (var c in AvailableCultures) { - foreach (var c in AvailableCultures) - { - var name = GetCultureName(c); - if (string.IsNullOrWhiteSpace(name)) - return false; - SetPublishInfo(c, name, DateTime.Now); - } - } - else if (!culture.IsNullOrWhiteSpace()) // one single culture - { - var name = GetCultureName(culture); + var name = GetCultureName(c); if (string.IsNullOrWhiteSpace(name)) return false; - SetPublishInfo(culture, name, DateTime.Now); + SetPublishInfo(c, name, DateTime.Now); } } + else // one single culture + { + var name = GetCultureName(culture); + if (string.IsNullOrWhiteSpace(name)) + return false; + SetPublishInfo(culture, name, DateTime.Now); + alsoInvariant = true; // we also want to publish invariant values + } // property.PublishValues only publishes what is valid, variation-wise foreach (var property in Properties) - property.PublishValues(culture, segment); + { + property.PublishValues(culture); + if (alsoInvariant) + property.PublishValues(null); + } _publishedState = PublishedState.Publishing; return true; } /// - public virtual void ClearPublishedValues(string culture = null, string segment = null) // fixme should it default to "*", "*" instead? + public virtual void UnpublishCulture(string culture = "*") { culture = culture.NullOrWhiteSpaceAsNull(); - segment = segment.NullOrWhiteSpaceAsNull(); // the variation should be supported by the content type properties - if (!ContentType.SupportsPropertyVariation(culture, segment, true)) - throw new NotSupportedException($"Content type \"{ContentType.Alias}\" with variation \"{ContentType.Variations}\" does not support \"{culture??""},{segment??""}\"."); + if (!ContentType.SupportsPropertyVariation(culture, "*", true)) + throw new NotSupportedException($"Culture \"{culture}\" is not supported by content type \"{ContentType.Alias}\" with variation \"{ContentType.Variations}\"."); - // when explicitely clearing values for a given segment, values for the segment are cleared, - // but that does not change which cultures are published. to unpublish a culture, clear 'null' or - // '*', and then we need to deal with culture names - if (segment == null || segment == "*") - { - // do not deal with invariant culture - it cannot be unpublished - // fixme - so should we throw instead? - - if (culture == "*") // all cultures - ClearPublishInfos(); - else if (!culture.IsNullOrWhiteSpace()) // one single culture - ClearPublishInfo(culture); - } + if (culture == "*") // all cultures + ClearPublishInfos(); + else // one single culture + ClearPublishInfo(culture); // property.PublishValues only publishes what is valid, variation-wise foreach (var property in Properties) - property.ClearPublishedValues(culture, segment); + property.UnpublishValues(culture); _publishedState = PublishedState.Publishing; } - private bool CopyingFromSelf(IContent other) - { - // copying from the same Id and VersionPk - return Id == other.Id && VersionId == other.VersionId; - } - - // fixme must ALSO refactor copyValues - - /// - public virtual void CopyAllValues(IContent other) - { - if (other.ContentTypeId != ContentTypeId) - throw new InvalidOperationException("Cannot copy values from a different content type."); - - // we could copy from another document entirely, - // or from another version of the same document, - // in which case there is a special case. - var published = CopyingFromSelf(other); - - // note: use property.SetValue(), don't assign pvalue.EditValue, else change tracking fails - - // clear all existing properties - foreach (var property in Properties) - foreach (var pvalue in property.Values) - if (property.PropertyType.SupportsVariation(pvalue.Culture, pvalue.Segment)) - property.SetValue(null, pvalue.Culture, pvalue.Segment); - - // copy other properties - var otherProperties = other.Properties; - foreach (var otherProperty in otherProperties) - { - var alias = otherProperty.PropertyType.Alias; - foreach (var pvalue in otherProperty.Values) - { - if (!otherProperty.PropertyType.SupportsVariation(pvalue.Culture, pvalue.Segment)) - continue; - var value = published ? pvalue.PublishedValue : pvalue.EditedValue; - SetValue(alias, value, pvalue.Culture, pvalue.Segment); - } - } - - // copy names - ClearCultureInfos(); - foreach (var (culture, name) in other.CultureNames) - SetCultureName(name, culture); - Name = other.Name; - } - - /// - public virtual void CopyValues(IContent other, string culture = null, string segment = null) - { - if (other.ContentTypeId != ContentTypeId) - throw new InvalidOperationException("Cannot copy values from a different content type."); - - var published = CopyingFromSelf(other); - - // segment is invariant in comparisons - segment = segment?.ToLowerInvariant(); - - // note: use property.SetValue(), don't assign pvalue.EditValue, else change tracking fails - - // clear all existing properties - foreach (var property in Properties) - { - if (!property.PropertyType.SupportsVariation(culture, segment)) - continue; - - foreach (var pvalue in property.Values) - if (pvalue.Culture.InvariantEquals(culture) && pvalue.Segment.InvariantEquals(segment)) - property.SetValue(null, pvalue.Culture, pvalue.Segment); - } - - // copy other properties - var otherProperties = other.Properties; - foreach (var otherProperty in otherProperties) - { - if (!otherProperty.PropertyType.SupportsVariation(culture, segment)) - continue; - - var alias = otherProperty.PropertyType.Alias; - SetValue(alias, otherProperty.GetValue(culture, segment, published), culture, segment); - } - - // copy name - SetCultureName(other.GetCultureName(culture), culture); - } - - /// - public virtual void CopyCultureValues(IContent other, string culture = null) - { - if (other.ContentTypeId != ContentTypeId) - throw new InvalidOperationException("Cannot copy values from a different content type."); - - var published = CopyingFromSelf(other); - - // note: use property.SetValue(), don't assign pvalue.EditValue, else change tracking fails - - // clear all existing properties - foreach (var property in Properties) - foreach (var pvalue in property.Values) - if (pvalue.Culture.InvariantEquals(culture) && property.PropertyType.SupportsVariation(pvalue.Culture, pvalue.Segment)) - property.SetValue(null, pvalue.Culture, pvalue.Segment); - - // copy other properties - var otherProperties = other.Properties; - foreach (var otherProperty in otherProperties) - { - var alias = otherProperty.PropertyType.Alias; - foreach (var pvalue in otherProperty.Values) - { - if (pvalue.Culture != culture || !otherProperty.PropertyType.SupportsVariation(pvalue.Culture, pvalue.Segment)) - continue; - var value = published ? pvalue.PublishedValue : pvalue.EditedValue; - SetValue(alias, value, pvalue.Culture, pvalue.Segment); - } - } - - // copy name - SetCultureName(other.GetCultureName(culture), culture); - } - /// /// Changes the for the current content object /// @@ -565,6 +416,11 @@ namespace Umbraco.Core.Models // take care of the published state _publishedState = _published ? PublishedState.Published : PublishedState.Unpublished; + + // take care of publish infos + _publishInfosOrig = _publishInfos == null + ? null + : new Dictionary(_publishInfos, StringComparer.OrdinalIgnoreCase); } /// @@ -597,7 +453,6 @@ namespace Umbraco.Core.Models clone.EnableChangeTracking(); return clone; - } } } diff --git a/src/Umbraco.Core/Models/ContentBase.cs b/src/Umbraco.Core/Models/ContentBase.cs index 283054d501..29e36829d2 100644 --- a/src/Umbraco.Core/Models/ContentBase.cs +++ b/src/Umbraco.Core/Models/ContentBase.cs @@ -302,22 +302,112 @@ namespace Umbraco.Core.Models #endregion + #region Copy + + /// + public virtual void CopyFrom(IContent other, string culture = "*") + { + if (other.ContentTypeId != ContentTypeId) + throw new InvalidOperationException("Cannot copy values from a different content type."); + + culture = culture?.ToLowerInvariant().NullOrWhiteSpaceAsNull(); + + // the variation should be supported by the content type properties + // if the content type is invariant, only '*' and 'null' is ok + // if the content type varies, everything is ok because some properties may be invariant + if (!ContentTypeBase.SupportsPropertyVariation(culture, "*", true)) + throw new NotSupportedException($"Culture \"{culture}\" is not supported by content type \"{ContentTypeBase.Alias}\" with variation \"{ContentTypeBase.Variations}\"."); + + // copying from the same Id and VersionPk + var copyingFromSelf = Id == other.Id && VersionId == other.VersionId; + var published = copyingFromSelf; + + // note: use property.SetValue(), don't assign pvalue.EditValue, else change tracking fails + + // clear all existing properties for the specified culture + foreach (var property in Properties) + { + // each property type may or may not support the variation + if (!property.PropertyType.SupportsVariation(culture, "*", wildcards: true)) + continue; + + foreach (var pvalue in property.Values) + if (property.PropertyType.SupportsVariation(pvalue.Culture, pvalue.Segment, wildcards: true) && + (culture == "*" || pvalue.Culture.InvariantEquals(culture))) + { + property.SetValue(null, pvalue.Culture, pvalue.Segment); + } + } + + // copy properties from 'other' + var otherProperties = other.Properties; + foreach (var otherProperty in otherProperties) + { + if (!otherProperty.PropertyType.SupportsVariation(culture, "*", wildcards: true)) + continue; + + var alias = otherProperty.PropertyType.Alias; + foreach (var pvalue in otherProperty.Values) + { + if (otherProperty.PropertyType.SupportsVariation(pvalue.Culture, pvalue.Segment, wildcards: true) && + (culture == "*" || pvalue.Culture.InvariantEquals(culture))) + { + var value = published ? pvalue.PublishedValue : pvalue.EditedValue; + SetValue(alias, value, pvalue.Culture, pvalue.Segment); + } + } + } + + // copy names, too + + if (culture == "*") + ClearCultureInfos(); + + if (culture == null || culture == "*") + Name = other.Name; + + foreach (var (otherCulture, otherName) in other.CultureNames) + { + if (culture == "*" || culture == otherCulture) + SetCultureName(otherName, otherCulture); + } + } + + #endregion + #region Validation /// - public bool IsValid(string culture = null, string segment = null) + public bool IsValid(string culture = "*") { - var name = GetCultureName(culture); - if (name.IsNullOrWhiteSpace()) return false; - return ValidateProperties(culture, segment).Length == 0; + culture = culture.NullOrWhiteSpaceAsNull(); + + if (culture == null) + { + if (Name.IsNullOrWhiteSpace()) return false; + return ValidateProperties(null).Length == 0; + } + + if (culture != "*") + { + var name = GetCultureName(culture); + if (name.IsNullOrWhiteSpace()) return false; + return ValidateProperties(culture).Length == 0; + } + + // 'all cultures' + // those that have a name are ok, those without a name... we don't validate + return AvailableCultures.All(c => ValidateProperties(c).Length == 0); } /// - public virtual Property[] ValidateProperties(string culture = null, string segment = null) + public virtual Property[] ValidateProperties(string culture = "*") { + var alsoInvariant = culture != null && culture != "*"; + return Properties.Where(x => // select properties... - x.PropertyType.SupportsVariation(culture, segment, true) && // that support the variation - !x.IsValid(culture, segment)) // and are not valid + x.PropertyType.SupportsVariation(culture, "*", true) && // that support the variation + (!x.IsValid(culture) || (alsoInvariant && !x.IsValid(null)))) // and are not valid .ToArray(); } diff --git a/src/Umbraco.Core/Models/EntityExtensions.cs b/src/Umbraco.Core/Models/EntityExtensions.cs index fa7d9b6110..5ef68e99ea 100644 --- a/src/Umbraco.Core/Models/EntityExtensions.cs +++ b/src/Umbraco.Core/Models/EntityExtensions.cs @@ -4,15 +4,6 @@ namespace Umbraco.Core.Models { public static class EntityExtensions { - - /// - /// Determines whether the entity was just created and persisted. - /// - public static bool IsNewEntity(this IRememberBeingDirty entity) - { - return entity.WasPropertyDirty("Id"); - } - /// /// Gets additional data. /// diff --git a/src/Umbraco.Core/Models/IContent.cs b/src/Umbraco.Core/Models/IContent.cs index 929ea990e5..8b63b7b9ab 100644 --- a/src/Umbraco.Core/Models/IContent.cs +++ b/src/Umbraco.Core/Models/IContent.cs @@ -81,7 +81,7 @@ namespace Umbraco.Core.Models ContentStatus Status { get; } /// - /// Gets a value indicating whether a given culture is published. + /// Gets a value indicating whether a culture is published. /// /// /// A culture becomes published whenever values for this culture are published, @@ -90,6 +90,14 @@ namespace Umbraco.Core.Models /// bool IsCulturePublished(string culture); + /// + /// Gets a value indicating whether a culture was published. + /// + /// + /// Mirrors whenever the content item is saved. + /// + bool WasCulturePublished(string culture); + /// /// Gets the date a culture was published. /// @@ -156,91 +164,27 @@ namespace Umbraco.Core.Models /// IContent DeepCloneWithResetIdentities(); - ///// - ///// Publishes all values. - ///// - ///// A value indicating whether the values could be published. - ///// - ///// Fails if values cannot be published, e.g. if some values are not valid. - ///// Sets the property values for all cultures, including the invariant ones. - ///// Sets the published name for all culture that are available, thus publishing them all. - ///// The document must then be published via the content service SaveAndPublish method. - ///// - //// fixme - should return an attemps with error results - //// fixme - needs API review as this is not used apart from in tests << YES but users could use it - //bool TryPublishAllValues(); - - ///// - ///// Publishes the values for a specified culture and all segments. - ///// - ///// A value indicating whether the values could be published. - ///// - ///// Fails if values cannot be published, e.g. if some values are not valid. - ///// Sets the property values for the specified culture, and only the specified culture: must - ///// be invoked with a null culture to set the invariant values. - ///// Sets the published name for the specified culture, thus publishing the culture. - ///// The document must then be published via the content service SaveAndPublish method. - ///// - //// fixme - needs API review as this is not used apart from in tests << NO it is THAT one that we should use for now - //// fixme - should return an attemps with error results - //// fixme - should it publish the invariant values too? - NO that's done when SaveAndPublish (is it? don't think so) - BUT could we want to avoid it? - //bool TryPublishCultureValues(string culture); - /// - /// Publishes values for a specific culture and segment. + /// Registers a culture to be published. /// - /// A value indicating whether the values could be published. + /// A value indicating whether the culture can be published. /// /// Fails if values cannot be published, e.g. if some values are not valid. /// Sets the property values but not the published name for the specified culture, - /// thus not explicitely publishing the culture. - /// The document must then be published via the content service SaveAndPublish method. + /// thus not explicitely publishing the culture. fixme uhuh? + /// Publishing must be finalized via the content service SavePublishing method. /// - // fixme - should return an attemps with error results - // fixme - publishing for segments is not supported - // we don't know whether should it also publish the specified culture? - // we don't know how to publish segments but not neutral, etc - // what shall we do then? - bool TryPublishValues(string culture = null, string segment = null); - - ///// - ///// Clears published values. - ///// - ///// - ///// Clears the published name for all cultures, thus unpublishing all cultures. - ///// - //void ClearAllPublishedValues(); + // fixme - should return an attempt with error results + bool PublishCulture(string culture = "*"); /// - /// Clears published values for a specified culture and segment. + /// Registers a culture to be unpublished. /// /// /// Clears the property values but not the published name for the specified culture, - /// thus leaving the culture published. + /// thus leaving the culture published. fixme wtf? + /// Publishing must be finalized via the content service SavePublishing method. /// - void ClearPublishedValues(string culture = null, string segment = null); // fixme should NOT use - - ///// - ///// Clears published values for a specified culture, all segments. - ///// - ///// - ///// Clears the published name for the specified culture, thus unpublishing the culture. - ///// - //void ClearCulturePublishedValues(string culture = null); // fixme that one should be used! - - /// - /// Copies values from another document. - /// - void CopyAllValues(IContent other); - - /// - /// Copies values from another document for a specified culture and segment. - /// - void CopyValues(IContent other, string culture = null, string segment = null); - - /// - /// Copies values from another document for a specified culture, all segments. - /// - void CopyCultureValues(IContent other, string culture = null); + void UnpublishCulture(string culture = "*"); } } diff --git a/src/Umbraco.Core/Models/IContentBase.cs b/src/Umbraco.Core/Models/IContentBase.cs index deda24cea0..3c56b2c737 100644 --- a/src/Umbraco.Core/Models/IContentBase.cs +++ b/src/Umbraco.Core/Models/IContentBase.cs @@ -116,47 +116,40 @@ namespace Umbraco.Core.Models /// /// Gets the value of a Property /// + /// Values 'null' and 'empty' are equivalent for culture and segment. object GetValue(string propertyTypeAlias, string culture = null, string segment = null, bool published = false); /// /// Gets the typed value of a Property /// + /// Values 'null' and 'empty' are equivalent for culture and segment. TValue GetValue(string propertyTypeAlias, string culture = null, string segment = null, bool published = false); /// /// Sets the (edited) value of a Property /// + /// Values 'null' and 'empty' are equivalent for culture and segment. void SetValue(string propertyTypeAlias, object value, string culture = null, string segment = null); + /// + /// Copies values from another document. + /// + void CopyFrom(IContent other, string culture = "*"); + + // fixme validate published cultures? + /// /// Checks if the content and property values are valid in order to be persisted. /// - /// - /// - /// - bool IsValid(string culture = null, string segment = null); + /// If the content type is variant, then culture can be either '*' or an actual culture, but neither 'null' nor + /// 'empty'. If the content type is invariant, then culture can be either '*' or null or empty. + bool IsValid(string culture = "*"); /// - /// Gets a value indicating if all properties values are valid. + /// Validates the content item's properties. /// - //fixme - needs API review as this is not used apart from in tests - //Property[] ValidateAllProperties(); - - /// - /// Validates the content item's properties for the provided culture/segment - /// - /// - /// - /// - /// - /// This will not perform validation for properties that do not match the required ContentVariation based on the culture/segment values provided - /// - Property[] ValidateProperties(string culture = null, string segment = null); - - /// - /// Gets a value indicating if the culture properties values are valid. - /// - //fixme - needs API review as this is not used apart from in tests - //Property[] ValidatePropertiesForCulture(string culture = null); + /// If the content type is variant, then culture can be either '*' or an actual culture, but neither 'null' nor + /// 'empty'. If the content type is invariant, then culture can be either '*' or null or empty. + Property[] ValidateProperties(string culture = "*"); } } diff --git a/src/Umbraco.Core/Models/Property.cs b/src/Umbraco.Core/Models/Property.cs index e1f05a4982..c0dd97ff87 100644 --- a/src/Umbraco.Core/Models/Property.cs +++ b/src/Umbraco.Core/Models/Property.cs @@ -195,68 +195,16 @@ namespace Umbraco.Core.Models : pvalue.EditedValue; } - // fixme clear - /* // internal - must be invoked by the content item // does *not* validate the value - content item must validate first - internal void PublishAllValues() - { - // if invariant-neutral is supported, publish invariant-neutral - if (PropertyType.ValidateVariation(null, null, false)) - PublishPropertyValue(_pvalue); - - // publish everything not invariant-neutral that is supported - if (_vvalues == null) return; - - var pvalues = _vvalues - .Where(x => PropertyType.ValidateVariation(x.Value.Culture, x.Value.Segment, false)) - .Select(x => x.Value); - foreach (var pvalue in pvalues) - PublishPropertyValue(pvalue); - } - - // internal - must be invoked by the content item - // does *not* validate the value - content item must validate first - internal void PublishValue(string culture = null, string segment = null) - { - PropertyType.ValidateVariation(culture, segment, true); - - var (pvalue, _) = GetPValue(culture, segment, false); - if (pvalue == null) return; - PublishPropertyValue(pvalue); - } - - // internal - must be invoked by the content item - // does *not* validate the value - content item must validate first - internal void PublishCultureValues(string culture = null) - { - // if invariant and invariant-neutral is supported, publish invariant-neutral - if (culture == null && PropertyType.ValidateVariation(null, null, false)) - PublishPropertyValue(_pvalue); - - // publish everything not invariant-neutral that matches the culture and is supported - if (_vvalues != null) - { - var pvalues = _vvalues - .Where(x => x.Value.Culture.InvariantEquals(culture)) - .Where(x => PropertyType.ValidateVariation(culture, x.Value.Segment, false)) - .Select(x => x.Value); - foreach (var pvalue in pvalues) - PublishPropertyValue(pvalue); - } - } - */ - - // internal - must be invoked by the content item - // does *not* validate the value - content item must validate first - internal void PublishValues(string culture = null, string segment = null) + internal void PublishValues(string culture = "*", string segment = "*") { culture = culture.NullOrWhiteSpaceAsNull(); segment = segment.NullOrWhiteSpaceAsNull(); // if invariant or all, and invariant-neutral is supported, publish invariant-neutral if ((culture == null || culture == "*") && (segment == null || segment == "*") && PropertyType.SupportsVariation(null, null)) - PublishPropertyValue(_pvalue); + PublishValue(_pvalue); // then deal with everything that varies if (_vvalues == null) return; @@ -264,24 +212,24 @@ namespace Umbraco.Core.Models // get the property values that are still relevant (wrt the property type variation), // and match the specified culture and segment (or anything when '*'). var pvalues = _vvalues.Where(x => - PropertyType.SupportsVariation(x.Value.Culture, x.Value.Segment) && // the value variation is ok + PropertyType.SupportsVariation(x.Value.Culture, x.Value.Segment, true) && // the value variation is ok (culture == "*" || x.Value.Culture.InvariantEquals(culture)) && // the culture matches (segment == "*" || x.Value.Segment.InvariantEquals(segment))) // the segment matches .Select(x => x.Value); foreach (var pvalue in pvalues) - PublishPropertyValue(pvalue); + PublishValue(pvalue); } // internal - must be invoked by the content item - internal void ClearPublishedValues(string culture = null, string segment = null) + internal void UnpublishValues(string culture = "*", string segment = "*") { culture = culture.NullOrWhiteSpaceAsNull(); segment = segment.NullOrWhiteSpaceAsNull(); // if invariant or all, and invariant-neutral is supported, publish invariant-neutral if ((culture == null || culture == "*") && (segment == null || segment == "*") && PropertyType.SupportsVariation(null, null)) - ClearPublishedPropertyValue(_pvalue); + UnpublishValue(_pvalue); // then deal with everything that varies if (_vvalues == null) return; @@ -289,61 +237,16 @@ namespace Umbraco.Core.Models // get the property values that are still relevant (wrt the property type variation), // and match the specified culture and segment (or anything when '*'). var pvalues = _vvalues.Where(x => - PropertyType.SupportsVariation(x.Value.Culture, x.Value.Segment) && // the value variation is ok + PropertyType.SupportsVariation(x.Value.Culture, x.Value.Segment, true) && // the value variation is ok (culture == "*" || x.Value.Culture.InvariantEquals(culture)) && // the culture matches (segment == "*" || x.Value.Segment.InvariantEquals(segment))) // the segment matches .Select(x => x.Value); foreach (var pvalue in pvalues) - ClearPublishedPropertyValue(pvalue); + UnpublishValue(pvalue); } - // fixme clear - /* - // internal - must be invoked by the content item - internal void ClearPublishedAllValues() - { - if (PropertyType.ValidateVariation(null, null, false)) - ClearPublishedPropertyValue(_pvalue); - - if (_vvalues != null) - { - var pvalues = _vvalues - .Where(x => PropertyType.ValidateVariation(x.Value.Culture, x.Value.Segment, false)) - .Select(x => x.Value); - foreach (var pvalue in pvalues) - ClearPublishedPropertyValue(pvalue); - } - } - - // internal - must be invoked by the content item - internal void ClearPublishedValue(string culture = null, string segment = null) - { - PropertyType.ValidateVariation(culture, segment, true); - var (pvalue, _) = GetPValue(culture, segment, false); - if (pvalue == null) return; - ClearPublishedPropertyValue(pvalue); - } - - // internal - must be invoked by the content item - internal void ClearCulturePublishedValues(string culture = null) - { - if (culture == null && PropertyType.ValidateVariation(null, null, false)) - ClearPublishedPropertyValue(_pvalue); - - if (_vvalues != null) - { - var pvalues = _vvalues - .Where(x => x.Value.Culture.InvariantEquals(culture)) - .Where(x => PropertyType.ValidateVariation(culture, x.Value.Segment, false)) - .Select(x => x.Value); - foreach (var pvalue in pvalues) - ClearPublishedPropertyValue(pvalue); - } - } - */ - - private void PublishPropertyValue(PropertyValue pvalue) + private void PublishValue(PropertyValue pvalue) { if (pvalue == null) return; @@ -354,7 +257,7 @@ namespace Umbraco.Core.Models DetectChanges(pvalue.EditedValue, origValue, Ps.Value.ValuesSelector, Ps.Value.PropertyValueComparer, false); } - private void ClearPublishedPropertyValue(PropertyValue pvalue) + private void UnpublishValue(PropertyValue pvalue) { if (pvalue == null) return; @@ -436,11 +339,13 @@ namespace Umbraco.Core.Models } /// - /// Gets a value indicating whether all properties are valid. + /// Gets a value indicating whether the property has valid values. /// - /// - internal bool IsValid(string culture = null, string segment = null) + internal bool IsValid(string culture = "*", string segment = "*") { + culture = culture.NullOrWhiteSpaceAsNull(); + segment = segment.NullOrWhiteSpaceAsNull(); + // if validating invariant/neutral, and it is supported, validate // (including ensuring that the value exists, if mandatory) if ((culture == null || culture == "*") && (segment == null || segment == "*") && PropertyType.SupportsVariation(null, null)) @@ -448,15 +353,20 @@ namespace Umbraco.Core.Models return false; // if validating only invariant/neutral, we are good - if (culture == null && segment == null) return true; + if (culture == null && segment == null) + return true; + + // if nothing else to validate, we are good + if ((culture == null || culture == "*") && (segment == null || segment == "*") && !PropertyType.VariesByCulture()) + return true; // for anything else, validate the existing values (including mandatory), // but we cannot validate mandatory globally (we don't know the possible cultures and segments) - if (_vvalues == null) return culture == "*" || segment == "*" || IsValidValue(null); + if (_vvalues == null) return culture == "*" || IsValidValue(null); var pvalues = _vvalues.Where(x => - PropertyType.SupportsVariation(x.Value.Culture, x.Value.Segment) && // the value variation is ok + PropertyType.SupportsVariation(x.Value.Culture, x.Value.Segment, true) && // the value variation is ok (culture == "*" || x.Value.Culture.InvariantEquals(culture)) && // the culture matches (segment == "*" || x.Value.Segment.InvariantEquals(segment))) // the segment matches .Select(x => x.Value) @@ -465,44 +375,6 @@ namespace Umbraco.Core.Models return pvalues.Count == 0 || pvalues.All(x => IsValidValue(x.EditedValue)); } - // fixme clear - ///// - ///// Gets a value indicating whether the culture/any values are valid. - ///// - ///// An invalid value can be saved, but only valid values can be published. - //internal bool IsCultureValid(string culture) - //{ - - // // culture-neutral is supported, validate culture-neutral - // // includes mandatory validation - // if (PropertyType.ValidateVariation(culture, null, false) && !IsValidValue(GetValue(culture))) - // return false; - - // // either culture-neutral is not supported, or it is valid - // // for anything non-neutral, validate the existing values (including mandatory), - // // but we cannot validate mandatory globally (we don't know the possible segments) - - // if (_vvalues == null) return true; - - // var pvalues = _vvalues - // .Where(x => x.Value.Culture.InvariantEquals(culture)) - // .Where(x => PropertyType.ValidateVariation(culture, x.Value.Segment, false)) - // .Select(x => x.Value) - // .ToArray(); - - // return pvalues.Length == 0 || pvalues.All(x => IsValidValue(x.EditedValue)); - //} - - ///// - ///// Gets a value indicating whether the value is valid. - ///// - ///// An invalid value can be saved, but only valid values can be published. - //public bool IsValid(string culture = null, string segment = null) - //{ - // // single value -> validates mandatory - // return IsValidValue(GetValue(culture, segment)); - //} - /// /// Boolean indicating whether the passed in value is valid /// diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs index 202f51729a..39a9e11a00 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs @@ -328,7 +328,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // if !publishing, we may have a new name != current publish name, // also impacts 'edited' - if (content.PublishName != content.Name) + if (!publishing && content.PublishName != content.Name) edited = true; // persist the document dto diff --git a/src/Umbraco.Core/Publishing/ScheduledPublisher.cs b/src/Umbraco.Core/Publishing/ScheduledPublisher.cs index b429a142e7..1d2d62b929 100644 --- a/src/Umbraco.Core/Publishing/ScheduledPublisher.cs +++ b/src/Umbraco.Core/Publishing/ScheduledPublisher.cs @@ -41,8 +41,8 @@ namespace Umbraco.Core.Publishing try { d.ReleaseDate = null; - d.TryPublishValues(); // fixme variants? - var result = _contentService.SaveAndPublish(d, _userService.GetProfileById(d.WriterId).Id); + d.PublishCulture(); // fixme variants? + var result = _contentService.SaveAndPublish(d, userId: _userService.GetProfileById(d.WriterId).Id); _logger.Debug(() => $"Result of publish attempt: {result.Result}"); if (result.Success == false) { diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs index edc10353c9..2e788382fe 100644 --- a/src/Umbraco.Core/Services/IContentService.cs +++ b/src/Umbraco.Core/Services/IContentService.cs @@ -348,23 +348,48 @@ namespace Umbraco.Core.Services /// /// Saves and publishes a document. /// - /// Property values must first be published at document level. - PublishResult SaveAndPublish(IContent content, int userId = 0, bool raiseEvents = true); + /// + /// By default, publishes all variations of the document, but it is possible to specify a culture to be published. + /// When a culture is being published, it includes all varying values along with all invariant values. For + /// anything more complicated, see . + /// The document is *always* saved, even when publishing fails. + /// If the content type is variant, then culture can be either '*' or an actual culture, but neither 'null' nor + /// 'empty'. If the content type is invariant, then culture can be either '*' or null or empty. + /// + PublishResult SaveAndPublish(IContent content, string culture = "*", int userId = 0, bool raiseEvents = true); + + /// + /// Saves and publishes a publishing document. + /// + /// + /// A publishing document is a document with values that are being published, i.e. + /// that have been published or cleared via and + /// . + /// The document is *always* saved, even when publishing fails. + /// + PublishResult SavePublishing(IContent content, int userId = 0, bool raiseEvents = true); /// /// Saves and publishes a document branch. /// - IEnumerable SaveAndPublishBranch(IContent content, bool force, string culture = null, string segment = null, int userId = 0); + IEnumerable SaveAndPublishBranch(IContent content, bool force, string culture = "*", int userId = 0); /// /// Saves and publishes a document branch. /// - IEnumerable SaveAndPublishBranch(IContent content, bool force, Func editing, Func publishValues, int userId = 0); + IEnumerable SaveAndPublishBranch(IContent content, bool force, Func editing, Func publishCultures, int userId = 0); /// - /// Unpublishes a document or optionally unpublishes a culture and/or segment for the document. + /// Unpublishes a document. /// - UnpublishResult Unpublish(IContent content, string culture = null, string segment = null, int userId = 0); + /// + /// By default, unpublishes the document as a whole, but it is possible to specify a culture to be + /// unpublished. Depending on whether that culture is mandatory, and other cultures remain published, + /// the document as a whole may or may not remain published. + /// If the content type is variant, then culture can be either '*' or an actual culture, but neither null nor + /// empty. If the content type is invariant, then culture can be either '*' or null or empty. + /// + UnpublishResult Unpublish(IContent content, string culture = "*", int userId = 0); /// /// Gets a value indicating whether a document is path-publishable. diff --git a/src/Umbraco.Core/Services/Implement/ContentService.cs b/src/Umbraco.Core/Services/Implement/ContentService.cs index 4051a26f7f..90619abf05 100644 --- a/src/Umbraco.Core/Services/Implement/ContentService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentService.cs @@ -858,20 +858,12 @@ namespace Umbraco.Core.Services.Implement // fixme - kill all those raiseEvents - // fixme - // the API is not consistent - // SaveAndPublish requires that SetPublishValues / ClearPublishValues is called beforehand - // Unpublish will unpublish one culture - no idea what happens if we SetPublishValues / ClearPublishValues beforehand - // and there's not way to just plainly unpublish the whole thing - refactor! - // - // plus, does it make any sense for the 'default' culture to also be 'mandatory'? - /// public OperationResult Save(IContent content, int userId = 0, bool raiseEvents = true) { var publishedState = content.PublishedState; if (publishedState != PublishedState.Published && publishedState != PublishedState.Unpublished) - throw new InvalidOperationException("Cannot save a (un)publishing, use the dedicated (un)publish method."); + throw new InvalidOperationException("Cannot save (un)publishing content, use the dedicated SavePublished method."); var evtMsgs = EventMessagesFactory.Get(); @@ -884,8 +876,6 @@ namespace Umbraco.Core.Services.Implement return OperationResult.Cancel(evtMsgs); } - var isNew = content.IsNewEntity(); - scope.WriteLock(Constants.Locks.ContentTree); if (content.HasIdentity == false) @@ -899,7 +889,7 @@ namespace Umbraco.Core.Services.Implement saveEventArgs.CanCancel = false; scope.Events.Dispatch(Saved, this, saveEventArgs, "Saved"); } - var changeType = isNew ? TreeChangeTypes.RefreshBranch : TreeChangeTypes.RefreshNode; + var changeType = TreeChangeTypes.RefreshNode; scope.Events.Dispatch(TreeChanged, this, new TreeChange(content, changeType).ToEventArgs()); Audit(AuditType.Save, "Save Content performed by user", userId, content.Id); scope.Complete(); @@ -923,8 +913,7 @@ namespace Umbraco.Core.Services.Implement return OperationResult.Cancel(evtMsgs); } - var treeChanges = contentsA.Select(x => new TreeChange(x, - x.IsNewEntity() ? TreeChangeTypes.RefreshBranch : TreeChangeTypes.RefreshNode)); + var treeChanges = contentsA.Select(x => new TreeChange(x, TreeChangeTypes.RefreshNode)); scope.WriteLock(Constants.Locks.ContentTree); foreach (var content in contentsA) @@ -951,147 +940,98 @@ namespace Umbraco.Core.Services.Implement } /// - public PublishResult SaveAndPublish(IContent content, int userId = 0, bool raiseEvents = true) + public PublishResult SaveAndPublish(IContent content, string culture = "*", int userId = 0, bool raiseEvents = true) { - // note: SaveAndPublish does *not* publishes value, it assumes that content.PublishValues() - // has been called for the variations that are to be published - those are are already - // published remain published - can also be used to simply put the content back into its - // previous published state after it had been completely unpublished - var evtMsgs = EventMessagesFactory.Get(); - PublishResult result; - if (((Content) content).PublishedState != PublishedState.Publishing && content.Published) + var publishedState = content.PublishedState; + if (publishedState != PublishedState.Published && publishedState != PublishedState.Unpublished) + throw new InvalidOperationException($"Cannot save-and-publish (un)publishing content, use the dedicated {nameof(SavePublishing)} method."); + + // cannot accept invariant (null or empty) culture for variant content type + // cannot accept a specific culture for invariant content type (but '*' is ok) + if (content.ContentType.VariesByCulture()) { - // already published, and values haven't changed - i.e. not changing anything - // nothing to do - // fixme - unless we *want* to bump dates? - return new PublishResult(PublishResultType.SuccessAlready, evtMsgs, content); + if (culture.IsNullOrWhiteSpace()) + throw new NotSupportedException("Invariant culture is not supported by variant content types."); + } + else + { + if (!culture.IsNullOrWhiteSpace() && culture != "*") + throw new NotSupportedException($"Culture \"{culture}\" is not supported by invariant content types."); } - using (var scope = ScopeProvider.CreateScope()) + // if culture is specific, first publish the invariant values, then publish the culture itself. + // if culture is '*', then publish them all (including variants) + + // explicitely SaveAndPublish a specific culture also publishes invariant values + if (!culture.IsNullOrWhiteSpace() && culture != "*") { - var saveEventArgs = new SaveEventArgs(content, evtMsgs); - if (raiseEvents && scope.Events.DispatchCancelable(Saving, this, saveEventArgs, "Saving")) - { - scope.Complete(); - return new PublishResult(PublishResultType.FailedCancelledByEvent, evtMsgs, content); - } - - var isNew = content.IsNewEntity(); - var changeType = isNew ? TreeChangeTypes.RefreshBranch : TreeChangeTypes.RefreshNode; - var previouslyPublished = content.HasIdentity && content.Published; - - scope.WriteLock(Constants.Locks.ContentTree); - - // ensure that the document can be published, and publish - // handling events, business rules, etc - result = StrategyCanPublish(scope, content, userId, /*checkPath:*/ true, evtMsgs); - if (result.Success) - result = StrategyPublish(scope, content, /*canPublish:*/ true, userId, evtMsgs); - - // save - always, even if not publishing (this is SaveAndPublish) - if (content.HasIdentity == false) - content.CreatorId = userId; - content.WriterId = userId; - - // if not going to publish, must reset the published state - if (!result.Success) - ((Content) content).Published = content.Published; - - _documentRepository.Save(content); - - if (raiseEvents) // always - { - saveEventArgs.CanCancel = false; - scope.Events.Dispatch(Saved, this, saveEventArgs, "Saved"); - } - - if (result.Success == false) - { - scope.Events.Dispatch(TreeChanged, this, new TreeChange(content, changeType).ToEventArgs()); - scope.Complete(); // compete the save - return result; - } - - if (isNew == false && previouslyPublished == false) - changeType = TreeChangeTypes.RefreshBranch; // whole branch - - // invalidate the node/branch - scope.Events.Dispatch(TreeChanged, this, new TreeChange(content, changeType).ToEventArgs()); - - scope.Events.Dispatch(Published, this, new PublishEventArgs(content, false, false), "Published"); - - // if was not published and now is... descendants that were 'published' (but - // had an unpublished ancestor) are 're-published' ie not explicitely published - // but back as 'published' nevertheless - if (isNew == false && previouslyPublished == false && HasChildren(content.Id)) - { - var descendants = GetPublishedDescendantsLocked(content).ToArray(); - scope.Events.Dispatch(Published, this, new PublishEventArgs(descendants, false, false), "Published"); - } - - Audit(AuditType.Publish, "Save and Publish performed by user", userId, content.Id); - - scope.Complete(); + // publish the invariant values + var publishInvariant = content.PublishCulture(null); + if (!publishInvariant) + return new PublishResult(PublishResultType.FailedContentInvalid, evtMsgs, content); } - return result; + // publish the culture(s) + var publishCulture = content.PublishCulture(culture); + if (!publishCulture) + return new PublishResult(PublishResultType.FailedContentInvalid, evtMsgs, content); + + // finally, "save publishing" + // what happens next depends on whether the content can be published or not + return SavePublishing(content, userId, raiseEvents); } /// - public UnpublishResult Unpublish(IContent content, string culture = null, string segment = null, int userId = 0) + public UnpublishResult Unpublish(IContent content, string culture = "*", int userId = 0) { - // unpublish a variation - and that may end up unpublishing the document as a whole, - // if the variation was mandatory (ie for a mandatory language - no idea what would happen w/ segments) - - if (!segment.IsNullOrWhiteSpace()) - throw new NotImplementedException("Segments are not supported."); - var evtMsgs = EventMessagesFactory.Get(); + var publishedState = content.PublishedState; + if (publishedState != PublishedState.Published && publishedState != PublishedState.Unpublished) + throw new InvalidOperationException($"Cannot save-and-publish (un)publishing content, use the dedicated {nameof(SavePublishing)} method."); + + // cannot accept invariant (null or empty) culture for variant content type + // cannot accept a specific culture for invariant content type (but '*' is ok) + if (content.ContentType.VariesByCulture()) + { + if (culture.IsNullOrWhiteSpace()) + throw new NotSupportedException("Invariant culture is not supported by variant content types."); + } + else + { + if (!culture.IsNullOrWhiteSpace() && culture != "*") + throw new NotSupportedException($"Culture \"{culture}\" is not supported by invariant content types."); + } + + // if the content is not published, nothing to do + if (!content.Published) + return new UnpublishResult(UnpublishResultType.SuccessAlready, evtMsgs, content); + + // all cultures = unpublish whole + if (culture == "*") + { + ((Content) content).PublishedState = PublishedState.Unpublishing; + } + else + { + // if the culture we want to unpublish was already unpublished, nothing to do + if (!content.WasCulturePublished(culture)) + return new UnpublishResult(UnpublishResultType.SuccessAlready, evtMsgs, content); + + // unpublish the culture + content.UnpublishCulture(culture); + } + + // finally, "save publishing" + // what happens next depends on whether the content can be published or not using (var scope = ScopeProvider.CreateScope()) { - scope.WriteLock(Constants.Locks.ContentTree); - - // not varying, or invariant culture - // simply unpublish the document - if (!content.ContentType.VariesByCulture() || culture.IsNullOrWhiteSpace()) - { - var unpublished = UnpublishScoped(scope, content, evtMsgs, userId); - if (unpublished.Success) scope.Complete(); - return unpublished; - } - - // varying, with culture = deal with cultures - - // if already unpublished, nothing to do - var publishedCultures = content.PublishedCultures.ToList(); - if (!publishedCultures.Contains(culture, StringComparer.OrdinalIgnoreCase)) - { - scope.Complete(); - return new UnpublishResult(UnpublishResultType.SuccessAlready, evtMsgs, content); - } - - // otherwise, in any case, clear the unpublishing variation - content.ClearPublishedValues(culture, segment); - - // would there be any culture left? - var mandatoryCultures = _languageRepository.GetMany().Where(x => x.Mandatory).Select(x => x.IsoCode); - var isMandatory = mandatoryCultures.Contains(culture, StringComparer.OrdinalIgnoreCase); - if (isMandatory || publishedCultures.Count == 1) - { - // nothing left = unpublish all - var unpublished = UnpublishScoped(scope, content, evtMsgs, userId); - if (unpublished.Success) scope.Complete(); - return unpublished; - } - - // else we need to republish, without that culture - var saved = SaveAndPublish(content, userId); + var saved = SavePublishing(content, userId); if (saved.Success) { - Audit(AuditType.UnPublish, $"UnPublish variation culture: {culture ?? string.Empty}, segment: {segment ?? string.Empty} performed by user", userId, content.Id); + Audit(AuditType.UnPublish, $"Unpublish variation culture: \"{culture ?? string.Empty}\" performed by user", userId, content.Id); scope.Complete(); return new UnpublishResult(UnpublishResultType.SuccessVariant, evtMsgs, content); } @@ -1104,36 +1044,163 @@ namespace Umbraco.Core.Services.Implement } } - /// - /// Unpublishes an entire document. - /// - private UnpublishResult UnpublishScoped(IScope scope, IContent content, EventMessages evtMsgs, int userId) + /// + public PublishResult SavePublishing(IContent content, int userId = 0, bool raiseEvents = true) { - var newest = GetById(content.Id); // ensure we have the newest version - if (content.VersionId != newest.VersionId) // but use the original object if it's already the newest version - content = newest; + var evtMsgs = EventMessagesFactory.Get(); + PublishResult publishResult = null; + UnpublishResult unpublishResult = null; - // if already unpublished, nothing to do - if (!content.Published) - return new UnpublishResult(UnpublishResultType.SuccessAlready, evtMsgs, content); + // nothing set = republish it all + if (content.PublishedState != PublishedState.Publishing && content.PublishedState != PublishedState.Unpublishing) + ((Content) content).PublishedState = PublishedState.Publishing; - // run strategies - // fixme should we still complete the uow? don't want to rollback here! - var attempt = StrategyCanUnpublish(scope, content, userId, evtMsgs); - if (attempt.Success == false) return attempt; // causes rollback - attempt = StrategyUnpublish(scope, content, true, userId, evtMsgs); - if (attempt.Success == false) return attempt; // causes rollback + using (var scope = ScopeProvider.CreateScope()) + { + // is the content going to end up published, or unpublished? + bool publishing, unpublishing; + if (content.ContentType.VariesByCulture()) + { + var publishedCultures = content.PublishedCultures.ToList(); + var cannotBePublished= publishedCultures.Count == 0; // no published cultures = cannot be published + if (!cannotBePublished) + { + var mandatoryCultures = _languageRepository.GetMany().Where(x => x.Mandatory).Select(x => x.IsoCode); + cannotBePublished = mandatoryCultures.Any(x => !publishedCultures.Contains(x)); // missing mandatory culture = cannot be published + } + unpublishing = content.Published && cannotBePublished; // if we cannot be published, and we are published, we unpublish + publishing = !cannotBePublished; // if we can be published, we publish + } + else + { + // invariant: we can publish, no culture problem, no need to unpublish + publishing = content.PublishedState == PublishedState.Publishing; + unpublishing = content.PublishedState == PublishedState.Unpublishing; + } - // save - content.WriterId = userId; - _documentRepository.Save(content); + var isNew = !content.HasIdentity; + var changeType = isNew ? TreeChangeTypes.RefreshNode : TreeChangeTypes.RefreshBranch; + var previouslyPublished = content.HasIdentity && content.Published; - // events and audit - scope.Events.Dispatch(UnPublished, this, new PublishEventArgs(content, false, false), "UnPublished"); - scope.Events.Dispatch(TreeChanged, this, new TreeChange(content, TreeChangeTypes.RefreshBranch).ToEventArgs()); - Audit(AuditType.UnPublish, "UnPublish performed by user", userId, content.Id); + scope.WriteLock(Constants.Locks.ContentTree); - return new UnpublishResult(evtMsgs, content); + // always save + var saveEventArgs = new SaveEventArgs(content, evtMsgs); + if (raiseEvents && scope.Events.DispatchCancelable(Saving, this, saveEventArgs, "Saving")) + { + scope.Complete(); + return new PublishResult(PublishResultType.FailedCancelledByEvent, evtMsgs, content); + } + + if (publishing) + { + // ensure that the document can be published, and publish + // handling events, business rules, etc + // note: StrategyPublish flips the PublishedState to Publishing! + publishResult = StrategyCanPublish(scope, content, userId, /*checkPath:*/ true, evtMsgs); + if (publishResult.Success) + publishResult = StrategyPublish(scope, content, /*canPublish:*/ true, userId, evtMsgs); + if (!publishResult.Success) + ((Content) content).Published = content.Published; // reset published state = save unchanged + } + + if (unpublishing) + { + var newest = GetById(content.Id); // ensure we have the newest version - in scope + if (content.VersionId != newest.VersionId) // but use the original object if it's already the newest version + content = newest; + + if (content.Published) + { + // ensure that the document can be unpublished, and unpublish + // handling events, business rules, etc + // note: StrategyUnpublish flips the PublishedState to Unpublishing! + unpublishResult = StrategyCanUnpublish(scope, content, userId, evtMsgs); + if (unpublishResult.Success) + unpublishResult = StrategyUnpublish(scope, content, true, userId, evtMsgs); + if (!unpublishResult.Success) + ((Content) content).Published = content.Published; // reset published state = save unchanged + } + else + { + // already unpublished - optimistic concurrency collision, really, + // and I am not sure at all what we should do, better die fast, else + // we may end up corrupting the db + throw new InvalidOperationException("Concurrency collision."); + } + } + + // save, always + if (content.HasIdentity == false) + content.CreatorId = userId; + content.WriterId = userId; + + // saving does NOT change the published version, unless PublishedState is Publishing or Unpublishing + _documentRepository.Save(content); + + // raise the Saved event, always + if (raiseEvents) + { + saveEventArgs.CanCancel = false; + scope.Events.Dispatch(Saved, this, saveEventArgs, "Saved"); + } + + if (unpublishing) // we have tried to unpublish + { + if (unpublishResult.Success) // and succeeded, trigger events + { + // events and audit + scope.Events.Dispatch(UnPublished, this, new PublishEventArgs(content, false, false), "UnPublished"); + scope.Events.Dispatch(TreeChanged, this, new TreeChange(content, TreeChangeTypes.RefreshBranch).ToEventArgs()); + Audit(AuditType.UnPublish, "UnPublish performed by user", userId, content.Id); + scope.Complete(); + return new PublishResult(PublishResultType.Success, evtMsgs, content); + } + + // or, failed + scope.Events.Dispatch(TreeChanged, this, new TreeChange(content, changeType).ToEventArgs()); + scope.Complete(); // compete the save + return new PublishResult(PublishResultType.Failed, evtMsgs, content); // bah + } + + if (publishing) // we have tried to publish + { + if (publishResult.Success) // and succeeded, trigger events + { + if (isNew == false && previouslyPublished == false) + changeType = TreeChangeTypes.RefreshBranch; // whole branch + + // invalidate the node/branch + scope.Events.Dispatch(TreeChanged, this, new TreeChange(content, changeType).ToEventArgs()); + scope.Events.Dispatch(Published, this, new PublishEventArgs(content, false, false), "Published"); + + // if was not published and now is... descendants that were 'published' (but + // had an unpublished ancestor) are 're-published' ie not explicitely published + // but back as 'published' nevertheless + if (isNew == false && previouslyPublished == false && HasChildren(content.Id)) + { + var descendants = GetPublishedDescendantsLocked(content).ToArray(); + scope.Events.Dispatch(Published, this, new PublishEventArgs(descendants, false, false), "Published"); + } + + Audit(AuditType.Publish, "Save and Publish performed by user", userId, content.Id); + scope.Complete(); + return publishResult; + } + + // or, failed + scope.Events.Dispatch(TreeChanged, this, new TreeChange(content, changeType).ToEventArgs()); + scope.Complete(); // compete the save + return publishResult; + } + + // we haven't tried anything - assume that is bad (may need to reconsider the case of unpublishing + // a culture, and the culture or content item was already unpublished...) - bah + + scope.Events.Dispatch(TreeChanged, this, new TreeChange(content, changeType).ToEventArgs()); + scope.Complete(); // compete the save + return new PublishResult(PublishResultType.Failed, evtMsgs, content); + } } /// @@ -1149,8 +1216,8 @@ namespace Umbraco.Core.Services.Implement try { d.ReleaseDate = null; - d.TryPublishValues(); // fixme variants? - result = SaveAndPublish(d, d.WriterId); + d.PublishCulture(); // fixme variants? + result = SaveAndPublish(d, userId: d.WriterId); if (result.Success == false) Logger.Error($"Failed to publish document id={d.Id}, reason={result.Result}."); } @@ -1182,19 +1249,22 @@ namespace Umbraco.Core.Services.Implement } /// - public IEnumerable SaveAndPublishBranch(IContent content, bool force, string culture = null, string segment = null, int userId = 0) + public IEnumerable SaveAndPublishBranch(IContent content, bool force, string culture = "*", int userId = 0) { - segment = segment?.ToLowerInvariant(); + // note: EditedValue and PublishedValue are objects here, so it is important to .Equals() + // and not to == them, else we would be comparing references, and that is a bad thing - bool IsEditing(IContent c, string l, string s) - => c.Properties.Any(x => x.Values.Where(y => y.Culture == l && y.Segment == s).Any(y => y.EditedValue != y.PublishedValue)); + bool IsEditing(IContent c, string l) + => c.PublishName != c.Name || + c.PublishedCultures.Any(x => c.GetCultureName(x) != c.GetPublishName(x)) || + c.Properties.Any(x => x.Values.Where(y => culture == "*" || y.Culture == l).Any(y => !y.EditedValue.Equals(y.PublishedValue))); - return SaveAndPublishBranch(content, force, document => IsEditing(document, culture, segment), document => document.TryPublishValues(culture, segment), userId); + return SaveAndPublishBranch(content, force, document => IsEditing(document, culture), document => document.PublishCulture(culture), userId); } /// public IEnumerable SaveAndPublishBranch(IContent document, bool force, - Func editing, Func publishValues, int userId = 0) + Func editing, Func publishCultures, int userId = 0) { var evtMsgs = EventMessagesFactory.Get(); var results = new List(); @@ -1214,7 +1284,7 @@ namespace Umbraco.Core.Services.Implement throw new InvalidOperationException("Do not publish values when publishing branches."); // deal with the branch root - if it fails, abort - var result = SaveAndPublishBranchOne(scope, document, editing, publishValues, true, publishedDocuments, evtMsgs, userId); + var result = SaveAndPublishBranchOne(scope, document, editing, publishCultures, true, publishedDocuments, evtMsgs, userId); results.Add(result); if (!result.Success) return results; @@ -1234,7 +1304,7 @@ namespace Umbraco.Core.Services.Implement // no need to check path here, // 1. because we know the parent is path-published (we just published it) // 2. because it would not work as nothing's been written out to the db until the uow completes - result = SaveAndPublishBranchOne(scope, d, editing, publishValues, false, publishedDocuments, evtMsgs, userId); + result = SaveAndPublishBranchOne(scope, d, editing, publishCultures, false, publishedDocuments, evtMsgs, userId); results.Add(result); if (result.Success) continue; diff --git a/src/Umbraco.Core/Services/Implement/MediaService.cs b/src/Umbraco.Core/Services/Implement/MediaService.cs index 504750874c..2d3e478446 100644 --- a/src/Umbraco.Core/Services/Implement/MediaService.cs +++ b/src/Umbraco.Core/Services/Implement/MediaService.cs @@ -779,8 +779,6 @@ namespace Umbraco.Core.Services.Implement if (string.IsNullOrWhiteSpace(media.Name)) throw new ArgumentException("Media has no name.", nameof(media)); - var isNew = media.IsNewEntity(); - scope.WriteLock(Constants.Locks.MediaTree); if (media.HasIdentity == false) media.CreatorId = userId; @@ -791,7 +789,7 @@ namespace Umbraco.Core.Services.Implement saveEventArgs.CanCancel = false; scope.Events.Dispatch(Saved, this, saveEventArgs); } - var changeType = isNew ? TreeChangeTypes.RefreshBranch : TreeChangeTypes.RefreshNode; + var changeType = TreeChangeTypes.RefreshNode; scope.Events.Dispatch(TreeChanged, this, new TreeChange(media, changeType).ToEventArgs()); Audit(AuditType.Save, "Save Media performed by user", userId, media.Id); @@ -832,7 +830,7 @@ namespace Umbraco.Core.Services.Implement return OperationResult.Attempt.Cancel(evtMsgs); } - var treeChanges = mediasA.Select(x => new TreeChange(x, x.IsNewEntity() ? TreeChangeTypes.RefreshBranch : TreeChangeTypes.RefreshNode)); + var treeChanges = mediasA.Select(x => new TreeChange(x, TreeChangeTypes.RefreshNode)); scope.WriteLock(Constants.Locks.MediaTree); foreach (var media in mediasA) diff --git a/src/Umbraco.Tests/Integration/ContentEventsTests.cs b/src/Umbraco.Tests/Integration/ContentEventsTests.cs index 73f95c44a7..03539d2273 100644 --- a/src/Umbraco.Tests/Integration/ContentEventsTests.cs +++ b/src/Umbraco.Tests/Integration/ContentEventsTests.cs @@ -108,17 +108,14 @@ namespace Umbraco.Tests.Integration private IContent CreateBranch() { var content1 = MockedContent.CreateSimpleContent(_contentType, "Content1"); - content1.TryPublishValues(); ServiceContext.ContentService.SaveAndPublish(content1); // 2 (published) // .1 (published) // .2 (not published) var content2 = MockedContent.CreateSimpleContent(_contentType, "Content2", content1); - content2.TryPublishValues(); ServiceContext.ContentService.SaveAndPublish(content2); var content21 = MockedContent.CreateSimpleContent(_contentType, "Content21", content2); - content21.TryPublishValues(); ServiceContext.ContentService.SaveAndPublish(content21); var content22 = MockedContent.CreateSimpleContent(_contentType, "Content22", content2); ServiceContext.ContentService.Save(content22); @@ -137,12 +134,10 @@ namespace Umbraco.Tests.Integration // .1 (published) // .2 (not published) var content4 = MockedContent.CreateSimpleContent(_contentType, "Content4", content1); - content4.TryPublishValues(); ServiceContext.ContentService.SaveAndPublish(content4); content4.Name = "Content4X"; ServiceContext.ContentService.Save(content4); var content41 = MockedContent.CreateSimpleContent(_contentType, "Content41", content4); - content41.TryPublishValues(); ServiceContext.ContentService.SaveAndPublish(content41); var content42 = MockedContent.CreateSimpleContent(_contentType, "Content42", content4); ServiceContext.ContentService.Save(content42); @@ -151,10 +146,8 @@ namespace Umbraco.Tests.Integration // .1 (published) // .2 (not published) var content5 = MockedContent.CreateSimpleContent(_contentType, "Content5", content1); - content5.TryPublishValues(); ServiceContext.ContentService.SaveAndPublish(content5); var content51 = MockedContent.CreateSimpleContent(_contentType, "Content51", content5); - content51.TryPublishValues(); ServiceContext.ContentService.SaveAndPublish(content51); var content52 = MockedContent.CreateSimpleContent(_contentType, "Content52", content5); ServiceContext.ContentService.Save(content52); @@ -165,6 +158,69 @@ namespace Umbraco.Tests.Integration #endregion + #region Validate Setup + + [Test] + public void CreatedBranchIsOk() + { + var content1 = CreateBranch(); + + var children1 = Children(content1).ToArray(); + + var content2 = children1[0]; + var children2 = Children(content2).ToArray(); + var content21 = children2[0]; + var content22 = children2[1]; + + var content3 = children1[1]; + var children3 = Children(content3).ToArray(); + var content31 = children3[0]; + var content32 = children3[1]; + + var content4 = children1[2]; + var children4 = Children(content4).ToArray(); + var content41 = children4[0]; + var content42 = children4[1]; + + var content5 = children1[3]; + var children5 = Children(content5).ToArray(); + var content51 = children5[0]; + var content52 = children5[1]; + + Assert.IsTrue(content1.Published); + Assert.IsFalse(content1.Edited); + + Assert.IsTrue(content2.Published); + Assert.IsFalse(content2.Edited); + Assert.IsTrue(content21.Published); + Assert.IsFalse(content21.Edited); + Assert.IsFalse(content22.Published); + Assert.IsTrue(content22.Edited); + + Assert.IsFalse(content3.Published); + Assert.IsTrue(content3.Edited); + Assert.IsFalse(content31.Published); + Assert.IsTrue(content31.Edited); + Assert.IsFalse(content32.Published); + Assert.IsTrue(content32.Edited); + + Assert.IsTrue(content4.Published); + Assert.IsTrue(content4.Edited); + Assert.IsTrue(content41.Published); + Assert.IsFalse(content41.Edited); + Assert.IsFalse(content42.Published); + Assert.IsTrue(content42.Edited); + + Assert.IsFalse(content5.Published); + Assert.IsTrue(content5.Edited); + Assert.IsTrue(content51.Published); + Assert.IsFalse(content51.Edited); + Assert.IsFalse(content52.Published); + Assert.IsTrue(content52.Edited); + } + + #endregion + #region Events tracer private class EventInstance @@ -441,7 +497,6 @@ namespace Umbraco.Tests.Integration var content = ServiceContext.ContentService.GetRootContent().FirstOrDefault(); Assert.IsNotNull(content); - content.TryPublishValues(); ServiceContext.ContentService.SaveAndPublish(content); ResetEvents(); @@ -478,7 +533,6 @@ namespace Umbraco.Tests.Integration var content = ServiceContext.ContentService.GetRootContent().FirstOrDefault(); Assert.IsNotNull(content); - content.TryPublishValues(); ServiceContext.ContentService.SaveAndPublish(content); ResetEvents(); @@ -515,7 +569,6 @@ namespace Umbraco.Tests.Integration var content = ServiceContext.ContentService.GetRootContent().FirstOrDefault(); Assert.IsNotNull(content); - content.TryPublishValues(); ServiceContext.ContentService.SaveAndPublish(content); ResetEvents(); @@ -555,7 +608,6 @@ namespace Umbraco.Tests.Integration ResetEvents(); content.Name = "changed"; - content.TryPublishValues(); ServiceContext.ContentService.SaveAndPublish(content); Assert.AreEqual(2, _msgCount); @@ -576,12 +628,10 @@ namespace Umbraco.Tests.Integration var content = ServiceContext.ContentService.GetRootContent().FirstOrDefault(); Assert.IsNotNull(content); - content.TryPublishValues(); ServiceContext.ContentService.SaveAndPublish(content); ResetEvents(); content.Name = "changed"; - content.TryPublishValues(); ServiceContext.ContentService.SaveAndPublish(content); Assert.AreEqual(2, _msgCount); @@ -590,7 +640,7 @@ namespace Umbraco.Tests.Integration var m = 0; Assert.AreEqual($"{m:000}: ContentRepository/Refresh/{content.Id}.p+p", _events[i++].ToString()); m++; - Assert.AreEqual($"{m:000}: ContentCacheRefresher/RefreshNode/{content.Id}", _events[i++].ToString()); + Assert.AreEqual($"{m:000}: ContentCacheRefresher/RefreshBranch/{content.Id}", _events[i++].ToString()); } [Test] @@ -608,7 +658,6 @@ namespace Umbraco.Tests.Integration ResetEvents(); content.Name = "changed"; - content.TryPublishValues(); ServiceContext.ContentService.SaveAndPublish(content); Assert.AreEqual(2, _msgCount); @@ -620,35 +669,6 @@ namespace Umbraco.Tests.Integration Assert.AreEqual($"{m:000}: ContentCacheRefresher/RefreshBranch/{content.Id}", _events[i++].ToString()); } - [Test] - public void PublishPublishedContent() - { - // rule: when a content is published, - // - repository : refresh (p) - // - published page cache :: refresh - // note: whenever the published cache is refreshed, subscribers must - // assume that the unpublished cache is also refreshed, with the same - // values, and deal with it. - - var content = ServiceContext.ContentService.GetRootContent().FirstOrDefault(); - Assert.IsNotNull(content); - content.TryPublishValues(); - ServiceContext.ContentService.SaveAndPublish(content); - - ResetEvents(); - content.Name = "changed"; - content.TryPublishValues(); - ServiceContext.ContentService.SaveAndPublish(content); - - Assert.AreEqual(2, _msgCount); - Assert.AreEqual(2, _events.Count); - var i = 0; - var m = 0; - Assert.AreEqual($"{m:000}: ContentRepository/Refresh/{content.Id}.p+p", _events[i++].ToString()); - m++; - Assert.AreEqual($"{m:000}: ContentCacheRefresher/RefreshNode/{content.Id}", _events[i++].ToString()); - } - [Test] public void UnpublishContent() { @@ -658,7 +678,6 @@ namespace Umbraco.Tests.Integration var content = ServiceContext.ContentService.GetRootContent().FirstOrDefault(); Assert.IsNotNull(content); - content.TryPublishValues(); ServiceContext.ContentService.SaveAndPublish(content); ResetEvents(); @@ -682,7 +701,6 @@ namespace Umbraco.Tests.Integration var content = ServiceContext.ContentService.GetRootContent().FirstOrDefault(); Assert.IsNotNull(content); - content.TryPublishValues(); ServiceContext.ContentService.SaveAndPublish(content); content.Name = "changed"; ServiceContext.ContentService.Save(content); @@ -743,7 +761,6 @@ namespace Umbraco.Tests.Integration ServiceContext.ContentService.Unpublish(content1); ResetEvents(); - content1.TryPublishValues(); ServiceContext.ContentService.SaveAndPublish(content1); Assert.AreEqual(2, _msgCount); @@ -775,32 +792,32 @@ namespace Umbraco.Tests.Integration var content1 = CreateBranch(); ServiceContext.ContentService.Unpublish(content1); - ResetEvents(); - ServiceContext.ContentService.SaveAndPublishBranch(content1, false); + // branch is: - Assert.AreEqual(6, _msgCount); - Assert.AreEqual(6, _events.Count); + ResetEvents(); + ServiceContext.ContentService.SaveAndPublishBranch(content1, force: false); // force = false, don't publish unpublished items + + foreach (var e in _events) + Console.WriteLine(e); + + Assert.AreEqual(3, _msgCount); + Assert.AreEqual(3, _events.Count); var i = 0; var m = 0; var content1C = Children(content1).ToArray(); var content2C = Children(content1C[0]).ToArray(); var content4C = Children(content1C[2]).ToArray(); - Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content1.Id}.u+p", _events[i++].ToString()); - Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content1C[0].Id}.p+p", _events[i++].ToString()); - Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content1C[2].Id}.p+p", _events[i++].ToString()); - Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content2C[0].Id}.p+p", _events[i++].ToString()); - Assert.AreEqual($"{m:000}: ContentRepository/Refresh/{content4C[0].Id}.p+p", _events[i++].ToString()); - m++; + + // force:false => only republish the root node + nodes that are edited + Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content1.Id}.u+p", _events[i++].ToString()); // content1 was unpublished, now published + + // change: only content4 shows here, because it has changes - others don't need to be published + //Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content1C[0].Id}.p+p", _events[i++].ToString()); // content1/content2 + Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content1C[2].Id}.p+p", _events[i++].ToString()); // content1/content4 + //Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content2C[0].Id}.p+p", _events[i++].ToString()); // content1/content2/content21 + //Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content4C[0].Id}.p+p", _events[i++].ToString()); // content1/content4/content41 + Assert.AreEqual($"{m:000}: ContentCacheRefresher/RefreshBranch/{content1.Id}", _events[i++].ToString()); // repub content1 - /* - Assert.AreEqual(string.Format("{0:000}: ContentCacheRefresher/RefreshPublished/{1}", m, content1C[0].Id), _events[i++].ToString()); // repub content1.content2 - Assert.AreEqual(string.Format("{0:000}: ContentCacheRefresher/RefreshPublished,Refresh/{1}", m, content1C[2].Id), _events[i++].ToString()); // repub content1.content4 - var c = ServiceContext.ContentService.GetPublishedVersion(((ContentCacheRefresher.JsonPayload)_events[i - 1].EventArgs).Id); - Assert.IsTrue(c.Published); // get the published one - Assert.AreEqual("Content4X", c.Name); // published has new name - Assert.AreEqual(string.Format("{0:000}: ContentCacheRefresher/RefreshPublished/{1}", m, content2C[0].Id), _events[i++].ToString()); // repub content1.content2.content21 - Assert.AreEqual(string.Format("{0:000}: ContentCacheRefresher/RefreshPublished/{1}", m, content4C[0].Id), _events[i].ToString()); // repub content1.content4.content41 - */ } [Test] @@ -812,10 +829,13 @@ namespace Umbraco.Tests.Integration ServiceContext.ContentService.Unpublish(content1); ResetEvents(); - ServiceContext.ContentService.SaveAndPublishBranch(content1, true); + ServiceContext.ContentService.SaveAndPublishBranch(content1, force: true); // force = true, also publish unpublished items - Assert.AreEqual(14, _msgCount); - Assert.AreEqual(14, _events.Count); + foreach (var e in _events) + Console.WriteLine(e); + + Assert.AreEqual(10, _msgCount); + Assert.AreEqual(10, _events.Count); var i = 0; var m = 0; var content1C = Children(content1).ToArray(); @@ -823,43 +843,26 @@ namespace Umbraco.Tests.Integration var content3C = Children(content1C[1]).ToArray(); var content4C = Children(content1C[2]).ToArray(); var content5C = Children(content1C[3]).ToArray(); + + // force:true => all nodes are republished, refreshing all nodes - but only with changes - published w/out changes are not repub Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content1.Id}.u+p", _events[i++].ToString()); - Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content1C[0].Id}.p+p", _events[i++].ToString()); + //Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content1C[0].Id}.p+p", _events[i++].ToString()); Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content1C[1].Id}.u+p", _events[i++].ToString()); Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content1C[2].Id}.p+p", _events[i++].ToString()); Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content1C[3].Id}.u+p", _events[i++].ToString()); // remember: ordered by level, sortOrder - Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content2C[0].Id}.p+p", _events[i++].ToString()); + //Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content2C[0].Id}.p+p", _events[i++].ToString()); Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content3C[0].Id}.u+p", _events[i++].ToString()); - Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content4C[0].Id}.p+p", _events[i++].ToString()); - Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content5C[0].Id}.p+p", _events[i++].ToString()); + //Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content4C[0].Id}.p+p", _events[i++].ToString()); + //Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content5C[0].Id}.p+p", _events[i++].ToString()); Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content2C[1].Id}.u+p", _events[i++].ToString()); Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content3C[1].Id}.u+p", _events[i++].ToString()); Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content4C[1].Id}.u+p", _events[i++].ToString()); - Assert.AreEqual($"{m:000}: ContentRepository/Refresh/{content5C[1].Id}.u+p", _events[i++].ToString()); - m++; + Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content5C[1].Id}.u+p", _events[i++].ToString()); + Assert.AreEqual($"{m:000}: ContentCacheRefresher/RefreshBranch/{content1.Id}", _events[i++].ToString()); // repub content1 - /* - Assert.AreEqual(string.Format("{0:000}: ContentCacheRefresher/RefreshPublished/{1}", m, content1C[0].Id), _events[i++].ToString()); // repub content1.content2 - Assert.AreEqual(string.Format("{0:000}: ContentCacheRefresher/RefreshPublished,Refresh/{1}", m, content1C[1].Id), _events[i++].ToString()); // repub content1.content3 - Assert.AreEqual(string.Format("{0:000}: ContentCacheRefresher/RefreshPublished,Refresh/{1}", m, content1C[2].Id), _events[i++].ToString()); // repub content1.content4 - var c = ServiceContext.ContentService.GetPublishedVersion(((ContentCacheRefresher.JsonPayload)_events[i - 1].EventArgs).Id); - Assert.IsTrue(c.Published); // get the published one - Assert.AreEqual("Content4X", c.Name); // published has new name - Assert.AreEqual(string.Format("{0:000}: ContentCacheRefresher/RefreshPublished,Refresh/{1}", m, content1C[3].Id), _events[i++].ToString()); // repub content1.content5 - - Assert.AreEqual(string.Format("{0:000}: ContentCacheRefresher/RefreshPublished/{1}", m, content2C[0].Id), _events[i++].ToString()); // repub content1.content2.content21 - Assert.AreEqual(string.Format("{0:000}: ContentCacheRefresher/RefreshPublished,Refresh/{1}", m, content3C[0].Id), _events[i++].ToString()); // repub content1.content3.content31 - Assert.AreEqual(string.Format("{0:000}: ContentCacheRefresher/RefreshPublished/{1}", m, content4C[0].Id), _events[i++].ToString()); // repub content1.content4.content41 - Assert.AreEqual(string.Format("{0:000}: ContentCacheRefresher/RefreshPublished/{1}", m, content5C[0].Id), _events[i++].ToString()); // repub content1.content5.content51 - - Assert.AreEqual(string.Format("{0:000}: ContentCacheRefresher/RefreshPublished,Refresh/{1}", m, content2C[1].Id), _events[i++].ToString()); // repub content1.content2.content22 - Assert.AreEqual(string.Format("{0:000}: ContentCacheRefresher/RefreshPublished,Refresh/{1}", m, content3C[1].Id), _events[i++].ToString()); // repub content1.content3.content32 - Assert.AreEqual(string.Format("{0:000}: ContentCacheRefresher/RefreshPublished,Refresh/{1}", m, content4C[1].Id), _events[i++].ToString()); // repub content1.content4.content42 - Assert.AreEqual(string.Format("{0:000}: ContentCacheRefresher/RefreshPublished,Refresh/{1}", m, content5C[1].Id), _events[i].ToString()); // repub content1.content5.content52 - */ } #endregion @@ -986,7 +989,6 @@ namespace Umbraco.Tests.Integration var content = CreateContent(); Assert.IsNotNull(content); - content.TryPublishValues(); ServiceContext.ContentService.SaveAndPublish(content); ResetEvents(); @@ -1010,7 +1012,6 @@ namespace Umbraco.Tests.Integration var content = CreateContent(); Assert.IsNotNull(content); - content.TryPublishValues(); ServiceContext.ContentService.SaveAndPublish(content); ServiceContext.ContentService.MoveToRecycleBin(content); @@ -1035,7 +1036,6 @@ namespace Umbraco.Tests.Integration var content = CreateContent(); Assert.IsNotNull(content); - content.TryPublishValues(); ServiceContext.ContentService.SaveAndPublish(content); content.Properties.First().SetValue("changed"); ServiceContext.ContentService.Save(content); @@ -1206,7 +1206,6 @@ namespace Umbraco.Tests.Integration { var content = CreateContent(); Assert.IsNotNull(content); - content.TryPublishValues(); ServiceContext.ContentService.SaveAndPublish(content); ResetEvents(); @@ -1226,7 +1225,6 @@ namespace Umbraco.Tests.Integration { var content = CreateContent(); Assert.IsNotNull(content); - content.TryPublishValues(); ServiceContext.ContentService.SaveAndPublish(content); content.Properties.First().SetValue("changed"); ServiceContext.ContentService.Save(content); @@ -1248,11 +1246,9 @@ namespace Umbraco.Tests.Integration { var content1 = CreateContent(); Assert.IsNotNull(content1); - content1.TryPublishValues(); ServiceContext.ContentService.SaveAndPublish(content1); var content2 = CreateContent(content1.Id); Assert.IsNotNull(content2); - content2.TryPublishValues(); ServiceContext.ContentService.SaveAndPublish(content2); ServiceContext.ContentService.Unpublish(content1); @@ -1334,7 +1330,6 @@ namespace Umbraco.Tests.Integration { var content1 = CreateContent(); Assert.IsNotNull(content1); - content1.TryPublishValues(); ServiceContext.ContentService.SaveAndPublish(content1); var content2 = CreateContent(); Assert.IsNotNull(content2); @@ -1356,7 +1351,6 @@ namespace Umbraco.Tests.Integration { var content1 = CreateContent(); Assert.IsNotNull(content1); - content1.TryPublishValues(); ServiceContext.ContentService.SaveAndPublish(content1); content1.Properties.First().SetValue("changed"); ServiceContext.ContentService.Save(content1); @@ -1382,7 +1376,6 @@ namespace Umbraco.Tests.Integration Assert.IsNotNull(content1); var content2 = CreateContent(); Assert.IsNotNull(content2); - content2.TryPublishValues(); ServiceContext.ContentService.SaveAndPublish(content2); ResetEvents(); @@ -1404,11 +1397,9 @@ namespace Umbraco.Tests.Integration Assert.IsNotNull(content1); var content2 = CreateContent(); Assert.IsNotNull(content2); - content2.TryPublishValues(); ServiceContext.ContentService.SaveAndPublish(content2); var content3 = CreateContent(); Assert.IsNotNull(content3); - content3.TryPublishValues(); ServiceContext.ContentService.SaveAndPublish(content3); ServiceContext.ContentService.Unpublish(content2); @@ -1429,11 +1420,9 @@ namespace Umbraco.Tests.Integration { var content1 = CreateContent(); Assert.IsNotNull(content1); - content1.TryPublishValues(); ServiceContext.ContentService.SaveAndPublish(content1); var content2 = CreateContent(); Assert.IsNotNull(content2); - content2.TryPublishValues(); ServiceContext.ContentService.SaveAndPublish(content2); ResetEvents(); @@ -1453,15 +1442,12 @@ namespace Umbraco.Tests.Integration { var content1 = CreateContent(); Assert.IsNotNull(content1); - content1.TryPublishValues(); ServiceContext.ContentService.SaveAndPublish(content1); var content2 = CreateContent(); Assert.IsNotNull(content2); - content2.TryPublishValues(); ServiceContext.ContentService.SaveAndPublish(content2); var content3 = CreateContent(content2.Id); Assert.IsNotNull(content3); - content3.TryPublishValues(); ServiceContext.ContentService.SaveAndPublish(content3); ServiceContext.ContentService.Unpublish(content2); @@ -1482,13 +1468,11 @@ namespace Umbraco.Tests.Integration { var content1 = CreateContent(); Assert.IsNotNull(content1); - content1.TryPublishValues(); ServiceContext.ContentService.SaveAndPublish(content1); content1.Properties.First().SetValue("changed"); ServiceContext.ContentService.Save(content1); var content2 = CreateContent(); Assert.IsNotNull(content2); - content2.TryPublishValues(); ServiceContext.ContentService.SaveAndPublish(content2); ResetEvents(); @@ -1508,17 +1492,14 @@ namespace Umbraco.Tests.Integration { var content1 = CreateContent(); Assert.IsNotNull(content1); - content1.TryPublishValues(); ServiceContext.ContentService.SaveAndPublish(content1); content1.Properties.First().SetValue("changed"); ServiceContext.ContentService.Save(content1); var content2 = CreateContent(); Assert.IsNotNull(content2); - content2.TryPublishValues(); ServiceContext.ContentService.SaveAndPublish(content2); var content3 = CreateContent(content2.Id); Assert.IsNotNull(content3); - content3.TryPublishValues(); ServiceContext.ContentService.SaveAndPublish(content3); ServiceContext.ContentService.Unpublish(content2); @@ -1539,16 +1520,13 @@ namespace Umbraco.Tests.Integration { var content1 = CreateContent(); Assert.IsNotNull(content1); - content1.TryPublishValues(); ServiceContext.ContentService.SaveAndPublish(content1); var content2 = CreateContent(content1.Id); Assert.IsNotNull(content2); - content2.TryPublishValues(); ServiceContext.ContentService.SaveAndPublish(content2); ServiceContext.ContentService.Unpublish(content1); var content3 = CreateContent(); Assert.IsNotNull(content3); - content3.TryPublishValues(); ServiceContext.ContentService.SaveAndPublish(content3); ResetEvents(); @@ -1568,20 +1546,16 @@ namespace Umbraco.Tests.Integration { var content1 = CreateContent(); Assert.IsNotNull(content1); - content1.TryPublishValues(); ServiceContext.ContentService.SaveAndPublish(content1); var content2 = CreateContent(content1.Id); Assert.IsNotNull(content2); - content2.TryPublishValues(); ServiceContext.ContentService.SaveAndPublish(content2); ServiceContext.ContentService.Unpublish(content1); var content3 = CreateContent(); Assert.IsNotNull(content3); - content3.TryPublishValues(); ServiceContext.ContentService.SaveAndPublish(content3); var content4 = CreateContent(content3.Id); Assert.IsNotNull(content4); - content4.TryPublishValues(); ServiceContext.ContentService.SaveAndPublish(content4); ServiceContext.ContentService.Unpublish(content3); @@ -1602,18 +1576,15 @@ namespace Umbraco.Tests.Integration { var content1 = CreateContent(); Assert.IsNotNull(content1); - content1.TryPublishValues(); ServiceContext.ContentService.SaveAndPublish(content1); var content2 = CreateContent(content1.Id); Assert.IsNotNull(content2); - content2.TryPublishValues(); ServiceContext.ContentService.SaveAndPublish(content2); content2.Properties.First().SetValue("changed"); ServiceContext.ContentService.Save(content2); ServiceContext.ContentService.Unpublish(content1); var content3 = CreateContent(); Assert.IsNotNull(content3); - content3.TryPublishValues(); ServiceContext.ContentService.SaveAndPublish(content3); ResetEvents(); @@ -1633,22 +1604,18 @@ namespace Umbraco.Tests.Integration { var content1 = CreateContent(); Assert.IsNotNull(content1); - content1.TryPublishValues(); ServiceContext.ContentService.SaveAndPublish(content1); var content2 = CreateContent(content1.Id); Assert.IsNotNull(content2); - content2.TryPublishValues(); ServiceContext.ContentService.SaveAndPublish(content2); content2.Properties.First().SetValue("changed"); ServiceContext.ContentService.Save(content2); ServiceContext.ContentService.Unpublish(content1); var content3 = CreateContent(); Assert.IsNotNull(content3); - content3.TryPublishValues(); ServiceContext.ContentService.SaveAndPublish(content3); var content4 = CreateContent(content3.Id); Assert.IsNotNull(content4); - content4.TryPublishValues(); ServiceContext.ContentService.SaveAndPublish(content4); ServiceContext.ContentService.Unpublish(content3); @@ -1669,11 +1636,9 @@ namespace Umbraco.Tests.Integration { var content1 = CreateContent(); Assert.IsNotNull(content1); - content1.TryPublishValues(); ServiceContext.ContentService.SaveAndPublish(content1); var content2 = CreateContent(content1.Id); Assert.IsNotNull(content2); - content2.TryPublishValues(); ServiceContext.ContentService.SaveAndPublish(content2); ServiceContext.ContentService.Unpublish(content1); var content3 = CreateContent(); @@ -1696,11 +1661,9 @@ namespace Umbraco.Tests.Integration { var content1 = CreateContent(); Assert.IsNotNull(content1); - content1.TryPublishValues(); ServiceContext.ContentService.SaveAndPublish(content1); var content2 = CreateContent(content1.Id); Assert.IsNotNull(content2); - content2.TryPublishValues(); ServiceContext.ContentService.SaveAndPublish(content2); content2.Properties.First().SetValue("changed"); ServiceContext.ContentService.Save(content2); @@ -1780,7 +1743,6 @@ namespace Umbraco.Tests.Integration var content2 = CreateContent(); Assert.IsNotNull(content2); - content2.TryPublishValues(); ServiceContext.ContentService.SaveAndPublish(content2); ResetEvents(); @@ -1834,11 +1796,9 @@ namespace Umbraco.Tests.Integration var content2 = CreateContent(); Assert.IsNotNull(content2); - content2.TryPublishValues(); ServiceContext.ContentService.SaveAndPublish(content2); var content3 = CreateContent(content2.Id); Assert.IsNotNull(content3); - content3.TryPublishValues(); ServiceContext.ContentService.SaveAndPublish(content3); ServiceContext.ContentService.Unpublish(content2); @@ -1893,7 +1853,6 @@ namespace Umbraco.Tests.Integration var content2 = CreateContent(); Assert.IsNotNull(content2); - content2.TryPublishValues(); ServiceContext.ContentService.SaveAndPublish(content2); ServiceContext.ContentService.Move(content1, content2.Id); @@ -2003,11 +1962,9 @@ namespace Umbraco.Tests.Integration var content2 = CreateContent(); Assert.IsNotNull(content2); - content2.TryPublishValues(); ServiceContext.ContentService.SaveAndPublish(content2); var content3 = CreateContent(content2.Id); Assert.IsNotNull(content3); - content3.TryPublishValues(); ServiceContext.ContentService.SaveAndPublish(content3); ServiceContext.ContentService.Unpublish(content2); @@ -2083,7 +2040,6 @@ namespace Umbraco.Tests.Integration { var content = CreateContent(); Assert.IsNotNull(content); - content.TryPublishValues(); ServiceContext.ContentService.SaveAndPublish(content); ResetEvents(); @@ -2103,7 +2059,6 @@ namespace Umbraco.Tests.Integration { var content = CreateContent(); Assert.IsNotNull(content); - content.TryPublishValues(); ServiceContext.ContentService.SaveAndPublish(content); var content2 = CreateContent(); Assert.IsNotNull(content2); @@ -2126,7 +2081,6 @@ namespace Umbraco.Tests.Integration { var content = CreateBranch(); Assert.IsNotNull(content); - content.TryPublishValues(); ServiceContext.ContentService.SaveAndPublish(content); ResetEvents(); @@ -2168,17 +2122,14 @@ namespace Umbraco.Tests.Integration { var content = CreateContent(); Assert.IsNotNull(content); - content.TryPublishValues(); ServiceContext.ContentService.SaveAndPublish(content); var v1 = content.VersionId; content.Properties.First().SetValue("changed"); - content.TryPublishValues(); ServiceContext.ContentService.SaveAndPublish(content); var v2 = content.VersionId; content.Properties.First().SetValue("again"); - content.TryPublishValues(); ServiceContext.ContentService.SaveAndPublish(content); var v3 = content.VersionId; @@ -2187,7 +2138,7 @@ namespace Umbraco.Tests.Integration Console.WriteLine(v3); ResetEvents(); - content.CopyValues(ServiceContext.ContentService.GetVersion(v2)); + content.CopyFrom(ServiceContext.ContentService.GetVersion(v2)); ServiceContext.ContentService.Save(content); Assert.AreEqual(2, _msgCount); @@ -2212,12 +2163,10 @@ namespace Umbraco.Tests.Integration Assert.IsFalse(content.IsPropertyDirty("Published")); Assert.IsFalse(content.WasPropertyDirty("Published")); - content.TryPublishValues(); ServiceContext.ContentService.SaveAndPublish(content); Assert.IsFalse(content.IsPropertyDirty("Published")); Assert.IsTrue(content.WasPropertyDirty("Published")); // has just been published - content.TryPublishValues(); ServiceContext.ContentService.SaveAndPublish(content); Assert.IsFalse(content.IsPropertyDirty("Published")); Assert.IsFalse(content.WasPropertyDirty("Published")); // was published already diff --git a/src/Umbraco.Tests/Models/VariationTests.cs b/src/Umbraco.Tests/Models/VariationTests.cs index 861dee1eca..19da19bcf0 100644 --- a/src/Umbraco.Tests/Models/VariationTests.cs +++ b/src/Umbraco.Tests/Models/VariationTests.cs @@ -150,7 +150,7 @@ namespace Umbraco.Tests.Models Assert.AreEqual("a", prop.GetValue(published: true)); // can clear value - prop.ClearPublishedValues(); + prop.UnpublishValues(); Assert.AreEqual("b", prop.GetValue()); Assert.IsNull(prop.GetValue(published: true)); @@ -174,7 +174,7 @@ namespace Umbraco.Tests.Models Assert.AreEqual("c", prop.GetValue(langFr, published: true)); // can clear all - prop.ClearPublishedValues("*"); + prop.UnpublishValues("*"); Assert.IsNull(prop.GetValue()); Assert.IsNull(prop.GetValue(published: true)); Assert.AreEqual("c", prop.GetValue(langFr)); @@ -188,14 +188,14 @@ namespace Umbraco.Tests.Models Assert.AreEqual("c", prop.GetValue(langFr, published: true)); // same for culture - prop.ClearPublishedValues(langFr); + prop.UnpublishValues(langFr); Assert.AreEqual("c", prop.GetValue(langFr)); Assert.IsNull(prop.GetValue(langFr, published: true)); prop.PublishValues(langFr); Assert.AreEqual("c", prop.GetValue(langFr)); Assert.AreEqual("c", prop.GetValue(langFr, published: true)); - prop.ClearPublishedValues(); // does not throw, internal, content item throws + prop.UnpublishValues(); // does not throw, internal, content item throws Assert.IsNull(prop.GetValue()); Assert.IsNull(prop.GetValue(published: true)); prop.PublishValues(); // does not throw, internal, content item throws @@ -263,7 +263,7 @@ namespace Umbraco.Tests.Models // can publish value // and get edited and published values - Assert.IsTrue(content.TryPublishValues()); + Assert.IsTrue(content.PublishCulture()); Assert.AreEqual("a", content.GetValue("prop")); Assert.AreEqual("a", content.GetValue("prop", published: true)); @@ -274,7 +274,7 @@ namespace Umbraco.Tests.Models Assert.AreEqual("a", content.GetValue("prop", published: true)); // can clear value - content.ClearPublishedValues(); + content.UnpublishCulture(); Assert.AreEqual("b", content.GetValue("prop")); Assert.IsNull(content.GetValue("prop", published: true)); @@ -292,40 +292,40 @@ namespace Umbraco.Tests.Models // can publish value // and get edited and published values - Assert.IsFalse(content.TryPublishValues(langFr)); // no name + Assert.IsFalse(content.PublishCulture(langFr)); // no name content.SetCultureName("name-fr", langFr); - Assert.IsTrue(content.TryPublishValues(langFr)); + Assert.IsTrue(content.PublishCulture(langFr)); Assert.IsNull(content.GetValue("prop")); Assert.IsNull(content.GetValue("prop", published: true)); Assert.AreEqual("c", content.GetValue("prop", langFr)); Assert.AreEqual("c", content.GetValue("prop", langFr, published: true)); // can clear all - content.ClearPublishedValues("*"); + content.UnpublishCulture("*"); Assert.IsNull(content.GetValue("prop")); Assert.IsNull(content.GetValue("prop", published: true)); Assert.AreEqual("c", content.GetValue("prop", langFr)); Assert.IsNull(content.GetValue("prop", langFr, published: true)); // can publish all - Assert.IsTrue(content.TryPublishValues("*")); + Assert.IsTrue(content.PublishCulture("*")); Assert.IsNull(content.GetValue("prop")); Assert.IsNull(content.GetValue("prop", published: true)); Assert.AreEqual("c", content.GetValue("prop", langFr)); Assert.AreEqual("c", content.GetValue("prop", langFr, published: true)); // same for culture - content.ClearPublishedValues(langFr); + content.UnpublishCulture(langFr); Assert.AreEqual("c", content.GetValue("prop", langFr)); Assert.IsNull(content.GetValue("prop", langFr, published: true)); - content.TryPublishValues(langFr); + content.PublishCulture(langFr); Assert.AreEqual("c", content.GetValue("prop", langFr)); Assert.AreEqual("c", content.GetValue("prop", langFr, published: true)); - content.ClearPublishedValues(); // clears invariant props if any + content.UnpublishCulture(); // clears invariant props if any Assert.IsNull(content.GetValue("prop")); Assert.IsNull(content.GetValue("prop", published: true)); - content.TryPublishValues(); // publishes invariant props if any + content.PublishCulture(); // publishes invariant props if any Assert.IsNull(content.GetValue("prop")); Assert.IsNull(content.GetValue("prop", published: true)); @@ -334,14 +334,14 @@ namespace Umbraco.Tests.Models other.SetValue("prop", "o1", langFr); // can copy other's edited value - content.CopyAllValues(other); + content.CopyFrom(other); Assert.IsNull(content.GetValue("prop")); Assert.IsNull(content.GetValue("prop", published: true)); Assert.AreEqual("o1", content.GetValue("prop", langFr)); Assert.AreEqual("c", content.GetValue("prop", langFr, published: true)); // can copy self's published value - content.CopyAllValues(content); + content.CopyFrom(content); Assert.IsNull(content.GetValue("prop")); Assert.IsNull(content.GetValue("prop", published: true)); Assert.AreEqual("c", content.GetValue("prop", langFr)); @@ -370,16 +370,16 @@ namespace Umbraco.Tests.Models content.SetCultureName("hello", langFr); - Assert.IsFalse(content.TryPublishValues(langFr)); // fails because prop1 is mandatory + Assert.IsFalse(content.PublishCulture(langFr)); // fails because prop1 is mandatory content.SetValue("prop1", "a", langFr); - Assert.IsTrue(content.TryPublishValues(langFr)); + Assert.IsTrue(content.PublishCulture(langFr)); Assert.AreEqual("a", content.GetValue("prop1", langFr, published: true)); //this will be null because we tried to publish values for a specific culture but this property is invariant Assert.IsNull(content.GetValue("prop2", published: true)); - Assert.IsFalse(content.TryPublishValues()); // fails because prop2 is mandatory + Assert.IsFalse(content.PublishCulture()); // fails because prop2 is mandatory content.SetValue("prop2", "b"); - Assert.IsTrue(content.TryPublishValues()); + Assert.IsTrue(content.PublishCulture()); Assert.AreEqual("b", content.GetValue("prop2", published: true)); } @@ -406,12 +406,12 @@ namespace Umbraco.Tests.Models content.SetValue("prop", "a-es", langEs); // cannot publish without a name - Assert.IsFalse(content.TryPublishValues(langFr)); + Assert.IsFalse(content.PublishCulture(langFr)); // works with a name // and then FR is available, and published content.SetCultureName("name-fr", langFr); - Assert.IsTrue(content.TryPublishValues(langFr)); + Assert.IsTrue(content.PublishCulture(langFr)); // now UK is available too content.SetCultureName("name-uk", langUk); diff --git a/src/Umbraco.Tests/Persistence/NPocoTests/PetaPocoCachesTest.cs b/src/Umbraco.Tests/Persistence/NPocoTests/PetaPocoCachesTest.cs index 0afc581f83..21a75b2e24 100644 --- a/src/Umbraco.Tests/Persistence/NPocoTests/PetaPocoCachesTest.cs +++ b/src/Umbraco.Tests/Persistence/NPocoTests/PetaPocoCachesTest.cs @@ -187,13 +187,13 @@ namespace Umbraco.Tests.Persistence.NPocoTests contentTypeService.Save(contentType); var content1 = MockedContent.CreateSimpleContent(contentType, "Tagged content 1", -1); content1.AssignTags("tags", new[] { "hello", "world", "some", "tags" }); - content1.TryPublishValues(); + content1.PublishCulture(); contentService.SaveAndPublish(content1); id2 = content1.Id; var content2 = MockedContent.CreateSimpleContent(contentType, "Tagged content 2", -1); content2.AssignTags("tags", new[] { "hello", "world", "some", "tags" }); - content2.TryPublishValues(); + content2.PublishCulture(); contentService.SaveAndPublish(content2); id3 = content2.Id; diff --git a/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs index 805ea2d862..17232d89ef 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs @@ -140,7 +140,7 @@ namespace Umbraco.Tests.Persistence.Repositories // publish = new edit version content1.SetValue("title", "title"); - ((Content)content1).TryPublishValues(); + ((Content)content1).PublishCulture(); ((Content)content1).PublishedState = PublishedState.Publishing; repository.Save(content1); @@ -202,7 +202,7 @@ namespace Umbraco.Tests.Persistence.Repositories Assert.AreEqual(false, scope.Database.ExecuteScalar($"SELECT published FROM {Constants.DatabaseSchema.Tables.Document} WHERE nodeId=@id", new { id = content1.Id })); // publish = version - ((Content)content1).TryPublishValues(); + ((Content)content1).PublishCulture(); ((Content)content1).PublishedState = PublishedState.Publishing; repository.Save(content1); @@ -238,7 +238,7 @@ namespace Umbraco.Tests.Persistence.Repositories // publish = new version content1.Name = "name-4"; content1.SetValue("title", "title-4"); - ((Content)content1).TryPublishValues(); + ((Content)content1).PublishCulture(); ((Content)content1).PublishedState = PublishedState.Publishing; repository.Save(content1); @@ -648,7 +648,7 @@ namespace Umbraco.Tests.Persistence.Repositories // publish them all foreach (var content in result) { - content.TryPublishValues(); + content.PublishCulture(); repository.Save(content); } diff --git a/src/Umbraco.Tests/Scoping/ScopedNuCacheTests.cs b/src/Umbraco.Tests/Scoping/ScopedNuCacheTests.cs index d59fe0bb51..cf3285cd7e 100644 --- a/src/Umbraco.Tests/Scoping/ScopedNuCacheTests.cs +++ b/src/Umbraco.Tests/Scoping/ScopedNuCacheTests.cs @@ -152,7 +152,6 @@ namespace Umbraco.Tests.Scoping using (var scope = ScopeProvider.CreateScope()) { - item.TryPublishValues(); Current.Services.ContentService.SaveAndPublish(item); scope.Complete(); } @@ -167,7 +166,6 @@ namespace Umbraco.Tests.Scoping using (var scope = ScopeProvider.CreateScope()) { item.Name = "changed"; - item.TryPublishValues(); Current.Services.ContentService.SaveAndPublish(item); if (complete) diff --git a/src/Umbraco.Tests/Scoping/ScopedXmlTests.cs b/src/Umbraco.Tests/Scoping/ScopedXmlTests.cs index 9de2012dce..5f4e653735 100644 --- a/src/Umbraco.Tests/Scoping/ScopedXmlTests.cs +++ b/src/Umbraco.Tests/Scoping/ScopedXmlTests.cs @@ -123,10 +123,8 @@ namespace Umbraco.Tests.Scoping using (var scope = ScopeProvider.CreateScope()) { - item.TryPublishValues(); Current.Services.ContentService.SaveAndPublish(item); // should create an xml clone item.Name = "changed"; - item.TryPublishValues(); Current.Services.ContentService.SaveAndPublish(item); // should re-use the xml clone // this should never change @@ -230,13 +228,11 @@ namespace Umbraco.Tests.Scoping using (var scope = ScopeProvider.CreateScope()) { - item.TryPublishValues(); Current.Services.ContentService.SaveAndPublish(item); for (var i = 0; i < count; i++) { var temp = new Content("content_" + i, -1, contentType); - temp.TryPublishValues(); Current.Services.ContentService.SaveAndPublish(temp); ids[i] = temp.Id; } diff --git a/src/Umbraco.Tests/Services/ContentServiceTests.cs b/src/Umbraco.Tests/Services/ContentServiceTests.cs index 92c38cee67..d5003674af 100644 --- a/src/Umbraco.Tests/Services/ContentServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentServiceTests.cs @@ -209,16 +209,16 @@ namespace Umbraco.Tests.Services { var contentService = ServiceContext.ContentService; var root = ServiceContext.ContentService.GetById(NodeDto.NodeIdSeed + 1); - root.TryPublishValues(); + root.PublishCulture(); Assert.IsTrue(contentService.SaveAndPublish(root).Success); var content = contentService.CreateAndSave("Test", -1, "umbTextpage", 0); - content.TryPublishValues(); + content.PublishCulture(); Assert.IsTrue(contentService.SaveAndPublish(content).Success); var hierarchy = CreateContentHierarchy().OrderBy(x => x.Level).ToArray(); contentService.Save(hierarchy, 0); foreach (var c in hierarchy) { - c.TryPublishValues(); + c.PublishCulture(); Assert.IsTrue(contentService.SaveAndPublish(c).Success); } @@ -268,7 +268,6 @@ namespace Umbraco.Tests.Services // Assert - content.TryPublishValues(); Assert.IsTrue(contentService.SaveAndPublish(content).Success); } @@ -283,7 +282,6 @@ namespace Umbraco.Tests.Services for (var i = 0; i < 20; i++) { content.SetValue("bodyText", "hello world " + Guid.NewGuid()); - content.TryPublishValues(); contentService.SaveAndPublish(content); } @@ -422,12 +420,10 @@ namespace Umbraco.Tests.Services var content1 = MockedContent.CreateSimpleContent(contentType, "Tagged content 1", -1); content1.AssignTags("tags", new[] { "hello", "world", "some", "tags", "plus" }); - content1.TryPublishValues(); contentService.SaveAndPublish(content1); var content2 = MockedContent.CreateSimpleContent(contentType, "Tagged content 2", -1); content2.AssignTags("tags", new[] { "hello", "world", "some", "tags" }); - content2.TryPublishValues(); contentService.SaveAndPublish(content2); // verify @@ -463,7 +459,7 @@ namespace Umbraco.Tests.Services allTags = tagService.GetAllContentTags(); Assert.AreEqual(4, allTags.Count()); - content1.TryPublishValues(); + content1.PublishCulture(); contentService.SaveAndPublish(content1); Assert.IsTrue(content1.Published); @@ -493,12 +489,10 @@ namespace Umbraco.Tests.Services var content1 = MockedContent.CreateSimpleContent(contentType, "Tagged content 1", -1); content1.AssignTags("tags", new[] { "hello", "world", "some", "tags", "bam" }); - content1.TryPublishValues(); contentService.SaveAndPublish(content1); var content2 = MockedContent.CreateSimpleContent(contentType, "Tagged content 2", -1); content2.AssignTags("tags", new[] { "hello", "world", "some", "tags" }); - content2.TryPublishValues(); contentService.SaveAndPublish(content2); // verify @@ -534,9 +528,9 @@ namespace Umbraco.Tests.Services allTags = tagService.GetAllContentTags(); Assert.AreEqual(0, allTags.Count()); - content1.TryPublishValues(); + content1.PublishCulture(); contentService.SaveAndPublish(content1); - content2.TryPublishValues(); + content2.PublishCulture(); contentService.SaveAndPublish(content2); // tags are back @@ -565,12 +559,12 @@ namespace Umbraco.Tests.Services var content1 = MockedContent.CreateSimpleContent(contentType, "Tagged content 1", -1); content1.AssignTags("tags", new[] { "hello", "world", "some", "tags", "plus" }); - content1.TryPublishValues(); + content1.PublishCulture(); contentService.SaveAndPublish(content1); var content2 = MockedContent.CreateSimpleContent(contentType, "Tagged content 2", content1.Id); content2.AssignTags("tags", new[] { "hello", "world", "some", "tags" }); - content2.TryPublishValues(); + content2.PublishCulture(); contentService.SaveAndPublish(content2); // verify @@ -605,7 +599,7 @@ namespace Umbraco.Tests.Services allTags = tagService.GetAllContentTags(); Assert.AreEqual(0, allTags.Count()); - content1.TryPublishValues(); + content1.PublishCulture(); contentService.SaveAndPublish(content1); Assert.IsTrue(content1.Published); @@ -643,12 +637,10 @@ namespace Umbraco.Tests.Services var content1 = MockedContent.CreateSimpleContent(contentType, "Tagged content 1", -1); content1.AssignTags("tags", new[] { "hello", "world", "some", "tags", "bam" }); - content1.TryPublishValues(); contentService.SaveAndPublish(content1); var content2 = MockedContent.CreateSimpleContent(contentType, "Tagged content 2", -1); content2.AssignTags("tags", new[] { "hello", "world", "some", "tags" }); - content2.TryPublishValues(); contentService.SaveAndPublish(content2); contentService.Unpublish(content1); @@ -664,7 +656,7 @@ namespace Umbraco.Tests.Services var allTags = tagService.GetAllContentTags(); Assert.AreEqual(0, allTags.Count()); - content2.TryPublishValues(); + content2.PublishCulture(); contentService.SaveAndPublish(content2); tags = tagService.GetTagsForEntity(content2.Id); @@ -690,12 +682,12 @@ namespace Umbraco.Tests.Services var content1 = MockedContent.CreateSimpleContent(contentType, "Tagged content 1", -1); content1.AssignTags("tags", new[] { "hello", "world", "some", "tags", "bam" }); - content1.TryPublishValues(); + content1.PublishCulture(); contentService.SaveAndPublish(content1); var content2 = MockedContent.CreateSimpleContent(contentType, "Tagged content 2", content1); content2.AssignTags("tags", new[] { "hello", "world", "some", "tags" }); - content2.TryPublishValues(); + content2.PublishCulture(); contentService.SaveAndPublish(content2); contentService.Unpublish(content1); @@ -711,7 +703,7 @@ namespace Umbraco.Tests.Services var allTags = tagService.GetAllContentTags(); Assert.AreEqual(0, allTags.Count()); - content1.TryPublishValues(); + content1.PublishCulture(); contentService.SaveAndPublish(content1); tags = tagService.GetTagsForEntity(content2.Id); @@ -795,7 +787,6 @@ namespace Umbraco.Tests.Services // create a content with tags and publish var content = MockedContent.CreateSimpleContent(contentType, "Tagged content", -1); content.AssignTags("tags", new[] { "hello", "world", "some", "tags" }); - content.TryPublishValues(); contentService.SaveAndPublish(content); // edit tags and save @@ -835,7 +826,6 @@ namespace Umbraco.Tests.Services // Act content.AssignTags("tags", new[] { "hello", "world", "some", "tags" }); - content.TryPublishValues(); contentService.SaveAndPublish(content); // Assert @@ -866,12 +856,10 @@ namespace Umbraco.Tests.Services contentTypeService.Save(contentType); var content = MockedContent.CreateSimpleContent(contentType, "Tagged content", -1); content.AssignTags("tags", new[] { "hello", "world", "some", "tags" }); - content.TryPublishValues(); contentService.SaveAndPublish(content); // Act content.AssignTags("tags", new[] { "another", "world" }, merge: true); - content.TryPublishValues(); contentService.SaveAndPublish(content); // Assert @@ -902,12 +890,10 @@ namespace Umbraco.Tests.Services contentTypeService.Save(contentType); var content = MockedContent.CreateSimpleContent(contentType, "Tagged content", -1); content.AssignTags("tags", new[] { "hello", "world", "some", "tags" }); - content.TryPublishValues(); contentService.SaveAndPublish(content); // Act content.RemoveTags("tags", new[] { "some", "world" }); - content.TryPublishValues(); contentService.SaveAndPublish(content); // Assert @@ -1090,7 +1076,6 @@ namespace Umbraco.Tests.Services var parent = ServiceContext.ContentService.GetById(NodeDto.NodeIdSeed + 2); Assert.IsFalse(parent.Published); - parent.TryPublishValues(); ServiceContext.ContentService.SaveAndPublish(parent); // publishing parent, so Text Page 2 can be updated. var content = contentService.GetById(NodeDto.NodeIdSeed + 4); @@ -1103,7 +1088,6 @@ namespace Umbraco.Tests.Services content.Name = "Text Page 2 Updated"; content.SetValue("author", "Jane Doe"); - content.TryPublishValues(); contentService.SaveAndPublish(content); // publishes the current version, creates a version var version2 = content.VersionId; @@ -1111,7 +1095,6 @@ namespace Umbraco.Tests.Services content.Name = "Text Page 2 ReUpdated"; content.SetValue("author", "Bob Hope"); - content.TryPublishValues(); contentService.SaveAndPublish(content); // publishes again, creates a version var version3 = content.VersionId; @@ -1178,11 +1161,9 @@ namespace Umbraco.Tests.Services // Arrange var contentService = ServiceContext.ContentService; var root = contentService.GetById(NodeDto.NodeIdSeed + 2); - root.TryPublishValues(); contentService.SaveAndPublish(root); var content = contentService.GetById(NodeDto.NodeIdSeed + 4); content.ExpireDate = DateTime.Now.AddSeconds(1); - content.TryPublishValues(); contentService.SaveAndPublish(content); // Act @@ -1232,8 +1213,7 @@ namespace Umbraco.Tests.Services // Arrange var contentService = ServiceContext.ContentService; var content = contentService.GetById(NodeDto.NodeIdSeed + 2); - content.TryPublishValues(); - var published = contentService.SaveAndPublish(content, 0); + var published = contentService.SaveAndPublish(content, userId: 0); using (var scope = ScopeProvider.CreateScope()) { @@ -1255,15 +1235,30 @@ namespace Umbraco.Tests.Services } [Test] - public void Can_Publish_Content() + public void Can_Publish_Content_1() { // Arrange var contentService = ServiceContext.ContentService; var content = contentService.GetById(NodeDto.NodeIdSeed + 2); // Act - content.TryPublishValues(); - var published = contentService.SaveAndPublish(content, 0); + content.PublishCulture(); + var published = contentService.SavePublishing(content, 0); + + // Assert + Assert.That(published.Success, Is.True); + Assert.That(content.Published, Is.True); + } + + [Test] + public void Can_Publish_Content_2() + { + // Arrange + var contentService = ServiceContext.ContentService; + var content = contentService.GetById(NodeDto.NodeIdSeed + 2); + + // Act + var published = contentService.SaveAndPublish(content, userId: 0); // Assert Assert.That(published.Success, Is.True); @@ -1276,7 +1271,7 @@ namespace Umbraco.Tests.Services // Arrange var contentService = ServiceContext.ContentService; var parent = contentService.Create("parent", -1, "umbTextpage"); - parent.TryPublishValues(); + contentService.SaveAndPublish(parent); var content = contentService.Create("child", parent, "umbTextpage"); contentService.Save(content); @@ -1302,8 +1297,8 @@ namespace Umbraco.Tests.Services Assert.AreEqual("Home", content.Name); content.Name = "foo"; - content.TryPublishValues(); - var published = contentService.SaveAndPublish(content, 0); + content.PublishCulture(); + var published = contentService.SavePublishing(content, 0); Assert.That(published.Success, Is.True); Assert.That(content.Published, Is.True); @@ -1343,17 +1338,14 @@ namespace Umbraco.Tests.Services var parent = contentService.GetById(parentId); - var parentCanPublishValues = parent.TryPublishValues(); var parentPublished = contentService.SaveAndPublish(parent); // parent can publish values // and therefore can be published - Assert.IsTrue(parentCanPublishValues); Assert.IsTrue(parentPublished.Success); Assert.IsTrue(parent.Published); - var contentCanPublishValues = content.TryPublishValues(); - var contentPublished = contentService.SaveAndPublish(content); + var contentCanPublishValues = content.PublishCulture(); // content cannot publish values because they are invalid Assert.IsFalse(contentCanPublishValues); @@ -1361,8 +1353,9 @@ namespace Umbraco.Tests.Services // and therefore cannot be published, // because it did not have a published version at all + var contentPublished = contentService.SaveAndPublish(content); Assert.IsFalse(contentPublished.Success); - Assert.AreEqual(PublishResultType.FailedNoPublishedValues, contentPublished.Result); + Assert.AreEqual(PublishResultType.FailedContentInvalid, contentPublished.Result); Assert.IsFalse(content.Published); } @@ -1428,12 +1421,12 @@ namespace Umbraco.Tests.Services contentService.Save(content); var parent = contentService.GetById(NodeDto.NodeIdSeed + 2); - parent.TryPublishValues(); - var parentPublished = contentService.SaveAndPublish(parent, 0);//Publish root Home node to enable publishing of 'NodeDto.NodeIdSeed + 3' + parent.PublishCulture(); + var parentPublished = contentService.SavePublishing(parent, 0);//Publish root Home node to enable publishing of 'NodeDto.NodeIdSeed + 3' // Act - content.TryPublishValues(); - var published = contentService.SaveAndPublish(content, 0); + content.PublishCulture(); + var published = contentService.SavePublishing(content, 0); // Assert Assert.That(parentPublished.Success, Is.True); @@ -1451,12 +1444,12 @@ namespace Umbraco.Tests.Services contentService.Save(content, 0); var parent = contentService.GetById(NodeDto.NodeIdSeed + 2); - parent.TryPublishValues(); - var parentPublished = contentService.SaveAndPublish(parent, 0);//Publish root Home node to enable publishing of 'NodeDto.NodeIdSeed + 3' + parent.PublishCulture(); + var parentPublished = contentService.SavePublishing(parent, 0);//Publish root Home node to enable publishing of 'NodeDto.NodeIdSeed + 3' // Act - content.TryPublishValues(); - var published = contentService.SaveAndPublish(content, 0); + content.PublishCulture(); + var published = contentService.SavePublishing(content, 0); // Assert Assert.That(parentPublished.Success, Is.True); @@ -1488,8 +1481,8 @@ namespace Umbraco.Tests.Services var content = contentService.GetById(NodeDto.NodeIdSeed + 5); // Act - content.TryPublishValues(); - var published = contentService.SaveAndPublish(content, 0); + content.PublishCulture(); + var published = contentService.SavePublishing(content, 0); // Assert Assert.That(published.Success, Is.False); @@ -1506,8 +1499,8 @@ namespace Umbraco.Tests.Services content.SetValue("author", "Barack Obama"); // Act - content.TryPublishValues(); - var published = contentService.SaveAndPublish(content, 0); + content.PublishCulture(); + var published = contentService.SavePublishing(content, 0); // Assert Assert.That(content.HasIdentity, Is.True); @@ -1531,15 +1524,15 @@ namespace Umbraco.Tests.Services content.SetValue("author", "Barack Obama"); // Act - content.TryPublishValues(); - var published = contentService.SaveAndPublish(content, 0); + content.PublishCulture(); + var published = contentService.SavePublishing(content, 0); var childContent = contentService.Create("Child", content.Id, "umbTextpage", 0); // Reset all identity properties childContent.Id = 0; childContent.Path = null; ((Content)childContent).ResetIdentity(); - childContent.TryPublishValues(); - var childPublished = contentService.SaveAndPublish(childContent, 0); + childContent.PublishCulture(); + var childPublished = contentService.SavePublishing(childContent, 0); // Assert Assert.That(content.HasIdentity, Is.True); @@ -1557,12 +1550,10 @@ namespace Umbraco.Tests.Services var contentService = ServiceContext.ContentService; var root = contentService.GetById(NodeDto.NodeIdSeed + 2); - root.TryPublishValues(); var rootPublished = contentService.SaveAndPublish(root); var content = contentService.GetById(NodeDto.NodeIdSeed + 4); content.Properties["title"].SetValue(content.Properties["title"].GetValue() + " Published"); - content.TryPublishValues(); var contentPublished = contentService.SaveAndPublish(content); var publishedVersion = content.VersionId; @@ -1862,14 +1853,14 @@ namespace Umbraco.Tests.Services content1.PropertyValues(obj); content1.ResetDirtyProperties(false); ServiceContext.ContentService.Save(content1, 0); - content1.TryPublishValues(); - Assert.IsTrue(ServiceContext.ContentService.SaveAndPublish(content1, 0).Success); + content1.PublishCulture(); + Assert.IsTrue(ServiceContext.ContentService.SavePublishing(content1, 0).Success); var content2 = MockedContent.CreateBasicContent(contentType); content2.PropertyValues(obj); content2.ResetDirtyProperties(false); ServiceContext.ContentService.Save(content2, 0); - content2.TryPublishValues(); - Assert.IsTrue(ServiceContext.ContentService.SaveAndPublish(content2, 0).Success); + content2.PublishCulture(); + Assert.IsTrue(ServiceContext.ContentService.SavePublishing(content2, 0).Success); var editorGroup = ServiceContext.UserService.GetUserGroupByAlias("editor"); editorGroup.StartContentId = content1.Id; @@ -2026,7 +2017,6 @@ namespace Umbraco.Tests.Services Assert.AreEqual(0, contentTags.Length); // publish - content.TryPublishValues(); contentService.SaveAndPublish(content); // now tags have been set (published) @@ -2043,7 +2033,6 @@ namespace Umbraco.Tests.Services Assert.AreEqual(0, copiedTags.Length); // publish - copy.TryPublishValues(); contentService.SaveAndPublish(copy); // now tags have been set (published) @@ -2062,7 +2051,6 @@ namespace Umbraco.Tests.Services var parent = ServiceContext.ContentService.GetById(NodeDto.NodeIdSeed + 2); Assert.IsFalse(parent.Published); - parent.TryPublishValues(); ServiceContext.ContentService.SaveAndPublish(parent); // publishing parent, so Text Page 2 can be updated. var content = contentService.GetById(NodeDto.NodeIdSeed + 4); @@ -2079,7 +2067,6 @@ namespace Umbraco.Tests.Services // non published = edited Assert.IsTrue(content.Edited); - content.TryPublishValues(); contentService.SaveAndPublish(content); // new version var version2 = content.VersionId; Assert.AreNotEqual(version1, version2); @@ -2104,7 +2091,6 @@ namespace Umbraco.Tests.Services content.Name = "Text Page 2 ReReUpdated"; - content.TryPublishValues(); contentService.SaveAndPublish(content); // new version var version3 = content.VersionId; Assert.AreNotEqual(version2, version3); @@ -2124,7 +2110,7 @@ namespace Umbraco.Tests.Services // rollback all values to version1 var rollback = contentService.GetById(NodeDto.NodeIdSeed + 4); var rollto = contentService.GetVersion(version1); - rollback.CopyValues(rollto); + rollback.CopyFrom(rollto); rollback.Name = rollto.Name; // must do it explicitely contentService.Save(rollback); @@ -2146,7 +2132,7 @@ namespace Umbraco.Tests.Services // special because... current has edits... this really is equivalent to rolling back to version2 var rollback2 = contentService.GetById(NodeDto.NodeIdSeed + 4); var rollto2 = contentService.GetVersion(version3); - rollback2.CopyValues(rollto2); + rollback2.CopyFrom(rollto2); rollback2.Name = rollto2.PublishName; // must do it explicitely AND must pick the publish one! contentService.Save(rollback2); @@ -2160,7 +2146,6 @@ namespace Umbraco.Tests.Services content = contentService.GetById(content.Id); Assert.AreEqual("Text Page 2 ReReUpdated", content.Name); Assert.AreEqual("Jane Doe", content.GetValue("author")); - content.TryPublishValues(); contentService.SaveAndPublish(content); Assert.IsFalse(content.Edited); content.Name = "Xxx"; @@ -2168,7 +2153,7 @@ namespace Umbraco.Tests.Services contentService.Save(content); Assert.IsTrue(content.Edited); rollto = contentService.GetVersion(content.VersionId); - content.CopyValues(rollto); + content.CopyFrom(rollto); content.Name = rollto.PublishName; // must do it explicitely AND must pick the publish one! contentService.Save(content); Assert.IsFalse(content.Edited); @@ -2278,7 +2263,6 @@ namespace Umbraco.Tests.Services Assert.IsFalse(scope.Database.Exists(content.Id)); } - content.TryPublishValues(); contentService.SaveAndPublish(content); using (var scope = ScopeProvider.CreateScope()) @@ -2420,7 +2404,6 @@ namespace Umbraco.Tests.Services // becomes Published, !Edited // creates a new version // can get published property values - content.TryPublishValues(); contentService.SaveAndPublish(content); Assert.IsTrue(content.Published); @@ -2578,7 +2561,9 @@ namespace Umbraco.Tests.Services { var languageService = ServiceContext.LocalizationService; - var langFr = new Language("fr-FR") { IsDefaultVariantLanguage = true }; + //var langFr = new Language("fr-FR") { IsDefaultVariantLanguage = true }; + var langXx = new Language("pt-PT") { IsDefaultVariantLanguage = true }; + var langFr = new Language("fr-FR"); var langUk = new Language("en-UK"); var langDe = new Language("de-DE"); @@ -2645,9 +2630,9 @@ namespace Umbraco.Tests.Services // act - content.TryPublishValues(langFr.IsoCode); - content.TryPublishValues(langUk.IsoCode); - contentService.SaveAndPublish(content); + content.PublishCulture(langFr.IsoCode); + content.PublishCulture(langUk.IsoCode); + contentService.SavePublishing(content); // both FR and UK have been published, // and content has been published, @@ -2692,7 +2677,6 @@ namespace Umbraco.Tests.Services // act - content.TryPublishValues(); contentService.SaveAndPublish(content); // now it has publish name for invariant neutral @@ -2752,11 +2736,12 @@ namespace Umbraco.Tests.Services // act // cannot just 'save' since we are changing what's published! - content.ClearPublishedValues(langFr.IsoCode); - contentService.SaveAndPublish(content); + content.UnpublishCulture(langFr.IsoCode); + contentService.SavePublishing(content); // content has been published, // the french culture is gone + // (only if french is not mandatory, else everything would be gone!) content2 = contentService.GetById(content.Id); @@ -2841,7 +2826,9 @@ namespace Umbraco.Tests.Services // act - contentService.SaveAndPublish(content); + // that HAS to be SavePublishing, because SaveAndPublish would just republish everything! + + contentService.SavePublishing(content); // content has been re-published, // everything is back to what it was before being unpublished @@ -2881,8 +2868,8 @@ namespace Umbraco.Tests.Services // act - content.TryPublishValues(langUk.IsoCode); - contentService.SaveAndPublish(content); + content.PublishCulture(langUk.IsoCode); + contentService.SavePublishing(content); content2 = contentService.GetById(content.Id); diff --git a/src/Umbraco.Tests/Services/ContentTypeServiceTests.cs b/src/Umbraco.Tests/Services/ContentTypeServiceTests.cs index b21f96a4bf..18c62c49cb 100644 --- a/src/Umbraco.Tests/Services/ContentTypeServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentTypeServiceTests.cs @@ -88,7 +88,6 @@ namespace Umbraco.Tests.Services var contentType = contentTypes[index]; var contentItem = MockedContent.CreateSimpleContent(contentType, "MyName_" + index + "_" + i, parentId); ServiceContext.ContentService.Save(contentItem); - contentItem.TryPublishValues(); ServiceContext.ContentService.SaveAndPublish(contentItem); parentId = contentItem.Id; @@ -189,7 +188,6 @@ namespace Umbraco.Tests.Services var contentType = contentTypes[index]; var contentItem = MockedContent.CreateSimpleContent(contentType, "MyName_" + index + "_" + i, parentId); ServiceContext.ContentService.Save(contentItem); - contentItem.TryPublishValues(); ServiceContext.ContentService.SaveAndPublish(contentItem); parentId = contentItem.Id; } @@ -225,19 +223,16 @@ namespace Umbraco.Tests.Services var root = MockedContent.CreateSimpleContent(contentType1, "Root", -1); ServiceContext.ContentService.Save(root); - root.TryPublishValues(); ServiceContext.ContentService.SaveAndPublish(root); var level1 = MockedContent.CreateSimpleContent(contentType2, "L1", root.Id); ServiceContext.ContentService.Save(level1); - level1.TryPublishValues(); ServiceContext.ContentService.SaveAndPublish(level1); for (int i = 0; i < 2; i++) { var level3 = MockedContent.CreateSimpleContent(contentType3, "L2" + i, level1.Id); ServiceContext.ContentService.Save(level3); - level3.TryPublishValues(); ServiceContext.ContentService.SaveAndPublish(level3); } @@ -267,7 +262,6 @@ namespace Umbraco.Tests.Services ServiceContext.FileService.SaveTemplate(contentType1.DefaultTemplate); ServiceContext.ContentTypeService.Save(contentType1); IContent contentItem = MockedContent.CreateTextpageContent(contentType1, "Testing", -1); - contentItem.TryPublishValues(); ServiceContext.ContentService.SaveAndPublish(contentItem); var initProps = contentItem.Properties.Count; var initPropTypes = contentItem.PropertyTypes.Count(); @@ -297,14 +291,12 @@ namespace Umbraco.Tests.Services var contentItems1 = MockedContent.CreateTextpageContent(contentType1, -1, 10).ToArray(); foreach (var x in contentItems1) { - x.TryPublishValues(); ServiceContext.ContentService.SaveAndPublish(x); } var contentItems2 = MockedContent.CreateTextpageContent(contentType2, -1, 5).ToArray(); foreach (var x in contentItems2) { - x.TryPublishValues(); ServiceContext.ContentService.SaveAndPublish(x); } @@ -362,7 +354,6 @@ namespace Umbraco.Tests.Services var contentItems1 = MockedContent.CreateTextpageContent(contentType1, -1, 10).ToArray(); foreach (var x in contentItems1) { - x.TryPublishValues(); ServiceContext.ContentService.SaveAndPublish(x); } var alias = contentType1.PropertyTypes.First().Alias; @@ -496,7 +487,6 @@ namespace Umbraco.Tests.Services // Act var homeDoc = cs.Create("Home Page", -1, contentTypeAlias); - homeDoc.TryPublishValues(); cs.SaveAndPublish(homeDoc); // Assert diff --git a/src/Umbraco.Tests/Services/PerformanceTests.cs b/src/Umbraco.Tests/Services/PerformanceTests.cs index c0daaa8fb9..664d15fc71 100644 --- a/src/Umbraco.Tests/Services/PerformanceTests.cs +++ b/src/Umbraco.Tests/Services/PerformanceTests.cs @@ -249,7 +249,7 @@ namespace Umbraco.Tests.Services var result = new List(); ServiceContext.ContentTypeService.Save(contentType1); IContent lastParent = MockedContent.CreateSimpleContent(contentType1); - lastParent.TryPublishValues(); + lastParent.PublishCulture(); ServiceContext.ContentService.SaveAndPublish(lastParent); result.Add(lastParent); //create 20 deep @@ -263,7 +263,7 @@ namespace Umbraco.Tests.Services //only publish evens if (j % 2 == 0) { - content.TryPublishValues(); + content.PublishCulture(); ServiceContext.ContentService.SaveAndPublish(content); } else diff --git a/src/Umbraco.Tests/Services/TagServiceTests.cs b/src/Umbraco.Tests/Services/TagServiceTests.cs index c24c729bf8..49aad03efb 100644 --- a/src/Umbraco.Tests/Services/TagServiceTests.cs +++ b/src/Umbraco.Tests/Services/TagServiceTests.cs @@ -36,21 +36,17 @@ namespace Umbraco.Tests.Services IContent content1 = MockedContent.CreateSimpleContent(contentType, "Tagged content 1", -1); content1.AssignTags("tags", new[] { "cow", "pig", "goat" }); - content1.TryPublishValues(); contentService.SaveAndPublish(content1); // change content1.AssignTags("tags", new[] { "elephant" }, true); content1.RemoveTags("tags", new[] { "cow" }); - content1.TryPublishValues(); contentService.SaveAndPublish(content1); // more changes content1.AssignTags("tags", new[] { "mouse" }, true); - content1.TryPublishValues(); contentService.SaveAndPublish(content1); content1.RemoveTags("tags", new[] { "mouse" }); - content1.TryPublishValues(); contentService.SaveAndPublish(content1); // get it back @@ -88,17 +84,14 @@ namespace Umbraco.Tests.Services var content1 = MockedContent.CreateSimpleContent(contentType, "Tagged content 1", -1); content1.AssignTags("tags", new[] { "cow", "pig", "goat" }); - content1.TryPublishValues(); contentService.SaveAndPublish(content1); var content2 = MockedContent.CreateSimpleContent(contentType, "Tagged content 2", -1); content2.AssignTags("tags", new[] { "cow", "pig" }); - content2.TryPublishValues(); contentService.SaveAndPublish(content2); var content3 = MockedContent.CreateSimpleContent(contentType, "Tagged content 3", -1); content3.AssignTags("tags", new[] { "cow" }); - content3.TryPublishValues(); contentService.SaveAndPublish(content3); // Act diff --git a/src/Umbraco.Web.UI/Umbraco/dialogs/ChangeDocType.aspx.cs b/src/Umbraco.Web.UI/Umbraco/dialogs/ChangeDocType.aspx.cs index cbd6ebbd75..2297ea1aa7 100644 --- a/src/Umbraco.Web.UI/Umbraco/dialogs/ChangeDocType.aspx.cs +++ b/src/Umbraco.Web.UI/Umbraco/dialogs/ChangeDocType.aspx.cs @@ -251,7 +251,7 @@ namespace Umbraco.Web.UI.Umbraco.Dialogs if (wasPublished) { // no values to publish, really - Services.ContentService.SaveAndPublish(_content, user.Id); + Services.ContentService.SaveAndPublish(_content, userId: user.Id); } // Sync the tree diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index 75b22d5404..6c0313bd1f 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -712,8 +712,7 @@ namespace Umbraco.Web.Editors if (!contentItem.PersistedContent.ContentType.VariesByCulture()) { //its invariant, proceed normally - contentItem.PersistedContent.TryPublishValues(); - publishStatus = Services.ContentService.SaveAndPublish(contentItem.PersistedContent, Security.CurrentUser.Id); + publishStatus = Services.ContentService.SaveAndPublish(contentItem.PersistedContent, userId: Security.CurrentUser.Id); wasCancelled = publishStatus.Result == PublishResultType.FailedCancelledByEvent; } else @@ -760,13 +759,13 @@ namespace Umbraco.Web.Editors if (canPublish) { //try to publish all the values on the model - canPublish = TryPublishValues(contentItem, otherVariantsToValidate, allLangs); + canPublish = PublishCulture(contentItem, otherVariantsToValidate, allLangs); } if (canPublish) { //proceed to publish if all validation still succeeds - publishStatus = Services.ContentService.SaveAndPublish(contentItem.PersistedContent, Security.CurrentUser.Id); + publishStatus = Services.ContentService.SavePublishing(contentItem.PersistedContent, Security.CurrentUser.Id); wasCancelled = publishStatus.Result == PublishResultType.FailedCancelledByEvent; } else @@ -786,16 +785,15 @@ namespace Umbraco.Web.Editors /// /// /// - private bool TryPublishValues(ContentItemSave contentItem, IEnumerable otherVariantsToValidate, IDictionary allLangs) + private bool PublishCulture(ContentItemSave contentItem, IEnumerable otherVariantsToValidate, IDictionary allLangs) { var culturesToPublish = new List { contentItem.Culture }; - if (!contentItem.Culture.IsNullOrWhiteSpace()) - culturesToPublish.Add(null); //we need to publish the invariant values if culture is specified, so we can pass in null culturesToPublish.AddRange(otherVariantsToValidate.Select(x => x.Culture)); foreach(var culture in culturesToPublish) { - var valid = contentItem.PersistedContent.TryPublishValues(culture); + // publishing any culture, implies the invariant culture + var valid = contentItem.PersistedContent.PublishCulture(culture); if (!valid) { var errMsg = Services.TextService.Localize("speechBubbles/contentCultureUnexpectedValidationError", new[] { allLangs[culture].CultureName }); @@ -827,8 +825,7 @@ namespace Umbraco.Web.Editors return HandleContentNotFound(id, false); } - foundContent.TryPublishValues(); // fixme variants? - var publishResult = Services.ContentService.SaveAndPublish(foundContent, Security.GetUserId().ResultOr(0)); + var publishResult = Services.ContentService.SavePublishing(foundContent, Security.GetUserId().ResultOr(0)); if (publishResult.Success == false) { var notificationModel = new SimpleNotificationModel(); diff --git a/src/Umbraco.Web/WebServices/BulkPublishController.cs b/src/Umbraco.Web/WebServices/BulkPublishController.cs index 04699536e9..fbe0d3554f 100644 --- a/src/Umbraco.Web/WebServices/BulkPublishController.cs +++ b/src/Umbraco.Web/WebServices/BulkPublishController.cs @@ -29,8 +29,8 @@ namespace Umbraco.Web.WebServices if (publishDescendants == false) { - content.TryPublishValues(); // fixme variants? validation - when this returns null? - var result = Services.ContentService.SaveAndPublish(content, Security.CurrentUser.Id); + content.PublishCulture(); // fixme variants? validation - when this returns null? + var result = Services.ContentService.SaveAndPublish(content, userId: Security.CurrentUser.Id); return Json(new { success = result.Success, From e8aa6701ef260ad9417cf36bb32cb6e9551b92b0 Mon Sep 17 00:00:00 2001 From: Stephan Date: Wed, 4 Jul 2018 09:03:59 +0200 Subject: [PATCH 08/14] Renormalize --- .../Factories/DictionaryItemFactory.cs | 132 +- .../views/documenttypes/edit.controller.js | 928 ++-- .../src/views/media/media.edit.controller.js | 544 +-- .../src/views/mediatypes/edit.controller.js | 838 ++-- .../Umbraco/config/lang/en_us.xml | 4340 ++++++++--------- 5 files changed, 3391 insertions(+), 3391 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Factories/DictionaryItemFactory.cs b/src/Umbraco.Core/Persistence/Factories/DictionaryItemFactory.cs index 8994026ccb..4236195402 100644 --- a/src/Umbraco.Core/Persistence/Factories/DictionaryItemFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/DictionaryItemFactory.cs @@ -1,66 +1,66 @@ -using System.Collections.Generic; -using Umbraco.Core.Models; -using Umbraco.Core.Persistence.Dtos; - -namespace Umbraco.Core.Persistence.Factories -{ - internal static class DictionaryItemFactory - { - #region Implementation of IEntityFactory - - public static IDictionaryItem BuildEntity(DictionaryDto dto) - { - var item = new DictionaryItem(dto.Parent, dto.Key); - - try - { - item.DisableChangeTracking(); - - item.Id = dto.PrimaryKey; - item.Key = dto.UniqueId; - - // reset dirty initial properties (U4-1946) - item.ResetDirtyProperties(false); - return item; - } - finally - { - item.EnableChangeTracking(); - } - } - - public static DictionaryDto BuildDto(IDictionaryItem entity) - { - return new DictionaryDto - { - UniqueId = entity.Key, - Key = entity.ItemKey, - Parent = entity.ParentId, - PrimaryKey = entity.Id, - LanguageTextDtos = BuildLanguageTextDtos(entity) - }; - } - - #endregion - - private static List BuildLanguageTextDtos(IDictionaryItem entity) - { - var list = new List(); - foreach (var translation in entity.Translations) - { - var text = new LanguageTextDto - { - LanguageId = translation.LanguageId, - UniqueId = translation.Key, - Value = translation.Value - }; - - if (translation.HasIdentity) - text.PrimaryKey = translation.Id; - - list.Add(text); - } - return list; - } - } -} +using System.Collections.Generic; +using Umbraco.Core.Models; +using Umbraco.Core.Persistence.Dtos; + +namespace Umbraco.Core.Persistence.Factories +{ + internal static class DictionaryItemFactory + { + #region Implementation of IEntityFactory + + public static IDictionaryItem BuildEntity(DictionaryDto dto) + { + var item = new DictionaryItem(dto.Parent, dto.Key); + + try + { + item.DisableChangeTracking(); + + item.Id = dto.PrimaryKey; + item.Key = dto.UniqueId; + + // reset dirty initial properties (U4-1946) + item.ResetDirtyProperties(false); + return item; + } + finally + { + item.EnableChangeTracking(); + } + } + + public static DictionaryDto BuildDto(IDictionaryItem entity) + { + return new DictionaryDto + { + UniqueId = entity.Key, + Key = entity.ItemKey, + Parent = entity.ParentId, + PrimaryKey = entity.Id, + LanguageTextDtos = BuildLanguageTextDtos(entity) + }; + } + + #endregion + + private static List BuildLanguageTextDtos(IDictionaryItem entity) + { + var list = new List(); + foreach (var translation in entity.Translations) + { + var text = new LanguageTextDto + { + LanguageId = translation.LanguageId, + UniqueId = translation.Key, + Value = translation.Value + }; + + if (translation.HasIdentity) + text.PrimaryKey = translation.Id; + + list.Add(text); + } + return list; + } + } +} diff --git a/src/Umbraco.Web.UI.Client/src/views/documenttypes/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/documenttypes/edit.controller.js index da4a53328d..30c2b493da 100644 --- a/src/Umbraco.Web.UI.Client/src/views/documenttypes/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/documenttypes/edit.controller.js @@ -1,464 +1,464 @@ -/** - * @ngdoc controller - * @name Umbraco.Editors.DocumentType.EditController - * @function - * - * @description - * The controller for the content type editor - */ -(function () { - "use strict"; - - function DocumentTypesEditController($scope, $routeParams, $injector, contentTypeResource, dataTypeResource, editorState, contentEditingHelper, formHelper, navigationService, iconHelper, contentTypeHelper, notificationsService, $filter, $q, localizationService, overlayHelper, eventsService) { - - var vm = this; - var evts = []; - - var disableTemplates = Umbraco.Sys.ServerVariables.features.disabledFeatures.disableTemplates; - var documentTypeId = $routeParams.id; - var create = $routeParams.create; - var noTemplate = $routeParams.notemplate; - var infiniteMode = $scope.model && $scope.model.infiniteMode; - - vm.save = save; - vm.close = close; - - vm.currentNode = null; - vm.contentType = {}; - vm.labels = {}; - vm.submitButtonKey = "buttons_save"; - vm.generateModelsKey = "buttons_saveAndGenerateModels"; - - vm.page = {}; - vm.page.loading = false; - vm.page.saveButtonState = "init"; - vm.page.navigation = []; - - var labelKeys = [ - "general_design", - "general_listView", - "general_rights", - "treeHeaders_templates", - "main_sections", - "shortcuts_navigateSections", - "shortcuts_addTab", - "shortcuts_addProperty", - "shortcuts_addEditor", - "shortcuts_editDataType", - "shortcuts_toggleListView", - "shortcuts_toggleAllowAsRoot", - "shortcuts_addChildNode", - "shortcuts_addTemplate" - ]; - - onInit(); - - function onInit() { - // get init values from model when in infinite mode - if(infiniteMode) { - documentTypeId = $scope.model.id; - create = $scope.model.create; - noTemplate = $scope.model.notemplate; - vm.submitButtonKey = "buttons_saveAndClose"; - vm.generateModelsKey = "buttons_generateModelsAndClose"; - } - } - - localizationService.localizeMany(labelKeys).then(function (values) { - // navigation - vm.labels.design = values[0]; - vm.labels.listview = values[1]; - vm.labels.permissions = values[2]; - vm.labels.templates = values[3]; - // keyboard shortcuts - vm.labels.sections = values[4]; - vm.labels.navigateSections = values[5]; - vm.labels.addTab = values[6]; - vm.labels.addProperty = values[7]; - vm.labels.addEditor = values[8]; - vm.labels.editDataType = values[9]; - vm.labels.toggleListView = values[10]; - vm.labels.allowAsRoot = values[11]; - vm.labels.addChildNode = values[12]; - vm.labels.addTemplate = values[13]; - - var buttons = [ - { - "name": vm.labels.design, - "alias": "design", - "icon": "icon-document-dashed-line", - "view": "views/documenttypes/views/design/design.html", - "active": true - }, - { - "name": vm.labels.listview, - "alias": "listView", - "icon": "icon-list", - "view": "views/documenttypes/views/listview/listview.html" - }, - { - "name": vm.labels.permissions, - "alias": "permissions", - "icon": "icon-keychain", - "view": "views/documenttypes/views/permissions/permissions.html" - }, - { - "name": vm.labels.templates, - "alias": "templates", - "icon": "icon-layout", - "view": "views/documenttypes/views/templates/templates.html" - } - ]; - - vm.page.keyboardShortcutsOverview = [ - { - "name": vm.labels.sections, - "shortcuts": [ - { - "description": vm.labels.navigateSections, - "keys": [{ "key": "1" }, { "key": "4" }], - "keyRange": true - } - ] - }, - { - "name": vm.labels.design, - "shortcuts": [ - { - "description": vm.labels.addTab, - "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "t" }] - }, - { - "description": vm.labels.addProperty, - "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "p" }] - }, - { - "description": vm.labels.addEditor, - "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "e" }] - }, - { - "description": vm.labels.editDataType, - "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "d" }] - } - ] - }, - { - "name": vm.labels.listview, - "shortcuts": [ - { - "description": vm.labels.toggleListView, - "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "l" }] - } - ] - }, - { - "name": vm.labels.permissions, - "shortcuts": [ - { - "description": vm.labels.allowAsRoot, - "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "r" }] - }, - { - "description": vm.labels.addChildNode, - "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "c" }] - } - ] - }, - { - "name": vm.labels.templates, - "shortcuts": [ - { - "description": vm.labels.addTemplate, - "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "t" }] - } - ] - } - ]; - - loadButtons(buttons); - - }); - - contentTypeHelper.checkModelsBuilderStatus().then(function (result) { - vm.page.modelsBuilder = result; - if (result) { - //Models builder mode: - vm.page.defaultButton = { - alias: "save", - hotKey: "ctrl+s", - hotKeyWhenHidden: true, - labelKey: vm.submitButtonKey, - letter: "S", - type: "submit", - handler: function () { vm.save(); } - }; - vm.page.subButtons = [{ - alias: "saveAndGenerateModels", - hotKey: "ctrl+g", - hotKeyWhenHidden: true, - labelKey: vm.generateModelsKey, - letter: "G", - handler: function () { - - vm.page.saveButtonState = "busy"; - - saveInternal().then(function (result) { - - vm.page.saveButtonState = "busy"; - - localizationService.localize("modelsBuilder_buildingModels").then(function (headerValue) { - localizationService.localize("modelsBuilder_waitingMessage").then(function (msgValue) { - notificationsService.info(headerValue, msgValue); - }); - }); - - contentTypeHelper.generateModels().then(function (result) { - - // generateModels() returns the dashboard content - if (!result.lastError) { - - //re-check model status - contentTypeHelper.checkModelsBuilderStatus().then(function (statusResult) { - vm.page.modelsBuilder = statusResult; - }); - - //clear and add success - vm.page.saveButtonState = "init"; - localizationService.localize("modelsBuilder_modelsGenerated").then(function (value) { - notificationsService.success(value); - }); - - } else { - vm.page.saveButtonState = "error"; - localizationService.localize("modelsBuilder_modelsExceptionInUlog").then(function (value) { - notificationsService.error(value); - }); - } - - }, function () { - vm.page.saveButtonState = "error"; - localizationService.localize("modelsBuilder_modelsGeneratedError").then(function (value) { - notificationsService.error(value); - }); - }); - }); - } - }]; - } - }); - - if (create) { - vm.page.loading = true; - - //we are creating so get an empty data type item - contentTypeResource.getScaffold(documentTypeId) - .then(function (dt) { - init(dt); - vm.page.loading = false; - }); - } - else { - loadDocumentType(); - } - - function loadDocumentType() { - vm.page.loading = true; - contentTypeResource.getById(documentTypeId).then(function (dt) { - init(dt); - // we don't need to sync the tree in infinite mode - if(!infiniteMode) { - syncTreeNode(vm.contentType, dt.path, true); - } - vm.page.loading = false; - }); - } - - function loadButtons(buttons) { - - angular.forEach(buttons, - function (val, index) { - - if (disableTemplates === true && val.alias === "templates") { - buttons.splice(index, 1); - } - - }); - - vm.page.navigation = buttons; - } - - /* ---------- SAVE ---------- */ - - function save() { - //return the saveInternal method but catch rejections since this is the upper most caller - return saveInternal().catch(angular.noop); - } - - /** This internal save method performs the actual saving and returns a promise, not to be bound to any buttons but used by other bound methods */ - function saveInternal() { - - // only save if there is no overlays open - if (overlayHelper.getNumberOfOverlays() === 0) { - - vm.page.saveButtonState = "busy"; - - // reformat allowed content types to array if id's - vm.contentType.allowedContentTypes = contentTypeHelper.createIdArray(vm.contentType.allowedContentTypes); - - return contentEditingHelper.contentEditorPerformSave({ - saveMethod: contentTypeResource.save, - scope: $scope, - content: vm.contentType, - //We do not redirect on failure for doc types - this is because it is not possible to actually save the doc - // type when server side validation fails - as opposed to content where we are capable of saving the content - // item if server side validation fails - redirectOnFailure: false, - // we need to rebind... the IDs that have been created! - rebindCallback: function (origContentType, savedContentType) { - vm.contentType.id = savedContentType.id; - vm.contentType.groups.forEach(function (group) { - if (!group.name) return; - var k = 0; - while (k < savedContentType.groups.length && savedContentType.groups[k].name != group.name) - k++; - if (k == savedContentType.groups.length) { - group.id = 0; - return; - } - var savedGroup = savedContentType.groups[k]; - if (!group.id) group.id = savedGroup.id; - - group.properties.forEach(function (property) { - if (property.id || !property.alias) return; - k = 0; - while (k < savedGroup.properties.length && savedGroup.properties[k].alias != property.alias) - k++; - if (k == savedGroup.properties.length) { - property.id = 0; - return; - } - var savedProperty = savedGroup.properties[k]; - property.id = savedProperty.id; - }); - }); - } - }).then(function (data) { - //success - // we don't need to sync the tree in infinite mode - if(!infiniteMode) { - syncTreeNode(vm.contentType, data.path); - } - - // emit event - var args = { documentType: vm.contentType }; - eventsService.emit("editors.documentType.saved", args); - - vm.page.saveButtonState = "success"; - - if(infiniteMode && $scope.model.submit) { - $scope.model.documentTypeAlias = vm.contentType.alias; - $scope.model.submit($scope.model); - } - - return $q.resolve(data); - }, function (err) { - //error - if (err) { - editorState.set($scope.content); - } - else { - localizationService.localize("speechBubbles_validationFailedHeader").then(function (headerValue) { - localizationService.localize("speechBubbles_validationFailedMessage").then(function (msgValue) { - notificationsService.error(headerValue, msgValue); - }); - }); - } - vm.page.saveButtonState = "error"; - return $q.reject(err); - }); - } - else { - return $q.reject(); - } - } - - function init(contentType) { - - // set all tab to inactive - if (contentType.groups.length !== 0) { - angular.forEach(contentType.groups, function (group) { - - angular.forEach(group.properties, function (property) { - // get data type details for each property - getDataTypeDetails(property); - }); - - }); - } - - // insert template on new doc types - if (!noTemplate && contentType.id === 0) { - contentType.defaultTemplate = contentTypeHelper.insertDefaultTemplatePlaceholder(contentType.defaultTemplate); - contentType.allowedTemplates = contentTypeHelper.insertTemplatePlaceholder(contentType.allowedTemplates); - } - - // convert icons for content type - convertLegacyIcons(contentType); - - //set a shared state - editorState.set(contentType); - - vm.contentType = contentType; - } - - function convertLegacyIcons(contentType) { - // make array to store contentType icon - var contentTypeArray = []; - - // push icon to array - contentTypeArray.push({ "icon": contentType.icon }); - - // run through icon method - iconHelper.formatContentTypeIcons(contentTypeArray); - - // set icon back on contentType - contentType.icon = contentTypeArray[0].icon; - } - - function getDataTypeDetails(property) { - if (property.propertyState !== "init") { - dataTypeResource.getById(property.dataTypeId) - .then(function (dataType) { - property.dataTypeIcon = dataType.icon; - property.dataTypeName = dataType.name; - }); - } - } - - /** Syncs the content type to it's tree node - this occurs on first load and after saving */ - function syncTreeNode(dt, path, initialLoad) { - navigationService.syncTree({ tree: "documenttypes", path: path.split(","), forceReload: initialLoad !== true }).then(function (syncArgs) { - vm.currentNode = syncArgs.node; - }); - } - - function close() { - if($scope.model.close) { - $scope.model.close($scope.model); - } - } - - evts.push(eventsService.on("app.refreshEditor", function (name, error) { - loadDocumentType(); - })); - - //ensure to unregister from all events! - $scope.$on('$destroy', function () { - for (var e in evts) { - eventsService.unsubscribe(evts[e]); - } - }); - } - - angular.module("umbraco").controller("Umbraco.Editors.DocumentTypes.EditController", DocumentTypesEditController); -})(); +/** + * @ngdoc controller + * @name Umbraco.Editors.DocumentType.EditController + * @function + * + * @description + * The controller for the content type editor + */ +(function () { + "use strict"; + + function DocumentTypesEditController($scope, $routeParams, $injector, contentTypeResource, dataTypeResource, editorState, contentEditingHelper, formHelper, navigationService, iconHelper, contentTypeHelper, notificationsService, $filter, $q, localizationService, overlayHelper, eventsService) { + + var vm = this; + var evts = []; + + var disableTemplates = Umbraco.Sys.ServerVariables.features.disabledFeatures.disableTemplates; + var documentTypeId = $routeParams.id; + var create = $routeParams.create; + var noTemplate = $routeParams.notemplate; + var infiniteMode = $scope.model && $scope.model.infiniteMode; + + vm.save = save; + vm.close = close; + + vm.currentNode = null; + vm.contentType = {}; + vm.labels = {}; + vm.submitButtonKey = "buttons_save"; + vm.generateModelsKey = "buttons_saveAndGenerateModels"; + + vm.page = {}; + vm.page.loading = false; + vm.page.saveButtonState = "init"; + vm.page.navigation = []; + + var labelKeys = [ + "general_design", + "general_listView", + "general_rights", + "treeHeaders_templates", + "main_sections", + "shortcuts_navigateSections", + "shortcuts_addTab", + "shortcuts_addProperty", + "shortcuts_addEditor", + "shortcuts_editDataType", + "shortcuts_toggleListView", + "shortcuts_toggleAllowAsRoot", + "shortcuts_addChildNode", + "shortcuts_addTemplate" + ]; + + onInit(); + + function onInit() { + // get init values from model when in infinite mode + if(infiniteMode) { + documentTypeId = $scope.model.id; + create = $scope.model.create; + noTemplate = $scope.model.notemplate; + vm.submitButtonKey = "buttons_saveAndClose"; + vm.generateModelsKey = "buttons_generateModelsAndClose"; + } + } + + localizationService.localizeMany(labelKeys).then(function (values) { + // navigation + vm.labels.design = values[0]; + vm.labels.listview = values[1]; + vm.labels.permissions = values[2]; + vm.labels.templates = values[3]; + // keyboard shortcuts + vm.labels.sections = values[4]; + vm.labels.navigateSections = values[5]; + vm.labels.addTab = values[6]; + vm.labels.addProperty = values[7]; + vm.labels.addEditor = values[8]; + vm.labels.editDataType = values[9]; + vm.labels.toggleListView = values[10]; + vm.labels.allowAsRoot = values[11]; + vm.labels.addChildNode = values[12]; + vm.labels.addTemplate = values[13]; + + var buttons = [ + { + "name": vm.labels.design, + "alias": "design", + "icon": "icon-document-dashed-line", + "view": "views/documenttypes/views/design/design.html", + "active": true + }, + { + "name": vm.labels.listview, + "alias": "listView", + "icon": "icon-list", + "view": "views/documenttypes/views/listview/listview.html" + }, + { + "name": vm.labels.permissions, + "alias": "permissions", + "icon": "icon-keychain", + "view": "views/documenttypes/views/permissions/permissions.html" + }, + { + "name": vm.labels.templates, + "alias": "templates", + "icon": "icon-layout", + "view": "views/documenttypes/views/templates/templates.html" + } + ]; + + vm.page.keyboardShortcutsOverview = [ + { + "name": vm.labels.sections, + "shortcuts": [ + { + "description": vm.labels.navigateSections, + "keys": [{ "key": "1" }, { "key": "4" }], + "keyRange": true + } + ] + }, + { + "name": vm.labels.design, + "shortcuts": [ + { + "description": vm.labels.addTab, + "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "t" }] + }, + { + "description": vm.labels.addProperty, + "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "p" }] + }, + { + "description": vm.labels.addEditor, + "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "e" }] + }, + { + "description": vm.labels.editDataType, + "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "d" }] + } + ] + }, + { + "name": vm.labels.listview, + "shortcuts": [ + { + "description": vm.labels.toggleListView, + "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "l" }] + } + ] + }, + { + "name": vm.labels.permissions, + "shortcuts": [ + { + "description": vm.labels.allowAsRoot, + "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "r" }] + }, + { + "description": vm.labels.addChildNode, + "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "c" }] + } + ] + }, + { + "name": vm.labels.templates, + "shortcuts": [ + { + "description": vm.labels.addTemplate, + "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "t" }] + } + ] + } + ]; + + loadButtons(buttons); + + }); + + contentTypeHelper.checkModelsBuilderStatus().then(function (result) { + vm.page.modelsBuilder = result; + if (result) { + //Models builder mode: + vm.page.defaultButton = { + alias: "save", + hotKey: "ctrl+s", + hotKeyWhenHidden: true, + labelKey: vm.submitButtonKey, + letter: "S", + type: "submit", + handler: function () { vm.save(); } + }; + vm.page.subButtons = [{ + alias: "saveAndGenerateModels", + hotKey: "ctrl+g", + hotKeyWhenHidden: true, + labelKey: vm.generateModelsKey, + letter: "G", + handler: function () { + + vm.page.saveButtonState = "busy"; + + saveInternal().then(function (result) { + + vm.page.saveButtonState = "busy"; + + localizationService.localize("modelsBuilder_buildingModels").then(function (headerValue) { + localizationService.localize("modelsBuilder_waitingMessage").then(function (msgValue) { + notificationsService.info(headerValue, msgValue); + }); + }); + + contentTypeHelper.generateModels().then(function (result) { + + // generateModels() returns the dashboard content + if (!result.lastError) { + + //re-check model status + contentTypeHelper.checkModelsBuilderStatus().then(function (statusResult) { + vm.page.modelsBuilder = statusResult; + }); + + //clear and add success + vm.page.saveButtonState = "init"; + localizationService.localize("modelsBuilder_modelsGenerated").then(function (value) { + notificationsService.success(value); + }); + + } else { + vm.page.saveButtonState = "error"; + localizationService.localize("modelsBuilder_modelsExceptionInUlog").then(function (value) { + notificationsService.error(value); + }); + } + + }, function () { + vm.page.saveButtonState = "error"; + localizationService.localize("modelsBuilder_modelsGeneratedError").then(function (value) { + notificationsService.error(value); + }); + }); + }); + } + }]; + } + }); + + if (create) { + vm.page.loading = true; + + //we are creating so get an empty data type item + contentTypeResource.getScaffold(documentTypeId) + .then(function (dt) { + init(dt); + vm.page.loading = false; + }); + } + else { + loadDocumentType(); + } + + function loadDocumentType() { + vm.page.loading = true; + contentTypeResource.getById(documentTypeId).then(function (dt) { + init(dt); + // we don't need to sync the tree in infinite mode + if(!infiniteMode) { + syncTreeNode(vm.contentType, dt.path, true); + } + vm.page.loading = false; + }); + } + + function loadButtons(buttons) { + + angular.forEach(buttons, + function (val, index) { + + if (disableTemplates === true && val.alias === "templates") { + buttons.splice(index, 1); + } + + }); + + vm.page.navigation = buttons; + } + + /* ---------- SAVE ---------- */ + + function save() { + //return the saveInternal method but catch rejections since this is the upper most caller + return saveInternal().catch(angular.noop); + } + + /** This internal save method performs the actual saving and returns a promise, not to be bound to any buttons but used by other bound methods */ + function saveInternal() { + + // only save if there is no overlays open + if (overlayHelper.getNumberOfOverlays() === 0) { + + vm.page.saveButtonState = "busy"; + + // reformat allowed content types to array if id's + vm.contentType.allowedContentTypes = contentTypeHelper.createIdArray(vm.contentType.allowedContentTypes); + + return contentEditingHelper.contentEditorPerformSave({ + saveMethod: contentTypeResource.save, + scope: $scope, + content: vm.contentType, + //We do not redirect on failure for doc types - this is because it is not possible to actually save the doc + // type when server side validation fails - as opposed to content where we are capable of saving the content + // item if server side validation fails + redirectOnFailure: false, + // we need to rebind... the IDs that have been created! + rebindCallback: function (origContentType, savedContentType) { + vm.contentType.id = savedContentType.id; + vm.contentType.groups.forEach(function (group) { + if (!group.name) return; + var k = 0; + while (k < savedContentType.groups.length && savedContentType.groups[k].name != group.name) + k++; + if (k == savedContentType.groups.length) { + group.id = 0; + return; + } + var savedGroup = savedContentType.groups[k]; + if (!group.id) group.id = savedGroup.id; + + group.properties.forEach(function (property) { + if (property.id || !property.alias) return; + k = 0; + while (k < savedGroup.properties.length && savedGroup.properties[k].alias != property.alias) + k++; + if (k == savedGroup.properties.length) { + property.id = 0; + return; + } + var savedProperty = savedGroup.properties[k]; + property.id = savedProperty.id; + }); + }); + } + }).then(function (data) { + //success + // we don't need to sync the tree in infinite mode + if(!infiniteMode) { + syncTreeNode(vm.contentType, data.path); + } + + // emit event + var args = { documentType: vm.contentType }; + eventsService.emit("editors.documentType.saved", args); + + vm.page.saveButtonState = "success"; + + if(infiniteMode && $scope.model.submit) { + $scope.model.documentTypeAlias = vm.contentType.alias; + $scope.model.submit($scope.model); + } + + return $q.resolve(data); + }, function (err) { + //error + if (err) { + editorState.set($scope.content); + } + else { + localizationService.localize("speechBubbles_validationFailedHeader").then(function (headerValue) { + localizationService.localize("speechBubbles_validationFailedMessage").then(function (msgValue) { + notificationsService.error(headerValue, msgValue); + }); + }); + } + vm.page.saveButtonState = "error"; + return $q.reject(err); + }); + } + else { + return $q.reject(); + } + } + + function init(contentType) { + + // set all tab to inactive + if (contentType.groups.length !== 0) { + angular.forEach(contentType.groups, function (group) { + + angular.forEach(group.properties, function (property) { + // get data type details for each property + getDataTypeDetails(property); + }); + + }); + } + + // insert template on new doc types + if (!noTemplate && contentType.id === 0) { + contentType.defaultTemplate = contentTypeHelper.insertDefaultTemplatePlaceholder(contentType.defaultTemplate); + contentType.allowedTemplates = contentTypeHelper.insertTemplatePlaceholder(contentType.allowedTemplates); + } + + // convert icons for content type + convertLegacyIcons(contentType); + + //set a shared state + editorState.set(contentType); + + vm.contentType = contentType; + } + + function convertLegacyIcons(contentType) { + // make array to store contentType icon + var contentTypeArray = []; + + // push icon to array + contentTypeArray.push({ "icon": contentType.icon }); + + // run through icon method + iconHelper.formatContentTypeIcons(contentTypeArray); + + // set icon back on contentType + contentType.icon = contentTypeArray[0].icon; + } + + function getDataTypeDetails(property) { + if (property.propertyState !== "init") { + dataTypeResource.getById(property.dataTypeId) + .then(function (dataType) { + property.dataTypeIcon = dataType.icon; + property.dataTypeName = dataType.name; + }); + } + } + + /** Syncs the content type to it's tree node - this occurs on first load and after saving */ + function syncTreeNode(dt, path, initialLoad) { + navigationService.syncTree({ tree: "documenttypes", path: path.split(","), forceReload: initialLoad !== true }).then(function (syncArgs) { + vm.currentNode = syncArgs.node; + }); + } + + function close() { + if($scope.model.close) { + $scope.model.close($scope.model); + } + } + + evts.push(eventsService.on("app.refreshEditor", function (name, error) { + loadDocumentType(); + })); + + //ensure to unregister from all events! + $scope.$on('$destroy', function () { + for (var e in evts) { + eventsService.unsubscribe(evts[e]); + } + }); + } + + angular.module("umbraco").controller("Umbraco.Editors.DocumentTypes.EditController", DocumentTypesEditController); +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/media/media.edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/media/media.edit.controller.js index e26facc880..8c171af2f0 100644 --- a/src/Umbraco.Web.UI.Client/src/views/media/media.edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/media/media.edit.controller.js @@ -1,272 +1,272 @@ -/** - * @ngdoc controller - * @name Umbraco.Editors.Media.EditController - * @function - * - * @description - * The controller for the media editor - */ -function mediaEditController($scope, $routeParams, $q, appState, mediaResource, entityResource, navigationService, notificationsService, angularHelper, serverValidationManager, contentEditingHelper, fileManager, treeService, formHelper, umbModelMapper, editorState, umbRequestHelper, $http, eventsService) { - - var evts = []; - var nodeId = null; - var create = false; - var infiniteMode = $scope.model && $scope.model.infiniteMode; - - // when opening the editor through infinite editing get the - // node id from the model instead of the route param - if(infiniteMode && $scope.model.id) { - nodeId = $scope.model.id; - } else { - nodeId = $routeParams.id; - } - - // when opening the editor through infinite editing get the - // create option from the model instead of the route param - if(infiniteMode) { - create = $scope.model.create; - } else { - create = $routeParams.create; - } - - //setup scope vars - $scope.currentSection = appState.getSectionState("currentSection"); - $scope.currentNode = null; //the editors affiliated node - - $scope.page = {}; - $scope.page.loading = false; - $scope.page.menu = {}; - $scope.page.menu.currentSection = appState.getSectionState("currentSection"); - $scope.page.menu.currentNode = null; //the editors affiliated node - $scope.page.listViewPath = null; - $scope.page.saveButtonState = "init"; - $scope.page.submitButtonLabel = "Save"; - - /** Syncs the content item to it's tree node - this occurs on first load and after saving */ - function syncTreeNode(content, path, initialLoad) { - - if (!$scope.content.isChildOfListView) { - navigationService.syncTree({ tree: "media", path: path.split(","), forceReload: initialLoad !== true }).then(function (syncArgs) { - $scope.page.menu.currentNode = syncArgs.node; - }); - } - else if (initialLoad === true) { - - //it's a child item, just sync the ui node to the parent - navigationService.syncTree({ tree: "media", path: path.substring(0, path.lastIndexOf(",")).split(","), forceReload: initialLoad !== true }); - - //if this is a child of a list view and it's the initial load of the editor, we need to get the tree node - // from the server so that we can load in the actions menu. - umbRequestHelper.resourcePromise( - $http.get(content.treeNodeUrl), - 'Failed to retrieve data for child node ' + content.id).then(function (node) { - $scope.page.menu.currentNode = node; - }); - } - } - - if (create) { - - $scope.page.loading = true; - - mediaResource.getScaffold(nodeId, $routeParams.doctype) - .then(function (data) { - $scope.content = data; - - editorState.set($scope.content); - - // We don't get the info tab from the server from version 7.8 so we need to manually add it - //contentEditingHelper.addInfoTab($scope.content.tabs); - - init($scope.content); - - $scope.page.loading = false; - - }); - } - else { - $scope.page.loading = true; - loadMedia() - .then(function(){ - $scope.page.loading = false; - }); - } - - function init(content) { - - // prototype content and info apps - var contentApp = { - "name": "Content", - "alias": "content", - "icon": "icon-document", - "view": "views/media/apps/content/content.html" - }; - - var infoApp = { - "name": "Info", - "alias": "info", - "icon": "icon-info", - "view": "views/media/apps/info/info.html" - }; - - var listview = { - "name": "Child items", - "alias": "childItems", - "icon": "icon-list", - "view": "views/media/apps/listview/listview.html" - }; - - $scope.content.apps = []; - - if($scope.content.contentTypeAlias === "Folder") { - // add list view app - $scope.content.apps.push(listview); - - // remove the list view tab - angular.forEach($scope.content.tabs, function(tab, index){ - if(tab.alias === "Contents") { - tab.hide = true; - } - }); - - } else { - $scope.content.apps.push(contentApp); - } - - $scope.content.apps.push(infoApp); - - // set first app to active - $scope.content.apps[0].active = true; - - // setup infinite mode - if(infiniteMode) { - $scope.page.submitButtonLabel = "Save and Close"; - } - - } - - $scope.save = function () { - - if (!$scope.busy && formHelper.submitForm({ scope: $scope })) { - - $scope.busy = true; - $scope.page.saveButtonState = "busy"; - - mediaResource.save($scope.content, create, fileManager.getFiles()) - .then(function(data) { - - formHelper.resetForm({ scope: $scope }); - - contentEditingHelper.handleSuccessfulSave({ - scope: $scope, - savedContent: data, - redirectOnSuccess: !infiniteMode, - rebindCallback: contentEditingHelper.reBindChangedProperties($scope.content, data) - }); - - editorState.set($scope.content); - $scope.busy = false; - - // when don't want to sync the tree when the editor is open in infinite mode - if(!infiniteMode) { - syncTreeNode($scope.content, data.path); - } - - init($scope.content); - - $scope.page.saveButtonState = "success"; - - // close the editor if it's infinite mode - if(infiniteMode && $scope.model.submit) { - $scope.model.mediaNode = $scope.content; - $scope.model.submit($scope.model); - } - - }, function(err) { - - contentEditingHelper.handleSaveError({ - err: err, - redirectOnError: !infiniteMode, - rebindCallback: contentEditingHelper.reBindChangedProperties($scope.content, err.data) - }); - - editorState.set($scope.content); - $scope.busy = false; - $scope.page.saveButtonState = "error"; - - }); - }else{ - $scope.busy = false; - } - - }; - - function loadMedia() { - - return mediaResource.getById(nodeId) - .then(function (data) { - - $scope.content = data; - - if (data.isChildOfListView && data.trashed === false) { - $scope.page.listViewPath = ($routeParams.page) - ? "/media/media/edit/" + data.parentId + "?page=" + $routeParams.page - : "/media/media/edit/" + data.parentId; - } - - editorState.set($scope.content); - - //in one particular special case, after we've created a new item we redirect back to the edit - // route but there might be server validation errors in the collection which we need to display - // after the redirect, so we will bind all subscriptions which will show the server validation errors - // if there are any and then clear them so the collection no longer persists them. - serverValidationManager.executeAndClearAllSubscriptions(); - - if(!infiniteMode) { - syncTreeNode($scope.content, data.path, true); - } - - if ($scope.content.parentId && $scope.content.parentId != -1) { - //We fetch all ancestors of the node to generate the footer breadcrump navigation - entityResource.getAncestors(nodeId, "media") - .then(function (anc) { - $scope.ancestors = anc; - }); - } - - // We don't get the info tab from the server from version 7.8 so we need to manually add it - //contentEditingHelper.addInfoTab($scope.content.tabs); - - init($scope.content); - - $scope.page.loading = false; - - $q.resolve($scope.content); - - }); - - } - - $scope.close = function() { - if($scope.model.close) { - $scope.model.close($scope.model); - } - }; - - evts.push(eventsService.on("editors.mediaType.saved", function(name, args) { - // if this media item uses the updated media type we need to reload the media item - if(args && args.mediaType && args.mediaType.key === $scope.content.contentType.key) { - loadMedia(); - } - })); - - //ensure to unregister from all events! - $scope.$on('$destroy', function () { - for (var e in evts) { - eventsService.unsubscribe(evts[e]); - } - }); - -} - -angular.module("umbraco").controller("Umbraco.Editors.Media.EditController", mediaEditController); +/** + * @ngdoc controller + * @name Umbraco.Editors.Media.EditController + * @function + * + * @description + * The controller for the media editor + */ +function mediaEditController($scope, $routeParams, $q, appState, mediaResource, entityResource, navigationService, notificationsService, angularHelper, serverValidationManager, contentEditingHelper, fileManager, treeService, formHelper, umbModelMapper, editorState, umbRequestHelper, $http, eventsService) { + + var evts = []; + var nodeId = null; + var create = false; + var infiniteMode = $scope.model && $scope.model.infiniteMode; + + // when opening the editor through infinite editing get the + // node id from the model instead of the route param + if(infiniteMode && $scope.model.id) { + nodeId = $scope.model.id; + } else { + nodeId = $routeParams.id; + } + + // when opening the editor through infinite editing get the + // create option from the model instead of the route param + if(infiniteMode) { + create = $scope.model.create; + } else { + create = $routeParams.create; + } + + //setup scope vars + $scope.currentSection = appState.getSectionState("currentSection"); + $scope.currentNode = null; //the editors affiliated node + + $scope.page = {}; + $scope.page.loading = false; + $scope.page.menu = {}; + $scope.page.menu.currentSection = appState.getSectionState("currentSection"); + $scope.page.menu.currentNode = null; //the editors affiliated node + $scope.page.listViewPath = null; + $scope.page.saveButtonState = "init"; + $scope.page.submitButtonLabel = "Save"; + + /** Syncs the content item to it's tree node - this occurs on first load and after saving */ + function syncTreeNode(content, path, initialLoad) { + + if (!$scope.content.isChildOfListView) { + navigationService.syncTree({ tree: "media", path: path.split(","), forceReload: initialLoad !== true }).then(function (syncArgs) { + $scope.page.menu.currentNode = syncArgs.node; + }); + } + else if (initialLoad === true) { + + //it's a child item, just sync the ui node to the parent + navigationService.syncTree({ tree: "media", path: path.substring(0, path.lastIndexOf(",")).split(","), forceReload: initialLoad !== true }); + + //if this is a child of a list view and it's the initial load of the editor, we need to get the tree node + // from the server so that we can load in the actions menu. + umbRequestHelper.resourcePromise( + $http.get(content.treeNodeUrl), + 'Failed to retrieve data for child node ' + content.id).then(function (node) { + $scope.page.menu.currentNode = node; + }); + } + } + + if (create) { + + $scope.page.loading = true; + + mediaResource.getScaffold(nodeId, $routeParams.doctype) + .then(function (data) { + $scope.content = data; + + editorState.set($scope.content); + + // We don't get the info tab from the server from version 7.8 so we need to manually add it + //contentEditingHelper.addInfoTab($scope.content.tabs); + + init($scope.content); + + $scope.page.loading = false; + + }); + } + else { + $scope.page.loading = true; + loadMedia() + .then(function(){ + $scope.page.loading = false; + }); + } + + function init(content) { + + // prototype content and info apps + var contentApp = { + "name": "Content", + "alias": "content", + "icon": "icon-document", + "view": "views/media/apps/content/content.html" + }; + + var infoApp = { + "name": "Info", + "alias": "info", + "icon": "icon-info", + "view": "views/media/apps/info/info.html" + }; + + var listview = { + "name": "Child items", + "alias": "childItems", + "icon": "icon-list", + "view": "views/media/apps/listview/listview.html" + }; + + $scope.content.apps = []; + + if($scope.content.contentTypeAlias === "Folder") { + // add list view app + $scope.content.apps.push(listview); + + // remove the list view tab + angular.forEach($scope.content.tabs, function(tab, index){ + if(tab.alias === "Contents") { + tab.hide = true; + } + }); + + } else { + $scope.content.apps.push(contentApp); + } + + $scope.content.apps.push(infoApp); + + // set first app to active + $scope.content.apps[0].active = true; + + // setup infinite mode + if(infiniteMode) { + $scope.page.submitButtonLabel = "Save and Close"; + } + + } + + $scope.save = function () { + + if (!$scope.busy && formHelper.submitForm({ scope: $scope })) { + + $scope.busy = true; + $scope.page.saveButtonState = "busy"; + + mediaResource.save($scope.content, create, fileManager.getFiles()) + .then(function(data) { + + formHelper.resetForm({ scope: $scope }); + + contentEditingHelper.handleSuccessfulSave({ + scope: $scope, + savedContent: data, + redirectOnSuccess: !infiniteMode, + rebindCallback: contentEditingHelper.reBindChangedProperties($scope.content, data) + }); + + editorState.set($scope.content); + $scope.busy = false; + + // when don't want to sync the tree when the editor is open in infinite mode + if(!infiniteMode) { + syncTreeNode($scope.content, data.path); + } + + init($scope.content); + + $scope.page.saveButtonState = "success"; + + // close the editor if it's infinite mode + if(infiniteMode && $scope.model.submit) { + $scope.model.mediaNode = $scope.content; + $scope.model.submit($scope.model); + } + + }, function(err) { + + contentEditingHelper.handleSaveError({ + err: err, + redirectOnError: !infiniteMode, + rebindCallback: contentEditingHelper.reBindChangedProperties($scope.content, err.data) + }); + + editorState.set($scope.content); + $scope.busy = false; + $scope.page.saveButtonState = "error"; + + }); + }else{ + $scope.busy = false; + } + + }; + + function loadMedia() { + + return mediaResource.getById(nodeId) + .then(function (data) { + + $scope.content = data; + + if (data.isChildOfListView && data.trashed === false) { + $scope.page.listViewPath = ($routeParams.page) + ? "/media/media/edit/" + data.parentId + "?page=" + $routeParams.page + : "/media/media/edit/" + data.parentId; + } + + editorState.set($scope.content); + + //in one particular special case, after we've created a new item we redirect back to the edit + // route but there might be server validation errors in the collection which we need to display + // after the redirect, so we will bind all subscriptions which will show the server validation errors + // if there are any and then clear them so the collection no longer persists them. + serverValidationManager.executeAndClearAllSubscriptions(); + + if(!infiniteMode) { + syncTreeNode($scope.content, data.path, true); + } + + if ($scope.content.parentId && $scope.content.parentId != -1) { + //We fetch all ancestors of the node to generate the footer breadcrump navigation + entityResource.getAncestors(nodeId, "media") + .then(function (anc) { + $scope.ancestors = anc; + }); + } + + // We don't get the info tab from the server from version 7.8 so we need to manually add it + //contentEditingHelper.addInfoTab($scope.content.tabs); + + init($scope.content); + + $scope.page.loading = false; + + $q.resolve($scope.content); + + }); + + } + + $scope.close = function() { + if($scope.model.close) { + $scope.model.close($scope.model); + } + }; + + evts.push(eventsService.on("editors.mediaType.saved", function(name, args) { + // if this media item uses the updated media type we need to reload the media item + if(args && args.mediaType && args.mediaType.key === $scope.content.contentType.key) { + loadMedia(); + } + })); + + //ensure to unregister from all events! + $scope.$on('$destroy', function () { + for (var e in evts) { + eventsService.unsubscribe(evts[e]); + } + }); + +} + +angular.module("umbraco").controller("Umbraco.Editors.Media.EditController", mediaEditController); diff --git a/src/Umbraco.Web.UI.Client/src/views/mediatypes/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/mediatypes/edit.controller.js index 0918a43188..2608ddf300 100644 --- a/src/Umbraco.Web.UI.Client/src/views/mediatypes/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/mediatypes/edit.controller.js @@ -1,419 +1,419 @@ -/** - * @ngdoc controller - * @name Umbraco.Editors.MediaType.EditController - * @function - * - * @description - * The controller for the media type editor - */ -(function () { - "use strict"; - - function MediaTypesEditController($scope, $routeParams, mediaTypeResource, dataTypeResource, editorState, contentEditingHelper, formHelper, navigationService, iconHelper, contentTypeHelper, notificationsService, $filter, $q, localizationService, overlayHelper, eventsService) { - - var vm = this; - var evts = []; - var mediaTypeId = $routeParams.id; - var create = $routeParams.create; - var infiniteMode = $scope.model && $scope.model.infiniteMode; - - vm.save = save; - vm.close = close; - - vm.currentNode = null; - vm.contentType = {}; - vm.page = {}; - vm.page.loading = false; - vm.page.saveButtonState = "init"; - vm.labels = {}; - vm.saveButtonKey = "buttons_save"; - vm.generateModelsKey = "buttons_saveAndGenerateModels"; - - onInit(); - - function onInit() { - // get init values from model when in infinite mode - if(infiniteMode) { - mediaTypeId = $scope.model.id; - create = $scope.model.create; - vm.saveButtonKey = "buttons_saveAndClose"; - vm.generateModelsKey = "buttons_generateModelsAndClose"; - } - } - - var labelKeys = [ - "general_design", - "general_listView", - "general_rights", - - "main_sections", - "shortcuts_navigateSections", - "shortcuts_addTab", - "shortcuts_addProperty", - "shortcuts_addEditor", - "shortcuts_editDataType", - "shortcuts_toggleListView", - "shortcuts_toggleAllowAsRoot", - "shortcuts_addChildNode" - ]; - - localizationService.localizeMany(labelKeys).then(function (values) { - // navigation - vm.labels.design = values[0]; - vm.labels.listview = values[1]; - vm.labels.permissions = values[2]; - // keyboard shortcuts - vm.labels.sections = values[3]; - vm.labels.navigateSections = values[4]; - vm.labels.addTab = values[5]; - vm.labels.addProperty = values[6]; - vm.labels.addEditor = values[7]; - vm.labels.editDataType = values[8]; - vm.labels.toggleListView = values[9]; - vm.labels.allowAsRoot = values[10]; - vm.labels.addChildNode = values[11]; - - vm.page.navigation = [ - { - "name": vm.labels.design, - "icon": "icon-document-dashed-line", - "view": "views/mediatypes/views/design/design.html", - "active": true - }, - { - "name": vm.labels.listview, - "icon": "icon-list", - "view": "views/mediatypes/views/listview/listview.html" - }, - { - "name": vm.labels.permissions, - "icon": "icon-keychain", - "view": "views/mediatypes/views/permissions/permissions.html" - } - ]; - - vm.page.keyboardShortcutsOverview = [ - { - "name": vm.labels.sections, - "shortcuts": [ - { - "description": vm.labels.navigateSections, - "keys": [{ "key": "1" }, { "key": "3" }], - "keyRange": true - } - ] - }, - { - "name": vm.labels.design, - "shortcuts": [ - { - "description": vm.labels.addTab, - "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "t" }] - }, - { - "description": vm.labels.addProperty, - "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "p" }] - }, - { - "description": vm.labels.addEditor, - "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "e" }] - }, - { - "description": vm.labels.editDataType, - "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "d" }] - } - ] - }, - { - "name": vm.labels.listview, - "shortcuts": [ - { - "description": vm.labels.toggleListView, - "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "l" }] - } - ] - }, - { - "name": vm.labels.permissions, - "shortcuts": [ - { - "description": vm.labels.allowAsRoot, - "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "r" }] - }, - { - "description": vm.labels.addChildNode, - "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "c" }] - } - ] - } - ]; - }); - - contentTypeHelper.checkModelsBuilderStatus().then(function (result) { - vm.page.modelsBuilder = result; - if (result) { - //Models builder mode: - vm.page.defaultButton = { - hotKey: "ctrl+s", - hotKeyWhenHidden: true, - labelKey: vm.saveButtonKey, - letter: "S", - type: "submit", - handler: function () { vm.save(); } - }; - vm.page.subButtons = [{ - hotKey: "ctrl+g", - hotKeyWhenHidden: true, - labelKey: vm.generateModelsKey, - letter: "G", - handler: function () { - - vm.page.saveButtonState = "busy"; - - vm.save().then(function (result) { - - vm.page.saveButtonState = "busy"; - - localizationService.localize("modelsBuilder_buildingModels").then(function (headerValue) { - localizationService.localize("modelsBuilder_waitingMessage").then(function(msgValue) { - notificationsService.info(headerValue, msgValue); - }); - }); - - contentTypeHelper.generateModels().then(function (result) { - - if (result.success) { - - //re-check model status - contentTypeHelper.checkModelsBuilderStatus().then(function (statusResult) { - vm.page.modelsBuilder = statusResult; - }); - - //clear and add success - vm.page.saveButtonState = "init"; - localizationService.localize("modelsBuilder_modelsGenerated").then(function(value) { - notificationsService.success(value); - }); - - } else { - vm.page.saveButtonState = "error"; - localizationService.localize("modelsBuilder_modelsExceptionInUlog").then(function(value) { - notificationsService.error(value); - }); - } - - }, function () { - vm.page.saveButtonState = "error"; - localizationService.localize("modelsBuilder_modelsGeneratedError").then(function(value) { - notificationsService.error(value); - }); - }); - - }); - - } - }]; - } - }); - - if (create) { - vm.page.loading = true; - - //we are creating so get an empty data type item - mediaTypeResource.getScaffold(mediaTypeId) - .then(function(dt) { - init(dt); - - vm.page.loading = false; - }); - } - else { - loadMediaType(); - } - - function loadMediaType() { - vm.page.loading = true; - - mediaTypeResource.getById(mediaTypeId).then(function(dt) { - init(dt); - - if(!infiniteMode) { - syncTreeNode(vm.contentType, dt.path, true); - } - - vm.page.loading = false; - }); - } - - /* ---------- SAVE ---------- */ - - function save() { - - // only save if there is no overlays open - if(overlayHelper.getNumberOfOverlays() === 0) { - - var deferred = $q.defer(); - - vm.page.saveButtonState = "busy"; - - // reformat allowed content types to array if id's - vm.contentType.allowedContentTypes = contentTypeHelper.createIdArray(vm.contentType.allowedContentTypes); - - contentEditingHelper.contentEditorPerformSave({ - saveMethod: mediaTypeResource.save, - scope: $scope, - content: vm.contentType, - //We do not redirect on failure for doc types - this is because it is not possible to actually save the doc - // type when server side validation fails - as opposed to content where we are capable of saving the content - // item if server side validation fails - redirectOnFailure: false, - // we need to rebind... the IDs that have been created! - rebindCallback: function (origContentType, savedContentType) { - vm.contentType.id = savedContentType.id; - vm.contentType.groups.forEach(function (group) { - if (!group.name) return; - - var k = 0; - while (k < savedContentType.groups.length && savedContentType.groups[k].name != group.name) - k++; - if (k == savedContentType.groups.length) { - group.id = 0; - return; - } - - var savedGroup = savedContentType.groups[k]; - if (!group.id) group.id = savedGroup.id; - - group.properties.forEach(function (property) { - if (property.id || !property.alias) return; - - k = 0; - while (k < savedGroup.properties.length && savedGroup.properties[k].alias != property.alias) - k++; - if (k == savedGroup.properties.length) { - property.id = 0; - return; - } - - var savedProperty = savedGroup.properties[k]; - property.id = savedProperty.id; - }); - }); - } - }).then(function (data) { - //success - - if(!infiniteMode) { - syncTreeNode(vm.contentType, data.path); - } - - // emit event - var args = { mediaType: vm.contentType }; - eventsService.emit("editors.mediaType.saved", args); - - vm.page.saveButtonState = "success"; - - if(infiniteMode && $scope.model.submit) { - $scope.model.submit(); - } - - deferred.resolve(data); - - }, function (err) { - //error - if (err) { - editorState.set($scope.content); - } - else { - localizationService.localize("speechBubbles_validationFailedHeader").then(function (headerValue) { - localizationService.localize("speechBubbles_validationFailedMessage").then(function (msgValue) { - notificationsService.error(headerValue, msgValue); - }); - }); - } - - vm.page.saveButtonState = "error"; - - deferred.reject(err); - }); - - return deferred.promise; - } - } - - function init(contentType) { - - // set all tab to inactive - if (contentType.groups.length !== 0) { - angular.forEach(contentType.groups, function (group) { - - angular.forEach(group.properties, function (property) { - // get data type details for each property - getDataTypeDetails(property); - }); - - }); - } - - // convert icons for content type - convertLegacyIcons(contentType); - - //set a shared state - editorState.set(contentType); - - vm.contentType = contentType; - } - - function convertLegacyIcons(contentType) { - // make array to store contentType icon - var contentTypeArray = []; - - // push icon to array - contentTypeArray.push({ "icon": contentType.icon }); - - // run through icon method - iconHelper.formatContentTypeIcons(contentTypeArray); - - // set icon back on contentType - contentType.icon = contentTypeArray[0].icon; - } - - function getDataTypeDetails(property) { - if (property.propertyState !== "init") { - - dataTypeResource.getById(property.dataTypeId) - .then(function(dataType) { - property.dataTypeIcon = dataType.icon; - property.dataTypeName = dataType.name; - }); - } - } - - - /** Syncs the content type to it's tree node - this occurs on first load and after saving */ - function syncTreeNode(dt, path, initialLoad) { - navigationService.syncTree({ tree: "mediatypes", path: path.split(","), forceReload: initialLoad !== true }).then(function(syncArgs) { - vm.currentNode = syncArgs.node; - }); - } - - function close() { - if(infiniteMode && $scope.model.close) { - $scope.model.close(); - } - } - - evts.push(eventsService.on("app.refreshEditor", function(name, error) { - loadMediaType(); - })); - - //ensure to unregister from all events! - $scope.$on('$destroy', function () { - for (var e in evts) { - eventsService.unsubscribe(evts[e]); - } - }); - } - - angular.module("umbraco").controller("Umbraco.Editors.MediaTypes.EditController", MediaTypesEditController); -})(); +/** + * @ngdoc controller + * @name Umbraco.Editors.MediaType.EditController + * @function + * + * @description + * The controller for the media type editor + */ +(function () { + "use strict"; + + function MediaTypesEditController($scope, $routeParams, mediaTypeResource, dataTypeResource, editorState, contentEditingHelper, formHelper, navigationService, iconHelper, contentTypeHelper, notificationsService, $filter, $q, localizationService, overlayHelper, eventsService) { + + var vm = this; + var evts = []; + var mediaTypeId = $routeParams.id; + var create = $routeParams.create; + var infiniteMode = $scope.model && $scope.model.infiniteMode; + + vm.save = save; + vm.close = close; + + vm.currentNode = null; + vm.contentType = {}; + vm.page = {}; + vm.page.loading = false; + vm.page.saveButtonState = "init"; + vm.labels = {}; + vm.saveButtonKey = "buttons_save"; + vm.generateModelsKey = "buttons_saveAndGenerateModels"; + + onInit(); + + function onInit() { + // get init values from model when in infinite mode + if(infiniteMode) { + mediaTypeId = $scope.model.id; + create = $scope.model.create; + vm.saveButtonKey = "buttons_saveAndClose"; + vm.generateModelsKey = "buttons_generateModelsAndClose"; + } + } + + var labelKeys = [ + "general_design", + "general_listView", + "general_rights", + + "main_sections", + "shortcuts_navigateSections", + "shortcuts_addTab", + "shortcuts_addProperty", + "shortcuts_addEditor", + "shortcuts_editDataType", + "shortcuts_toggleListView", + "shortcuts_toggleAllowAsRoot", + "shortcuts_addChildNode" + ]; + + localizationService.localizeMany(labelKeys).then(function (values) { + // navigation + vm.labels.design = values[0]; + vm.labels.listview = values[1]; + vm.labels.permissions = values[2]; + // keyboard shortcuts + vm.labels.sections = values[3]; + vm.labels.navigateSections = values[4]; + vm.labels.addTab = values[5]; + vm.labels.addProperty = values[6]; + vm.labels.addEditor = values[7]; + vm.labels.editDataType = values[8]; + vm.labels.toggleListView = values[9]; + vm.labels.allowAsRoot = values[10]; + vm.labels.addChildNode = values[11]; + + vm.page.navigation = [ + { + "name": vm.labels.design, + "icon": "icon-document-dashed-line", + "view": "views/mediatypes/views/design/design.html", + "active": true + }, + { + "name": vm.labels.listview, + "icon": "icon-list", + "view": "views/mediatypes/views/listview/listview.html" + }, + { + "name": vm.labels.permissions, + "icon": "icon-keychain", + "view": "views/mediatypes/views/permissions/permissions.html" + } + ]; + + vm.page.keyboardShortcutsOverview = [ + { + "name": vm.labels.sections, + "shortcuts": [ + { + "description": vm.labels.navigateSections, + "keys": [{ "key": "1" }, { "key": "3" }], + "keyRange": true + } + ] + }, + { + "name": vm.labels.design, + "shortcuts": [ + { + "description": vm.labels.addTab, + "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "t" }] + }, + { + "description": vm.labels.addProperty, + "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "p" }] + }, + { + "description": vm.labels.addEditor, + "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "e" }] + }, + { + "description": vm.labels.editDataType, + "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "d" }] + } + ] + }, + { + "name": vm.labels.listview, + "shortcuts": [ + { + "description": vm.labels.toggleListView, + "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "l" }] + } + ] + }, + { + "name": vm.labels.permissions, + "shortcuts": [ + { + "description": vm.labels.allowAsRoot, + "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "r" }] + }, + { + "description": vm.labels.addChildNode, + "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "c" }] + } + ] + } + ]; + }); + + contentTypeHelper.checkModelsBuilderStatus().then(function (result) { + vm.page.modelsBuilder = result; + if (result) { + //Models builder mode: + vm.page.defaultButton = { + hotKey: "ctrl+s", + hotKeyWhenHidden: true, + labelKey: vm.saveButtonKey, + letter: "S", + type: "submit", + handler: function () { vm.save(); } + }; + vm.page.subButtons = [{ + hotKey: "ctrl+g", + hotKeyWhenHidden: true, + labelKey: vm.generateModelsKey, + letter: "G", + handler: function () { + + vm.page.saveButtonState = "busy"; + + vm.save().then(function (result) { + + vm.page.saveButtonState = "busy"; + + localizationService.localize("modelsBuilder_buildingModels").then(function (headerValue) { + localizationService.localize("modelsBuilder_waitingMessage").then(function(msgValue) { + notificationsService.info(headerValue, msgValue); + }); + }); + + contentTypeHelper.generateModels().then(function (result) { + + if (result.success) { + + //re-check model status + contentTypeHelper.checkModelsBuilderStatus().then(function (statusResult) { + vm.page.modelsBuilder = statusResult; + }); + + //clear and add success + vm.page.saveButtonState = "init"; + localizationService.localize("modelsBuilder_modelsGenerated").then(function(value) { + notificationsService.success(value); + }); + + } else { + vm.page.saveButtonState = "error"; + localizationService.localize("modelsBuilder_modelsExceptionInUlog").then(function(value) { + notificationsService.error(value); + }); + } + + }, function () { + vm.page.saveButtonState = "error"; + localizationService.localize("modelsBuilder_modelsGeneratedError").then(function(value) { + notificationsService.error(value); + }); + }); + + }); + + } + }]; + } + }); + + if (create) { + vm.page.loading = true; + + //we are creating so get an empty data type item + mediaTypeResource.getScaffold(mediaTypeId) + .then(function(dt) { + init(dt); + + vm.page.loading = false; + }); + } + else { + loadMediaType(); + } + + function loadMediaType() { + vm.page.loading = true; + + mediaTypeResource.getById(mediaTypeId).then(function(dt) { + init(dt); + + if(!infiniteMode) { + syncTreeNode(vm.contentType, dt.path, true); + } + + vm.page.loading = false; + }); + } + + /* ---------- SAVE ---------- */ + + function save() { + + // only save if there is no overlays open + if(overlayHelper.getNumberOfOverlays() === 0) { + + var deferred = $q.defer(); + + vm.page.saveButtonState = "busy"; + + // reformat allowed content types to array if id's + vm.contentType.allowedContentTypes = contentTypeHelper.createIdArray(vm.contentType.allowedContentTypes); + + contentEditingHelper.contentEditorPerformSave({ + saveMethod: mediaTypeResource.save, + scope: $scope, + content: vm.contentType, + //We do not redirect on failure for doc types - this is because it is not possible to actually save the doc + // type when server side validation fails - as opposed to content where we are capable of saving the content + // item if server side validation fails + redirectOnFailure: false, + // we need to rebind... the IDs that have been created! + rebindCallback: function (origContentType, savedContentType) { + vm.contentType.id = savedContentType.id; + vm.contentType.groups.forEach(function (group) { + if (!group.name) return; + + var k = 0; + while (k < savedContentType.groups.length && savedContentType.groups[k].name != group.name) + k++; + if (k == savedContentType.groups.length) { + group.id = 0; + return; + } + + var savedGroup = savedContentType.groups[k]; + if (!group.id) group.id = savedGroup.id; + + group.properties.forEach(function (property) { + if (property.id || !property.alias) return; + + k = 0; + while (k < savedGroup.properties.length && savedGroup.properties[k].alias != property.alias) + k++; + if (k == savedGroup.properties.length) { + property.id = 0; + return; + } + + var savedProperty = savedGroup.properties[k]; + property.id = savedProperty.id; + }); + }); + } + }).then(function (data) { + //success + + if(!infiniteMode) { + syncTreeNode(vm.contentType, data.path); + } + + // emit event + var args = { mediaType: vm.contentType }; + eventsService.emit("editors.mediaType.saved", args); + + vm.page.saveButtonState = "success"; + + if(infiniteMode && $scope.model.submit) { + $scope.model.submit(); + } + + deferred.resolve(data); + + }, function (err) { + //error + if (err) { + editorState.set($scope.content); + } + else { + localizationService.localize("speechBubbles_validationFailedHeader").then(function (headerValue) { + localizationService.localize("speechBubbles_validationFailedMessage").then(function (msgValue) { + notificationsService.error(headerValue, msgValue); + }); + }); + } + + vm.page.saveButtonState = "error"; + + deferred.reject(err); + }); + + return deferred.promise; + } + } + + function init(contentType) { + + // set all tab to inactive + if (contentType.groups.length !== 0) { + angular.forEach(contentType.groups, function (group) { + + angular.forEach(group.properties, function (property) { + // get data type details for each property + getDataTypeDetails(property); + }); + + }); + } + + // convert icons for content type + convertLegacyIcons(contentType); + + //set a shared state + editorState.set(contentType); + + vm.contentType = contentType; + } + + function convertLegacyIcons(contentType) { + // make array to store contentType icon + var contentTypeArray = []; + + // push icon to array + contentTypeArray.push({ "icon": contentType.icon }); + + // run through icon method + iconHelper.formatContentTypeIcons(contentTypeArray); + + // set icon back on contentType + contentType.icon = contentTypeArray[0].icon; + } + + function getDataTypeDetails(property) { + if (property.propertyState !== "init") { + + dataTypeResource.getById(property.dataTypeId) + .then(function(dataType) { + property.dataTypeIcon = dataType.icon; + property.dataTypeName = dataType.name; + }); + } + } + + + /** Syncs the content type to it's tree node - this occurs on first load and after saving */ + function syncTreeNode(dt, path, initialLoad) { + navigationService.syncTree({ tree: "mediatypes", path: path.split(","), forceReload: initialLoad !== true }).then(function(syncArgs) { + vm.currentNode = syncArgs.node; + }); + } + + function close() { + if(infiniteMode && $scope.model.close) { + $scope.model.close(); + } + } + + evts.push(eventsService.on("app.refreshEditor", function(name, error) { + loadMediaType(); + })); + + //ensure to unregister from all events! + $scope.$on('$destroy', function () { + for (var e in evts) { + eventsService.unsubscribe(evts[e]); + } + }); + } + + angular.module("umbraco").controller("Umbraco.Editors.MediaTypes.EditController", MediaTypesEditController); +})(); diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml index 3b3d5b4b6e..711280ea4c 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -1,2170 +1,2170 @@ - - - - The Umbraco community - http://our.umbraco.org/documentation/Extending-Umbraco/Language-Files - - - Culture and Hostnames - Audit Trail - Browse Node - Change Document Type - Copy - Create - Export - Create Package - Create group - Delete - Disable - Empty recycle bin - Enable - Export Document Type - Import Document Type - Import Package - Edit in Canvas - Exit - Move - Notifications - Public access - Publish - Unpublish - Reload - Republish entire site - Rename - Restore - Set permissions for the page %0% - Choose where to move - In the tree structure below - Permissions - Rollback - Send To Publish - Send To Translation - Set group - Sort - Translate - Update - Set permissions - Unlock - Create Content Template - - - Content - Administration - Structure - Other - - - Allow access to assign culture and hostnames - Allow access to view a node's history log - Allow access to view a node - Allow access to change document type for a node - Allow access to copy a node - Allow access to create nodes - Allow access to delete nodes - Allow access to move a node - Allow access to set and change public access for a node - Allow access to publish a node - Allow access to change permissions for a node - Allow access to roll back a node to a previous state - Allow access to send a node for approval before publishing - Allow access to send a node for translation - Allow access to change the sort order for nodes - Allow access to translate a node - Allow access to save a node - Allow access to create a Content Template - - - Permission denied. - Add new Domain - remove - Invalid node. - Invalid domain format. - Domain has already been assigned. - Language - Domain - New domain '%0%' has been created - Domain '%0%' is deleted - Domain '%0%' has already been assigned - Domain '%0%' has been updated - Edit Current Domains - - - - Inherit - Culture - - or inherit culture from parent nodes. Will also apply
- to the current node, unless a domain below applies too.]]> -
- Domains - - - Viewing for - - - Clear selection - Select - Select current folder - Do something else - Bold - Cancel Paragraph Indent - Insert form field - Insert graphic headline - Edit Html - Indent Paragraph - Italic - Center - Justify Left - Justify Right - Insert Link - Insert local link (anchor) - Bullet List - Numeric List - Insert macro - Insert picture - Edit relations - Return to list - Save - Save and close - Publish - Publish… - Save and schedule - Save and send for approval - Save list view - Preview - Preview is disabled because there's no template assigned - Choose style - Show styles - Insert table - Generate models - Generate models and close - Save and generate models - Undo - Redo - - - To change the document type for the selected content, first select from the list of valid types for this location. - Then confirm and/or amend the mapping of properties from the current type to the new, and click Save. - The content has been re-published. - Current Property - Current type - The document type cannot be changed, as there are no alternatives valid for this location. An alternative will be valid if it is allowed under the parent of the selected content item and that all existing child content items are allowed to be created under it. - Document Type Changed - Map Properties - Map to Property - New Template - New Type - none - Content - Select New Document Type - The document type of the selected content has been successfully changed to [new type] and the following properties mapped: - to - Could not complete property mapping as one or more properties have more than one mapping defined. - Only alternate types valid for the current location are displayed. - - - Is Published - About this page - Alias - (how would you describe the picture over the phone) - Alternative Links - Click to edit this item - Created by - Original author - Updated by - Created - Date/time this document was created - Document Type - Editing - Remove at - This item has been changed after publication - This item is not published - Culture '%0%' for this item is not published - Culture '%0%' for this item is not available - Last published - There are no items to show - There are no items to show in the list. - No content has been added - No members have been added - Media Type - Link to media item(s) - Member Group - Role - Member Type - No changes have been made - No date chosen - Page title - This media item has no link - Properties - This document is published but is not visible because the parent '%0%' is unpublished - This document is published but is not in the cache - Could not get the url - This document is published but its url would collide with content %0% - Publish - Published - Published (pending changes) - Unpublished (pending changes) - Publication Status - Publish at - Unpublish at - Clear Date - Set date - Sortorder is updated - To sort the nodes, simply drag the nodes or click one of the column headers. You can select multiple nodes by holding the "shift" or "control" key while selecting - Statistics - Title (optional) - Alternative text (optional) - Type - Unpublish - Unpublished - Last edited - Date/time this document was edited - Remove file(s) - Link to document - Member of group(s) - Not a member of group(s) - Child items - Target - This translates to the following time on the server: - What does this mean?]]> - Are you sure you want to delete this item? - Property %0% uses editor %1% which is not supported by Nested Content. - Add another text box - Remove this text box - Content root - This value is hidden. If you need access to view this value please contact your website administrator. - This value is hidden. - What languages would you like to publish? - Published Languages. - Ready to Publish? - - - Create a new Content Template from '%0%' - Blank - Select a Content Template - Content Template created - A Content Template was created from '%0%' - Another Content Template with the same name already exists - A Content Template is pre-defined content that an editor can select to use as the basis for creating new content - - - Click to upload - Drop your files here... - Link to media - or click here to choose files - Only allowed file types are - Cannot upload this file, it does not have an approved file type - Max file size is - Media root - - - Create a new member - All Members - - - Where do you want to create the new %0% - Create an item under - Select the document type you want to make a content template for - Choose a type and a title - "document types".]]> - "media types".]]> - Document Type without a template - New folder - New data type - New javascript file - New empty partial view - New partial view macro - New partial view from snippet - New empty partial view macro - New partial view macro from snippet - New partial view macro (without macro) - - - Browse your website - - Hide - If Umbraco isn't opening, you might need to allow popups from this site - has opened in a new window - Restart - Visit - Welcome - - - Stay - Discard changes - You have unsaved changes - Are you sure you want to navigate away from this page? - you have unsaved changes - - - Done - - Deleted %0% item - Deleted %0% items - Deleted %0% out of %1% item - Deleted %0% out of %1% items - - Published %0% item - Published %0% items - Published %0% out of %1% item - Published %0% out of %1% items - - Unpublished %0% item - Unpublished %0% items - Unpublished %0% out of %1% item - Unpublished %0% out of %1% items - - Moved %0% item - Moved %0% items - Moved %0% out of %1% item - Moved %0% out of %1% items - - Copied %0% item - Copied %0% items - Copied %0% out of %1% item - Copied %0% out of %1% items - - - Link title - Link - Name - Close this window - Are you sure you want to delete - Are you sure you want to disable - Please check this box to confirm deletion of %0% item(s) - Are you sure? - Are you sure? - Cut - Edit Dictionary Item - Edit Language - Insert local link - Insert character - Insert graphic headline - Insert picture - Insert link - Click to add a Macro - Insert table - Last Edited - Link - Internal link: - When using local links, insert "#" in front of link - Open in new window? - Macro Settings - This macro does not contain any properties you can edit - Paste - Edit permissions for - Set permissions for - Set permissions for %0% for user group %1% - Select the users groups you want to set permissions for - The items in the recycle bin are now being deleted. Please do not close this window while this operation takes place - The recycle bin is now empty - When items are deleted from the recycle bin, they will be gone forever - regexlib.com's webservice is currently experiencing some problems, which we have no control over. We are very sorry for this inconvenience.]]> - Search for a regular expression to add validation to a form field. Example: 'email, 'zip-code' 'url' - Remove Macro - Required Field - Site is reindexed - The website cache has been refreshed. All publish content is now up to date. While all unpublished content is still unpublished - The website cache will be refreshed. All published content will be updated, while unpublished content will stay unpublished. - Number of columns - Number of rows - - Set a placeholder id by setting an ID on your placeholder you can inject content into this template from child templates, - by referring this ID using a <asp:content /> element.]]> - - - Select a placeholder id from the list below. You can only - choose Id's from the current template's master.]]> - - Click on the image to see full size - Pick item - View Cache Item - Create folder... - Relate to original - Include descendants - The friendliest community - Link to page - Opens the linked document in a new window or tab - Link to media - Link to file - Select content start node - Select media - Select icon - Select item - Select link - Select macro - Select content - Select media start node - Select member - Select member group - Select node - Select sections - Select users - No icons were found - There are no parameters for this macro - There are no macros available to insert - External login providers - Exception Details - Stacktrace - Inner Exception - Link your - Un-link your - account - Select editor - Select snippet - This will delete the node and all its languages. If you only want to delete one language go and unpublish it instead. - - - - %0%' below
You can add additional languages under the 'languages' in the menu on the left - ]]> -
- Culture Name - Edit the key of the dictionary item. - - - - - - Enter your username - Enter your password - Confirm your password - Name the %0%... - Enter a name... - Enter an email... - Enter a username... - Label... - Enter a description... - Type to search... - Type to filter... - Type to add tags (press enter after each tag)... - Enter your email... - Enter a message... - Your username is usually your email - - - Allow at root - Only Content Types with this checked can be created at the root level of Content and Media trees - Allowed child node types - Document Type Compositions - Create - Delete tab - Description - New tab - Tab - Thumbnail - Enable list view - Configures the content item to show a sortable & searchable list of its children, the children will not be shown in the tree - Current list view - The active list view data type - Create custom list view - Remove custom list view - - - Renamed - Enter a new folder name here - %0% was renamed to %1% - - - Add prevalue - Database datatype - Property editor GUID - Property editor - Buttons - Enable advanced settings for - Enable context menu - Maximum default size of inserted images - Related stylesheets - Show label - Width and height - All property types & property data - using this data type will be deleted permanently, please confirm you want to delete these as well - Yes, delete - and all property types & property data using this data type - Select the folder to move - to in the tree structure below - was moved underneath - - - Your data has been saved, but before you can publish this page there are some errors you need to fix first: - The current membership provider does not support changing password (EnablePasswordRetrieval need to be true) - %0% already exists - There were errors: - There were errors: - The password should be a minimum of %0% characters long and contain at least %1% non-alpha numeric character(s) - %0% must be an integer - The %0% field in the %1% tab is mandatory - %0% is a mandatory field - %0% at %1% is not in a correct format - %0% is not in a correct format - - - Received an error from the server - The specified file type has been disallowed by the administrator - NOTE! Even though CodeMirror is enabled by configuration, it is disabled in Internet Explorer because it's not stable enough. - Please fill both alias and name on the new property type! - There is a problem with read/write access to a specific file or folder - Error loading Partial View script (file: %0%) - Error loading userControl '%0%' - Error loading customControl (Assembly: %0%, Type: '%1%') - Error loading MacroEngine script (file: %0%) - Please enter a title - Please choose a type - You're about to make the picture larger than the original size. Are you sure that you want to proceed? - Error in python script - The python script has not been saved, because it contained error(s) - Startnode deleted, please contact your administrator - Please mark content before changing style - No active styles available - Please place cursor at the left of the two cells you wish to merge - You cannot split a cell that hasn't been merged. - There is a configuration error with the data type used for this property, please check the data type - - - Options - About - Action - Actions - Add - Alias - All - Are you sure? - Back - Border - by - Cancel - Cell margin - Choose - Close - Close Window - Comment - Confirm - Constrain - Constrain proportions - Continue - Copy - Create - Database - Date - Default - Delete - Deleted - Deleting... - Design - Dictionary - Dimensions - Down - Download - Edit - Edited - Elements - Email - Error - Find - First - General - Groups - Height - Help - Hide - History - Icon - Import - Info - Inner margin - Insert - Install - Invalid - Justify - Label - Language - Last - Layout - Links - Loading - Locked - Login - Log off - Logout - Macro - Mandatory - Message - Move - More - Name - New - Next - No - of - Off - OK - Open - On - or - Order by - Password - Path - Placeholder ID - One moment please... - Previous - Properties - Email to receive form data - Recycle Bin - Your recycle bin is empty - Remaining - Remove - Rename - Renew - Required - Retrieve - Retry - Permissions - Scheduled Publishing - Search - Sorry, we can not find what you are looking for - No items have been added - Server - Settings - Show - Show page on Send - Size - Sort - Status - Submit - Type - Type to search... - Up - Update - Upgrade - Upload - Url - User - Username - Value - View - Welcome... - Width - Yes - Folder - Search results - Reorder - I am done reordering - Preview - Change password - to - List view - Saving... - current - Embed - Retrieve - selected - - - Black - Green - Yellow - Orange - Blue - Blue Grey - Grey - Brown - Light Blue - Cyan - Light Green - Lime - Amber - Deep Orange - Red - Pink - Purple - Deep Purple - Indigo - - - Add tab - Add property - Add editor - Add template - Add child node - Add child - - Edit data type - - Navigate sections - - Shortcuts - show shortcuts - - Toggle list view - Toggle allow as root - - Comment/Uncomment lines - Remove line - Copy Lines Up - Copy Lines Down - Move Lines Up - Move Lines Down - - General - Editor - - - Background color - Bold - Text color - Font - Text - - - Page - - - The installer cannot connect to the database. - Could not save the web.config file. Please modify the connection string manually. - Your database has been found and is identified as - Database configuration - - install button to install the Umbraco %0% database - ]]> - - Next to proceed.]]> - - Database not found! Please check that the information in the "connection string" of the "web.config" file is correct.

-

To proceed, please edit the "web.config" file (using Visual Studio or your favourite text editor), scroll to the bottom, add the connection string for your database in the key named "UmbracoDbDSN" and save the file.

-

- Click the retry button when - done.
- More information on editing web.config here.

]]> -
- - - Please contact your ISP if necessary. - If you're installing on a local machine or server you might need information from your system administrator.]]> - - - - Press the upgrade button to upgrade your database to Umbraco %0%

-

- Don't worry - no content will be deleted and everything will continue working afterwards! -

- ]]> -
- - Press Next to - proceed. ]]> - - next to continue the configuration wizard]]> - The Default users' password needs to be changed!]]> - The Default user has been disabled or has no access to Umbraco!

No further actions needs to be taken. Click Next to proceed.]]> - The Default user's password has been successfully changed since the installation!

No further actions needs to be taken. Click Next to proceed.]]> - The password is changed! - Get a great start, watch our introduction videos - By clicking the next button (or modifying the umbracoConfigurationStatus in web.config), you accept the license for this software as specified in the box below. Notice that this Umbraco distribution consists of two different licenses, the open source MIT license for the framework and the Umbraco freeware license that covers the UI. - Not installed yet. - Affected files and folders - More information on setting up permissions for Umbraco here - You need to grant ASP.NET modify permissions to the following files/folders - - Your permission settings are almost perfect!

- You can run Umbraco without problems, but you will not be able to install packages which are recommended to take full advantage of Umbraco.]]> -
- How to Resolve - Click here to read the text version - video tutorial on setting up folder permissions for Umbraco or read the text version.]]> - - Your permission settings might be an issue! -

- You can run Umbraco without problems, but you will not be able to create folders or install packages which are recommended to take full advantage of Umbraco.]]> -
- - Your permission settings are not ready for Umbraco! -

- In order to run Umbraco, you'll need to update your permission settings.]]> -
- - Your permission settings are perfect!

- You are ready to run Umbraco and install packages!]]> -
- Resolving folder issue - Follow this link for more information on problems with ASP.NET and creating folders - Setting up folder permissions - - - - I want to start from scratch - - learn how) - You can still choose to install Runway later on. Please go to the Developer section and choose Packages. - ]]> - - You've just set up a clean Umbraco platform. What do you want to do next? - Runway is installed - - - This is our list of recommended modules, check off the ones you would like to install, or view the full list of modules - ]]> - - Only recommended for experienced users - I want to start with a simple website - - - "Runway" is a simple website providing some basic document types and templates. The installer can set up Runway for you automatically, - but you can easily edit, extend or remove it. It's not necessary and you can perfectly use Umbraco without it. However, - Runway offers an easy foundation based on best practices to get you started faster than ever. - If you choose to install Runway, you can optionally select basic building blocks called Runway Modules to enhance your Runway pages. -

- - Included with Runway: Home page, Getting Started page, Installing Modules page.
- Optional Modules: Top Navigation, Sitemap, Contact, Gallery. -
- ]]> -
- What is Runway - Step 1/5 Accept license - Step 2/5: Database configuration - Step 3/5: Validating File Permissions - Step 4/5: Check Umbraco security - Step 5/5: Umbraco is ready to get you started - Thank you for choosing Umbraco - - Browse your new site -You installed Runway, so why not see how your new website looks.]]> - - - Further help and information -Get help from our award winning community, browse the documentation or watch some free videos on how to build a simple site, how to use packages and a quick guide to the Umbraco terminology]]> - - Umbraco %0% is installed and ready for use - - /web.config file and update the AppSetting key UmbracoConfigurationStatus in the bottom to the value of '%0%'.]]> - - - started instantly by clicking the "Launch Umbraco" button below.
If you are new to Umbraco, -you can find plenty of resources on our getting started pages.]]> -
- - Launch Umbraco -To manage your website, simply open the Umbraco back office and start adding content, updating the templates and stylesheets or add new functionality]]> - - Connection to database failed. - Umbraco Version 3 - Umbraco Version 4 - Watch - - Umbraco %0% for a fresh install or upgrading from version 3.0. -

- Press "next" to start the wizard.]]> -
- - - Culture Code - Culture Name - - - You've been idle and logout will automatically occur in - Renew now to save your work - - - Happy super Sunday - Happy manic Monday - Happy tubular Tuesday - Happy wonderful Wednesday - Happy thunderous Thursday - Happy funky Friday - Happy Caturday - Log in below - Sign in with - Session timed out - © 2001 - %0%
Umbraco.com

]]>
- Forgotten password? - An email will be sent to the address specified with a link to reset your password - An email with password reset instructions will be sent to the specified address if it matched our records - Return to login form - Please provide a new password - Your Password has been updated - The link you have clicked on is invalid or has expired - Umbraco: Reset Password - - - - - - - - - - - - -
- - - - - -
- -
- -
-
- - - - - - -
-
-
- - - - -
- - - - -
-

- Password reset requested -

-

- Your username to login to the Umbraco back-office is: %0% -

-

- - - - - - -
- - Click this link to reset your password - -
-

-

If you cannot click on the link, copy and paste this URL into your browser window:

- - - - -
- - %1% - -
-

-
-
-


-
-
- - - ]]> -
- - - Dashboard - Sections - Content - - - Choose page above... - %0% has been copied to %1% - Select where the document %0% should be copied to below - %0% has been moved to %1% - Select where the document %0% should be moved to below - has been selected as the root of your new content, click 'ok' below. - No node selected yet, please select a node in the list above before clicking 'ok' - The current node is not allowed under the chosen node because of its type - The current node cannot be moved to one of its subpages - The current node cannot exist at the root - The action isn't allowed since you have insufficient permissions on 1 or more child documents. - Relate copied items to original - - - Edit your notification for %0% - - - - - - - - - - - - - - - -
- - - - - -
- -
- -
-
- - - - - - -
-
-
- - - - -
- - - - -
-

- Hi %0%, -

-

- This is an automated mail to inform you that the task '%1%' has been performed on the page '%2%' by the user '%3%' -

- - - - - - -
- -
- EDIT
-
-

-

Update summary:

- - %6% -
-

-

- Have a nice day!

- Cheers from the Umbraco robot -

-
-
-


-
-
- - - ]]> -
- [%0%] Notification about %1% performed on %2% - Notifications - - - - - button and locating the package. Umbraco packages usually have a ".umb" or ".zip" extension. - ]]> - - Drop to upload - or click here to choose package file - Upload package - Install a local package by selecting it from your machine. Only install packages from sources you know and trust - Upload another package - Cancel and upload another package - License - I accept - terms of use - Install package - Finish - Installed packages - You don’t have any packages installed - 'Packages' icon in the top right of your screen]]> - Search for packages - Results for - We couldn’t find anything for - Please try searching for another package or browse through the categories - Popular - New releases - has - karma points - Information - Owner - Contributors - Created - Current version - .NET version - Downloads - Likes - Compatibility - This package is compatible with the following versions of Umbraco, as reported by community members. Full compatability cannot be gauranteed for versions reported below 100% - External sources - Author - Demonstration - Documentation - Package meta data - Package name - Package doesn't contain any items - -
- You can safely remove this from the system by clicking "uninstall package" below.]]> -
- No upgrades available - Package options - Package readme - Package repository - Confirm package uninstall - Package was uninstalled - The package was successfully uninstalled - Uninstall package - - - Notice: any documents, media etc depending on the items you remove, will stop working, and could lead to system instability, - so uninstall with caution. If in doubt, contact the package author.]]> - - Download update from the repository - Upgrade package - Upgrade instructions - There's an upgrade available for this package. You can download it directly from the Umbraco package repository. - Package version - Package version history - View package website - Package already installed - This package cannot be installed, it requires a minimum Umbraco version of - Uninstalling... - Downloading... - Importing... - Installing... - Restarting, please wait... - All done, your browser will now refresh, please wait... - Please click 'Finish' to complete installation and reload the page. - Uploading package... - - - Paste with full formatting (Not recommended) - The text you're trying to paste contains special characters or formatting. This could be caused by copying text from Microsoft Word. Umbraco can remove special characters or formatting automatically, so the pasted content will be more suitable for the web. - Paste as raw text without any formatting at all - Paste, but remove formatting (Recommended) - - - Role based protection - using Umbraco's member groups.]]> - You need to create a membergroup before you can use role-based authentication - Error Page - Used when people are logged on, but do not have access - Choose how to restrict access to this page - %0% is now protected - Protection removed from %0% - Login Page - Choose the page that contains the login form - Remove Protection - Select the pages that contain login form and error messages - Pick the roles who have access to this page - Set the login and password for this page - Single user protection - If you just want to setup simple protection using a single login and password - - - - - - - - - - - - - - - - - - Include unpublished subpages - Publishing in progress - please wait... - %0% out of %1% pages have been published... - %0% has been published - %0% and subpages have been published - Publish %0% and all its subpages - - Publish to publish %0% and thereby making its content publicly available.

- You can publish this page and all its subpages by checking Include unpublished subpages below. - ]]> -
- - - You have not configured any approved colors - - - You have picked a content item currently deleted or in the recycle bin - You have picked content items currently deleted or in the recycle bin - - - You have picked a media item currently deleted or in the recycle bin - You have picked media items currently deleted or in the recycle bin - Deleted item - - - enter external link - choose internal page - Caption - Link - Open in new window - enter the display caption - Enter the link - - - Reset - Define crop - Give the crop an alias and its default width and height - Save crop - Add new crop - - - Current version - Red text will not be shown in the selected version. , green means added]]> - Document has been rolled back - This displays the selected version as HTML, if you wish to see the difference between 2 versions at the same time, use the diff view - Rollback to - Select version - View - - - Edit script file - - - Concierge - Content - Courier - Developer - Umbraco Configuration Wizard - Media - Members - Newsletters - Settings - Statistics - Translation - Users - Help - Forms - Analytics - - - go to - Help topics for - Video chapters for - The best Umbraco video tutorials - - - Default template - Dictionary Key - To import a document type, find the ".udt" file on your computer by clicking the "Browse" button and click "Import" (you'll be asked for confirmation on the next screen) - New Tab Title - Node type - Type - Stylesheet - Script - Stylesheet property - Tab - Tab Title - Tabs - Master Content Type enabled - This Content Type uses - as a Master Content Type. Tabs from Master Content Types are not shown and can only be edited on the Master Content Type itself - No properties defined on this tab. Click on the "add a new property" link at the top to create a new property. - Master Document Type - Create matching template - Add icon - - - Sort order - Creation date - Sorting complete. - Drag the different items up or down below to set how they should be arranged. Or click the column headers to sort the entire collection of items - - - - Validation - Validation errors must be fixed before the item can be saved - Failed - Saved - Insufficient user permissions, could not complete the operation - Cancelled - Operation was cancelled by a 3rd party add-in - Publishing was cancelled by a 3rd party add-in - Property type already exists - Property type created - DataType: %1%]]> - Propertytype deleted - Document Type saved - Tab created - Tab deleted - Tab with id: %0% deleted - Stylesheet not saved - Stylesheet saved - Stylesheet saved without any errors - Datatype saved - Dictionary item saved - Publishing failed because the parent page isn't published - Content published - and visible on the website - Content saved - Remember to publish to make changes visible - Sent For Approval - Changes have been sent for approval - Media saved - Media saved without any errors - Member saved - Stylesheet Property Saved - Stylesheet saved - Template saved - Error saving user (check log) - User Saved - User type saved - User group saved - File not saved - file could not be saved. Please check file permissions - File saved - File saved without any errors - Language saved - Media Type saved - Member Type saved - Python script not saved - Python script could not be saved due to error - Python script saved - No errors in python script - Template not saved - Please make sure that you do not have 2 templates with the same alias - Template saved - Template saved without any errors! - Content unpublished - Content variation %0% unpublished - Partial view saved - Partial view saved without any errors! - Partial view not saved - An error occurred saving the file. - Permissions saved for - Script view saved - Script view saved without any errors! - Script view not saved - An error occurred saving the file. - An error occurred saving the file. - Deleted %0% user groups - %0% was deleted - Enabled %0% users - An error occurred while enabling the users - Disabled %0% users - An error occurred while disabling the users - %0% is now enabled - An error occurred while enabling the user - %0% is now disabled - An error occurred while disabling the user - User groups have been set - Deleted %0% user groups - %0% was deleted - Unlocked %0% users - An error occurred while unlocking the users - %0% is now unlocked - An error occurred while unlocking the user - Member was exported to file - An error occurred while exporting the member - Cannot publish the document since the required '%0%' is not published - Validation failed for language '%0%' - Unexpected validation failed for language '%0%' - - - Uses CSS syntax ex: h1, .redHeader, .blueTex - Edit stylesheet - Edit stylesheet property - Name to identify the style property in the rich text editor - Preview - Styles - - - Edit template - - Sections - Insert content area - Insert content area placeholder - - Insert - Choose what to insert into your template - - Dictionary item - A dictionary item is a placeholder for a translatable piece of text, which makes it easy to create designs for multilingual websites. - - Macro - - A Macro is a configurable component which is great for - reusable parts of your design, where you need the option to provide parameters, - such as galleries, forms and lists. - - - Value - Displays the value of a named field from the current page, with options to modify the value or fallback to alternative values. - - Partial view - - A partial view is a separate template file which can be rendered inside another - template, it's great for reusing markup or for separating complex templates into separate files. - - - Master template - No master template - No master - - Render child template - - @RenderBody() placeholder. - ]]> - - - - Define a named section - - @section { ... }. This can be rendered in a - specific area of the parent of this template, by using @RenderSection. - ]]> - - - Render a named section - - @RenderSection(name) placeholder. - This renders an area of a child template which is wrapped in a corresponding @section [name]{ ... } definition. - ]]> - - - Section Name - Section is mandatory - - If mandatory, the child template must contain a @section definition, otherwise an error is shown. - - - - Query builder - Build a query - items returned, in - - I want - all content - content of type "%0%" - from - my website - where - and - - is - is not - before - before (including selected date) - after - after (including selected date) - equals - does not equal - contains - does not contain - greater than - greater than or equal to - less than - less than or equal to - - Id - Name - Created Date - Last Updated Date - - order by - ascending - descending - - Template - - - Rich Text Editor - Image - Macro - Embed - Headline - Quote - Choose type of content - Choose a layout - Add a row - Add content - Drop content - Settings applied - - This content is not allowed here - This content is allowed here - - Click to embed - Click to insert image - Image caption... - Write here... - - Grid Layouts - Layouts are the overall work area for the grid editor, usually you only need one or two different layouts - Add Grid Layout - Adjust the layout by setting column widths and adding additional sections - Row configurations - Rows are predefined cells arranged horizontally - Add row configuration - Adjust the row by setting cell widths and adding additional cells - - Columns - Total combined number of columns in the grid layout - - Settings - Configure what settings editors can change - - - Styles - Configure what styling editors can change - - Settings will only save if the entered json configuration is valid - - Allow all editors - Allow all row configurations - Maximum items - Leave blank or set to 0 for unlimited - Set as default - Choose extra - Choose default - are added - - - - Compositions - You have not added any tabs - Add new tab - Add another tab - Inherited from - Add property - Required label - - Enable list view - Configures the content item to show a sortable and searchable list of its children, the children will not be shown in the tree - - Allowed Templates - Choose which templates editors are allowed to use on content of this type - Allow as root - Allow editors to create content of this type in the root of the content tree - Yes - allow content of this type in the root - - Allowed child node types - Allow content of the specified types to be created underneath content of this type - - Choose child node - Inherit tabs and properties from an existing document type. New tabs will be added to the current document type or merged if a tab with an identical name exists. - This content type is used in a composition, and therefore cannot be composed itself. - There are no content types available to use as a composition. - - Available editors - Reuse - Editor settings - - Configuration - - Yes, delete - - was moved underneath - was copied underneath - Select the folder to move - Select the folder to copy - to in the tree structure below - - All Document types - All Documents - All media items - - using this document type will be deleted permanently, please confirm you want to delete these as well. - using this media type will be deleted permanently, please confirm you want to delete these as well. - using this member type will be deleted permanently, please confirm you want to delete these as well - - and all documents using this type - and all media items using this type - and all members using this type - - using this editor will get updated with the new settings - - Member can edit - Allow this property value to be edited by the member on their profile page - Is sensitive data - Hide this property value from content editors that don't have access to view sensitive information - Show on member profile - Allow this property value to be displayed on the member profile page - - tab has no sort order - - Where is this composition used? - This composition is currently used in the composition of the following content types: - - - - Add language - Mandatory - Properties on this language has to be filled out before the node can be published. - Default language - An Umbraco site can only have one default langugae set. - Switching default language may result in default content missing. - - - - Building models - this can take a bit of time, don't worry - Models generated - Models could not be generated - Models generation has failed, see exception in U log - - - - Add fallback field - Fallback field - Add default value - Default value - Fallback field - Default value - Casing - Encoding - Choose field - Convert line breaks - Yes, convert line breaks - Replaces line breaks with 'br' html tag - Custom Fields - Date only - Format and encoding - Format as date - Format the value as a date, or a date with time, according to the active culture - HTML encode - Will replace special characters by their HTML equivalent. - Will be inserted after the field value - Will be inserted before the field value - Lowercase - Modify output - None - Output sample - Insert after field - Insert before field - Recursive - Yes, make it recursive - Separator - Standard Fields - Uppercase - URL encode - Will format special characters in URLs - Will only be used when the field values above are empty - This field will only be used if the primary field is empty - Date and time - - - Tasks assigned to you - - assigned to you. To see a detailed view including comments, click on "Details" or just the page name. - You can also download the page as XML directly by clicking the "Download Xml" link.
- To close a translation task, please go to the Details view and click the "Close" button. - ]]> -
- close task - Translation details - Download all translation tasks as XML - Download XML - Download XML DTD - Fields - Include subpages - - - - [%0%] Translation task for %1% - No translator users found. Please create a translator user before you start sending content to translation - Tasks created by you - - created by you. To see a detailed view including comments, - click on "Details" or just the page name. You can also download the page as XML directly by clicking the "Download Xml" link. - To close a translation task, please go to the Details view and click the "Close" button. - ]]> - - The page '%0%' has been send to translation - Please select the language that the content should be translated into - Send the page '%0%' to translation - Assigned by - Task opened - Total words - Translate to - Translation completed. - You can preview the pages, you've just translated, by clicking below. If the original page is found, you will get a comparison of the 2 pages. - Translation failed, the XML file might be corrupt - Translation options - Translator - Upload translation XML - - - Content - Content Templates - Media - Cache Browser - Recycle Bin - Created packages - Data Types - Dictionary - Installed packages - Install skin - Install starter kit - Languages - Install local package - Macros - Media Types - Members - Member Groups - Member Roles - Member Types - Document Types - Relation Types - Packages - Packages - Partial Views - Partial View Macro Files - Python Files - Install from repository - Install Runway - Runway modules - Scripting Files - Scripts - Stylesheets - Templates - Analytics - Users - - - New update ready - %0% is ready, click here for download - No connection to server - Error checking for update. Please review trace-stack for further information - - - Access - Based on the assigned groups and start nodes, the user has access to the following nodes - Assign access - Administrator - Category field - User created - Change Your Password - Change photo - New password - hasn't been locked out - The password hasn't been changed - Confirm new password - You can change your password for accessing the Umbraco Back Office by filling out the form below and click the 'Change Password' button - Content Channel - Create another user - Create new users to give them access to Umbraco. When a new user is created a password will be generated that you can share with the user. - Description field - Disable User - Document Type - Editor - Excerpt field - Failed login attempts - Go to user profile - Add groups to assign access and permissions - Invite another user - Invite new users to give them access to Umbraco. An invite email will be sent to the user with information on how to log in to Umbraco. - Language - Set the language you will see in menus and dialogs - Last lockout date - Last login - Password last changed - Username - Media start node - Limit the media library to a specific start node - Media start nodes - Limit the media library to specific start nodes - Sections - Disable Umbraco Access - has not logged in yet - Old password - Password - Reset password - Your password has been changed! - Please confirm the new password - Enter your new password - Your new password cannot be blank! - Current password - Invalid current password - There was a difference between the new password and the confirmed password. Please try again! - The confirmed password doesn't match the new password! - Replace child node permissions - You are currently modifying permissions for the pages: - Select pages to modify their permissions - Remove photo - Default permissions - Granular permissions - Set permissions for specific nodes - Profile - Search all children - Add sections to give users access - Select user groups - No start node selected - No start nodes selected - Active - All - Disabled - Locked out - Invited - Content start node - Limit the content tree to a specific start node - Content start nodes - Limit the content tree to specific start nodes - Name (A-Z) - Name (Z-A) - Newest - Oldest - Last login - User last updated - has been created - The new user has successfully been created. To log in to Umbraco use the password below. - User management - Name - User permissions - User group permissions - User group - User groups - has been invited - An invitation has been sent to the new user with details about how to log in to Umbraco. - Hello there and welcome to Umbraco! In just 1 minute you’ll be good to go, we just need you to setup a password and add a picture for your avatar. - Upload a picture to make it easy for other users to recognize you. - Writer - Translator - Change - Your profile - Your recent history - Session expires in - Invite user - Create user - Send invite - Back to users - Umbraco: Invitation - - - - - - - - - - - - -
- - - - - -
- -
- -
-
- - - - - - -
-
-
- - - - -
- - - - -
-

- Hi %0%, -

-

- You have been invited by %1% to the Umbraco Back Office. -

-

- Message from %1%: -
- %2% -

- - - - - - -
- - - - - - -
- - Click this link to accept the invite - -
-
-

If you cannot click on the link, copy and paste this URL into your browser window:

- - - - -
- - %3% - -
-

-
-
-


-
-
- - ]]> -
- Invite - - - Validation - Validate as email - Validate as a number - Validate as a Url - ...or enter a custom validation - Field is mandatory - Enter a regular expression - You need to add at least - You can only have - items - items selected - Invalid date - Not a number - Invalid email - - - - Value is set to the recommended value: '%0%'. - Value was set to '%1%' for XPath '%2%' in configuration file '%3%'. - Expected value '%1%' for '%2%' in configuration file '%3%', but found '%0%'. - Found unexpected value '%0%' for '%2%' in configuration file '%3%'. - - - Custom errors are set to '%0%'. - Custom errors are currently set to '%0%'. It is recommended to set this to '%1%' before go live. - Custom errors successfully set to '%0%'. - - MacroErrors are set to '%0%'. - MacroErrors are set to '%0%' which will prevent some or all pages in your site from loading completely if there are any errors in macros. Rectifying this will set the value to '%1%'. - MacroErrors are now set to '%0%'. - - - Try Skip IIS Custom Errors is set to '%0%' and you're using IIS version '%1%'. - Try Skip IIS Custom Errors is currently '%0%'. It is recommended to set this to '%1%' for your IIS version (%2%). - Try Skip IIS Custom Errors successfully set to '%0%'. - - - File does not exist: '%0%'. - '%0%' in config file '%1%'.]]> - There was an error, check log for full error: %0%. - - Members - Total XML: %0%, Total: %1%, Total invalid: %2% - Media - Total XML: %0%, Total: %1%, Total invalid: %2% - Content - Total XML: %0%, Total published: %1%, Total invalid: %2% - - Your website's certificate is valid. - Certificate validation error: '%0%' - Your website's SSL certificate has expired. - Your website's SSL certificate is expiring in %0% days. - Error pinging the URL %0% - '%1%' - You are currently %0% viewing the site using the HTTPS scheme. - The appSetting 'umbracoUseSSL' is set to 'false' in your web.config file. Once you access this site using the HTTPS scheme, that should be set to 'true'. - The appSetting 'umbracoUseSSL' is set to '%0%' in your web.config file, your cookies are %1% marked as secure. - Could not update the 'umbracoUseSSL' setting in your web.config file. Error: %0% - - - Enable HTTPS - Sets umbracoSSL setting to true in the appSettings of the web.config file. - The appSetting 'umbracoUseSSL' is now set to 'true' in your web.config file, your cookies will be marked as secure. - - Fix - Cannot fix a check with a value comparison type of 'ShouldNotEqual'. - Cannot fix a check with a value comparison type of 'ShouldEqual' with a provided value. - Value to fix check not provided. - - Debug compilation mode is disabled. - Debug compilation mode is currently enabled. It is recommended to disable this setting before go live. - Debug compilation mode successfully disabled. - - Trace mode is disabled. - Trace mode is currently enabled. It is recommended to disable this setting before go live. - Trace mode successfully disabled. - - All folders have the correct permissions set. - - %0%.]]> - %0%. If they aren't being written to no action need be taken.]]> - - All files have the correct permissions set. - - %0%.]]> - %0%. If they aren't being written to no action need be taken.]]> - - X-Frame-Options used to control whether a site can be IFRAMEd by another was found.]]> - X-Frame-Options used to control whether a site can be IFRAMEd by another was not found.]]> - Set Header in Config - Adds a value to the httpProtocol/customHeaders section of web.config to prevent the site being IFRAMEd by other websites. - A setting to create a header preventing IFRAMEing of the site by other websites has been added to your web.config file. - Could not update web.config file. Error: %0% - - X-Content-Type-Options used to protect against MIME sniffing vulnerabilities was found.]]> - X-Content-Type-Options used to protect against MIME sniffing vulnerabilities was not found.]]> - Adds a value to the httpProtocol/customHeaders section of web.config to protect against MIME sniffing vulnerabilities. - A setting to create a header protecting against MIME sniffing vulnerabilities has been added to your web.config file. - - Strict-Transport-Security, also known as the HSTS-header, was found.]]> - Strict-Transport-Security was not found.]]> - Adds the header 'Strict-Transport-Security' with the value 'max-age=10886400; preload' to the httpProtocol/customHeaders section of web.config. Use this fix only if you will have your domains running with https for the next 18 weeks (minimum). - The HSTS header has been added to your web.config file. - - X-XSS-Protection was found.]]> - X-XSS-Protection was not found.]]> - Adds the header 'X-XSS-Protection' with the value '1; mode=block' to the httpProtocol/customHeaders section of web.config. - The X-XSS-Protection header has been added to your web.config file. - - - %0%.]]> - No headers revealing information about the website technology were found. - - In the Web.config file, system.net/mailsettings could not be found. - In the Web.config file system.net/mailsettings section, the host is not configured. - SMTP settings are configured correctly and the service is operating as expected. - The SMTP server configured with host '%0%' and port '%1%' could not be reached. Please check to ensure the SMTP settings in the Web.config file system.net/mailsettings are correct. - - %0%.]]> - %0%.]]> -

Results of the scheduled Umbraco Health Checks run on %0% at %1% are as follows:

%2%]]>
- Umbraco Health Check Status - - - Disable URL tracker - Enable URL tracker - Original URL - Redirected To - No redirects have been made - When a published page gets renamed or moved a redirect will automatically be made to the new page. - Remove - Are you sure you want to remove the redirect from '%0%' to '%1%'? - Redirect URL removed. - Error removing redirect URL. - Are you sure you want to disable the URL tracker? - URL tracker has now been disabled. - Error disabling the URL tracker, more information can be found in your log file. - URL tracker has now been enabled. - Error enabling the URL tracker, more information can be found in your log file. - - - No Dictionary items to choose from - - - characters left - -
+ + + + The Umbraco community + http://our.umbraco.org/documentation/Extending-Umbraco/Language-Files + + + Culture and Hostnames + Audit Trail + Browse Node + Change Document Type + Copy + Create + Export + Create Package + Create group + Delete + Disable + Empty recycle bin + Enable + Export Document Type + Import Document Type + Import Package + Edit in Canvas + Exit + Move + Notifications + Public access + Publish + Unpublish + Reload + Republish entire site + Rename + Restore + Set permissions for the page %0% + Choose where to move + In the tree structure below + Permissions + Rollback + Send To Publish + Send To Translation + Set group + Sort + Translate + Update + Set permissions + Unlock + Create Content Template + + + Content + Administration + Structure + Other + + + Allow access to assign culture and hostnames + Allow access to view a node's history log + Allow access to view a node + Allow access to change document type for a node + Allow access to copy a node + Allow access to create nodes + Allow access to delete nodes + Allow access to move a node + Allow access to set and change public access for a node + Allow access to publish a node + Allow access to change permissions for a node + Allow access to roll back a node to a previous state + Allow access to send a node for approval before publishing + Allow access to send a node for translation + Allow access to change the sort order for nodes + Allow access to translate a node + Allow access to save a node + Allow access to create a Content Template + + + Permission denied. + Add new Domain + remove + Invalid node. + Invalid domain format. + Domain has already been assigned. + Language + Domain + New domain '%0%' has been created + Domain '%0%' is deleted + Domain '%0%' has already been assigned + Domain '%0%' has been updated + Edit Current Domains + + + + Inherit + Culture + + or inherit culture from parent nodes. Will also apply
+ to the current node, unless a domain below applies too.]]> +
+ Domains + + + Viewing for + + + Clear selection + Select + Select current folder + Do something else + Bold + Cancel Paragraph Indent + Insert form field + Insert graphic headline + Edit Html + Indent Paragraph + Italic + Center + Justify Left + Justify Right + Insert Link + Insert local link (anchor) + Bullet List + Numeric List + Insert macro + Insert picture + Edit relations + Return to list + Save + Save and close + Publish + Publish… + Save and schedule + Save and send for approval + Save list view + Preview + Preview is disabled because there's no template assigned + Choose style + Show styles + Insert table + Generate models + Generate models and close + Save and generate models + Undo + Redo + + + To change the document type for the selected content, first select from the list of valid types for this location. + Then confirm and/or amend the mapping of properties from the current type to the new, and click Save. + The content has been re-published. + Current Property + Current type + The document type cannot be changed, as there are no alternatives valid for this location. An alternative will be valid if it is allowed under the parent of the selected content item and that all existing child content items are allowed to be created under it. + Document Type Changed + Map Properties + Map to Property + New Template + New Type + none + Content + Select New Document Type + The document type of the selected content has been successfully changed to [new type] and the following properties mapped: + to + Could not complete property mapping as one or more properties have more than one mapping defined. + Only alternate types valid for the current location are displayed. + + + Is Published + About this page + Alias + (how would you describe the picture over the phone) + Alternative Links + Click to edit this item + Created by + Original author + Updated by + Created + Date/time this document was created + Document Type + Editing + Remove at + This item has been changed after publication + This item is not published + Culture '%0%' for this item is not published + Culture '%0%' for this item is not available + Last published + There are no items to show + There are no items to show in the list. + No content has been added + No members have been added + Media Type + Link to media item(s) + Member Group + Role + Member Type + No changes have been made + No date chosen + Page title + This media item has no link + Properties + This document is published but is not visible because the parent '%0%' is unpublished + This document is published but is not in the cache + Could not get the url + This document is published but its url would collide with content %0% + Publish + Published + Published (pending changes) + Unpublished (pending changes) + Publication Status + Publish at + Unpublish at + Clear Date + Set date + Sortorder is updated + To sort the nodes, simply drag the nodes or click one of the column headers. You can select multiple nodes by holding the "shift" or "control" key while selecting + Statistics + Title (optional) + Alternative text (optional) + Type + Unpublish + Unpublished + Last edited + Date/time this document was edited + Remove file(s) + Link to document + Member of group(s) + Not a member of group(s) + Child items + Target + This translates to the following time on the server: + What does this mean?]]> + Are you sure you want to delete this item? + Property %0% uses editor %1% which is not supported by Nested Content. + Add another text box + Remove this text box + Content root + This value is hidden. If you need access to view this value please contact your website administrator. + This value is hidden. + What languages would you like to publish? + Published Languages. + Ready to Publish? + + + Create a new Content Template from '%0%' + Blank + Select a Content Template + Content Template created + A Content Template was created from '%0%' + Another Content Template with the same name already exists + A Content Template is pre-defined content that an editor can select to use as the basis for creating new content + + + Click to upload + Drop your files here... + Link to media + or click here to choose files + Only allowed file types are + Cannot upload this file, it does not have an approved file type + Max file size is + Media root + + + Create a new member + All Members + + + Where do you want to create the new %0% + Create an item under + Select the document type you want to make a content template for + Choose a type and a title + "document types".]]> + "media types".]]> + Document Type without a template + New folder + New data type + New javascript file + New empty partial view + New partial view macro + New partial view from snippet + New empty partial view macro + New partial view macro from snippet + New partial view macro (without macro) + + + Browse your website + - Hide + If Umbraco isn't opening, you might need to allow popups from this site + has opened in a new window + Restart + Visit + Welcome + + + Stay + Discard changes + You have unsaved changes + Are you sure you want to navigate away from this page? - you have unsaved changes + + + Done + + Deleted %0% item + Deleted %0% items + Deleted %0% out of %1% item + Deleted %0% out of %1% items + + Published %0% item + Published %0% items + Published %0% out of %1% item + Published %0% out of %1% items + + Unpublished %0% item + Unpublished %0% items + Unpublished %0% out of %1% item + Unpublished %0% out of %1% items + + Moved %0% item + Moved %0% items + Moved %0% out of %1% item + Moved %0% out of %1% items + + Copied %0% item + Copied %0% items + Copied %0% out of %1% item + Copied %0% out of %1% items + + + Link title + Link + Name + Close this window + Are you sure you want to delete + Are you sure you want to disable + Please check this box to confirm deletion of %0% item(s) + Are you sure? + Are you sure? + Cut + Edit Dictionary Item + Edit Language + Insert local link + Insert character + Insert graphic headline + Insert picture + Insert link + Click to add a Macro + Insert table + Last Edited + Link + Internal link: + When using local links, insert "#" in front of link + Open in new window? + Macro Settings + This macro does not contain any properties you can edit + Paste + Edit permissions for + Set permissions for + Set permissions for %0% for user group %1% + Select the users groups you want to set permissions for + The items in the recycle bin are now being deleted. Please do not close this window while this operation takes place + The recycle bin is now empty + When items are deleted from the recycle bin, they will be gone forever + regexlib.com's webservice is currently experiencing some problems, which we have no control over. We are very sorry for this inconvenience.]]> + Search for a regular expression to add validation to a form field. Example: 'email, 'zip-code' 'url' + Remove Macro + Required Field + Site is reindexed + The website cache has been refreshed. All publish content is now up to date. While all unpublished content is still unpublished + The website cache will be refreshed. All published content will be updated, while unpublished content will stay unpublished. + Number of columns + Number of rows + + Set a placeholder id by setting an ID on your placeholder you can inject content into this template from child templates, + by referring this ID using a <asp:content /> element.]]> + + + Select a placeholder id from the list below. You can only + choose Id's from the current template's master.]]> + + Click on the image to see full size + Pick item + View Cache Item + Create folder... + Relate to original + Include descendants + The friendliest community + Link to page + Opens the linked document in a new window or tab + Link to media + Link to file + Select content start node + Select media + Select icon + Select item + Select link + Select macro + Select content + Select media start node + Select member + Select member group + Select node + Select sections + Select users + No icons were found + There are no parameters for this macro + There are no macros available to insert + External login providers + Exception Details + Stacktrace + Inner Exception + Link your + Un-link your + account + Select editor + Select snippet + This will delete the node and all its languages. If you only want to delete one language go and unpublish it instead. + + + + %0%' below
You can add additional languages under the 'languages' in the menu on the left + ]]> +
+ Culture Name + Edit the key of the dictionary item. + + + + + + Enter your username + Enter your password + Confirm your password + Name the %0%... + Enter a name... + Enter an email... + Enter a username... + Label... + Enter a description... + Type to search... + Type to filter... + Type to add tags (press enter after each tag)... + Enter your email... + Enter a message... + Your username is usually your email + + + Allow at root + Only Content Types with this checked can be created at the root level of Content and Media trees + Allowed child node types + Document Type Compositions + Create + Delete tab + Description + New tab + Tab + Thumbnail + Enable list view + Configures the content item to show a sortable & searchable list of its children, the children will not be shown in the tree + Current list view + The active list view data type + Create custom list view + Remove custom list view + + + Renamed + Enter a new folder name here + %0% was renamed to %1% + + + Add prevalue + Database datatype + Property editor GUID + Property editor + Buttons + Enable advanced settings for + Enable context menu + Maximum default size of inserted images + Related stylesheets + Show label + Width and height + All property types & property data + using this data type will be deleted permanently, please confirm you want to delete these as well + Yes, delete + and all property types & property data using this data type + Select the folder to move + to in the tree structure below + was moved underneath + + + Your data has been saved, but before you can publish this page there are some errors you need to fix first: + The current membership provider does not support changing password (EnablePasswordRetrieval need to be true) + %0% already exists + There were errors: + There were errors: + The password should be a minimum of %0% characters long and contain at least %1% non-alpha numeric character(s) + %0% must be an integer + The %0% field in the %1% tab is mandatory + %0% is a mandatory field + %0% at %1% is not in a correct format + %0% is not in a correct format + + + Received an error from the server + The specified file type has been disallowed by the administrator + NOTE! Even though CodeMirror is enabled by configuration, it is disabled in Internet Explorer because it's not stable enough. + Please fill both alias and name on the new property type! + There is a problem with read/write access to a specific file or folder + Error loading Partial View script (file: %0%) + Error loading userControl '%0%' + Error loading customControl (Assembly: %0%, Type: '%1%') + Error loading MacroEngine script (file: %0%) + Please enter a title + Please choose a type + You're about to make the picture larger than the original size. Are you sure that you want to proceed? + Error in python script + The python script has not been saved, because it contained error(s) + Startnode deleted, please contact your administrator + Please mark content before changing style + No active styles available + Please place cursor at the left of the two cells you wish to merge + You cannot split a cell that hasn't been merged. + There is a configuration error with the data type used for this property, please check the data type + + + Options + About + Action + Actions + Add + Alias + All + Are you sure? + Back + Border + by + Cancel + Cell margin + Choose + Close + Close Window + Comment + Confirm + Constrain + Constrain proportions + Continue + Copy + Create + Database + Date + Default + Delete + Deleted + Deleting... + Design + Dictionary + Dimensions + Down + Download + Edit + Edited + Elements + Email + Error + Find + First + General + Groups + Height + Help + Hide + History + Icon + Import + Info + Inner margin + Insert + Install + Invalid + Justify + Label + Language + Last + Layout + Links + Loading + Locked + Login + Log off + Logout + Macro + Mandatory + Message + Move + More + Name + New + Next + No + of + Off + OK + Open + On + or + Order by + Password + Path + Placeholder ID + One moment please... + Previous + Properties + Email to receive form data + Recycle Bin + Your recycle bin is empty + Remaining + Remove + Rename + Renew + Required + Retrieve + Retry + Permissions + Scheduled Publishing + Search + Sorry, we can not find what you are looking for + No items have been added + Server + Settings + Show + Show page on Send + Size + Sort + Status + Submit + Type + Type to search... + Up + Update + Upgrade + Upload + Url + User + Username + Value + View + Welcome... + Width + Yes + Folder + Search results + Reorder + I am done reordering + Preview + Change password + to + List view + Saving... + current + Embed + Retrieve + selected + + + Black + Green + Yellow + Orange + Blue + Blue Grey + Grey + Brown + Light Blue + Cyan + Light Green + Lime + Amber + Deep Orange + Red + Pink + Purple + Deep Purple + Indigo + + + Add tab + Add property + Add editor + Add template + Add child node + Add child + + Edit data type + + Navigate sections + + Shortcuts + show shortcuts + + Toggle list view + Toggle allow as root + + Comment/Uncomment lines + Remove line + Copy Lines Up + Copy Lines Down + Move Lines Up + Move Lines Down + + General + Editor + + + Background color + Bold + Text color + Font + Text + + + Page + + + The installer cannot connect to the database. + Could not save the web.config file. Please modify the connection string manually. + Your database has been found and is identified as + Database configuration + + install button to install the Umbraco %0% database + ]]> + + Next to proceed.]]> + + Database not found! Please check that the information in the "connection string" of the "web.config" file is correct.

+

To proceed, please edit the "web.config" file (using Visual Studio or your favourite text editor), scroll to the bottom, add the connection string for your database in the key named "UmbracoDbDSN" and save the file.

+

+ Click the retry button when + done.
+ More information on editing web.config here.

]]> +
+ + + Please contact your ISP if necessary. + If you're installing on a local machine or server you might need information from your system administrator.]]> + + + + Press the upgrade button to upgrade your database to Umbraco %0%

+

+ Don't worry - no content will be deleted and everything will continue working afterwards! +

+ ]]> +
+ + Press Next to + proceed. ]]> + + next to continue the configuration wizard]]> + The Default users' password needs to be changed!]]> + The Default user has been disabled or has no access to Umbraco!

No further actions needs to be taken. Click Next to proceed.]]> + The Default user's password has been successfully changed since the installation!

No further actions needs to be taken. Click Next to proceed.]]> + The password is changed! + Get a great start, watch our introduction videos + By clicking the next button (or modifying the umbracoConfigurationStatus in web.config), you accept the license for this software as specified in the box below. Notice that this Umbraco distribution consists of two different licenses, the open source MIT license for the framework and the Umbraco freeware license that covers the UI. + Not installed yet. + Affected files and folders + More information on setting up permissions for Umbraco here + You need to grant ASP.NET modify permissions to the following files/folders + + Your permission settings are almost perfect!

+ You can run Umbraco without problems, but you will not be able to install packages which are recommended to take full advantage of Umbraco.]]> +
+ How to Resolve + Click here to read the text version + video tutorial on setting up folder permissions for Umbraco or read the text version.]]> + + Your permission settings might be an issue! +

+ You can run Umbraco without problems, but you will not be able to create folders or install packages which are recommended to take full advantage of Umbraco.]]> +
+ + Your permission settings are not ready for Umbraco! +

+ In order to run Umbraco, you'll need to update your permission settings.]]> +
+ + Your permission settings are perfect!

+ You are ready to run Umbraco and install packages!]]> +
+ Resolving folder issue + Follow this link for more information on problems with ASP.NET and creating folders + Setting up folder permissions + + + + I want to start from scratch + + learn how) + You can still choose to install Runway later on. Please go to the Developer section and choose Packages. + ]]> + + You've just set up a clean Umbraco platform. What do you want to do next? + Runway is installed + + + This is our list of recommended modules, check off the ones you would like to install, or view the full list of modules + ]]> + + Only recommended for experienced users + I want to start with a simple website + + + "Runway" is a simple website providing some basic document types and templates. The installer can set up Runway for you automatically, + but you can easily edit, extend or remove it. It's not necessary and you can perfectly use Umbraco without it. However, + Runway offers an easy foundation based on best practices to get you started faster than ever. + If you choose to install Runway, you can optionally select basic building blocks called Runway Modules to enhance your Runway pages. +

+ + Included with Runway: Home page, Getting Started page, Installing Modules page.
+ Optional Modules: Top Navigation, Sitemap, Contact, Gallery. +
+ ]]> +
+ What is Runway + Step 1/5 Accept license + Step 2/5: Database configuration + Step 3/5: Validating File Permissions + Step 4/5: Check Umbraco security + Step 5/5: Umbraco is ready to get you started + Thank you for choosing Umbraco + + Browse your new site +You installed Runway, so why not see how your new website looks.]]> + + + Further help and information +Get help from our award winning community, browse the documentation or watch some free videos on how to build a simple site, how to use packages and a quick guide to the Umbraco terminology]]> + + Umbraco %0% is installed and ready for use + + /web.config file and update the AppSetting key UmbracoConfigurationStatus in the bottom to the value of '%0%'.]]> + + + started instantly by clicking the "Launch Umbraco" button below.
If you are new to Umbraco, +you can find plenty of resources on our getting started pages.]]> +
+ + Launch Umbraco +To manage your website, simply open the Umbraco back office and start adding content, updating the templates and stylesheets or add new functionality]]> + + Connection to database failed. + Umbraco Version 3 + Umbraco Version 4 + Watch + + Umbraco %0% for a fresh install or upgrading from version 3.0. +

+ Press "next" to start the wizard.]]> +
+ + + Culture Code + Culture Name + + + You've been idle and logout will automatically occur in + Renew now to save your work + + + Happy super Sunday + Happy manic Monday + Happy tubular Tuesday + Happy wonderful Wednesday + Happy thunderous Thursday + Happy funky Friday + Happy Caturday + Log in below + Sign in with + Session timed out + © 2001 - %0%
Umbraco.com

]]>
+ Forgotten password? + An email will be sent to the address specified with a link to reset your password + An email with password reset instructions will be sent to the specified address if it matched our records + Return to login form + Please provide a new password + Your Password has been updated + The link you have clicked on is invalid or has expired + Umbraco: Reset Password + + + + + + + + + + + + +
+ + + + + +
+ +
+ +
+
+ + + + + + +
+
+
+ + + + +
+ + + + +
+

+ Password reset requested +

+

+ Your username to login to the Umbraco back-office is: %0% +

+

+ + + + + + +
+ + Click this link to reset your password + +
+

+

If you cannot click on the link, copy and paste this URL into your browser window:

+ + + + +
+ + %1% + +
+

+
+
+


+
+
+ + + ]]> +
+ + + Dashboard + Sections + Content + + + Choose page above... + %0% has been copied to %1% + Select where the document %0% should be copied to below + %0% has been moved to %1% + Select where the document %0% should be moved to below + has been selected as the root of your new content, click 'ok' below. + No node selected yet, please select a node in the list above before clicking 'ok' + The current node is not allowed under the chosen node because of its type + The current node cannot be moved to one of its subpages + The current node cannot exist at the root + The action isn't allowed since you have insufficient permissions on 1 or more child documents. + Relate copied items to original + + + Edit your notification for %0% + + + + + + + + + + + + + + + +
+ + + + + +
+ +
+ +
+
+ + + + + + +
+
+
+ + + + +
+ + + + +
+

+ Hi %0%, +

+

+ This is an automated mail to inform you that the task '%1%' has been performed on the page '%2%' by the user '%3%' +

+ + + + + + +
+ +
+ EDIT
+
+

+

Update summary:

+ + %6% +
+

+

+ Have a nice day!

+ Cheers from the Umbraco robot +

+
+
+


+
+
+ + + ]]> +
+ [%0%] Notification about %1% performed on %2% + Notifications + + + + + button and locating the package. Umbraco packages usually have a ".umb" or ".zip" extension. + ]]> + + Drop to upload + or click here to choose package file + Upload package + Install a local package by selecting it from your machine. Only install packages from sources you know and trust + Upload another package + Cancel and upload another package + License + I accept + terms of use + Install package + Finish + Installed packages + You don’t have any packages installed + 'Packages' icon in the top right of your screen]]> + Search for packages + Results for + We couldn’t find anything for + Please try searching for another package or browse through the categories + Popular + New releases + has + karma points + Information + Owner + Contributors + Created + Current version + .NET version + Downloads + Likes + Compatibility + This package is compatible with the following versions of Umbraco, as reported by community members. Full compatability cannot be gauranteed for versions reported below 100% + External sources + Author + Demonstration + Documentation + Package meta data + Package name + Package doesn't contain any items + +
+ You can safely remove this from the system by clicking "uninstall package" below.]]> +
+ No upgrades available + Package options + Package readme + Package repository + Confirm package uninstall + Package was uninstalled + The package was successfully uninstalled + Uninstall package + + + Notice: any documents, media etc depending on the items you remove, will stop working, and could lead to system instability, + so uninstall with caution. If in doubt, contact the package author.]]> + + Download update from the repository + Upgrade package + Upgrade instructions + There's an upgrade available for this package. You can download it directly from the Umbraco package repository. + Package version + Package version history + View package website + Package already installed + This package cannot be installed, it requires a minimum Umbraco version of + Uninstalling... + Downloading... + Importing... + Installing... + Restarting, please wait... + All done, your browser will now refresh, please wait... + Please click 'Finish' to complete installation and reload the page. + Uploading package... + + + Paste with full formatting (Not recommended) + The text you're trying to paste contains special characters or formatting. This could be caused by copying text from Microsoft Word. Umbraco can remove special characters or formatting automatically, so the pasted content will be more suitable for the web. + Paste as raw text without any formatting at all + Paste, but remove formatting (Recommended) + + + Role based protection + using Umbraco's member groups.]]> + You need to create a membergroup before you can use role-based authentication + Error Page + Used when people are logged on, but do not have access + Choose how to restrict access to this page + %0% is now protected + Protection removed from %0% + Login Page + Choose the page that contains the login form + Remove Protection + Select the pages that contain login form and error messages + Pick the roles who have access to this page + Set the login and password for this page + Single user protection + If you just want to setup simple protection using a single login and password + + + + + + + + + + + + + + + + + + Include unpublished subpages + Publishing in progress - please wait... + %0% out of %1% pages have been published... + %0% has been published + %0% and subpages have been published + Publish %0% and all its subpages + + Publish to publish %0% and thereby making its content publicly available.

+ You can publish this page and all its subpages by checking Include unpublished subpages below. + ]]> +
+ + + You have not configured any approved colors + + + You have picked a content item currently deleted or in the recycle bin + You have picked content items currently deleted or in the recycle bin + + + You have picked a media item currently deleted or in the recycle bin + You have picked media items currently deleted or in the recycle bin + Deleted item + + + enter external link + choose internal page + Caption + Link + Open in new window + enter the display caption + Enter the link + + + Reset + Define crop + Give the crop an alias and its default width and height + Save crop + Add new crop + + + Current version + Red text will not be shown in the selected version. , green means added]]> + Document has been rolled back + This displays the selected version as HTML, if you wish to see the difference between 2 versions at the same time, use the diff view + Rollback to + Select version + View + + + Edit script file + + + Concierge + Content + Courier + Developer + Umbraco Configuration Wizard + Media + Members + Newsletters + Settings + Statistics + Translation + Users + Help + Forms + Analytics + + + go to + Help topics for + Video chapters for + The best Umbraco video tutorials + + + Default template + Dictionary Key + To import a document type, find the ".udt" file on your computer by clicking the "Browse" button and click "Import" (you'll be asked for confirmation on the next screen) + New Tab Title + Node type + Type + Stylesheet + Script + Stylesheet property + Tab + Tab Title + Tabs + Master Content Type enabled + This Content Type uses + as a Master Content Type. Tabs from Master Content Types are not shown and can only be edited on the Master Content Type itself + No properties defined on this tab. Click on the "add a new property" link at the top to create a new property. + Master Document Type + Create matching template + Add icon + + + Sort order + Creation date + Sorting complete. + Drag the different items up or down below to set how they should be arranged. Or click the column headers to sort the entire collection of items + + + + Validation + Validation errors must be fixed before the item can be saved + Failed + Saved + Insufficient user permissions, could not complete the operation + Cancelled + Operation was cancelled by a 3rd party add-in + Publishing was cancelled by a 3rd party add-in + Property type already exists + Property type created + DataType: %1%]]> + Propertytype deleted + Document Type saved + Tab created + Tab deleted + Tab with id: %0% deleted + Stylesheet not saved + Stylesheet saved + Stylesheet saved without any errors + Datatype saved + Dictionary item saved + Publishing failed because the parent page isn't published + Content published + and visible on the website + Content saved + Remember to publish to make changes visible + Sent For Approval + Changes have been sent for approval + Media saved + Media saved without any errors + Member saved + Stylesheet Property Saved + Stylesheet saved + Template saved + Error saving user (check log) + User Saved + User type saved + User group saved + File not saved + file could not be saved. Please check file permissions + File saved + File saved without any errors + Language saved + Media Type saved + Member Type saved + Python script not saved + Python script could not be saved due to error + Python script saved + No errors in python script + Template not saved + Please make sure that you do not have 2 templates with the same alias + Template saved + Template saved without any errors! + Content unpublished + Content variation %0% unpublished + Partial view saved + Partial view saved without any errors! + Partial view not saved + An error occurred saving the file. + Permissions saved for + Script view saved + Script view saved without any errors! + Script view not saved + An error occurred saving the file. + An error occurred saving the file. + Deleted %0% user groups + %0% was deleted + Enabled %0% users + An error occurred while enabling the users + Disabled %0% users + An error occurred while disabling the users + %0% is now enabled + An error occurred while enabling the user + %0% is now disabled + An error occurred while disabling the user + User groups have been set + Deleted %0% user groups + %0% was deleted + Unlocked %0% users + An error occurred while unlocking the users + %0% is now unlocked + An error occurred while unlocking the user + Member was exported to file + An error occurred while exporting the member + Cannot publish the document since the required '%0%' is not published + Validation failed for language '%0%' + Unexpected validation failed for language '%0%' + + + Uses CSS syntax ex: h1, .redHeader, .blueTex + Edit stylesheet + Edit stylesheet property + Name to identify the style property in the rich text editor + Preview + Styles + + + Edit template + + Sections + Insert content area + Insert content area placeholder + + Insert + Choose what to insert into your template + + Dictionary item + A dictionary item is a placeholder for a translatable piece of text, which makes it easy to create designs for multilingual websites. + + Macro + + A Macro is a configurable component which is great for + reusable parts of your design, where you need the option to provide parameters, + such as galleries, forms and lists. + + + Value + Displays the value of a named field from the current page, with options to modify the value or fallback to alternative values. + + Partial view + + A partial view is a separate template file which can be rendered inside another + template, it's great for reusing markup or for separating complex templates into separate files. + + + Master template + No master template + No master + + Render child template + + @RenderBody() placeholder. + ]]> + + + + Define a named section + + @section { ... }. This can be rendered in a + specific area of the parent of this template, by using @RenderSection. + ]]> + + + Render a named section + + @RenderSection(name) placeholder. + This renders an area of a child template which is wrapped in a corresponding @section [name]{ ... } definition. + ]]> + + + Section Name + Section is mandatory + + If mandatory, the child template must contain a @section definition, otherwise an error is shown. + + + + Query builder + Build a query + items returned, in + + I want + all content + content of type "%0%" + from + my website + where + and + + is + is not + before + before (including selected date) + after + after (including selected date) + equals + does not equal + contains + does not contain + greater than + greater than or equal to + less than + less than or equal to + + Id + Name + Created Date + Last Updated Date + + order by + ascending + descending + + Template + + + Rich Text Editor + Image + Macro + Embed + Headline + Quote + Choose type of content + Choose a layout + Add a row + Add content + Drop content + Settings applied + + This content is not allowed here + This content is allowed here + + Click to embed + Click to insert image + Image caption... + Write here... + + Grid Layouts + Layouts are the overall work area for the grid editor, usually you only need one or two different layouts + Add Grid Layout + Adjust the layout by setting column widths and adding additional sections + Row configurations + Rows are predefined cells arranged horizontally + Add row configuration + Adjust the row by setting cell widths and adding additional cells + + Columns + Total combined number of columns in the grid layout + + Settings + Configure what settings editors can change + + + Styles + Configure what styling editors can change + + Settings will only save if the entered json configuration is valid + + Allow all editors + Allow all row configurations + Maximum items + Leave blank or set to 0 for unlimited + Set as default + Choose extra + Choose default + are added + + + + Compositions + You have not added any tabs + Add new tab + Add another tab + Inherited from + Add property + Required label + + Enable list view + Configures the content item to show a sortable and searchable list of its children, the children will not be shown in the tree + + Allowed Templates + Choose which templates editors are allowed to use on content of this type + Allow as root + Allow editors to create content of this type in the root of the content tree + Yes - allow content of this type in the root + + Allowed child node types + Allow content of the specified types to be created underneath content of this type + + Choose child node + Inherit tabs and properties from an existing document type. New tabs will be added to the current document type or merged if a tab with an identical name exists. + This content type is used in a composition, and therefore cannot be composed itself. + There are no content types available to use as a composition. + + Available editors + Reuse + Editor settings + + Configuration + + Yes, delete + + was moved underneath + was copied underneath + Select the folder to move + Select the folder to copy + to in the tree structure below + + All Document types + All Documents + All media items + + using this document type will be deleted permanently, please confirm you want to delete these as well. + using this media type will be deleted permanently, please confirm you want to delete these as well. + using this member type will be deleted permanently, please confirm you want to delete these as well + + and all documents using this type + and all media items using this type + and all members using this type + + using this editor will get updated with the new settings + + Member can edit + Allow this property value to be edited by the member on their profile page + Is sensitive data + Hide this property value from content editors that don't have access to view sensitive information + Show on member profile + Allow this property value to be displayed on the member profile page + + tab has no sort order + + Where is this composition used? + This composition is currently used in the composition of the following content types: + + + + Add language + Mandatory + Properties on this language has to be filled out before the node can be published. + Default language + An Umbraco site can only have one default langugae set. + Switching default language may result in default content missing. + + + + Building models + this can take a bit of time, don't worry + Models generated + Models could not be generated + Models generation has failed, see exception in U log + + + + Add fallback field + Fallback field + Add default value + Default value + Fallback field + Default value + Casing + Encoding + Choose field + Convert line breaks + Yes, convert line breaks + Replaces line breaks with 'br' html tag + Custom Fields + Date only + Format and encoding + Format as date + Format the value as a date, or a date with time, according to the active culture + HTML encode + Will replace special characters by their HTML equivalent. + Will be inserted after the field value + Will be inserted before the field value + Lowercase + Modify output + None + Output sample + Insert after field + Insert before field + Recursive + Yes, make it recursive + Separator + Standard Fields + Uppercase + URL encode + Will format special characters in URLs + Will only be used when the field values above are empty + This field will only be used if the primary field is empty + Date and time + + + Tasks assigned to you + + assigned to you. To see a detailed view including comments, click on "Details" or just the page name. + You can also download the page as XML directly by clicking the "Download Xml" link.
+ To close a translation task, please go to the Details view and click the "Close" button. + ]]> +
+ close task + Translation details + Download all translation tasks as XML + Download XML + Download XML DTD + Fields + Include subpages + + + + [%0%] Translation task for %1% + No translator users found. Please create a translator user before you start sending content to translation + Tasks created by you + + created by you. To see a detailed view including comments, + click on "Details" or just the page name. You can also download the page as XML directly by clicking the "Download Xml" link. + To close a translation task, please go to the Details view and click the "Close" button. + ]]> + + The page '%0%' has been send to translation + Please select the language that the content should be translated into + Send the page '%0%' to translation + Assigned by + Task opened + Total words + Translate to + Translation completed. + You can preview the pages, you've just translated, by clicking below. If the original page is found, you will get a comparison of the 2 pages. + Translation failed, the XML file might be corrupt + Translation options + Translator + Upload translation XML + + + Content + Content Templates + Media + Cache Browser + Recycle Bin + Created packages + Data Types + Dictionary + Installed packages + Install skin + Install starter kit + Languages + Install local package + Macros + Media Types + Members + Member Groups + Member Roles + Member Types + Document Types + Relation Types + Packages + Packages + Partial Views + Partial View Macro Files + Python Files + Install from repository + Install Runway + Runway modules + Scripting Files + Scripts + Stylesheets + Templates + Analytics + Users + + + New update ready + %0% is ready, click here for download + No connection to server + Error checking for update. Please review trace-stack for further information + + + Access + Based on the assigned groups and start nodes, the user has access to the following nodes + Assign access + Administrator + Category field + User created + Change Your Password + Change photo + New password + hasn't been locked out + The password hasn't been changed + Confirm new password + You can change your password for accessing the Umbraco Back Office by filling out the form below and click the 'Change Password' button + Content Channel + Create another user + Create new users to give them access to Umbraco. When a new user is created a password will be generated that you can share with the user. + Description field + Disable User + Document Type + Editor + Excerpt field + Failed login attempts + Go to user profile + Add groups to assign access and permissions + Invite another user + Invite new users to give them access to Umbraco. An invite email will be sent to the user with information on how to log in to Umbraco. + Language + Set the language you will see in menus and dialogs + Last lockout date + Last login + Password last changed + Username + Media start node + Limit the media library to a specific start node + Media start nodes + Limit the media library to specific start nodes + Sections + Disable Umbraco Access + has not logged in yet + Old password + Password + Reset password + Your password has been changed! + Please confirm the new password + Enter your new password + Your new password cannot be blank! + Current password + Invalid current password + There was a difference between the new password and the confirmed password. Please try again! + The confirmed password doesn't match the new password! + Replace child node permissions + You are currently modifying permissions for the pages: + Select pages to modify their permissions + Remove photo + Default permissions + Granular permissions + Set permissions for specific nodes + Profile + Search all children + Add sections to give users access + Select user groups + No start node selected + No start nodes selected + Active + All + Disabled + Locked out + Invited + Content start node + Limit the content tree to a specific start node + Content start nodes + Limit the content tree to specific start nodes + Name (A-Z) + Name (Z-A) + Newest + Oldest + Last login + User last updated + has been created + The new user has successfully been created. To log in to Umbraco use the password below. + User management + Name + User permissions + User group permissions + User group + User groups + has been invited + An invitation has been sent to the new user with details about how to log in to Umbraco. + Hello there and welcome to Umbraco! In just 1 minute you’ll be good to go, we just need you to setup a password and add a picture for your avatar. + Upload a picture to make it easy for other users to recognize you. + Writer + Translator + Change + Your profile + Your recent history + Session expires in + Invite user + Create user + Send invite + Back to users + Umbraco: Invitation + + + + + + + + + + + + +
+ + + + + +
+ +
+ +
+
+ + + + + + +
+
+
+ + + + +
+ + + + +
+

+ Hi %0%, +

+

+ You have been invited by %1% to the Umbraco Back Office. +

+

+ Message from %1%: +
+ %2% +

+ + + + + + +
+ + + + + + +
+ + Click this link to accept the invite + +
+
+

If you cannot click on the link, copy and paste this URL into your browser window:

+ + + + +
+ + %3% + +
+

+
+
+


+
+
+ + ]]> +
+ Invite + + + Validation + Validate as email + Validate as a number + Validate as a Url + ...or enter a custom validation + Field is mandatory + Enter a regular expression + You need to add at least + You can only have + items + items selected + Invalid date + Not a number + Invalid email + + + + Value is set to the recommended value: '%0%'. + Value was set to '%1%' for XPath '%2%' in configuration file '%3%'. + Expected value '%1%' for '%2%' in configuration file '%3%', but found '%0%'. + Found unexpected value '%0%' for '%2%' in configuration file '%3%'. + + + Custom errors are set to '%0%'. + Custom errors are currently set to '%0%'. It is recommended to set this to '%1%' before go live. + Custom errors successfully set to '%0%'. + + MacroErrors are set to '%0%'. + MacroErrors are set to '%0%' which will prevent some or all pages in your site from loading completely if there are any errors in macros. Rectifying this will set the value to '%1%'. + MacroErrors are now set to '%0%'. + + + Try Skip IIS Custom Errors is set to '%0%' and you're using IIS version '%1%'. + Try Skip IIS Custom Errors is currently '%0%'. It is recommended to set this to '%1%' for your IIS version (%2%). + Try Skip IIS Custom Errors successfully set to '%0%'. + + + File does not exist: '%0%'. + '%0%' in config file '%1%'.]]> + There was an error, check log for full error: %0%. + + Members - Total XML: %0%, Total: %1%, Total invalid: %2% + Media - Total XML: %0%, Total: %1%, Total invalid: %2% + Content - Total XML: %0%, Total published: %1%, Total invalid: %2% + + Your website's certificate is valid. + Certificate validation error: '%0%' + Your website's SSL certificate has expired. + Your website's SSL certificate is expiring in %0% days. + Error pinging the URL %0% - '%1%' + You are currently %0% viewing the site using the HTTPS scheme. + The appSetting 'umbracoUseSSL' is set to 'false' in your web.config file. Once you access this site using the HTTPS scheme, that should be set to 'true'. + The appSetting 'umbracoUseSSL' is set to '%0%' in your web.config file, your cookies are %1% marked as secure. + Could not update the 'umbracoUseSSL' setting in your web.config file. Error: %0% + + + Enable HTTPS + Sets umbracoSSL setting to true in the appSettings of the web.config file. + The appSetting 'umbracoUseSSL' is now set to 'true' in your web.config file, your cookies will be marked as secure. + + Fix + Cannot fix a check with a value comparison type of 'ShouldNotEqual'. + Cannot fix a check with a value comparison type of 'ShouldEqual' with a provided value. + Value to fix check not provided. + + Debug compilation mode is disabled. + Debug compilation mode is currently enabled. It is recommended to disable this setting before go live. + Debug compilation mode successfully disabled. + + Trace mode is disabled. + Trace mode is currently enabled. It is recommended to disable this setting before go live. + Trace mode successfully disabled. + + All folders have the correct permissions set. + + %0%.]]> + %0%. If they aren't being written to no action need be taken.]]> + + All files have the correct permissions set. + + %0%.]]> + %0%. If they aren't being written to no action need be taken.]]> + + X-Frame-Options used to control whether a site can be IFRAMEd by another was found.]]> + X-Frame-Options used to control whether a site can be IFRAMEd by another was not found.]]> + Set Header in Config + Adds a value to the httpProtocol/customHeaders section of web.config to prevent the site being IFRAMEd by other websites. + A setting to create a header preventing IFRAMEing of the site by other websites has been added to your web.config file. + Could not update web.config file. Error: %0% + + X-Content-Type-Options used to protect against MIME sniffing vulnerabilities was found.]]> + X-Content-Type-Options used to protect against MIME sniffing vulnerabilities was not found.]]> + Adds a value to the httpProtocol/customHeaders section of web.config to protect against MIME sniffing vulnerabilities. + A setting to create a header protecting against MIME sniffing vulnerabilities has been added to your web.config file. + + Strict-Transport-Security, also known as the HSTS-header, was found.]]> + Strict-Transport-Security was not found.]]> + Adds the header 'Strict-Transport-Security' with the value 'max-age=10886400; preload' to the httpProtocol/customHeaders section of web.config. Use this fix only if you will have your domains running with https for the next 18 weeks (minimum). + The HSTS header has been added to your web.config file. + + X-XSS-Protection was found.]]> + X-XSS-Protection was not found.]]> + Adds the header 'X-XSS-Protection' with the value '1; mode=block' to the httpProtocol/customHeaders section of web.config. + The X-XSS-Protection header has been added to your web.config file. + + + %0%.]]> + No headers revealing information about the website technology were found. + + In the Web.config file, system.net/mailsettings could not be found. + In the Web.config file system.net/mailsettings section, the host is not configured. + SMTP settings are configured correctly and the service is operating as expected. + The SMTP server configured with host '%0%' and port '%1%' could not be reached. Please check to ensure the SMTP settings in the Web.config file system.net/mailsettings are correct. + + %0%.]]> + %0%.]]> +

Results of the scheduled Umbraco Health Checks run on %0% at %1% are as follows:

%2%]]>
+ Umbraco Health Check Status + + + Disable URL tracker + Enable URL tracker + Original URL + Redirected To + No redirects have been made + When a published page gets renamed or moved a redirect will automatically be made to the new page. + Remove + Are you sure you want to remove the redirect from '%0%' to '%1%'? + Redirect URL removed. + Error removing redirect URL. + Are you sure you want to disable the URL tracker? + URL tracker has now been disabled. + Error disabling the URL tracker, more information can be found in your log file. + URL tracker has now been enabled. + Error enabling the URL tracker, more information can be found in your log file. + + + No Dictionary items to choose from + + + characters left + +
From 0d3ea107c5a786c598f007a1d77385dd28c7a56b Mon Sep 17 00:00:00 2001 From: Stephan Date: Wed, 4 Jul 2018 10:41:08 +0200 Subject: [PATCH 09/14] Deal with some FIXME --- .../ContentVariationExtensions.cs | 56 ++++++++++++++++++- src/Umbraco.Core/Models/Content.cs | 2 +- src/Umbraco.Core/Models/ContentVariation.cs | 27 --------- src/Umbraco.Core/Models/IContent.cs | 6 +- .../Implement/DocumentRepository.cs | 34 +++-------- .../WebServices/BulkPublishController.cs | 17 +++--- 6 files changed, 71 insertions(+), 71 deletions(-) diff --git a/src/Umbraco.Core/ContentVariationExtensions.cs b/src/Umbraco.Core/ContentVariationExtensions.cs index 03abbcbc9e..092de4d6d6 100644 --- a/src/Umbraco.Core/ContentVariationExtensions.cs +++ b/src/Umbraco.Core/ContentVariationExtensions.cs @@ -9,35 +9,87 @@ namespace Umbraco.Core ///
public static class ContentVariationExtensions { - // fixme document + /// + /// Determines whether the content type is invariant. + /// public static bool VariesByNothing(this IContentTypeBase contentType) => contentType.Variations.VariesByNothing(); + + /// + /// Determines whether the content type varies by culture. + /// + /// And then it could also vary by segment. public static bool VariesByCulture(this IContentTypeBase contentType) => contentType.Variations.VariesByCulture(); + + /// + /// Determines whether the content type varies by segment. + /// + /// And then it could also vary by culture. public static bool VariesBySegment(this IContentTypeBase contentType) => contentType.Variations.VariesBySegment(); + + /// + /// Determines whether the content type varies by culture and segment. + /// public static bool VariesByCultureAndSegment(this IContentTypeBase contentType) => contentType.Variations.VariesByCultureAndSegment(); + /// + /// Determines whether the property type is invariant. + /// public static bool VariesByNothing(this PropertyType propertyType) => propertyType.Variations.VariesByNothing(); + + /// + /// Determines whether the property type varies by culture. + /// + /// And then it could also vary by segment. public static bool VariesByCulture(this PropertyType propertyType) => propertyType.Variations.VariesByCulture(); + + /// + /// Determines whether the property type varies by segment. + /// + /// And then it could also vary by culture. public static bool VariesBySegment(this PropertyType propertyType) => propertyType.Variations.VariesBySegment(); + + /// + /// Determines whether the property type varies by culture and segment. + /// public static bool VariesByCultureAndSegment(this PropertyType propertyType) => propertyType.Variations.VariesByCultureAndSegment(); + /// + /// Determines whether the content type is invariant. + /// public static bool VariesByNothing(this PublishedContentType contentType) => contentType.Variations.VariesByNothing(); + + /// + /// Determines whether the content type varies by culture. + /// + /// And then it could also vary by segment. public static bool VariesByCulture(this PublishedContentType contentType) => contentType.Variations.VariesByCulture(); + + /// + /// Determines whether the content type varies by segment. + /// + /// And then it could also vary by culture. public static bool VariesBySegment(this PublishedContentType contentType) => contentType.Variations.VariesBySegment(); + + /// + /// Determines whether the content type varies by culture and segment. + /// public static bool VariesByCultureAndSegment(this PublishedContentType contentType) => contentType.Variations.VariesByCultureAndSegment(); /// - /// Determines whether a variation varies by nothing. + /// Determines whether a variation is invariant. /// public static bool VariesByNothing(this ContentVariation variation) => variation == ContentVariation.Nothing; /// /// Determines whether a variation varies by culture. /// + /// And then it could also vary by segment. public static bool VariesByCulture(this ContentVariation variation) => (variation & ContentVariation.Culture) > 0; /// /// Determines whether a variation varies by segment. /// + /// And then it could also vary by culture. public static bool VariesBySegment(this ContentVariation variation) => (variation & ContentVariation.Segment) > 0; /// diff --git a/src/Umbraco.Core/Models/Content.cs b/src/Umbraco.Core/Models/Content.cs index 47358d39be..10869c62da 100644 --- a/src/Umbraco.Core/Models/Content.cs +++ b/src/Umbraco.Core/Models/Content.cs @@ -320,7 +320,7 @@ namespace Umbraco.Core.Models // the values we want to publish should be valid if (ValidateProperties(culture).Any()) - return false; // fixme - should return an attempt with error results + return false; var alsoInvariant = false; if (culture == "*") // all cultures diff --git a/src/Umbraco.Core/Models/ContentVariation.cs b/src/Umbraco.Core/Models/ContentVariation.cs index 2759f2e075..486f0e54f2 100644 --- a/src/Umbraco.Core/Models/ContentVariation.cs +++ b/src/Umbraco.Core/Models/ContentVariation.cs @@ -33,32 +33,5 @@ namespace Umbraco.Core.Models /// Values vary by culture and segment. /// CultureAndSegment = Culture | Segment - - - // fixme - remove once we have a migration for DB values! - ///// - ///// Unknown. - ///// - //Unknown = 0, - - ///// - ///// Accepts values for the invariant culture and the neutral segment. - ///// - //InvariantNeutral = 1, - - ///// - ///// Accepts values for a specified culture and the neutral segment. - ///// - //CultureNeutral = 2, - - ///// - ///// Accepts values for the invariant culture and a specified segment. - ///// - //InvariantSegment = 4, - - ///// - ///// Accepts values for a specified culture and a specified segment. - ///// - //CultureSegment = 8 } } diff --git a/src/Umbraco.Core/Models/IContent.cs b/src/Umbraco.Core/Models/IContent.cs index 8b63b7b9ab..9e79a75e25 100644 --- a/src/Umbraco.Core/Models/IContent.cs +++ b/src/Umbraco.Core/Models/IContent.cs @@ -170,8 +170,6 @@ namespace Umbraco.Core.Models /// A value indicating whether the culture can be published. /// /// Fails if values cannot be published, e.g. if some values are not valid. - /// Sets the property values but not the published name for the specified culture, - /// thus not explicitely publishing the culture. fixme uhuh? /// Publishing must be finalized via the content service SavePublishing method. /// // fixme - should return an attempt with error results @@ -181,9 +179,7 @@ namespace Umbraco.Core.Models /// Registers a culture to be unpublished. ///
/// - /// Clears the property values but not the published name for the specified culture, - /// thus leaving the culture published. fixme wtf? - /// Publishing must be finalized via the content service SavePublishing method. + /// Unpublishing must be finalized via the content service SavePublishing method. /// void UnpublishCulture(string culture = "*"); } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs index 39a9e11a00..093723cea5 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs @@ -515,7 +515,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var deleteDocumentVariations = Sql().Delete().Where(x => x.NodeId == content.Id); Database.Execute(deleteDocumentVariations); - // fixme is we'd like to use the native NPoco InsertBulk here but it causes problems (not sure exaclty all scenarios) + // fixme NPoco InsertBulk issue? + // we should use the native NPoco InsertBulk here but it causes problems (not sure exaclty all scenarios) // but by using SQL Server and updating a variants name will cause: Unable to cast object of type // 'Umbraco.Core.Persistence.FaultHandling.RetryDbConnection' to type 'System.Data.SqlClient.SqlConnection'. // (same in PersistNewItem above) @@ -1104,13 +1105,6 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // ensure that that invariant name is unique EnsureInvariantNameIsUnique(content); - // now that we have an invariant name, which is unique, - // if publishing, ensure that we have an invariant publish name - // invariant content = must be there, else throw - then must follow the invariant name - // variant content = update with invariant name - // fixme wtf is this we never needed it, PublishName derives from Name when publishing! - //if (publishing) EnsureInvariantPublishName(content); - // and finally, // ensure that each culture has a unique node name // no published name = not published @@ -1147,24 +1141,6 @@ namespace Umbraco.Core.Persistence.Repositories.Implement content.Name = EnsureUniqueNodeName(content.ParentId, content.Name, content.Id); } - //private void EnsureInvariantPublishName(Content content) - //{ - // if (content.ContentType.VariesByCulture()) - // { - // // content varies by culture, reuse name as publish name - // content.UpdatePublishName(null, content.Name); - // } - // else - // { - // // content is invariant, and invariant content must have an explicit invariant name - // if (string.IsNullOrWhiteSpace(content.PublishName)) - // throw new InvalidOperationException("Cannot save content with an empty name."); - // // and then, must follow the name itself - // if (content.PublishName != content.Name) - // content.UpdatePublishName(null, content.Name); - // } - //} - protected override string EnsureUniqueNodeName(int parentId, string nodeName, int id = 0) { return EnsureUniqueNaming == false ? nodeName : base.EnsureUniqueNodeName(parentId, nodeName, id); @@ -1193,6 +1169,10 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (names.Count == 0) return; + // note: the code below means we are going to unique-ify every culture names, regardless + // of whether the name has changed (ie the culture has been updated) - some saving culture + // fr-FR could cause culture en-UK name to change - not sure that is clean + foreach(var (culture, name) in content.CultureNames) { var langId = LanguageRepository.GetIdByIsoCode(culture); @@ -1207,7 +1187,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // update the name, and the publish name if published content.SetCultureName(uniqueName, culture); - if (publishing && content.PublishNames.ContainsKey(culture)) // fixme but what about those cultures we are NOT publishing NOW?! they shouldn't change their name! + if (publishing && content.PublishNames.ContainsKey(culture)) content.SetPublishInfo(culture, uniqueName, DateTime.Now); } } diff --git a/src/Umbraco.Web/WebServices/BulkPublishController.cs b/src/Umbraco.Web/WebServices/BulkPublishController.cs index fbe0d3554f..e6810f2a78 100644 --- a/src/Umbraco.Web/WebServices/BulkPublishController.cs +++ b/src/Umbraco.Web/WebServices/BulkPublishController.cs @@ -1,5 +1,4 @@ -using System.Collections.Generic; -using System.Linq; +using System.Linq; using System.Text; using System.Web.Mvc; using Umbraco.Core; @@ -29,7 +28,7 @@ namespace Umbraco.Web.WebServices if (publishDescendants == false) { - content.PublishCulture(); // fixme variants? validation - when this returns null? + // fixme variants? var result = Services.ContentService.SaveAndPublish(content, userId: Security.CurrentUser.Id); return Json(new { @@ -40,12 +39,12 @@ namespace Umbraco.Web.WebServices else { // fixme variants? - var result = Services.ContentService.SaveAndPublishBranch(content, includeUnpublished); + var result = Services.ContentService.SaveAndPublishBranch(content, includeUnpublished).ToArray(); return Json(new { success = result.All(x => x.Success), - message = GetMessageForStatuses(result.ToArray(), content) + message = GetMessageForStatuses(result, content) }); } } @@ -79,19 +78,19 @@ namespace Umbraco.Web.WebServices return Services.TextService.Localize("publish/nodePublish", new[] { status.Content.Name}); case PublishResultType.FailedPathNotPublished: return Services.TextService.Localize("publish/contentPublishedFailedByParent", - new [] { string.Format("{0} ({1})", status.Content.Name, status.Content.Id) }); + new [] { $"{status.Content.Name} ({status.Content.Id})" }); case PublishResultType.FailedHasExpired: case PublishResultType.FailedAwaitingRelease: case PublishResultType.FailedIsTrashed: return "Cannot publish document with a status of " + status.Result; case PublishResultType.FailedCancelledByEvent: return Services.TextService.Localize("publish/contentPublishedFailedByEvent", - new [] { string.Format("'{0}' ({1})", status.Content.Name, status.Content.Id) }); + new [] { $"'{status.Content.Name}' ({status.Content.Id})" }); case PublishResultType.FailedContentInvalid: return Services.TextService.Localize("publish/contentPublishedFailedInvalid", new []{ - string.Format("'{0}' ({1})", status.Content.Name, status.Content.Id), - string.Format("'{0}'", string.Join(", ", status.InvalidProperties.Select(x => x.Alias))) + $"'{status.Content.Name}' ({status.Content.Id})", + $"'{string.Join(", ", status.InvalidProperties.Select(x => x.Alias))}'" }); default: return status.Result.ToString(); From 0af18b23765bd33899e2b9d7bb6b7dd97c97004f Mon Sep 17 00:00:00 2001 From: Stephan Date: Thu, 5 Jul 2018 17:07:40 +0200 Subject: [PATCH 10/14] Fix publishing --- .../Services/Implement/ContentService.cs | 39 +++++++++++-------- .../Services/PublishResultType.cs | 21 ++++++++-- src/Umbraco.Web/Editors/ContentController.cs | 32 ++++++++------- 3 files changed, 59 insertions(+), 33 deletions(-) diff --git a/src/Umbraco.Core/Services/Implement/ContentService.cs b/src/Umbraco.Core/Services/Implement/ContentService.cs index 90619abf05..731f29ed4e 100644 --- a/src/Umbraco.Core/Services/Implement/ContentService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentService.cs @@ -1055,27 +1055,31 @@ namespace Umbraco.Core.Services.Implement if (content.PublishedState != PublishedState.Publishing && content.PublishedState != PublishedState.Unpublishing) ((Content) content).PublishedState = PublishedState.Publishing; + // state here is either Publishing or Unpublishing + var publishing = content.PublishedState == PublishedState.Publishing; + var unpublishing = content.PublishedState == PublishedState.Unpublishing; + using (var scope = ScopeProvider.CreateScope()) { // is the content going to end up published, or unpublished? - bool publishing, unpublishing; - if (content.ContentType.VariesByCulture()) + if (publishing && content.ContentType.VariesByCulture()) { var publishedCultures = content.PublishedCultures.ToList(); - var cannotBePublished= publishedCultures.Count == 0; // no published cultures = cannot be published + var cannotBePublished = publishedCultures.Count == 0; // no published cultures = cannot be published if (!cannotBePublished) { var mandatoryCultures = _languageRepository.GetMany().Where(x => x.Mandatory).Select(x => x.IsoCode); - cannotBePublished = mandatoryCultures.Any(x => !publishedCultures.Contains(x)); // missing mandatory culture = cannot be published + cannotBePublished = mandatoryCultures.Any(x => !publishedCultures.Contains(x, StringComparer.OrdinalIgnoreCase)); // missing mandatory culture = cannot be published + } + + if (cannotBePublished) + { + publishing = false; + unpublishing = content.Published; // if not published yet, nothing to do + + // we may end up in a state where we won't publish nor unpublish + // keep going, though, as we want to save anways } - unpublishing = content.Published && cannotBePublished; // if we cannot be published, and we are published, we unpublish - publishing = !cannotBePublished; // if we can be published, we publish - } - else - { - // invariant: we can publish, no culture problem, no need to unpublish - publishing = content.PublishedState == PublishedState.Publishing; - unpublishing = content.PublishedState == PublishedState.Unpublishing; } var isNew = !content.HasIdentity; @@ -1160,7 +1164,7 @@ namespace Umbraco.Core.Services.Implement // or, failed scope.Events.Dispatch(TreeChanged, this, new TreeChange(content, changeType).ToEventArgs()); scope.Complete(); // compete the save - return new PublishResult(PublishResultType.Failed, evtMsgs, content); // bah + return new PublishResult(PublishResultType.FailedToUnpublish, evtMsgs, content); // bah } if (publishing) // we have tried to publish @@ -1194,12 +1198,15 @@ namespace Umbraco.Core.Services.Implement return publishResult; } - // we haven't tried anything - assume that is bad (may need to reconsider the case of unpublishing - // a culture, and the culture or content item was already unpublished...) - bah + // both publishing and unpublishing are false + // this means that we wanted to publish, in a variant scenario, a document that + // was not published yet, and we could not, due to cultures issues + // + // raise event (we saved), report scope.Events.Dispatch(TreeChanged, this, new TreeChange(content, changeType).ToEventArgs()); scope.Complete(); // compete the save - return new PublishResult(PublishResultType.Failed, evtMsgs, content); + return new PublishResult(PublishResultType.FailedByCulture, evtMsgs, content); } } diff --git a/src/Umbraco.Core/Services/PublishResultType.cs b/src/Umbraco.Core/Services/PublishResultType.cs index b4bfe078b7..15b2f503c7 100644 --- a/src/Umbraco.Core/Services/PublishResultType.cs +++ b/src/Umbraco.Core/Services/PublishResultType.cs @@ -18,7 +18,7 @@ /// The item was already published. ///
SuccessAlready = 1, - + /// /// The operation failed. /// @@ -58,8 +58,23 @@ FailedContentInvalid = Failed | 6, /// - /// The document could not be published because it does not have published values. + /// Cannot republish a document that hasn't been published. /// - FailedNoPublishedValues = Failed | 7 + FailedNoPublishedValues = Failed | 7, // in ContentService.StrategyCanPublish - fixme weird + + /// + /// Some mandatory cultures are missing, or are not valid. + /// + FailedCannotPublish = Failed | 8, // in ContentController.PublishInternal - fixme // FailedByCulture? + + /// + /// Publishing changes triggered an unpublishing, due to missing mandatory cultures, and unpublishing failed. + /// + FailedToUnpublish = Failed | 9, // in ContentService.SavePublishing + + /// + /// Some mandatory cultures are missing. + /// + FailedByCulture = Failed | 10, // in ContentService.SavePublishing } } diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index 6c0313bd1f..0033a97bce 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -772,7 +772,7 @@ namespace Umbraco.Web.Editors { //can only save var saveResult = Services.ContentService.Save(contentItem.PersistedContent, Security.CurrentUser.Id); - publishStatus = new PublishResult(PublishResultType.Failed, null, contentItem.PersistedContent); + publishStatus = new PublishResult(PublishResultType.FailedCannotPublish, null, contentItem.PersistedContent); wasCancelled = saveResult.Result == OperationResultType.FailedCancelledByEvent; } } @@ -1150,7 +1150,7 @@ namespace Umbraco.Web.Editors display.AddWarningNotification( Services.TextService.Localize("publish"), Services.TextService.Localize("publish/contentPublishedFailedByParent", - new[] { string.Format("{0} ({1})", status.Content.Name, status.Content.Id) }).Trim()); + new[] { $"{status.Content.Name} ({status.Content.Id})" }).Trim()); break; case PublishResultType.FailedCancelledByEvent: AddCancelMessage(display, "publish", "speechBubbles/contentPublishedFailedByEvent"); @@ -1159,32 +1159,36 @@ namespace Umbraco.Web.Editors display.AddWarningNotification( Services.TextService.Localize("publish"), Services.TextService.Localize("publish/contentPublishedFailedAwaitingRelease", - new[] { string.Format("{0} ({1})", status.Content.Name, status.Content.Id) }).Trim()); + new[] { $"{status.Content.Name} ({status.Content.Id})" }).Trim()); break; case PublishResultType.FailedHasExpired: display.AddWarningNotification( Services.TextService.Localize("publish"), Services.TextService.Localize("publish/contentPublishedFailedExpired", - new[] - { - string.Format("{0} ({1})", status.Content.Name, status.Content.Id), - }).Trim()); + new[] { $"{status.Content.Name} ({status.Content.Id})", }).Trim()); break; case PublishResultType.FailedIsTrashed: - //TODO: We should add proper error messaging for this! + display.AddWarningNotification( + Services.TextService.Localize("publish"), + "publish/contentPublishedFailedIsTrashed"); // fixme properly localize! break; case PublishResultType.FailedContentInvalid: display.AddWarningNotification( Services.TextService.Localize("publish"), Services.TextService.Localize("publish/contentPublishedFailedInvalid", - new[] - { - string.Format("{0} ({1})", status.Content.Name, status.Content.Id), - string.Join(",", status.InvalidProperties.Select(x => x.Alias)) - }).Trim()); + new[] + { + $"{status.Content.Name} ({status.Content.Id})", + string.Join(",", status.InvalidProperties.Select(x => x.Alias)) + }).Trim()); + break; + case PublishResultType.FailedByCulture: + display.AddWarningNotification( + Services.TextService.Localize("publish"), + "publish/contentPublishedFailedByCulture"); // fixme properly localize! break; default: - throw new IndexOutOfRangeException(); + throw new IndexOutOfRangeException($"PublishedResultType \"{status.Result}\" was not expected."); } } From 75d685ef433f3193b3cb61ae9104d7e4dfdb5d4f Mon Sep 17 00:00:00 2001 From: Stephan Date: Thu, 5 Jul 2018 17:08:40 +0200 Subject: [PATCH 11/14] NuCache - fix parent published --- .../PublishedCache/NuCache/ContentNodeKit.cs | 4 ++-- .../PublishedCache/NuCache/ContentStore.cs | 14 +++++++++++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/ContentNodeKit.cs b/src/Umbraco.Web/PublishedCache/NuCache/ContentNodeKit.cs index f284d54cf1..5a47b99382 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/ContentNodeKit.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/ContentNodeKit.cs @@ -17,9 +17,9 @@ namespace Umbraco.Web.PublishedCache.NuCache public static ContentNodeKit Null { get; } = new ContentNodeKit { ContentTypeId = -1 }; - public void Build(PublishedContentType contentType, IPublishedSnapshotAccessor publishedSnapshotAccessor, IVariationContextAccessor variationContextAccessor) + public void Build(PublishedContentType contentType, IPublishedSnapshotAccessor publishedSnapshotAccessor, IVariationContextAccessor variationContextAccessor, bool canBePublished) { - Node.SetContentTypeAndData(contentType, DraftData, PublishedData, publishedSnapshotAccessor, variationContextAccessor); + Node.SetContentTypeAndData(contentType, DraftData, canBePublished ? PublishedData : null, publishedSnapshotAccessor, variationContextAccessor); } } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs index 303f4fc2ba..834594af9e 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs @@ -415,8 +415,11 @@ namespace Umbraco.Web.PublishedCache.NuCache if (_contentTypesById.TryGetValue(kit.ContentTypeId, out LinkedNode link) == false || link.Value == null) return false; + // check whether parent is published + var canBePublished = ParentPublishedLocked(kit); + // and use - kit.Build(link.Value, _publishedSnapshotAccessor, _variationContextAccessor); + kit.Build(link.Value, _publishedSnapshotAccessor, _variationContextAccessor, canBePublished); return true; } @@ -643,6 +646,15 @@ namespace Umbraco.Web.PublishedCache.NuCache return link?.Value != null; } + private bool ParentPublishedLocked(ContentNodeKit kit) + { + if (kit.Node.ParentContentId < 0) + return true; + var link = GetParentLink(kit.Node); + var node = link?.Value; + return node?.Published != null; + } + private void AddToParentLocked(ContentNode content) { // add to root content index, From e9a31437e11b043b6302bb42002ff56c858648d4 Mon Sep 17 00:00:00 2001 From: Stephan Date: Thu, 5 Jul 2018 17:30:52 +0200 Subject: [PATCH 12/14] Fix .editorconfig line endings --- .editorconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.editorconfig b/.editorconfig index d2f3002c12..80eabec233 100644 --- a/.editorconfig +++ b/.editorconfig @@ -8,7 +8,7 @@ root = true # Use 4 spaces as indentation [*] insert_final_newline = true -end_of_line = lf +end_of_line = crlf indent_style = space indent_size = 4 From fdfdd541e3389de9d58211d8da55603ea2f6d1c6 Mon Sep 17 00:00:00 2001 From: Stephan Date: Thu, 5 Jul 2018 17:14:11 +0200 Subject: [PATCH 13/14] Content Url and Audit (in progress) --- .../Services/Implement/ContentService.cs | 46 ++-- .../Services/UnpublishResultType.cs | 2 +- .../content/umb-content-node-info.html | 57 ++--- .../components/media/umb-media-node-info.html | 10 +- src/Umbraco.Web.UI/Umbraco/config/lang/en.xml | 2 + .../Umbraco/config/lang/en_us.xml | 2 + src/Umbraco.Web/Editors/ContentController.cs | 5 +- .../ContentEditing/ContentItemDisplay.cs | 3 +- .../Models/Mapping/ContentUrlResolver.cs | 6 +- src/Umbraco.Web/Routing/UrlInfo.cs | 50 ++++ .../Routing/UrlProviderExtensions.cs | 226 ++++++++++-------- src/Umbraco.Web/Umbraco.Web.csproj | 1 + 12 files changed, 250 insertions(+), 160 deletions(-) create mode 100644 src/Umbraco.Web/Routing/UrlInfo.cs diff --git a/src/Umbraco.Core/Services/Implement/ContentService.cs b/src/Umbraco.Core/Services/Implement/ContentService.cs index 731f29ed4e..e953a6073e 100644 --- a/src/Umbraco.Core/Services/Implement/ContentService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentService.cs @@ -891,7 +891,7 @@ namespace Umbraco.Core.Services.Implement } var changeType = TreeChangeTypes.RefreshNode; scope.Events.Dispatch(TreeChanged, this, new TreeChange(content, changeType).ToEventArgs()); - Audit(AuditType.Save, "Save Content performed by user", userId, content.Id); + Audit(AuditType.Save, "Saved by user", userId, content.Id); scope.Complete(); } @@ -931,7 +931,7 @@ namespace Umbraco.Core.Services.Implement scope.Events.Dispatch(Saved, this, saveEventArgs, "Saved"); } scope.Events.Dispatch(TreeChanged, this, treeChanges.ToEventArgs()); - Audit(AuditType.Save, "Bulk Save content performed by user", userId == -1 ? 0 : userId, Constants.System.Root); + Audit(AuditType.Save, "Bulk-saved by user", userId == -1 ? 0 : userId, Constants.System.Root); scope.Complete(); } @@ -988,6 +988,8 @@ namespace Umbraco.Core.Services.Implement { var evtMsgs = EventMessagesFactory.Get(); + culture = culture.NullOrWhiteSpaceAsNull(); + var publishedState = content.PublishedState; if (publishedState != PublishedState.Published && publishedState != PublishedState.Unpublished) throw new InvalidOperationException($"Cannot save-and-publish (un)publishing content, use the dedicated {nameof(SavePublishing)} method."); @@ -996,12 +998,12 @@ namespace Umbraco.Core.Services.Implement // cannot accept a specific culture for invariant content type (but '*' is ok) if (content.ContentType.VariesByCulture()) { - if (culture.IsNullOrWhiteSpace()) + if (culture == null) throw new NotSupportedException("Invariant culture is not supported by variant content types."); } else { - if (!culture.IsNullOrWhiteSpace() && culture != "*") + if (culture != null && culture != "*") throw new NotSupportedException($"Culture \"{culture}\" is not supported by invariant content types."); } @@ -1010,7 +1012,7 @@ namespace Umbraco.Core.Services.Implement return new UnpublishResult(UnpublishResultType.SuccessAlready, evtMsgs, content); // all cultures = unpublish whole - if (culture == "*") + if (culture == "*" || (!content.ContentType.VariesByCulture() && culture == null)) { ((Content) content).PublishedState = PublishedState.Unpublishing; } @@ -1031,9 +1033,21 @@ namespace Umbraco.Core.Services.Implement var saved = SavePublishing(content, userId); if (saved.Success) { - Audit(AuditType.UnPublish, $"Unpublish variation culture: \"{culture ?? string.Empty}\" performed by user", userId, content.Id); + UnpublishResultType result; + if (culture == "*" || culture == null) + { + Audit(AuditType.UnPublish, "Unpublished by user", userId, content.Id); + result = UnpublishResultType.Success; + } + else + { + Audit(AuditType.UnPublish, $"Culture \"{culture}\" unpublished by user", userId, content.Id); + if (!content.Published) + Audit(AuditType.UnPublish, $"Unpublished (culture \"{culture}\" is mandatory) by user", userId, content.Id); + result = content.Published ? UnpublishResultType.SuccessCulture : UnpublishResultType.SuccessMandatoryCulture; + } scope.Complete(); - return new UnpublishResult(UnpublishResultType.SuccessVariant, evtMsgs, content); + return new UnpublishResult(result, evtMsgs, content); } // failed - map result @@ -1156,7 +1170,7 @@ namespace Umbraco.Core.Services.Implement // events and audit scope.Events.Dispatch(UnPublished, this, new PublishEventArgs(content, false, false), "UnPublished"); scope.Events.Dispatch(TreeChanged, this, new TreeChange(content, TreeChangeTypes.RefreshBranch).ToEventArgs()); - Audit(AuditType.UnPublish, "UnPublish performed by user", userId, content.Id); + Audit(AuditType.UnPublish, "Unpublished by user", userId, content.Id); scope.Complete(); return new PublishResult(PublishResultType.Success, evtMsgs, content); } @@ -1187,7 +1201,7 @@ namespace Umbraco.Core.Services.Implement scope.Events.Dispatch(Published, this, new PublishEventArgs(descendants, false, false), "Published"); } - Audit(AuditType.Publish, "Save and Publish performed by user", userId, content.Id); + Audit(AuditType.Publish, "Published by user", userId, content.Id); scope.Complete(); return publishResult; } @@ -1321,7 +1335,7 @@ namespace Umbraco.Core.Services.Implement scope.Events.Dispatch(TreeChanged, this, new TreeChange(document, TreeChangeTypes.RefreshBranch).ToEventArgs()); scope.Events.Dispatch(Published, this, new PublishEventArgs(publishedDocuments, false, false), "Published"); - Audit(AuditType.Publish, "SaveAndPublishBranch performed by user", userId, document.Id); + Audit(AuditType.Publish, "Branch published by user", userId, document.Id); scope.Complete(); } @@ -1390,7 +1404,7 @@ namespace Umbraco.Core.Services.Implement DeleteLocked(scope, content); scope.Events.Dispatch(TreeChanged, this, new TreeChange(content, TreeChangeTypes.Remove).ToEventArgs()); - Audit(AuditType.Delete, "Delete Content performed by user", userId, content.Id); + Audit(AuditType.Delete, "Deleted by user", userId, content.Id); scope.Complete(); } @@ -1458,7 +1472,7 @@ namespace Umbraco.Core.Services.Implement deleteRevisionsEventArgs.CanCancel = false; scope.Events.Dispatch(DeletedVersions, this, deleteRevisionsEventArgs); - Audit(AuditType.Delete, "Delete Content by version date performed by user", userId, Constants.System.Root); + Audit(AuditType.Delete, "Delete (by version date) by user", userId, Constants.System.Root); scope.Complete(); } @@ -1495,7 +1509,7 @@ namespace Umbraco.Core.Services.Implement _documentRepository.DeleteVersion(versionId); scope.Events.Dispatch(DeletedVersions, this, new DeleteRevisionsEventArgs(id, false,/* specificVersion:*/ versionId)); - Audit(AuditType.Delete, "Delete Content by version performed by user", userId, Constants.System.Root); + Audit(AuditType.Delete, "Delete (by version) by user", userId, Constants.System.Root); scope.Complete(); } @@ -1540,7 +1554,7 @@ namespace Umbraco.Core.Services.Implement moveEventArgs.CanCancel = false; moveEventArgs.MoveInfoCollection = moveInfo; scope.Events.Dispatch(Trashed, this, moveEventArgs, nameof(Trashed)); - Audit(AuditType.Move, "Move Content to Recycle Bin performed by user", userId, content.Id); + Audit(AuditType.Move, "Moved to Recycle Bin by user", userId, content.Id); scope.Complete(); } @@ -1612,7 +1626,7 @@ namespace Umbraco.Core.Services.Implement moveEventArgs.MoveInfoCollection = moveInfo; moveEventArgs.CanCancel = false; scope.Events.Dispatch(Moved, this, moveEventArgs, nameof(Moved)); - Audit(AuditType.Move, "Move Content performed by user", userId, content.Id); + Audit(AuditType.Move, "Moved by user", userId, content.Id); scope.Complete(); } @@ -1709,7 +1723,7 @@ namespace Umbraco.Core.Services.Implement recycleBinEventArgs.RecycleBinEmptiedSuccessfully = true; // oh my?! scope.Events.Dispatch(EmptiedRecycleBin, this, recycleBinEventArgs); scope.Events.Dispatch(TreeChanged, this, deleted.Select(x => new TreeChange(x, TreeChangeTypes.Remove)).ToEventArgs()); - Audit(AuditType.Delete, "Empty Content Recycle Bin performed by user", 0, Constants.System.RecycleBinContent); + Audit(AuditType.Delete, "Recycle Bin emptied by user", 0, Constants.System.RecycleBinContent); scope.Complete(); } diff --git a/src/Umbraco.Core/Services/UnpublishResultType.cs b/src/Umbraco.Core/Services/UnpublishResultType.cs index 010c37d7a5..e61e786a05 100644 --- a/src/Umbraco.Core/Services/UnpublishResultType.cs +++ b/src/Umbraco.Core/Services/UnpublishResultType.cs @@ -18,7 +18,7 @@ /// /// The specified variant was unpublished, the content item itself remains published. /// - SuccessVariant = 2, + SuccessCulture = 2, /// /// The specified variant was a mandatory culture therefore it was unpublished and the content item itself is unpublished diff --git a/src/Umbraco.Web.UI.Client/src/views/components/content/umb-content-node-info.html b/src/Umbraco.Web.UI.Client/src/views/components/content/umb-content-node-info.html index b33e27d048..c9e4af4062 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/content/umb-content-node-info.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/content/umb-content-node-info.html @@ -1,19 +1,22 @@
- +
- + @@ -23,12 +26,12 @@ - +
- +
-
- +
+
- +
{{item.timestampFormatted}}
- +
{{ item.comment }}
- +
@@ -84,8 +87,8 @@
- - + +
@@ -96,15 +99,15 @@
- + - +
- +
@@ -115,12 +118,12 @@
{{node.releaseDateDay}} {{node.releaseDateTime}}
- Set date - + Set date +
- + Clear date @@ -140,7 +143,7 @@
- +
{{node.removeDateMonth}} {{node.removeDateYear}}
{{node.removeDateDayNumber}}
@@ -159,7 +162,7 @@
- + @@ -200,7 +203,7 @@ {{ node.key }} - +
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/media/umb-media-node-info.html b/src/Umbraco.Web.UI.Client/src/views/components/media/umb-media-node-info.html index 9f4043db39..9d060090ec 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/media/umb-media-node-info.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/media/umb-media-node-info.html @@ -5,16 +5,16 @@ -