Sunday 10 November 2013

Orchard CMS Taxonomy Term Autoroute Token Slug v2.0

This is a new and improved solution to my original post http://sheltonial.blogspot.com.au/2013/10/orchard-cms-taxonomy-term-autoroute.html.

The purpose of this post is to provide a method of returning a full taxonomy term path slug as a token in Orchard CMS. I am currently using this solution in Orchard 1.7.1 as of changeset 91d0048f2796f093a8cefe2111ff013818d220fb.

The problem with the previous solution was that it only rendered the 1 selected taxonomy term into the token result, which was great if you were working with the root terms, but as soon as you went deeper it did not render the full taxonomy term path into the token result.

The following solution allows a full taxonomy term path to be slugified and rendered into a token result.

using System;
using System.Linq;
using Orchard.Taxonomies.Fields;
using Orchard.Localization;
using Orchard.Tokens;

namespace Orchard.Taxonomies.Tokens {
    public class TaxonomyTokens : ITokenProvider {

        public TaxonomyTokens() {
            T = NullLocalizer.Instance;
        }

        public Localizer T { get; set; }

        public void Describe(DescribeContext context) {
            // Usage:
            // Content.Fields.Article.Categories.Terms -> 'Science, Sports, Arts'
            // Content.Fields.Article.Categories.Terms:0 -> 'Science'

            // When used with an indexer, it can be chained with Content tokens
            // Content.Fields.Article.Categories.Terms:0.DisplayUrl -> http://...

            context.For("TaxonomyField", T("Taxonomy Field"), T("Tokens for Taxonomy Fields"))
                   .Token("Terms", T("Terms"), T("The terms (Content) associated with field."))
                   .Token("Terms[:*]", T("Terms"), T("A term by its index. Can be chained with Content tokens."))
                   .Token("TermPathSlug", T("Terms"), T("Selected taxonomy term full path slugified."))
                   ;
        }

        public void Evaluate(EvaluateContext context) {
            //context.For<TaxonomyField>("TaxonomyField").Data.TermsField.Value.ElementAt(0).Slug

            context.For<TaxonomyField>("TaxonomyField")
                   .Token("Terms", field => String.Join(", ", field.Terms.Select(t => t.Name).ToArray()))
                   .Token(
                       token => token.StartsWith("Terms:", StringComparison.OrdinalIgnoreCase) ? token.Substring("Terms:".Length) : null,
                       (token, t) => {
                           var index = Convert.ToInt32(token);
                           return index + 1 > t.Terms.Count() ? null : t.Terms.ElementAt(index).Name;
                       })
                   .Token("TermPathSlug", field => context.For<TaxonomyField>("TaxonomyField").Data.TermsField.Value.ElementAt(0).Slug)
                // todo: extend Chain() in order to accept a filter like in Token() so that we can chain on an expression
                   .Chain("Terms:0", "Content", t => t.Terms.ElementAt(0))
                   .Chain("Terms:1", "Content", t => t.Terms.ElementAt(1))
                   .Chain("Terms:2", "Content", t => t.Terms.ElementAt(2))
                   .Chain("Terms:3", "Content", t => t.Terms.ElementAt(3))
                   ;
        }
    }
}

An example of an autoroute pattern that works is {Content.Fields.BlogPost.Category.TermPathSlug}, where [BlogPost] is the name of your content type, and [Category] is the name of the field within our content type, not the taxonomy or term.

This could be used is reflecting a taxonomy term hierarchy within a URL using an AutoRoute pattern.

Please be aware that this solution grabs the first selected taxonomy term, so your taxonomy field should be configured so that only 1 term can be selected.

Refer to https://orchard.codeplex.com/workitem/20215 regarding updates on the Orchard work request relating to this.

Hope this helps! Any questions or if you have an improved solution let me know.

Wednesday 16 October 2013

Orchard CMS Taxonomy Term Autoroute Token Slug

Edit 17/10/2013:
Refer to https://orchard.codeplex.com/workitem/20215 on updates regarding this feature.

