workflow replicator activity: how to determine if task was approved or rejected - sharepoint

I have basic workflow with replicator activity inside it. Replicator contains my custom sequence activity with standard create task --> ontaskchanged --> complete task sequence.
Now: tasks are created and can be completed without problem. The thing is I cannot find a way to get a value of completed task. Was it approved or rejected ?
Please provide couple lines of code of replicator's ChildCompleted event to get anything out of Sequence activity instance (or any other way).
thanks
UPDATE: It seems in order to exchange values between instances of workflow you need to use DependencyProperty. So solution here is:
1) add DependencyProperty to parent workflow and add property which you will use to store value like this:
public static DependencyProperty childStatusProperty =
System.Workflow.ComponentModel.DependencyProperty.Register("childStatus",
typeof(string), typeof(parentWorkflowTypeName));
public string childStatus
{
get
{
return (string)base.GetValue(childStatusProperty);
}
set
{
base.SetValue(childStatusProperty, value);
}
}
2) in custom sequence activity access parent's instance and use defined DependencyProperty to set property to value like this:
private void completeTask1_MethodInvoking(object sender, EventArgs e)
{
var replicator = this.Parent;
var workflowParent = (parentWorkflowTypeName)replicator.Parent;
workflowParent.childStatus = "my custom status value";
}
3) read this value using normal property:
//from parent workflow
string status = childStatus;

The issue is that you have to record somewhere the list of all tasks created. I guess you are creating the tasks in parallel (not sequential).
I had the same issue, it took me a while to fix this.
Please check this link as a good starting point: http://rmanimaran.wordpress.com/2010/12/02/sharepoint-workflow-replicator-parallel-approval-problem-solution/

Related

How to access Shopware\Core\System\SalesChannel\SalesChannelContext in Entity Related event subscribers in shopware 6?

