Groundwork for rss support by container id

--HG--
extra : convert_revision : svn%3A5ff7c347-ad56-4c35-b696-ccb81de16e03/trunk%4045452
This commit is contained in:
loudej
2010-01-15 03:44:54 +00:00
parent 775567683c
commit c4eaca47e5
19 changed files with 607 additions and 0 deletions

View File

@@ -0,0 +1,189 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web.Mvc;
using System.Xml.Linq;
using Autofac.Builder;
using Autofac.Modules;
using Moq;
using NUnit.Framework;
using Orchard.ContentManagement;
using Orchard.ContentManagement.Handlers;
using Orchard.Core.Common.Models;
using Orchard.Core.Common.Records;
using Orchard.Core.Feeds;
using Orchard.Core.Feeds.Controllers;
using Orchard.Core.Feeds.Models;
using Orchard.Core.Feeds.Rss;
using Orchard.Core.Feeds.Services;
using Orchard.Mvc.Results;
using Orchard.Tests.Packages;
using Orchard.Tests.Stubs;
namespace Orchard.Core.Tests.Feeds.Controllers {
[TestFixture]
public class FeedControllerTests {
[Test]
public void InvalidFormatShpuldReturnNotFoundResult() {
var controller = new FeedController(
Enumerable.Empty<IFeedQueryProvider>(),
Enumerable.Empty<IFeedFormatterProvider>(),
Enumerable.Empty<IFeedItemBuilder>()) {
ValueProvider = Values.From(new { })
};
var result = controller.Index("no-such-format");
Assert.That(result, Is.Not.Null);
Assert.That(result, Is.TypeOf<NotFoundResult>());
}
[Test]
public void ControllerShouldReturnAnActionResult() {
var formatProvider = new Mock<IFeedFormatterProvider>();
var format = new Mock<IFeedFormatter>();
formatProvider.Setup(x => x.Match(It.IsAny<FeedContext>()))
.Returns(new FeedFormatterMatch { FeedFormatter = format.Object, Priority = 10 });
var queryProvider = new Mock<IFeedQueryProvider>();
var query = new Mock<IFeedQuery>();
queryProvider.Setup(x => x.Match(It.IsAny<FeedContext>()))
.Returns(new FeedQueryMatch { FeedQuery = query.Object, Priority = 10 });
format.Setup(x => x.Process(It.IsAny<FeedContext>(), It.IsAny<Action>())).Returns(new ContentResult());
var controller = new FeedController(
new[] { queryProvider.Object },
new[] { formatProvider.Object },
Enumerable.Empty<IFeedItemBuilder>()) {
ValueProvider = Values.From(new { })
};
var result = controller.Index("test-format");
Assert.That(result, Is.Not.Null);
Assert.That(result, Is.InstanceOf<ActionResult>());
formatProvider.Verify();
queryProvider.Verify();
format.Verify();
}
class StubQuery : IFeedQueryProvider, IFeedQuery {
private readonly IEnumerable<ContentItem> _items;
public StubQuery(IEnumerable<ContentItem> items) {
_items = items;
}
public FeedQueryMatch Match(FeedContext context) {
return new FeedQueryMatch { FeedQuery = this, Priority = 10 };
}
public void Execute(FeedContext context) {
foreach (var item in _items) {
context.FeedFormatter.AddItem(context, item);
}
}
}
[Test]
public void RssFeedShouldBeStructuredAppropriately() {
var query = new StubQuery(Enumerable.Empty<ContentItem>());
var builder = new ContainerBuilder();
builder.RegisterModule(new ImplicitCollectionSupportModule());
builder.Register<FeedController>();
builder.Register<RssFeedFormatProvider>().As<IFeedFormatterProvider>();
builder.Register(query).As<IFeedQueryProvider>();
var container = builder.Build();
var controller = container.Resolve<FeedController>();
controller.ValueProvider = Values.From(new { });
var result = controller.Index("rss");
Assert.That(result, Is.Not.Null);
Assert.That(result, Is.InstanceOf<RssResult>());
var doc = ((RssResult)result).Document;
Assert.That(doc.Root.Name, Is.EqualTo(XName.Get("rss")));
Assert.That(doc.Root.Elements().Single().Name, Is.EqualTo(XName.Get("channel")));
}
[Test]
public void OneItemPerContentItemShouldBeCreated() {
var query = new StubQuery(new[] {
new ContentItem(),
new ContentItem(),
});
var builder = new ContainerBuilder();
builder.RegisterModule(new ImplicitCollectionSupportModule());
builder.Register<FeedController>();
builder.Register<RssFeedFormatProvider>().As<IFeedFormatterProvider>();
builder.Register(query).As<IFeedQueryProvider>();
var container = builder.Build();
var controller = container.Resolve<FeedController>();
controller.ValueProvider = Values.From(new { });
var result = controller.Index("rss");
Assert.That(result, Is.Not.Null);
Assert.That(result, Is.InstanceOf<RssResult>());
var doc = ((RssResult)result).Document;
var items = doc.Elements("rss").Elements("channel").Elements("item");
Assert.That(items.Count(), Is.EqualTo(2));
}
[Test]
public void CorePartValuesAreExtracted() {
var clock = new StubClock();
var hello = new ContentItemBuilder("hello")
.Weld<CommonAspect>()
.Weld<RoutableAspect>()
.Weld<BodyAspect>()
.Build();
hello.As<CommonAspect>().Record = new CommonRecord();
hello.As<RoutableAspect>().Record = new RoutableRecord();
hello.As<BodyAspect>().Record = new BodyRecord();
hello.As<CommonAspect>().PublishedUtc = clock.UtcNow;
hello.As<RoutableAspect>().Title = "alpha";
hello.As<RoutableAspect>().Slug = "beta";
hello.As<BodyAspect>().Text = "gamma";
var query = new StubQuery(new[] {
hello,
});
var mockContentManager = new Mock<IContentManager>();
mockContentManager.Setup(x => x.GetItemMetadata(It.IsAny<IContent>()))
.Returns(new ContentItemMetadata { DisplayText = "foo" });
var builder = new ContainerBuilder();
builder.RegisterModule(new ImplicitCollectionSupportModule());
builder.Register<FeedController>();
builder.Register(mockContentManager.Object).As<IContentManager>();
builder.Register<RssFeedFormatProvider>().As<IFeedFormatterProvider>();
builder.Register<CorePartsFeedItemBuilder>().As<IFeedItemBuilder>();
builder.Register(query).As<IFeedQueryProvider>();
var container = builder.Build();
var controller = container.Resolve<FeedController>();
controller.ValueProvider = Values.From(new { });
var result = controller.Index("rss");
Assert.That(result, Is.Not.Null);
Assert.That(result, Is.InstanceOf<RssResult>());
var doc = ((RssResult)result).Document;
var item = doc.Elements("rss").Elements("channel").Elements("item").Single();
Assert.That(item.Element("title").Value, Is.EqualTo("foo"));
Assert.That(item.Element("description").Value, Is.EqualTo("gamma"));
}
}
}

