SPServices Stories #6 – Custom Quizzing System

Introduction

Many times, SPServices Stories play out on the field of the Codeplex Discussions. As someone posts questions about how to build some of the components of their solution, the overall plan comes into focus.

Over the last week or so, I’ve been helping Oli Howson (@Mr_Howson) in the discussions here and here. As the bits and pieces came to together for me, I thought that Oli’s work in progress would make a great SPServices Story, especially since he took the trouble to write up what he was trying to accomplish in the discussion thread.

Oli’s project is in process, and it’s certainly possible that he will make changes from here. However, I thought it would be useful to post things “as-is” so that everyone could see how he’s going about it. If he makes any significant changes, we’ll try to post them back as well.

If you have any comments for Oli about how you might do things differently, I’m sure he’d be interested. I know I would be.

Oli’s entire bespoke page is shown below, so you get to see the markup, the script, everything.

Custom Quizzing System

I am a teacher – running the ICT and Computer Science department of a South London Academy – we teach both disciplines to 11-18 year olds. For key-stage 3 (Y7, 8 and 9) we have for the last few years set homework on our VLE (Virtual Learning Environment) which had a built-in testing system. Due to that being about the only part of the VLE that wasn’t naff, it was recently retired and a new SharePoint system brought in. It’s got a few bespoke bits, and I am not an admin. Now I’m faced with the dilemma: I have 555-ish students needing their homework setting every week, I deliver all my learning resources via the SharePoint system, but there is no built-in quizzing system. Yes – there are surveys, but they don’t mark and have their own foibles. So I built this JavaScript-based quizzing system.

Methodology

The teacher in charge of setting homework that week creates a multiple-choice quiz on a stand-alone piece of client software I wrote in Delphi. This then creates an array string which it pastes into the quiz template (with a relevant name) and copies the file to the relevant document store in the SharePoint server. The teacher then just creates a link from the homework page to the relevant quiz, and when the kids hit the quiz the results go into the relevant list (created with the same name as the quiz). The difficult bit was making sure that the list was created the first time the quiz is run. The idea is the teacher hits the quiz once the link is up to make sure it has worked. When they submit, it creates the list, adds the columns, and updates the view. The second time it tests if the list exists (it does now!) and just inserts their score, which it also shows to the kids and then closes the window.

Well, I think I’m there! I’m going to get this beta tested by a group of Year 9 students tomorrow, but I’ll put the code below for reference to anyone that might find it interesting. I’m sure I’ve made loads of faux-pas as I’ve written a grand total of about three things in JavaScript, and have very limited knowledge of SharePoint.

Wheeee :)

[important]Code update with Oli’s changes in version 1.1.0 on 2013-02-14[/important]

<!doctype html>

