Override the list of contentItems generated by Taxonomy Term

Topics: Writing modules
Aug 4, 2013 at 5:35 PM
Edited Aug 4, 2013 at 5:36 PM
Hi,

I´m developing an ECommerce website and i'm using taxonomies in order to classify products with categories.
I´m using the Taxonomy Menu Link in order to generate the categories Menu.
I´m facing a problem here, because my client wants the list of products generated when someone hits the category menu link ordered by price or by another field.
I believe the best way i can do this, is to write my own controller actions and querying the content manager and then do the ordering and filtering there, but i´m a bit stuck here on how to achieve this, a here is my question:
  • The generation of the taxonomy term contentITems list is delegated to the TermPartDriver Display method, how can i delegate this to my own controller in my module instead of the TermPartDriver.
Thanks.
Coordinator
Aug 5, 2013 at 1:24 AM
Taxonomy Menu Link is gone from 1.7. Use projections instead, or your own controller action. The code that works in a driver should work fine in a controller with a few modifications. How exactly did it not work when you tried?
Aug 5, 2013 at 1:38 AM
Edited Aug 5, 2013 at 1:40 AM
Hello Bertand, i´m using Orchard 1.7 and the Taxonomy Menu Link is there. I´m also generating a widget menu based on a query that filters by this taxonomy. But my dificulty is about "sending" or "pointing" the link of the taxonomy term to a controller action in my own module instead of the Taxonomies Module TermPart driver.
Coordinator
Aug 5, 2013 at 1:41 AM
You're right, I was confusing it with something else. Still, don't use it in that case, use a projection instead. This way, you can point it at anything you want.
Aug 5, 2013 at 1:44 AM
Edited Aug 5, 2013 at 1:45 AM
Ok, i´ve tryied that also, but i´m not figuring out how to make a query that has a dynamic filtering on the taxonomy term. eg probably query string would work? but how?
Aug 5, 2013 at 1:52 AM
Edited Aug 5, 2013 at 1:59 AM
Sorry i think i misunderstand what you wrote. Do you mean creating a projection with the taxonomomy terms and rewrite the urls to point to my controller action and passing for example a query string parameter with the taxonomy. Or do you mean creating a projection to list my products filtered by the taxonomy term?
Aug 5, 2013 at 2:15 AM
Edited Aug 5, 2013 at 2:18 AM
I'm going for the first one adding to the main menu a query that filters by taxonomy. Which bindings should i use in the query layout in order to rewite the url that will show in the menu, so that i can point it to my controller
eg. /onlinestore/?productCategory={productCategory}
Coordinator
Aug 5, 2013 at 2:24 AM
Yes, I meant a projection with the terms, with a rewrite to point to your controller. Not sure about the details of the rewrite. Try it.
Aug 5, 2013 at 2:28 AM
Edited Aug 5, 2013 at 2:29 AM
Ok i´m going to try that, if i find the solution i´ll post it so that others can benefit from it, if i don´t i´ll keep asking.

