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.
Related
I need to open a new google meet room, and send it. I can't use standard "share" button in app. I need to catch the final url.
I can't catch that with curl (it's not a normal redirect).
My idea is that i need to open a request/link in background or in the same page, wait some second and catch the link, after i can release the page and user can enter.
Do you know something that can help me?
Edit:
Yes, i had miss to tell that i need to generate a room from a click and catch the url from code. Generally, i should to make this with Google Calendar API, but in this case i can't.
I use google Calendar API. I make a webApp for my organization, that from a form (with user information to send togheter meet link) make a google calendar event with google meet room (from google loggedin user account), catch the link and send it by a smsGateway.
function FormForMeet (Args) {
// get calendar name, if it already exists
var meetsCalendar = CalendarApp.getCalendarsByName ('CalendarName');
Logger.log (meetsCalendar.length)
if (meetsCalendar.length> = 1) {
// if some calendar be found, it catch the first, obviously here you can use some differet method. I had choose this because I don't expect to find more than 1
var calendar = meetsCalendar [0] .getId ();
}
else
{
// If miss, create new one.
var calendar = CalendarApp.createCalendar ('CalendarName', {summary: 'descrip Calendar',
// set a color of calendar label :D
color: CalendarApp.Color.PURPLE});
calendar = calendar.getId ();
}
// Call function to create meet
var LinkMeet = CreateConference_ (calendar);
// here you can use what you want for send Args + LinkMeet);
// if you want return link
return LinkMeet;
}
// Function to create Conference. You can obviously use the code up without make a new function.
function CreateConference_ (calendarId) {
// Custom of event, here I created the conferences according to my needs (now, with 1 h / conference)
var now = new Date ();
var start = new Date (now.getTime ()). toISOString ();
var end = new Date (now.getTime () + (1 * 60 * 60 * 1000)). toISOString ();
// generate random string to request
var rdmreqId = genStrg ();
var event = {
"end": {
"dateTime": end
},
"start": {
"dateTime": start
},
"summary": "conferenceName",
"conferenceData": {
"createRequest": {
"conferenceSolutionKey": {
"type": "hangoutsMeet"
},
"requestId": rdmreqId
}
}
};
// insert event in calendar
event = Calendar.Events.insert (event, calendarId, {
"conferenceDataVersion": 1});
// if use the function you can return the link to send
return event.hangoutLink
}
// random strind
function genStrg () {
var data = "something";
var text = "";
var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789! # # $% & <> * -";
for (var j = 2; j <= data.length; j ++) {
text = ""; // Reset text to empty string
for (var i = 0; i <possible.length; i ++) {
text + = possible.charAt (Math.floor (Math.random () * possible.length));
}
return text;
}
}
all google meet links look something like this:
https://meet.google.com/abc-defg-hij
You should be able to just copy and paste this link from your browser page. If someone enters this link, they will be taken to the meet lobby and they can enter at any time.
If you can't access this link for some reason, like if you're on mobile, you have to put your meet code (the abc-defg-hij) at the end of the aforementioned url.
edit: You actually can find the link if you're on mobile by going into your meeting lobby and scrolling down until you get to "joining information". Under that there should be the meeting link and the numbers to join by phone.
I'm having an issue changing the workflow state for an item programmatically. The state isn't being changed no matter what I do to the field. I've tried using (new SecurityDisabler()){} and putting the item in editing mode then changing the field manually. I've noticed that the item itself has the Lock set to <r />, could this be causing an issue?
Here is some sample code of what I've tried to do:
[HttpPost]
[MultipleButton(Name = "action", Argument = "Submit")]
public ActionResult Submit(LoI model)
{
if (model.Submitted || !model.Signed)
{
return Redirect("/Profile/LoI");
}
ModifyCandidateInfo(model, true);
Session["message"] = Translate.Text("loi-submitted-message");
Session["messageClass"] = "success";
return Redirect("/Profile/LoI");
}
private static void ModifyCandidateInfo(LoI model, bool isSubmission)
{
using (new SecurityDisabler())
{
var candidateFolder = CBUtility.GetCandidateFolder();
var loi= candidateFolder.GetChildren().SingleOrDefault(loi => loi.TemplateID == LoITemplateId);
if (loi == null) return;
loi.Editing.BeginEdit();
EditFields(loi, model);
EditChildren(loi, model);
//Send emails upon submission
if (isSubmission)
{
loi.ExecuteCommand("Submit",
loi.Name + " submitted for " + model.CandidateName);
using (new SecurityDisabler())
{
loi.Editing.BeginEdit();
loi.Fields["__Workflow state"].Value = "{F352B651-341B-4CCF-89FE-BD77F5E4D540}";
loi.Editing.EndEdit();
}
}
loi.Editing.EndEdit();
}
}
I initalized the item's workflow with the following function:
public static void InitializeWorkflow(Item item, ID workflowId)
{
item.Editing.BeginEdit();
var workflow =
item.Database.WorkflowProvider.GetWorkflow(workflowId.ToString());
workflow.Start(item);
item.Editing.EndEdit();
}
The item starts at the default drafting state and executed a "Submit" command that fires off emails. Through the Sitecore UI if I hit submit it'll go to the next workflow state but not programmatically when I fire off the ExecuteCommand function. Below you'll find the ExecuteCommand function.
public static WorkflowResult ExecuteCommand(this Item item, string commandName, string comment)
{
using (new SecurityDisabler())
{
var workflow = item.Database.WorkflowProvider.GetWorkflow(item);
if (workflow == null)
{
return new WorkflowResult(false, "No workflow assigned to item");
}
var command = workflow.GetCommands(item[FieldIDs.WorkflowState])
.FirstOrDefault(c => c.DisplayName == commandName);
return command == null
? new WorkflowResult(false, "Workflow command not found")
: workflow.Execute(command.CommandID, item, comment, false);
}
}
The command fires off fine and the emails are sent but I can't figure out why the state won't change. Could someone provide me with other suggestions or a solution?
Am I reading the workflow state id correctly? I'm using the item ID for the workflow state.
I think your code is really similar to my implementation. This is my code's background.
All items have the same workflow named "WF" and it has three workflow states (Working, Awaiting Approval, and Approved). One page-item having "WF" has some rendering items and those datasource items. Suppose a content editor is ready to submit and approve the item with its related items. By hitting the "Submit" and "Approval" button in the page, all page-item's related items have the same workflow state as the page-item's one.
Most code are from Marek Musielak and this code is perfectly working in my side.
public class UpdateWorkflowState
{
// List all controls in page item
public RenderingReference[] GetListOfSublayouts(string itemId, Item targetItem)
{
RenderingReference[] renderings = null;
if (Sitecore.Data.ID.IsID(itemId))
{
renderings = targetItem.Visualization.GetRenderings(Sitecore.Context.Device, true);
}
return renderings;
}
// Return all datasource defined on one item
public IEnumerable<string> GetDatasourceValue(WorkflowPipelineArgs args, Item targetItem)
{
List<string> uniqueDatasourceValues = new List<string>();
Sitecore.Layouts.RenderingReference[] renderings = GetListOfSublayouts(targetItem.ID.ToString(), targetItem);
LayoutField layoutField = new LayoutField(targetItem.Fields[Sitecore.FieldIDs.FinalLayoutField]);
LayoutDefinition layoutDefinition = LayoutDefinition.Parse(layoutField.Value);
DeviceDefinition deviceDefinition = layoutDefinition.GetDevice(Sitecore.Context.Device.ID.ToString());
foreach (var rendering in renderings)
{
if (!uniqueDatasourceValues.Contains(rendering.Settings.DataSource))
uniqueDatasourceValues.Add(rendering.Settings.DataSource);
}
return uniqueDatasourceValues;
}
// Check workflow state and update state
public WorkflowResult ChangeWorkflowState(Item item, ID workflowStateId)
{
using (new EditContext(item))
{
item[FieldIDs.WorkflowState] = workflowStateId.ToString();
}
Sitecore.Layouts.RenderingReference[] renderings = GetListOfSublayouts(item.ID.ToString(), item);
return new WorkflowResult(true, "OK", workflowStateId);
}
// Verify workflow state and update workflow state
public WorkflowResult ChangeWorkflowState(Item item, string workflowStateName)
{
IWorkflow workflow = item.Database.WorkflowProvider.GetWorkflow(item);
if (workflow == null)
{
return new WorkflowResult(false, "No workflow assigned to item");
}
WorkflowState newState = workflow.GetStates().FirstOrDefault(state => state.DisplayName == workflowStateName);
if (newState == null)
{
return new WorkflowResult(false, "Cannot find workflow state " + workflowStateName);
}
unlockItem(newState, item);
return ChangeWorkflowState(item, ID.Parse(newState.StateID));
}
// Unlock the item when it is on FinalState
public void unlockItem(WorkflowState newState, Item item)
{
if (newState.FinalState && item.Locking.IsLocked())
{
using (new EditContext(item, false, false))
{
item["__lock"] = "<r />";
}
}
}
}
I am talking about loopback push component. I am trying to intercept the "create" method of "Installation" model. My code looks like this -
server/boot/installationex.js
module.exports = function (app) {
var Installation = app.models.Installation;
var create = Installation.create;
Installation.create = function (data, cb) {
//reinitializing old implementation
this.create = create;
console.log("Received data: "+JSON.stringify(data));
if (!data || !data.imei) {
console.log("No data or imei was provided, creating new");
this.create(data, cb);
return;
}
//saving 'this' reference
var that = this;
//search by imei filter
var filter = {where: {imei: data.imei}};
this.findOne(filter, function (err, result) {
if (err) {
console.log("Error occurred while looking for installation by IMEI");
cb(err);
return;
}
if (!result) {
console.log("No installation found by IMEI, will create a new installation");
that.create(data, cb);
return;
}
console.log("Found existing installation with id: " + JSON.stringify(result));
result.deviceToken = result.gpsLocation = result.osVersion = result.vendor = result.phoneNumbers = null;
if (data.deviceToken) {
result.deviceToken = data.deviceToken;
}
if (data.gpsLocation) {
result.gpsLocation = data.gpsLocation;
}
if (data.osVersion) {
result.osVersion = data.osVersion;
}
if (data.vendor) {
//result.vendor=data.vendor;
result.vendor = 'jahid';
}
if (data.phoneNumbers) {
result.phoneNumbers = data.phoneNumbers;
}
that.upsert(result, cb);
});
}
}
Unfortunately this code is invoked only once, I mean the first time. After that this code is never invoked. I became sure by looking at the log. It only prints the log first time. After that it does not print any log.
Any idea why this glue code is only invoked once? My intention is to intercept all create method invocation for Installation model. And check if there is already an entry for supplied "IMEI", if so then reuse that. Otherwise create new.
Thanks in advance.
Best regards,
Jahid
What I would start here with is:
instead of implementing your own intercepting mechanism use Model Hooks
check out findOrCreate() method
boot scripts are only run once during application startup. if you want a function that triggers every time a function is called, use a remote hook or model hook. probably something along the lines of:
...
Installation.beforeRemote('create', ...
...
see http://docs.strongloop.com/display/LB/Adding+logic+to+models for more info
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.
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)));
}
}
}
}
}
}