View File

@@ -60,6 +60,10 @@
<HintPath>..\..\lib\sqlite\System.Data.SQLite.DLL</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System.Web.Mvc, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\..\lib\aspnetmvc\System.Web.Mvc.dll</HintPath>
</Reference>
<Reference Include="System.Xml.Linq">
<RequiredTargetFramework>3.5</RequiredTargetFramework>
</Reference>
@@ -71,6 +75,7 @@
</ItemGroup>
<ItemGroup>
<Compile Include="Common\Providers\CommonAspectProviderTests.cs" />
<Compile Include="Feeds\Controllers\FeedControllerTests.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Scheduling\ScheduledTaskManagerTests.cs" />
<Compile Include="Scheduling\ScheduledTaskExecutorTests.cs" />

View File

@@ -0,0 +1,57 @@
using System.Collections.Generic;
using System.Linq;
using System.Web.Mvc;
using JetBrains.Annotations;
using Orchard.Core.Feeds.Models;
using Orchard.Logging;
using Orchard.Mvc.Results;
namespace Orchard.Core.Feeds.Controllers {
public class FeedController : Controller {
private readonly IEnumerable<IFeedFormatterProvider> _feedFormatProviders;
private readonly IEnumerable<IFeedQueryProvider> _feedQueryProviders;
private readonly IEnumerable<IFeedItemBuilder> _feedItemBuilders;
public FeedController(
IEnumerable<IFeedQueryProvider> feedQueryProviders,
IEnumerable<IFeedFormatterProvider> feedFormatProviders,
IEnumerable<IFeedItemBuilder> feedItemBuilders) {
_feedQueryProviders = feedQueryProviders;
_feedFormatProviders = feedFormatProviders;
_feedItemBuilders = feedItemBuilders;
Logger = NullLogger.Instance;
}
public ILogger Logger { get; set; }
public ActionResult Index(string format) {
var context = new FeedContext(ValueProvider, format);
var bestFormatterMatch = _feedFormatProviders
.Select(provider => provider.Match(context))
.Where(match => match != null && match.FeedFormatter != null)
.OrderByDescending(match => match.Priority)
.FirstOrDefault();
if (bestFormatterMatch == null || bestFormatterMatch.FeedFormatter == null)
return new NotFoundResult();
context.FeedFormatter = bestFormatterMatch.FeedFormatter;
var bestQueryMatch = _feedQueryProviders
.Select(provider => provider.Match(context))
.Where(match => match != null && match.FeedQuery != null)
.OrderByDescending(match => match.Priority)
.FirstOrDefault();
if (bestQueryMatch == null || bestQueryMatch.FeedQuery == null)
return new NotFoundResult();
return context.FeedFormatter.Process(context, () => {
bestQueryMatch.FeedQuery.Execute(context);
_feedItemBuilders.Invoke(x => x.Populate(context), Logger);
});
}
}
}