Thank´s Bertrand
Aug 6, 2013 at 7:48 PM
Edited Aug 6, 2013 at 7:49 PM
Hello again Bertrand,
I´ve been struggling with this for a while and can´t figure out how to rewrite the taxonomy term url to point somewhere else, in this case my controller.
Even if i created a query layout with the url rewrite (which i can't figure out which tokens to use, i tried every available, even in the bindings) when creating a menu with this query, the menu will ignore the the layout and get the url from the Term autoroute.

Then after googling for a while i found this:

http://weblogs.asp.net/bleroy/archive/2012/02/23/more-than-one-driver-for-a-single-orchard-part.aspx

And since what i need is to have the ability of ordering by a certain criteria the list of of content items that have the taxonomy term (in this case product categories).
I was thinking of creating another driver for Term in my module, and in that new Driver Display method get the ordered list and then adding it to the shape.

I this post you wrote, you mention about creating a new part instead of just creating a new driver, probably it would be better since i only need this type of sorting in this specific taxonomy, a not afecting other taxonomy fields on other content types.

Can you give me some directions? Is this the right way to acomplish this.

Thank´s
Coordinator
Aug 6, 2013 at 7:51 PM
Sounds interesting. Not sure what guidance I can provide however, sorry.
Aug 6, 2013 at 7:59 PM
Edited Aug 6, 2013 at 8:07 PM
Wait, i just remembered one thing, the code on the driver just executes if the shape gets placed in placement.info, right?
that way probably i could just create another driver for the Term in my module and use it or not (depending on the needs) by placing the shape in placement.info.

Is this right? or am i thinking right?
Coordinator
Aug 6, 2013 at 9:06 PM
mmh, yes, that sounds about right.
Aug 7, 2013 at 8:25 PM
Edited Aug 7, 2013 at 8:32 PM
UPDATE:

Yes it sounds and it works like a charm!

The keys to solve this was your blog post "more than one driver per part" and placement.info in the module i´m developing.
With these 2 you can extend a core feature but you also can 'override' an existing feature, getting the best of both worlds.

What I did in my module was:
  • Created a new TermPartDriver.cs file, with a DisplayMethod. In this method i queried the contentManager with the sorting criteria that i wanted and added the resulting list to the shape (by the way, i gave another name to the shape).
  • On the placement. info file i created an entry to match the Term Content Type in this case "ProductCategoryTerm", and in there hide the original core Parts_TermPart and place my new shape Parts_ProductCategoryList.
This way i can "override" the core behaviour just for this Term and the rest is unafected.

After this all doors were open, and then i added code in the shape view to call another controller in order to do other filtering and price ordering.

Thank´s Bertrand, what you revealed in you blog really helped me and also give another perspective on Orchard development.
Coordinator
Aug 7, 2013 at 9:38 PM
Cool.
Feb 25, 2014 at 9:15 AM
Nduarte,

Could you share your code? I'm also looking for a way to use taxonomies to filter products and combine the filters, I'm developing a site for someone who sells wine.

Just like an ecommerce site I want to display all the bottles and use filtering on the left like this:

Color
Red
White

Country
France
Spain
...

So when a user clicks on the radiobutton red and France he should only see the red wines from France.

Kind regards,

Borrie
Feb 25, 2014 at 12:54 PM
Hi borrierulez,

What you´re trying to achieve is a bit more complex than what did, i just wanted to sort by a certain criteria the list of content items associated with a specific taxonomy term. In your case, you want to combine diferent terms, i would do it using controllers, and creating a service that query the content items and do the filtering there.
This is the aproach i would take on this one, but that´s just my opinion.

I hope it helps,

Nuno.
Feb 25, 2014 at 1:04 PM
Nduarte,

Can you post or send me your code that you made for your commerce from the controller and stuf? Maybe I can build from there, I'll post back my code also.

I've seen some people asking also for multi checkbox or radiobutton filtering but I couldn't find an answer anywhere.

Borrie
Feb 25, 2014 at 1:30 PM
borrierulez,

I didn´t build the logic using controllers, i just override the TermPartDriver to query the list of contentitems by a specific ordering criteria. what you are trying to acomplish is more complex and i´m just trying to help you giving ideas/directions based on what i think i would do.

This is what I did.
Below you have the code for the TermPartDriver (you can find in the Taxonomies Module):
        protected override DriverResult Display(TermPart part, string displayType, dynamic shapeHelper) {
            return Combined(
                ContentShape("Parts_TermPart_Feed", () => {
                    
                    // generates a link to the RSS feed for this term
                    _feedManager.Register(part.Name, "rss", new RouteValueDictionary { { "term", part.Id } });
                    return null;
                }),
                ContentShape("Parts_TermPart", () => {
                    var pagerParameters = new PagerParameters();
                    var httpContext = _httpContextAccessor.Current();
                    if (httpContext != null) {
                        pagerParameters.Page = Convert.ToInt32(httpContext.Request.QueryString["page"]);
                    }
                    
                    var pager = new Pager(_siteService.GetSiteSettings(), pagerParameters);
                    var taxonomy = _taxonomyService.GetTaxonomy(part.TaxonomyId);
                    var totalItemCount = _taxonomyService.GetContentItemsCount(part);

                    // asign Taxonomy and Term to the content item shape (Content) in order to provide 
                    // alternates when those content items are displayed when they are listed on a term
                    var termContentItems = _taxonomyService.GetContentItems(part, pager.GetStartIndex(), pager.PageSize)
                        .Select(c => _contentManager.BuildDisplay(c, "Summary").Taxonomy(taxonomy).Term(part));

                    var list = shapeHelper.List();

                    list.AddRange(termContentItems);

                    var pagerShape = shapeHelper.Pager(pager)
                            .TotalItemCount(totalItemCount)
                            .Taxonomy(taxonomy)
                            .Term(part);

                    return shapeHelper.Parts_TermPart(ContentItems: list, Taxonomy: taxonomy, Pager: pagerShape);
                }));
        }
you get the list of content items associated with the term using the _taxonomyService.GetContentItemsCount(part), you just have to change that logic there, creating a service that querys the contentManager with your specific needs (check the TaxonomyService.cs to see how the query is built ).

Nuno.
Feb 25, 2014 at 1:41 PM
Hey man, thanks a lot already!! I've been searching the web (i'm not a hardcore developer :) ) And seems the best way to do this is using Ajax so you can select multiple items without a refresh.

