Check if items exist in the current language? - sitecore

I have a Sitecore solution where there are 3 different languages enabled. On top of the page, there is a link to each language. When you click this link, you get the current page you are standing on, in the selected language.
But not all pages are translated into all languages. So if I am standing on page x in English language, and this page is only available in English and German but not Chinese, then the Chinese link should not be shown.
So the question is - How do I check if the current item has a version of a specific language?

To see if there is a version of the current item you can do this: Sitecore.Context.Item.Versions.Count > 0
[updated for comment]
I don't claim that this is the most efficient way to determine if an item has a version in a language, but this will work:
bool hasVersion = HasLanguageVersion(Sitecore.Context.Item, "en");
private bool HasLanguageVersion(Sitecore.Data.Items.Item item, string languageName)
{
var language = item.Languages.FirstOrDefault(l => l.Name == languageName);
if (language != null)
{
var languageSpecificItem = global::Sitecore.Context.Database.GetItem(item.ID, language);
if (languageSpecificItem != null && languageSpecificItem.Versions.Count > 0)
{
return true;
}
}
return false;
}

You can retrieve a collection (LanguageCollection) of an items content languages (ie. the languages for which the item has content).
LanguageCollection collection = ItemManager.GetContentLanguages(Sitecore.Context.Item);
foreach (var lang in collection)
{
var itm = Sitecore.Context.Database.GetItem(Sitecore.Context.Item.ID,lang);
if(itm.Versions.Count > 0)
{
Response.Write("Found language " + lang + "<br />");
}
}
Hope this helps :)
NB: Add a comment dude.. please dont just make random edits to my answer. This is the height of rudeness.
Edit: Correcting .. Turns out the method doesn't take into account versions of that language existing.---
to clarify, ItemManager.GetContentLanguages does not get you the list of languages on a given item. It gives the list of all languages you have opted to include in your environment. Under the hood, it does 2 things (based on decompiled code for sitecore 7.2):
it calls LanguageManager.GetLanguages(item.Database));
it adds to this any languages not already added by step 1 by calling item.Database.DataManager.DataSource.GetLanguages(item.ID);

If you have the context items in a list, use a Linq expression:
List<Item> languageContentItems =
contentItems.Where(x=> x.Version != null && x.Versions.Count > 0).ToList();
I'm thoroughly confused as to why x.Version.Number wouldn't be the correct syntax vs. using x.Versions.Count because the x.Versions inline-documentation states that it returns all language versions of the item, which would mean that x.Versions.Count should return a count of all versions in all languages, when we really only want to see if the item has a version for the current context language.

This works like charm for me:
item.Versions.GetVersions(false).Any();

don't forget about Fallback option sometimes
if (item.Versions.Count > 0 && !item.IsFallback)
would work better

have a look at this post for a method which returns a list of languages an item has versions in: https://stackoverflow.com/a/31351810/551811

I use the following extension method on Item. This assumes you have an item to start from of course.
public static bool HasVersionInLanguage(this Item item, Sitecore.Globalization.Language lang)
{
return ItemManager.GetVersions(item, lang).Any();
}
If you don't have the item in memory you could change this to a 'normal' method and pass the ID of the item as a second parameter..
public bool HasVersionInLanguage(ID itemId, Sitecore.Globalization.Language lang)
{
return ItemManager.GetVersions(item, lang).Any();
}

Related

Sitecore: Item display name does not support parenthesis

