mercoledì 22 dicembre 2010

SharePoint 2010 - Workflow per cambiare i permessi ad un documento

Per un cliente avevo l'esigenza di rendere un documento immodificabile e non cancellabile.
In questo post ho intenzione di raccontarvi come ho risolto il problema con le nuove activity di SharePoint Designer 2010, con una sorpesa finale.
Vi raconterò:
  • Come creare i worflow per togliere e rimettere i permessi al documento
  • Come fare delle custom action accessibili dal ribbon con SharePoint Designer, per eseguire il workflow
  • Come crare delle custom action che tramite ECMA Client Object Model eseguono un workflow associato alla lista
Creare i workflow per manipolari i permissi
Prima di tutto si comincia creando un  nuovo Workflow con SharePoint Designer, molto comodi sono i Globally Reusable Workflow, che possono essere associati a verie liste e usati in tutti i siti della site collection (ricordatevi di premere il pulsante Publish Globally nel ribbon del Designer). Il primo step è quello di creare un Impersonation Step che ci consente di impersonare in fase di esecuzione un altro utente e nella fattispecie l'utente che ha creato il workflow. In questo modo è possibile far eseguire a utenti che non ne avrebbero i permessi determinate azioni.


L'azione da aggiungere è "Replece List Item permissions". Di seguito i passi per aggiungere, modificare o rimuovere permessi ad un item.









Per vedere tutte le immagini.
SP2010 Permission Workflow

Usando l'action "Inherit parent permissions for item..." è possibile riportare la situazione dei permessi alla condizione originale.

Quick Step con SharePoint Designer
Nel ribbon di ogni Library (tab Library), nella sezione "Customize Library" c'è il bottone "New Quick Step". Premendo questo bottone si apre SharePoint Designer sull'edit della lista corrente e viene visualizzato un pannello per configurare una custom action che sarà visibile nel tab Documents sotto la voce "Quick Steps". Da questa interfaccia è possibile associare all'azione: un link ad una pagina aspx del sito, l'avvio di un workflow associato alla lista oppure un link ad una URL esterna. Vedi fugure di seguito.




Custom Action per attivare un workflow
Visto che da Designer è possibile creare una custom action che consente di avviare un workflow, ho voluto provare a creare delle mie custom action,da posizionare in punti diversi del ribbon e con logiche di attivazione tutte mie, che facessero la stessa cosa. Il primo passo è stato cercare di capire quale azione fosse associata e questi Quick Step. Analizzando un po' il codice HTML generato e relativi Javascript ho scoperto, che queste actuion invocano la pagina "/_layouts/wfstart.aspx" con i seguenti parametri:
  • List={ListId}
  • ID={ItemId}
  • TemplateID
  • AssociationName
I primi due parametri sono chiari, l'ultimo è il nome che viene dato al workflow quando lo si associa alla lista, ma TemplateID ho fatto un po' più fatica a capire cosa fosse. Alla fine è risultato essere il GUID dell'associazione del workflow alla lista. Nei miei script è stato semplice ricavare i primi due parametri, l'ultimo ho deciso di metterlo staticamente nel codice (intanto stavo scrivendo action per attivare workflow che associavo io sempre con lo stesso nome), mentre il TemplateID ho sudato un po' a tirarlo fuori usando ECAM Client Object Model (ho spulciato un po' l'SDK). Alla fine il risultato è stato quanto segue:

<?xml version="1.0" encoding="utf-8"?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  <CustomAction
  Id="Install JS"
  ScriptSrc="/_layouts/RibbonButtonsDemo/demo.js"
  Location="ScriptLink"
  Sequence="100">
  </CustomAction>
  <CustomAction Id="SharePoint.Ribbon.LockDocuments"
                Title="Lock Documents"
                Location="CommandUI.Ribbon"
                RegistrationType="List"
                RegistrationId="101">
    <CommandUIExtension>
      <CommandUIDefinitions>
        <CommandUIDefinition Location="Ribbon.Documents.Manage.Controls._children">
          <Button Id="Ribbon.Documents.New.MsysLockDocsButton"
          Alt="Lock selected document"
          Sequence="5"
          LabelText="Lock"
          Image16by16="/_layouts/images/RibbonButtonsDemo/lock-icon16x16.png"
          Image32by32="/_layouts/images/RibbonButtonsDemo/page-lock-icon32x32.png"
          Command="Command.LockButton"/>
        </CommandUIDefinition>
        <CommandUIDefinition Location="Ribbon.Documents.Manage.Controls._children">
          <Button Id="Ribbon.Documents.New.MsysUnlockDocsButton"
          Alt="Unlock selected document"
          Sequence="5"
          LabelText="Unlock"
          Image16by16="/_layouts/images/RibbonButtonsDemo/lock-off-icon16x16.png"
          Image32by32="/_layouts/images/RibbonButtonsDemo/page-lock-off-icon32x32.png"
          Command="Command.UnlockButton"/>
        </CommandUIDefinition>
      </CommandUIDefinitions>
      <CommandUIHandlers>
        <CommandUIHandler Command="Command.LockButton"
                          CommandAction="javascript:ExecuteOrDelayUntilScriptLoaded(LockDocs, 'sp.js');"
                          EnabledScript="javascript:onlyOne();"/>
        <CommandUIHandler Command="Command.UnlockButton"
                          CommandAction="javascript:ExecuteOrDelayUntilScriptLoaded(UnLockDocs, 'sp.js');"
                          EnabledScript="javascript:onlyOne();"/>
      </CommandUIHandlers>
    </CommandUIExtension>
  </CustomAction>
</Elements>
Tutto il codice Javascript lo messo in un file js esterno salvato nella cartella layouts.

var context;
function LockDocs() {
    // First get the context and web
    context = SP.ClientContext.get_current();
    
    this.web = context.get_web(); 
    // Get the current selected list, then load the list using the getById method of Web (SPWeb)
    var listId = SP.ListOperation.Selection.getSelectedList();
    var sdlist = this.web.get_lists().getById(listId);
    this.wfa = sdlist.get_workflowAssociations().getByName('Change permissions');
    context.load(this.wfa);
    context.executeQueryAsync(Function.createDelegate(this, this.OnSuccess), Function.createDelegate(this, this.OnFail));
}

function UnLockDocs() {
    // First get the context and web
    context = SP.ClientContext.get_current();

    this.web = context.get_web();
    // Get the current selected list, then load the list using the getById method of Web (SPWeb)
    var listId = SP.ListOperation.Selection.getSelectedList();
    var sdlist = this.web.get_lists().getById(listId);
    this.wfa = sdlist.get_workflowAssociations().getByName('Re-active Permissions');
    context.load(this.wfa);
    context.executeQueryAsync(Function.createDelegate(this, this.OnSuccess), Function.createDelegate(this, this.OnFail));
}

function OnSuccess(sender, args) {
    var listId = SP.ListOperation.Selection.getSelectedList();
    // Get the currently selected item of the list. This will return a dicustonary with an id field
    var items = SP.ListOperation.Selection.getSelectedItems(context);
    var mijnid = items[0];
    var itemId = mijnid.id;
    var _url = '/_layouts/wfstart.aspx?List=' + listId + '&ID=' + itemId + '&TemplateID=' + this.wfa.get_id() + '&AssociationName=Change permissions';
    alert(_url);
    document.location = _url;
}

function OnFail(sender, args) {
    alert('Fail');
    //alert('Fail to get list list. Error :'+ args.get_message());
}

function moreThanOneEnabled() {
    var items = SP.ListOperation.Selection.getSelectedItems();
    var ci = CountDictionary(items);
    return (ci > 0);
}

function onlyOne() {
    var items = SP.ListOperation.Selection.getSelectedItems();
    var ci = CountDictionary(items);
    return (ci == 1);
}

Come potete vedere recupero il TemplateID dal workflow associato, nel metodo di feedback OnSuccess.

Sorpresa
La sorpresa consiste nel fatto che in SharePoint 2010 esiste una nuova funzionalità (una feature da attivare a livello di site collection), chiamata "In place Record", che fa proprio quello che serviva al mio cliente e in modo più elegante. Quindi tutta la fatica fatta è servita solo a titolo di studio :-)