<html lang="en">
<head>
 <meta charset="utf-8" />
 <title>jQuery UI Tabs - Default functionality</title>
 <link rel="stylesheet" href="/mertonshared/computerstudies/homework/Documents/includes/jquery-ui.css" />
 <script language="javascript" src="/mertonshared/computerstudies/homework/Documents/includes/jquery-1.8.1.js"></script>
 <script language="javascript" src="/mertonshared/computerstudies/homework/Documents/includes/jquery-ui.js"></script>
 <script language="javascript" src="/mertonshared/computerstudies/homework/Documents/includes/jquery.SPServices-0.7.2.js" type="text/javascript"></script>
 <script>
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 // Quizzer v1.0.2                                                                                //
 // Change Log:                                                                                   //
 //  - 1.1.0 Added check whether user has completed before - one try only. Reordered some actions //
 //          Added permissions setting when list is being created. Changed submit button value.   //
 //  - 1.0.2 Removed some pointless alerts for ordinary users, reenabled windows.close(), added   //
 //           some useful comments.                                                               //
 //  - 1.0.1 Added view change to show all fields                                                 //
 // Currently available:                                                                          //
 //  - Multiple choice                                                                            //
 //  - Unlimited questions                                                                        //
 //  - Four options per question                                                                  //
 // Future Plans                                                                                  //
 //  - Varying number of options per question                                                     //
 //  - Missing word completion                                                                    //
 //  - Include YouTube clip                                                                       //
 ///////////////////////////////////////////////////////////////////////////////////////////////////

 var startseconds;
 var filename;
 var score;
 var thisUserName;
 var previouslytried;

 function checkiflistexists() {
    //alert('checking existance of list: '+filename);
    $().SPServices ({
        operation: "GetList",
        listName:  filename,
        completefunc: function (xData, Status) {
                //tp1 = xData.responseText;
                //alert(tp1);
                //alert(Status);
                if (Status == 'success') {
                    //alert ('Exists - dont create - just insert data');
                    insertdata();
                }
                else {
                    alert('Dont exist');
                    $().SPServices({
                        operation: "AddList",
                        listName: filename,
                        description: "List created for quiz: "+filename,
                        templateID: 100,
                        completefunc: function (xData, Status) {
                            alert('Trying to create list');
                            alert(Status);
                            alert('Now add fields');
                            var nfields = "<Fields><Method ID='1'><Field Type='Text' DisplayName='Score' ResultType='Text'></Field></Method><Method ID='2'><Field Type='Text' DisplayName='TimeTaken' ResultsType='Text'></Field></Method><Method ID='3'><Field Type='Text' DisplayName='CompletedOn' ResultsType='Text'></Field></Method></Fields>";
                            $().SPServices({
                                operation: 'UpdateList',
                                listName: filename,
                                newFields: nfields,
                                completefunc: function(xData, Status) {
                                    tp1 = xData.responseText;
                                    tp4 = tp1.indexOf("errorstring");
                                    if (tp4 < 0) {
                                        alert("Fields created! - Update View");
                                        var viewname = "";
                                        $().SPServices({
                                            operation: "GetViewCollection",
                                            async: false,
                                            listName: filename,
                                            completefunc: function (xData, Status) {
                                                alert('Complete Func - GewViewCollection');
                                                $(xData.responseXML).find("[nodeName='View']").each(function() {
                                                    var viewdisplayname = $(this).attr("DisplayName");
                                                    if (viewdisplayname=="AllItems") {
                                                        viewname = $(this).attr("Name");
                                                        return false;
                                                    }
                                                });
                                            }
                                        });
                                        alert('Ok - done GetViewCollection - now update the view');
                                        var viewfields = "<ViewFields><FieldRef Name=\"Title\" /><FieldRef Name=\"Score\" /><FieldRef Name=\"TimeTaken\" /><FieldRef Name=\"CompletedOn\" /></ViewFields>";
                                        $().SPServices({
                                            operation: 'UpdateView',
                                            async: false,
                                            listName: filename,
                                            viewName: viewname,
                                            viewFields: viewfields,
                                            completefunc: function(xData, Status) {
                                                alert('Trying to update view');
                                                alert(Status);
                                                alert('Updated view - now update permissions');
                                                //insertdata();
                                                $().SPServices({
                                                    operation: 'AddPermission',
                                                    objectType: 'List',
                                                    objectName: filename,
                                                    permissionIdentifier: "HARRISNET\\ham-grp-students",
                                                    permissionType: 'user',
                                                    permissionMask: 1011028719,
                                                    completefunc: function(xData, Status) {
                                                        alert('Trying to update permissions');
                                                        alert(Status);
                                                        alert(xData.responseXML.xml);
                                                        alert('Done... hopefully! - better insert the data!');
                                                        insertdata();
                                                    }
                                                });
                                            }
                                        });
                                    }
                                    else {
                                    // Error creating fields!
                                    alert("Error creating fields!");
                                    }
                                }
                            });
                        }
                    });
                }
            }
        });
 }

 function insertdata() {
    var endseconds = new Date().getTime() / 1000;
    endseconds = endseconds - startseconds;
    var d = new Date();
    var dd = d.toDateString();
    var dt = d.toTimeString();
    $().SPServices({
        operation: "UpdateListItems",
        async: false,
        batchCmd: "New",
        listName: filename,
        valuepairs: [["Title", thisUserName],["Score",score],["TimeTaken",Math.round(endseconds).toString()+" seconds"],["CompletedOn",dd+" "+dt]],
        completefunc: function (xData, Status) {
            //alert('Trying to add data');
            if (Status == 'success') {
                //inserted();
            }
            else {
                alert(Status+' : There was a problem inserting your score into the database. Please notify Mr Howson!');
                //inserted();
            }
        }
    });
    alert('You achieved a score of '+score);
    window.close();
 }

 function checkanswers() {
    var form = document.getElementById('answers');
    score = 0;
    for (var i = 0; i < form.elements.length; i++ ) {
        if (form.elements[i].type == 'radio') {
            if (form.elements[i].checked == true) {
                if (questions[form.elements[i].name.substring(9)-1][questions[form.elements[i].name.substring(9)-1].length-1] == form.elements[i].value)
                {
                 score++;
                }
            }
        }
    }
  $(document).ready(function() {
    checkiflistexists();
  });
 }

 function initialise() {
    var rowcount = 0;
    startseconds = new Date().getTime() / 1000;
    var url = window.location.pathname;
    thisUserName = $().SPServices.SPGetCurrentUser({
        fieldName: "Title",
        debug: false
    });
    filename = url.substring(url.lastIndexOf('/')+1);
    filename = filename.substring(0,filename.lastIndexOf('.'));
    //alert(filename);
    //alert('Getting items');
  $().SPServices({
    operation: "GetListItems",
    async: false,
    listName: filename,
    CAMLViewFields: "<ViewFields><FieldRef Name='Title' /><FieldRef Name='Score' /></ViewFields>",
    CAMLQuery: "<Query><Where><Eq><FieldRef Name='Title' /><Value Type='Text'>"+thisUserName+"</Value></Eq></Where></Query>",
    CAMLRowLimit: 1,
    completefunc: function (xData, Status) {
      $(xData.responseXML).SPFilterNode("z:row").each(function() {
        //var liHtml = "<li>" + $(this).attr("ows_Title") + "</li>";
        score = $(this).attr("ows_Score");
        //$("#tasksUL").append(liHtml);
        rowcount++;
      });
    }
  });
    //alert(rowcount);
    //alert('done');
  if (rowcount > 0) {
   previouslytried = true;
   document.getElementById('tabs').style.visibility = 'hidden';
   alert('Sorry - you have already tried this quiz - one try only!\nLast time you got a '+score);
   window.close();
  }
  else {
   previouslytried = false;
  }
 }

 function inserted() {
    //window.close();
 }

 $(function() {
  $( "#tabs" ).tabs();
 });

 var questions = new Array;
  //The section below should be uncommented when not testing - this will be replaced by the client
  // side application with the questions array.
  [INSERTQUESTIONS]

 //The following questions can be uncommented for testing purposes
 //questions[0] = ['q1','a','b','c',1];
 //questions[1] = ['q2','d','e','f',2];
 //questions[2] = ['q3','g','h','i',3];
 </script>
