Implementing SEO friendly URLs in Sitecore-uCommerce - sitecore

I am implementing a default Sitecore-uCommerce SEO friendly URLs mechanics and I run into issues.
I looked into ItemResolver processor of uCommerce and I still do not understand how uCommerce sets the Sitecore Context Item. It seems like it uses an uCommerce Item's Guid for Sitecore.Context.Item. Somehow it is not the case, but I do not see the real Sitecore Item to be set as Context item. And uCommerce items do not have the layout details on them. Or am I wrong?
private ID FindSitecoreIdForProduct(int productId)
{
IRepository<Product> repository = ObjectFactory.Instance.Resolve<IRepository<Product>>();
Product product = repository.Get(productId);
if (product != null)
{
return new ID(product.Guid);
}
return ID.Null;
}
Then it makes
ID iD = this.FindSitecoreIdForProduct(productId);
if (iD == ID.Null)
{
return;
}
Context.Item = Context.Database.GetItem(iD);
I would like it to be a specific Sitecore Item with a rendering showing the product details. The URLs are of type
http://sitename.com/productdetailpage/productname/c-25/p-125
If you could just explain me how uCommerce gets to the real Sitecore Item and sets it as a Context.Item, I guess it will be enough for me.

You are on the right way.
They move the context item to other item. I didn't like how they deal with url, and I need other ProductResolver.
Ucommerce have products in his own db, and they created a dataProvider to bring products into Sitecore.
The Ucommerce products are located in Sitecore under /sitecore/uCommerce/Products .
The shops,category and subcategory are located under /sitecore/uCommerce/Store.
Please check this link to have a clean idea how ucommerce is dealing with url:
http://docs.ucommerce.net/ucommerce/v7.0/sitecore/working-with-nice-urls-in-sitecore.html
I also have the same issue like you and I created a custom ItemResolver.
I defined processor in this way on httpRequestBegin pipeline.
<processor type="NameSpace.ProductResolver, Assembly" patch:instead="processor[#type='UCommerce.Sitecore.Pipelines.ItemResolver, UCommerce.Sitecore']"/>
I created in Sitecore a new template name ProductPage, I create a new item named Product of type ProductPage
My requirements was to have url like : /Shoes/Running/NIKEAIRZOOMPEGASUS33
When you browse to /category/subcategory/productid my productResolver is triggered.
I check if category,subcategory and product exist.
If they exist I set the current category and current product.
SiteContext.Current.CatalogContext.CurrentProduct=current_product;
//you need to check if product is in current category
I set my Context Item to be the Product item
var pathList = args.LocalPath.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries).ToList();
var currentProduct= GetCurrentProduct(pathList);
// in above function I am getting the current product, checking if is correct category and subcategory
if (currentProduct != null)
{
SiteContext.Current.CatalogContext.CurrentProduct = currentProduct;
Sitecore.Context.Item = productItem;
}

Related

How to get item attributes via api?

