Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implements ContentPicker localization options discussed in #7352 #7391

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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<Fields.ContentPickerField> {
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<ContentItem>(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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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<ContentPickerField>();

foreach (var field in fields) {
var settings = field.PartFieldDefinition.Settings.GetModel<ContentPickerFieldLocalizationSettings>();
if (settings.TryToLocalizeItems) {
//try to replace items in the field with their translation
var itemsInField = _contentManager.GetMany<ContentItem>(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<int, int>(ci.Id, ci.Id); //this item is fine
var localized = _localizationService.GetLocalizations(ci).FirstOrDefault(lp => lp.Culture == lPart.Culture);
return localized == null ? new Tuple<int, int>(ci.Id, -ci.Id) : new Tuple<int, int>(ci.Id, localized.Id); //return negative id where we found no translation
}
else {
//we only go here if RemoveItemsWithNoLocalizationPart == false
return new Tuple<int, int>(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<int, int>(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<ContentItem>(field.Ids, VersionOptions.Published, QueryHints.Empty);
var itemsWithoutLocalizationPart = itemsInField.Where(ci => !ci.Parts.Any(part => part is LocalizationPart));
List<int> 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))
));
}
}
}
}
}

}
}
5 changes: 5 additions & 0 deletions src/Orchard.Web/Modules/Orchard.ContentPicker/Module.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,10 @@
<Reference Include="System.Xml.Linq" />
</ItemGroup>
<ItemGroup>
<Compile Include="Drivers\ContentPickerFieldLocalizationDriver.cs" />
<Compile Include="Handlers\ContentPickerFieldLocalizationExtensionHandler.cs" />
<Compile Include="Settings\ContentPickerFieldLocalizationEditorEvents.cs" />
<Compile Include="Settings\ContentPickerFieldLocalizationSettings.cs" />
<Compile Include="ViewModels\NavigationPartViewModel.cs" />
<Content Include="Scripts\ContentPicker.js" />
<Content Include="Scripts\SelectableContentTab.js" />
Expand Down Expand Up @@ -182,13 +186,23 @@
<Name>Orchard.Core</Name>
<Private>false</Private>
</ProjectReference>
<ProjectReference Include="..\Orchard.Localization\Orchard.Localization.csproj">
<Project>{fbc8b571-ed50-49d8-8d9d-64ab7454a0d6}</Project>
<Name>Orchard.Localization</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<Content Include="Views\ContentPicker.Edit.cshtml" />
</ItemGroup>
<ItemGroup>
<Content Include="packages.config" />
</ItemGroup>
<ItemGroup>
<Content Include="Views\DefinitionTemplates\ContentPickerFieldLocalizationSettings.cshtml" />
</ItemGroup>
<ItemGroup>
<Content Include="Views\EditorTemplates\Fields\ContentPickerLocalization.Edit.cshtml" />
</ItemGroup>
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">10.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<Placement>
<Place Fields_ContentPicker_Edit="Content:2.3"/>
<Place Fields_ContentPickerLocalization_Edit="Content:2.3"/>
<Place Parts_Navigation_Edit="Content:10"/>
<Place Parts_ContentMenuItem_Edit="Content:10"/>

Expand Down
Original file line number Diff line number Diff line change
@@ -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<TemplateViewModel> PartFieldEditor(ContentPartFieldDefinition definition) {
if (definition.FieldDefinition.Name == "ContentPickerField") {
var model = definition.Settings.GetModel<ContentPickerFieldLocalizationSettings>();
yield return DefinitionTemplate(model);
}
}

public override IEnumerable<TemplateViewModel> 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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
@model Orchard.ContentPicker.Settings.ContentPickerFieldLocalizationSettings

<fieldset>
@Html.CheckBoxFor(m => m.TryToLocalizeItems)
<label for="@Html.FieldIdFor(m => m.TryToLocalizeItems)" class="forcheckbox">@T("Try to replace selected items with their correct localization.")</label>
<span class="hint">@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.")</span>
<div data-controllerid="@Html.FieldIdFor(m => m.TryToLocalizeItems)">
@Html.CheckBoxFor(m => m.RemoveItemsWithoutLocalization)
<label for="@Html.FieldIdFor(m => m.RemoveItemsWithoutLocalization)" class="forcheckbox">@T("Remove items that do not have the correct translation.")</label>
<span class="hint">@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').")</span>
@Html.CheckBoxFor(m => m.RemoveItemsWithNoLocalizationPart)
<label for="@Html.FieldIdFor(m => m.RemoveItemsWithNoLocalizationPart)" class="forcheckbox">@T("Remove items that cannot be localized.")</label>
<span class="hint">@T("Check to remove items from the ContentPickerField when the items selected cannot be localized (do not have a LocalizationPart).")</span>
</div>

@Html.CheckBoxFor(m => m.AssertItemsHaveSameCulture)
<label for="@Html.FieldIdFor(m => m.AssertItemsHaveSameCulture)" class="forcheckbox">@T("Verify culture of selected items.")</label>
<span class="hint">@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.")</span>
<div data-controllerid="@Html.FieldIdFor(m => m.AssertItemsHaveSameCulture)">
@Html.CheckBoxFor(m => m.BlockForItemsWithNoLocalizationPart)
<label for="@Html.FieldIdFor(m => m.BlockForItemsWithNoLocalizationPart)" class="forcheckbox">@T("Do not admit items that cannot be localized.")</label>
<span class="hint">@T("Check to stop publication of contents when the items selected cannot be localized (do not have a LocalizationPart).")</span>
</div>
</fieldset>
Original file line number Diff line number Diff line change
@@ -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<ContentPickerFieldLocalizationSettings>();

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()) {
<script type="text/javascript">
$(function () {
$("fieldset[data-part-name='@dataPartName'][data-field-name='@dataFieldName']").find("span.hint")[0].innerText += "@tryTranslateMsg";
});
@foreach (var contentItem in Model.ContentItems) {
var loc = contentItem.As<LocalizationPart>();
if (loc != null && loc.Culture!=null && !string.IsNullOrWhiteSpace(loc.Culture.Culture)) {
<text>
$(function () {
$("span[data-id='@contentItem.Id'][data-fieldid='@Html.FieldIdFor(m => m.Field.Ids)'].content-picker-item")[0].append(" (@loc.Culture.Culture)");
})
</text>
}
}
</script>
}