Layer rules

Nov 8, 2010 at 12:22 PM

Hi,

I wanted to create a layer rule for placing widgets on pages and posts with a specified tag. I was wondering if there was any guidance on layer rules?

Thanks,

Richard Garside.

Coordinator
Nov 8, 2010 at 7:42 PM

Layer rules are really just Ruby expressions. They are evaluated in a sandbox where all classes that implement IRuleProvider are available.

AuthenticatedRuleProvider is a good one to look at to see how to create your own.

Rules can be deployed in modules and made available to share on the gallery ;).

Nov 9, 2010 at 4:16 PM

Hi Bertrand,

I've created a rule provider, but I can't work out how to find the tags for the current page.

My first thought was to inject an instance of IOrchardServices, and use ContentManager but this was just a stab in the dark really and I couldn't find how to get the info I needed. Could you give me a hint or point me towards a bit of code that checks the tags that the displayed piece of content has. I was also wondering if I needed to check if the displayed content had tags attached at all.

Thanks for your help,

Richard.

Coordinator
Nov 9, 2010 at 7:36 PM

Ah, apparently you don't have access to that information from rules. The only information you can use for now is information about the request.

Nov 10, 2010 at 10:30 AM

Should I add this as a work item, or is it already on the agenda?

Coordinator
Nov 10, 2010 at 1:58 PM

I have launched a process on a background thread in my head, and it ended this morning with a solution. You can implement an IContentHandler class, registering the OnDisplay method, and if the DisplayType is Detail, then using and IoC injected IWorkContextAccessor, retrieve GetContext(), and SetState() this value. Thus from any view you can do a As<TagsPart> on the current displayed item, if available. You could even go further by providing the list of all content items/display type in the current request.

Nov 10, 2010 at 2:15 PM

I can't quite get my head around that. I've only just started delving into the underbelly of Orchard.

How would my IRuleProvider class get access to the IContentHandler class?

Coordinator
Nov 10, 2010 at 4:25 PM

Nice, Sebastien. I think you'd have that content handler set-up the state for you but the rule doesn't need to access the content handler itself. What it needs to inject is just the IWorkContextAccessor that's needed to retrieve the state the content handler set-up. Does this help?

Coordinator
Nov 10, 2010 at 5:01 PM

Here is the working solution:

The content handler which enlists all displayed content items within a request (here there is a filter on the Detail display type which makes sense for this purpose)

using System.Collections.Generic;
using Orchard.ContentManagement;
using Orchard.ContentManagement.Handlers;

namespace Orchard.Experimental.Handlers {
    public class CurrentContentItemHandler : ContentHandler {
        private readonly IWorkContextAccessor _workContextAccessor;

        public CurrentContentItemHandler(IWorkContextAccessor workContextAccessor) {
            _workContextAccessor = workContextAccessor;
        }

        protected override void BuildDisplayShape(BuildDisplayContext context) {
            if (context.DisplayType == "Detail") {
                var workContext = _workContextAccessor.GetContext();
                var contentItems = workContext.GetState<List<IContent>>("ContentItems");
                if (contentItems == null) {
                    workContext.SetState("ContentItems", contentItems = new List<IContent>());
                }

                contentItems.Add(context.ContentItem);
            }
        }
    }
}

Then a new IRuleProvider implementation to add a tagged() function to the widget rules engine:

using System;
using System.Collections.Generic;
using System.Linq;
using Orchard.ContentManagement;
using Orchard.Mvc;
using Orchard.Tags.Models;
using Orchard.UI.Widgets;

namespace Orchard.Experimental.RuleEngine {
    public class WithTagsRuleProvider : IRuleProvider {
        private readonly IHttpContextAccessor _httpContextAccessor;
        private readonly IWorkContextAccessor _workContextAccessor;

        public WithTagsRuleProvider(IHttpContextAccessor httpContextAccessor, IWorkContextAccessor workContextAccessor) {
            _httpContextAccessor = httpContextAccessor;
            _workContextAccessor = workContextAccessor;
        }

        public void Process(RuleContext ruleContext) {
            if (!String.Equals(ruleContext.FunctionName, "tagged"StringComparison.OrdinalIgnoreCase))
                return;

            var tag = Convert.ToString(ruleContext.Arguments[0]);
            var workContext = _workContextAccessor.GetContext();
            var contentItems = workContext.GetState<List<IContent>>("ContentItems");

            if(contentItems != null && contentItems.Any(c => c.As<TagsPart>() != null)){
                ruleContext.Result = true;
                return;
            }

            ruleContext.Result = false;
        }
    }
}

The module will need a reference to Orchard.Tags project.

Ideally, I would love to have the list of displayed content items in the WorkContext out of the box. It could open some nice scenarios. It might also be in a specific module so that it can be reused. And then the new rule filter would be in the Tags module.


Coordinator
Nov 10, 2010 at 5:03 PM

Oh, just a remark. This code is a proof of concept, you need to add one or two more lines to really filter based on the actual tag. I'm just filtering any content item with a TagPart, not one which actually has a tag ;) Getting the argument passed to tagged() is already here though.

Nov 12, 2010 at 12:04 AM

Thanks. Wish you were here to solve all my problems and write most of my code for me.

I've just modified this bit slightly from:

if(contentItems != null && contentItems.Any(c => c.As<TagsPart>() != null)){
    ruleContext.Result = true;
    return;
}

to:

if (contentItems != null)
{
	var taggedContent = contentItems.Where(c => c.As<TagsPart>() != null);

	if (taggedContent.Any(c => c.As<TagsPart>().CurrentTags.Any(t => t.TagName == tag)))
	{
		ruleContext.Result = true;
		return;
	}
}

I've got a working version. Will test it on my blog over the weekend. If all is well I'll add it to the gallery so people can use it in 0.8.

Do you think you'll add this as standard for v1? It seems like such an obvious thing and most of the work is done, it would seem a shame not to.

Coordinator
Nov 12, 2010 at 12:14 AM

That would be great but time is really short. Can't promise anything at this point. At least if there's a module available for it...

Nov 14, 2010 at 9:44 PM
Edited Nov 14, 2010 at 9:55 PM

This is now working on my live site.

I've used it to create a side panel that includes books relevant to the content of the post. You can see it in action on this post.

Will post the module to the gallery once I get a username.

Coordinator
Nov 15, 2010 at 4:05 AM

Nice!