Posts Tagged ‘api’

11 Sep 09

Writing ajax search suggestions using the MindTouch API

After writing a recent blog post about using the search API to offer efficient and widespread navigation, I started thinking about another neat use for the search API, search suggestions.  This may be a misnomer so please allow me to explain.  Search suggestions refer to the dropdown menus under a search box that use ajax to offer realtime search results and commonly eliminate the need to travel to a search results page, ultimately making the search experience faster and generally much more impressive.  Here are some examples.

Apple

The Apple website offers what I consider the most elegant search suggestion solution.  They have combined design and function to offer a tool that is easy to understand and thorough enough to cover multiple “departments”.

apple-search-suggest1

Google

The Google search suggestion solution was actually launched as a full Google product and was kept separate from the standard Google search until recently. While it is not as elegant as the Apple offering it does offer more search results (which is obviously expected) and does eliminate the need to visit the search results page.  They’ve also included relevant advertisements which may be a testament to its success.

google-search-suggest

Building a MindTouch Search Suggest

Search API

To get started the first thing you need to familiarize yourself with is MindTouch Search API.  Basically all you need to know is how to construct the url.

www.yourmindtouch.com/@api/deki/site/search?limit=5&q=help

Using this url you can quickly access the search API for different terms and limit your result set accordingly.

HTML

In order to get our search suggest working we need to layout some basic HTML.  This HTML includes the search input and the dropdown menu that will later get populated by jQuery.  You must add this HTML to a MindTouch page in source mode. 

<h1>Search Suggestion</h1>
<input type="text" id="search-input">

<div id="search-suggest">
	<a href="#" class="close">x</a>
	<div class="search-pages">
		<strong>pages</strong>
	</div>
	<div class="search-files">
		<strong>files</strong>
	</div>
	<div class="search-comments">
		<strong>comments</strong>
	</div>
</div>

JQuery

Now that you know how to access the API and you have your base HTML you need to connect all the parts.   Again this needs to be entered in source mode .  I’ve added some highlighted comments to walk you through the jquery.

<script type="text/javascript">
	$("body").ready( function() {

                /*Hide the dropdown menu when a user clicks the close button*/
		$("#search-suggest a.close").click( function() {
			$("#search-suggest").hide();
			return false;
		});

                /*Run this function each time the uses pushes a key in the search box*/
  		$("#search-input").keyup( function() {
			if ($(this).val()==''){
				$("#search-suggest").hide();
				return false;
			}
                        /*set q = to the search term*/
			var q = $(this).val();

                    /*Delay the function from executing for 400ms so it doesn't run on every keyup*/
 		    clearTimeout($.data(this, "timer"));
		    var ms = 400; //milliseconds
		    var wait = setTimeout(function() {

                      /*Run the search function with search string q*/
		      loadsearch(q);
		    }, ms);

		    $.data(this, "timer", wait);
		});
	});

