Showing posts with label Web Services. Show all posts
Showing posts with label Web Services. Show all posts

February 11, 2010

Visualize your SharePoint Calendar with a Dynamic Timeline

[Update Feb 2010: see new blog post for  more advanced version]
I’m a lazy developer. I hate reinventing the wheel. Worse, I like stealing other people’s wheels! I guess that’s why I love JavaScript so much: no need to have access to any server, I can embed my script into this very blog (or into a Content Editor Web Part if I’m using SharePoint). I can point to a JavaScript library other geniuses have invented, and use their fancy functions against some of the enterprise data accessible through XML (RSS, Web services, REST, etc.), and voila! I can impress my boss in just an hour of work! In future articles, I’m going to show a few examples of amazing things you can do in JavaScript and enterprise data.
Starting with this example. Here I’m going to show how to visualize a SharePoint calendar with the SIMILE Timeline widget.
 Timeline
This is what we’re going to use:
  • SIMILE Timeline widget for the visualization.
  • Darren’s SharePoint JavaScript API to access any SharePoint data. Any SharePoint list or document library data can be accessed through a web service, so Darren’s API makes accessing SharePoint data really easy (you can also use JQuery as I will show in future articles, although I like this one better). You will soon find yourself using this API in many scripts, so to improve its availability I suggest you copy Darren’s JavaScript files on your production server.
  • And finally SharePoint calendar data itself. As I said, the events details are accessible in JavaScript through the calendar web service. To make life easier, all you need to provide is the RSS feed associated to your calendar (see bottom of the script). The feed URL contains both the path of your calendar and its list ID, which is all we need to access the web service.
So there you have it. Modify just the few lines where you see “myserver” (i.e. line 7, 8 9 and bottom of the script), insert the script into a Content Editor Web Part, and enjoy!

