Placement.info support enabled

Enabled Module and Theme Placement.info files are parsed, and influence the location of certain shapes
This only applies to part or field shapes that are added by returning ContentShape(...) from a display/editor driver

--HG--
branch : composition
extra : transplant_source : %CF%B0%17%E1%18%C1%06o%B2%91a%23%A1%3D%872%BE%F8%01%F3
This commit is contained in:
Louis DeJardin
2010-10-12 17:33:11 -07:00
parent fd6f482295
commit 7e84e73cae
15 changed files with 396 additions and 64 deletions

View File

@@ -0,0 +1,52 @@
using System;
using System.Collections.Generic;
using System.IO;
using Orchard.Caching;
using Orchard.FileSystems.WebSite;
namespace Orchard.Tests.DisplayManagement.Descriptors {
public class InMemoryWebSiteFolder : IWebSiteFolder {
public InMemoryWebSiteFolder() {
Contents = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
}
public IDictionary<string, string> Contents { get; set; }
public IEnumerable<string> ListDirectories(string virtualPath) {
throw new NotImplementedException();
}
public IEnumerable<string> ListFiles(string virtualPath, bool recursive) {
throw new NotImplementedException();
}
public bool FileExists(string virtualPath) {
throw new NotImplementedException();
}
public string ReadFile(string virtualPath) {
string result;
return Contents.TryGetValue(virtualPath, out result) ? result : null;
}
public string ReadFile(string virtualPath, bool actualContent) {
throw new NotImplementedException();
}
public void CopyFileTo(string virtualPath, Stream destination) {
throw new NotImplementedException();
}
public void CopyFileTo(string virtualPath, Stream destination, bool actualContent) {
throw new NotImplementedException();
}
public IVolatileToken WhenPathChanges(string virtualPath) {
return new Token { IsCurrent = true };
}
public class Token : IVolatileToken {
public bool IsCurrent { get; set; }
}
}
}

View File

@@ -0,0 +1,92 @@
using System.Linq;
using System.Text;
using Autofac;
using NUnit.Framework;
using Orchard.Caching;
using Orchard.DisplayManagement.Descriptors.ShapePlacementStrategy;
using Orchard.FileSystems.WebSite;
using Orchard.Tests.Stubs;
namespace Orchard.Tests.DisplayManagement.Descriptors {
[TestFixture]
public class PlacementFileParserTests : ContainerTestBase {
private IPlacementFileParser _parser;
private InMemoryWebSiteFolder _folder;
protected override void Register(Autofac.ContainerBuilder builder) {
builder.RegisterType<PlacementFileParser>().As<IPlacementFileParser>();
builder.RegisterType<StubCacheManager>().As<ICacheManager>();
builder.RegisterType<InMemoryWebSiteFolder>().As<IWebSiteFolder>()
.As<InMemoryWebSiteFolder>().InstancePerLifetimeScope();
}
protected override void Resolve(IContainer container) {
_parser = container.Resolve<IPlacementFileParser>();
_folder = container.Resolve<InMemoryWebSiteFolder>();
}
[Test]
public void ParsingMissingFileIsNull() {
var result = _parser.Parse("~/hello.xml");
Assert.That(result, Is.Null);
}
[Test]
public void ParsingEmptyFileAsNothing() {
_folder.Contents["~/hello.xml"] = "<Placement/>";
var result = _parser.Parse("~/hello.xml");
Assert.That(result, Is.Not.Null);
Assert.That(result.Nodes, Is.Not.Null);
Assert.That(result.Nodes.Count(), Is.EqualTo(0));
}
[Test]
public void ItemsComeBackAsPlacementNodes() {
_folder.Contents["~/hello.xml"] = @"
<Placement>
<Match ContentType=""BlogPost""/>
<Match ContentType=""Page""/>
</Placement>
";
var result = _parser.Parse("~/hello.xml");
Assert.That(result, Is.Not.Null);
Assert.That(result.Nodes, Is.Not.Null);
Assert.That(result.Nodes.Count(), Is.EqualTo(2));
}
[Test]
public void NestedItemsComeBackAsNestedNodes() {
_folder.Contents["~/hello.xml"] = @"
<Placement>
<Match ContentType=""BlogPost"">
<Match DisplayType=""Detail""/>
</Match>
<Match ContentType=""Page""/>
</Placement>
";
var result = _parser.Parse("~/hello.xml");
Assert.That(result, Is.Not.Null);
Assert.That(result.Nodes, Is.Not.Null);
Assert.That(result.Nodes.Count(), Is.EqualTo(2));
Assert.That(result.Nodes.First().Nodes.Count(), Is.EqualTo(1));
Assert.That(result.Nodes.Last().Nodes.Count(), Is.EqualTo(0));
}
[Test]
public void EachPlaceAttributeIsShapeLocation() {
_folder.Contents["~/hello.xml"] = @"
<Place Foo=""Header"" Bar=""Content:after""/>
";
var result = _parser.Parse("~/hello.xml");
Assert.That(result, Is.Not.Null);
Assert.That(result.Nodes, Is.Not.Null);
Assert.That(result.Nodes.Count(), Is.EqualTo(2));
var foo = result.Nodes.OfType<PlacementShapeLocation>().Single(x=>x.ShapeType == "Foo");
var bar = result.Nodes.OfType<PlacementShapeLocation>().Single(x=>x.ShapeType == "Bar");
Assert.That(foo.Location, Is.EqualTo("Header"));
Assert.That(bar.Location, Is.EqualTo("Content:after"));
}
}
}