</head>

<body onload="initialise()">
 <div id="tabs">
  <ul>
   <script language="JavaScript">
    for (var i = 0; i< questions.length; i++)
    {
     document.write('<li><a href="#tabs-'+(i+1)+'">Question '+(i+1)+'</a></li>');
    }
    document.write('<li><a href="#tabs-'+(i+1)+'">Summary</a></li>');
   </script>
  </ul>
   <form name="answers" id="answers">
   <script language="JavaScript">
    for (var i = 0; i < questions.length; i++)
    {
     document.write('<div id="tabs-'+(i+1)+'">');
     document.write(' <p>'+questions[i][0]+'</p>');
     for (var j = 1; j < questions[i].length-1; j++)
     {
      document.write(' <p><input type="radio" name="question-'+(i+1)+'" value="'+j+'">'+questions[i][j]+'<br></p>');
     }
     document.write('</div>');
    }
    document.write('<div id="tabs-'+(i+1)+'">');
    document.write(' <p><input type="submit" onclick="checkanswers(); return false;" value="Submit Homework"></p>');
    document.write('</div>');
  </script>
  </form>
 </div>
</body>
</html>

SPServices Stories #5 – Gritter and Sharepoint: Integrate Nifty Notifications in Your Intranet!

Introduction

This SPServices Story comes to use from Rick El-Darwish (@RtfulDodg3r) in Geneva, Switzerland. For those of you who aren’t familiar with it, Gritter is a Growl-like notification jQuery plugin. That’s a mouthful, but try the links and you’ll quickly get the picture. If you’re an OS X user, then you’ll recognize the capability right away.

Proof of concept Gritter SharepointProof of concept Gritter Sharepoint hover

Sure, SharePoint has a notification scheme built into it – Showing transient messages using the SharePoint 2010 Notification Area – SharePoint 2010 UI tip #2 (thanks, as always, @waldekm) – and there is at least one Codeplex project – SPNotifications – that provides similar functionality. However, the built-in notifications are limited in functionality and the SPNotifications requires server-side code. The script-based approach has a much smaller footprint and the content can easily be maintained by an end user. (Getting developers out of the mix with content ought to always be the goal.) Also, by using open source jQuery-based alternatives, you always have the option to expand the functionality to meet your own needs.

