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

Upgrades to enumeration fields and queries related to them #8801

Open
wants to merge 5 commits into
base: 1.10.x
Choose a base branch
from
Open
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
Expand Up @@ -223,4 +223,4 @@
</FlavorProperties>
</VisualStudio>
</ProjectExtensions>
</Project>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@
<Compile Include="Providers\Executors\MemberBindingsStep.cs" />
<Compile Include="Providers\Filters\ContentPartRecordsForm.cs" />
<Compile Include="Providers\Filters\EagerFetchFilter.cs" />
<Compile Include="Providers\Filters\EnumerationFieldsFilter.cs" />
<Compile Include="Providers\Layouts\RawLayout.cs" />
<Compile Include="Providers\Layouts\RawLayoutForms.cs" />
<Compile Include="Providers\Layouts\RawShapes.cs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,12 @@ public void Describe(DescribeFilterContext describe) {
continue;
}

// Exclude EnumerationFields, that use a specific IFilterProvider
var fields = part.Fields.Where(fd => !fd.FieldDefinition.Name.Equals("EnumerationField"));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can't be an acceptable solution. The upgrade should make things right such that not such hack is necessary.


var descriptor = describe.For(part.Name + "ContentFields", T("{0} Content Fields", part.Name.CamelFriendly()), T("Content Fields for {0}", part.Name.CamelFriendly()));