View File

@@ -216,6 +216,8 @@
<Compile Include="ContainerTestBase.cs" />
<Compile Include="DisplayManagement\Descriptors\BasicShapeTemplateHarvesterTests.cs" />
<Compile Include="DisplayManagement\Descriptors\DefaultShapeTableManagerTests.cs" />
<Compile Include="DisplayManagement\Descriptors\InMemoryWebSiteFolder.cs" />
<Compile Include="DisplayManagement\Descriptors\PlacementFileParserTests.cs" />
<Compile Include="DisplayManagement\Descriptors\ShapeAttributeBindingStrategyTests.cs" />
<Compile Include="DisplayManagement\Descriptors\ShapeTemplateBindingStrategyTests.cs" />
<Compile Include="DisplayManagement\ShapeFactoryTests.cs" />

View File

@@ -115,6 +115,7 @@
<None Include="Views\EditorTemplates\Parts\Widgets.WidgetPart.cshtml" />
<None Include="Views\EditorTemplates\Parts\Widgets.LayerPart.cshtml" />
<None Include="Views\EditorTemplates\Parts\Widgets.WidgetBagPart.cshtml" />
<Content Include="Views\Items_Widget.cshtml" />
</ItemGroup>
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v10.0\WebApplications\Microsoft.WebApplication.targets" />

View File

@@ -0,0 +1,4 @@
TheWidget
@Display(Model.Content)
@Display(Model.Primary)
@Display(Model.Body)

View File

@@ -125,6 +125,7 @@
<Content Include="Default.aspx" />
<Content Include="Global.asax" />
<Content Include="Refresh.html" />
<None Include="Themes\Classic\Placement.info" />
<Content Include="Themes\Contoso\Styles\search.css" />
<Content Include="Themes\Contoso\Zones\Footer.html" />
<Content Include="Themes\Corporate\Views\DisplayTemplates\Parts\Blogs.BlogPost.Metadata.ascx" />

View File

@@ -0,0 +1,21 @@
<Placement>
<!--
note - commas, and wildcards, do NOT work, but would be very nice
<Match ContentType="Page">
<Match DisplayType="Detail,Summary*" >
<Place
Parts_RoutableTitle="Header"
Parts_BodyText="Content:8"
/>
</Match>
</Match>
-->
<Match ContentType="Page" DisplayType="Detail">
<Place
Parts_RoutableTitle="Content:5.b"
Parts_Common_Body="Header:after" />
</Match>
</Placement>

View File

@@ -5,6 +5,8 @@
<add path="*.aspx" verb="*" type="System.Web.HttpNotFoundHandler"/>
<add path="*.ascx" verb="*" type="System.Web.HttpNotFoundHandler"/>
<add path="*.master" verb="*" type="System.Web.HttpNotFoundHandler"/>
<add path="*.cshtml" verb="*" type="System.Web.HttpNotFoundHandler"/>
<add path="*.info" verb="*" type="System.Web.HttpNotFoundHandler"/>
</httpHandlers>
<!--
@@ -29,7 +31,7 @@
<validation validateIntegratedModeConfiguration="false"/>
<handlers>
<remove name="BlockViewHandler"/>
<add name="BlockViewHandler" path="*.aspx,*.ascx,*.master" verb="*" preCondition="integratedMode" type="System.Web.HttpNotFoundHandler"/>
<add name="BlockViewHandler" path="*.aspx,*.ascx,*.master,*.cshtml,*.info" verb="*" preCondition="integratedMode" type="System.Web.HttpNotFoundHandler"/>
</handlers>
</system.webServer>
<runtime>

