Zafu: CouchDB and KendoUI (part 4)


In recent posts I’ve shown how to use KendoUI cascading ComboBox and map/reduce in CouchApps, Lets combine the two of them.

Populating KendoUI ComboBox

NOTE: I’m going to be using the database used in here, refer to that post for details.

Lets start defining the map and reduce JavaScript code using in this view:

// Map function
function(doc) {
    if (doc.type==="cities") emit(doc.state, doc.city );
}
// Reduce function
function(keys, values) {
    return true;
}

The map function emits pairs of records with a key that is the state and a value that is the city. This allows me to use the same map/reduce for getting the states (setting reduce and group to true) and for getting the cities in a state (by setting as key the name of the state).

Then I will start populating the first ComboBox with the name of the states:

$("#state").kendoComboBox({
    autoBind      :true,
    ignoreCase    :true,
    highlightFirst:true,
    delay         :100,
    placeholder   :"State",
    suggest       :true,
    dataSource    :{
        serverFiltering:true,
        transport      :{
            read:function (operation) {
                db.view("post20/getCities", {
                    success:function (data) {
                        var states = [];
                        var rows = data.rows;
                        $.each(rows, function (idx, value) {
                            states.push(value.key);
                        });
                        operation.success(states);
                    },
                    error  :function (status) {
                        console.log(status);
                    },
                    group  :true,
                    reduce :true
                });
            }
        }
    }
}).data("kendoComboBox");

In the success function what I do is extracting the keys retrieved that are the name of the states.

Populating the cities uses the same view but disables reduce by setting it to false, In addition I sort the results in order to get the cities order alphabetically.

$("#city").kendoComboBox({
    autoBind      :false,
    cascadeFrom   :"state",
    highlightFirst:true,
    delay         :100,
    index         :0,
    placeholder   :"City",
    suggest       :true,
    dataSource    :{
        serverFiltering:true,
        transport      :{
            read:function (operation) {
                db.view("post20/getCities", {
                    success:function (data) {
                        var cities = [];
                        var rows = data.rows;
                        $.each(rows, function (idx, value) {
                            cities.push(value.value);
                        });
                        operation.success(cities.sort());
                    },
                    error  :function (status) {
                        console.log(status);
                    },
                    key    :$("#state").val(),
                    reduce :false
                });
            }
        }
    }
}).data("kendoComboBox");

Optimizations

Both the data organization in the documents and views are not optimal neither in time nor in space but designed for illustrating map/reduce and KendoUI Cascading ComboBoxes.

Advertisements

CouchApp: how to use views map/reduce


There is a JavaScript embedded in CouchDB: the one that is used in Futon. Lets see how to use it from your CouchApp for executing map/reduce views. This post is a follow on of this one.

Executing a view using jquery.couchdb.js

Background

Invoking a view is done by using:

db.view("design/view", {
    success:function (data) {
        // Got response from CouchDB
        // Process data
        // and then return retrieved data
    },
    error  :function (status) {
        // Error function...
    }
});

Where design is the CouchDB design document storing the view (view).

Using a key in a map function

Lets consider a CouchDB database with cities and states from USA stored in documents with the following structure:

{
   "_id": "0254D19B-7360-459E-A9F2-715E36AD1822",
   "_rev": "1-d6c3ca78003b4116ecef930d538b2d94",
   "type": "cities",
   "city": "Pasadena",
   "state": "California"
},
{
   "_id": "05C7AD05-0CC6-4C6E-A21B-E4C8344C242A",
   "_rev": "1-e0a64cd86be76166d3b1592376b8c23c",
   "type": "cities",
   "city": "Lakewood",
   "state": "Colorado"
}

I.e., Documents with field type value cities that allows me to isolate records containing the pairs (city – state) from others stored in the same database; and the the name of the city and the state itself.

Then I have defined the following map function:

function(doc) {
    if (doc.type==="cities") emit(doc.state, doc.city );
}

That emits every document of type cities. These emitted records will have as key the name of the state and as value, the name of the city.

If I execute the view using:

db.view("post20/getCities", {
    success:function (data) {
        console.log(JSON.stringify(data));
    },
    error  :function (status) {
        console.log(status);
    }
});

Where post20 is the design document and getCities is the name of the view.

I get something like:

{
    "total_rows": 285, 
    "offset": 0,
    "rows": [
        {"id":"34716A25-2D33-43F7-BDA2-CBC063BC04A4", "key":"Alabama", "value":"Mobile"},
        {"id":"4F5D34B0-8F24-4C84-98D3-6F104EB19C42", "key":"Alabama", "value":"Montgomery"},
        {"id":"B3BB27B0-9D6E-4776-A685-67378E9198A3", "key":"Alabama", "value":"Birmingham"},
        {"id":"91B00C11-6648-4E15-83F2-957561ACE157", "key":"Alaska", "value":"Anchorage"},
        {"id":"212EA2DA-40C3-4443-AFB7-91366891D3D5", "key":"Arizona", "value":"Tucson"},
        {"id":"7132A69F-B1CE-4AB6-ABF8-22EC6745C35D", "key":"Arizona", "value":"Phoenix"},
        {"id":"9938B89F-E119-47AA-9186-F15BDEF557E1", "key":"Arizona", "value":"Surprise"},
        {"id":"A2E9C9F5-033F-454D-8B54-3A303F2E13C2", "key":"Arizona", "value":"Peoria"},
        {"id":"BFD01743-2297-47B1-B997-4B63B9BF4213", "key":"Arizona", "value":"Mesa"},
        ...
}

But, I want to choose only the cities from California then I have to set California as key (and that’s why I defined the map function that emits the state as key. To send the key to CouchDB I have to say:

db.view("post20/getCities", {
    success:function (data) {
        console.log(JSON.stringify(data));
    },
    error  :function (status) {
        console.log(status);
    },
    key    :"California"
});

and I get as result:

{
    "total_rows":285,
    "offset"    :16,
    "rows"      :[
        {"id":"0254D19B-7360-459E-A9F2-715E36AD1822", "key":"California", "value":"Pasadena"},
        {"id":"07A64E55-2AB1-4AA2-81E8-3D1D54508A26", "key":"California", "value":"Vallejo"},
        {"id":"07CB593A-8F8A-4100-B650-BB949775EBA4", "key":"California", "value":"Ontario"},
        {"id":"0884A3CD-2D54-4B4C-9492-4BCF168F04DA", "key":"California", "value":"Salinas"},
        {"id":"0C835A0A-6109-4986-904B-5C6102D86EB4", "key":"California", "value":"San Buenaventura (Ventura)"},
        {"id":"10687A90-F40B-4154-BF46-A1D976C8E800", "key":"California", "value":"Fresno"},
        ...
    ]
}

Neat!

What about if I want to retrieve the list of cities of Alaska and Wisconsin?

db.view("post20/getCities", {
    success:function (data) {
        console.log(JSON.stringify(data));
    },
    error  :function (status) {
        console.log(status);
    },
    keys   :[ "Alaska", "Wisconsin" ]
});

and I get:

{"total_rows":285, "offset":4, "rows":[
    {"id":"91B00C11-6648-4E15-83F2-957561ACE157", "key":"Alaska", "value":"Anchorage"},
    {"id":"0A2056EC-4C2E-4285-8CCE-EA0F4C71D353", "key":"Wisconsin", "value":"Milwaukee"},
    {"id":"322DC6B3-3B5E-4310-B674-6957A40A4D03", "key":"Wisconsin", "value":"Green Bay"},
    {"id":"E1371AA5-18DB-48A4-AD90-B212030E6C6F", "key":"Wisconsin", "value":"Madison"}
]}

NOTE: Realize that I have had to change key by keys (plural) and then define the list of values to choose from.

Using reduce

Lets define a reduce function for getting the states of our database.

Getting unique values in CouchDB

Using this same database, if I want to retrieve the states selecting the key of the already defined map function or even if I define a map function as:

function(doc) {
    if (doc.type==="cities") emit(null, doc.state );
}

I get the states as many times, as cities for that state I have in the database (I get states duplicated).

{
    "total_rows":285,
    "offset"    :0,
    "rows"      :[
        {"id":"0254D19B-7360-459E-A9F2-715E36AD1822", "key":null, "value":"California"},
        {"id":"02C0F725-A811-492F-BCC0-8921E04899F4", "key":null, "value":"Indiana"},
        {"id":"04A29C30-CCDC-4D73-B9D4-D8ABBBA9AB46", "key":null, "value":"Texas"},
        {"id":"05C7AD05-0CC6-4C6E-A21B-E4C8344C242A", "key":null, "value":"Colorado"},
        {"id":"07A64E55-2AB1-4AA2-81E8-3D1D54508A26", "key":null, "value":"California"},
        {"id":"07CB593A-8F8A-4100-B650-BB949775EBA4", "key":null, "value":"California"},
        {"id":"07F27430-1A5A-4F12-9116-C720AE84E246", "key":null, "value":"Utah"},
        {"id":"0884A3CD-2D54-4B4C-9492-4BCF168F04DA", "key":null, "value":"California"},
        {"id":"090E0E6C-A4EC-4E84-B071-5C771ADA3DB2", "key":null, "value":"Minnesota"},
        {"id":"094E4363-69EA-443A-964D-29F3B2F04546", "key":null, "value":"Texas"},
        {"id":"0A2056EC-4C2E-4285-8CCE-EA0F4C71D353", "key":null, "value":"Wisconsin"},
        {"id":"0C835A0A-6109-4986-904B-5C6102D86EB4", "key":null, "value":"California"},
        {"id":"0CFD3158-AD42-448B-83A8-BB13C5FF87EF", "key":null, "value":"Washington"},
        {"id":"0D3EE837-0BC1-47CE-A8FA-3FA54400549C", "key":null, "value":"Ohio"},
        {"id":"10687A90-F40B-4154-BF46-A1D976C8E800", "key":null, "value":"California"}
        ...
    ]
}

The way of getting only unique values in CouchDB is defining a reduce function as follow:

function(keys, values) {
    return true;
}

and then ask CouchDB for reducing and grouping the result of map function.

Using map/reduce in jquery.couchdb.js

Choosing to reduce the result is not much different, basically I have to define the reduce function for our view:

function(keys, values) {
    return true;
}

And invoking view in jquery.couchdb is:

db.view("post20/getCities", {
    success:function (data) {
        console.log(JSON.stringify(data));
    },
    error  :function (status) {
        console.log(status);
    },
    reduce: true,
    group: true
});

And I get:

{"rows":[
    {"key":"Alabama", "value":true},
    {"key":"Alaska", "value":true},
    {"key":"Arizona", "value":true},
    {"key":"Arkansas", "value":true},
    {"key":"California", "value":true},
    ...
]}

NOTE: As an alternative to this reduce function, I might have decided to provide a little more functionality by choosing to return as value the number of cities found for that state. If so, I can simply use a built-in function called _count and my reduce would be:

_count

Yes! just _count, as I said its a built-in function and then I don’t need to provide the body of the function, the name is enough.

I would invoke the view in the same way (with reduce and group set to true) and I get:

{
    "rows":[
        {"key":"Alabama", "value":4},
        {"key":"Alaska", "value":1},
        {"key":"Arizona", "value":10},
        {"key":"Arkansas", "value":1},
        {"key":"California", "value":69},
        ...
    ]
} 

Summarizing

Adding extra attributes to a view is adding extra pairs of keyvalue to the options (second argument) of view method. Here I’ve covered key, keys, reduce and group but you can also use others as startkey and endkey:

db.view("post20/getCities", {
    success:function (data) {
        console.log(JSON.stringify(data));
    },
    error  :function (status) {
        console.log(status);
    },
    group  :true,
    reduce :true,
    startkey: "Arizona",
    endkey: "Colorado"
});

That retrieves:

{"rows":[
    {"key":"Arizona", "value":true},
    {"key":"Arkansas", "value":true},
    {"key":"California", "value":true},
    {"key":"Colorado", "value":true}
]} 

CouchApp uploading attachments (part 1)


Not sure why but jquery.couchdb.js does not include a method for uploading CouchDB attachments… But I need it, so I started googling trying to find how and found this and this.

Since I’m not just a developer but also software architect and I want clean and nice solutions (avoiding subsequent problems) I tried to understand what was proposed and how it works.

CouchDB uploading attachments: the basics

For uploading an attachment I need:

  1. Database URL (ex: /couchapp)
  2. The document id (ex: doc_with_attachment)
  3. The revision number (ex: 1-967a00dff5e02add41819138abb3284d).
  4. The file(s) being uploaded.

With this information I have to do a POST to a multipart form to an URL that I build up as follow:

var url = "/couchapp/" + $.couch.encodeDocId("doc_with_attachment")

Next, I need an HTML form:

<form id="upload" method="post" action="/couchapp/doc_with_attachment">
    <label>Revision : <input id="revision" type="text" name="_rev"/></label><br/>
    <input id="attachment" type="file" name="_attachments"/><br/>
    <br/>
    <input type="submit"/>
</form>

And _rev is retrieved in HTML load time using:

// Access the DataBase
var db = $.couch.db("couchapp");
db.openDoc("doc_with_attachment", {
    success:function (result) {
        $("#revision").val(result._rev);
    },
    error:function (a, b, c) {
        alert("Error: " + c);
        console.log("Error on openDoc:", a, b, c);
    }
});

If i select a file to upload and click on Submit button what I get is:

Trying to upload an attachment in CouchDB
Trying to upload an attachment in CouchDB

That is more or less what Chris Strom already mention on this first post.

So, lets add enctype and set it to multipart/form-data:

<form id="upload" method="post" 
      action="/couchapp/doc_with_attachment" 
      enctype="multipart/form-data">
    <label>Revision : <input id="revision" type="text" name="_rev"/></label><br/>
    <input id="attachment" type="file" name="_attachments"/><br/>
    <br/>
    <input type="submit"/>
</form>

If I run it now, I get:

CouchApp upload attachment with multipart/form-data
CouchApp upload attachment with multipart/form-data

And futon shows me that the file has been actually uploaded:

Futon showing uploaded file
Futon showing uploaded file

Of course, you can hide the revision input:

CouchDB / CouchApp uploading attachment
CouchDB / CouchApp uploading attachment

and still works!
CouchDB / CouchApp uploading attachment: result
CouchDB / CouchApp uploading attachment: result

Good! pretty simple.

Stay tuned!

Next post convers Uploading attachments from JavaScript in CouchDB. Subscribe to this blog to receive notice.

Zafu: CouchDB and KendoUI (part 1)


I know that Couch is about relaxing and Kendo is about fighting but wikipedia says:

kendo developed under the strong influence of Zen Buddhism.

So, lets practice some meditation about CouchDB, CouchApp, KendoUI in nothing better that a Zafu (meditation seat used in zazen).

CouchDB and KendoUI

Since CouchDB might work as a web server, you might deploy documents into it and get them served to a browser.
For deploying the documents in a CouchDB database, you might use couchapp that in some way allows to synchronize a local folder structure into the database.

Step1: minimum couchapp

Create the following folder structure:

Simple, just one folder (_attachments) containing a file (index.html). The content of index.html is:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8"/>
    <title>Zafu: CouchDB and KendoUI</title>

    <!-- Web Page styling -->
    <style type="text/css">
        html {
            font-family: Verdana, sans-serif;
            font-size: 12px;
            background-color: #a9a9a9;
        }
    </style>
</head>
<body>
<div id="window">
    Hello, CouchApp!<br/>
    Hello, KendoUI!
</div>
</body>
</html>

Now, push it into your CouchDB server by running:

couchapp push http://localhost:5984/app1

or if you have to supply a user and password:

couchapp push http://user:password@localhost:5984/app1

NOTE: You should change user and password by the user name and his/her password.

And if I open the URL that couchapp displayed I get…

Realize by the URL that the HTML page is actually being served by CouchDB.

But wait! This is just an HTML page in CouchDB. Where is KendoUI?

Step2: deploying KendoUI inside CouchDB

Go to you _attachments folder and copy KendoUI minimum JavaScript and CSS files:

If we change index.html for using KendoUI window as:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8"/>
    <title>Zafu: CouchDB and KendoUI</title>
    <!-- Kendo UI Web styles-->
    <link href="styles/kendo.common.min.css" rel="stylesheet" type="text/css"/>
    <link href="styles/kendo.black.min.css" rel="stylesheet" type="text/css"/>

    <!-- Kendo UI Web scripts-->
    <script src="js/jquery.min.js" type="text/javascript"></script>
    <script src="js/kendo.web.min.js" type="text/javascript"></script>

    <!-- Kendo Globalization -->
    <script src="js/cultures/kendo.culture.en-US.min.js"></script>

    <!-- Web Page styling -->
    <style type="text/css">
        html {
            font-family: Verdana, sans-serif;
            font-size: 12px;
            background-color: #a9a9a9;
        }
    </style>

    <!-- Initialize Form Elements -->
    <script type="text/javascript">
        $(document).ready(function () {
            kendo.culture("en-US");
            var window = $("#window").kendoWindow({
                width:"200",
                height:"60px",
                resizable:true,
                title:"Welcome to Zafu!"
            }).data("kendoWindow");
            window.center();
        });
    </script>

</head>
<body>
<div id="window">
    Hello, CouchApp!<br/>
    Hello, KendoUI!
</div>
</body>
</html>

and push it in couchdb using couchapp, I get:

Next posts (here and here) on how to actually get data from CouchDB and the final one about using KendoUI for displaying it.