View File

@@ -0,0 +1,13 @@
using System;
using System.Web.Mvc;
using Orchard.ContentManagement;
using Orchard.Core.Feeds.Models;
namespace Orchard.Core.Feeds {
public interface IFeedFormatter {
ActionResult Process(FeedContext context, Action populate);
FeedItem AddItem(FeedContext context, ContentItem contentItem);
void AddProperty(FeedContext context, FeedItem feedItem, string name, string value);
}
}

View File

@@ -0,0 +1,12 @@
using Orchard.Core.Feeds.Models;
namespace Orchard.Core.Feeds {
public interface IFeedFormatterProvider : IDependency {
FeedFormatterMatch Match(FeedContext context);
}
public class FeedFormatterMatch {
public int Priority { get; set; }
public IFeedFormatter FeedFormatter { get; set; }
}
}

View File

@@ -0,0 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Orchard.Core.Feeds.Models;
namespace Orchard.Core.Feeds {
public interface IFeedItemBuilder : IEvents {
void Populate(FeedContext context);
}
}

View File

@@ -0,0 +1,7 @@
using Orchard.Core.Feeds.Models;
namespace Orchard.Core.Feeds {
public interface IFeedQuery {
void Execute(FeedContext context);
}
}

View File

@@ -0,0 +1,14 @@
using System.Collections.Generic;
using Orchard.ContentManagement;
using Orchard.Core.Feeds.Models;
namespace Orchard.Core.Feeds {
public interface IFeedQueryProvider : IEvents {
FeedQueryMatch Match(FeedContext context);
}
public class FeedQueryMatch {
public int Priority { get; set; }
public IFeedQuery FeedQuery { get; set; }
}
}

View File

@@ -0,0 +1,22 @@
using System.Collections.Generic;
using System.Web.Mvc;
namespace Orchard.Core.Feeds.Models {
public class FeedContext {
public FeedContext(IValueProvider valueProvider, string format) {
ValueProvider = valueProvider;
Format = format;
Response = new FeedResponse();
FeedData = new Dictionary<string, object>();
}
public IValueProvider ValueProvider { get; set; }
public string Format { get; set; }
public IFeedFormatter FeedFormatter { get; set; }
public IDictionary<string, object> FeedData { get; set; }
public FeedResponse Response { get; set; }
}
}

View File

@@ -0,0 +1,9 @@
using System.Xml.Linq;
using Orchard.ContentManagement;
namespace Orchard.Core.Feeds.Models {
public class FeedItem {
public ContentItem ContentItem { get; set; }
public XElement Element { get; set; }
}
}

View File

@@ -0,0 +1,15 @@
using System.Collections.Generic;
using System.Web.Mvc;
using System.Xml.Linq;
namespace Orchard.Core.Feeds.Models {
public class FeedResponse {
public FeedResponse() {
Items = new List<FeedItem>();
}
public IList<FeedItem> Items { get; set; }
public XElement Element { get; set; }
public ActionResult Result { get; set; }
}
}

View File

@@ -0,0 +1 @@
name: Feeds

View File

@@ -0,0 +1,34 @@
using System.Collections.Generic;
using System.Web.Mvc;
using System.Web.Routing;
using Orchard.Mvc.Routes;
namespace Orchard.Core.Feeds.Rss {
public class Routes : IRouteProvider {
public IEnumerable<RouteDescriptor> GetRoutes() {
return new[] {
new RouteDescriptor {Priority =-10,
Route = new Route(
"rss",
new RouteValueDictionary {
{"area", "Feeds"},
{"controller", "Feed"},
{"action", "Index"},
{"format", "rss"},
},
new RouteValueDictionary(),
new RouteValueDictionary {
{"area", "Feeds"}
},
new MvcRouteHandler())
}
};
}
public void GetRoutes(ICollection<RouteDescriptor> routes) {
foreach (RouteDescriptor routeDescriptor in GetRoutes()) {
routes.Add(routeDescriptor);
}
}
}
}

