publish related media items in Sitecore 6.5 without using workflow - sitecore

Our client wants to automatically publish related media items when publishing a page. They're not using workflow which would have made things simpler, so I need to find another way. At the moment I've created a custom publish pipeline processor (as shown in this blog post) where I've enabled History storage for the web database and get the list of changed items from there. When looping through the changed items I'm checking for any related media items and publish them.
This works fine, but I just wanted to check if there's any pitfalls to watch out for or if there is a better way of doing this. Anyone have any ideas?

The best way without using workflow is to replace the AddItemReferences processor in the PublishItem workflow. There you can add what types of items will be published along with the original item.
Here is a blog post Alex Shyba about it.
Here is my local implementation
public class AddItemReferences : Sitecore.Publishing.Pipelines.PublishItem.AddItemReferences
{
private readonly static ILogger _logger = AppLogger.GetNamedLogger(typeof(AddItemReferences));
protected override List<Item> GetItemReferences(PublishItemContext context)
{
Assert.ArgumentNotNull(context, "context");
var list = new List<Item>();
// calling base method which processes links from FileDropArea field
list.AddRange(base.GetItemReferences(context));
// adding our "own" related items
list.AddRange(GetRelatedReferences(context));
return list;
}
protected virtual List<Item> GetRelatedReferences(PublishItemContext context)
{
Assert.ArgumentNotNull(context, "context");
var relatedReferenceList = new List<Item>();
if (context.PublishOptions.Mode == PublishMode.SingleItem )
{
try
{
var sourceItem = context.PublishHelper.GetSourceItem(context.ItemId);
if (sourceItem.Paths.IsContentItem)
{
var itemLinks = sourceItem.Links.GetValidLinks();
ItemLink[] referers = Globals.LinkDatabase.GetReferers(sourceItem);
relatedReferenceList.AddRange(GetMediaItems(itemLinks));
relatedReferenceList.AddRange(GetAliases(referers));
}
}
catch (Exception ex)
{
var options = context.PublishOptions;
StringBuilder msg = new StringBuilder();
msg.AppendLine("Publishing options");
msg.AppendLine("Deep: " + options.Deep);
msg.AppendLine("From date: " + options.FromDate);
msg.AppendLine("Language: " + options.Language);
msg.AppendLine("Mode: " + options.Mode);
msg.AppendLine("PublishDate: " + options.PublishDate);
msg.AppendLine("Targets: " + string.Join(",",options.PublishingTargets.ToArray()));
msg.AppendLine("Republish all: " + options.RepublishAll);
msg.AppendLine("Root item: " + options.RootItem);
msg.AppendLine("Source database: " + options.SourceDatabase.Name);
_logger.LogError(msg.ToString(), ex);
}
}
return relatedReferenceList;
}
private static IEnumerable<Item> GetMediaItems(ItemLink[] itemLinks)
{
foreach (var link in itemLinks)
{
var item = link.GetTargetItem();
if (item == null)
continue;
if (item.Paths.IsMediaItem)
{
yield return item;
}
}
}
private static IEnumerable<Item> GetAliases(ItemLink[] referrers)
{
foreach (var link in referrers)
{
var item = link.GetSourceItem();
if (item != null && IsAlias(item))
yield return item;
}
}
private static bool IsAlias(Item item)
{
return item.TemplateID.Guid == DataAccessSettings.Templates.AliasTemplateId;
}
}

Input for risk areas:
Missing entries in History storage if editing session is above 30 days prior to publish
Finding related media items involves both link fields and also rich text fields, there can be possible direct links to media, these could be handled and transformed to correctly formatted links.
Alternative solutions
Depending on the Sitecore maturity of your editors another user model could be that you autopublish the media Items from the Save Pipeline. For some users this is easier to understand, since the publishing model is then restricted to handling page visibility.

Related

Contact phone numbers Lync SDK 2013