By using SPServices and Gritter together, Rick was able to create very user-friendly notifications on SharePoint pages which were driven by list-based content. Rick starts out with a simple example and shows you how to set up the list-driven approach. With a little extrapolation, I’m sure you will be able to see quite a few additional uses for this approach in your own environments.

You can see Rick’s original post on his blog, Forensic Aspirations, Inferred Logic: Rick’s tale of FAIL.

Gritter and Sharepoint: Integrate Nifty Notifications in Your Intranet!

I’m working with a client on building up a project management tool in SharePoint; one thing that’s really piqued their interest is the concept of getting short flashes of information when their staff logs into the landing page. I think it’s a lovely idea, and I’m intend on giving them this functionality – but how, you might ask? There doesn’t seem to be any SharePoint feature for doing that!

We tend to forget that SharePoint is, above all, a web-based solution. This means that, with a little ingenuity (and sometimes a lot of sweat and blood), you can integrate some of the latest, coolest web features into your existing SharePoint. Fortunately, notifications are not too complicated. In this short article, we’re going to walk through creating very cool notifications using Gritter, a jQuery-based “plugin”, with Sharepoint.

Step One: Create a Sandbox

This may be as simple as creating a new page in your Site Pages repository. I seriously recommend implementing a proof-of-concept rather than work on your production page… If you’re not familiar with these libraries, the last place you want to test things out is on your production work, as easy as these implementations may seem.

Apart from your page, your sandbox will need a few extra files. These you can either place in the Site Assets repository of your PoC portal, or in the Site Assets repo of your root. The latter has the benefit of being accessible to your entire audience (or at least I assume so, it will depend on your permissions). The files that you need are the latest minified version of jQuery, the latest version of Gritter, and the latest version of SPServices (double-check these pages for compatibility issues, of course – if Gritter or SPServices tell you that they won’t work with certain versions of jQuery, don’t use those versions…)

When downloading Gritter, you’ll notice that it is a zip file that has several folders and files. I recommend that you keep those in one single place in your Site Assets. I find it’s easiest to use SharePoint Designer to do that.

Step Two: Add Your jQuery References

Now that you have a sandbox, you can start working with it. In case you’re wondering, this section is assuming that you’re working with SharePoint Designer (SPD) to do your work.

Open your sandbox page in SPD, editing it in Advanced Mode. Locate the PlaceHolderMain placeholder and add the references to your script files:

<!– Inserted by Rick – references to jQuery scripts –>
<!– These are the references to jQuery and SPServices. –>
<script type="text/javascript" src="../SiteAssets/jquery-1.8.3.min.js"></script>
<script type="text/javascript" src="../SiteAssets/jquery.SPServices-0.7.2.min.js"></script>
<!– These are the references to Gritter: –>
<link rel="stylesheet" type="text/css" href="../SiteAssets/gritter/css/jquery.gritter.css" />
<script type="text/javascript" src="../SiteAssets/gritter/js/jquery.gritter.min.js"></script>
<!– End Rick insertions. –>

You can test that the libraries loaded correctly by firing up Chrome, navigating to your PoC page, and opening your Console (F12). In the Console, type the following:

$
$(document).SPServices
$.gritter

If any of these return ‘undefined’, review your references, make sure the files are uploaded in the correct location.

Step Three: Setting up your notifications!

OK, now we know that all the necessary libraries are loaded. Time to develop notifications. Always develop incrementally, testing your code one chunk at a time. To that effect, here’s code that you should insert after the above script blocks:

<!– Here’s a test notification –>
<script type="text/javascript">
$(document).ready( function() {
  $.gritter.add({
    title: "This is a test notification",
    text: "This will fade after some time"
  });
});
</script>

If that works, you know that Gritter is functional for static content! Now it’s time to pull the real notifications from a list — this is where SPServices comes in. Before we proceed, we need something to pull information from: create a custom list with a single title for your PoC, “Latest Activities” for instance. Then, you will call the GetListItems function using SPServices.

The following code replaces your test notification:

<!– Code for notifications –>
<script type="text/javascript">

//This function throws up the notification:
function notify(curTitle, curContent) {
  $.gritter.add({
    title: curTitle,
    text: curContent
  });
}