I've also posted it here (the question) if you want to subscribe to it: https://orchard.codeplex.com/discussions/534927

I'll get to work using your code and ajax. This will take me a long time though :)

Borrie
Aug 25, 2014 at 5:43 PM
Hi all,
linked to this article "http://weblogs.asp.net/bleroy/archive/2012/02/23/more-than-one-driver-for-a-single-orchard-part.aspx" I have a question:

How should I override and not add a driver for an existing part? Is it possible?
I am using Orchard 1.7.2 and this is my scenario:
  1. I have a ContentType (Product) with a LocalizationPart and a TaxonomyField (e.g taxonomy: producttype )
  2. producttype taxonomy also have a LocalizationPart in order to have localized URL and different terms based on different taxonomies (e.g. en-US taxomomy: producttype, it-IT: tipoprodotto)
  3. I would like to take terms of a localized taxonomy (based on LocalizationPart of my ContentItem)
  4. So I have overrided the driver for TaxnomyFieldDriver (now it takes desired terms) but the effect was that my field (Fields_TaxonomyField_Edit) is rendered twice
  5. So I tried to use Placement.Info of my custom module in order to prevent rendering Fields_TaxonomyField_Edit but without effects
  6. And I created a new ContentShape Fields_TaxonomyField_Localized_Edit
  <Place Fields_TaxonomyField_Edit="-" Fields_TaxonomyField_Localized_Edit="Content:9" />
In my module manifest I also declared dependency from Orchard.Taxonomy and Orchard.Localization

Someone have Any Idea of why I cannot make it working?
Any alternative seggestion?

Thank you in advance for your support.
Aug 25, 2014 at 8:23 PM
Can you post the driver code and placement.info?
Aug 26, 2014 at 7:34 AM
Hi nduarte,
thank you for your fast answer.
here's my code

My driver (taken from original TaxonomyFieldDriver and modified to use localization)
public class TaxonomyFieldExtensionDriver : ContentFieldDriver<TaxonomyField> {
        private readonly ITaxonomyService _taxonomyService;
        private readonly ILocalizationService _localizationService;
        public IOrchardServices Services { get; set; }


        public TaxonomyFieldExtensionDriver(
            IOrchardServices services,
            ITaxonomyService taxonomyService,
            IRepository<TermContentItem> repository,
            ILocalizationService localizationService) {
            _taxonomyService = taxonomyService;
            Services = services;
            T = NullLocalizer.Instance;
            _localizationService = localizationService;
        }

        public Localizer T { get; set; }

        private static string GetPrefix(ContentField field, ContentPart part) {
            return part.PartDefinition.Name + "." + field.Name;
        }

        private static string GetDifferentiator(TaxonomyField field, ContentPart part) {
            return field.Name;
        }
        protected override DriverResult Display(ContentPart part, TaxonomyField field, string displayType, dynamic shapeHelper) {

            return ContentShape("Fields_TaxonomyField_Localized", GetDifferentiator(field, part),
                () => {
                    var settings = field.PartFieldDefinition.Settings.GetModel<TaxonomyFieldSettings>();
                    var terms = _taxonomyService.GetTermsForContentItem(part.ContentItem.Id, field.Name).ToList();
                    var taxonomy = _taxonomyService.GetTaxonomyByName(settings.Taxonomy);

                    return shapeHelper.Fields_TaxonomyField(
                        ContentField: field,
                        Terms: terms,
                        Settings: settings,
                        Taxonomy: taxonomy);
                });
        }

