How to get media - twig

I'm trying to access the product.media property inside the file /component/product/card/box-standard.html.twig with the following code:
{{ dump(product) }}
But the product.media returns the value null.
The cover image is available as expected with product.cover.media, only the media property has no result.
Do I have to enable it somehow in this twig file that the media is loaded? The goal is to create an image carousel in the image category page to show multiple images, not only the cover image.

The media association is not available in product listings by default, probably because only the cover image is needed and loading unnecessary things degrades performance.
In your case you need to load that field.
It is possible with the following approach:
Create a new subscriber for the ProductListingCriteriaEvent
Add the media association
The subscriber would look similar to this:
class AddFieldsToProductListingSubscriber implements EventSubscriberInterface {
public static function getSubscribedEvents(): array
{
return [
ProductListingCriteriaEvent::class => 'handleRequest',
];
}
public function handleRequest(ProductListingCriteriaEvent $event): void
{
$event
->getCriteria()
->addAssociation('media');
}
}
Now
{{ dump(product.media) }}
will no contain the product media.

Related

What information can be returned to the DotNetNuke ModuleSearchBase via an API Controller?

We currently have the following code on our custom DNN module:
public class FeatureController : ModuleSearchBase
{
public CommonDataDefinitions.Products.WebProductDetails ProductDetails { get; set; } = null;
public override IList<SearchDocument> GetModifiedSearchDocuments(ModuleInfo moduleInfo, DateTime beginDateUtc)
{
var searchDocuments = new List<SearchDocument>
{
WHAT CAN I RETURN HERE?
};
return searchDocuments;
throw new NotImplementedException();
}
}
Our Detailed Product View module retrieves the following information depending on the SKU in a query string on load using a web API Controller.
Product.Title
Product.Description
Product.Image
Product.Price
Product.DetailedDescription
Product.StockCode Product.MetaTitle
Product.MetaKeywords
Product.MetaDescription
The SearchModulebase code will be in the FeatureController class.
This page will be loaded each time someone looks at a product in detail when they navigate from the Product Filter Module.
1. Since the module will be loaded each time when someone clicks on a particular product. How do you run this code only once and return all the products from the API Controller? Do we need to create an Object which will retrieve everything?
2. How do you prevent the module from becoming slow when all the products have to be retrieved on the on load event?
3. Which SearchDocument information can be returned for the DNN Crawler to index?
4. When the DNN Crawler reads the Feature Controller code, how do you initialize your API Controller to go and fetch and Populate the results to be indexed?

How to access document attachments from hierarchical transformation

My site has this structure:
Products
category 1
item 1
item 1 attachments
category 2
item 2
item 2 attachments
I have successfully written a hierarchical transformation that shows the data on the page at the top level. I cannot for the life of me figure out how to access the attachments for each document though.
Anyone have any idea?
If you are talking about Group attachments (this is when you have a field in your page type using the Attachments data type) then in order to access the attachments inside your transformation you need to write either a custom macro (when using Text/XML transformation) or custom transformation method. Both can be done very easily. The code itself that gets you the attachments can be like this:
public ObjectQuery<AttachmentInfo> GetAttachmentsFromField(string className, int documentID, string attachmentColumnName)
{
// get class info
var classInfo = new FormInfo(DataClassInfoProvider.GetDataClassInfo(className).ClassFormDefinition);
if (classInfo != null)
{
// get attachment field definition
var attachmentsField = classInfo.GetFormField(attachmentColumnName);
if (attachmentsField != null)
{
// get attachments strored in the field by GUID
var attachments = AttachmentInfoProvider.GetAttachments()
.WhereEquals("3CCC6E6C-56F3-42EB-8385-979973D99C55", attachmentsField.Guid)
.And()
.WhereEquals("AttachmentDocumentID", documentID);
return attachments;
}
}
return null;
}
With this it is very important to take into account that this code introduces several other SQL queries against database and therefore it should be optimized by using caching appropriately.
I suppose you mean unsorted attachments you add in Properties -> Attachments section. In this case yuo can register following control in your transformation:
<%# Register Src="~/CMSInlineControls/DocumentAttachments.ascx" TagName="DocumentAttachments" TagPrefix="cms" %>
And use it like this:
<cms:DocumentAttachments ID="ucDocAttachments" runat="server" TransformationName="cms.root.attachment" Path='<%# Eval("NodeAliasPath") %>' />
I wrote a pretty detailed blog post on this a while back. It describes very similar attributes to Enn's answer but gives great detail on why you do specific things.