//This retrieves the latest item of your Latest Activities.
function getLastActivity() {
  $(document).SPServices({
    operation: "GetListItems",
    async: false,
    listName: "Latest Activities",
    CAMLRowLimit: 1,
    CAMLQuery: "<Query><OrderBy><FieldRef Name=’Created’ Ascending=’False’ /></OrderBy></Query>",
    CAMLViewFields: "<ViewFields><FieldRef Name=’Title’ /></ViewFields>",
    completefunc: function(xData, Status) {
      $(xData.responseXML).SPFilterNode("z:row").each(function() {
        notify(‘For your information…’, $(this).attr("ows_Title"));
      });
    }
  });
}

//On document load, throw up the notification:
$(document).ready( function() {
  getLastActivity();
});
</script>

Et voilà — Gritter and SharePoint notifications in a nutshell! Your page will load and, once loaded, will call the getLastActivity function. getLastActivity pulls the latest item from the Latest Activities list (we use the CAMLQuery parameter to order by create date, and the CAMLRowLimit parameter to only return one parameter), and use a callback function to call the notify() function. The notify function is what is responsible for rendering the Gritter notification.

Happy notifying!

Rick.

SPServices Stories #4 – Using SPServices to Process Files from a Non-Microsoft Source

Introduction

SPServices Stories #4 comes to us from Michael Broschat. Michael claims he’s just a “regular old contractor, nothing special”. He lives in the DC metro area (Northern Virginia, specifically), although he’s originally a West Coast boy.

Using SPServices to Process Files from a Non-Microsoft Source

Our system involves email messages sent to a SharePoint library by a non-Microsoft forms application. Although this particular application normally saves its results as XML files, it cannot _send_ those results anywhere (say, the SharePoint library). It can send the results as delimited strings, however, without field names.

I developed a scheme whereby I provide the field structure in an array (arFields) then match it with the field values derived from the delimited-string email. I then write same to a list.

So, three steps in a four-step process have email looking something like this:

smithxx@us.here*BP*11/01/2012*11/19/2012*18*1 - Sent to Supervisor

The final step contains much more information, for a total of 25 fields. The endpoint list includes all 25 fields, of course. Typically, a user entry will comprise four entries but accommodation must also be made for changes made while the four-step process operates. Once a step 4 has been received, any further attempts to augment this process for that user are ignored.

This JavaScript code (with SPServices, jQuery) runs via an enabling button on the list display page. But even before the button appears, the first routine has run. This routine looks in the library for any entries that have not yet been exported. If any, the button is enabled and its label includes the number of records to read and create in an associated list.

$(document).ready(function() {
  jQuery.support.cors = true;
  $().SPServices({
    operation:  "GetListItems",
    listName: "{D3795486-9926-424E-8F14-59BE5DB65BA8}",	//dev  GFSSEForm
    CAMLViewFields: "<ViewFields><FieldRef Name='LinkFilename'/><FieldRef Name='Exported' /></ViewFields>",
    completefunc: function (xData, Status) {
      $(xData.responseXML).SPFilterNode('z:row').each(function() {
        var thisFSObjType = parseInt($(this).attr("ows_FSObjType").split(";#")[1]);
        if(thisFSObjType === 0) {
          if(parseInt($(this).attr("ows_Exported"))===0) iRecordsToExport++;
        }  //if this is a file name
      });  //each()
      if(iRecordsToExport > 0) {
        $("#btnInitiate").html('There are ' + iRecordsToExport + ' records to export...');
        $("#btnInitiate").prop('disabled', false);
      } else {
        $("#btnInitiate").html('No records to export...'); //evidently, once we're off IE7
        $("#btnInitiate").prop('disabled', true);
      }
    }  //completefunc
  });  //SPServices
});  //document.ready

When the Records to Export button is launched, more or less the same code runs again, but this time I want to see only those email entries that have not already been dealt with (ie, Exported = false). This is done via CAML. I collect the library IDs of all items I’m writing in this batch (by the way, there is an interesting distinction between dealing with a batch and dealing with the set of records already written to the list), along with a couple other values, and create an array of IDs. The rest of the processing in this application is from that array.