View File

@@ -0,0 +1,55 @@
using System;
using System.Collections.Generic;
using System.Web.Mvc;
using System.Xml.Linq;
using JetBrains.Annotations;
using Orchard.ContentManagement;
using Orchard.Core.Feeds.Models;
namespace Orchard.Core.Feeds.Rss {
[UsedImplicitly]
public class RssFeedFormatProvider : IFeedFormatterProvider, IFeedFormatter {
public FeedFormatterMatch Match(FeedContext context) {
if (context.Format == "rss") {
return new FeedFormatterMatch {
FeedFormatter = this,
Priority = -5
};
}
return null;
}
public ActionResult Process(FeedContext context, Action populate) {
var rss = new XElement("rss");
rss.SetAttributeValue("version", "2.0");
var channel = new XElement("channel");
context.Response.Element = channel;
rss.Add(channel);
populate();
return new RssResult(new XDocument(rss));
}
public FeedItem AddItem(FeedContext context, ContentItem contentItem) {
var feedItem = new FeedItem {
ContentItem = contentItem,
Element = new XElement("item"),
};
context.Response.Items.Add(feedItem);
context.Response.Element.Add(feedItem.Element);
return feedItem;
}
public void AddProperty(FeedContext context, FeedItem feedItem, string name, string value) {
if (feedItem == null) {
context.Response.Element.Add(new XElement(name, value));
}
else {
feedItem.Element.Add(new XElement(name, value));
}
}
}
}

View File

@@ -0,0 +1,23 @@
using System.Web.Mvc;
using System.Xml;
using System.Xml.Linq;
namespace Orchard.Core.Feeds.Rss {
public class RssResult : ActionResult {
public XDocument Document { get; private set; }
public RssResult(XDocument document) {
Document = document;
}
public override void ExecuteResult(ControllerContext context) {
// not returning application/rss+xml because of
// https://bugzilla.mozilla.org/show_bug.cgi?id=256379
context.HttpContext.Response.ContentType = "text/xml";
using (var writer = XmlWriter.Create(context.HttpContext.Response.Output))
Document.WriteTo(writer);
}
}
}

View File

@@ -0,0 +1,63 @@
using System;
using System.Xml.Linq;
using Orchard.ContentManagement;
using Orchard.ContentManagement.Aspects;
using Orchard.Core.Common.Models;
using Orchard.Core.Feeds.Models;
namespace Orchard.Core.Feeds.Services {
public class CorePartsFeedItemBuilder : IFeedItemBuilder {
private readonly IContentManager _contentManager;
public CorePartsFeedItemBuilder(IContentManager contentManager) {
_contentManager = contentManager;
}
public void Populate(FeedContext context) {
foreach (var feedItem in context.Response.Items) {
// locate parts
var contentItem = feedItem.ContentItem;
var metadata = _contentManager.GetItemMetadata(contentItem);
var common = contentItem.Get<ICommonAspect>();
var routable = contentItem.Get<RoutableAspect>();
var body = contentItem.Get<BodyAspect>();
// standard fields
var link = "/todo";// metadata.DisplayRouteValues();
var title = metadata.DisplayText;
if (string.IsNullOrEmpty(title) && routable != null)
title = routable.Title;
var contentText = title;
if (body != null && !string.IsNullOrEmpty(body.Text))
contentText = body.Text;
DateTime? publishedDate = null;
if (common != null)
publishedDate = common.PublishedUtc ?? common.ModifiedUtc;
// TODO: author
// add to known formats
if (context.Format == "rss") {
feedItem.Element.SetElementValue("title", title);
feedItem.Element.SetElementValue("link", link);
feedItem.Element.SetElementValue("description", contentText);
if (publishedDate != null)
feedItem.Element.SetElementValue("pubDate", publishedDate);//TODO: format
//feedItem.Data.SetElementValue("description", contentText);
feedItem.Element.Add(new XElement("guid", new XAttribute("isPermaLink", "true"), new XText(link)));
}
else {
context.FeedFormatter.AddProperty(context, feedItem, "link", link);
context.FeedFormatter.AddProperty(context, feedItem, "title", title);
context.FeedFormatter.AddProperty(context, feedItem, "description", contentText);
if (publishedDate != null)
context.FeedFormatter.AddProperty(context, feedItem, "published-date", Convert.ToString(publishedDate)); // format? cvt to generic T?
}
}
}
}
}