<script language="javascript" type="text/javascript">
var Timeline_urlPrefix = "http://simile.mit.edu/timeline/api/";
// Include these javascript files only if not loaded already by another web part
// In portals like a SharePoint page, you never know what script might already be loaded by the master page or other web parts
includeJSScript("http://simile.mit.edu/timeline/api/timeline-api.js");
includeJSScript("http://myserver/js/spapi/spapi_core.js");
includeJSScript("http://myserver/js/spapi/spapi_types.js");
includeJSScript("http://myserver/js/spapi/spapi_lists.js");
function includeJSScript(p_file) {
// before we insert this script, we need to check if it already exists
var bAlreadyExists = false;
var scripts = document.getElementsByTagName('script');
for (var i = 0; i < scripts.length; i++) {
if (scripts[i].src == p_file) {
//scripts[i] is the one
bAlreadyExists = true;
break;
}
}
if (!bAlreadyExists) {
var v_script = document.createElement('script');
v_script.type = 'text/javascript';
v_script.src = p_file;
document.getElementsByTagName('head')[0].appendChild(v_script);
}
}
// Call the web service associated to the calendar to extract its items
function getCalendarListItems() 
{ 
var lists = new SPAPI_Lists(ExtractWebSiteURL(MyList_RSS_Url)); 
var items = lists.getListItems(ExtractListID(MyList_RSS_Url),'',"<Query><OrderBy><FieldRef Name='EventDate' /></OrderBy></Query>",'',100); 
if (items.status == 200) 
{ 
var rows = items.responseXML.getElementsByTagName("z:row");        
return rows; 
} 
else 
{ 
return null; 
} 
} 
var resizeTimerID = null; 
function formatDateString(strDate) 
{ 
var yearStr = strDate.substr(0, 4); 
var monthStr = strDate.substr(5, 2); 
var dayStr = strDate.substr(8, 2);        
return monthStr + "/" + dayStr + "/" + yearStr + " " + strDate.substr(11); 
} 
// This is one of the most important functions. Once the calendar events are retrieved, 
// we loop through them and add them to the timeline
function main() 
{ 
var items = getCalendarListItems(); 
if (items == null) 
{ 
document.getElementById("my-timeline").innerHTML = "Cannot get items from list <i>" + ExtractWebSiteURL(MyList_RSS_Url) + "/" + ExtractListID(MyList_RSS_Url) + "<i>"; 
return; 
} 
var eventSource = new Timeline.DefaultEventSource(); 
// for each event returned by the calednar web service, we create a timeline event
for (var i = 0; i < items.length; ++i) 
{ 
var ows_EventDate = formatDateString(items[i].getAttribute("ows_EventDate")); 
var ows_EndDate = formatDateString(items[i].getAttribute("ows_EndDate")); 
var ows_Title = items[i].getAttribute("ows_Title"); 
var ows_Location = items[i].getAttribute("ows_Location");
var eventDate = new Date(ows_EventDate); 
var endDate = new Date(ows_EndDate); 
var event = new Timeline.DefaultEventSource.Event( 
eventDate, //start 
endDate, //end 
eventDate, //latestStart 
endDate , //earliestEnd 
true, //instant (use FALSE if events are longer than a few hours of duration
ows_Title, //text 
"<strong>Where? </strong>" + ows_Location + "<br><strong>When? </strong>"  //description that appears in a bubble when user clicks on the event
); 
eventSource.add(event); 
} 
// This is where we define 3 timelines. Advanced users can play with these parameters to use different timeline or timeline behaviors
// See http://code.google.com/p/simile-widgets/wiki/Timeline for more information
var theme = Timeline.ClassicTheme.create(); // create the theme    
theme.event.bubble.width = 300;   // modify this bubble size to fit your needs   
theme.event.bubble.height = 170;    
var bandInfos = [ 
Timeline.createBandInfo({ 
trackGap:       0.5, 
width:          "60%", 
intervalUnit:   Timeline.DateTime.WEEK, 
intervalPixels: 100, 
timeZone : 8, 
eventSource: eventSource, theme:theme 
}), 
Timeline.createBandInfo({ 
showEventText:  false, 
trackHeight:    0.5, 
trackGap:       0.2, 
width:          "25%", 
intervalUnit:   Timeline.DateTime.MONTH, 
intervalPixels: 150, 
timeZone : 8, 
eventSource: eventSource 
}), 
Timeline.createBandInfo({ 
showEventText:  false, 
trackHeight:    0.5, 
trackGap:       0.2, 
width:          "15%", 
intervalUnit:   Timeline.DateTime.YEAR, 
intervalPixels: 400, 
timeZone : 8, 
eventSource: eventSource 
}) 
]; 
bandInfos[1].syncWith = 0; 
bandInfos[2].highlight = true; 
bandInfos[2].syncWith = 1; 
var timeLine = Timeline.create(document.getElementById("my-timeline"), bandInfos); 
} 
function ExtractWebSiteURL(sUrl) {
var index = sUrl.toLowerCase().indexOf("_layouts");
var MyCurrentPath = "";
if (index != -1) {
MyCurrentPath = sUrl.substring(0, index);
MyCurrentPath = MyCurrentPath.substring(0,MyCurrentPath.lastIndexOf('/'));
}
else { return null;}
return MyCurrentPath 
}
function ExtractListID(sUrl) {
var index = sUrl.toLowerCase().indexOf("_layouts");
var DestinationListID = "";
if (index != -1) {
DestinationListID = unescape(sUrl.substring(index + 28, sUrl.length));
}
else { return null;}
return DestinationListID 
}
// _spBodyOnLoadFunctionNames.push is a SharePoint function that insures that the script will be run only AFTER the page has been loaded
_spBodyOnLoadFunctionNames.push("main"); 
// ******************** Settings ******************** 
// URl of the RSS associated to your calendarvar
MyList_RSS_Url = "http://myserver/mysite/_layouts/listfeed.aspx?List=%7B3186664F%2D626C%2D4925%2D896B%2D53517E1D0244%7D"; 
</script>
<!-- Feel free to modify the following parameters: height, border, font -->
<div id="my-timeline" style="height: 120px; border: 1px solid #aaa; font-size: 9pt"></div>  