return {
  //the only public method; handler for the web page button (enabled when at least one library item has not been exported)
  processRecord: function () {
    //this filter restricts the selection just to items that haven't been exported;
    var myQueryOptions = "<QueryOptions />";
    var myQuery = "<Query><OrderBy><FieldRef Name='Created_x0020_Date' /></OrderBy><Where><Eq><FieldRef Name='Exported' /><Value Type='Integer'>0</Value></Eq></Where></Query>";
    // now sorts by Created, ascending.
    $().SPServices({
      operation:  "GetListItems",
      async: false,
      listName: "{D3795486-9926-424E-8F14-59BE5DB65BA8}",	//dev  GFSSEForm
      CAMLViewFields: "<ViewFields><FieldRef Name='LinkFilename'/><FieldRef Name='Exported' /></ViewFields>",
      CAMLQuery: myQuery,
      CAMLQueryOptions: myQueryOptions,
      completefunc: function (xData, Status) {
        $(xData.responseXML).SPFilterNode('z:row').each(function() {
          //pick up some metadata from the SharePoint library
          //gather ows_ID='14', ows_FSObjType='14;#0', ows_Created_x0020_Date='14;#2012-11-20 06:55:13'
          var thisFSObjType = parseInt($(this).attr("ows_FSObjType").split(";#")[1]);
          idCurrent = $(this).attr("ows_ID");	//available globally
          dateCurrentCreated = $(this).attr("ows_Created_x0020_Date").split(";#")[1];	//available globally
          var formFilename = $(this).attr("ows_LinkFilename");
          if(thisFSObjType == 0) {
            arIDs.push([idCurrent, dateCurrentCreated, formFilename]);
          }  //if this is a file name
        });  //each(); arIDs is an array built from looking at all non-Exported items in the library
        //actually, you need to know the contents before you can decide whether there are any duplicate entries.
        //	ordering by Created, we're processing all entries in order of their entry into the library
      }  //completefunc
    });  //SPServices
    //here, do the Ajax call against the array of arrays; async:false is no longer used with $.ajax, deferred
    //	taking over that function; 2 Jan took the following routine out from completefunc; seems to save a stack level
    $.each(arIDs, function(index, value) {
      var promise = $.ajax({
        type:"GET",
        url:"GFSSEForm/" + arIDs[index][2],		//for SP2010 and above, full path is needed for the library name
        dataType:"text"
      });
      //the promise code executes when the file has been opened by AJAX
      promise.done(doStuff);	//magically passes the data along, too
      promise.fail(function() {alert("failed to open this eform: " + arIDs[index][2]);});
    });  //each
  }
};	// public method processRecord() [return]

By this point, the email file in the SharePoint library has been opened and its contents are ready to process.

There are two ways for email to get into the library: sent by the non-Microsoft forms application and sent directly via a client’s Outlook. The former way gets encoded (binary64), whereas the latter way does not. Both must be parsed for usable content but the encoded email must first be decoded. I use the following routine to handle both:

//	Sets the global variable: arValues; deals with both base64-encoded email and also non-encoded email
//	CRLF = \r\n
function decodeEmail(block) {
  var iBase64 = block.indexOf("base64");
  if(iBase64 > -1) {
    var startBlock = iBase64 + 6;
    var endBlock = block.indexOf("\n--", startBlock);
    var emailBlock = $.trim(block.substring(startBlock, endBlock));
    var strEmail = emailBlock.replace(new RegExp("\\n", "g"), "");
    var strDecoded = Base64.decode(strEmail);
    strDecoded = stripHTML(strDecoded);
    var iLong = strDecoded.indexOf("\r\n\r\n");	//intended for non-SMTP messages
    if(iLong > -1) {
      //take up to first \r\n
      strDecoded = strDecoded.substring(0,iLong+2);
    }
    arValues = strDecoded.split("*");
  } else {
    //	charset="us-ascii"; charset=utf-8; charset="gb2312"
    //here if there was no "base64" in the message; perhaps you should look for [charset="]us-ascii["]
    // this routine greatly strengthened 24 Jan 2012
    var iTrueStart = block.indexOf("quoted-printable");	//24 Jan fine; whole routine looks good
    var iTrueStart2 = iTrueStart + 16;
    var endBlock = block.indexOf("\n--", iTrueStart);
    var strBlock2 = $.trim(block.substring(iTrueStart2, endBlock));
    var newBlock = strBlock2.replace("=\r\n", "");		//kill all CRLFs
    var newBlock2 = newBlock.replace(/\<.*\>/g, "");  //a weird <mailto...> string in one message
    //you could have just called your own stripHTML()!
    var newBlock3 = newBlock2.replace(/=\r\n/g, "");	//one last holdout: =CRLF
    arValues = newBlock3.split("*");

  }
}