How to create layout elements in Orchard 1.9

Can someone please guide me on how to create layout elements in Orchard 1.9. I couldn't find any resource online.
In general, creating a new layout element is similar to creating a new part. There is a driver and a few views involved in the process. To the point - you need to implement as follows:
An element class.. Class that inherits from Element, which contains all the element data. A model, so to speak.
A driver. Class that inherits from ElementDriver<TElement>, where TElement is the type you created above. Each element has it's own driver that handles displaying admin editor (and the postback) and frontend display views.
Shapes. All shapes should be placed under /Views/Elements/ folder, by convention.
Display shape. Named after your element, ie. MyElement.cshtml. This one renders your element on frontend.
Design display shape.. Named after your element, with .Design suffix, ie. MyElement.Design.cshtml. This one renders your element inside the layout editor.
Editor shape.. This one should be put in /Views/EditorTemplates/ folder instead. Default naming convention is Elements.MyElement.cshtml. It renders the editor shown when you drop a new element on layout editor canvas.
With all above done, your new element should appear in the list of elements on the right side of the layout editor, ready to use.
If you want to do some more complex elements, please check the existing implementations. Layouts module has a very decent architecture so you should get up to speed pretty quickly. Just keep in mind the necessary steps I wrote above.
To create a custom layout element first create a class that inherits from Element. Element is found in the Orchard.Layouts namespace so you need to add a reference. To follow Orchard standards put this file in a folder called Elements.
public class MyElement : Element
{
public override string Category
{
get { return "Content"; }
}
public string MyCustomProperty
{
get { return this.Retrieve(x => x.MyCustomProperty); }
set { this.Store(x => x.MyCustomProperty, value); }
}
}
Next, create a driver class in a folder called Drivers. This class inherits from ElementDriver<TElement> and likely you will want to override the OnBuildEditor and OnDisplaying methods. OnBuildEditor is used for handling creating our editors shape and updating our database when the editor is saved. OnDisplaying is used when we need to do things when displaying our element. Oftentimes, you will want to add properties to the shape which can be done with context.ElementShape.MyAdditionalProperty = "My Value";
public class MyElementDriver : ElementDriver<MyElement>
{
protected override EditorResult OnBuildEditor(MyElement element, ElementEditorContext context)
{
var viewModel = new MyElementEditorViewModel
{
MyCustomProperty = element.MyCustomProperty
};
var editor = context.ShapeFactory.EditorTemplate(TemplateName: "Elements.MyElement", Model: viewModel);
if (context.Updater != null)
{
context.Updater.TryUpdateModel(viewModel, context.Prefix, null, null);
element.MyCustomProperty = viewModel.MyCustomProperty;
}
return Editor(context, editor);
}
protected override void OnDisplaying(Reddit element, ElementDisplayContext context)
{
context.ElementShape.MyAdditionalProperty = "My Value";
}
}
We then just need our views. Our editor view goes into Views/EditorTemplates. The file name needs to be what we set the template name of the editor shape. In our case the view name will be Elements.MyElement.cshtml.
#model MyNameSpace.ViewModels.MyElementEditorViewModel
<fieldset>
<div>
#Html.LabelFor(m => m.MyCustomProperty, T("My Custom Property"))
#Html.TextBoxFor(m => m.MyCustomProperty, new { #class = "text medium" })
</div>
</fieldset>
Finally, we just need a view for our frontend. This view goes into the following folder Views/Elements. The name of the view file is the same as our element class name. For this example the file would be called MyElement.cshtml.
#using MyNameSpace.Elements
#using MyNameSpace.Models
#{
var element = (MyElement)Model.Element;
}
<h1>#element.MyCustomProperty</h1>
You will then have a new element that you can drag into your layout with the layout editor.
For more details on creating an element from start to finish check out my blog post on creating a Reddit element.

Orchard CMS front-end all possible content filtering by user permissions

