Previewing and Rejecting drafts using Contrib.Taxonomies in Orchard:
I have a requirement to preview (and if necessary, reject) taxonomy terms after an import.
I have attempted to do this with the following code:
A. I have changed the last line of the standard Import code to say:
Services.ContentManager.Create(term, VersionOptions.Draft);
(instead of VersionOptions.Published)
B. My preview screen has a "Publish" button, with a controller action with this code:
// remove current published terms
foreach (var term in _taxonomyService.GetTerms(taxonomyId)) {
_taxonomyService.DeleteTerm(term);
}
// publish draft terms created at import
foreach (var draftTerm in _MyTaxonomyService.GetDraftTerms(taxonomyId)) {
Services.ContentManager.Publish(draftTerm.As<ContentItem>());
}
C. The preview screen also has a "Reject" button, with a controller action with this code:
// delete drafts
foreach (var draftTerm in _MyTaxonomyService.GetDraftTerms(taxonomyId)) {
_taxonomyService.DeleteTerm(draftTerm);
}
With the above code, I can "Publish" as many times as I like and it works as expected.
However, if I "Reject" and then Publish I get "Permalinks in conflict" for the top-level term.
How can I remove the drafts without getting a permalinks conflict the next time I publish?
(Also note that removing drafts should not effect permalinks/display aliases of the current published terms).
Ok, the fundamental problem was that _taxonomyService.DeleteTerm() was firing the AutoRouteHandler.OnRemoved() event method.
This event method will call down the stack to AliasStorage.Remove(), which only takes path/aliassource as arguments, i.e. it will remove ALL AliasRecords that match the given path, i.e. aliases which the published version is still dependent on!
My solution to this problem was to replace the _taxonomyService.DeleteTerm() call with my own custom method _MyTaxonomyService.DeleteDraftTerm() for deleting draft terms. Note the removal of the AutoRoutePartHandler from the handler event calls:
public void DeleteDraftTerm(ContentItem contentItem)
{
var draftVersions = _contentItemVersionRepository.Fetch(x => x.ContentItemRecord == contentItem.Record && x.Latest && !x.Published);
var context = new RemoveContentContext(contentItem);
_handlers.Value.Invoke(handler => handler.Removing(context), Logger);
foreach (var version in draftVersions)
{
if (version.Latest)
{
version.Latest = false;
}
}
// comment this line out and replace with lines below
// _handlers.Value.Invoke(handler => handler.Removed(context), Logger);
var handlersExceptAutoRoute = _handlers.Value.Where(x => x.GetType() != typeof(AutoroutePartHandler));
handlersExceptAutoRoute.Invoke(handler => handler.Removed(context), Logger);
}
Tbh, I'm not really sure why this is necessary though. Before I added this new code, when I removed a draft content item with an autoroute part it seemed to result in there being no aliasrecords, but still having published autoroutepart records. So AutoRoutePartHandler.ProcessAlias() would think there was a duplicate published path and raise the conflict warning.
Related
I want to create a custom Kofax module. When it comes to the batch processing the scanned documents get converted to PDF files. I want to fetch these PDF files, manipulate them (add a custom footer to the PDF document) and hand them back to Kofax.
So what I know so far:
create Kofax export scripts
add a custom module to Kofax
I have the APIRef.chm (Kofax.Capture.SDK.CustomModule) and the CMSplit as an example project. Unfortunately I struggle getting into it. Are there any resources out there showing step by step how to get into custom module development?
So I know that the IBatch interface represents one selected batch and the IBatchCollection represents the collection of all batches.
I would just like to know how to setup a "Hello World" example and could add my code to it and I think I don't even need a WinForms application because I only need to manipulate the PDF files and that's it...
Since I realized that your question was rather about how to create a custom module in general, allow me to add another answer. Start with a C# Console Application.
Add Required Assemblies
Below assemblies are required by a custom module. All of them reside in the KC's binaries folder (by default C:\Program Files (x86)\Kofax\CaptureSS\ServLib\Bin on a server).
Setup Part
Add a new User Control and Windows Form for setup. This is purely optional - a CM might not even have a setup form, but I'd recommend adding it regardless. The user control is the most important part, here - it will add the menu entry in KC Administration, and initialize the form itself:
[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
public interface ISetupForm
{
[DispId(1)]
AdminApplication Application { set; }
[DispId(2)]
void ActionEvent(int EventNumber, object Argument, out int Cancel);
}
[ClassInterface(ClassInterfaceType.None)]
[ProgId("Quipu.KC.CM.Setup")]
public class SetupUserControl : UserControl, ISetupForm
{
private AdminApplication adminApplication;
public AdminApplication Application
{
set
{
value.AddMenu("Quipu.KC.CM.Setup", "Quipu.KC.CM - Setup", "BatchClass");
adminApplication = value;
}
}
public void ActionEvent(int EventNumber, object Argument, out int Cancel)
{
Cancel = 0;
if ((KfxOcxEvent)EventNumber == KfxOcxEvent.KfxOcxEventMenuClicked && (string)Argument == "Quipu.KC.CM.Setup")
{
SetupForm form = new SetupForm();
form.ShowDialog(adminApplication.ActiveBatchClass);
}
}
}
Runtime Part
Since I started with a console application, I could go ahead and put all the logic into Program.cs. Note that is for demo-purposes only, and I would recommend adding specific classes and forms later on. The example below logs into Kofax Capture, grabs the next available batch, and just outputs its name.
class Program
{
static void Main(string[] args)
{
AppDomain.CurrentDomain.AssemblyResolve += (sender, eventArgs) => KcAssemblyResolver.Resolve(eventArgs);
Run(args);
return;
}
static void Run(string[] args)
{
// start processing here
// todo encapsulate this to a separate class!
// login to KC
var login = new Login();
login.EnableSecurityBoost = true;
login.Login();
login.ApplicationName = "Quipu.KC.CM";
login.Version = "1.0";
login.ValidateUser("Quipu.KC.CM.exe", false, "", "");
var session = login.RuntimeSession;
// todo add timer-based polling here (note: mutex!)
var activeBatch = session.NextBatchGet(login.ProcessID);
Console.WriteLine(activeBatch.Name);
activeBatch.BatchClose(
KfxDbState.KfxDbBatchReady,
KfxDbQueue.KfxDbQueueNext,
0,
"");
session.Dispose();
login.Logout();
}
}
Registering, COM-Visibility, and more
Registering a Custom Module is done via RegAsm.exe and ideally with the help of an AEX file. Here's an example - please refer to the documentation for more details and all available settings.
[Modules]
Minimal CM
[Minimal CM]
RuntimeProgram=Quipu/CM/Quipu.KC.CM/Quipu.KC.CM.exe
ModuleID=Quipu.KC.CM.exe
Description=Minimal Template for a Custom Module in C#
Version=1.0
SupportsTableFields=True
SupportsNonImageFiles=True
SetupProgram=Minimal CM Setup
[Setup Programs]
Minimal CM Setup
[Minimal CM Setup]
Visible=0
OCXFile=Quipu/CM/Quipu.KC.CM/Quipu.KC.CM.exe
ProgID=Quipu.KC.CM.Setup
Last but not least, make sure your assemblies are COM-visible:
I put up the entire code on GitHub, feel free to fork it. Hope it helps.
Kofax exposes a batch as an XML, and DBLite is basically a wrapper for said XML. The structure is explained in AcBatch.htm and AcDocs.htm (to be found under the CaptureSV directory). Here's the basic idea (just documents are shown):
AscentCaptureRuntime
Batch
Documents
Document
A single document has child elements itself such as pages, and multiple properties such as Confidence, FormTypeName, and PDFGenerationFileName. This is what you want. Here's how you would navigate down the document collection, storing the filename in a variable named pdfFileName:
IACDataElement runtime = activeBatch.ExtractRuntimeACDataElement(0);
IACDataElement batch = runtime.FindChildElementByName("Batch");
var documents = batch.FindChildElementByName("Documents").FindChildElementsByName("Document");
for (int i = 0; i < documents.Count; i++)
{
// 1-based index in kofax
var pdfFileName = documents[i + 1]["PDFGenerationFileName"];
}
Personally, I don't like this structure, so I created my own wrapper for their wrapper, but that's up to you.
With regard to the custom module itself, the sample shipped is already a decent start. Basically, you would have a basic form that shows up if the user launches the module manually - which is entirely optional if work happens in the back, preferably as Windows Service. I like to start with a console application, adding forms only when needed. Here, I would launch the form as follows, or start the service. Note that I have different branches in case the user wants to install my Custom Module as service:
else if (Environment.UserInteractive)
{
// run as module
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new RuntimeForm(args));
}
else
{
// run as service
ServiceBase.Run(new CustomModuleService());
}
}
The runtime for itself just logs you into Kofax Capture, registers event handlers, and processes batch by batch:
// login to KC
cm = new CustomModule();
cm.Login("", "");
// add progress event handlers
cm.BatchOpened += Cm_BatchOpened;
cm.BatchClosed += Cm_BatchClosed;
cm.DocumentOpened += Cm_DocumentOpened;
cm.DocumentClosed += Cm_DocumentClosed;
cm.ErrorOccured += Cm_ErrorOccured;
// process in background thread so that the form does not freeze
worker = new BackgroundWorker();
worker.DoWork += (s, a) => Process();
worker.RunWorkerAsync();
Then, your CM fetches the next batch. This can either make use of Kofax' Batch Notification Service, or be based on a timer. For the former, just handle the BatchAvailable event of the session object:
session.BatchAvailable += Session_BatchAvailable;
For the latter, define a timer - preferrably with a configurable polling interval:
pollTimer.Interval = pollIntervalSeconds * 1000;
pollTimer.Elapsed += PollTimer_Elapsed;
pollTimer.Enabled = true;
When the timer elapses, you could do the following:
private void PollTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
{
mutex.WaitOne();
ProcessBatches();
mutex.ReleaseMutex();
}
On an xpage I calculate the message for an xp:confirm control:
var arr = viewScope.get("attachmentsAll");
if(arr.length>0){
return "";
}else{
return arr.length + " Are you sure want to upload the file?";
}
the viewScope is being updated after the event has been executed. I check this via a xp:text and I notice that this assumption is true.
<xp:text escape="true" id="computedField1"><xp:this.value><![CDATA[#{javascript:var arr = viewScope.get("attachmentsAll")
return arr.length + " number?"}]]></xp:this.value></xp:text>
The xp:confirm and xp:text reside in the same panel that is being partially updated after the event.
Can anyone explain me why the value for the viewScope variable is updated in the xp:text control and not in the xp:confirm control?
The main idea of my answer to your previous question was to put hidden input with computed value. What if you try to use <xp:this.script> instead of xp:confirm, and get the confirmation message from that hidden input the same way?
Update
The reason and the alternative solution that does not require to make any changes to existing xpage
It came out that the back-end instance of xp:confirm evaluates the new message correctly. The new value is even being sent to the browser with the response to ajax request. But one of the functions of XSP client module is built so that it won't update the querySubmit listener function if there already exists one with the same name. So, we are stuck with the old confirmation function that contains the old message. There is a way to override this behavior without breaking any other features. I have tried this and it works for me.
Create a new JavaScript library (client-side). Add the code:
if (!XSP._pushListenerTuned) {
XSP.__pushListener = XSP._pushListener;
XSP._pushListener = function x__pl(listeners, formId, clientId, scriptId, listener) {
if (scriptId && scriptId.endsWith("_confirm")) {
for (var i = 0; i < listeners.length; i++) {
if (scriptId == listeners[i].scriptId) {
listeners.splice(i, 1);
}
}
listeners.push(new this._SubmitListener(formId, listener, clientId, scriptId));
} else {
XSP.__pushListener(listeners, formId, clientId, scriptId, listener);
}
}
XSP._pushListenerTuned = true;
}
Attach your new library as resource globally via theme or as page resource on required page. I guess placing the above code as scriptBlock on a required page should also work. Now, any xp:confirm component on any page (if you used theme resource), or on particular page and everywhere after visiting this page (if you used page resource or scriptBlock), will work as naturally expected.
I have a custom module that uses Kentico API (DocumentHelper) to update certain fields of my document and then publish but I do not want it to trigger the event handlers that are linked to my document page type. I tried adding comments to .Publish("admin_edit") hoping that I can catch it from the WorkflowEventargs parameter but the VersionComment property always return null. Is there a way to accomplish this in Kentico?
update field:
var document = DocumentHelper.GetDocument(documentID, tree);
var workflowManager = WorkflowManager.GetInstance(tree);
var workflow = workflowManager.GetNodeWorkflow(document);
if (workflow != null)
{
document.CheckOut();
document.SetValue("SomeFIeld", "some value");
document.Update(true);
document.CheckIn();
document.Publish("admin_edit");
}
event handler:
public override void Init()
{
WorkflowEvents.Publish.After += Publish_After;
}
private void Publish_After(object sender, WorkflowEventArgs e)
{
if (!string.IsNullOrEmpty(e.VersionComment) &&
e.VersionComment.Contains("admin_edit"))
return;
}
You always get null for Version information, because that is related to the 'Page versioning' events, specially for 'SaveVersion'. You can find more about that on this link. If you expand 'Properties' you will see which properties are populated for the specific event. In your case, you can try something like this, to add your message for last version and then check for that comment on 'Publish_After' event, see code bellow:
var document = DocumentHelper.GetDocument(documentID, tree);
var workflowManager = WorkflowManager.GetInstance(tree);
var workflow = workflowManager.GetNodeWorkflow(document);
if (workflow != null)
{
document.CheckOut();
document.SetValue("SomeFIeld", "some value");
document.Update(true);
document.CheckIn(versionComment: "admin_edit");
document.Publish();
}
and then, in event handler, take last version and check for comment like this:
if (e.PublishedDocument?.VersionHistory?.Count > 0)
{
var lastVersion = e.PublishedDocument.VersionHistory[0] as VersionHistoryInfo;
if (lastVersion.VersionComment.Equals("admin_edit"))
{
return;
}
}
NOTE: In case that you have a lot of concurrent content editors, there is a chance that your last version is not version from API (someone changed content and saved it right after your API call made change). There is a low chance for that, but still is possible. If this is something that you will use often, you must take it in consideration. This code is tested for Kentico 11.
I have implemented List.cshtml to provide a custom display for an image gallery. This is the first time I have tried to override a Projection with a Template and at first it seemed to work fine. Then I noticed that when I try to access the Projection on the backend Orchard 1.7 falls over with:
RuntimeBinderException 'Orchard.ContentManagement.ContentItem' does
not contain a definition for 'TagsPart'
Here is some code from the template List.cshtml:
List<TagRecord> uniqueTags = new List<TagRecord>();
List<dynamic> items = Model.Items;
if (items != null && items.Any())
{
foreach (var item in items)
{
if (item != null && item.ContentItem != null)
{
TagsPart part = item.ContentItem.TagsPart;
if (part != null && part.CurrentTags != null)
{
foreach (var t in part.CurrentTags)
{
if (!uniqueTags.Contains(t))
{
uniqueTags.Add(t);
}
}
}
}
}
I am ignorant on a couple of points, which I suspect may be causing the error:
How to specify a template for a Projection (more specific than 'List.cshtml'). Can I use Placement.info? How?
How should I test for the presence of a specific part in the ContentItem? Just assigning TagsPart part = item.ContentItem.TagsPart; throws the exception above.
UPDATE: I had implemented this as a Module; that is, the List.cshtml was in the Views folder of a simple Module. If I move List.cshtml to the Theme then the problem goes away. However, I would still prefer to use a module so that the layout is independent of the theme.
Orchard 1.7 includes a new query layout provider called 'Shape'. I simply used this provider, gave it a Shape Type of 'LightboxIsotope', and created a view called 'LightboxIsotope.cshtml'.
In a projection you can customize the html rendered for each property on the List. In order to acomplish this you need to go to your query, and add a new Layout, choose the properties you need, and set everything you want.
If the layouts provided in the Queries Module donĀ“t fullfill your requirements, you can allways create your own layout provider, this blog post shows an example:
http://www.stevetaylor.me.uk/image-carousel-using-twitter-bootstrap-and-orchard-cms-projections
I have a simple IBackgroundTask implementation that performs a query and then either performs an insert or one or more updates depending on whether a specific item exists or not. However, the updates are not persisted, and I don't understand why. New items are created just as expected.
The content item I'm updating has a CommonPart and I've tried authenticating as a valid user. I've also tried flushing the content manager at the end of the Sweep method. What am I missing?
This is my Sweep, slightly edited for brevity:
public void Sweep()
{
// Authenticate as the site's super user
var superUser = _membershipService.GetUser(_orchardServices.WorkContext.CurrentSite.SuperUser);
_authenticationService.SetAuthenticatedUserForRequest(superUser);
// Create a dummy "Person" content item
var item = _contentManager.New("Person");
var person = item.As<PersonPart>();
if (person == null)
{
return;
}
person.ExternalId = Random.Next(1, 10).ToString();
person.FirstName = GenerateFirstName();
person.LastName = GenerateLastName();
// Check if the person already exists
var matchingPersons = _contentManager
.Query<PersonPart, PersonRecord>(VersionOptions.AllVersions)
.Where(record => record.ExternalId == person.ExternalId)
.List().ToArray();
if (!matchingPersons.Any())
{
// Insert new person and quit
_contentManager.Create(item, VersionOptions.Draft);
return;
}
// There are at least one matching person, update it
foreach (var updatedPerson in matchingPersons)
{
updatedPerson.FirstName = person.FirstName;
updatedPerson.LastName = person.LastName;
}
_contentManager.Flush();
}
Try to add _contentManager.Publish(updatedPerson). If you do not want to publish, but just to save, you don't need to do anything more, as changes in Orchard as saved automatically unless the ambient transaction is aborted. The call to Flush is not necessary at all. This is the case both during a regular request and on a background task.