In my experience, getting values from functions does not always work within this environment (JavaScript within SharePoint). I have had to rely upon global variables (which have created their own problems at times). When that email decoding code runs, it places the parsed values into a global array: arValues, which is then used by the various routines that follow.

doStuff runs when the promise has been fulfilled. In other words, it only runs when data from the email is in hand. It sends the record off to writeNew or, if the number of field values does not match the current field template, stops the record from being processed.

function doStuff(data) {
  decodeEmail(data);	//sets global arValues, regardless of email type
  arFields = [];	//to ensure correct value within the batch loop
  if(arValues.length == 7) arFields = arFields100;
  if(arValues.length == 25) arFields = arFields200;
  if(arFields.length > 0) {
    boolIgnore = false;	//ensures correct starting point for each email
    iGlobal++;
    arValues[2] = dateFormat(arValues[2]);	//watch for changes in this default field order
    arValues[3] = dateFormat(arValues[3]);
    if(arValues.length === arFields.length) {
      var strArguments = arValues.join();
      writeNew(strArguments);	//wait until this routine before handling a dupe;
    } else alert("Number of values differed from number of form fields; not written." + arFields.length + " fields, " + arValues.length + " values (" + arValues + ")");
  } // was arFields set? If not, just return and let it pass
}  // doStuff()

writeNew simply uses SPServices to write the email contents to the list. It does this by preparing an array value to contain the values in the proper manner for UpdateListItems::New. After writing the record, I call setUpdated to modify the library entry, passing the library ID, which setUpdated uses to access the library metadata.

// strArguments is arValues rendered as string
function writeNew(strArguments) {
  $("#divId").ajaxError( function(event, request, settings, exception) {
    $(this).append("Error here: " + settings.url + ", exception: " + exception);
  });
  var iFields = arFields.length;
  var i = 0;
  var strTest = "";
  var strField = "";
  var vpairs = [];
  var strPairs = "";
  var arValues2 = strArguments.split(',');
  for(i=0; i<iFields; i++) {
    strTest = String(arValues2[i]);
    if(strTest.length > 255) strTest = strTest.substring(0,255);
    strField = arFields[i];
    vpairs.push([strField,strTest]);
  }
  //check to see whether this email address is in HoldMe; if so, processing stops, but run setUpdated(arIDs[idIndex][0]) and advance the index
  notInHoldMe(vpairs[0][1]);	//sets global value regarding presence in HoldMe list
  if(!inHoldMe) {
    var jsDate = getJSDate();		//picks up date values from arValues
    vpairs[4][1]=jsDate;
    $().SPServices({
      operation: "UpdateListItems",
      batchCmd: "New",
      async: false,
      listName: "{2D9F4CDB-A5F0-4EED-8996-C26FB2D08294}",  //development list GFSSVerified
      valuepairs: vpairs,
      completefunc: function(xData, Status) {
        if(Status == 'success') {
          //'success' is a relative term; you must also examine any error text, to see whether an error occurred
          var strError = $(xData.responseXML).SPFilterNode('ErrorText').text();
          if(strError != "") {
            $("#showErrors").append("<p>Error adding: " + $(xData.responseXML).SPFilterNode('z:row').attr("ows_Title") + " " + strError + "</p>");
          } else setUpdated(arIDs[idIndex][0]);  //possibly delete the row at this point
          idIndex++;
          if(vpairs[6][1].substring(0,1) == "4") setLocked(vpairs[0][1]);	// ie, after writing Step 4
          //record has been written; now find out whether it was a duplicate
          else findExisting(vpairs[0][1], vpairs[6][1].substring(0,1));
        } else alert("error: " + xData.responseText);
      }	//completefunc
    }); //SPServices
  }	//if not locked
  else {
    alert("The record for " + vpairs[0][1] + " is locked...");
    setUpdated(arIDs[idIndex][0]);
    idIndex++;
  }
}  // writeNew()

All records are to be written to the list, but it will happen that some records are duplicates (because a later action changes the previous action). In that case, the earlier record needs to be marked as ‘orphan’. The original idea was to simply delete the record but someone wanted to keep it. Therefore, I need to filter orphans from various stages of processing. The routine called findExisting deals with this issue. I use CAML to filter orphans.

