diff --git a/src/Orchard.Web/Modules/Orchard.ContentPicker/Drivers/ContentPickerFieldLocalizationDriver.cs b/src/Orchard.Web/Modules/Orchard.ContentPicker/Drivers/ContentPickerFieldLocalizationDriver.cs new file mode 100644 index 00000000000..1f56bafebf9 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.ContentPicker/Drivers/ContentPickerFieldLocalizationDriver.cs @@ -0,0 +1,44 @@ +using System.Linq; +using Orchard.ContentManagement; +using Orchard.ContentManagement.Drivers; +using Orchard.ContentPicker.Fields; +using Orchard.ContentPicker.ViewModels; +using Orchard.Environment.Extensions; + +namespace Orchard.ContentPicker.Drivers { + [OrchardFeature("Orchard.ContentPicker.LocalizationExtensions")] + public class ContentPickerFieldLocalizationDriver : ContentFieldDriver { + private readonly IContentManager _contentManager; + + public ContentPickerFieldLocalizationDriver(IContentManager contentManager) { + _contentManager = contentManager; + } + + private static string GetPrefix(Fields.ContentPickerField field, ContentPart part) { + return part.PartDefinition.Name + "." + field.Name; + } + + private static string GetDifferentiator(Fields.ContentPickerField field, ContentPart part) { + return field.Name; + } + + protected override DriverResult Editor(ContentPart part, Fields.ContentPickerField field, dynamic shapeHelper) { + return ContentShape("Fields_ContentPickerLocalization_Edit", GetDifferentiator(field, part), + () => { + var model = new ContentPickerFieldViewModel { + Field = field, + Part = part, + ContentItems = _contentManager.GetMany(field.Ids, VersionOptions.Latest, QueryHints.Empty).ToList() + }; + + model.SelectedIds = string.Join(",", field.Ids); + + return shapeHelper.EditorTemplate(TemplateName: "Fields/ContentPickerLocalization.Edit", Model: model, Prefix: GetPrefix(field, part)); + }); + } + + protected override DriverResult Editor(ContentPart part, ContentPickerField field, IUpdateModel updater, dynamic shapeHelper) { + return Editor(part, field, shapeHelper); + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.ContentPicker/Handlers/ContentPickerFieldLocalizationExtensionHandler.cs b/src/Orchard.Web/Modules/Orchard.ContentPicker/Handlers/ContentPickerFieldLocalizationExtensionHandler.cs new file mode 100644 index 00000000000..7b604f2f10b --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.ContentPicker/Handlers/ContentPickerFieldLocalizationExtensionHandler.cs @@ -0,0 +1,119 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Orchard.ContentManagement; +using Orchard.ContentManagement.Handlers; +using Orchard.ContentPicker.Fields; +using Orchard.ContentPicker.Settings; +using Orchard.Environment.Extensions; +using Orchard.Localization; +using Orchard.Localization.Models; +using Orchard.Localization.Services; +using Orchard.UI.Notify; + +namespace Orchard.ContentPicker.Handlers { + [OrchardFeature("Orchard.ContentPicker.LocalizationExtensions")] + public class ContentPickerFieldLocalizationExtensionHandler : ContentHandler { + private readonly IOrchardServices _orchardServices; + private readonly IContentManager _contentManager; + private readonly ILocalizationService _localizationService; + public Localizer T { get; set; } + + public ContentPickerFieldLocalizationExtensionHandler( + IOrchardServices orchardServices, + IContentManager contentManager, + ILocalizationService localizationService) { + _orchardServices = orchardServices; + _contentManager = contentManager; + _localizationService = localizationService; + + T = NullLocalizer.Instance; + } + + protected override void UpdateEditorShape(UpdateEditorContext context) { + base.UpdateEditorShape(context); + //Here we implement the logic based on the settings introduced in ContentPickerFieldLocalizationSettings + //These settings should only be active if the ContentItem that is being updated has a LocalizationPart + if (context.ContentItem.Parts.Any(part => part is LocalizationPart)) { + var lPart = (LocalizationPart)context.ContentItem.Parts.Single(part => part is LocalizationPart); + var fields = context.ContentItem.Parts.SelectMany(x => x.Fields.Where(f => f.FieldDefinition.Name == typeof(ContentPickerField).Name)).Cast(); + + foreach (var field in fields) { + var settings = field.PartFieldDefinition.Settings.GetModel(); + if (settings.TryToLocalizeItems) { + //try to replace items in the field with their translation + var itemsInField = _contentManager.GetMany(field.Ids, VersionOptions.Published, QueryHints.Empty); + if (settings.RemoveItemsWithNoLocalizationPart && itemsInField.Where(ci => !ci.Parts.Any(part => part is LocalizationPart)).Any()) { + //keep only items that have a LocalizationPart + _orchardServices.Notifier.Warning(T( + "{0}: The following items could have no localization, so they were removed: {1}", + field.DisplayName, + string.Join(", ", itemsInField.Where(ci => !ci.Parts.Any(part => part is LocalizationPart)).Select(ci => _contentManager.GetItemMetadata(ci).DisplayText)) + )); + itemsInField = itemsInField.Where(ci => ci.Parts.Any(part => part is LocalizationPart)); + } + //use an (int, int) tuple to track translations + var newIds = itemsInField.Select(ci => { + if (ci.Parts.Any(part => part is LocalizationPart)) { + if (_localizationService.GetContentCulture(ci) == lPart.Culture.Culture) + return new Tuple(ci.Id, ci.Id); //this item is fine + var localized = _localizationService.GetLocalizations(ci).FirstOrDefault(lp => lp.Culture == lPart.Culture); + return localized == null ? new Tuple(ci.Id, -ci.Id) : new Tuple(ci.Id, localized.Id); //return negative id where we found no translation + } + else { + //we only go here if RemoveItemsWithNoLocalizationPart == false + return new Tuple(ci.Id, ci.Id); + } + }); + if (newIds.Any(tup => tup.Item2 < 0)) { + if (settings.RemoveItemsWithoutLocalization) { + //remove the items for which we could not find a localization + _orchardServices.Notifier.Warning(T( + "{0}: We could not find a localization for the following items, so they were removed: {1}", + field.DisplayName, + string.Join(", ", newIds.Where(tup => tup.Item2 < 0).Select(tup => _contentManager.GetItemMetadata(_contentManager.GetLatest(tup.Item1)).DisplayText)) + )); + newIds = newIds.Where(tup => tup.Item2 > 0); + } + else { + //negative Ids are made positive again + newIds = newIds.Select(tup => tup = new Tuple(tup.Item1, Math.Abs(tup.Item2))); + } + } + if (newIds.Where(tup => tup.Item1 != tup.Item2).Any()) { + _orchardServices.Notifier.Warning(T( + "{0}: The following items were replaced by their correct localization: {1}", + field.DisplayName, + string.Join(", ", newIds.Where(tup => tup.Item1 != tup.Item2).Select(tup => _contentManager.GetItemMetadata(_contentManager.GetLatest(tup.Item1)).DisplayText)) + )); + } + + field.Ids = newIds.Select(tup => tup.Item2).Distinct().ToArray(); + } + if (settings.AssertItemsHaveSameCulture) { + //verify that the items in the ContentPickerField are all in the culture of the ContentItem whose editor we are updating + var itemsInField = _contentManager.GetMany(field.Ids, VersionOptions.Published, QueryHints.Empty); + var itemsWithoutLocalizationPart = itemsInField.Where(ci => !ci.Parts.Any(part => part is LocalizationPart)); + List badItemIds = itemsInField.Where(ci => ci.Parts.Any(part => part is LocalizationPart && ((LocalizationPart)part).Culture != lPart.Culture)).Select(ci => ci.Id).ToList(); + if (itemsWithoutLocalizationPart.Count() > 0) { + //Verify items from the ContentPickerField that cannot be localized + _orchardServices.Notifier.Warning(T("{0}: Some of the selected items cannot be localized: {1}", + field.DisplayName, + string.Join(", ", itemsWithoutLocalizationPart.Select(ci => _contentManager.GetItemMetadata(ci).DisplayText)) + )); + if (settings.BlockForItemsWithNoLocalizationPart) { + badItemIds.AddRange(itemsWithoutLocalizationPart.Select(ci => ci.Id)); + } + } + if (badItemIds.Count > 0) { + context.Updater.AddModelError(field.DisplayName, T("Some of the items selected have the wrong localization: {0}", + string.Join(", ", badItemIds.Select(id => _contentManager.GetItemMetadata(_contentManager.GetLatest(id)).DisplayText)) + )); + } + } + } + } + } + + } +} diff --git a/src/Orchard.Web/Modules/Orchard.ContentPicker/Module.txt b/src/Orchard.Web/Modules/Orchard.ContentPicker/Module.txt index 7bc8a95b77f..fd0def349f7 100644 --- a/src/Orchard.Web/Modules/Orchard.ContentPicker/Module.txt +++ b/src/Orchard.Web/Modules/Orchard.ContentPicker/Module.txt @@ -10,3 +10,8 @@ Features: Description: UI for selecting Content Items. Dependencies: Contents, Navigation Category: Input Editor + Orchard.ContentPicker.LocalizationExtensions: + Name: Orchard.ContentPicker.LocalizationExtensions + Description: Provides settings to enable advanced localization behaviours for ContentPickerFields. + Dependencies: Orchard.ContentPicker, Orchard.Localization, Orchard.Resources + Category: Input Editor diff --git a/src/Orchard.Web/Modules/Orchard.ContentPicker/Orchard.ContentPicker.csproj b/src/Orchard.Web/Modules/Orchard.ContentPicker/Orchard.ContentPicker.csproj index 3306448051e..cc9fe0d430b 100644 --- a/src/Orchard.Web/Modules/Orchard.ContentPicker/Orchard.ContentPicker.csproj +++ b/src/Orchard.Web/Modules/Orchard.ContentPicker/Orchard.ContentPicker.csproj @@ -98,6 +98,10 @@ + + + + @@ -182,6 +186,10 @@ Orchard.Core false + + {fbc8b571-ed50-49d8-8d9d-64ab7454a0d6} + Orchard.Localization + @@ -189,6 +197,12 @@ + + + + + + 10.0 $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) diff --git a/src/Orchard.Web/Modules/Orchard.ContentPicker/Placement.info b/src/Orchard.Web/Modules/Orchard.ContentPicker/Placement.info index cf849e55bc6..68d147e41f9 100644 --- a/src/Orchard.Web/Modules/Orchard.ContentPicker/Placement.info +++ b/src/Orchard.Web/Modules/Orchard.ContentPicker/Placement.info @@ -1,5 +1,6 @@  + diff --git a/src/Orchard.Web/Modules/Orchard.ContentPicker/Settings/ContentPickerFieldLocalizationEditorEvents.cs b/src/Orchard.Web/Modules/Orchard.ContentPicker/Settings/ContentPickerFieldLocalizationEditorEvents.cs new file mode 100644 index 00000000000..e06cf2c167f --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.ContentPicker/Settings/ContentPickerFieldLocalizationEditorEvents.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using System.Globalization; +using Orchard.ContentManagement; +using Orchard.ContentManagement.MetaData; +using Orchard.ContentManagement.MetaData.Builders; +using Orchard.ContentManagement.MetaData.Models; +using Orchard.ContentManagement.ViewModels; +using Orchard.Environment.Extensions; + +namespace Orchard.ContentPicker.Settings { + [OrchardFeature("Orchard.ContentPicker.LocalizationExtensions")] + public class ContentPickerFieldLocalizationEditorEvents : ContentDefinitionEditorEventsBase { + + public override IEnumerable PartFieldEditor(ContentPartFieldDefinition definition) { + if (definition.FieldDefinition.Name == "ContentPickerField") { + var model = definition.Settings.GetModel(); + yield return DefinitionTemplate(model); + } + } + + public override IEnumerable PartFieldEditorUpdate(ContentPartFieldDefinitionBuilder builder, IUpdateModel updateModel) { + if (builder.FieldType != "ContentPickerField") { + yield break; + } + + var model = new ContentPickerFieldLocalizationSettings(); + if (updateModel.TryUpdateModel(model, "ContentPickerFieldLocalizationSettings", null, null)) { + builder.WithSetting("ContentPickerFieldLocalizationSettings.TryToLocalizeItems", model.TryToLocalizeItems.ToString(CultureInfo.InvariantCulture)); + builder.WithSetting("ContentPickerFieldLocalizationSettings.RemoveItemsWithoutLocalization", model.RemoveItemsWithoutLocalization.ToString(CultureInfo.InvariantCulture)); + builder.WithSetting("ContentPickerFieldLocalizationSettings.RemoveItemsWithNoLocalizationPart", model.RemoveItemsWithNoLocalizationPart.ToString(CultureInfo.InvariantCulture)); + builder.WithSetting("ContentPickerFieldLocalizationSettings.AssertItemsHaveSameCulture", model.AssertItemsHaveSameCulture.ToString(CultureInfo.InvariantCulture)); + builder.WithSetting("ContentPickerFieldLocalizationSettings.BlockForItemsWithNoLocalizationPart", model.BlockForItemsWithNoLocalizationPart.ToString(CultureInfo.InvariantCulture)); + } + + yield return DefinitionTemplate(model); + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.ContentPicker/Settings/ContentPickerFieldLocalizationSettings.cs b/src/Orchard.Web/Modules/Orchard.ContentPicker/Settings/ContentPickerFieldLocalizationSettings.cs new file mode 100644 index 00000000000..b8aea7e4373 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.ContentPicker/Settings/ContentPickerFieldLocalizationSettings.cs @@ -0,0 +1,16 @@ +using Orchard.Environment.Extensions; + +namespace Orchard.ContentPicker.Settings { + [OrchardFeature("Orchard.ContentPicker.LocalizationExtensions")] + public class ContentPickerFieldLocalizationSettings { + + public ContentPickerFieldLocalizationSettings() { + TryToLocalizeItems = true; + } + public bool TryToLocalizeItems { get; set; } + public bool RemoveItemsWithoutLocalization { get; set; } + public bool RemoveItemsWithNoLocalizationPart { get; set; } + public bool AssertItemsHaveSameCulture { get; set; } + public bool BlockForItemsWithNoLocalizationPart { get; set; } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.ContentPicker/Views/DefinitionTemplates/ContentPickerFieldLocalizationSettings.cshtml b/src/Orchard.Web/Modules/Orchard.ContentPicker/Views/DefinitionTemplates/ContentPickerFieldLocalizationSettings.cshtml new file mode 100644 index 00000000000..fbcfebf61a3 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.ContentPicker/Views/DefinitionTemplates/ContentPickerFieldLocalizationSettings.cshtml @@ -0,0 +1,24 @@ +@model Orchard.ContentPicker.Settings.ContentPickerFieldLocalizationSettings + +
+ @Html.CheckBoxFor(m => m.TryToLocalizeItems) + + @T("Check to attempt to replace items selected in this field with their translation in the main ContentItem's culture. This only applies if the main ContentItem has a LocalizationPart.") +
+ @Html.CheckBoxFor(m => m.RemoveItemsWithoutLocalization) + + @T("Check to remove items from the ContentPickerField when the items selected do not have a version in the correct culture (they have a LocalizationPart, but not a translation in the main ContentItem's culture').") + @Html.CheckBoxFor(m => m.RemoveItemsWithNoLocalizationPart) + + @T("Check to remove items from the ContentPickerField when the items selected cannot be localized (do not have a LocalizationPart).") +
+ + @Html.CheckBoxFor(m => m.AssertItemsHaveSameCulture) + + @T("Check to prevent publication of contents when the items selected have a different culture. This only applies if the main ContentItem has a LocalizationPart.") +
+ @Html.CheckBoxFor(m => m.BlockForItemsWithNoLocalizationPart) + + @T("Check to stop publication of contents when the items selected cannot be localized (do not have a LocalizationPart).") +
+
diff --git a/src/Orchard.Web/Modules/Orchard.ContentPicker/Views/EditorTemplates/Fields/ContentPickerLocalization.Edit.cshtml b/src/Orchard.Web/Modules/Orchard.ContentPicker/Views/EditorTemplates/Fields/ContentPickerLocalization.Edit.cshtml new file mode 100644 index 00000000000..41f232d1a76 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.ContentPicker/Views/EditorTemplates/Fields/ContentPickerLocalization.Edit.cshtml @@ -0,0 +1,39 @@ +@model Orchard.ContentPicker.ViewModels.ContentPickerFieldViewModel +@using Orchard.ContentPicker.Settings; +@using Orchard.Localization.Models; +@using Orchard.ContentManagement; + +@{ + Script.Require("jQuery").AtFoot(); + + var settings = Model.Field.PartFieldDefinition.Settings.GetModel(); + + string tryTranslateMsg = T("Selected items with a localization different than the current one will be localized.").Text; + string removeMissingMsg = T("Selected items for which there is no correct localization will be removed.").Text; + string removeUnlocalizableMsg = T("Selected items that cannot have localizations will be removed.").Text; + + if (settings.RemoveItemsWithoutLocalization) { tryTranslateMsg += " " + removeMissingMsg; } + if (settings.RemoveItemsWithNoLocalizationPart) { tryTranslateMsg += " " + removeUnlocalizableMsg; } + + //We will use a script to find the fieldset for the field we are currently processing. + //The fieldset contains a span of class "hint". We will add tryTranslateMsg to it. + string dataPartName = HttpUtility.JavaScriptStringEncode(Model.Part.PartDefinition.Name); + string dataFieldName = HttpUtility.JavaScriptStringEncode(Model.Field.PartFieldDefinition.Name); +} +@using (Script.Foot()) { + +} \ No newline at end of file