foreach(var field in part.Fields) {
foreach(var field in fields) {
var localField = field;
var localPart = part;
var drivers = _contentFieldDrivers.Where(x => x.GetFieldInfo().Any(fi => fi.FieldTypeName == localField.FieldDefinition.Name)).ToList();
Expand Down Expand Up @@ -76,6 +79,7 @@ public void ApplyFilter(FilterContext context, IFieldTypeEditor fieldTypeEditor,

// generate the predicate based on the editor which has been used
dynamic fullState = context.State;

fullState.VersionScope = context.QueryPartRecord.VersionScope;
Action<IHqlExpressionFactory> predicate = fieldTypeEditor.GetFilterPredicate(fullState);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Orchard.ContentManagement;
using Orchard.ContentManagement.Drivers;
using Orchard.ContentManagement.Handlers;
using Orchard.ContentManagement.MetaData;
using Orchard.ContentManagement.MetaData.Models;
using Orchard.Localization;
using Orchard.Projections.Descriptors.Filter;
using Orchard.Projections.FieldTypeEditors;
using Orchard.Projections.Services;
using Orchard.Utility.Extensions;

namespace Orchard.Projections.Providers.Filters {
public class EnumerationFieldsFilter : IFilterProvider {
private readonly IContentDefinitionManager _contentDefinitionManager;
private readonly IEnumerable<IContentFieldDriver> _contentFieldDrivers;
private readonly IEnumerable<IFieldTypeEditor> _fieldTypeEditors;

public EnumerationFieldsFilter(
IContentDefinitionManager contentDefinitionManager,
IEnumerable<IContentFieldDriver> contentFieldDrivers,
IEnumerable<IFieldTypeEditor> fieldTypeEditors) {

_contentDefinitionManager = contentDefinitionManager;
_contentFieldDrivers = contentFieldDrivers;
_fieldTypeEditors = fieldTypeEditors;
T = NullLocalizer.Instance;
}

public Localizer T { get; set; }

public void Describe(DescribeFilterContext describe) {
foreach (var part in _contentDefinitionManager.ListPartDefinitions()) {
var enumerationFieldDefinitions = part.Fields.Where(fd => fd.FieldDefinition.Name.Equals("EnumerationField"));

var descriptor = describe.For(part.Name + "ContentFields", T("{0} Content Fields", part.Name.CamelFriendly()), T("Content Fields for {0}", part.Name.CamelFriendly()));

foreach (var field in enumerationFieldDefinitions) {
var localField = field;
var localPart = part;
var drivers = _contentFieldDrivers.Where(x => x.GetFieldInfo().Any(fi => fi.FieldTypeName == localField.FieldDefinition.Name)).ToList();

var membersContext = new DescribeMembersContext(
(storageName, storageType, displayName, description) => {
// look for a compatible field type editor
IFieldTypeEditor fieldTypeEditor = _fieldTypeEditors.FirstOrDefault(x => x.CanHandle(storageType));

if (fieldTypeEditor == null) {
return;
}

descriptor.Element(
type: localPart.Name + "." + localField.Name + "." + storageName,
name: new LocalizedString(localField.DisplayName + (displayName != null ? ":" + displayName.Text : "")),
description: description ?? T("{0} property for {1}", storageName, localField.DisplayName),
filter: context => ApplyFilter(context, fieldTypeEditor, storageName, storageType, localPart, localField),
display: context => fieldTypeEditor.DisplayFilter(localPart.Name.CamelFriendly() + "." + localField.DisplayName, storageName, context.State),
form: fieldTypeEditor.FormName);
});


foreach (var driver in drivers) {
driver.Describe(membersContext);
}
}
}
}

public void ApplyFilter(FilterContext context, IFieldTypeEditor fieldTypeEditor, string storageName, Type storageType, ContentPartDefinition part, ContentPartFieldDefinition field) {
var propertyName = String.Join(".", part.Name, field.Name, storageName ?? "");

// use an alias with the join so that two filters on the same Field Type wont collide
var relationship = fieldTypeEditor.GetFilterRelationship(propertyName.ToSafeName());

// Equals and NotEquals operators
// In both cases, to ensure compatibility with non-upgraded EnumerationFields, both original values (with no ';' character as a separator) and new values must be considered.
// For this reason, separator characters are added before and after the value.
if ((context.State.Operator.ToString().Equals("Equals", StringComparison.OrdinalIgnoreCase) ||
context.State.Operator.ToString().Equals("NotEquals", StringComparison.OrdinalIgnoreCase))) {

context.State.Value = ";" + context.State.Value.ToString().Trim(';') + ";";
}

// Starts and NotStarts operators
// The separator ';' character at the start of the value has to be forced
if ((context.State.Operator.ToString().Equals("Starts", StringComparison.OrdinalIgnoreCase) ||
context.State.Operator.ToString().Equals("NotStarts", StringComparison.OrdinalIgnoreCase))) {

context.State.Value = ";" + context.State.Value.ToString().TrimStart(';');
}

// Ends and NotEnds operators
// The separator ';' character at the end of the value has to be forced
if ((context.State.Operator.ToString().Equals("Ends", StringComparison.OrdinalIgnoreCase) ||
context.State.Operator.ToString().Equals("NotEnds", StringComparison.OrdinalIgnoreCase))) {

context.State.Value = context.State.Value.ToString().TrimEnd(';') + ";";
}

// generate the predicate based on the editor which has been used
dynamic fullState = context.State;

fullState.VersionScope = context.QueryPartRecord.VersionScope;
Action<IHqlExpressionFactory> predicate = fieldTypeEditor.GetFilterPredicate(fullState);

// combines the predicate with a filter on the specific property name of the storage, as implemented in FieldIndexService
Action<IHqlExpressionFactory> andPredicate = x => x.And(y => y.Eq("PropertyName", propertyName), predicate);

// apply where clause
context.Query = context.Query.Where(relationship, andPredicate);
}

public LocalizedString DisplayFilter(FilterContext context, ContentPartDefinition part, ContentPartFieldDefinition fieldDefinition) {
string op = context.State.Operator;
string value = context.State.Value;

return T("Field {0} {1} \"{2}\"", fieldDefinition.Name, op, value);
}
}
}
1 change: 1 addition & 0 deletions src/Orchard.Web/Modules/Upgrade/AdminMenu.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public void GetNavigation(NavigationBuilder builder) {
.Add(T("Fields (1.5)"), "5", item => item.Action("Index", "Field", new { area = "Upgrade" }).LocalNav().Permission(StandardPermissions.SiteOwner))
.Add(T("Menu (1.5)"), "6", item => item.Action("Index", "Menu", new { area = "Upgrade" }).LocalNav().Permission(StandardPermissions.SiteOwner))
.Add(T("Routes (1.4)"), "7", item => item.Action("Index", "Route", new { area = "Upgrade" }).LocalNav().Permission(StandardPermissions.SiteOwner))
.Add(T("Enumeration Field values"), "8", item => item.Action("Index", "EnumerationFields", new {area = "Upgrade"}).LocalNav().Permission(StandardPermissions.SiteOwner))
);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using System;
using System.Linq;
using System.Web.Mvc;
using Orchard.Environment.Features;
using Orchard.Localization;
using Orchard.Logging;
using Orchard.Tasks.Scheduling;
using Orchard.UI.Admin;
using Orchard.UI.Notify;
using Upgrade.Handlers;

namespace Upgrade.Controllers {
[Admin]
public class EnumerationFieldsController : Controller {
private readonly IFeatureManager _featureManager;
private readonly INotifier _notifier;
private readonly IScheduledTaskManager _scheduledTaskManager;

public EnumerationFieldsController(IFeatureManager featureManager,
INotifier notifier,
IScheduledTaskManager scheduledTaskManager) {

T = NullLocalizer.Instance;
_featureManager = featureManager;
_notifier = notifier;
_scheduledTaskManager = scheduledTaskManager;
}

public Localizer T { get; set; }
public ILogger Logger { get; set; }

public ActionResult Index() {
ViewBag.CanMigrate = false;

if (_featureManager.GetEnabledFeatures().All(x => x.Id != "Orchard.Fields")) {
_notifier.Warning(T("You need to enable Orchard.Fields in order to migrate Enumeration Field values."));
} else {
ViewBag.CanMigrate = true;
}

return View();
}

[HttpPost, ActionName("Index")]
public ActionResult IndexPOST() {
_scheduledTaskManager.CreateTask(EnumerationFieldsUpgradeScheduledTaskHandler.TaskType,
DateTime.UtcNow.AddMinutes(1),
null);

_notifier.Information(T("Upgrade scheduled. It may take several minutes to complete."));

return RedirectToAction("Index");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Orchard;
using Orchard.ContentManagement;
using Orchard.ContentManagement.Handlers;
using Orchard.ContentManagement.MetaData;
using Orchard.Core.Common.Models;
using Orchard.Fields.Fields;
using Orchard.Fields.Settings;
using Orchard.Logging;
using Orchard.Tasks.Scheduling;

namespace Upgrade.Handlers {
public class EnumerationFieldsUpgradeScheduledTaskHandler : IScheduledTaskHandler {
private readonly IContentDefinitionManager _contentDefinitionManager;
private readonly IContentManager _contentManager;
private readonly IEnumerable<IContentHandler> _contentHandlers;

public static string TaskType = "Upgrade.EnumerationFields.Task";

public ILogger Logger { get; set; }

public EnumerationFieldsUpgradeScheduledTaskHandler(IContentDefinitionManager contentDefinitionManager,
IContentManager contentManager,
IEnumerable<IContentHandler> contentHandlers) {

_contentDefinitionManager = contentDefinitionManager;
_contentManager = contentManager;
_contentHandlers = contentHandlers;
Logger = NullLogger.Instance;
}

public void Process(ScheduledTaskContext context) {
if (context.Task.TaskType.Equals(TaskType, StringComparison.OrdinalIgnoreCase)) {
// Content types
var contentTypes = _contentDefinitionManager.ListTypeDefinitions()
// Where a part
.Where(ct => ct.Parts
// Contains a Enumeration Field
.Any(pd => pd.PartDefinition.Fields
.Any(pfd => pfd.FieldDefinition.Name.Equals("EnumerationField", StringComparison.OrdinalIgnoreCase))))
.Select(ct => ct.Name)
.ToArray();

var query = _contentManager.Query()
.ForType(contentTypes)
// Ensure to read the latest version of every content item
.ForVersion(VersionOptions.Latest);

var sliceSize = 20;
var i = 0;
var contentItems = query.Slice(i * sliceSize, sliceSize);

while (contentItems.Any()) {
foreach (var ci in contentItems) {
UpdateItem(ci);

// If current content item isn't published, check if there is a published version and update it too
if (!ci.IsPublished()) {
var pci = _contentManager.Get(ci.Id, VersionOptions.Published);
if (pci != null) {
UpdateItem(pci);
}
}
}
i++;
contentItems = query.Slice(i * sliceSize, sliceSize);
}
}
}

private ContentItem UpdateItem(ContentItem ci) {
var updateContext = new UpdateContentContext(ci);
_contentHandlers.Invoke(handler => handler.Updating(updateContext), Logger);

// Force the update for each EnumerationField.
var enumFields = ci.Parts
.SelectMany(pa => pa.Fields)
.Where(fi => fi is EnumerationField)
.ToList();

foreach(EnumerationField field in enumFields) {
var v = field.Value ?? string.Empty;

if (string.IsNullOrWhiteSpace(v)) {
// If value is empty, check for its default value and assign it instead.
var settings = field.PartFieldDefinition.Settings.GetModel<EnumerationFieldSettings>();
if (!(string.IsNullOrWhiteSpace(settings.DefaultValue))) {
v = settings.DefaultValue;
}
}
field.Value = v.ToString();
}

_contentHandlers.Invoke(handler => handler.Updated(updateContext), Logger);

if (ci.IsPublished()) {
ci.VersionRecord.Published = false;
_contentManager.Publish(ci);
}
return ci;
}
}
}
7 changes: 7 additions & 0 deletions src/Orchard.Web/Modules/Upgrade/Upgrade.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,9 @@
<Content Include="Styles\menu.upgrade-admin.css" />
<Content Include="Web.config" />
<Content Include="Styles\Web.config" />
<Compile Include="Controllers\EnumerationFieldsController.cs" />
<Compile Include="Controllers\ProjectionsController.cs" />
<Compile Include="Handlers\EnumerationFieldsUpgradeScheduledTaskHandler.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Content Include="Module.txt" />
</ItemGroup>
Expand All @@ -134,6 +136,10 @@
<Project>{05660f47-d649-48bd-9ded-df4e01e7cff9}</Project>
<Name>Orchard.Email</Name>
</ProjectReference>
<ProjectReference Include="..\Orchard.Fields\Orchard.Fields.csproj">
<Project>{3787DDE5-E5C8-4841-BDA7-DCB325388064}</Project>
<Name>Orchard.Fields</Name>
</ProjectReference>
<ProjectReference Include="..\Orchard.MediaLibrary\Orchard.MediaLibrary.csproj">
<Project>{73a7688a-5bd3-4f7e-adfa-ce36c5a10e3b}</Project>
<Name>Orchard.MediaLibrary</Name>
Expand Down Expand Up @@ -180,6 +186,7 @@
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
<Content Include="Views\EnumerationFields\Index.cshtml" />
</ItemGroup>
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">10.0</VisualStudioVersion>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
@{
Layout.Title = T("Upgrade Enumeration Fields").ToString();
}

@using (Html.BeginFormAntiForgeryPost()) {

<fieldset>
<span class="hint">@T("The update procedure processes every content item which has a EnumerationFiled in the definition of any of its parts.")</span>
<span class="hint">@T("Every content item is saved / published again to ensure both infoset and string field index records are upgraded.")</span>
<span class="hint">@T("The operation is scheduled in a task and may take several minutes, depending on the number of content items involved.")</span>
<button type="submit">@T("Upgrade values")</button>
</fieldset>
}