        protected override DriverResult Editor(ContentPart part, TaxonomyField field, dynamic shapeHelper) {
            return ContentShape("Fields_TaxonomyField_Localized_Edit", GetDifferentiator(field, part), () => {
                var settings = field.PartFieldDefinition.Settings.GetModel<TaxonomyFieldSettings>();
                var taxonomyName = settings.Taxonomy;
                var taxonomyLocalizedName = "";
                var taxonomy = _taxonomyService.GetTaxonomyByName(settings.Taxonomy);
                /*Localized context*/
                if (part.ContentItem.As<LocalizationPart>() != null && part.ContentItem.As<LocalizationPart>().MasterContentItem != null) {
                    taxonomyLocalizedName = _localizationService.GetLocalizedContentItem(taxonomy, part.ContentItem.As<LocalizationPart>().Culture.Culture).As<TitlePart>().Title;
                    taxonomy = _taxonomyService.GetTaxonomyByName(taxonomyLocalizedName);
                }
                var appliedTerms = _taxonomyService.GetTermsForContentItem(part.ContentItem.Id, field.Name).Distinct(new TermPartComparer()).ToDictionary(t => t.Id, t => t);
                var terms = taxonomy != null
                    ? _taxonomyService.GetTerms(taxonomy.Id).Where(t => !string.IsNullOrWhiteSpace(t.Name)).Select(t => t.CreateTermEntry()).ToList()
                    : new List<TermEntry>(0);

                terms.ForEach(t => t.IsChecked = appliedTerms.ContainsKey(t.Id));

                var viewModel = new TaxonomyFieldViewModel {
                    DisplayName = field.DisplayName,
                    Name = field.Name,
                    Terms = terms,
                    Settings = settings,
                    SingleTermId = terms.Where(t => t.IsChecked).Select(t => t.Id).FirstOrDefault(),
                    TaxonomyId = taxonomy != null ? taxonomy.Id : 0
                };

                var templateName = settings.Autocomplete ? "Fields/TaxonomyField.Autocomplete" : "Fields/TaxonomyField";
                return shapeHelper.EditorTemplate(TemplateName: templateName, Model: viewModel, Prefix: GetPrefix(field, part));
            });
        }

        protected override DriverResult Editor(ContentPart part, TaxonomyField field, IUpdateModel updater, dynamic shapeHelper) {
            var viewModel = new TaxonomyFieldViewModel { Terms = new List<TermEntry>() };

            if (updater.TryUpdateModel(viewModel, GetPrefix(field, part), null, null)) {
                /*Localized context*/
                /*Se il contenuto è localizzato arrivano già la tassonomia e i termini corretti e localizzati quindi non devo fare nulla di diverso rispetto al driver originale*/
                var checkedTerms = viewModel.Terms
                    .Where(t => (t.IsChecked || t.Id == viewModel.SingleTermId))
                    .Select(t => GetOrCreateTerm(t, viewModel.TaxonomyId, field))
                    .Where(t => t != null).ToList();

                var settings = field.PartFieldDefinition.Settings.GetModel<TaxonomyFieldSettings>();
                if (settings.Required && !checkedTerms.Any()) {
                    updater.AddModelError(GetPrefix(field, part), T("The field {0} is mandatory.", T(field.DisplayName)));
                } else
                    _taxonomyService.UpdateTerms(part.ContentItem, checkedTerms, field.Name);
            }

            return Editor(part, field, shapeHelper);
        }

        protected override void Exporting(ContentPart part, TaxonomyField field, ExportContentContext context) {
            var appliedTerms = _taxonomyService.GetTermsForContentItem(part.ContentItem.Id, field.Name);

            // stores all content items associated to this field
            var termIdentities = appliedTerms.Select(x => Services.ContentManager.GetItemMetadata(x).Identity.ToString())
                .ToArray();

            context.Element(XmlConvert.EncodeLocalName(field.FieldDefinition.Name + "." + field.Name)).SetAttributeValue("Terms", String.Join(",", termIdentities));
        }