function loadsearch(q) {
        /*API url*/
	var qurl="http://developer.mindtouch.com/@api/deki/site/search?limit=10&q="+q;
        /*Count variable to keep track of how many pages, files and comments*/
	var cntpage=0;
	var cntfile=0;
	var cntcomments=0;
        /*Load the search using ajax, return the results in xml variable*/
	jQuery.get(qurl, function(xml) {
                /*Remove the existing links so you can add new content*/
		$("#search-suggest .search-pages a").remove();
		$("#search-suggest .search-files a").remove();
		$("#search-suggest .search-comments a").remove();
		$("#search-suggest .search-comments span").remove();
                /*Use Jquery selectors with XML to find the necessary nodes - Path, Title*/
		$(xml).find('search > page').each(function(){
			var qpath = $(this).find(" > path").text();
			var qtitle = $(this).find("> title").text();
                        /*Add the new search result to the search-pages dropdown*/
		        $("#search-suggest .search-pages ").append('<a class="page" href="/' + qpath + '">' + qtitle.substr(0,30) + '</a>');
			cntpage++;
		});
                /*Use Jquery selectors with XML to find the necessary nodes - Contents, Filename*/
		$(xml).find('search > file').each(function(){
			var quri = $(this).find(" > contents").attr("href");
			var qtitle = $(this).find(" > filename").text();
			$("#search-suggest .search-files").append('<a class="file" href="' + quri + '">' + qtitle + '</a>');
			cntfile++;
		});
                /*Use Jquery selectors with XML to find the necessary nodes - Username, Comment, URI*/
		$(xml).find('search > comment').each(function(){
			var quser = $(this).find("username").text();
			var qcomment = $(this).find(" > content").text();
			var quri = $(this).find("path").text();
			$("#search-suggest .search-comments").append('<a href="/' + quri + '" class="comment-user">' + quser + '</a><span class="comment-text">' + qcomment.substr(0,40) + '...</span>');
			cntcomments++;
		});
                /*If there are no pages, hide the page dropdown*/
		if(cntpage>0){
			$("#search-suggest .search-pages").show();
		}else {
			$("#search-suggest .search-pages").hide();
		}
                /*If there are no files, hide the file dropdown*/
		if(cntfile>0){
			$("#search-suggest .search-files").show();
		}else {
			$("#search-suggest .search-files").hide();
		}
                /*If there are no comments, hide the comment dropdown*/
		if(cntcomments>0){
			$("#search-suggest .search-comments").show();
		}else {
			$("#search-suggest .search-comments").hide();
		}
                /*If there are any results show the appropriate dropdowns*/
		if(cntpage>0 || cntfile>0 || cntcomments>0){
			$("#search-suggest").show();
		}else {
			$("#search-suggest").hide();
		}
	});
}
</script>

CSS

Lastly, once you’ve got it working it needs to look good, otherwise it might not be legible at all.   I’ve put some simple CSS together but feel free customize the look & feel.

<div>
<style>
#search-suggest strong{
	margin:0 0 10px 0;
	color:#999;
	display:block;
}
#search-suggest {
	border:1px solid #ccc;
	display:none;
	padding:0;
	position:absolute;
	overflow:hidden;
	background:#fff;
	font-size:11px;
	min-width:150px;
	z-index:50;
}
#search-suggest .search-pages{
	float:left;
	padding:5px 20px;
	max-width:200px;
}
#search-suggest .search-files{
	float:left;
	padding:5px 20px;
	max-width:200px;
}
#search-suggest .search-comments{
	float:left;
	padding:5px 20px;
	max-width:200px;
}
#search-suggest a {
	display:block;
	padding:0 0 0 20px;
	height:16px;
	overflow:hidden;
	margin:0 0 5px 0;
	background-image:url(/skins/common/icons/icons.gif);
	background-repeat:no-repeat;
}
#search-suggest .search-pages a {
	background-position:0 -768px;

}
#search-suggest .search-files a {
	background-position:0 -1040px;
}
#search-suggest .search-comments .comment-user {
	display:block;
	font-weight:bold;
	margin-bottom:0px;
	padding:0;
}
#search-suggest .search-comments .comment-text {
	display:block;
	font-weight:100;
	margin-bottom:10px;
	color:#999;
}
.no-show {
	display:none;
}
#search-suggest a.close{
	float:right;
	display:block;
	font-size:14px;
	width:15px;
	padding:0 0 1px 0;
	line-height:1;
	-moz-border-radius:7px;
	border:2px solid #fff;
	font-weight:bold;
	text-align:center;
	background:#ccc;
	color:#fff;
	text-decoration:none;
}
#search-suggest .close:hover{
	background:#9f1313;
	text-decoration:none;
	color:#fff;
}
</style>
</div>

MindTouch

The MindTouch search suggestion tool cleanly separates pages, files and comments to show you a wide variety of search results.  Additionally it is legible and does eliminate the need to travel to the search results page.

mindtouch-search-suggest1

I hope you enjoyed this tutorial

Damien Howley
@DamienH

30 Jun 09

Animated Ajax Pageview Ticker

In my infinite quest to make everything better I found myself quite frustrated with the use of “plaintext” to display the number of pageviews on a MindTouch page .  Plaintext was clearly way too simple and I saw an opportunity to make it way more complex, quite possibly even confusing.

Using plain numbers is boring, they don’t move, shake, glide or slide.  Plaintext is not flashy enough and it doesn’t update.   If another ten billion people visit the page you’ll never know it’s true popularity, at least not until you refresh (so 90′s).  I know, all very pointless points but this is what I have to tell myself to justify such an endeavour.