December 27, 2009

SharePoint People Search Autocomplete

Back in July, Jan Tielens demonstrated how to write an autocomplete mechanism for SharePoint search. Muhimbi then proposed a greatly enhanced version 2.

In our version, we have enhanced the code just a little to enable the following:

  • paging: you can display results n at a time, and offer a link to go to the next page

Next

  • sorting: allow users to refresh results by alphabetical order, by department, etc. (you can add your own sorting criteria)

SortBy

  • search across all people metadata:  let people either search on name, skills, office number, phone number, or any other indexed metadata you include in the search query

Terms

  • Rich formatted results: display picture, presence awareness (Office Communicator) and links to internal or external systems (Yammer, Skype, Facebook, etc.)

RichResult

  • Refine search results: allow users to click on any metadata on the autocomplete search to drill down into specific criteria

drilldown

Example of the final result:

PeopleAutocomplete

To implement this version of the people search autocomplete, copy the code below, modify it as you need (mandatory: modify the server name), and paste it on a Content Editor web part.

December 10, 2009

Using SharePoint Search Web Service to Surface Blog Posts or other Content Type

One of the most powerful though untapped features of SharePoint search is its web service. SharePoint search web service url looks like this: http://Sp_Server/_vti_bin/search.asmx. JavaScript aficionadi like myself get excited when they hear about web services, because we immediately start thinking of the rich functionalities we can then easily add to any HTML page, or to SharePoint with a Content Editor Web Part (by far my favorite web part). Why the excitement? Mainly for 2 reasons:
  • Contrary to other data interaction mechanisms like ODBC, BDC, etc., web services are self-describing. All you need to know about methods, inputs and outputs is in the WSDL file (check http://Sp_Server/_vti_bin/search.asmx?wsdl). No need to contact the developer or database administrator for a password or parameters type.
  • Armed with Ajax (remember, Ajax is just JavaScript, nothing to brag about or to fear), and, say, a Content Editor Web Part (or just a basic HTML page), you don’t even need to have access rights to any server to get this functionality on your site. Everything is done on the client side.
So let’s see how we can use SharePoint search web service to search only from a certain content type on your site, like blog posts, wikis, threaded discussions, documents, pictures, etc. (see basic list of available content types here). Since the web service returns some XML, it’s up to you to decide how and where to display the results. In this example, the result of the code below looks like this:
capture
 Generate the query
In theory, you’d need to study a little bit how to build a QueryPacket, or learn about the SQL Search language. In reality, you don’t have to study anything at all, thanks to query generators like SharePoint Search Service Tool or Search Coder. I often use both, since they have complementary functionalities. So it shouldn’t take you too long to come up with the following query. Notice that “AND (CONTAINS (ContentType,'"post"'))” is the trick to filter by content type.
<QueryPacket xmlns="urn:Microsoft.Search.Query" Revision="1000">
<Query domain="QDomain">
<SupportedFormats>
<Format>urn:Microsoft.Search.Response.Document.Document</Format>
</SupportedFormats>
<Context>
<QueryText language="en-US" type="MSSQLFT">
<![CDATA[ 
SELECT Title, Rank, Size, Description, Write, Path, 
PersonalSpace, Author, Title, Path, Created, 
CreatedBy, PictureURL, Account, EmployeeID 
FROM portal..scope()  
WHERE FREETEXT(DefaultProperties, 'My search terms') 
AND  ( ("SCOPE" = 'All Sites') )  AND (CONTAINS (ContentType,'"post"')) 
ORDER BY "Rank" Desc, "Created" Desc" 
]]>
</QueryText>
</Context>

<Range><StartAt>1</StartAt><Count>10</Count></Range>
<EnableStemming>true</EnableStemming>
<TrimDuplicates>true</TrimDuplicates>
<IgnoreAllNoiseQuery>true</IgnoreAllNoiseQuery>
<ImplicitAndBehavior>true</ImplicitAndBehavior>
<IncludeRelevanceResults>true</IncludeRelevanceResults>
<IncludeSpecialTermResults>true</IncludeSpecialTermResults>
<IncludeHighConfidenceResults>true</IncludeHighConfidenceResults>
</Query>
</QueryPacket>





Call the Web Service


In a previous blog, I’ve showed how to use Darren’s JavaScript library to interact with SharePoint web services. I could have done the same here, especially since he developed a library specifically for search, but I chose to use JQuery instead. Why? No reason, I just like to explore different technologies ;)


 Final Result