Hello I'm using Lync SDK 2013, to display number phones from contact in ListBox, and use the Items (phone number) to call this number by my API. So i did a WPF application, that contains just a ListBox, and 2 buttons (Call - Hang up). My apllication is added as custom command in Lync, in RightClick in the contact. and it doesn't have any Lync Controls. So what i want to do is: if i Right Click on the contact, my application launches and gives me the number phone List in the ListBox. I did it with a WPF that contains the controls: ContactSearchInputBox (to search a contact) and ContactSearchResultList and it works very Well, I don't know how to do it without controls.
Any One Can Help Me !!!! :(
You need to read and understand the Lync SDK 2013 Lync Contact documentation.
If you wish to "simulate" the Lync Contact "search" (as per the Client search) then you need to look into the search API.
The other concepts you need to understand is the results returned from all the API are NOT guaranteed to return all the Lync Contact data that asked for.
There is no way with the Lync SDK to "load" all the contact information which is what most people seem to not understand.
The results returned are what the local cache has and no more. To get the all the Lync Contact information you need to understand the ContactSubscription model.
For each Lync Contact that you wish to be notified of field updates (or loads) you "subscribe" to the Lync Contact and then you will be notified via the Contact.ContactInformationChanged event.
So your UI must to able to auto-update the information as the fields get loaded / updated from any initial value returned from any Lync Contact value.
public partial class ChoosePhoneNumber : Window
{
LyncClient lync_client;
Contact contact;
ContactSubscription contact_subscription;
List<ContactInformationType> contact_information_list;
ContactManager contact_manager;
public ChoosePhoneNumber()
{
InitializeComponent();
connect_lync();
get_subscribed_contact(this.contact);
}
}
private void connect_lync()
{
try
{
lync_client = LyncClient.GetClient();
contact_manager = lync_client.ContactManager;
}
catch (ClientNotFoundException)
{
MessageBox.Show("Client is ot running", "Error While GetClient", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private void get_subscribed_contact(Contact contact)
{
List<object> contact_phone_numbers_list = new List<object>();
contact_information_list = new List<ContactInformationType>();
contact_information_list.Add(ContactInformationType.ContactEndpoints);
contact_information_list.Add(ContactInformationType.DisplayName);
contact = contact_manager.GetContactByUri("number"); // I put here the number phone of a contact in my list
contact_subscription = LyncClient.GetClient().ContactManager.CreateSubscription();
contact_subscription.AddContact(contact);
contact.ContactInformationChanged += Contact_ContactInformationChanged;
contact_subscription.Subscribe(ContactSubscriptionRefreshRate.High, contact_information_list);
List<object> endpoints = (List<object>)contact.GetContactInformation(ContactInformationType.ContactEndpoints);
var phone_numbers_list = endpoints.Where<object>(N => ((ContactEndpoint)N).Type == ContactEndpointType.HomePhone ||
((ContactEndpoint)N).Type == ContactEndpointType.MobilePhone || ((ContactEndpoint)N).Type == ContactEndpointType.OtherPhone
|| ((ContactEndpoint)N).Type == ContactEndpointType.WorkPhone).ToList<object>();
var name = contact.GetContactInformation(ContactInformationType.DisplayName);
if (phone_numbers_list != null)
{
foreach (var phone_number in phone_numbers_list)
{
contact_phone_numbers_list.Add(((ContactEndpoint)phone_number).DisplayName);
}
conboboxPhoneNumbers.ItemsSource = contact_phone_numbers_list;
}
}
private void Contact_ContactInformationChanged(object sender, ContactInformationChangedEventArgs e)
{
var contact = (Contact)sender;
if (e.ChangedContactInformation.Contains(ContactInformationType.ContactEndpoints))
{
update_endpoints(contact);
}
}
private void update_endpoints(Contact contact)
{
if ((lync_client != null) && (lync_client.State == ClientState.SignedIn))
{
ContactEndpoint endpoints = (ContactEndpoint)contact.GetContactInformation(ContactInformationType.ContactEndpoints);
}
}
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
App.Current.DispatcherUnhandledException += Current_DispatcherUnhandledException; ;
try
{
string argsParam = "Contacts=";
if (e.Args.Length > 1)
{
if (e.Args[2].Contains(argsParam))
{
var contacts_sip_uri = e.Args[2].Split('<', '>')[1];
Params.contacts = contacts_sip_uri;
}
}
}
catch (Exception ex)
{
MessageBox.Show("Reading Startup Arguments Error - " + ex.Message);
}
}
private void Current_DispatcherUnhandledException(object sender, System.Windows.Threading.DispatcherUnhandledExceptionEventArgs e)
{
string message = e.Exception.Message;
if (e.Exception.InnerException != null)
{
message += string.Format("{0}Inner Exception: {1}", Environment.NewLine, e.Exception.InnerException.Message);
}
MessageBox.Show(message, "Unhandled Exception", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
Params is a public static class that contains just contact as public static string contacts { get; set; }
public static class ParamContact
{
public static string contacts { get; set; }
}

Display items in bucket with Sitecore Data Provider

Hej Guys
I have a rather large problem, I've been tasked with creating a Custom Data Provider for extracting Stock Keeping Units(SKUs) from a SOLR database into sitecore, without actually populating the database with items.
I've created a data provider, which succesfully pulls data from the SOLR database a "creates" the items in sitecore, by using the following code:
public class SkuDataProvider : DataProvider, ISkuDataProvider
{
private readonly string _targetDatabaseName = "master";
private readonly string _idTablePrefix = "Skus";
private readonly ID _skuTemplateId = new ID("{F806B403-BDAF-4C60-959D-E706A82FC1DC}");
private readonly ID _skuRootTemplateId = new ID("{9767BC47-0A95-40E9-A2DE-3766FF241411}");
private readonly IEnumerable<SkuItemInfo> _skus;
public SkuDataProvider(/*IProductPageService productPageService*/)
{
_skus = new MockDataForSkuDataProvider().GetSimpleSkuCollection();
}
public override ItemDefinition GetItemDefinition(ID itemId, CallContext context)
{
Assert.ArgumentNotNull(itemId, "itemID");
// Retrieve the sku id from Sitecore's IDTable
var skuId = GetSkuIdFromIdTable(itemId);
if (!string.IsNullOrEmpty(skuId))
{
// Retrieve the sku data from the skus collection
var sku = _skus.FirstOrDefault(o => o.SkuId == skuId);
if (sku != null)
{
// Ensure the sku item name is valid for the Sitecore content tree
var itemName = ItemUtil.ProposeValidItemName($"{sku.SkuId}_{sku.Name}");
// Return a Sitecore item definition for the sku using the sku template
return new ItemDefinition(itemId, itemName, ID.Parse(_skuTemplateId), ID.Null);
}
}
return null;
}
private string GetSkuIdFromIdTable(ID itemId)
{
var idTableEntries = IDTable.GetKeys(_idTablePrefix, itemId);
if (idTableEntries.Any())
return idTableEntries[0].Key.ToString();
return null;
}
public override IDList GetChildIDs(ItemDefinition parentItem, CallContext context)
{
if (CanProcessParent(parentItem.ID))
{
var itemIdList = new IDList();
foreach (var sku in _skus)
{
var skuId = sku.SkuId;
// Retrieve the Sitecore item ID mapped to his sku
IDTableEntry mappedId = IDTable.GetID(_idTablePrefix, skuId) ??
IDTable.GetNewID(_idTablePrefix, skuId, parentItem.ID);
itemIdList.Add(mappedId.ID);
}
context.DataManager.Database.Caches.DataCache.Clear();
return itemIdList;
}
return base.GetChildIDs(parentItem, context);
}
private bool CanProcessParent(ID id)
{
var item = Factory.GetDatabase(_targetDatabaseName).Items[id];
bool canProcess = item.Paths.IsContentItem && item.TemplateID == _skuRootTemplateId && item.ID == new ID("{F37753A0-BC79-4FF7-B975-A8F142AACD76}");
return canProcess;
}
public override ID GetParentID(ItemDefinition itemDefinition, CallContext context)
{
var idTableEntries = IDTable.GetKeys(_idTablePrefix, itemDefinition.ID);
if (idTableEntries.Any())
{
return idTableEntries.First().ParentID;
}
return base.GetParentID(itemDefinition, context);
}
public override FieldList GetItemFields(ItemDefinition itemDefinition, VersionUri version, CallContext context)
{
var fields = new FieldList();
var idTableEntries = IDTable.GetKeys(_idTablePrefix, itemDefinition.ID);
if (idTableEntries.Any())
{
if (context.DataManager.DataSource.ItemExists(itemDefinition.ID))
{
ReflectionUtil.CallMethod(typeof(ItemCache), CacheManager.GetItemCache(context.DataManager.Database), "RemoveItem", true, true, new object[] { itemDefinition.ID });
}
var template = TemplateManager.GetTemplate(_skuTemplateId, Factory.GetDatabase(_targetDatabaseName));
if (template != null)
{
var skuId = GetSkuIdFromIdTable(itemDefinition.ID);
if (!string.IsNullOrEmpty(skuId))
{
var sku = _skus.FirstOrDefault(o => o.SkuId == skuId);
if (sku != null)
{
foreach (var field in GetDataFields(template))
{
fields.Add(field.ID, GetFieldValue(field, sku));
}
}
}
}
}
return fields;
}
protected virtual IEnumerable<TemplateField> GetDataFields(Template template)
{
return template.GetFields().Where(ItemUtil.IsDataField);
}
private string GetFieldValue(TemplateField field, SkuItemInfo sku)
{
string fieldValue = string.Empty;
switch (field.Name)
{
case "Name":
fieldValue = sku.Name;
break;
case "SkuId":
fieldValue = sku.SkuId;
break;
default:
break;
}
return fieldValue;
}
}
}
The problem emerges when accessing the Sitecore backend, where all items appears below the bucket item in a hierarchly-way.
I've checked that the Root item is set a bucket and that the template used is bucketable.
Furthermore when inserting manually in the backend, the item is correctly inserted in the bucket.
Do anyone got an idea for me, on how to fix this issue?
Best Regards
Nicolai
You need to set the Is Bucketable flag on the standard values of the template item rather than the template item itself.
Also, the way that items get "bucketed" is via events when the item is being created or saved. Sitecore then creates the bucket folders to store the items in. In your case as you have virtual items, you will need to handle their path via the data provider.
If you just want them hidden in the same way that they are in a standard bucket, then I would suggest creating a bucket folder under your SKU Root folder and using that item as the parent for all SKU virtual items. That way the bucket folder will be hidden by sitecore and you will get the same view as a standard bucket.
This is the template to use:

In Sharepoint how to get list advanced settings for Opening Documents in the Browser using REST API

Using REST API i want to access this
Settings >> Advanced Settings >> Opening Documents in the Browser
Can anybody know about this?
Thanks
In SSOM this feature corresponds to SPList.DefaultItemOpen property:
Gets or sets a value that specifies whether to open list items in a
client application or in the browser.
In REST/CSOM this property is not exposed but it could be extracted and determined via List schema Xml. For more details about this approach follow this post.
Example
The following example demonstrates how to determine whether to open list items in a client application or in the browser using REST API:
function schemaXml2Json(schemaXml)
{
var jsonObject = {};
var schemaXmlDoc = $.parseXML(schemaXml);
$(schemaXmlDoc).find('List').each(function() {
$.each(this.attributes, function(i, attr){
jsonObject[attr.name] = attr.value;
});
});
return jsonObject;
}
function getDefaultItemOpen(webUrl,listTitle)
{
var endpointUrl = webUrl + "/_api/web/lists/getbytitle('" + listTitle + "')?$select=schemaXml";
return $.getJSON(endpointUrl).then(function(data){
var listProperties = schemaXml2Json(data.SchemaXml);
var flags = parseInt(listProperties.Flags);
var defaultItemOpen = (flags & 268435456) != 0 ? "Browser" : "PreferClient";
return defaultItemOpen;
});
}
Usage
getDefaultItemOpen(_spPageContextInfo.webAbsoluteUrl,'Documents')
.done(function(value){
console.log('DefaultItemOpen: ' + value);
});

Edting collection of items is slow in Sitecore

I've implemented an on item:saved handler per this question I posted here: Run code when Publishing Restriction is saved in Sitecore
When an author changes the publishing restrictions on a page, I iterate through each of the related components for that page, updating the publishing restrictions on each to match the page item. This works, but some pages have 150 or so components and the process of editing each is taking for ever. The result is that the UI hangs for up to 5 minutes while it runs. Not good.
I'm doing this:
compItem.Editing.BeginEdit();
compItem.Publishing.ValidFrom = pageItem.Publishing.ValidFrom;
compItem.Publishing.ValidTo = pageItem.Publishing.ValidTo;
compItem.Editing.EndEdit(true, true);
I've played around with the updateStatistics and silent arguments. If do it "silent" the UI responds, but of course it still takes forever for the update to run in the background which could cause issues, since there will be a window of time where the pub restrictions between the page and components would be out of sync.
Any thoughts on why updating 150 items is so slow? Any ways to speed it up?
Here's the full code:
public void OnItemSaved(object sender, EventArgs args)
{
Item item = Event.ExtractParameter(args, 0) as Item;
if (item == null)
return;
//if it's a page, then update the page component templates with the same publish restrictions.
if(this.HasBaseTemplate(item, GlobalId.PageBaseTemplate))
{
ItemChanges itemChanges = Event.ExtractParameter(args, 1) as ItemChanges;
if (itemChanges != null &&
(itemChanges.FieldChanges.Contains(__Validfrom) || itemChanges.FieldChanges.Contains(__Validto)))
{
foreach (Item i in this.GetPageComponents(item))
{
try
{
i.Editing.BeginEdit();
i.Publishing.ValidFrom = item.Publishing.ValidFrom;
i.Publishing.ValidTo = item.Publishing.ValidTo;
i.Editing.EndEdit(true, false);
}
catch(Exception ex)
{
i.Editing.CancelEdit();
}
}
}
}
}
protected IEnumerable<Item> GetPageComponents(Item page)
{
var links = page.Links.GetAllLinks(false, true);
var foundIds = new HashSet<ID>();
var foundComponentIds = new HashSet<ID>();
var componentIds = new List<ID> { page.ID };
using (var context = ContentSearchManager.GetIndex("sitecore_master_index").CreateSearchContext())
{
while (componentIds.Any())
{
var query = context.GetQueryable<LinkSearchResultItem>();
var predicate = PredicateBuilder.False<LinkSearchResultItem>();
foreach (var id in componentIds)
{
predicate = predicate.Or(sri => sri.ItemId == id);
}
query = query.Where(predicate);
var results = query.GetResults().Hits.Select(h => h.Document);
foundIds.Add(componentIds);
componentIds.Clear();
componentIds.AddRange(results
.Where(sri => (sri.Path.StartsWith("/sitecore/content/BECU/Global/Page Components/", StringComparison.InvariantCultureIgnoreCase) || sri.ItemId == page.ID) && sri.Links != null)
.SelectMany(sri => sri.Links)
.Except(foundIds));
foundComponentIds.Add(results
.Where(sri => (sri.Path.StartsWith("/sitecore/content/BECU/Global/Page Components/", StringComparison.InvariantCultureIgnoreCase)))
.Select(sri => sri.ItemId));
}
}
var database = page.Database;
return foundComponentIds.Select(id => database.GetItem(id)).Where(i => i != null);
}
I would recommend that you try wrapping your edit code with a Sitecore.Data.BulkUpdateContext as follows
...
using(new Sitecore.Data.BulkUpdateContext())
{
foreach (Item i in this.GetPageComponents(item))
{
try
{
i.Editing.BeginEdit();
i.Publishing.ValidFrom = item.Publishing.ValidFrom;
i.Publishing.ValidTo = item.Publishing.ValidTo;
i.Editing.EndEdit(true, false);
}
catch(Exception ex)
{
i.Editing.CancelEdit();
}
}
}
...
When an item is updated in Sitecore, several other background processes and events as a result of updating the item. Such an example is indexing which will slow down the update of a large number of items at once.
The BulkUpdateContext will disable most of these events and processes until the update is complete thereby hopefully speeding up the update of your items.
Note: I have yet to use this BulkUpdateContext myself but I found several posts including this Stackoverflow question where it claims that the BulkUpdateContext only improves item creation speed, not updates. However that may only apply to the particular version of Sitecore that was being used at the time. It may may no longer be the case with new versions of Sitecore (7.X and 8), so I think it is still worth a try.

How to use sitecore query in datasource location? (dynamic datasource)

Is it possible to set the datasource location (not the datasource) to be a sitecore query?
What I'm trying to do is to have the sublayout set its datasource location to a folder under the item containing it (current item).
The sublayout datasource location should point to a folder under the current item. So I tried setting the datasource location to query:./Items/* but that did not work.
You don't need the query -- the sublayout datasource location can simply use a relative path. e.g.
./Items
Obviously though, that folder needs to exist already. I've been meaning to blog this code, and it may be overkill but I'll post here since it may help you. The following can be added to the getRenderingDatasource pipeline to create a relative path datasource location if it doesn't exist already. Add it before the GetDatasourceLocation processor.
On the sublayout, you'll want to add a parameter contentFolderTemplate=[GUID] to specify the template of the item that gets created.
public class CreateContentFolder
{
protected const string CONTENT_FOLDER_TEMPLATE_PARAM = "contentFolderTemplate";
public void Process(GetRenderingDatasourceArgs args)
{
Assert.IsNotNull(args, "args");
Sitecore.Data.Items.RenderingItem rendering = new Sitecore.Data.Items.RenderingItem(args.RenderingItem);
UrlString urlString = new UrlString(rendering.Parameters);
var contentFolder = urlString.Parameters[CONTENT_FOLDER_TEMPLATE_PARAM];
if (string.IsNullOrEmpty(contentFolder))
{
return;
}
if (!ID.IsID(contentFolder))
{
Log.Warn(string.Format("{0} for Rendering {1} contains improperly formatted ID: {2}", CONTENT_FOLDER_TEMPLATE_PARAM, args.RenderingItem.Name, contentFolder), this);
return;
}
string text = args.RenderingItem["Datasource Location"];
if (!string.IsNullOrEmpty(text))
{
if (text.StartsWith("./") && !string.IsNullOrEmpty(args.ContextItemPath))
{
var itemPath = args.ContextItemPath + text.Remove(0, 1);
var item = args.ContentDatabase.GetItem(itemPath);
var contextItem = args.ContentDatabase.GetItem(args.ContextItemPath);
if (item == null && contextItem != null)
{
string itemName = text.Remove(0, 2);
//if we create an item in the current site context, the WebEditRibbonForm will see an ItemSaved event and think it needs to reload the page
using (new SiteContextSwitcher(SiteContextFactory.GetSiteContext("system")))
{
contextItem.Add(itemName, new TemplateID(ID.Parse(contentFolder)));
}
}
}
}
}
}