counter_ticker_bg

To combat my frustration I decided to build an automated number ticker that updates using ajax to display the REAL-TIME pageviews of a MindTouch page.  The ajax utilizes the MindTouch API to retrieve the latest pageview data.  I used CSS to make the nice numbers which referenced this image.

The Number Ticker currently hits the API every 10 seconds.  This can be reduced to savor server resources and should be a major consideration when deciding to use this script or not.

Lastly, I consolidated it all into one MindTouch template so it can be included into multiple pages.  To create this template:

  1. copy the code below
  2. go to your template namespace (yourwiki.com/template:)
  3. create a new page
  4. view source
  5. paste and save.

Template Code

<h1>Template:AjaxCounterTicker</h1>
<input type="hidden" id="pgapi" value="{{page.api}}">
<div class="counter-wrap">
    <div class="counter-number">
        &nbsp;
    </div>
</div>
<div>
<style>
.counter-wrap {
    height:18px;
    overflow:hidden;
}
.counter-number {
    height:198px;
    width:12px;
    position:relative;
    background-image:url(http://developer.mindtouch.com/@api/deki/files/4548/=counter_ticker_bg.gif);
    float:left;
}
</style>
</div>
<script type="text/javascript">
Deki.$("body").ready( function() {
    getviewcount();
    setInterval("getviewcount()", 10000);
});
function getviewcount() {
    var pgapi = Deki.$("#pgapi").val();
    Deki.$.ajax({
        type: "GET",
        url: pgapi,
        async: false,
        dataType: "xml",
        success: function(xml) {
            var pgviewnum = Deki.$(xml).find("metric\\.views").text();
            loadticker(add_commas(pgviewnum));
        }
    });
}
Deki.$(".counter-number").each( function(i) {
    Deki.$(this).attr('id','num'+i);
});
function loadinput() {
    var newval = Deki.$("#numgo").val();
    loadticker(newval);
}
function loadticker(ticnum) {
    var numheight=18;
    addticker(ticnum);
    if (ticnum && ticnum != 0) {
        var s = String(ticnum);
        for (i=s.length;i>=0; i--)
        {
            var onum=s.charAt(i);
            Deki.$("#num"+i).attr('value',onum);
        }
        Deki.$(".counter-number").each( function() {
            var nval=Deki.$(this).attr("value");
            if (!isNaN(nval)) {
                var nheight = Number(nval)*numheight*-1;
                Deki.$(this).animate({ top: nheight+'px'}, 1500 );
            }
            if (nval==','){
                Deki.$(this).animate({ top: '-180px'}, 1500 );
            }
          });
    }
}
function addticker(newnum) {
    var digitcnt = Deki.$(".counter-number").size();
    var nnum = String(newnum).length;
    var digitdiff = Number(nnum - Number(digitcnt));
    if (digitdiff <0) {
        var ltdig = (Number(nnum)-1);
        Deki.$(".counter-number:gt(" + ltdig + ")").remove();
    }
    for(i=1;i<=digitdiff;i++) {
        Deki.$(".counter-wrap").append('<div class="counter-number" id="num' + (Number(digitcnt+i-1)) + '">&nbsp;</div>');
    }
}
function add_commas(nStr) {
    nStr += '';
    x = nStr.split('.');
    x1 = x[0];
    x2 = x.length > 1 ? '.' + x[1] : '';
    var rgx = /(\d+)(\d{3})/;
    while (rgx.test(x1)) {
        x1 = x1.replace(rgx, '$1' + ',' + '$2');
    }
    return x1 + x2;
}
</script>

How to call the Template

{{AjaxCounterTicker();}}

If you have questions please post them as comments and I’ll be sure to respond.  Thank you for your time.

Damien Howley
@DamienH

24 Apr 09

MindTouch SSO: auto-login from your existing app

Authentication nirvana
Authentication Nirvana

One of the first concerns admins have when considering or deploying MindTouch to an intranet is how everyone will log in. It’s essential that the bar is set as low as possible for your users to get started by telling your users that their existing credentials for another application will already work with MindTouch. Better yet, having them be logged in automatically into the wiki will lower the bar to making contributions.

This is the first in a series of posts to detail by example the various approaches you can take to integrate MindTouch into your environment in regards to authentication. This post focuses on allowing your existing intranet portal site or another application where users log in manually to automatically log them into your MindTouch installation.

First a little background about how MindTouch keeps users logged in. Like many apps, it uses a simple cookie to determine who the current user is. This cookie is issued by the API (POST: users/authenticate) in response to valid credentials (for local users or to an external authentication module) and is passed back through the UI out to the browser where it’s saved. All subsequent requests to the UI and to the API have this cookie passed and requests are processed under the context of the user described in the cookie. Since the UI here simply acts as a middleman to the API, having another actor such as your portal site or 3rd party app perform the same authentication request becomes simple.

This single sign on technique requires that you

  • Have access to modify source code or hook into your existing app with which you’d like to integrate your authentication
  • Your MindTouch installation is hosted in a subdomain of your existing app. For example your portal is: http://intranet.company.com then your install must be at or below http://wiki.intranet.company.com
  • Enable trusted authentication in your MindTouch installation

This technique should generally be used if you

  • Want your users that are logged into an application you have to automatically be signed into your MindTouch instance
  • Don’t have an LDAP directory or the users you want to be able to login do not exist in the directory
  • Have many external or non Windows users making integrated windows auth / NTLM / Kerberos auth impossible

As described above, this requires an http call from your existing app to the API’s POST:users/authenticate feature in your app’s sign in logic before it returns to the user.  In this request, the username should be set and an empty password provided using the standard Authorization header. Two query parameters are needed: apikey={your apikey} and authprovider=1. By providing the apikey you’re saying that you want the user created if it doesn’t exist or to authenticate without providing a password. The authprovider query parameter tells MindTouch which authentication provider to use. 1 is built in authentication and no calls to external providers are done. If a different authprovider id is specified, an external user lookup and group sync will be performed but the request will fail if the user doesn’t exist externally. You’ll need to configure your MindTouch install to ensure trusted authentication is enabled by setting “security/allow-trusted-auth” to “true” in control panel -> configuration.

Some example code using built in C# methods:

public static string BuildMindTouchAuthToken(string username) {
    string authtoken = null;

    //These settings should be looked up from configuration
    string dekiApiUri = "http://wiki.intranet.mycompany.com/@api/deki";
    string apikey = "somekey";
    string authProviderId = "1"; 

    string authRequestUri = string.Format("{0}/users/authenticate?authprovider={1}&APIKEY={2}", dekiApiUri.TrimEnd('/'), authProviderId, APIKEY);

    //Perform HTTP request to MindTouch's POST: users/authenticate
    HttpWebRequest authRequest = HttpWebRequest.Create(authRequestUri) as HttpWebRequest;
    authRequest.Credentials = new NetworkCredential(username, string.Empty);
    authRequest.Method = "POST";
    authRequest.PreAuthenticate = true;
    authRequest.ContentLength = 0;
    authRequest.CookieContainer = new CookieContainer();
    HttpWebResponse authResponse = authRequest.GetResponse() as HttpWebResponse;
    if(authResponse.StatusCode == HttpStatusCode.OK) {
        authtoken = authResponse.Cookies["authtoken"].Value;
    }

    return authtoken;
}

This will return an auth token for the provided user that you will then need to return back to the browser with a wildcard subdomain. Here’s ASP.NET code:

    string authtoken = BuildMindTouchAuthToken(username);
    if(!string.IsNullOrEmpty(authtoken)) {
        HttpCookie authTokenCookie = new HttpCookie("authtoken", authtoken);
        authTokenCookie.Domain = ".wiki.intranet.mycompany.com";
        authTokenCookie.Expires = DateTime.UtcNow.AddDays(7); //Set to same as MindTouch's security/cookie-expire-secs. (7 days by default)
        Response.SetCookie(authTokenCookie);
    }

Once this cookie is returned back to the browser, every user that logs into your application also gets automatically logged in to your MindTouch install. I’ll be creating a section describing this and other authentication techniques in the dev wiki. Meanwhile, if you have any questions or ideas about this don’t hesitate to leave a note! Otherwise reach me at the usual. Happy integrating!

irc://irc.freenode.net/#mindtouch

twitter: maxmass

16 Apr 09

Hooking into the MindTouch Event Bus

With 9.02 out the door, I wanted to shed a bit more light on a new feature I briefly touched on before: The Deki Event Bus. This article mostly covers the purpose and benefits of events, only a brief overview of its implementation, but I promise to follow it up with some posts that walk through building real world use cases with the event bus.

What are Events all about?

MindTouch emits a number of events into the new PubSubService that is built into Dream. Every service is provided a Plug instance to the defaullt PubSubService making it simple to publish and subscribe to events. By subscribing to events, your service will be called automatically when an event occurs, allowing it to react to changes in the Wiki in real time. But events are a different extension model from the familiar model of making REST calls to interact with MindTouch, so there are a couple of things to be aware of:

Events are asynchronous and do not affect the action that caused them, which means that by the time you receive an event the cause of it has already completed. They cannot be used as hooks to intercept and alter existing behavior, although the receiver of the event could easily use the API to alter the resource referenced by the event.

Events are real time only, so if you are not subscribed when the event happens, you miss it. There is no back-log of events that is fed to subscribers. This means that if a service restarts and events happen before the restarted service re-subscribes, the events will be missed. Events should not be used as authoritative when it comes to the wiki’s state, as you may miss some  and drift out of sync. Instead they should be used as triggers to fetch the up-to-date information from the Wiki. Because of this, events generally do not carry a lot of data, trying to limit themselves to only immutable information such as canonical URIs and IDs for resources.

Events are multicast, so any number of services can subscribe to them without affecting another. This also means that you cannot assume that you are either the first or last to react to the event. Nor can you assume that just because you are acting on one event another event of the same type has not already occured.

Events provide a loose coupling where neither the sender nor the receiver has to know any implementation details of the other. Much in the same way as service features provide a loose coupling for synchronous interaction with MindTouch, events provide asynchronous notifications of actions occuring in MindTouch.

This last point is the true benefit of the event system: It allows us to change its implementation details of actions without affecting code that relies on those actions. Between the service api and the event schemas, there exist a contract for interactions. And this loose coupling benefit isn’t just something we think is beneficial to those who wish to extend MindTouch’s functionality. We are using it ourselves already in 9.02 because it allows us to break the Wiki into more independent components that are easier and safer to maintain or to swap out with alternative implementations.

Using Events

Since every dream service receives a plug to the default PubSubService, subscribing to events is as simple as creating a feature to receive events and posting a subscription against the PubSubService to register that feature.

Let’s assume that we have a service that wants to listen to all user logins. This service exists at http://myhost.com/userloginwatch/ and has a public feature for receiving the user login events (for illustrative purposes the feature is public so we don’t have to worry about authorization details):

[DreamFeature("POST:notify", "receive a login notification")]
public Yield PostLogin(
    DreamContext context,
    DreamMessage request,
    Result<DreamMessage> response) {
  ...
  response.Return(DreamMessage.Ok());
  yield break;
}

In order to hook this service into the event bus, it needs to post a subscription document, like the one below, against its pubsub Plug:

<subscription-set>
  <uri.owner>http://myhost.com/userloginwatch/</uri.owner>
  <subscription>
    <channel>event://*/deki/users/login</channel>
    <recipient authtoken="APIKEY">
      <uri>http://myhost.com/userloginwatch/notify</uri>
    </recipient>
  </subscription>
</subscription-set>

Our recipient is the PostLogin() feature which exists at http://myhost.com/userloginwatch/notify. In addition to the Uri, Deki needs to know that our service is authorized to receive this event and expects the Deki apikey as the authtoken for the recipient. Since we also want to remove our subscription when we’re done, we also need to capture the location at which the subscription is stored. This is generally done in the Start() of the service like this:

// post the subscription
XUri serverUri = Self.Uri.AsServerUri();
XDoc subscription =
  Result<DreamMessage> subscribe;
  yield return subscribe = PubSub.At("subscribers")
    .PostAsync(new XDoc("subscription-set")
      .Elem("uri.owner", serverUri.ToString())
      .Start("subscription")
        .Elem("channel", "event://*/deki/users/login")
        .Start("recipient")
          .Attr("authtoken", _apikey)
          .Elem("uri", serverUri.At("notify").ToString())
        .End()
      .End());

// retrieve the access key for our subscription,
// so we can later remove it
string accessKey = subscribe.Value.AsDocument()["access-key"].AsText;
XUri location = subscribe.Value.Headers.Location;

// add the access key as a cookie so it is automatically sent
Cookies.Update(
  DreamCookie.NewSetCookie("access-key", accessKey, location),
  null);

// and store our subscription location,
// so we can delete it in Stop()
XUri localLocation = location.AsLocalUri().WithoutQuery();
_subscriptionLocation = Plug.New();

With this done, every time a user logs in, our service will have its PostLogin() feature invoked with an event document like this:

<deki-event wikiid="{wikiid}" event-time="{datetime}">
  <channel>event://{wikiid}/deki/users/login</channel>
  <userid>{userid}</userid>
  <uri>{user-uri}</uri>
</deki-event>

This allows our service to asynchronously react to a user logging in, by tracking login stats, retrieving and examining the user, etc.

The present and future of Events

9.02 offers a rich set of events, tracking changes in MindTouch, that can be subscribed to. We see events as a significant extension facility, and are using it ourselves because of the benefits of its loose coupling, and to dogfood the technology for everyone else. We’ve shipped two services in 9.02 that use the event bus, the Lucene index service was rewritten to use events for queueing up resources that need to be indexed and the new Page Alerts system was introduced to create a mechanism for users to subscribe to Page changes.

I hope that events will prove to be a useful extension mechanism for all those who wish to add custom behavior to Deki. Currently these events are primarily based on actions that change the state of resources in MindTouch. In the future we will continue to expand the number of available events, likely adding more events based on operational actions. If there are any specific events you would like to see, please let me know in the comments.

12 Mar 09

Exploring Deki Lyons: Properties

attachThe upcoming MindTouch Deki Lyons release has a new feature that further enhances Deki’s ability to be extended and used as a glue to your other applications and data sources: properties. It offers a way to associate pages, users, attachment, and the whole site with content-agnostic data that can then be retrieved and consumed by the front end or any client of the Deki API.

This simple yet powerful concept provides your Deki extensions, DekiScripts, and external applications a convenient storage mechanism to persist state that’s pertinent to the resources being operated on. With Lyons, properties can be associated with users, pages, files attachments, and the site itself. Data as simple as a one character text string to a 16MB binary blob can be stored and accessed with the Deki API via the same RESTful conventions that you’re already used to. SteveB has implemented some read-only DekiScript functions and is wrapping the API interface with a Javascript layer to allow full read/write capabilities from within pages. GuerricS has also added a simple UI for viewing and modifying page properties (More from top menu -> Set Page Properties).

Since this is a dev blog it’d be criminal of me not to dive further into some geeky details about the implementation of the feature. Shortly after seeing the new attachments data model come to life it was apparent that the same db model and most of the code can be reused to represent a much more generic revisionable store of resources. With just a few model changes and code abstractions the property feature was a natural progression. SteveB and I spent a few weeks flushing out the API interface with much of it being based on the existing attachment features but with some added inspiration from Atom (we couldn’t have come up with an http header named Slug ourselves!). The UI is already using site properties as a means of storing custom styles and configuration documents for the editor. As part of the file attachment upgrade we decided to use a file property to store file descriptions. We’re also looking towards properties as a basis for some upcoming core Deki features (page drafts, auth service user info integration, content ratings, etc).

Although this first iteration should provide considerable value to Deki hackers and Deki itself, some enhancements are already being planned in the areas of search, permission integration, and revisioning that will allow even more flexibility. Meanwhile we’re going to be listening to community and customer feedback and having some fun putting together our own features to take advantage of it.  I’m looking forward to seeing Lyons roar out into the open and to seeing the cool stuff you’re going to build with it!

Take a look at how to use the properties API here and stay tuned for another post with examples. Discuss here, on the forum, or in irc!

Copyright © 2011 MindTouch, Inc. Powered by