Good day!
In my Orchard, I have several content types all with my custom part attached. This part defines to what users this content is available. For each logged user there is external service, which defines what content user can or cannot access. Now I need access restriction to apply everywhere where orchard display content lists, this includes results by specific tag from a tag cloud, or results listed from Taxonomy term. I seems can’t find any good way to do it except modifying TaxonomyServices code as well as TagCloud services, to join also my part and filter by it. Is this indeed the only way to do it or there are other solutions? I would like to avoid doing changes to built-in modules if possible but cannot find other way.
Thanks in advance.
I'm currently bumbling around with the same issue. One way I'm currently looking at is to hook into the content manager.
[OrchardSuppressDependency("Orchard.ContentManagement.DefaultContentManager")]
public class ModContentManager : DefaultContentManager, IContentManager
{
//private readonly Lazy<IShapeFactory> _shapeFactory;
private readonly IModAuthContext _modAuthContext;
public ModContentManager(IComponentContext context,
IRepository<ContentTypeRecord> contentTypeRepository,
IRepository<ContentItemRecord> contentItemRepository,
IRepository<ContentItemVersionRecord> contentItemVersionRepository,
IContentDefinitionManager contentDefinitionManager,
ICacheManager cacheManager,
Func<IContentManagerSession> contentManagerSession,
Lazy<IContentDisplay> contentDisplay,
Lazy<ISessionLocator> sessionLocator,
Lazy<IEnumerable<IContentHandler>> handlers,
Lazy<IEnumerable<IIdentityResolverSelector>> identityResolverSelectors,
Lazy<IEnumerable<ISqlStatementProvider>> sqlStatementProviders,
ShellSettings shellSettings,
ISignals signals,
//Lazy<IShapeFactory> shapeFactory,
IModAuthContext modAuthContext)
: base(context,
contentTypeRepository,
contentItemRepository,
contentItemVersionRepository,
contentDefinitionManager,
cacheManager,
contentManagerSession,
contentDisplay,
sessionLocator,
handlers,
identityResolverSelectors,
sqlStatementProviders,
shellSettings,
signals) {
//_shapeFactory = shapeFactory;
_modAuthContext = modAuthContext;
}
public new dynamic BuildDisplay(IContent content, string displayType = "", string groupId = "") {
// So you could do something like...
// var myPart = content.As<MyAuthoPart>();
// if(!myPart.IsUserAuthorized)...
// then display something else or display nothing (I think returning null works for this but
//don't quote me on that. Can always return a random empty shape)
// else return base.BuildDisplay(content, displayType, groupId);
// ever want to display a shape based on the name...
//dynamic shapes = _shapeFactory.Value;
}
}
}
Could also hook into the IAuthorizationServiceEventHandler, which is activated before in the main ItemController and do a check to see if you are rendering a projection or taxonomy list set a value to tell your content manager to perform checks else just let them through. Might help :)

Orchard CMS: Do I have to add a new layer for each page when the specific content for each page is spread in different columns?

