The article I wrote for DIWUG SharePoint eMagazine 4 now also online here:
One of the new features of SharePoint 2010 is tagging. Users can tag several items within SharePoint, such as pages and list items. The tags a user used can be viewed in a tag cloud on their profile page.
In Central Administration the page ‘Manage Social Tags and Notes’ is available to manage the users’ social terms. Here social terms can be found and eventually deleted. To manage the users’ social terms a user has to have sufficient permissions to Central Administration.
Besides using the ‘Manage Social Tags and Notes’ page, the SocialDataService web service can be used to retrieve information about tagging. This web service exposes a lot of operations to get all kinds of information about social data in SharePoint 2010. To retrieve the list of operations http://Site/_vti_bin/SocialDataService.asmx can be called within a SharePoint site. When a specific operation is selected from the list the expected request xml is displayed and the response xml the web service returns.
The jQuery library SPServices abstracts SharePoint’s Web Services and makes them easier to use. A list of implemented web services within SPServices can be found at CodePlex: http://spservices.codeplex.com
The SPServices library can be used fully client side and is an ideal solution to accomplish the goal of this article: creating a client side dashboard of some social analytics within SharePoint 2010. Marc D. Anderson has developed the SPServices library and is very open to community requests for enhancements as well as to help with any bugs, questions, or issues. You can contact him through the Discussions on the CodePlex site at any time.
The social dashboard functionality
Imagine an intranet portal at a certain company. The intranet portal is setup with SharePoint 2010 and the board wants to promote the social aspects of SharePoint, especially tagging, to the employees of the company.
At every company a lot of things are said between employees during a visit to the coffee machine or water cooler. To help discover how employees really interact and how the organization functions as a whole social interaction within SharePoint is really helpful. The board wants to give the employees a voice and thereby uncover what they find most important.
Besides this, the use of tagging will expose hidden knowledge in the organization. When an employee decides to move on to another company SharePoint keeps this information from being lost.
The SharePoint search will also benefit by the use of tagging. Often tagged content will have a positive effect on the contents search result ranking.
Unfortunately, adaptation of this new feature is currently poor in the organization and the use of tagging has to be stimulated in a way. Since people always like to be rewarded for their effort, the board decided to give away a bottle of champagne every month to the user who tags the most content.
To give the employees some information on how they’re doing related to their colleagues, some statistics about tagging will be published on a page within SharePoint which is accessible to all employees. To make this page attractive, not only the most active users are displayed, but also the most used tags and the tags the current user used already.
Additional information is available to managers and members of the board to stimulate persons even more.
A tabular overview of the functionality based on the role of the user:
Functionality
|
Employee
|
Manager
|
Board member
|
Tags of current user
|
V
|
V
|
V
|
Most used tags
|
V
|
V
|
V
|
Drill down to url
|
V
|
V
|
V
|
Top active users
|
V
|
V
|
V
|
Drill down to tag
|
X
|
V
|
V
|
Activity of employees in own department
|
X
|
V
|
V
|
Activity of employees in a selectable department
|
X
|
X
|
V
|
All the code displayed in the article is based on jQuery or html. With references to jQuery, the SPServices library and the jQuery template plugin all the code can be pasted in a Content Editor web part on a SharePoint page.
<script src="/jQueryLibrary/jquery-1.4.4.min.js" type="text/javascript"></script> <script src=" /jQueryLibrary/jquery.SPServices-0.5.8.min.js" type="text/javascript"></script> <script src=" /jQueryLibrary/jquery.tmpl.js" type="text/javascript"></script>
Tags of current user
To determine the tags a certain user used, one call to the SocialDataService web service with the help of SPServices will do. SPServices exposes an operation called ‘GetTagsOfUser’ which expects the parameter user account name. In this case the user account name is the account name of the currently logged in user.
The SPServices library exposes a function to get the account name of the current logged in user, ‘SPGetCurrentUser’. The code to get the account name of the current user:
var currentUserAccount = $().SPServices.SPGetCurrentUser({ fieldName: "Name" });
The SPGetCurrentUser function does an AJAX call to grab /_layouts/userdisp.aspx?Force=True and ‘scrapes’ the values from the page based on the internal field name. The ‘Name’ field used in the code is the internal field name of the account name.
Since the fieldname ‘Name’ in the above code is the default option of the SPGetCurrentUser function, the call can be combined with the operation ‘GetTagsOfUser’.
$().SPServices({ operation: "GetTagsOfUser", userAccountName: $().SPServices.SPGetCurrentUser(), completefunc: function (xData, Status) { $(xData.responseXML).find("SocialTagDetail").each(function () { tagName = $("Term>Name", $(this)).text(); tagsofuser.push({ Tag: tagName, Count: 1 }); }); tagsofuser = uniqueTags(tagsofuser); SortByCount(tagsofuser); $("#currentUserTagsText").show(); $("#currentUserTags").html(""); $("#tagsofuserTemplate").tmpl(tagsofuser) .appendTo("#currentUserTags"); } });
The callback function gets the response of the web service. A part of the response is displayed here:
<?xml version="1.0"?> <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <soap:Body> <GetTagsOfUserResponse xmlns="http://microsoft.com/webservices/SharePointPortalServer/SocialDataService"> <GetTagsOfUserResult> <SocialTagDetail> <Url>http://sp2010/Lists/Tasks/AllItems.aspx</Url> <Owner>sp2010\mark</Owner> <LastModifiedTime>2010-12-19T13:28:09.437</LastModifiedTime> <Title>Tasks - All Tasks</Title> <Term> <Id>974a854f-31b4-431f-91cb-a6289f58c978</Id> <Name>I like it</Name> </Term> <IsPrivate>false</IsPrivate> </SocialTagDetail> Rest of the SocialTagDetails are intentionally left out� </GetTagsOfUserResult> </GetTagsOfUserResponse> </soap:Body> </soap:Envelope>
The term name is stored in an array of objects. The Count is set to one, because this tag is default present one time. When the duplicates are removed in the uniqueTags method the Count of the Tag is adjusted if the tag is present more than once. Since this is a helper method it is not showed here. Afterwards the tags are sorted descending by the number stored in the variable Count. A jQuery template, tagsofuserTemplate, is used to show the results on the page. The template is rendered with the data and appended to a div with the id currentUserTags. The template uses the objects in the array:
<div><script id="tagsofuserTemplate" type="text/x-jquery-tmpl"> <div> </div> <div><div style="float:left;width:40%"> <div> </div> <div> ${Tag} <div> </div> <div></div> <div> </div> <div><div style="float:left;width:20%"> <div> </div> <div> ${Count} <div> </div> <div></div><br /> <div> </div> <div></script> <div>
The tags of the current user are displayed as show in the figure below:
Since detailed information of the tags is available at the users’ profile page, a link is displayed to get there.
Most used tags
The web service doesn’t provide an operation to get the most used tags, but only an operation to get all the tags. Therefor all the tags terms have to be retrieved and afterwards counted and sorted to get the most used ones.
$().SPServices({ operation: "GetAllTagTerms", debug: false, completefunc: function (xData, Status) { $(xData.responseXML).find("SocialTermDetail").each(function () { termName = $("Term>Name", $(this)).text(); termGuid = $("Term>Id", $(this)).text(); counter = $("Count", $(this)).text(); terms.push({ Term: termName, Count: counter, TermGuid: termGuid }); }); SortByCount(terms); terms.length = 5; $("#showterms").html(""); $("#termTemplate").tmpl(terms) .appendTo("#showterms"); } });
In the above code the operation ‘GetAllTagTerms’ of the SPServices library is used. This operation gets all the tag terms. The name, id and the counter of each term are retrieved from the returned xml. The tag terms are sorted by the Count variable and the number of items returned is minimized to the top 5 most used tag terms. The result is displayed on the page by a jQuery template:
<div><div style="float:left;width:40%"> <div> </div> <div> <a href="#" id=${TermGuid}> <div> </div> <div> ${Term} <div> </div> <div> </a> <div> </div> <div></div> <div> </div> <div><div style="float:left;width:20%"> <div> </div> <div> ${Count} <div> </div> <div></div> <div>
The most used tags are displayed as show in the figure below:
To drill down on a tag the method GetAllTagUrls with the parameter this.id, in this case the guid of the term selected, is called when a user selects one of the tags.
$().SPServices({ operation: "GetAllTagUrls", termID: termId, debug: true, completefunc: function (xData, Status) { $(xData.responseXML).find("SocialUrlDetail").each(function () { url = $("Url", $(this)).text(); countUrl = $("Count", $(this)).text(); urlsofterm.push({ Url: url, Count: countUrl }); }); SortByCount(urlsofterm); $("#urlsoftermContainer").show(); $("#showurlsofterm").html(""); $("#urlTemplate").tmpl(urlsofterm) .appendTo("#showurlsofterm"); } });
The ‘GetAllTagUrls’ of the SPServices library is called with the termID as parameter. The result is displayed with another jQuery template:
Top active users
The SocialDataService web service doesn’t provide an operation to get the top active users either. To determine the top active users in the site collection first all the available users have to be retrieved. The SPServices library allows a call to the operation ‘GetUserCollectionFromSite’, which the web service Users and Groups exposes.
$().SPServices({ operation: "GetUserCollectionFromSite", completefunc: function (xData, Status) { $(xData.responseXML).find("User").each(function () { userName = $(this).attr("LoginName"); userNames.push({ UserName: userName }); }); GetTopTagsOfUsers(userNames); } });
In the code above the request to the web service is made and the result, the login names of all the users, is stored in an array.
With the array of login names the operation ‘GetTopTagsOfUsers’ is called to count the tags used by each user and format the result in a clear way.
To count the tags used for a single user the SocialDataService web service provides the ‘CountTagsOfUser’ operation. The operation is called with the account name of the user as parameter. The result is formatted in an array of objects to store the account name and the number of tags of the user.
function CountTagsOfUser(useraccountname, f) { var tagCount = ""; var tagsofuser = []; $().SPServices({ operation: "CountTagsOfUser", userAccountName: useraccountname, debug: true, completefunc: function (xData, Status) { $(xData.responseXML).find("CountTagsOfUserResult").each(function () { tagCount = $(this).text(); tagsofuser.push({ Count: tagCount, UserAccountName: useraccountname }); }); SortByCount(tagsofuser); if (typeof f == "function") f(tagsofuser); return tagsofuser; } }); }
A callback function is necessary to use the CountTagsForUser in the function GetTopTagsOfUsers and returns the result array when the counting of the tags of the specified user has finished.
function GetTopTagsOfUsers(usernames) { var result = []; $.each(usernames, function (key, value) { CountTagsOfUser(value["UserName"], function (items) { $.each(items, function (i, n) { result.push({ Count: n["Count"], UserAccountName: n["UserAccountName"] }); }); if (usernames.length == result.length) { SortByCount(result); result.length = 5; $("#showcounttagsofuser").html(""); if (isCEO || isManager) { $("#counttagsofuserTemplate").tmpl(result) .appendTo("#showcounttagsofuser"); } else { $("#counttagsofuserTemplateForUser").tmpl(result) .appendTo("#showcounttagsofuser"); } } }); }); }
The ‘GetTopTagsOfUser’ function retrieves for every user present in the site collection the number of tags and stores the result in an array of objects. Once this function completes, the number of usernames are equal to the tagcount result, the results are sorted descending to get the most active user on top and the number of results is limited to the top five users.
When showing the results on the screen the first difference is made between a ‘regular’ employee and a manager/board member.
For a ‘regular’ employee a different template is used for rendering, because they aren’t allowed to drill down on a user to see which tags these users did use.
The template for a ‘regular’ employee:
<script id="counttagsofuserTemplateForUser" type="text/x-jquery-tmpl"> <div style="float:left;width:40%"> ${UserAccountName} </div> <div style="float:left;width:20%"> ${Count} </div> </script>
The top active users part on the page for a ‘regular’ employee:
And for a manager/board member:
<script id="counttagsofuserTemplate" type="text/x-jquery-tmpl"> <div style="float:left;width:40%"> <a href="#" id=${UserAccountName}> ${UserAccountName} </a> </div> <div style="float:left;width:20%"> ${Count} </div> </script>
The top active users part on the page for a manager/ board member:
The GetTagsOfUser operation used to drill down on a user is the same method used by displaying the tags of the current user mentioned in the beginning of the article.
What’s the difference between a ‘regular’ employee, a manager and a board member?
The differences are stored in the profile properties of the user profile.
The user profiles of all the people present in the company are set up with the Department field filled and/or the Manager field filled. The Department field of the board has to be filled with ‘CEO’ to know this user a not a regular manager, but a member of the board.
The following table lists some examples of user profiles, their properties and their roles:
UserName
|
Department
|
Manager
|
Role
|
Alex
|
ICT
|
Andrew
|
Employee
|
Andrew
|
ICT
|
|
Manager
|
Anita
|
Marketing
|
Mark
|
Employee
|
Chris
|
HR
|
Dave
|
Employee
|
Dave
|
HR
|
|
Manager
|
Jeff
|
Marketing
|
Mark
|
Employee
|
Mark
|
Marketing
|
|
Manager
|
Paul
|
CEO
|
|
Board member
|
To give an example on how to read the table: Jeff is an employee and member of the Marketing department. His manager is Mark. This makes Jeff a ‘regular’ employee.
The role column lists the role of the user based on the previous columns.
The information about the department and manager of a person is stored in their User Profile. The SPServices library supports a lot of operations on the UserProfileService web service. The one needed here is ‘GetUserProfileByName’ to get the properties ‘Department’ and ‘Manager’. This method expects a parameter ‘AccountName’, which is the currently logged in user.
var CEOdepartment = "CEO"; $().SPServices({ operation: "GetUserProfileByName", AccountName: currentUserAccount, completefunc: function (xData, Status) { $(xData.responseXML).find("GetUserProfileByNameResult>PropertyData").each(function () { //check for CEO department if ($("Name", $(this)).text().toUpperCase() == "Department".toUpperCase()) { departmentOfCurrentUser = $("Values>ValueData>Value", $(this)).text(); if (departmentOfCurrentUser.toUpperCase() == CEOdepartment.toUpperCase()) { isCEO = true; GetDepartmentsAndUsers(); } } else if ($("Name", $(this)).text().toUpperCase() == "Manager".toUpperCase()) { managerName = $("Values>ValueData>Value", $(this)).text(); if (managerName == "") { if (!isCEO) { isManager = true; GetDepartmentsAndUsers(); } } } }); } });
The callback function gets the response of the web service. A part of the xml response looks like:
<?xml version="1.0"?> <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <soap:Body> <GetUserProfileByNameResponse xmlns="http://microsoft.com/webservices/SharePointPortalServer/UserProfileService"> <GetUserProfileByNameResult> <PropertyData> <IsPrivacyChanged>false</IsPrivacyChanged> <IsValueChanged>false</IsValueChanged> <Name>AccountName</Name> <Privacy>Public</Privacy> <Values> <ValueData> <Value xsi:type="xsd:string">sp2010\paul</Value> </ValueData> </Values> </PropertyData> <PropertyData> <IsPrivacyChanged>false</IsPrivacyChanged> <IsValueChanged>false</IsValueChanged> <Name>Department</Name> <Privacy>Public</Privacy> <Values> <ValueData> <Value xsi:type="xsd:string">CEO</Value> </ValueData> </Values> </PropertyData> </GetUserProfileByNameResult> </GetUserProfileByNameResponse> </soap:Body> </soap:Envelope>
The current user in the response xml has the account name sp2010\paul and is a member of the CEO department.
The find method of the responseXML gets to the PropertyData in the xml and tries to find the ‘Department’ and ‘Manager’ properties. Once found the value of the property is retrieved and in this case the variable ‘isCEO’ is set to true, because Paul is a member of the CEO department. The department of the current user is stored in the variable departmentOfCurrentUser and the operation ‘GetDepartmentsAndUsers’ is called for later use on other functionality.
The above code is used to determine the role of the current logged in user and the ‘Top active users’ part on the page will display itself differently based on this role.
Worried about security?
All information is gathered by calls to web services. Calls on the web services make everything security trimmed, so the approach is secure.
Activity of employees in own or a selectable department
Managers are allowed to view the activity of all the employees which are a member of their department. The account names of these employees are displayed in a dropdown box on the page.
Besides viewing the activity of a single user, the board wants to view the activity within a whole department. The departments are listed in a dropdown box cascading the users of the selected department.
These two functionalities can be implemented in a single operation. The method ‘GetDepartmentsAndUsers’ is called in the previous code. This operation is responsible to provide the values to set up the cascading functionality between departments and account names of the users. This method is only called when the current logged in user is a manager of a CEO.
To retrieve all the departments and account names the Search web service is called. One call is made to the web service to get the departments and the account names by formatting the query to return these properties in the result. The SQL syntax is used and the scope is set to the People scope.
var queryTextSQL = "<QueryPacket xmlns='urn:Microsoft.Search.Query' Revision='1000'>" queryTextSQL += "<Query>" queryTextSQL += "<Context>" queryTextSQL += "<QueryText language='en-US' type='MSSQLFT'>" if (isCEO) { queryTextSQL += "SELECT Title, Rank, Size, Description, Write, Path, AccountName, Department FROM scope() WHERE ( (\"SCOPE\" = 'People') ) ORDER BY \"Rank\" DESC" } else { queryTextSQL += "SELECT Title, Rank, Size, Description, Write, Path, AccountName, Department FROM scope() WHERE ( (\"SCOPE\" = 'People') ) AND (CONTAINS (Department,'" + departmentOfCurrentUser + "')) ORDER BY \"Rank\" DESC" } queryTextSQL += "</QueryText>" queryTextSQL += "</Context>" queryTextSQL += "</Query>" queryTextSQL += "</QueryPacket>"; $().ready(function () { var resultText = ""; $().SPServices({ operation: "QueryEx", queryXml: queryTextSQL, completefunc: function (xData, Status) { $(xData.responseXML).find("RelevantResults").each(function (i) { accountName = $("ACCOUNTNAME", $(this)).text(); departments[i] = $("DEPARTMENT", $(this)).text(); users.push({ AccountName: accountName, Department: departments[i] }); }); if (isCEO) { $('#departmentContainer').show(); fillDepartmentsDropdown('#departmentSelection', $.unique(departments), "Select department"); } if (isCEO || isManager) { $('#userContainer').show(); fillUsersDropdown('#userSelection', users, "Select user"); } } }); });
Dependent if the user is a manager or board member different queries are fired. When a manager only the users which are member of the department, set earlier in the variable ‘departmentOfCurrentUser’, are retrieved.
When the response is retrieved the departments are stored in an array of strings and the users are stored in an array of objects. The object contains the account name of the user and the department which the user is a member of. The department of the user is needed to accomplish the cascading dropdown between departments and account names. The department dropdown is only filled and showed when the current logged in user is member of the CEO department. If the user is a manager of another department only the users of that department are shown in the users’ dropdown box.
There are two different methods to fill the dropdown boxes for departments and users, because the users are stored in an array of objects, only the account name is used to fill the box:
function fillUsersDropdown(dropdownName, arrayToUse, text) { $(dropdownName).html(""); $.each(arrayToUse, function (key, value) { $(dropdownName).append('<option value="' + value["AccountName"] + '">' + value["AccountName"] + '</option>'); }); $(dropdownName).prepend("<option value='0' selected='true'>" + text + "</option>"); $(dropdownName).find("option:first")[0].selected = true; }
The id’s departmentContainer and userContainer used in the code contain the markup:
<div style="float: left; width: 100%; display: none;" id="departmentContainer"> <div style="float: left; width: 15%"> Select department</div> <select id="departmentSelection" style="float: left; width: 15%"> </select> </div> <br /> <div id="userContainer" style="float: left; width: 100%; display: none;"> <div style="float: left; width: 15%"> Select user</div> <select id="userSelection" style="float: left; width: 15%"> </select> <div style="clear: both; float: left; width: 25%"> <a href="#" onclick="GetTagsOfSelection(userSelection.value, departmentSelection.value);return false"> Show tags of selection</a></div> <h4 style="clear: both; float: left; display: none;" id="showtagsofuserText"> Tags of selection:</h4> <div id="showtagsofuser" style="clear: both;"> </div> </div>
The page now looks like this when the current logged in user is a board member:
And a manager:
The cascading functionality is now easy:
$('select').change(function (e) { if (this.id == "departmentSelection") { $('#userSelection option').each(function (i, option) { $(option).remove(); }); j = 0; childArray.length = 0; if (this[this.selectedIndex].value != 0) { departmentSelected = this[this.selectedIndex].text; //filter the users array on the selected department filteredUsers = $.grep(users, function (filter) { return filter["Department"] == departmentSelected; }); fillUsersDropdown('#userSelection', filteredUsers, "Select user"); } else { fillUsersDropdown('#userSelection', users, "Select user"); } } });
The code first empties the items in the users’ dropdown box and fills it again based on the selected department.
The picture below shows the cascading functionality between departments and users:
After making a selection of a department and/or a user ‘Show tags of selection’ can be selected. Selecting this fires the operation ‘GetTagsOfSelection()’ with the values of the selected department and/or user as parameters.
function GetTagsOfSelection(userSelection, departmentSelection) { var result = []; if (userSelection != 0) { GetTagsOfUser(userSelection, true, true); } else if (departmentSelection != 0) { $.each(filteredUsers, function (key, value) { GetTagsOfUser(value["AccountName"], false, true, false, function (items) { $.each(items, function (i, n) { result.push({ Tag: n["Tag"], Count: 1 }); }); result = uniqueTags(result); SortByCount(result); $("#showtagsofuserText").show(); $("#showtagsofuser").html(""); $("#tagsofuserTemplate").tmpl(result) .appendTo("#showtagsofuser"); //} }); }); } }
When a single user is selected the operation GetTagsOfUser() is used again. When a single department is selected the tags of all the users’ member of the selected department are retrieved, counted and sorted descending. The jQuery template is rendered with the results.
The page now looks like this when the current logged in user is a board member:
The result
Grabbing all the functionality together the social dashboards look like:
For an employee:
For a manager:
For a board member:
This month the board member, Paul, wins the bottle of champagne, because he tagged the most content. Since this isn’t the wanted outcome, employees have to win the bottle of course; Paul throws in two bottles of champagne for the winner next month.
This company just started to use the social aspects of SharePoint. The number of tags used isn’t very high yet. The search results ranking within SharePoint will not benefit from this tagging; neither the hidden knowledge in the organization is stored in SharePoint yet. When the stimulation of the use of tagging continues, employees will embed the use of tagging in their daily work and it will not take long to see the positive effects of it.
Summary
The SocialDataService web service is a good starting point on retrieving and displaying social information like tagging on a page. To use the service fully client side the library SPServices is an excellent solution.
To display information as shown in this article the SocialDataService web service does not provide all the necessary information and other web services have to be used. Other web services used here are the Users and Groups, the UserProfile and the Search web services. With the use of all these services and additional jQuery the goal of creating a client side dashboard of some social analytics is accomplished.
Performance wise the solutions are not always the most optimal. For example when the most active users are gathered. First it grabs all the users from the site collection, counts the tags for all of these users and then it is limited to show the top 5 of highest tag counts. It’s not the web service not being efficient; it’s the lack of the availability of a function to get the highest tags counts without the need to make the count on a specific user.
Hi Anita,
Do you have all the code for the entire dashboard available for download so that I can more easily adapt it to my research.
Thanks,
Detlef
Hi Detlef,
Sorry, the code is not available for download.
Regards, Anita
Netjes!! Kijk wel uit voor Caml injections (firebug helpt) om security issues te voorkomen.