How to configure EXM to create new messages with a custom language version, as default - sitecore

I am trying to configure the EXM root for an Austrian website to create the new messages with a de-AT language version and to have the de-AT language selected as default.
My question is: How can I configure EXM to automatically create a language version for de-AT when a new message is created?
What I've done so far ..
I managed to achieve having the de-AT selected automatically by playing around with the Language - Select the target language field from the Message Context section of the Standard fields - but the actual message item that is created does not contain the de-AT version - and I am getting an error when trying to save the message.
Error message: Edited language version 'German (Austria)' could not be found. It may have been deleted by another user.
As it can be seen in this screnshot, when I open EXM and I create a new message, the de-AT language version is automatically selected. The problem is that the message has no de-AT language version assigned, so it won't allow to save the item.

I think you missed to add language version to standard values of your message template.
Templates in EXM works in a same way as anywhere in Sitecore. You should have language versions for your emails under:
/sitecore/templates/Email Campaign/Messages
/sitecore/templates/Branches/Email Campaign/Messages

None of the 'tricks' worked to automatically add a new language version when you create a new message inside EXM, therefore I've added a new OnItemSave event which checks if the Item is derived from the base Message template and creates a new language version - based on the own business logic.
Config:
<configuration xmlns:x="http://www.sitecore.net/xmlconfig/">
<sitecore>
<events>
<event name="item:added">
<handler type="ABC.SitecoreExtensions.Handlers.EmailExperienceExtensions, ABC" method="OnItemAdded" />
</event>
</events>
</sitecore>
Code
namespace ABC.SitecoreExtensions.Handlers
{
class EmailExperienceExtensions
{
readonly Sitecore.Data.Database masterDb = Sitecore.Configuration.Factory.GetDatabase("master");
private const string EXM_BASE_EMAIL_TEMPLATE_ID = "{A0EA9681-5C86-43AB-80F7-C522DADF6F12}";
public void OnItemAdded(object sender, EventArgs args)
{
Assert.ArgumentNotNull((object)args, "args");
Item obj1 = Event.ExtractParameter(args, 0) as Item;
if (obj1 == null)
return;
if (obj1.IsDerived(new Sitecore.Data.ID(EXM_BASE_EMAIL_TEMPLATE_ID )))
{
//logic to determine the context site and to pickup the language
....
if (rootItem == null)
{
return;
}
var siteContext = SiteContext.GetSite(rootItem.Name);
var lang = LanguageManager.GetLanguage(siteContext.Language);
Item ca = masterDb.GetItem(obj1.Paths.FullPath, lang);
using (new Sitecore.SecurityModel.SecurityDisabler())
{
try
{
if (0 == ca.Versions.Count)
{
ca.Versions.AddVersion();
}
}
catch (Exception ex)
{
// catch exception
}
}
}
}
}
}

Related

Sitecore goals for Media Item don't affect engagement value