I have built a custom subscriber in my plugin for Shopware 6 that subscribes to
\Shopware\Core\Content\Product\ProductEvents::PRODUCT_WRITTEN_EVENT = 'product.written';
public function onProductWrittenEntity(EntityWrittenEvent $event): void
{
//$event->getContext() is returning the Shopware\Core\Framework\Context
}
I want to get domain URL of this current salesChannel having those productIds which are currently written. how can i do that?
You can obtain the salesChannelId by running the following code:
$event->getContext()->getSource()->getSalesChannelId()
With that salesChannelId and inserting the SalesChannelRepository via the services.xml into your Subscriber, you can load the required information from that sales-channel.
When you edit the products over the API or inside the administration, you are in a "admin context", that means no sales-channel is available. This is because your changes are globally and you are not limited to a specific sales-channel.
The SalesChannelContext is only available if the action that was triggered originated in the storefront or came over the store-api.
Long story short:
You can't access the salesChannelContext from the EntityWrittenEvent, as most of the times there is no specific SalesChannel, where the event was triggered.
Maybe you can explain your use case a little bit more, so we can suggest alternatives.
in case someone run in this Problem:
You can for example Subscribe to the Event "SalesChannelContextResolvedEvent". Store all the Data in a variable as type array (Argument2 from the construct, "$this->saleschannelContext"). And call it in where ever you need it, for example an other event (you can call it so -> "$this->salechannelContext").
public function __construct(EntityRepository $discountExtensionRepository){
$this->discountExtensionRepository = $discountExtensionRepository;
$this->salechannelContext = array();
}
public static function getSubscribedEvents(): array{
return [
SalesChannelContextResolvedEvent::class => "onPageLoaded",
ProductEvents::PRODUCT_LOADED_EVENT => 'onProductsLoaded'
];
}
public function onPageLoaded(SalesChannelContextResolvedEvent $event){
$this->salechannelContext = $event->getSaleschannelContext();
}
public function onProductsLoaded(EntityLoadedEvent $event):void{
dump($this->salechannelContext);
}
Probably not the best practice way because i guess there is a way to get it directly from die Product event, but it is one way of manys.
[EDIT]: You can get all Storefront and Product informations with this event.
use Shopware\Core\System\SalesChannel\Entity\SalesChannelEntityLoadedEvent;
public static function getSubscribedEvents(): array{
return [
'sales_channel.product.loaded' => 'onSalesChannelLoaded'
];
}
public function onSalesChannelLoaded(SalesChannelEntityLoadedEvent $event):void{

Data View Customization in Extension

I have overwritten the data view for a custom graph in an extension, which returns the correct data without issue, both by re-declaring the view, and using the delegate object techniques. The issue is that when I do, the AllowSelect/AllowDelete modifications on the view in the primary graph stop working, once I comment out the overwrite, the logic works as normal.
Not sure what I'm missing, but any thoughts would be appreciated
Edit: To clarify, on the main graph, without the extension, the data retrieval and Allow... work without issue
public class FTTicketEntry : PXGraph<FTTicketEntry, UsrFTHeader>
{
public PXSelect<UsrFTHeader> FTHeader;
public PXSelect<UsrFTGridLabor, Where<UsrFTGridLabor.ticketNbr, Equal<Current<UsrFTHeader.ticketNbr>>>> FTGridLabor;
And with the extension, the data is returned correctly from the modified view, but the Allow... do not work from the main graph, only when entered on the extension
public class FTTicketEntryExtension : PXGraphExtension<FTTicketEntry>
{
public PXSelect<UsrFTGridLabor, Where<UsrFTGridLabor.ticketNbr, Equal<Current<UsrFTHeader.ticketNbr>>, And<UsrFTGridLabor.projectID, Equal<Current<UsrFTHeader.projectID>>, And<UsrFTGridLabor.taskID, Equal<Current<UsrFTHeader.taskID>>>>>> FTGridLabor;
I have also tried the other process on the extension with the same results, the data is filtered correctly, but the Allow... commands fail.
public PXSelect<UsrFTGridLabor, Where<UsrFTGridLabor.ticketNbr, Equal<Current<UsrFTHeader.ticketNbr>>>> FTGridLabor;
public virtual IEnumerable fTGridLabor()
{
foreach (PXResult<UsrFTGridLabor> record in Base.FTGridLabor.Select())
{
UsrFTGridLabor p = (UsrFTGridLabor)record;
if (p.ProjectID == Base.FTHeader.Current.ProjectID && p.TaskID == Base.FTHeader.Current.TaskID)
{
yield return record;
}
}
}
My main concern with not wanting to use PXSelectReadOnly, is that there is a status field on the header which drives when certain combinations of the conditions are required and are called on the rowselected events, sometimes all and sometimes none, and the main issue is that I obviously don't want to have to replicate all of the UI logic into the extension, when overwriting the view was the main intent of the extension for the screen.
Appreciate the assistance, and hopefully you see something I'm overlooking or have missed
Thanks
Every BLC instance stores all actual data views and actions within 2 collections: Views and Actions. Whenever, you customize a data view or an action with a BLC extension, the original data view / action gets replaced in the appropriate collection by your custom object declared within the extension class. After the original data view or action was removed from the appropriate collection, it's quite obvious that any change made to the original object will not make any effect, since the original object is not used by the BLC anymore.
The easiest way to access actual object from either of these 2 collections would be as follows: Views["FTGridLabor"].Allow... = value;
Alternatively, you might operate with AllowInsert, AllowUpdate and AllowDelete properties on the cache level: FTGridLabor.Cache.Allow... = value;
By changing AllowXXX properties on the cache level, you completely eliminate the need for setting AllowXXX on the data view, since PXCache.AllowXXX properties have higher priority when compared to identical properties on the data view level:
public class PXView
{
...
protected bool _AllowUpdate = true;
public bool AllowUpdate
{
get
{
if (_AllowUpdate && !IsReadOnly)
{
return Cache.AllowUpdate;
}
return false;
}
set
{
_AllowUpdate = value;
}
}
...
}
With all that said, to resolve your issue with UI Logic not applying to modified view, please consider one of the following options:
Set AllowXXX property values in both the original BLC and its extensions via the object obtained from the Views collection:
Views["FTGridLabor"].Allow... = value;
operate with AllowXXX property values on the cache level: FTGridLabor.Cache.Allow... = value;
First check if your DataView should/should not be a variant of PXSelectReadonly.
Without more information my advice would be to set the Allow properties in Initialize method of your extension:
public override void Initialize()
{
// This is similar to PXSelectReadonly
DataView.AllowDelete = false;
DataView.AllowInsert = false;
DataView.AllowUpdate = false;
}

Spring Aggregation Group

I did create an aggregate service as below
#EnableBinding(Processor.class)
class Configuration {
#Autowired
Processor processor;
#ServiceActivator(inputChannel = Processor.INPUT)
#Bean
public MessageHandler aggregator() {
AggregatingMessageHandler aggregatingMessageHandler =
new AggregatingMessageHandler(new DefaultAggregatingMessageGroupProcessor(),
new SimpleMessageStore(10));
//AggregatorFactoryBean aggregatorFactoryBean = new AggregatorFactoryBean();
//aggregatorFactoryBean.setMessageStore();
aggregatingMessageHandler.setOutputChannel(processor.output());
//aggregatorFactoryBean.setDiscardChannel(processor.output());
aggregatingMessageHandler.setSendPartialResultOnExpiry(true);
aggregatingMessageHandler.setSendTimeout(1000L);
aggregatingMessageHandler.setCorrelationStrategy(new ExpressionEvaluatingCorrelationStrategy("requestType"));
aggregatingMessageHandler.setReleaseStrategy(new MessageCountReleaseStrategy(3)); //ExpressionEvaluatingReleaseStrategy("size() == 5")
aggregatingMessageHandler.setExpireGroupsUponCompletion(true);
aggregatingMessageHandler.setGroupTimeoutExpression(new ValueExpression<>(3000L)); //size() ge 2 ? 5000 : -1
aggregatingMessageHandler.setExpireGroupsUponTimeout(true);
return aggregatingMessageHandler;
}
}
Now i want to release the group as soon as a new group is created, so i only have one group at a time.
To be more specific i do receive two types of requests 'PUT' and 'DEL' . i want to keep aggregating per the above rules but as soon as i receive a request type other than what i am aggregating i want to release the current group and start aggregating the new Type.
The reason i want to do this is because these requests are sent to another party that don't support having PUT and DEL requests at the same time and i can't delay any DEL request as sequence between PUT and DEL is important.
I understand that i need to create a custom release Pojo but will i be able to check the current groups ?
For Example
If i receive 6 messages like below
PUT PUT PUT DEL DEL PUT
they should be aggregated as below
3PUT
2 DEL
1 PUT
OK. Thank you for sharing more info.
Yes, you custom ReleaseStrategy can check that message type and return true to lead to the group completion function.
As long as you have only static correlationKey, so only one group is there in the store. When your message is stepping to the ReleaseStrategy, there won't be much magic just to check the current group for completion signal. Since there are no any other groups in the store, there is no need any complex release logic.
You should add expireGroupsUponCompletion = true to let the group to be removed after completion and the next message will form a new group for the same correlationKey.
UPDATE
Thank you for further info!
So, yes, your original PoC is good. And even static correlationKey is fine, since you are just going to collect incoming messages to batches.
Your custom ReleaseStrategy should analyze MessageGroup for a message with different key and return true in that case.
The custom MessageGroupProcessor should filter a message with different key from the output List and send that message to the aggregator back to let to form a new group for a sequence for its key.
i ended up implementing the below ReleaseStrategy as i found it simpler than removing message and queuing it again.
class MessageCountAndOnlyOneGroupReleaseStrategy implements org.springframework.integration.aggregator.ReleaseStrategy {
private final int threshold;
private final MessageGroupProcessor messageGroupProcessor;
public MessageCountAndOnlyOneGroupReleaseStrategy(int threshold,MessageGroupProcessor messageGroupProcessor) {
super();
this.threshold = threshold;
this.messageGroupProcessor = messageGroupProcessor;
}
private MessageGroup currentGroup;
#Override
public boolean canRelease(MessageGroup group) {
if(currentGroup == null)
currentGroup = group;
if(!group.getGroupId().equals(currentGroup.getGroupId())) {
messageGroupProcessor.processMessageGroup(currentGroup);
currentGroup = group;
return false;
}
return group.size() >= this.threshold;
}
}
Note that i did used new HeaderAttributeCorrelationStrategy("request_type") instead of just FOO for CollorationStrategy

How to stop 'Send order notification' & 'Send payment notification' in checkout process from code side in kentico

I know there is option in kentico admin setting to stop the sending notification email. but I want to check this in the code for my customization. so could you please suggest me where should I get the code in kentico.
Setting in kentico
Please refer to the official documentation.
You need to use SettingsKeyInfoProvider:
SettingsKeyInfoProvider.SetValue("CMSSettingName", "SiteName", value);
Leave out the site name parameter if you want to set it globally.
The settings names you are looking for are CMSStoreSendOrderNotification and CMSStoreSendPaymentNotification.
You can find more settings by querying the DB:
SELECT * FROM [CMS_SettingsKey] where keyname like '%cmsstoresend%'
If you are looking to intercept an action when a notification is being sent, you can use Global events for the EmailInfo object like this:
[assembly: RegisterModule(typeof(GlobalEventsModule))]
public class GlobalEventsModule : Module
{
public GlobalEventsModule() : base (typeof(GlobalEventsModule).Name)
{
}
protected override void OnInit()
{
base.OnInit();
EmailInfo.TYPEINFO.Events.Insert.Before += Insert_Before;
}
private void Insert_Before(object sender, ObjectEventArgs e)
{
// executed before an e-mail is inserted into DB
var email = (EmailInfo)e.Object;
}
}
To cancel the execution in code you can call Cancel() method (although you might get exceptions in this case - you have to test for yourself in your scenario):
private void Insert_Before(object sender, ObjectEventArgs e)
{
var email = (EmailInfo)e.Object;
e.Cancel();
}
This will also work only if you are using Email queue (which is highly recommended anyway) and will be executed for all outgoing e-mails, not just notifications.
Using the CMS.Ecommerce library you can check these settings through the API
SiteInfoIdentifier sii = new SiteInfoIdentifier(SiteContext.CurrentSiteID);
bool sendOrderNotificationEmail = CMS.Ecommerce.ECommerceSettings.SendOrderNotification(sii);
If you wanted to set them programmatically you would have to use the SettingsKeyInfoProvider
SettingsKeyInfoProvider.SetValue("CMSStoreSendOrderNotification ", false);

Subsonic - Where do i include my busines logic or custom validation

Im using subsonic 2.2
I tried asking this question another way but didnt get the answer i was looking for.
Basically i ususally include validation at page level or in my code behind for my user controls or aspx pages. However i haev seen some small bits of info advising this can be done within partial classes generated from subsonic.
So my question is, where do i put these, are there particular events i add my validation / business logic into such as inserting, or updating. - If so, and validation isnt met, how do i stop the insert or update. And if anyone has a code example of how this looks it would be great to start me off.
Any info greatly appreciated.
First you should create a partial class for you DAL object you want to use.
In my project I have a folder Generated where the generated classes live in and I have another folder Extended.
Let's say you have a Subsonic generated class Product. Create a new file Product.cs in your Extended (or whatever) folder an create a partial class Product and ensure that the namespace matches the subsonic generated classes namespace.
namespace Your.Namespace.DAL
{
public partial class Product
{
}
}
Now you have the ability to extend the product class. The interesting part ist that subsonic offers some methods to override.
namespace Your.Namespace.DAL
{
public partial class Product
{
public override bool Validate()
{
ValidateColumnSettings();
if (string.IsNullOrEmpty(this.ProductName))
this.Errors.Add("ProductName cannot be empty");
return Errors.Count == 0;
}
// another way
protected override void BeforeValidate()
{
if (string.IsNullOrEmpty(this.ProductName))
throw new Exception("ProductName cannot be empty");
}
protected override void BeforeInsert()
{
this.ProductUUID = Guid.NewGuid().ToString();
}
protected override void BeforeUpdate()
{
this.Total = this.Net + this.Tax;
}
protected override void AfterCommit()
{
DB.Update<ProductSales>()
.Set(ProductSales.ProductName).EqualTo(this.ProductName)
.Where(ProductSales.ProductId).IsEqualTo(this.ProductId)
.Execute();
}
}
}
In response to Dan's question:
First, have a look here: http://github.com/subsonic/SubSonic-2.0/blob/master/SubSonic/ActiveRecord/ActiveRecord.cs
In this file lives the whole logic I showed in my other post.
Validate: Is called during Save(), if Validate() returns false an exception is thrown.
Get's only called if the Property ValidateWhenSaving (which is a constant so you have to recompile SubSonic to change it) is true (default)
BeforeValidate: Is called during Save() when ValidateWhenSaving is true. Does nothing by default
BeforeInsert: Is called during Save() if the record is new. Does nothing by default.
BeforeUpdate: Is called during Save() if the record is new. Does nothing by default.
AfterCommit: Is called after sucessfully inserting/updating a record. Does nothing by default.
In my Validate() example, I first let the default ValidatColumnSettings() method run, which will add errors like "Maximum String lenght exceeded for column ProductName" if product name is longer than the value defined in the database. Then I add another errorstring if ProductName is empty and return false if the overall error count is bigger than zero.
This will throw an exception during Save() so you can't store the record in the DB.
I would suggest you call Validate() yourself and if it returns false you display the elements of this.Errors at the bottom of the page (the easy way) or (more elegant) you create a Dictionary<string, string> where the key is the columnname and the value is the reason.
private Dictionary<string, string> CustomErrors = new Dictionary<string, string>
protected override bool Validate()
{
this.CustomErrors.Clear();
ValidateColumnSettings();
if (string.IsNullOrEmpty(this.ProductName))
this.CustomErrors.Add(this.Columns.ProductName, "cannot be empty");
if (this.UnitPrice < 0)
this.CustomErrors.Add(this.Columns.UnitPrice, "has to be 0 or bigger");
return this.CustomErrors.Count == 0 && Errors.Count == 0;
}
Then if Validate() returns false you can add the reason directly besides/below the right field in your webpage.
If Validate() returns true you can safely call Save() but keep in mind that Save() could throw other errors during persistance like "Dublicate Key ...";
Thanks for the response, but can you confirm this for me as im alittle confused, if your validating the column (ProductName) value within validate() or the beforevalidate() is string empty or NULL, doesnt this mean that the insert / update has already been actioned, as otherwise it wouldnt know that youve tried to insert or update a null value from the UI / aspx fields within the page to the column??
Also, within asp.net insert or updating events we use e.cancel = true to stop the insert update, if beforevalidate failes does it automatically stop the action to insert or update?
If this is the case, isnt it eaiser to add page level validation to stop the insert or update being fired in the first place.
I guess im alittle confused at the lifecyle for these methods and when they come into play

Resources