function findExisting(user, action) {
  $("#divId").ajaxError( function(event, request, settings, exception) {
    $(this).append("Error in findExisting: " + settings.url + ", exception: " + exception + "<br>");
  });
  var queryOptions = "<QueryOptions />";
  var query = "<Query><Where><And><And><Eq><FieldRef Name='Title' /><Value Type='Text'>" + user + "</Value></Eq><BeginsWith><FieldRef Name='col07x' /><Value Type='Text'>" + action + "</Value></BeginsWith></And><Neq><FieldRef Name='col26x' /><Value Type='Integer'>1</Value></Neq></And></Where></Query>";  //col26x is 'Orphan'
  //CAML looks for existing items having same name and action, ignoring any that have already been marked as orphans
  $().SPServices({
    operation:  "GetListItems",
    async: false,		//required!!!!!
    listName: "{2D9F4CDB-A5F0-4EED-8996-C26FB2D08294}",  //development list GFSSVerified
    CAMLViewFields: "<ViewFields><FieldRef Name='Title'/></ViewFields>",
    CAMLQuery: query,
    CAMLQueryOptions: queryOptions,
    completefunc: function (xData, Status) {
      var iCount = parseInt($(xData.responseXML).SPFilterNode("rs:data").attr("ItemCount"));
      if(iCount > 1) {	//you're here because this value _at least_ was written
        //within this batch, there are multiples of this user/action; pass the multiple IDs
        var arDupIDs = [];
        var iDupID = 0;
        // routine examines each entry in arDupIDs, and replaces any value with lesser; ends up with earliest entry, which is then orphaned
        // limitation here is that it only--practically speaking--handles two instances; three or more would lose all but one
        $(xData.responseXML).SPFilterNode('z:row').each(function() {
          iDupID = parseInt($(this).attr("ows_ID"));
          arDupIDs.push(iDupID);
          if(arDupIDs.length > 1) {
            if(iDupID < arDupIDs[0]) arDupIDs[0] = iDupID;
          }
        });
        orphanGFSSRow(arDupIDs[0]);
      }	// if at least one
    }	//completefunc
  });  //SPServices
}	//findExisting()

The completefunc routine looks only at items that have duplicates. It determines the earliest item, then sends off its ID for marking as orphan.

SPServices is also used to lock an account (by placing the email address in a separate list), by checking for existence of the email address currently being processed in the Locked list. One function sets the lock, while notInHoldMe() queries the lock list.

SusQTech “30 on Thursday” Webinar “Top 10 jQuery Uses in SharePoint 2010″

“30 on Thursdays”

I want to thank the fine folks at SusQTech (@SusQTech) – Steve Witt (@SPLumberjack) and Julia Oates (@juliaoates), in particular – for asking me to present in their “30 on Thursday” webinar series today.

We had a great turn out and it’s a given that I love to talk about SharePoint and jQuery together. It’s like peas and carrots or chocolate and peanut butter.

My slides from the webinar are available on SlideShare. If you missed it, the recording will be available soon on the “30 on Thursday” site. (Every time I type “30 on Thursday” I’m reminded how long ago it was that I was 30.)

Referencing jQuery, jQueryUI, and SPServices from CDNs – Revisited

In my previous post entitled Referencing jQuery, jQueryUI, and SPServices from CDNs, I provided the references to quickly add jQuery, jQueryUI, and SPServices from the CDNs I typically use.

However, I made a bit of a faux pas in what I provided. It’s better to omit the protocol in the references. Browsers will simply use the current protocol, whether it be http: or https:, as needed. This means that you won’t have to worry about any issues down the road should you decide to implement SSL. It also means that the location your user happens to use to access the site doesn’t matter.

Here’s my updated set of references with the protocols omitted and updated to the versions I’m currently using:

<!-- Reference the jQueryUI theme's stylesheet on the Google CDN. Here we're using the "Start" theme -->
<link  type="text/css" rel="stylesheet" href="//ajax.googleapis.com/ajax/libs/jqueryui/1.10.0/themes/start/jquery-ui.css" />
<!-- Reference jQuery on the Google CDN -->
<script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js"></script>
<!-- Reference jQueryUI on the Google CDN -->
<script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jqueryui/1.10.0/jquery-ui.min.js"></script>
<!-- Reference SPServices on cdnjs (Cloudflare) -->
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/jquery.SPServices/0.7.2/jquery.SPServices-0.7.2.min.js"></script>