I use Sitecore 7.2 revision 140526 + DMS. I'm trying to assign goal to the media item (PDF). When I try to download this media item the goal get registered in the PageEvents table but Engagement Value of the visit does not change. I found following code inside StartTrackingProcessor:
Tracker.CurrentPage.OnCancel += (EventHandler)((obj, args) =>
{
AcceptChangesArgs acceptChangesArgs = args as AcceptChangesArgs;
if (acceptChangesArgs == null)
return;
VisitorDataSet.VisitsRow currentVisit = acceptChangesArgs.Visitor.CurrentVisit;
if (currentVisit == null)
return;
Tracker.CurrentPage.RollBackValue(pageEventData, currentVisit);
});
This code prevents engagement value from increasing for the pages that have been cancelled somewhere in the pipeline.
And then I found the code that cancel the page in the Sitecore.Analytics.RobotDetection.Media.MediaRequestEventHandler whech is enabled by Sitecore.Analytics.RobotDetection.config:
using (new ContextItemSwitcher(obj))
{
try
{
this.StartTracking();
VisitorDataSet.PagesRow previousPage = Tracker.CurrentVisit.PreviousPage;
if (previousPage != null)
{
Guid pageId = previousPage.PageId;
foreach (VisitorDataSet.PageEventsRow pageEventsRow in Enumerable.ToArray<VisitorDataSet.PageEventsRow>(Tracker.CurrentPage.PageEvents))
pageEventsRow.PageId = pageId;
Tracker.CurrentPage.Cancel();
}
this.EndTracking();
}
catch (Exception ex)
{
Log.Error("Media request analytics failed", ex, this.GetType());
}
}
So this MediaRequestEventHandler cancels the page and therefore engagement value does not increase. I can override this behavior or disable RobotDetection.config but I want to understand the consequences and why it was done this way and I know there are recommendations not to disable Sitecore.Analytics.RobotDetection.config
So my question: what is the best way to assign points to the visit for media item download?
For those who might come across the same problem here is the response from sitecore support. This behavior was reported as a bug and as a workaround they suggested to comment out following lines in Sitecore.Analytics.RobotDetection.config:
<event name="media:request">
<handler patch:instead="*[#type='Sitecore.Analytics.Media.MediaRequestEventHandler, Sitecore.Analytics']" type="Sitecore.Analytics.RobotDetection.Media.MediaRequestEventHandler, Sitecore.Analytics.RobotDetection" method="OnMediaRequest"/>
</event>

Sitecore 8: Sync a bucket item on save