I changed the display name of an item from Item1 to (This is the new title)
However, in the tree, while the closing ) shows up, the opening appears encoded.
It shows as:
(This is the new title)
How do I fix this please?
This is a known bug, introduced in 9.3 iirc. Reference number 393368. As far as I'm aware, there isn't a patch yet for it, but you can patch it yourself by replacing the faulty pipeline.
Look at Sitecore.Pipelines.Save.Save class in Sitecore.Kernel. In the deep nested Process() method, you'll see this code (reflected with dotPeek):
if (this.NeedsHtmlTagEncode(field1))
field1.Value = WebUtil.SafeEncode(field1.Value);
The NeedsHtmlTagEncode returns true for DisplayName (for some unknown reason). You can workaround this issue by replacing the Save processor with one that inherits the old one and overrides the protected virtual bool NeedsHtmlTagEncode(SaveArgs.SaveField field) method and just let it return false. Then you just patch out the existing processor with your own one with the xpath /sitecore/processors/saveUI/processor[#type='Sitecore.Pipelines.Save.Save, Sitecore.Kernel'].
An update since we just encountered this in one of our projects:
In addition to the bug mikaelnet wrote about, which affects when you change the Display name field in the Content Editor Appearance section and then save the item, there is a second bug that affects the Display name ribbon menu button you are using here.
From what I found, the problem is in the /sitecore/shell/Applications/Dialogs/Prompt/prompt.js file and was introduced some time between 9.0 and 9.3.
This is the 9.0 version:
function ok_click(evt) {
evt && Event.stop(evt);
​
var maxlength = (dialogArguments.maxLength != null ? parseInt(dialogArguments.maxLength, 10) : 0);
​
if (dialogArguments.validation != null) {
var re = new RegExp(dialogArguments.validation);
}
​
var result = $("Value").value;
...
The 9.3 version has an added sanitizeHtml call:
function ok_click(evt) {
evt && Event.stop(evt);
​
var maxlength = (dialogArguments.maxLength != null ? parseInt(dialogArguments.maxLength, 10) : 0);
​
if (dialogArguments.validation != null) {
var re = new RegExp(dialogArguments.validation);
}
​
var result = sanitizeHtml($("Value").value);
...
I believe both issues were reported in the mentioned bug and we have seen a patched version of the sanitizeHtml() method that fixes some encoding issues, but I don't see why they would encode the result in the first place so the underlying issue you are seeing is still there.

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.

Customize list rendering for pagination

i'm trying to implement infinite scroll in Orchard doing minimal changes.
The script I use need to perfectly identify with a jquery selector the Next page link of pager.
Currently a standard orchard pager renders this way:
<li>></li>
the desiderable rendering is:
<li class="next">></li>
I tried many ways to override the Pager_Next template but no joy.
The pager is a list and list is done by code. No easy way to override.
A great article the should explain how to do miss some basic part (as to override the whole list for example):
http://weblogs.asp.net/bleroy/overriding-the-pager-rendering-in-orchard
Right now my workaround was to change the Orchard source CoreShapes.cs for list rendering adding these two lines:
if (itemTag != null) {
if (index == 0)
itemTag.AddCssClass("first");
if (index == count - 1)
itemTag.AddCssClass("last");
//new lines
if (index == 1 && count > 2)
itemTag.AddCssClass("previous");
if (index == count - 2 && count > 2)
itemTag.AddCssClass("next");
So far it works BUT I do not like it
1) It changes orchard source, this is bad
2) It changes all the lists (and not just pager)
So "How may I override the list for JUST my theme and for JUST pager in a way that a class is added automatically at the Page_Next li tag?"
Thanks
Try something like this in your Pager.Next.cshtml alternate view:
#using System.Web.Routing;
#{
string text = Model.Value.ToString();
string action = Model.RouteValues["action"].ToString();
RouteValueDictionary routeValues = (RouteValueDictionary)Model.RouteValues;
}
<span><a class="next" href="#Url.Action(action, routeValues)">#text</a></span>
I'm not sure if the answer by joshb is close enough for your needs.
Otherwise I would say almost the same thing.
Change the Pager.Next.cshtml and Pager.Previous.cshtml in your theme, and simply add a way for you to identify the link.
The use jQuery to add the class you need to the parent.
This is by no means an elegant solution, but it will accomplish what you're asking for without changing the core and without affecting other lists.
My test looks like this, just for testing and only for next, but you get the idea.
(Don't use id obviously as it would give you duplicate id's, this is just a quick and dirty example)
Pager.Next.cshtml :
#{
var RouteValues = (object)Model.RouteValues;
RouteValueDictionary rvd;
if (RouteValues == null) {
rvd = new RouteValueDictionary();
}
else {
rvd = RouteValues is RouteValueDictionary ? (RouteValueDictionary)RouteValues : new RouteValueDictionary(RouteValues);
}
}
<a id="pagination-pager-next" href="#Url.Action((string)rvd["action"], rvd)"><i class="fa fa-angle-right"></i></a>
And added this at the end of Pager.cshtml, but you may have a better place for it:
<script>
$(document).ready(function () {
$("#pagination-pager-next").parent().addClass("next");
});
</script>

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.