We are using cloud Dynamics 365 Business Central and trying to get items with all attributes via OData.
In Microsoft documentation we found this endpoint:
api.businesscentral.dynamics.com/v1.0[our tenant id]/Sandbox/ODataV4/Company('CRONUS%20DE')/items
But unfortunately, the response does not contain item attributes and values, such as Farbe, Tiefe, etc.
Next, we tried to add new Web Services. But some of this endpoints return empty values and some of them (7506, 7507, 7508, 7510) don't work and return:
No HTTP resource was found that matches the request URI
Objects 7500, 7501, 7503 contain information about attributes. But non of them (7500 - 7510) does not contain a relation between Item, Attributes, and Values.
Maybe there is another way to get items with their attribute values? We also tried to research microsoft graph but not successful.
i am having similar troubles with this. i find the dynamics api to be exceptionally unintuitive and difficult to use. the furthest i have been able to get has been to go into the api settings for dynamics and uncover the tables for a few item attributes (i believe that the table numbers are as those below:
7500 - Item Attribute
7501 - Item Attribute Value
7502 - Item Attribute Translation
7504 - Item Attribute Value Selection
7505 - Item Attribute Value Mapping
i cannot comment on why 7503 is missing.
using 7500 as an example, when you uncover the table, the system provides a resulting endpoint (unfortunately, they always promote OData, and the outdated SOAP resource; i can't figure out why they have such a vendetta against the simple and easy-to-use REST endpoint).
https://api.businesscentral.dynamics.com/v2.0/<TENANT_ID>/<ENVIRONMENT_NAME>/ODataV4/Company('COMPANY_IDENTIFIER')/ItemAttributes
using this endpoint, you can get a listing of the attribute types themselves (e.g. let's say you've defined an attribute called 'BaseColor', you should get a result here for the name of the attribute 'BaseColor', its ID, its type, etc.),
with the ItemAttributeValues endpoint, you should get the actual attribute values that are in existence (e.g. for some item, you happened to set its 'BaseColor' attribute to 'Blue', you should get a response for this attribute value with a attribute type of 'BaseColor', a value, as 'Blue' along with the entity's ID, etc).
yet, when it comes to any instantiated attribute values for items, i can't figure out how to get the association of the attributes with those items. i expect that the "item attribute value mapping" option would be something along the lines of a item_id - attribute_id pair so that for any item in question, one could query the attributes list with something like a filter. but as you said, upon uncovering some of these elements, their respective endpoints return nothing. you get to the point where you say 'OH...AWSOME! there is a value-item mapping. that makes sense, and i can definitely use that'. a few moments later, the API spits in your face with an error, or craps on you by returning something you don't expect like an empty data set.
this api is a constant uphill battle, and totally riddled with landmines. a complete blow-me-up-pain-in-the-arse.
EDIT: 2021-06-09
i've looked into this some more. i was able to set up an export package for the various tables in question, specifically 7500, 7501, and 7505. the magical table was 7505 as it is the relationship between an attribute value and the item with which it is associated. exporting the package to excel results in good data. yet, when trying to expose this information in the OData resource, something strange happens:
in web services, i try to open up page 7505 which populates the object name as ItemAttributeValueMapping and i set the service name to 'ItemAttributeValueMapping'. This is normal.
the system complains when i fail to specify that the object type is a page. so, i go back in the line and set the selection to "Page"
when i tab through to publish the change, the object name automatically changes to 'ItemAttributeValueTranslations'.
EDIT: 2021-06-15
After a lot of fiddling about, i finally reached a point where i decided that the only way to address this was to write an al query which would expose the appropriate value-item mapping information. there is a page which provides some source code for doing this:
github AL-Code-Samples
to get something out of the API, i had to use microsoft visual studio code. there are a few good videos on how to get this up and running to get a test app working for your business central instance (i used eric hougaar's videos: Getting started with Business Central AL Development).
when you have set up your app to connect to your instance by inserting your tenant and entering your credentials, you can modify the source code as below to create a query in your system.
query 50102 "<YOUR_QUERY_NAME>"
{
QueryType = API;
APIPublisher = '<YOUR_NAME>';
APIGroup = '<YOUR_APP_GROUP>';
APIVersion = 'v1.0';
Caption = '<YOUR CAPTION>';
EntityName = '<YOUR_QUERY_NAME>';
EntitySetName = '<YOUR_API_ENDPOINT_NAME>';
elements
{
dataitem(Item; Item)
{
column(No_; "No.")
{
}
column(Description; Description)
{
}
column(Unit_Cost; "Unit Cost")
{
}
dataitem(Item_Attribute_Value_Mapping; "Item Attribute Value Mapping")
{
DataItemLink = "No." = Item."No.";
column(Item_Attribute_ID; "Item Attribute ID")
{
}
column(Item_Attribute_Value_ID; "Item Attribute Value ID")
{
}
dataitem(QueryElement6; "Item Attribute")
{
DataItemLink = ID = Item_Attribute_Value_Mapping."Item Attribute ID";
column(Name; Name)
{
}
dataItem(Queryelement10; "Item Attribute Value")
{
DataItemLink = "Attribute ID" = Item_Attribute_Value_Mapping."Item Attribute ID",
ID = Item_Attribute_Value_Mapping."Item Attribute Value ID";
column(Value; Value)
{
}
column(Numeric_Value; "Numeric Value")
{
}
}
}
}
}
}
}
once this code gets successfully uploaded to your server and returning a page (you have to wait for it), you can then use specified query number to expose the data in the API by going to business central's "web services" and adding a 'Query' to item 50102 (or whatever number you use). the endpoint will automatically be populated and you can use it to send you back the necessary JSON which will show a product, with its attribute values.
hope that helps.
You should try with below endpoint:
/v2.0/tenant_id/enviornment_name/ODataV4/Company(company_id)/Items

Enrolling Anonymous Users in Engagement Plans

I know that it is possible to enroll users in an engagement plan from with Sitecore by adding them to a specific state in the plan when they visit a campaign URL, adding them when they submit a Web Forms for Marketers Form, and manually adding them in the Supervisor interface.
Additionally, I know that you can use the API to add a user as described here:
http://briancaos.wordpress.com/2013/06/03/programming-for-sitecore-dms-engagement-plans/
However, that method requires a username.
I would like to enroll anonymous users in an engagement plan when they visit any page represented by a particular template in Sitecore (ie, page from the Product template). Is this possible using the API?
To expand on my above comment, and to supplement your own answer, here's a processor that you could add to the after the ItemResolver in the httpRequestBegin pipeline that would achieve the desired result. It is a very basic version that you could embellish as you see fit
class CampaignRedirect
{
public void Process(HttpRequestArgs args)
{
var request = HttpContext.Current.Request;
// must not already have the querystring in the URL
if(request.QueryString["sc_camp"] != null &&
request.QueryString["sc_camp"] != "XXXXXXXX")
return;
// must have a context item
if(Sitecore.Context.Item == null)
return;
var item = Sitecore.Context.Item;
// must be the right template
if(item.TemplateID.ToString() != "{XXXXXXXXX-XXXX-XXXXXX}")
return;
var basicUrl = LinkManager.GetItemUrl(item);
var response = HttpContext.Current.Response;
response.Redirect(basicUrl + "?sc_camp=XXXXXXX");
}
}
If you're not familiar with adding processors, take a look here.
Per Sitecore support, this is not currently possible. However, I was able to achieve what I wanted by adding a jQuery AJAX call to the campaign URL to the sublayout used by the page type in question. Naturally this only works for clients with JS enabled, but for my purposes, that is not an issue.
<script type="text/javascript">$(function() { $.get('/?sc_camp=[campaignid]'); });</script>
Edited 2014-05-19
I found a way to do this via the Sitecore API. This is rough and needs to check for null values, exceptions, etc., but it does work:
string cookieVal = Request.Cookies["SC_ANALYTICS_GLOBAL_COOKIE"].Value;
List<Guid> guids = new List<Guid>() {
new Guid(cookieVal)
};
Guid automationStateId = new Guid("{24963AE9-1C8C-4E18-8EEE-01BC249D1F1B}");
Guid automationId = Sitecore.Context.Database.GetItem(new Sitecore.Data.ID(automationStateId)).ParentID.ToGuid();
Sitecore.Analytics.Automation.Data.AutomationManager.Provider.CreateAutomationStatesFromBulk(guids, automationId, automationStateId);

Set queryable source on Rendering Parameter Template field

I have a Rendering Parameter template applied to a sublayout. It has a single Droptree field on it, and I want to set the Source of that field to a Sitecore query so I can limit the options available for that field.
Source can be:
query:./*
or
query:./ancestor-or-self::*[##templatename='MyTemplate']/
The query just needs to grab items relative to the content item that we're on. This normally works with Droptree fields in the content editor.
However I'm finding that the query isn't working here because we're in the rendering parameters, so it's not using the content item as it's context.
The query fails and I just get the full Sitecore tree.
I found this can be fixed up for the Datasource field with 'Queryable Datasource Locations' at this link:-
http://www.cognifide.com/blogs/sitecore/reduce-multisite-chaos-with-sitecore-queries/
However I don't know where to start to get this working for other rendering parameter fields.
Any ideas? (I'm using Sitecore 6.6 Update 5)
Unfortunately, the pipeline mentioned in Adam Najmanowicz's answer works for some other types, like Droplink and Multilist, but the pipeline isn't run for Droptree fields.
After looking into this deeper I found that the Source of a Droptree field IS using the wrong context item, as Adam mentioned, but the code comes from the Droptree field itself:-
Sitecore.Shell.Applications.ContentEditor.Tree, Sitecore.Kernel
Utilising the query string code from Adam's answer, we can create a 'fixed' Droptree custom field, that is almost the same as the regular Droptree but will use the correct context item instead.
The code will inherit from the normal Tree control, and only change the way that the Source property is set.
public class QueryableTree : Sitecore.Shell.Applications.ContentEditor.Tree
{
// override the Source property from the base class
public new string Source
{
get
{
return StringUtil.GetString(new string[]
{
base.Source // slightly altered from the original
});
}
set
{
Assert.ArgumentNotNull(value, "value");
if (!value.StartsWith("query:", StringComparison.InvariantCulture))
{
base.Source = value; // slightly altered from the original
return;
}
Item item = Client.ContentDatabase.GetItem(this.ItemID);
// Added code that figures out if we're looking at rendering parameters,
// and if so, figures out what the context item actually is.
string url = WebUtil.GetQueryString();
if (!string.IsNullOrWhiteSpace(url) && url.Contains("hdl"))
{
FieldEditorParameters parameters = FieldEditorOptions.Parse(new UrlString(url)).Parameters;
var currentItemId = parameters["contentitem"];
if (!string.IsNullOrEmpty(currentItemId))
{
Sitecore.Data.ItemUri contentItemUri = new Sitecore.Data.ItemUri(currentItemId);
item = Sitecore.Data.Database.GetItem(contentItemUri);
}
}
if (item == null)
{
return;
}
Item item2 = item.Axes.SelectSingleItem(value.Substring("query:".Length));
if (item2 == null)
{
return;
}
base.Source = item2.ID.ToString(); // slightly altered from the original
}
}
The above code is pretty much the same as the Source property on the base Tree field, except that we figure out the proper context item from the URL if we've detected that we're in the rendering parameters dialog.
To create the custom field, you just need to edit the Web.Config file as described here. Then add the custom field to the core database as described here.
This means that parameters can now have queries for their source, allowing us to limit the available items to the content editor. (Useful for multi-site solutions).
The key here would be to set the Field Editor's context to be relative to the item you are editing instead of the Rendering parameters (that I think it has by default).
So you could have processor:
public class ResolveRelativeQuerySource
{
public void Process(GetLookupSourceItemsArgs args)
{
Assert.IsNotNull(args, "args");
if (!args.Source.StartsWith("query:"))
return;
Item contextItem = null;
string url = WebUtil.GetQueryString();
if (!string.IsNullOrWhiteSpace(url) && url.Contains("hdl"))
{
FieldEditorParameters parameters = FieldEditorOptions.Parse(new UrlString(url)).Parameters;
var currentItemId = parameters["contentitem"];
if (!string.IsNullOrEmpty(currentItemId))
{
Sitecore.Data.ItemUri contentItemUri = new Sitecore.Data.ItemUri(currentItemId);
contextItem = Sitecore.Data.Database.GetItem(contentItemUri);
}
}
else
{
contextItem = args.Item;
}
}
}
hooked as:
<sitecore>
<pipelines>
<getLookupSourceItems>
<processor patch:before="*[#type='Sitecore.Pipelines.GetLookupSourceItems.ProcessQuerySource, Sitecore.Kernel']"
type="Cognifide.SiteCore.Logic.Processors.ResolveRelativeQuerySource, Cognifide.SiteCore" />
</getLookupSourceItems>
</pipelines>
</sitecore>
Together with ResolveQueryableDatasources from Przemek's blog this should solve your problem.

How to create a dynamic source for a multilist in Sitecore?

I have the following content tree structure:
Home
Products
Product A
Product B
Organizations
Org 1
Org 2
Org Config X
Org Config Y
Each Organization beneath Organizations has a field called "Associated Products" which is a multilist. This tells the system which Products go with each Organization. The Org Config data template has a field called "Selected Products". When I add a new Org Config content item (which always lives directly beneath an Organization) I would like to be able to restrict the items that are displayed in the "Selected Products" field (which is a multilist) to only display Products that are already associated with the parent Organization. I am thinking there might be a way to do this with Sitecore Query but I can't figure it out. Any ideas?
With the help of Sitecore I figured it out. Basically you have to create a custom control that inherits from MultilistEx. Then you need to override the DoRender() event. Before you call base.DoRender() you must change the source (this.Source) to use a Sitecore query. Previously I was trying to do it in the OnLoad event. So my code now looks like this:
public class CustomMultiList : MultilistEx
{
private void ExcludeItems()
{
...custom code here that builds a list of Item IDs to exclude from the Multilist source...
...list should look like this "##id != 'some guid' and ##id != 'some guid' and so forth...
...you could also build a list of item ids to include. Any Sitecore query will do...
...you can use this.ItemID to get a reference to the current item that is being edited in the Content Editor...
this.Source = "query:" + this.Source + "/*[" + myListOfItemIdsToExclude + "]";
}
protected override void DoRender(output)
{
this.ExcludeItems();
base.DoRender(output);
}
}
I think you'll probably need to create a custom field for this. Here's some articles related to the subject:
http://www.sitecore.net/unitedkingdom/Community/Best-Practice-Blogs/Martin-Knudsen/Posts/2012/09/Creating-a-custom-Sitecore-Field.aspx
http://gettingtoknowsitecore.blogspot.co.uk/2010/03/custom-fields-part-1.html
http://sitecoreblog.blogspot.co.uk/2012_04_01_archive.html

Magento categories navigation

I have a Magento website with the following category structure (CAPITALS letters are CATEGORIES and small letters are products):
ROOT CATEGORY
APPARELS
SHOP BY SIZE
product1
product2
product3
SHOP BY COLLECTION
product4
product5
SHOP BY DESIGN
product6
product7
product8
product9
I want to show my navigation menu as SHOP BY SIZE, SHOP BY COLLECTION and SHOP BY DESIGN. I don't want the navigation to start with APPARELS level. Is there any way to do this?
Note: As per Magento design, ROOT CATEGORY cannot be shown in navigation menu. The navigation menu starts from categories in 2nd level i.e. APPARELS in this case.
Take a look at navigation.php, you could alter the core functionality but by using a module with rewrite (never directly alter a corefile!). I always start there when i need custom navigation functionality.
http://freegento.com/doc/db/d56/_catalog_2_block_2_navigation_8php-source.html
edit, alltough i often use this method, i would advise to avoid rewriting as much as possible, I don't think its possible in this case tough because we are talking about displaying lvl 2 categories as main nav
IF you truly want to use the design Root -> Apparels -> Shop By * you can do this with a single override and modification
config.xml - this is obviously a heavily simplified file, you'll need to provide a helper rewrite for the file.
<?xml version="1.0"?>
<config>
<helpers>
<catalog>
<rewrite>
<category>Namespace_Module_Helper_Catalog_Category</category>
</rewrite>
</catalog>
</helpers>
</config>
Category.php
This assumes you want to use the first child category under your site's root category. In your case it would be "Apparels". This modification takes into account the use of flat or non-flat category tables. There are other options for selecting the ID, one would be a system configuration with the category list as a source thus allowing you to choose your navigations root category directly.
The crux of this file is getting the Parent ID to be the "root category" you want to base the navigation on. Again, for your case the parent ID would be set to that of the "apparels" category.
class Namespace_Module_Helper_Catalog_Category extends Mage_Catalog_Helper_Category {
public function getStoreCategories($sorted=false, $asCollection=false, $toLoad=true)
{
$parent = Mage::app()->getStore()->getRootCategoryId();
$cacheKey = sprintf('%d-%d-%d-%d', $parent, $sorted, $asCollection, $toLoad);
if (isset($this->_storeCategories[$cacheKey])) {
return $this->_storeCategories[$cacheKey];
}
/**
* Check if parent node of the store still exists
*/
$category = Mage::getModel('catalog/category');
/* #var $category Mage_Catalog_Model_Category */
if (!$category->checkId($parent)) {
if ($asCollection) {
return new Varien_Data_Collection();
}
return array();
}
/* Change ian on 1/4/13 at 11:16 AM - Description: Here we capture the id of first child for use as the 'root' */
$category->load($parent);
/** #var $collection Mage_Catalog_Model_Resource_Category_Collection */
$collection = $category->getChildrenCategories();
if (is_array($collection)) {
$category = array_shift($collection); //get the first category in the array. Unknown key.
$parent = $category->getId();
} else {
$parent = $collection->getFirstItem()->getId();
}
$recursionLevel = max(0, (int) Mage::app()->getStore()->getConfig('catalog/navigation/max_depth'));
$storeCategories = $category->getCategories($parent, $recursionLevel, $sorted, $asCollection, $toLoad);
$this->_storeCategories[$cacheKey] = $storeCategories;
return $storeCategories;
}
}