I have seen how we can provide default conditions and default actions to newly created bucket items. I also know that we can create a custom rule for building path based on custom date field.
But, how can we set the item path when the date field is and the is saved.
Consider an example. We have a bucket item template say "News" which has a date field say "Release Date". We have the settings where on item creation, the item path has the creation date like "/News/2015/09/16/item1". Now, we need to have some logic where we can change the path when the "release date" of "item1" is updated and the item is Saved.
How can we update the item path when item's release date is updated and item is Saved !! do i need to implement some logic in OnItemSaved() method ?
I already went through posts on GeekHive
The simplest way to do this would be to hook into the item:saved event and sync the bucket in there. The following code is untested:
public class ItemSavedEventHandler
{
public void Execute(object sender, ItemSavedEventArgs args)
{
Assert.IsNotNull(sender, "sender is null");
Assert.IsNotNull(args, "args is null");
// WARNING: using the events disabler is not recommended in this case.
// If you do this, the path of the item in question will not be updated and will cause issues when you edit the page and try to specify rendering data sources (the locations won't resolve)
using (new EventsDisabler())
{
var parameter = args.Item;
if (!BucketManager.IsItemContainedWithinBucket(paremeter))
{
return;
}
var bucketItem = parameter.GetParentBucketItemOrParent();
if (!bucketItem.IsABucket())
{
return;
}
BucketManager.Sync(bucketItem);
}
}
}
On a bucket with a lot of items, this will considerably slow down the save process tho.
If I understood you right, you want your bucket item path to be based on date updated rather than created? Am I right with that?
If yes, that it is not going to be a straightforward thing to do. I see the following approach to implement that.
Configure your bucket to be organised by update date, not created (you mentioned you already know how to configure that behavior). Every Sitecore item derived from Standard Template should have Statistics section where is __Updated field (with two underscores a the beginning) that automatically updates on each item save by corresponding event. You should use that field.
Once done, sync all existing items to apply that bucketing items paths.
Handle item:saved event
Within item:saved event handler: unbucket that particular item and re-bucket that item again (with item:unbucket and item:bucket commands)
Your that particular item will be bucketed implementing your bucketing path rule.
Hope that helps!
Building on some of the answers, here's the most readable / performant solution for most use cases:
using Sitecore.Buckets.Extensions;
using Sitecore.Buckets.Managers;
using Sitecore.Data.Events;
using Sitecore.Diagnostics;
using System;
using Sitecore.Data.Items;
using Sitecore.Events;
namespace XXXX.Project.Web.Infrastructure.Pipelines
{
public class MoveItemIntoBucketOnSave
{
public void OnItemSaved(object sender, EventArgs args)
{
Assert.IsNotNull(sender, "sender is null");
Assert.IsNotNull(args, "args is null");
var savedItem = Event.ExtractParameter(args, 0) as Item;
if (savedItem == null || savedItem.Database.Name.ToLower() != "master" || !savedItem.IsItemBucketable())
{
return;
}
// WARNING: see update below about EventDisabler
using (new EventDisabler())
{
if (!BucketManager.IsItemContainedWithinBucket(savedItem))
{
return;
}
var bucketItem = savedItem.GetParentBucketItemOrParent();
if (!bucketItem.IsABucket())
{
return;
}
// If you want to sync the entire bucket
// BucketManager.Sync(bucketItem);
BucketManager.MoveItemIntoBucket(savedItem, bucketItem);
}
}
}
}
I'm not worried about there being any empty bucket folders after this operation since they will be cleaned up during a full bucket sync, and content authors would not typically be traversing the bucket tree as they should be using search.
Here's the config:
<?xml version="1.0"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
<sitecore>
<events>
<event name="item:saved">
<handler type="XXXX.Project.Web.Infrastructure.Pipelines.MoveItemIntoBucketOnSave, XXXX.Project.Web" method="OnItemSaved" />
</event>
</events>
</sitecore>
</configuration>
UPDATE: I do not recommend using the EventDisabler. If you add a new page and then try to add a rendering to the page and specify a datasource for it, the datasource locations won't resolve because Sitecore still thinks the path of newly created item is a direct child of the bucket item, rather than wherever the item was moved to within the bucket. See this question for more information.
UPDATE 2:
Note that this method will get called twice when a new bucketed item is created. You should think very carefully about what this means for you, and if you should add any other checks prior to calling any other code within this method.
You can achieve this by programmatically moving the bucketed item to the root of the bucket with the BucketManager. Doing this will force it to reevaluate the bucket rules and reorganize it:
BucketManager.MoveItemIntoBucket(bucketedItem, bucketItem);
Note that this is different from BucketManager.Sync(bucketItem) because it does not sync the whole bucket, but instead handles just the single item that was changed.
In our solutions, we typically create an item:saved event handler to do this automatically:
using Sitecore.Buckets.Managers;
using Sitecore.Buckets.Util;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Events;
using System;
using System.Text.RegularExpressions;
namespace Custom.Events.ItemSaved
{
public class ReorganizeBucketedItemInBucket
{
public void OnItemSaved(object sender, EventArgs args)
{
var bucketedItem = Event.ExtractParameter(args, 0) as Item;
// If we don't have an item or we're not saving in the master DB, ignore this save
if (bucketedItem == null || !"master".Equals(bucketedItem.Database?.Name, StringComparison.OrdinalIgnoreCase))
return;
if (!bucketedItem.TemplateID.Equals(new ID("{bucketed-item-template-id}"))) return;
var itemChanges = Event.ExtractParameter(args, 1) as ItemChanges;
// If there were no changes or the changes didn't include the date field, ignore this save
if (itemChanges == null || !itemChanges.HasFieldsChanged || !itemChanges.IsFieldModified(new ID("{field-id-of-date-field}")))
return;
Item bucketItem = bucketedItem.Axes.SelectSingleItem($"{EscapePath(bucketedItem.Paths.FullPath)}/ancestor-or-self::*[##templateid = '{{bucket-container-template-id}}']");
// If this item isn't in a bucket (or is in a bucket of another, unexpected type), ignore it
if (bucketItem == null) return;
Item parent = bucketedItem.Parent;
BucketManager.MoveItemIntoBucket(bucketedItem, bucketItem);
// Delete empty ancestor bucket folders
while (parent != null && !parent.HasChildren && parent.TemplateID == BucketConfigurationSettings.BucketTemplateId)
{
Item tempParent = parent.Parent;
parent.Delete();
parent = tempParent;
}
}
/// <summary>
/// Wraps each segment of a sitecore path with "#"'s
/// </summary>
public string EscapePath(string path)
{
return Regex.Replace(path, #"([^/]+)", "#$1#").Replace("#*#", "*");
}
}
}
And don't forget your patch config, of course:
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
<sitecore>
<events>
<event name="item:saved">
<handler type="Custom.Events.ItemSaved.ReorganizeBucketedItemInBucket, Custom.Events" method="OnItemSaved"></handler>
</event>
</events>
</sitecore>
</configuration>
You'll need to implement a pipeline processor.
You can do this by adding the following into a .config file in your App_Code/Include folder.
<processors>
<saveUI>
<processor mode="on" type="Namespace.ClassName, Your.Assembly" patch:after="processor[last()]" />
</saveUI>
</processor
You'll also need to implement that class - there's nothing special about it except that it must have a public Process method with a Sitecore.Pipelines.Save.SaveArgs parameter.
namespace CustomFunctions
{
public class SaveAction
{
public void Process(SaveArgs args)
{
// There's a collection of items
// I'm not sure what the situation where there's more than one item is though.
var items = args.SavedItems;
var bucket = SomeFunctionToGetParent(items);
BucketManager.Sync(items);
}
}
}
I've never actually implemented this, but I think my code should give you an idea of how to get started - though this pipeline processor would be called every time an item is saved, so you need efficient checking to make sure that the item needs to have your bucket syncing processor used.

Sitecore automatically sets publishing restrictions on item when edited

Our instance of Sitecore is auto setting a publishing restriction on the item with the current date/time when a user selects Edit. This only appears to happen when RequireLockBeforeEditing=true. Is this expected behavior? Why does this happen and is there a way to turn it off?
<setting name="RequireLockBeforeEditing" value="true" /> is the default setting and will result in a new item Version being placed into Draft when the author selects Edit. In this senario, it's normal for Sitecore to assign a Publishable From date/time to the new version. This is not so much a restriction as it is a date/time stamp of when that version was created.
The date is needed as it is used to determine which version should be live. If there is no "from" date set, the version will not be publishable.
As for your custom code, if you post it here, I may be able to help you avoid it conflicting with Sitecore's default behavior. There may be some simple checks that you can do such as ignoring the Edit if the publishable from date matches the last updated date.
EDIT:
Instead of modifying Sitecore's default behavior (which will probably bite you later on) consider checking to see if the item being edited is brand new. When a new version is added, the item's created date and Valid from fields will match.
public void OnItemSaving(object sender, EventArgs args)
{
try
{
Item item = Event.ExtractParameter(args, 0) as Item;
ItemChanges itemChanges = Event.ExtractParameter(args, 1) as ItemChanges;
// Ensure that a change was made to the valid to/from fields
if (item != null &&
itemChanges != null &&
itemChanges.FieldChanges.ContainsAnyOf(FieldIDs.ValidFrom, FieldIDs.ValidTo))
{
// Ensure that the item is not brand new (such as when an author locks or clicks Edit)
if (item.Publishing.ValidTo != DateTime.MaxValue ||
item.Publishing.ValidFrom.ToString("MMddyyyyHHmmss") !=
item.Statistics.Created.ToString("MMddyyyyHHmmss"))
{
// Do work here...
}
}
}
catch (Exception ex)
{
Log.Error("Error in item:saved event", ex, this);
}
}

Sitecore display name url does not work without language code

On this Sitecore website (6.5.0 rev. 120706), I have this sitecore item called XYZ. So I have http://example.com/XYZ/.
I've added french localization, so using display names I now have:
http://example.com/XYZ-en/
http://example.com/XYZ-fr/
The english version works well, but the french does not and resolves to 404 unless I go to the english first, click on my language switcher button first. When I click on it, I'm redirected to http://example.com/fr-CA/XYZ-fr/, which works. From then on, the english url stops working, the french one works. As I switch languages like that, I always only have one of the two that work.
That button runs this code:
protected void LanguageLinkClick(object sender, EventArgs e)
{
var lang = (Sitecore.Context.Language.Name == "en") ? "fr-ca" : "en";
Tools.RedirectToLanguage(lang, Response);
}
That Tools function runs the following code:
public static void RedirectToLanguage(string pStrLangToSet, HttpResponse pResponse)
{
if (!string.IsNullOrEmpty(pStrLangToSet))
{
var newLang = Language.Parse(pStrLangToSet);
if (newLang != null)
{
Sitecore.Context.SetLanguage(newLang, true);
var itm = Sitecore.Context.Item;
if (Sitecore.Context.Item != null)
{
var itemInLang = Sitecore.Context.Database.Items[itm.ID, newLang];
if (itemInLang != null)
{
pResponse.Redirect(BuildUrl(itemInLang));
}
}
}
}
}
This is somewhat old code that is in this old project.
Is there anything I should look for that would intercept default display name behavior? Or isthis behavior with display names something that's not managed out of the box?
Thanks for any help!
This is the expected behaviour. This article from John West describes the steps involved in the language resolving process and details of the ItemResolver process can be found here.
What I assume you have set in the LinkProvider for your site is useDisplayName=true and maybe languageEmbedding=false, something like the following:
<add name="sitecore" type="Sitecore.Links.LinkProvider, Sitecore.Kernel"
alwaysIncludeServerUrl="false" addAspxExtension="true" encodeNames="true"
languageLocation="filePath" lowercaseUrls="false" shortenUrls="true"
languageEmbedding="never" useDisplayName="true" />
This tells the LinkManager to build URLs using the display name of the Item, hence you have multilingual URLs. Your RedirectToLanguage method switches the context language, from EN to FR-CA and vice versa, which puts Sitecore into that specific language mode for the user.
The following pipeline in <httpRequestBegin> tries to resolve an Item from your requested URL:
<processor type="Sitecore.Pipelines.HttpRequest.ItemResolver, Sitecore.Kernel" />
And one of the attempts in that method is to ResolveUsingDisplayName(args). So it will be trying to resolve an Item with an EN display name, but the Context Language is set to FR-CA so in fact it will never find the item, hence the 404.
You have 2 options here:
Set languageEmbedding="always" in your LinkProvider, which mean your URLs will be formatted like "/en/Nice-Product-With-Lots-Of-Options" and "/fr-ca/Mon-Produit-Avec-Plusieurs-Options".
Add a custom pipeline processor after the default ItemResolver which tries to resolve using the alternate language, and if it finds a match sets the Context Item and switches language.
The following will call the default Sitecore ItemResolver after you switch to the alternate language and then attempt to resolve, which will try to find the Item using display name:
public class AlternateLanguageItemResolver : HttpRequestProcessor
{
public override void Process(HttpRequestArgs args)
{
Assert.ArgumentNotNull(args, "args");
if (Context.Item != null || Context.Database == null || args.Url.ItemPath.Length == 0)
return;
var origLanguage = Sitecore.Context.Language;
Sitecore.Context.Language = AltLanguage;
// try to find it using default ItemResolver with alternate language
var itemResolver = new Sitecore.Pipelines.HttpRequest.ItemResolver();
itemResolver.Process(args);
if (Context.Item == null)
{
// well we didn't find it, so switch the context back so everyting can continue as normal
Sitecore.Context.Language = origLanguage;
return;
}
// We found the Item! Switch the User Language for future requests
Sitecore.Context.SetLanguage(AltLanguage, true);
}
private Language _altLanguage = null;
private Language AltLanguage
{
get
{
if (_altLanguage == null)
{
var altLang = (Sitecore.Context.Language.Name == "en") ? "fr-ca" : "en";
_altLanguage = Language.Parse(altLang);
}
return _altLanguage;
}
}
}
And patch it in after the default ItemResolver:
<processor type="Sitecore.Sample.AlternateLanguageItemResolver, Sitecore.Sample"
patch:after="processor[#type='Sitecore.Pipelines.HttpRequest.ItemResolver, Sitecore.Kernel']"/>
This is an untested prototype, check that subsequent requests are returned in the correct switched language. Otherwise redirect back to itself like you in your original code.
So if I understand correctly you'd like these urls to resolve the language for you:
http://example.com/XYZ-en/
http://example.com/XYZ-fr/
The LanguageResolver will resolve the language in this order:
1. language can be distilled from the url (cannot be done with above urls)
2. a language cookie is available for this site (sitename#lang)
3. fall back to default configured language
When you switch, using your switcher, the url paths start with the language code, then the resolver is able to resolve the language by url and persists the language in the language cookie. This is why you experience this behavior of one working language at a time.
The best you can do is always use language in urls (linkprovider configuration), otherwise you'd have to hook in the languageresolver and do some funky check on displayname for each language, but that will probably get very expensive.

Can Glass.Mapper V3 support language fallback (field-level and item-level)?

We just updated our project to use Glass.Mapper V3. We LOVE it. But we've encountered an issue. It doesn't seem to respect language fallback.
We have our site set up so that if a user picks a non-default language, they will see the item of that language if it exists. If not, they will see the default ("fallback") language version. We have also set this up at the field level, so that if there is a non-default version of an item but not all the fields are changed, any unchanged fields will fall back to the default language version's value for that field.
Is there anything we can do to enable Glass to use language fallback?
I am updating this with a bit of background on why we do the check. If you ask for a Sitecore item that doesn't exist you get a null value, so that is simple to handle. However if you ask for a Sitecore item that doesn't exist in that particular language returns an item with no versions. This means we have to do this check because otherwise Glass would end up returning empty class which I don't think makes much sense.
This answer will get a little experimental.
First in the the Spherical.cs file you need to disable the check:
protected void Application_BeginRequest()
{
Sitecore.Context.Items["Disable"] = new VersionCountDisabler();
}
We can then move the check to later on to the Object Construction pipeline. First create a task:
public class FallbackCheckTask : IObjectConstructionTask
{
public void Execute(ObjectConstructionArgs args)
{
if (args.Result == null)
{
var scContext = args.AbstractTypeCreationContext as SitecoreTypeCreationContext;
if (scContext.Item == null)
{
args.AbortPipeline();
return;
}
//this checks to see if the item was created by the fallback module
if (scContext.Item is Sitecore.Data.Managers.StubItem)
{
return;
}
// we could be trying to convert rendering parameters to a glass model, and if so, just return.
if (String.Compare(scContext.Item.Paths.FullPath, "[orphan]/renderingParameters", true) == 0)
{
return;
}
if (scContext.Item.Versions.Count == 0)
{
args.AbortPipeline();
return;
}
}
}
}
Then finally register this task in the GlassMapperScCustom class:
public static void CastleConfig(IWindsorContainer container){
var config = new Config();
container.Register(
Component.For<IObjectConstructionTask>().ImplementedBy<FallbackCheckTask>().LifestyleTransient()
);
container.Install(new SitecoreInstaller(config));
}
I haven't tested this but it should in theory work <- disclaimer ;-)
There are few potential issues with provided solution when sitecore 7 (7.2) + IoC + solr + mvc is used.
When using IoC ex Winsdor please make sure that your Global.asax looks like this one <%# Application Codebehind="Global.asax.cs" Inherits="Sitecore.ContentSearch.SolrProvider.CastleWindsorIntegration.WindsorApplication" Language="C#" %>. Once, by mistake this file has been changed to <%# Application Codebehind="Global.asax.cs" Inherits="Merck.Manuals.Web.Global" Language="C#" %> and language fallback was not working. Also the errors we were getting wasn't descriptive as we thought that solr schema is incorrect.
Code Sitecore.Context.Items["Disable"] = new VersionCountDisabler(); can be added to PreprocessRequestProcessor and it works fine which is better solution that modifying global.asax.