        protected override void Importing(ContentPart part, TaxonomyField field, ImportContentContext context) {
            var termIdentities = context.Attribute(XmlConvert.EncodeLocalName(field.FieldDefinition.Name + "." + field.Name), "Terms");
            if (termIdentities == null) {
                return;
            }

            var terms = termIdentities
                            .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
                            .Select(context.GetItemFromSession)
                            .Where(contentItem => contentItem != null)
                            .ToList();

            _taxonomyService.UpdateTerms(part.ContentItem, terms.Select(x => x.As<TermPart>()), field.Name);
        }

        private TermPart GetOrCreateTerm(TermEntry entry, int taxonomyId, TaxonomyField field) {
            var term = entry.Id > 0 ? _taxonomyService.GetTerm(entry.Id) : default(TermPart);

            if (term == null) {
                var settings = field.PartFieldDefinition.Settings.GetModel<TaxonomyFieldSettings>();

                if (!settings.AllowCustomTerms || !Services.Authorizer.Authorize(Permissions.CreateTerm)) {
                    Services.Notifier.Error(T("You're not allowed to create new terms for this taxonomy"));
                    return null;
                }

                var taxonomy = _taxonomyService.GetTaxonomy(taxonomyId);
                term = _taxonomyService.NewTerm(taxonomy);
                term.Container = taxonomy.ContentItem;
                term.Name = entry.Name.Trim();
                term.Selectable = true;

                _taxonomyService.ProcessPath(term);
                Services.ContentManager.Create(term, VersionOptions.Published);
                Services.Notifier.Information(T("The {0} term has been created.", term.Name));
            }

            return term;
        }
    }

    internal class TermPartComparer : IEqualityComparer<TermPart> {
        public bool Equals(TermPart x, TermPart y) {
            return x.Id.Equals(y.Id);
        }

        public int GetHashCode(TermPart obj) {
            return obj.Id.GetHashCode();
        }
    }
placement.info (the interesting portion of it)
  <Place Fields_TaxonomyField_Edit="-" Fields_TaxonomyField_Localized_Edit="Content:9" />
  <Place Parts_TermPart="-" Parts_TermPart_Localized="Content:5" />
Thank you in advance
Aug 26, 2014 at 3:08 PM
Edited Aug 26, 2014 at 3:09 PM
The problem is in the get method of the editor in your driver (i´m assuming that you have the views in place as well).
There you are returning the template of the original TaxonomyField instead of TaxonomyField_Localized :
                var templateName = settings.Autocomplete ? "Fields/TaxonomyField.Autocomplete" : "Fields/TaxonomyField"; 
                return shapeHelper.EditorTemplate(TemplateName: templateName, Model: viewModel, Prefix: GetPrefix(field, part));
Aug 26, 2014 at 3:25 PM
To use original view is exactly what I want to do. I don't want to override the views too, I want to use original views.
In the meantime I found a solution (less pretty but it works)
I added this two Attrubutes to my driver
    [OrchardFeature("Laser.Orchard.StartupConfig.TaxonomiesExtensions")]
    [OrchardSuppressDependency("Orchard.Taxonomies.Drivers.TaxonomyFieldDriver")]
    public class TaxonomyFieldExtensionDriver : ContentFieldDriver<TaxonomyField> {
        private readonly ITaxonomyService _taxonomyService;
                   ...
                   ...
In this case the original dirver is not fired and my custom driver fires original views.

Is there a better alternative? Is there any negative effect to this solution?
Thank you.
Aug 26, 2014 at 3:38 PM
Well that works too.
In my case i needed to override the views also, and since a driver only gets hit when there is an entry on placement.info, it suited well my needs.
OrchardSuppressDependency is also a documented way to override something in orchard.
About negative aspects, i know none, but probably that answer could be better answered by the core developers.
Aug 28, 2014 at 7:20 AM
Edited Aug 28, 2014 at 7:21 AM
nduarte,
thank you for your advices and your time spent to answer.
Bye.