So now all you have to do is copy the  code below, change the server name and paste the code into a Content Editor Web Part (or any other HTML page). Notice that while this example is about filtering by content type, the same technique can be used to filter by scope and any metadata you wish. In fact, you’ll quickly realize that this search web service is way more powerful than SharePoint Content Query Web Part, as search doesn’t care about site collection boundaries and has access to a wide variety of filters.



<script language="javascript">
// _spBodyOnLoadFunctionNames.push is a SharePoint OOTB function 
// that ensures the function is called only after the DOM has been loaded
_spBodyOnLoadFunctionNames.push("DisplayBlogSearchResults");

// Change these parameters as needed
var maxResultsToDisplayBlogs = 5;
var webSite = “http://url.of.site/;

function DisplayBlogSearchResults() {

// the search terms is passed in the query string (e.g., blogsearch?k=tax+reform)
var query = unescape(querySt("k"));    

var queryXML = 
"<QueryPacket xmlns=\"urn:Microsoft.Search.Query\" Revision=\"1000\">"+
"    <Query domain=\"QDomain\">"+
"        <SupportedFormats><Format>urn:Microsoft.Search.Response.Document.Document"+
" </Format></SupportedFormats>"+
"        <Context>"+
"     <QueryText language=\"en-US\" type=\"MSSQLFT\"><![CDATA[ "+
"SELECT Title, Rank, Size, Description, Write, Path, PersonalSpace, "+
"Author, Title, Path, Created, CreatedBy, "+
" PictureURL, Account, EmployeeID FROM "+
" portal..scope() " + 
//" WHERE CONTAINS ('\"" + query + "\"') " +
" WHERE FREETEXT(DefaultProperties, '" + query + "') " +
" AND  ( (\"SCOPE\" = 'All Sites') )  AND (CONTAINS (ContentType,'\"post\"'))" +
" ORDER BY \"Rank\" Desc, \"Created\" Desc"  +
" ]]>" +
"      </QueryText>" +
"        </Context>"+
"        <Range><StartAt>1</StartAt><Count>" + 
maxResultsToDisplayBlogs + "</Count></Range>"+
"        <EnableStemming>true</EnableStemming>"+
"<TrimDuplicates>true</TrimDuplicates>"+
"<IgnoreAllNoiseQuery>true</IgnoreAllNoiseQuery>"+
"<ImplicitAndBehavior>true</ImplicitAndBehavior>"+
"<IncludeRelevanceResults>true</IncludeRelevanceResults>"+
"<IncludeSpecialTermResults>true</IncludeSpecialTermResults>"+
"<IncludeHighConfidenceResults>true</IncludeHighConfidenceResults>"+
"</Query></QueryPacket>";

var soapEnv =
"<soap:Envelope xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' " +
"xmlns:xsd='http://www.w3.org/2001/XMLSchema' " +
"xmlns:soap='http://schemas.xmlsoap.org/soap/envelope/'>"+
" <soap:Body>"+
"<QueryEx xmlns='http://microsoft.com/webservices/OfficeServer/QueryService'>"+
"     <queryXml>" + escapeHTML(queryXML) + "</queryXml>"+
"   </QueryEx>"+
" </soap:Body>"+
"</soap:Envelope>";

$.ajax({
url: webSite +"/_vti_bin/search.asmx",
type: "POST",
dataType: "xml",
data: soapEnv,
complete: processResult,
contentType: "text/xml; charset=\"utf-8\""
});          


}

