We are working on implementing some custom code on a workflow in a Sitecore 6.2 site. Our workflow currently looks something like the following:
Our goal is simple: email the submitter whether their content revision was approved or rejected in the "Awaiting Approval" step along with the comments that the reviewer made. To accomplish this we are adding an action under the "Approve" and "Reject" steps like so:
We are having two big issues in trying to write this code
There doesn't seem to be any easy way to determine which Command was chosen (the workaround would be to pass an argument in the action step but I'd much rather detect which was chosen)
I can't seem to get the comments within this workflow state (I can get them is the next state though)
For further context, here is the code that I have so far:
var contentItem = args.DataItem;
var contentDatabase = contentItem.Database;
var contentWorkflow = contentDatabase.WorkflowProvider.GetWorkflow(contentItem);
var contentHistory = contentWorkflow.GetHistory(contentItem);
//Get the workflow history so that we can email the last person in that chain.
if (contentHistory.Length > 0)
{
//contentWorkflow.GetCommands
var status = contentWorkflow.GetState(contentHistory[contentHistory.Length - 1].NewState);
//submitting user (string)
string lastUser = contentHistory[contentHistory.Length - 1].User;
//approve/reject comments
var message = contentHistory[contentHistory.Length - 1].Text;
//sitecore user (so we can get email address)
var submittingUser = sc.Security.Accounts.User.FromName(lastUser, false);
}
I ended up with the following code. I still see no good way to differentiate between commands but have instead implemented two separate classes (one for approve, one for reject):
public void Process(WorkflowPipelineArgs args)
{
//all variables get initialized
string contentPath = args.DataItem.Paths.ContentPath;
var contentItem = args.DataItem;
var contentWorkflow = contentItem.Database.WorkflowProvider.GetWorkflow(contentItem);
var contentHistory = contentWorkflow.GetHistory(contentItem);
var status = "Approved";
var subject = "Item approved in workflow: ";
var message = "The above item was approved in workflow.";
var comments = args.Comments;
//Get the workflow history so that we can email the last person in that chain.
if (contentHistory.Length > 0)
{
//submitting user (string)
string lastUser = contentHistory[contentHistory.Length - 1].User;
var submittingUser = Sitecore.Security.Accounts.User.FromName(lastUser, false);
//send email however you like (we use postmark, for example)
//submittingUser.Profile.Email
}
}
I have answered a very similar question.
Basically you need to get the Mail Workflow Action and then you need to further extend it to use the original's submitter's email.
Easiest way to get the command item itself is ProcessorItem.InnerItem.Parent
This will give you the GUID for commands like submit, reject etc.
args.CommandItem.ID
This will give you the GUID for states like Draft, approved etc.
args.CommandItem.ParentID
Related
I'm making a c# call to the Stripe.net API to fetch a balance history for a connected account. I'm trying to expand on the balance transaction object to see where the charge is coming from (ex. the customer who made the charge) as all the charges to connected accounts on my platform are from charge objects with a destination property to the connected account.
Here is my code and a screenshot of what the expanded source looks like, but think I should see a charge id or a customer or something refering me to the initial customer somewhere, but I don't...
var balanceService = new StripeBalanceService();
balanceService.ExpandSource = true;
var list = new List <string> () {
"data.source.source_transfer"
};
StripeList <StripeBalanceTransaction> balanceTransactions
= balanceService.List(
new StripeBalanceTransactionListOptions() {
Limit = 20,
Type = "payment",
Expand = list
},
new StripeRequestOptions() {
StripeConnectAccountId = accountId
}
);
foreach(var transaction in balanceTransactions) {
var test = transaction;
}
I feel like I should see a charge id (ex. ch_xxx) or a Customer value (which is null) all I see of any relevance is a payment id (ex. py_xxx)
It is possible to get the charge object(ch_xxx), it is just a little involved!
As you are using destination charges, the charge(ch_xxx) takes place on the platform account, and then a transfer(tr_xxx) is made to the connected account. That transfer creates a payment(py_xxx) on the connected account, which results in a balance transaction(txn_xxx).
As your code expands the source of those balance transactions, you get the payment(py_xxx). The payment is equivalent to a charge, so it has a source_transfer field. You can expand this field also! This will give you the transfer object(tr_xxx). Finally, the transfer has a source_transaction field, and this can be exapanded to give the original charge(ch_xxx)!
Putting that all together, you will want to expand on "data.source.source_transfer.source_transaction".
If you use a Stripe library in a dynamic language you can see this in action ... unfortunately, stripe-dotnet has an open issue right now which means that you can not do this directly. Instead, you will need to make the API calls manually by calling the various Retrieve functions on the IDs, instead of doing a single expansion. It would look something like this:
var paymentId = transaction.Source.Id;
var chargeService = new StripeChargeService();
var payment = chargeService.Get(
paymentId,
new StripeRequestOptions()
{
StripeConnectAccountId = accountId
}
);
var transferService = new StripeTransferService();
transferService.ExpandSourceTransaction = true;
var transfer = transferService.Get(payment.SourceTransferId);
var charge = transfer.SourceTransaction;
Console.WriteLine(charge.Id);
I have look around the other post about this Project Backlog, but i want to those missing field in this image here
I need those missing fields like workitem, Title, Assigned To, State, Effort, Business.
I have this code with me right now.
/ Set up default team sprint date and time
var teamConfig = _tfs.GetService<TeamSettingsConfigurationService>();
var css = _tfs.GetService<ICommonStructureService4>();
string rootNodePath = string.Format("\\{0}\\Iteration\\Release 1\\Sprint 1", _selectedTeamProject.Name);
var pathRoot = css.GetNodeFromPath(rootNodePath);
css.SetIterationDates(pathRoot.Uri, DateTime.Now.AddDays(-5), DateTime.Now.AddDays(7));
var configs = teamConfig.GetTeamConfigurationsForUser(new[] { _selectedTeamProject.Uri });
var team = configs.Where(c => c.TeamName == "Demo").FirstOrDefault();
var ts = team.TeamSettings;
ts.BacklogIterationPath = string.Format(#"{0}\Release 1", _selectedTeamProject.Name);
ts.IterationPaths = new string[] { string.Format(#"{0}\Release 1\Sprint 1", _selectedTeamProject.Name), string.Format(#"{0}\Release 1\Sprint 2", _selectedTeamProject.Name) };
var tfv = new TeamFieldValue();
tfv.IncludeChildren = true;
tfv.Value = _selectedTeamProject.Name;
ts.TeamFieldValues = new []{tfv};
teamConfig.SetTeamSettings(team.TeamId, ts);
According to your screenshot, seems you are using the Work item Summary web part. After the upgrade to TFS2018, your TFS SharePoint sites will display, but all integration functionality is disabled.
The official recommended way is using TFS Dashboards for a better way to create dashboards. From that it's more easy to track/display the fields in a work item.
You could directly use some 3-party Work Item widget such as this one which also provides a summary for a selected work item.
To get or update work items such as product backlog fields pro grammatically, you could use Rest API-- Get a list of work items to handle this. It will also return all related fields name and value. Which also include a C# (GetWorkItemsByIDs method) sample code. About how to customize a dashboard in sharepoint, please take a look at this thread.
Programming would be much easier without users...
What I really need to be able to do is:
Put the content of a web page (including styles) into the body of an email and also set the subject.
OR
Send the current user an email containing the body of a web page.
I really don't care how this is implemented -- server or client side. I've not come up with any good way of client side besides trying to push the web page into the clipboard for the users to then paste into their email.
App Background
I wrote a web site using c#, ts, angular. The site manages xml documents.
The users can select a document and click the "Human Readable" button or the "XML" button. The "Human Readable" is xml with xsl to make it look pretty for the humans. The XML button is apparently for non-humans.
The "Human Readable" version opens in another browser tab.
The users want a new "email" button for emailing the human readable. The person clicking the email button has access to my web site but the recipient may not.
I've attempted educating my users to do Ctrl+A, Ctrl+C, open email, Ctrl+V but this is beyond most of their capabilities.
I have tried so many different ways to accomplish this and all have failed.
I currently do a mailto link which opens their email and the body contains a link to the Human Readable.
Here's what I've tried so far -- this may not be a conclusive list of my attempts as I've been at this for a few days now.
I've tried putting a button in the human readable (xsl with javascript) in an attempt to copy the resulting html into the clipboard for the users to paste.
A button on the web site to scrape an iFrame into the clipboard
Many iterations of javascript copy/paste techniques
a c# controller that does a ReadAsStringAsync().Result function (which I will post below because I like that solution the best so far...
Option #4 I'm partial to and I got almost working -- if it weren't for that pesky xsl not getting formatted it would probably work. My results are the data being presented without xml tags and no styles.
[ActionName("PostEmailHumanReadable")]
public void EmailHumanReadable(List<DocumentVM> documents)
{
foreach (var document in documents)
{
var docId = document.document.DocId;
var docTypeId = document.document.DocTypeId;
var co = string.Empty;
var order = string.Empty;
var name = string.Empty;
var po = string.Empty;
// get the data for the subject line
using (var efUoW = new EFUnitOfWork(EDIEnvironment.EDIEnvironment.Instance.ConnectionString))
{
var doc = efUoW.DocumentRepository.GetById(docId);
co = doc.CompanyId.ToString();
var orders = efUoW.DocumentOrderRepository.GetByDocumentID(docId).ToList();
if (!string.IsNullOrEmpty(doc.Source_Order))
order = doc.Source_Order;
else
order = "n/a";
foreach (var o in orders)
order += string.Format("{0} ", o.OrderId);
//order = string.Join(",", doc.DocumentOrders.Select(q => q.OrderId).ToList());
name = doc.BillToName;
name = doc.PurchaseOrder;
}
var subject = string.Format("{0}, {1}, {2}, {3}", co, order, name, po);
// get the human readable
var hrResponse = GetFile(docId, docTypeId);
var hrText = hrResponse.Content.ReadAsStringAsync().Result;
// format the url
var url = string.Format("<a href='/api/Documents/Getfile?DocId={0}&DocTypeId={1}'>click here to open the jEDI Human Readable</a><br><br>", docId, docTypeId);
// find the current user's email address
var users = new List<string>();
users.Add(AppUser.ADUserName);
//var to = EmailUtility.GetEmailID(users);
// and finally send the email
EmailUtility.SendEmail(EmailUtility.GetEmailID(users), null, subject, url + hrText);
}
}
[ActionName("GetFile")]
public HttpResponseMessage GetFile(int DocId, int DocTypeId)
{
if (DocTypeId == (int)DocumentTypeEnum.EDI850)
{
using (var efUoW = new Factory_UOW().EF_UOW())
{
var doc = efUoW.DocumentRepository.GetById(DocId);
var xdoc = XDocument.Parse(doc.Message);
var proc = new XProcessingInstruction("xml-stylesheet", "type='text/xsl' href='/EDI850.xsl'");
xdoc.Root.AddBeforeSelf(proc);
var response = new HttpResponseMessage();
response.Content = new System.Net.Http.StringContent(xdoc.ToString());
response.Content.Headers.ContentType = new MediaTypeHeaderValue("text/xml");
response.Content.Headers.Add("X-UA-Compatible", "IE=edge");
return response;
}
I would be very grateful for any assistance in getting this to work.
And, yes, I know I shouldn't do the Async().Result -- blocking and all that... Let's just get this working first, shall we?
Hello SO folks and more specifically Google folks monitoring this tag per your support page. I am working from .NET and PlaylistItems.List("snippet,contentDetails") does not do a whole lot compared to the old RSS Feed search. In fact adding part contentDetails adds little value in that only the VideoID is now returned but it is already part of Snippet.ResourceId.VideoId
"kind": "youtube#playlistItem",
bla,
bla,
"contentDetails": {
"videoId": "DLME0PsJRnk"
}
Why add a "part" which is only going to return one bit of information?
How about supporting something like "snippet,contentDetails(duration,PublishedAt,Views)"
I feel this is kind of basic metadata (snippet) most apps would want to list to the users.
While you are at it please please remove this non-sense of Java casing of parameters. Why would you leak-out your language of choice into an API, that's really sad. Yes it is frustrating to keep checking whether I case-spelled them correctly.
Well, it looks like you are forcing "us" to build a list of VideoIds than turn around and make more API calls when I was doing it previously with fewer.
It also means, I will have to manage the 50 items max paging twice, once for the playlist if it is over 50 videos and then manage manually my list of VideosIds paging when I turn around to make Videos.List calls.
Let me know if I missed an All-In-One call type of API, thank you.
Here is what I have now working, let me know if there is a better way
// 20150802
public async Task<List<YouTubeInfo>> PlaylistVideosInfo(String PlaylistID)
{
var YoutubeService = YouTubeService();
//
List<YouTubeInfo> VideoInfos = new List<YouTubeInfo>();
//
var NextPageToken = "";
while (NextPageToken != null)
{
//
var SearchListRequest = YoutubeService.PlaylistItems.List("snippet");
SearchListRequest.PlaylistId = PlaylistID;
SearchListRequest.MaxResults = 50;
SearchListRequest.PageToken = NextPageToken;
// Call the search.list method to retrieve results matching the specified query term.
var SearchListResponse = await SearchListRequest.ExecuteAsync();
// Collect Video IDs from this page
var VideoIDsBatch = new List<string>(); // batch Video detail search by 50 max
foreach (var searchResult in SearchListResponse.Items)
{
VideoIDsBatch.Add(searchResult.Snippet.ResourceId.VideoId);
}
// Make API call for this batch - expect a single page :(
var VideoListRequest = YoutubeService.Videos.List("snippet,contentDetails");
VideoListRequest.Id = String.Join(",", VideoIDsBatch);
VideoListRequest.MaxResults = 50;
var VideoListResponse = await VideoListRequest.ExecuteAsync();
// Collect each Video details
foreach (var VideoResult in VideoListResponse.Items)
{
YouTubeInfoAdd(VideoInfos, VideoResult);
}
// request next page
NextPageToken = SearchListResponse.NextPageToken;
}
// Return All Videos' detail
return VideoInfos;
}
I am posting this because it might help someone using the VersionOne API SDK Client. I wanted to change the status of a test programmatically, to one of the following categories: Ready, InTesting, Passed, or Failed. I originally tried to change the attribute 'Status.Name' however I would get an error that the attribute is a Read-Only attribute. Another suggestion was to create a new attribute with the same name and that the new attribute would override the previous read-only attribute with the same name. However, it appears that I was looking at it backwards.
internal void TestStatusPassed(string str_TestID)
{
var testId = Oid.FromToken(str_TestID, _context.MetaModel);
var query = new Query(testId);
var testType = _context.MetaModel.GetAssetType("Test");
var sourceAttribute = testType.GetAttributeDefinition("Status.Name");
query.Selection.Add(sourceAttribute);
var result = _context.Services.Retrieve(query);
var test = result.Assets[0];
var oldSource = GetValue(test.GetAttribute(sourceAttribute).Value);
test.SetAttributeValue(sourceAttribute, "Passed");
_context.Services.Save(test);
}
This code will throw an exception "Cannot change a read-only attribute"...
I pulled the XML data for one test from the VersionOne Rest API and noticed a relation named "TestStatus" and then it had a number '9123' assigned to it. So I moved that test manually to 'In Testing' and the "TestStatus" changed to '9121'. Then I moved it to failed and the "TestStatus" changed to '155'. I repeated this with several tests from different testsets and noticed that the numbers for each status were consistent and then changed the code slightly and then I was able to programmatically change the status of each test. I changed "Status.Name" to "Status" and "Passed" to "TestStatus:9123" and now it moves the test into the passed category programmatically.
internal void TestStatusPassed(string str_TestID)
{
var testId = Oid.FromToken(str_TestID, _context.MetaModel);
var query = new Query(testId);
var testType = _context.MetaModel.GetAssetType("Test");
var sourceAttribute = testType.GetAttributeDefinition("Status");
query.Selection.Add(sourceAttribute);
var result = _context.Services.Retrieve(query);
var test = result.Assets[0];
var oldSource = GetValue(test.GetAttribute(sourceAttribute).Value);
test.SetAttributeValue(sourceAttribute, "TestStatus:9123");
_context.Services.Save(test);
}