View File

@@ -0,0 +1,60 @@
using JetBrains.Annotations;
using Orchard.ContentManagement;
using Orchard.Core.Common.Models;
using Orchard.Core.Common.Records;
using Orchard.Core.Feeds.Models;
namespace Orchard.Core.Feeds.StandardQueries {
[UsedImplicitly]
public class ContainerFeedQuery : IFeedQueryProvider, IFeedQuery {
private readonly IContentManager _contentManager;
public ContainerFeedQuery(IContentManager contentManager) {
_contentManager = contentManager;
}
public FeedQueryMatch Match(FeedContext context) {
var containerIdValue = context.ValueProvider.GetValue("containerid");
if (containerIdValue == null)
return null;
return new FeedQueryMatch { FeedQuery = this, Priority = -5 };
}
public void Execute(FeedContext context) {
var containerIdValue = context.ValueProvider.GetValue("containerid");
if (containerIdValue == null)
return;
var limitValue = context.ValueProvider.GetValue("limit");
var limit = 20;
if (limitValue != null)
limit = (int)limitValue.ConvertTo(typeof(int));
var containerId = (int)containerIdValue.ConvertTo(typeof(int));
var container = _contentManager.Get(containerId);
var containerRoutable = container.As<RoutableAspect>();
var containerBody = container.As<BodyAspect>();
if (containerRoutable != null) {
context.FeedFormatter.AddProperty(context, null, "title", containerRoutable.Title);
context.FeedFormatter.AddProperty(context, null, "link", "/" + containerRoutable.Slug);
}
if (containerBody != null) {
context.FeedFormatter.AddProperty(context, null, "description", containerBody.Text);
}
else if (containerRoutable != null) {
context.FeedFormatter.AddProperty(context, null, "description", containerRoutable.Title);
}
var items = _contentManager.Query()
.Where<CommonRecord>(x => x.Container == container.Record)
.OrderByDescending(x => x.PublishedUtc)
.Slice(0, limit);
foreach (var item in items) {
context.FeedFormatter.AddItem(context, item);
}
}
}
}

View File

@@ -81,6 +81,20 @@
<Compile Include="Common\ViewModels\BodyEditorViewModel.cs" />
<Compile Include="Common\ViewModels\RoutableEditorViewModel.cs" />
<Compile Include="Common\ViewModels\OwnerEditorViewModel.cs" />
<Compile Include="Feeds\Controllers\FeedController.cs" />
<Compile Include="Feeds\Routes.cs" />
<Compile Include="Feeds\StandardQueries\ContainerFeedQuery.cs" />
<Compile Include="Feeds\StandardBuilders\CorePartsFeedItemBuilder.cs" />
<Compile Include="Feeds\IFeedFormatter.cs" />
<Compile Include="Feeds\IFeedFormatterProvider.cs" />
<Compile Include="Feeds\IFeedQuery.cs" />
<Compile Include="Feeds\IFeedQueryProvider.cs" />
<Compile Include="Feeds\IFeedItemBuilder.cs" />
<Compile Include="Feeds\Models\FeedContext.cs" />
<Compile Include="Feeds\Models\FeedItem.cs" />
<Compile Include="Feeds\Models\FeedResponse.cs" />
<Compile Include="Feeds\Rss\RssFeedFormat.cs" />
<Compile Include="Feeds\Rss\RssResult.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Scheduling\Records\ScheduledTaskRecord.cs" />
<Compile Include="Scheduling\Services\PublishingTaskHandler.cs" />
@@ -152,6 +166,7 @@
<Content Include="Common\Views\EditorTemplates\Parts\Common.Routable.ascx" />
<Content Include="Common\Views\EditorTemplates\Parts\Common.Body.ascx" />
<Content Include="Common\Views\EditorTemplates\Parts\Common.Owner.ascx" />
<Content Include="Feeds\Package.txt" />
<Content Include="Scheduling\Package.txt" />
<Content Include="Settings\Views\EditorTemplates\Items\Settings.Site.ascx" />
<Content Include="Themes\Scripts\jquery-1.4.js" />

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic;
using JetBrains.Annotations;
using Orchard.Logging;
namespace Orchard.Tasks {
@@ -7,6 +8,7 @@ namespace Orchard.Tasks {
void Sweep();
}
[UsedImplicitly]
public class BackgroundService : IBackgroundService {
private readonly IEnumerable<IBackgroundTask> _tasks;