Edit 11/11/2013:
Since writing this article I've discovered a better method which allows a full taxonomy term path to be slugified and returned into a token result: http://sheltonial.blogspot.com.au/2013/11/orchard-cms-taxonomy-term-autoroute.html

Recently (as of 17/10/2013) the Orchard CMS team has fixed a bug which was preventing taxonomy tokens from returning a value.

Currently I am working off changeset a62f0281ebdac780a1d3a583dbc5821ca8fbc6ce on version 1.7.1 and can confirm that this is working.

An example of an autoroute pattern that works is {Content.Fields.BlogPost.Category.Terms:0}, where [BlogPost] is the name of your content type, and [Category] is the name of the field within our content type, not the taxonomy or term.

This means we can start injecting taxonomy terms into our autoroute patterns and enjoy structuring a url hierarchy based on taxonomy terms and being able to interact with this new url data within queries and layer rules. This is fine if your taxonomy is a single word containing no spaces or special characters, but once you introduce other characters these are also injected into the URL.

The following code adds a new token called TermSlug which allows a taxonomy term to be slugified:

  • File to modify: \src\Orchard.Web\Modules\Orchard.Taxonomies\Tokens\TaxonomyTokens.cs
  • Usage: {Content.Fields.BlogPost.Category.TermsSlug:0} (refer to example above)
// Comment
using System;
using System.Linq;
using Orchard.Taxonomies.Fields;
using Orchard.Localization;
using Orchard.Tokens;
using Orchard.Autoroute.Services;

namespace Orchard.Taxonomies.Tokens {
    public class TaxonomyTokens : ITokenProvider {
        private readonly ISlugService _slugService;

        public TaxonomyTokens(ISlugService slugService)
        {
            T = NullLocalizer.Instance;
            _slugService = slugService;
        }

        public Localizer T { get; set; }

        public void Describe(DescribeContext context) {
            // Usage:
            // Content.Fields.Article.Categories.Terms -> 'Science, Sports, Arts'
            // Content.Fields.Article.Categories.Terms:0 -> 'Science'

            // When used with an indexer, it can be chained with Content tokens
            // Content.Fields.Article.Categories.Terms:0.DisplayUrl -> http://...

            context.For("TaxonomyField", T("Taxonomy Field"), T("Tokens for Taxonomy Fields"))
                   .Token("Terms", T("Terms"), T("The terms (Content) associated with field."))
                   .Token("Terms[:*]", T("Terms"), T("A term by its index. Can be chained with Content tokens."))
                   .Token("TermsSlug[:*]", T("Terms"), T("A term slug by its index. Can be chained with Content tokens."))
                   ;
        }

        public void Evaluate(EvaluateContext context) {

            context.For("TaxonomyField")
                   .Token("Terms", field => String.Join(", ", field.Terms.Select(t => t.Name).ToArray()))
                   .Token(
                       token => token.StartsWith("Terms:", StringComparison.OrdinalIgnoreCase) ? token.Substring("Terms:".Length) : null,
                       (token, t) => {
                           var index = Convert.ToInt32(token);
                           return index + 1 > t.Terms.Count() ? null : t.Terms.ElementAt(index).Name;
                       })
                    .Token(
                       token => token.StartsWith("TermsSlug:", StringComparison.OrdinalIgnoreCase) ? token.Substring("TermsSlug:".Length) : null,
                       (token, t) =>
                       {
                           var index = Convert.ToInt32(token);
                           return index + 1 > t.Terms.Count() ? null :  _slugService.Slugify(t.Terms.ElementAt(index).Name);
                       })
                // todo: extend Chain() in order to accept a filter like in Token() so that we can chain on an expression
                   .Chain("Terms:0", "Content", t => t.Terms.ElementAt(0))
                   .Chain("Terms:1", "Content", t => t.Terms.ElementAt(1))
                   .Chain("Terms:2", "Content", t => t.Terms.ElementAt(2))
                   .Chain("Terms:3", "Content", t => t.Terms.ElementAt(3))
                   ;
        }
    }
}

Hope this helps!