View File

@@ -21,7 +21,7 @@ namespace Orchard.ContentManagement {
private readonly IRepository<ContentItemVersionRecord> _contentItemVersionRepository;
private readonly IContentDefinitionManager _contentDefinitionManager;
private readonly Func<IContentManagerSession> _contentManagerSession;
private readonly IContentDisplay _contentDisplay;
private readonly Lazy<IContentDisplay> _contentDisplay;
public DefaultContentManager(
IComponentContext context,
@@ -30,7 +30,7 @@ namespace Orchard.ContentManagement {
IRepository<ContentItemVersionRecord> contentItemVersionRepository,
IContentDefinitionManager contentDefinitionManager,
Func<IContentManagerSession> contentManagerSession,
IContentDisplay contentDisplay) {
Lazy<IContentDisplay> contentDisplay) {
_context = context;
_contentTypeRepository = contentTypeRepository;
_contentItemRepository = contentItemRepository;
@@ -370,15 +370,15 @@ namespace Orchard.ContentManagement {
public dynamic BuildDisplay(IContent content, string displayType = "") {
return _contentDisplay.BuildDisplay(content, displayType);
return _contentDisplay.Value.BuildDisplay(content, displayType);
}
public dynamic BuildEditor(IContent content) {
return _contentDisplay.BuildEditor(content);
return _contentDisplay.Value.BuildEditor(content);
}
public dynamic UpdateEditor(IContent content, IUpdateModel updater) {
return _contentDisplay.UpdateEditor(content, updater);
return _contentDisplay.Value.UpdateEditor(content, updater);
}
public IContentQuery<ContentItem> Query() {

View File

@@ -0,0 +1,19 @@
using System.Collections.Generic;
namespace Orchard.DisplayManagement.Descriptors.ShapePlacementStrategy {
public class PlacementFile : PlacementNode {
}
public class PlacementNode {
public IEnumerable<PlacementNode> Nodes { get; set; }
}
public class PlacementMatch : PlacementNode {
public IDictionary<string, string> Terms { get; set; }
}
public class PlacementShapeLocation : PlacementNode {
public string ShapeType { get; set; }
public string Location { get; set; }
}
}

View File

@@ -0,0 +1,83 @@
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Orchard.Caching;
using Orchard.FileSystems.WebSite;
namespace Orchard.DisplayManagement.Descriptors.ShapePlacementStrategy {
/// <summary>
/// Parses and caches the Placement.info file contents for a given IWebSiteFolder vdir
/// </summary>
public interface IPlacementFileParser : IDependency {
PlacementFile Parse(string virtualPath);
}
public class PlacementFileParser : IPlacementFileParser {
private readonly ICacheManager _cacheManager;
private readonly IWebSiteFolder _webSiteFolder;
public PlacementFileParser(ICacheManager cacheManager, IWebSiteFolder webSiteFolder) {
_cacheManager = cacheManager;
_webSiteFolder = webSiteFolder;
}
public PlacementFile Parse(string virtualPath) {
return _cacheManager.Get(virtualPath, context => {
context.Monitor(_webSiteFolder.WhenPathChanges(virtualPath));
var placementText = _webSiteFolder.ReadFile(virtualPath);
return ParseImplementation(virtualPath, placementText);
});
}
private PlacementFile ParseImplementation(string virtualPath, string placementText) {
if (placementText == null)
return null;
var element = XElement.Parse(placementText);
return new PlacementFile {
Nodes = Accept(element).ToList()
};
}
private IEnumerable<PlacementNode> Accept(XElement element) {
switch (element.Name.LocalName) {
case "Placement":
return AcceptMatch(element);
case "Match":
return AcceptMatch(element);
case "Place":
return AcceptPlace(element);
}
return Enumerable.Empty<PlacementNode>();
}
private IEnumerable<PlacementNode> AcceptMatch(XElement element) {
if (element.HasAttributes == false) {
// Match with no attributes will collapse child results upward
// rather than return an unconditional node
return element.Elements().SelectMany(Accept);
}
// return match node that carries back key/value dictionary of condition,
// and has child rules nested as Nodes
return new[]{new PlacementMatch{
Terms = element.Attributes().ToDictionary(attr=>attr.Name.LocalName, attr=>attr.Value),
Nodes=element.Elements().SelectMany(Accept).ToArray(),
}};
}
private IEnumerable<PlacementShapeLocation> AcceptPlace(XElement element) {
// return attributes as part locations
return element.Attributes().Select(attr => new PlacementShapeLocation {
ShapeType = attr.Name.LocalName,
Location = attr.Value
});
}
}
}

View File

@@ -0,0 +1,110 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using Orchard.Environment.Descriptor.Models;
using Orchard.Environment.Extensions;
using Orchard.Environment.Extensions.Models;
namespace Orchard.DisplayManagement.Descriptors.ShapePlacementStrategy {
/// <summary>
/// This component discovers and announces the shape alterations implied by the contents of the Placement.info files
/// </summary>
public class ShapePlacementParsingStrategy : IShapeTableProvider {
private readonly IExtensionManager _extensionManager;
private readonly ShellDescriptor _shellDescriptor;
private readonly IPlacementFileParser _placementFileParser;
public ShapePlacementParsingStrategy(
IExtensionManager extensionManager,
ShellDescriptor shellDescriptor,
IPlacementFileParser placementFileParser) {
_extensionManager = extensionManager;
_shellDescriptor = shellDescriptor;
_placementFileParser = placementFileParser;
}
public void Discover(ShapeTableBuilder builder) {
var availableFeatures = _extensionManager.AvailableFeatures();
var activeFeatures = availableFeatures.Where(fd => FeatureIsTheme(fd) || FeatureIsEnabled(fd));
var activeExtensions = Once(activeFeatures);
foreach (var extensionDescriptor in activeExtensions) {
foreach (var featureDescriptor in extensionDescriptor.Features.Where(fd=>fd.Name == fd.Extension.Name)) {
ProcessFeatureDescriptor(builder, featureDescriptor);
}
}
}
private void ProcessFeatureDescriptor(ShapeTableBuilder builder, FeatureDescriptor featureDescriptor) {
// The App_Data subfolder is used because it is not served.
// But it's still considered a site-deployable content file, which is why the IWebSiteFolder (not the IAppDataFolder) is appropriate
var virtualPath = featureDescriptor.Extension.Location + "/" + featureDescriptor.Extension.Name + "/App_Data/Placement.xml";
var placementFile = _placementFileParser.Parse(virtualPath);
if (placementFile != null) {
ProcessPlacementFile(builder, featureDescriptor, placementFile);
}
}
private void ProcessPlacementFile(ShapeTableBuilder builder, FeatureDescriptor featureDescriptor, PlacementFile placementFile) {
var feature = new Feature {Descriptor = featureDescriptor};
// invert the tree into a list of leaves and the stack
var entries = DrillDownShapeLocations(placementFile.Nodes, Enumerable.Empty<PlacementMatch>());
foreach (var entry in entries) {
var shapeLocation = entry.Item1;
var matches = entry.Item2;
Func<ShapePlacementContext, bool> predicate = ctx => true;
predicate = matches.SelectMany(match=>match.Terms).Aggregate(predicate, BuildPredicate);
builder.Describe(shapeLocation.ShapeType)
.From(feature)
.Placement(predicate, shapeLocation.Location);
}
}
private Func<ShapePlacementContext, bool> BuildPredicate(Func<ShapePlacementContext, bool> predicate, KeyValuePair<string, string> term) {
var expression = term.Value;
switch(term.Key) {
case "ContentType":
return ctx=>ctx.ContentType == expression ? true : predicate(ctx);
case "DisplayType":
return ctx=>ctx.DisplayType == expression ? true : predicate(ctx);
}
return predicate;
}
private static IEnumerable<Tuple<PlacementShapeLocation, IEnumerable<PlacementMatch>>> DrillDownShapeLocations(
IEnumerable<PlacementNode> nodes,
IEnumerable<PlacementMatch> path) {
// return shape locations nodes in this place
foreach (var placementShapeLocation in nodes.OfType<PlacementShapeLocation>()) {
yield return new Tuple<PlacementShapeLocation, IEnumerable<PlacementMatch>>(placementShapeLocation, path);
}
// recurse down into match nodes
foreach (var placementMatch in nodes.OfType<PlacementMatch>()) {
foreach (var findShapeLocation in DrillDownShapeLocations(placementMatch.Nodes, path.Concat(new[] {placementMatch}))) {
yield return findShapeLocation;
}
}
}
private bool FeatureIsTheme(FeatureDescriptor fd) {
return fd.Extension.ExtensionType == "Theme";
}
private bool FeatureIsEnabled(FeatureDescriptor fd) {
return _shellDescriptor.Features.Any(sf => sf.Name == fd.Name);
}
private static IEnumerable<ExtensionDescriptor> Once(IEnumerable<FeatureDescriptor> featureDescriptors) {
var once = new ConcurrentDictionary<string, object>();
return featureDescriptors.Select(fd => fd.Extension).Where(ed => once.TryAdd(ed.Name, null)).ToList();
}
}
}

View File

@@ -1,56 +0,0 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Orchard.Environment.Descriptor.Models;
using Orchard.Environment.Extensions;
using Orchard.Environment.Extensions.Models;
namespace Orchard.DisplayManagement.Descriptors.ShapePlacements {
public class ShapePlacementParsingStrategy : IShapeTableProvider {
private readonly IExtensionManager _extensionManager;
private readonly ShellDescriptor _shellDescriptor;
public ShapePlacementParsingStrategy(
IExtensionManager extensionManager,
ShellDescriptor shellDescriptor) {
_extensionManager = extensionManager;
_shellDescriptor = shellDescriptor;
}
public void Discover(ShapeTableBuilder builder) {
var availableFeatures = _extensionManager.AvailableFeatures();
var activeFeatures = availableFeatures.Where(fd => FeatureIsTheme(fd) || FeatureIsEnabled(fd));
var activeExtensions = Once(activeFeatures);
foreach (var extensionDescriptor in activeExtensions) {
foreach (var featureDescriptor in extensionDescriptor.Features.Where(fd=>fd.Name == fd.Extension.Name)) {
builder.Describe("Parts_RoutableTitle")
.From(new Feature{Descriptor = featureDescriptor})
.Placement(ctx => ctx.ContentType == "WidgetPage", "Content:after");
}
//var featureDescriptors = extensionDescriptor.Where(fd => fd.Name == hit.extensionDescriptor.Name);
//foreach (var featureDescriptor in featureDescriptors) {
}
//builder.Describe("Parts_RoutableTitle")
//.Placement(ctx => ctx.ContentType == "Page", "Content:after");
}
private bool FeatureIsTheme(FeatureDescriptor fd) {
return fd.Extension.ExtensionType == "Theme";
}
private bool FeatureIsEnabled(FeatureDescriptor fd) {
return _shellDescriptor.Features.Any(sf => sf.Name == fd.Name);
}
private static IEnumerable<ExtensionDescriptor> Once(IEnumerable<FeatureDescriptor> featureDescriptors) {
var once = new ConcurrentDictionary<string, object>();
return featureDescriptors.Select(fd => fd.Extension).Where(ed => once.TryAdd(ed.Name, null)).ToList();
}
}
}

View File

@@ -165,7 +165,8 @@
<Compile Include="DisplayManagement\Descriptors\ShapeDescriptor.cs" />
<Compile Include="DisplayManagement\Descriptors\ShapeAlteration.cs" />
<Compile Include="DisplayManagement\Descriptors\ShapeAlterationBuilder.cs" />
<Compile Include="DisplayManagement\Descriptors\ShapePlacements\ShapePlacementParsingStrategy.cs" />
<Compile Include="DisplayManagement\Descriptors\ShapePlacementStrategy\PlacementFileParser.cs" />
<Compile Include="DisplayManagement\Descriptors\ShapePlacementStrategy\ShapePlacementParsingStrategy.cs" />
<Compile Include="DisplayManagement\Descriptors\ShapeTable.cs" />
<Compile Include="DisplayManagement\Descriptors\ShapeTableBuilder.cs" />
<Compile Include="DisplayManagement\Implementation\IShapeDisplayEvents.cs" />