// processResult is called async. when the web service returns something
function processResult(xData, status) {

var TotalResults = 0;                
var xmlDoc = xData.responseXML;
var docs = xmlDoc.selectNodes("//RelevantResults");
// Total Results returned 
// (should be equal or less than <Count> parameter of search query)
TotalResults = docs.length;

// Total available results (while the query only returns
// the first <Count> results, there might be more available)
var TotalAvailable = 0;
var docTotalAvailable  = xmlDoc.selectNodes("//xs:element[@name='RelevantResults']");
if (docTotalAvailable.length != 0) {
TotalAvailable = docTotalAvailable[0].getAttribute("msprop:TotalRows"); 
}

var strDisplay="";
if (TotalResults >0) {
strDisplay = "<table width='100%' style='BORDER: #8ebbf5 1px solid'><tr>";
strDisplay += "<td><img src='http://atgdev-intranet.imf.org/_layouts/images/buddychat.jpg' ";
strDisplay += "style='float:left;vertical-align:middle'/>"; 
strDisplay += "<span style='font-size:12px'>"; 
strDisplay += "<strong>You may also be interested by these " + TotalResults 
strDisplay += " blogs:</strong></span></td>";
strDisplay += "</tr>";
}

for(var i = 0; i < TotalResults ; i++){
var title = docs[i].selectSingleNode("TITLE") != null ? docs[i].selectSingleNode("TITLE").text : "TITLE not found";
var path = docs[i].selectSingleNode("PATH") != null ? docs[i].selectSingleNode("PATH").text : "PATH not found";
var creationDate = docs[i].selectSingleNode("CREATED") != null ? docs[i].selectSingleNode("CREATED").text : "CREATED not found";
var author = docs[i].selectSingleNode("AUTHOR") != null ? docs[i].selectSingleNode("AUTHOR").text : "PATH not found";

strDisplay += "<tr><td>";
strDisplay += "<img src='/_layouts/images/bullet.gif' style='vertical-align:middle' />&nbsp;";
strDisplay += "<a href='" + path + "'>" + title + "</a>";
strDisplay += "<div style='color:#dbdbdb;text-align:right'>written on " + formatDateString(creationDate)
strDisplay += " by <span style='color:#545454'>" + author + "</span></div>";
strDisplay += "</td></tr>";

}
// Verify if we have displayed the total available or not
if (TotalAvailable > TotalResults) {
strDisplay += "<tr style='text-align:right'><td><a href=''><br />See all " + TotalAvailable + " results...</a></td></tr>"
} 
if (TotalResults >0) {
strDisplay += "</table>";
}

// Display result is specific DIV (id=idBlogSearchResults). Could be located anywhere in your page.
$("#idBlogSearchResults").html(strDisplay);

}

function querySt(ji) {
hu = window.location.search.substring(1);
gy = hu.split("&");
for (i=0;i<gy.length;i++) {
ft = gy[i].split("=");
if (ft[0] == ji)  return ft[1];
}
}

function escapeHTML (str) {
return str.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}

function unescapeHTML (str) {
return str.replace(/&lt;/g,'<').replace(/&gt;/g,'>');
}

function formatDateString(strDate) {
var yearStr = strDate.substr(0, 4);     
var monthStr = strDate.substr(5, 2);     
var dayStr = strDate.substr(8, 2);           
return monthStr + "/" + dayStr + "/" + yearStr; 
} 

</script>





Have fun!