Sitecore Multisite Manager and 'source' field in template builder

Is there any way to parametise the Datasource for the 'source' field in the Template Builder?
We have a multisite setup. As part of this it would save a lot of time and irritation if we could point our Droptrees and Treelists point at the appropriate locations rather than common parents.
For instance:
Content
--Site1
--Data
--Site2
--Data
Instead of having to point our site at the root Content folder I want to point it at the individual data folders, so I want to do something like:
DataSource=/sitecore/content/$sitename/Data
I can't find any articles on this. Is it something that's possible?
Not by default, but you can use this technique to code your datasources:
http://newguid.net/sitecore/2013/coded-field-datasources-in-sitecore/
You could possibly use relative paths if it fits with the rest of your site structure. It could be as simple as:
./Data
But if the fields are on random items all over the tree, that might not be helpul.
Otherwise try looking at:
How to use sitecore query in datasource location? (dynamic datasouce)
You might want to look at using a Querable Datasource Location and plugging into the getRenderingDatasource pipeline.
It's really going to depend on your use cases. The thing I like about this solution is there is no need to create a whole bunch of controls which effectively do he same thing as the default Sitecore ones, and you don't have to individually code up each datasource you require - just set the query you need to get the data. You can also just set the datasource query in the __standard values for the templates.
This is very similar to Holger's suggestion, I just think this code is neater :)
Since Sitecore 7 requires VS 2012 and our company isn't going to upgrade any time soon I was forced to find a Sitecore 6 solution to this.
Drawing on this article and this one I came up with this solution.
public class SCWTreeList : TreeList
{
protected override void OnLoad(EventArgs e)
{
if (!String.IsNullOrEmpty(Source))
this.Source = SourceQuery.Resolve(SContext.ContentDatabase.Items[ItemID], Source);
base.OnLoad(e);
}
}
This creates a custom TreeList control and passes it's Source field through to a class to handle it. All that class needs to do is resolve anything you have in the Source field into a sitecore query path which can then be reassigned to the source field. This will then go on to be handled by Sitecore's own query engine.
So for our multi-site solution it enabled paths such as this:
{A588F1CE-3BB7-46FA-AFF1-3918E8925E09}/$sitename
To resolve to paths such as this:
/sitecore/medialibrary/Product Images/Site2
Our controls will then only show items for the correct site.
This is the method that handles resolving the GUIDs and tokens:
public static string Resolve(Item item, string query)
{
// Resolve tokens
if (query.Contains("$"))
{
MatchCollection matches = Regex.Matches(query, "\\$[a-z]+");
foreach (Match match in matches)
query = query.Replace(match.Value, ResolveToken(item, match.Value));
}
// Resolve GUIDs.
MatchCollection guidMatches = Regex.Matches(query, "^{[a-zA-Z0-9-]+}");
foreach (Match match in guidMatches)
{
Guid guid = Guid.Parse(match.Value);
Item queryItem = SContext.ContentDatabase.GetItem(new ID(guid));
if (item != null)
query = query.Replace(match.Value, queryItem.Paths.FullPath);
}
return query;
}
Token handling below, as you can see it requires that any item using the $siteref token is inside an Site Folder item that we created. That allows us to use a field which contains the name that all of our multi-site content folders must follow - Site Reference. As long at that naming convention is obeyed it allows us to reference folders within the media library or any other shared content within Sitecore.
static string ResolveToken(Item root, string token)
{
switch (token)
{
case "$siteref":
string sRef = string.Empty;
Item siteFolder = root.Axes.GetAncestors().First(x => x.TemplateID.Guid == TemplateKeys.CMS.SiteFolder);
if (siteFolder != null)
sRef = siteFolder.Fields["Site Reference"].Value;
return sRef;
}
throw new Exception("Token '" + token + "' is not recognised. Please disable wishful thinking and try again.");
}
So far this works for TreeLists, DropTrees and DropLists. It would be nice to get it working with DropLinks but this method does not seem to work.
This feels like scratching the surface, I'm sure there's a lot more you could do with this approach.