Lets say I want a different main image for each page, situated above the page title. Also, I need to place page specific images in the left bar, and page specific text in the right bar. In the right and left bars, I also want layer specific content.
I can't see how I can achieve this without creating a layer for each and every page in the site, but then I end up with a glut of layers that only serve one page which seems too complex.
What am I missing?
If there is a way of doing this using Content parts, it would be great if you can point me at tutorials, blogs, videos to help get my head round the issue.
NOTE:
Sitefinity does this sort of thing well, but I find Orchard much simpler for creating module, as well as the fact that it is MVC which I find much easier.
Orchard is free, I understand (and appreciate) that. Just hoping that as the product evolves this kind of thing will be easier?
In other words, I'm hoping for the best of all worlds...
There is a feature in the works for 1.5 to make that easier, but in the meantime, you can already get this to work quite easily with just a little bit of code. You should first add the fields that you need to your content type. Then, you are going to send them to top-level layout zones using placement. Out of the box, placement only targets local content zones, but this is what we can work around with a bit of code by Pete Hurst, a.k.a. randompete. Here's the code:
ZoneProxyBehavior.cs:
=====================
using System;
using System.Collections.Generic;
using System.Linq;
using ClaySharp;
using ClaySharp.Behaviors;
using Orchard.Environment.Extensions;
namespace Downplay.Origami.ZoneProxy.Shapes {
[OrchardFeature("Downplay.Origami.ZoneProxy")]
public class ZoneProxyBehavior : ClayBehavior {
public IDictionary<string, Func<dynamic>> Proxies { get; set; }
public ZoneProxyBehavior(IDictionary<string, Func<dynamic>> proxies) {
Proxies = proxies;
}
public override object GetMember(Func<object> proceed, object self, string name) {
if (name == "Zones") {
return ClayActivator.CreateInstance(new IClayBehavior[] {
new InterfaceProxyBehavior(),
new ZonesProxyBehavior(()=>proceed(), Proxies, self)
});
}
// Otherwise proceed to other behaviours, including the original ZoneHoldingBehavior
return proceed();
}
public class ZonesProxyBehavior : ClayBehavior {
private readonly Func<dynamic> _zonesActivator;
private readonly IDictionary<string, Func<dynamic>> _proxies;
private object _parent;
public ZonesProxyBehavior(Func<dynamic> zonesActivator, IDictionary<string, Func<dynamic>> proxies, object self) {
_zonesActivator = zonesActivator;
_proxies = proxies;
_parent = self;
}
public override object GetIndex(Func<object> proceed, object self, IEnumerable<object> keys) {
var keyList = keys.ToList();
var count = keyList.Count();
if (count == 1) {
// Here's the new bit
var key = System.Convert.ToString(keyList.Single());
// Check for the proxy symbol
if (key.Contains("#")) {
// Find the proxy!
var split = key.Split('#');
// Access the proxy shape
return _proxies[split[0]]()
// Find the right zone on it
.Zones[split[1]];
}
// Otherwise, defer to the ZonesBehavior activator, which we made available
// This will always return a ZoneOnDemandBehavior for the local shape
return _zonesActivator()[key];
}
return proceed();
}
public override object GetMember(Func<object> proceed, object self, string name) {
// This is rarely called (shape.Zones.ZoneName - normally you'd just use shape.ZoneName)
// But we can handle it easily also by deference to the ZonesBehavior activator
return _zonesActivator()[name];
}
}
}
}
And:
ZoneShapes.cs:
==============
using System;
using System.Collections.Generic;
using Orchard.DisplayManagement.Descriptors;
using Orchard;
using Orchard.Environment.Extensions;
namespace Downplay.Origami.ZoneProxy.Shapes {
[OrchardFeature("Downplay.Origami.ZoneProxy")]
public class ZoneShapes : IShapeTableProvider {
private readonly IWorkContextAccessor _workContextAccessor;
public ZoneShapes(IWorkContextAccessor workContextAccessor) {
_workContextAccessor = workContextAccessor;
}
public void Discover(ShapeTableBuilder builder) {
builder.Describe("Content")
.OnCreating(creating => creating.Behaviors.Add(
new ZoneProxyBehavior(
new Dictionary<string, Func<dynamic>> { { "Layout", () => _workContextAccessor.GetContext().Layout } })));
}
}
}
With this, you will be able to address top-level layout zones using Layout# in front of the zone name you want to address, for example Layout#BeforeContent:1.
ADDENDUM:
I have used Bertrand Le Roy's code (make that Pete Hurst's code) and created a module with it, then added 3 content parts that are all copies of the bodypart in Core/Common.
In the same module I have created a ContentType and added my three custom ContentParts to it, plus autoroute and bodypart and tags, etc, everything to make it just like the Orchard Pages ContentType, only with more Parts, each with their own shape.
I have called my ContentType a View.
So you can now create pages for your site using Views. You then use the ZoneProxy to shunt the custom ContentPart shapes (Parts_MainImage, Parts_RightContent, Parts_LeftContent) into whatever Zones I need them in. And job done.
Not quite Sitefinity, but as Bill would say, Good enough.
The reason you have to create your own ContentParts that copy BodyPart instead of just using a TextField, is that all TextFields have the same Shape, so if you use ZoneProxy to place them, they all end up in the same Zone. Ie, you build the custom ContentParts JUST so that you get the Shapes. Cos it is the shapes that you place with the ZoneProxy code.
Once I have tested this, I will upload it as a module onto the Orchard Gallery. It will be called Wingspan.Views.
I am away on holiday until 12th June 2012, so don't expect it before the end of the month.
But essentially, with Pete Hurst's code, that is how I have solved my problem.
EDIT:
I could have got the same results by just creating the three content parts (LeftContent, RightContent, MainImage, etc), or whatever content parts are needed, and then adding them to the Page content type.
That way, you only add what is needed.
However, there is some advantage in having a standard ContentType that can be just used out of the box.
Using placement (Placement.info file) you could use the MainImage content part for a footer, for example. Ie, the names should probably be part 1, part 2, etc.
None of this would be necessary if there was a way of giving the shape produced by the TextField a custom name. That way, you could add as may TextFields as you liked, and then place them using the ZoneProxy code. I'm not sure if this would be possible.

Resources