diff --git a/.couchappignore b/.couchappignore new file mode 100644 index 0000000..d7b3b38 --- /dev/null +++ b/.couchappignore @@ -0,0 +1,3 @@ +[ + "tropo" +] diff --git a/.couchapprc b/.couchapprc new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/.couchapprc @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..0276f8d --- /dev/null +++ b/README.md @@ -0,0 +1,48 @@ +## CouchConf NYC Demo + +This is a simple [Tropo](http://www.tropo.com/) + [CouchBase](http://www.couchbase.com/) app I demoed at [CouchConf NYC](http://www.couchbase.com/couchconf-nyc). It's a realtime SMS voting app that was used to select the best rock song over 6 minutes in length, but it can be used as the basis for any kind of voting app you want to build. + +The magic is in Tropo's ability to make HTTP requests from inside a running telephony application to a CouchDB instance running on your local machine, your server, in the cloud - anywhere! + +Tropo + CouchBase == Cloud Telephony Awesomeness! + +## What is a CouchApp? + +CouchApps are web applications which can be served directly from [CouchDB](http://couchdb.apache.org). This gives them the nice property of replicating just like any other data stored in CouchDB. They are also simple to write as they can use the built-in jQuery libraries and plugins that ship with CouchDB. + +[More info about CouchApps here.](http://couchapp.org) + +## Deploying this app + +Assuming you just cloned this app from git, and you have changed into the app directory in your terminal, you want to push it to your CouchDB with the CouchApp command line tool, like this: + + couchapp push . http://name:password@hostname:5984/mydatabase + +If you don't have a password on your CouchDB (admin party) you can do it like this (but it's a bad, idea, set a password): + + couchapp push . http://hostname:5984/mydatabase + +If you get sick of typing the URL, you should setup a `.couchapprc` file in the root of your directory. Remember not to check this into version control as it will have passwords in it. + +The `.couchapprc` file should have contents like this: + + { + "env" : { + "public" : { + "db" : "http://name:pass@mycouch.couchone.com/mydatabase" + }, + "default" : { + "db" : "http://name:pass@localhost:5984/mydatabase" + } + } + } + +Now that you have the `.couchapprc` file set up, you can push your app to the CouchDB as simply as: + + couchapp push + +This pushes to the `default` as specified. To push to the `public` you'd run: + + couchapp push public + +Of course you can continue to add more deployment targets as you see fit, and give them whatever names you like. diff --git a/_attachments/index.html b/_attachments/index.html new file mode 100644 index 0000000..4e581b7 --- /dev/null +++ b/_attachments/index.html @@ -0,0 +1,65 @@ + + +Tropo-Powered Realtime SMS Voting + + +

+
+
+
+ + + + + + \ No newline at end of file diff --git a/_attachments/style/main.css b/_attachments/style/main.css new file mode 100644 index 0000000..bd0624c --- /dev/null +++ b/_attachments/style/main.css @@ -0,0 +1,51 @@ +body { + text-align: center; + background-image: url('http://blog.tropo.com/files/2011/10/Tropo-Vert.png'), url('http://blog.tropo.com/files/2011/10/couch-300x300.png'); + background-position: 2% 2%, 98% 2%; + background-size: 10%, 10%; + background-repeat: no-repeat, no-repeat;; +} + +#row { + width: 65%; + margin-left: 5px; +} + +#choice { + text-align: center; + padding: 5px; + border-style: dashed; + border-width: 1px; + margin: 2px; + margin-top: 0px; + background-color: silver; +} + +.score { + font-weight: bold; + color: maroon; + font-size: 115%; + margin-bottom: 2px; + margin-top: 2px; +} + +.name { + font-weight: bold; +} + +.selection { + float: left; + font-size: 125%; + font-weight: bold; + color: blue; + margin-top: 3px; + text-decoration: overline; +} + +h4 { + margin-top: 5px; +} + +.footer { +font-size: 80%; +} diff --git a/_id b/_id new file mode 100644 index 0000000..c795b4c --- /dev/null +++ b/_id @@ -0,0 +1 @@ +_design/couchconfdemo \ No newline at end of file diff --git a/couchapp.json b/couchapp.json new file mode 100644 index 0000000..c54af2c --- /dev/null +++ b/couchapp.json @@ -0,0 +1,4 @@ +{ + "name": "Tropo-Powered Realtime SMS Voting", + "description": "CouchApp" +} diff --git a/evently/items/_changes/data.js b/evently/items/_changes/data.js new file mode 100644 index 0000000..d6ac7b9 --- /dev/null +++ b/evently/items/_changes/data.js @@ -0,0 +1,11 @@ +function(data) { + // $.log(data) + var p; + return { + items : data.rows.map(function(r) { + p = (r.value && r.value.profile) || {}; + p.message = r.value && r.value.message; + return p; + }) + } +}; \ No newline at end of file diff --git a/evently/items/_changes/mustache.html b/evently/items/_changes/mustache.html new file mode 100644 index 0000000..9e45164 --- /dev/null +++ b/evently/items/_changes/mustache.html @@ -0,0 +1,18 @@ +

Customize this format here: ddoc.evently.items._changes.mustache

+

Recent Messages

+ +

Protip: If you setup continuous replication between this database and a remote one, this list will reflect remote changes in near real-time.

+

This would be a good place to add pagination.

diff --git a/evently/items/_changes/query.json b/evently/items/_changes/query.json new file mode 100644 index 0000000..f06e992 --- /dev/null +++ b/evently/items/_changes/query.json @@ -0,0 +1,5 @@ +{ + "view" : "recent-items", + "descending" : "true", + "limit" : 50 +} \ No newline at end of file diff --git a/evently/profile/profileReady/mustache.html b/evently/profile/profileReady/mustache.html new file mode 100644 index 0000000..1ad2bc0 --- /dev/null +++ b/evently/profile/profileReady/mustache.html @@ -0,0 +1,14 @@ +

Most applications will customize this template (ddoc.evently.profile.profileReady.mustache) for user input.

+ +
+ {{#gravatar_url}}{{/gravatar_url}} +
+ {{name}} +
+
+ +
+ +
+ +
\ No newline at end of file diff --git a/evently/profile/profileReady/selectors/form/submit.js b/evently/profile/profileReady/selectors/form/submit.js new file mode 100644 index 0000000..35f4b6f --- /dev/null +++ b/evently/profile/profileReady/selectors/form/submit.js @@ -0,0 +1,12 @@ +function() { + var form = $(this); + var fdoc = form.serializeObject(); + fdoc.created_at = new Date(); + fdoc.profile = $$("#profile").profile; + $$(this).app.db.saveDoc(fdoc, { + success : function() { + form[0].reset(); + } + }); + return false; +}; diff --git a/language b/language new file mode 100644 index 0000000..f504a95 --- /dev/null +++ b/language @@ -0,0 +1 @@ +javascript \ No newline at end of file diff --git a/rewrites.json b/rewrites.json new file mode 100644 index 0000000..a9a5b0b --- /dev/null +++ b/rewrites.json @@ -0,0 +1,34 @@ +[ +{ + "from": "", + "to": "index.html" +}, +{ + "from": "vendor/*", + "to": "vendor/*" +}, +{ + "from": "app/*", + "to": "app/*" +}, +{ + "from": "style/*", + "to": "style/*" +}, +{ + "from": "_view/*", + "to": "_view/*" +}, +{ + "from": "_changes/*", + "to": "_changes/*" +}, +{ + "from": "couchconfdemo/*", + "to": "../../*" +}, +{ + "from": "couchconfdemo/", + "to": "../../" +} +] diff --git a/tropo/sms-vote.php b/tropo/sms-vote.php new file mode 100644 index 0000000..0873ad0 --- /dev/null +++ b/tropo/sms-vote.php @@ -0,0 +1,60 @@ + $vote)); + $url = COUCH_HOST.":".COUCH_DB_PORT."/".COUCH_DB_NAME."/$id"; + + $putData = tmpfile(); + fwrite($putData, $doc); + fseek($putData, 0); + + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_PUT, true); + curl_setopt($ch, CURLOPT_INFILE, $putData); + curl_setopt($ch, CURLOPT_INFILESIZE, strlen($doc)); + curl_setopt($ch, CURLOPT_USERPWD, COUCH_USER . ":" . COUCH_PASS); + curl_exec($ch); + $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + // Conflict update means someone tried to vote twice. :-( + if ($code != '201') { + _log("*** HTTP response code: $code ***"); + return false; + } + return true; + +} + +// If the caller uses the voice channel, there is no initialText. +if(!$currentCall->initialText) { + $vote = ask("Please enter the number of the selection you wish to vote for.", array("choices" => "1,2,3,4,5,6", "attempts" => 3)); +} + +// If the text channel is used, initialText is used to complete ask(); +else { + $vote = ask("", array("choices" => "[ANY]", "attempts" => 1)); +} + +// Save the vote. +if(saveVote($currentCall->callerID, $vote->value)) { + if($currentCall->channel == "VOICE") { + say("Thank you, your vote has been recorded."); + } + else { + _log("*** Vote recorded for " . $currentCall->callerID . " ***"); + } +} else { + say("Sorry, there was a problem saving your vote."); +} + +?> diff --git a/vendor/couchapp/_attachments/jquery.couch.app.js b/vendor/couchapp/_attachments/jquery.couch.app.js new file mode 100644 index 0000000..fc500a2 --- /dev/null +++ b/vendor/couchapp/_attachments/jquery.couch.app.js @@ -0,0 +1,235 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy +// of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +// Usage: The passed in function is called when the page is ready. +// CouchApp passes in the app object, which takes care of linking to +// the proper database, and provides access to the CouchApp helpers. +// $.couch.app(function(app) { +// app.db.view(...) +// ... +// }); + +(function($) { + + function Design(db, name, code) { + this.doc_id = "_design/"+name; + if (code) { + this.code_path = this.doc_id + "/" + code; + } else { + this.code_path = this.doc_id; + } + this.view = function(view, opts) { + db.view(name+'/'+view, opts); + }; + this.list = function(list, view, opts) { + db.list(name+'/'+list, view, opts); + }; + } + + function docForm() { alert("docForm has been moved to vendor/couchapp/lib/docForm.js, use app.require to load") }; + + function resolveModule(path, names, parents, current) { + parents = parents || []; + if (names.length === 0) { + if (typeof current != "string") { + throw ["error","invalid_require_path", + 'Must require a JavaScript string, not: '+(typeof current)]; + } + return [current, parents]; + } + var n = names.shift(); + if (n == '..') { + parents.pop(); + var pp = parents.pop(); + if (!pp) { + throw ["error", "invalid_require_path", path]; + } + return resolveModule(path, names, parents, pp); + } else if (n == '.') { + var p = parents.pop(); + if (!p) { + throw ["error", "invalid_require_path", path]; + } + return resolveModule(path, names, parents, p); + } else { + parents = []; + } + if (!current[n]) { + throw ["error", "invalid_require_path", path]; + } + parents.push(current); + return resolveModule(path, names, parents, current[n]); + } + + function makeRequire(ddoc) { + var moduleCache = []; + function getCachedModule(name, parents) { + var key, i, len = moduleCache.length; + for (i=0;i>> 0; + if (typeof fun != "function") + throw new TypeError(); + + var thisp = arguments[1]; + for (var i = 0; i < len; i++) + { + if (i in this) + fun.call(thisp, this[i], i, this); + } + }; +} + +if (!Array.prototype.indexOf) +{ + Array.prototype.indexOf = function(elt) + { + var len = this.length >>> 0; + + var from = Number(arguments[1]) || 0; + from = (from < 0) + ? Math.ceil(from) + : Math.floor(from); + if (from < 0) + from += len; + + for (; from < len; from++) + { + if (from in this && + this[from] === elt) + return from; + } + return -1; + }; +} diff --git a/vendor/couchapp/_attachments/jquery.couch.app.util.js b/vendor/couchapp/_attachments/jquery.couch.app.util.js new file mode 100644 index 0000000..b71aa54 --- /dev/null +++ b/vendor/couchapp/_attachments/jquery.couch.app.util.js @@ -0,0 +1,90 @@ +$.log = function(m) { + if (window && window.console && window.console.log) { + window.console.log(arguments.length == 1 ? m : arguments); + } +}; + +// http://stackoverflow.com/questions/1184624/serialize-form-to-json-with-jquery/1186309#1186309 +$.fn.serializeObject = function() { + var o = {}; + var a = this.serializeArray(); + $.each(a, function() { + if (o[this.name]) { + if (!o[this.name].push) { + o[this.name] = [o[this.name]]; + } + o[this.name].push(this.value || ''); + } else { + o[this.name] = this.value || ''; + } + }); + return o; +}; + +// todo remove this crap +function escapeHTML(st) { + return( + st && st.replace(/&/g,'&'). + replace(/>/g,'>'). + replace(/'+a+''; + }).replace(/\@([\w\-]+)/g,function(user,name) { + return ''+user+''; + }).replace(/\#([\w\-\.]+)/g,function(word,tag) { + return ''+word+''; + }); +}; + +$.fn.prettyDate = function() { + $(this).each(function() { + var string, title = $(this).attr("title"); + if (title) { + string = $.prettyDate(title); + } else { + string = $.prettyDate($(this).text()); + } + $(this).text(string); + }); +}; + +$.prettyDate = function(time){ + + var date = new Date(time.replace(/-/g,"/").replace("T", " ").replace("Z", " +0000").replace(/(\d*\:\d*:\d*)\.\d*/g,"$1")), + diff = (((new Date()).getTime() - date.getTime()) / 1000), + day_diff = Math.floor(diff / 86400); + + if (isNaN(day_diff)) return time; + + return day_diff < 1 && ( + diff < 60 && "just now" || + diff < 120 && "1 minute ago" || + diff < 3600 && Math.floor( diff / 60 ) + " minutes ago" || + diff < 7200 && "1 hour ago" || + diff < 86400 && Math.floor( diff / 3600 ) + " hours ago") || + day_diff == 1 && "yesterday" || + day_diff < 21 && day_diff + " days ago" || + day_diff < 45 && Math.ceil( day_diff / 7 ) + " weeks ago" || + time; + // day_diff < 730 && Math.ceil( day_diff / 31 ) + " months ago" || + // Math.ceil( day_diff / 365 ) + " years ago"; +}; + +$.argsToArray = function(args) { + if (!args.callee) return args; + var array = []; + for (var i=0; i < args.length; i++) { + array.push(args[i]); + }; + return array; +} diff --git a/vendor/couchapp/_attachments/jquery.evently.js b/vendor/couchapp/_attachments/jquery.evently.js new file mode 100644 index 0000000..5128a20 --- /dev/null +++ b/vendor/couchapp/_attachments/jquery.evently.js @@ -0,0 +1,399 @@ +// $$ inspired by @wycats: http://yehudakatz.com/2009/04/20/evented-programming-with-jquery/ +function $$(node) { + var data = $(node).data("$$"); + if (data) { + return data; + } else { + data = {}; + $(node).data("$$", data); + return data; + } +}; + +(function($) { + // utility functions used in the implementation + + function forIn(obj, fun) { + var name; + for (name in obj) { + if (obj.hasOwnProperty(name)) { + fun(name, obj[name]); + } + } + }; + $.forIn = forIn; + function funViaString(fun, hint) { + if (fun && fun.match && fun.match(/^function/)) { + eval("var f = "+fun); + if (typeof f == "function") { + return function() { + try { + return f.apply(this, arguments); + } catch(e) { + // IF YOU SEE AN ERROR HERE IT HAPPENED WHEN WE TRIED TO RUN YOUR FUNCTION + $.log({"message": "Error in evently function.", "error": e, + "src" : fun, "hint":hint}); + throw(e); + } + }; + } + } + return fun; + }; + + function runIfFun(me, fun, args) { + // if the field is a function, call it, bound to the widget + var f = funViaString(fun, me); + if (typeof f == "function") { + return f.apply(me, args); + } else { + return fun; + } + } + + $.evently = { + connect : function(source, target, events) { + events.forEach(function(ev) { + $(source).bind(ev, function() { + var args = $.makeArray(arguments); + // remove the original event to keep from stacking args extra deep + // it would be nice if jquery had a way to pass the original + // event to the trigger method. + args.shift(); + $(target).trigger(ev, args); + return false; + }); + }); + }, + paths : [], + changesDBs : {}, + changesOpts : {} + }; + + function extractFrom(name, evs) { + return evs[name]; + }; + + function extractEvents(name, ddoc) { + // extract events from ddoc.evently and ddoc.vendor.*.evently + var events = [true, {}] + , vendor = ddoc.vendor || {} + , evently = ddoc.evently || {} + ; + $.forIn(vendor, function(k, v) { + if (v.evently && v.evently[name]) { + events.push(v.evently[name]); + } + }); + if (evently[name]) {events.push(evently[name]);} + return $.extend.apply(null, events); + } + + function extractPartials(ddoc) { + var partials = [true, {}] + , vendor = ddoc.vendor || {} + , evently = ddoc.evently || {} + ; + $.forIn(vendor, function(k, v) { + if (v.evently && v.evently._partials) { + partials.push(v.evently._partials); + } + }); + if (evently._partials) {partials.push(evently._partials);} + return $.extend.apply(null, partials); + }; + + function applyCommon(events) { + if (events._common) { + $.forIn(events, function(k, v) { + events[k] = $.extend(true, {}, events._common, v); + }); + delete events._common; + return events; + } else { + return events; + } + } + + $.fn.evently = function(events, app, args) { + var elem = $(this); + // store the app on the element for later use + if (app) { + $$(elem).app = app; + } + + if (typeof events == "string") { + events = extractEvents(events, app.ddoc); + } + events = applyCommon(events); + $$(elem).evently = events; + if (app && app.ddoc) { + $$(elem).partials = extractPartials(app.ddoc); + } + // setup the handlers onto elem + forIn(events, function(name, h) { + eventlyHandler(elem, name, h, args); + }); + + if (events._init) { + elem.trigger("_init", args); + } + + if (app && events._changes) { + $("body").bind("evently-changes-"+app.db.name, function() { + elem.trigger("_changes"); + }); + followChanges(app); + elem.trigger("_changes"); + } + }; + + // eventlyHandler applies the user's handler (h) to the + // elem, bound to trigger based on name. + function eventlyHandler(elem, name, h, args) { + if ($.evently.log) { + elem.bind(name, function() { + $.log(elem, name); + }); + } + if (h.path) { + elem.pathbinder(name, h.path); + } + var f = funViaString(h, name); + if (typeof f == "function") { + elem.bind(name, {args:args}, f); + } else if (typeof f == "string") { + elem.bind(name, {args:args}, function() { + $(this).trigger(f, arguments); + return false; + }); + } else if ($.isArray(h)) { + // handle arrays recursively + for (var i=0; i < h.length; i++) { + eventlyHandler(elem, name, h[i], args); + } + } else { + // an object is using the evently / mustache template system + if (h.fun) { + throw("e.fun has been removed, please rename to e.before") + } + // templates, selectors, etc are intepreted + // when our named event is triggered. + elem.bind(name, {args:args}, function() { + renderElement($(this), h, arguments); + return false; + }); + } + }; + + $.fn.replace = function(elem) { + // $.log("Replace", this) + $(this).empty().append(elem); + }; + + // todo: ability to call this + // to render and "prepend/append/etc" a new element to the host element (me) + // as well as call this in a way that replaces the host elements content + // this would be easy if there is a simple way to get at the element we just appended + // (as html) so that we can attache the selectors + function renderElement(me, h, args, qrun, arun) { + // if there's a query object we run the query, + // and then call the data function with the response. + if (h.before && (!qrun || !arun)) { + funViaString(h.before, me).apply(me, args); + } + if (h.async && !arun) { + runAsync(me, h, args) + } else if (h.query && !qrun) { + // $.log("query before renderElement", arguments) + runQuery(me, h, args) + } else { + // $.log("renderElement") + // $.log(me, h, args, qrun) + // otherwise we just render the template with the current args + var selectors = runIfFun(me, h.selectors, args); + var act = (h.render || "replace").replace(/\s/g,""); + var app = $$(me).app; + if (h.mustache) { + // $.log("rendering", h.mustache) + var newElem = mustachioed(me, h, args); + me[act](newElem); + } + if (selectors) { + if (act == "replace") { + var s = me; + } else { + var s = newElem; + } + forIn(selectors, function(selector, handlers) { + // $.log("selector", selector); + // $.log("selected", $(selector, s)); + $(selector, s).evently(handlers, app, args); + // $.log("applied", selector); + }); + } + if (h.after) { + runIfFun(me, h.after, args); + } + } + }; + + // todo this should return the new element + function mustachioed(me, h, args) { + var partials = $$(me).partials; + return $($.mustache( + runIfFun(me, h.mustache, args), + runIfFun(me, h.data, args), + runIfFun(me, $.extend(true, partials, h.partials), args))); + }; + + function runAsync(me, h, args) { + // the callback is the first argument + funViaString(h.async, me).apply(me, [function() { + renderElement(me, h, + $.argsToArray(arguments).concat($.argsToArray(args)), false, true); + }].concat($.argsToArray(args))); + }; + + + function runQuery(me, h, args) { + // $.log("runQuery: args", args) + var app = $$(me).app; + var qu = runIfFun(me, h.query, args); + var qType = qu.type; + var viewName = qu.view; + var userSuccess = qu.success; + // $.log("qType", qType) + + var q = {}; + forIn(qu, function(k, v) { + if (["type", "view"].indexOf(k) == -1) { + q[k] = v; + } + }); + + if (qType == "newRows") { + q.success = function(resp) { + // $.log("runQuery newRows success", resp.rows.length, me, resp) + resp.rows.reverse().forEach(function(row) { + renderElement(me, h, [row].concat($.argsToArray(args)), true) + }); + if (userSuccess) userSuccess(resp); + }; + newRows(me, app, viewName, q); + } else { + q.success = function(resp) { + // $.log("runQuery success", resp) + renderElement(me, h, [resp].concat($.argsToArray(args)), true); + userSuccess && userSuccess(resp); + }; + // $.log(app) + app.view(viewName, q); + } + } + + // this is for the items handler + // var lastViewId, highKey, inFlight; + // this needs to key per elem + function newRows(elem, app, view, opts) { + // $.log("newRows", arguments); + // on success we'll set the top key + var thisViewId, successCallback = opts.success, full = false; + function successFun(resp) { + // $.log("newRows success", resp) + $$(elem).inFlight = false; + var JSONhighKey = JSON.stringify($$(elem).highKey); + resp.rows = resp.rows.filter(function(r) { + return JSON.stringify(r.key) != JSONhighKey; + }); + if (resp.rows.length > 0) { + if (opts.descending) { + $$(elem).highKey = resp.rows[0].key; + } else { + $$(elem).highKey = resp.rows[resp.rows.length -1].key; + } + }; + if (successCallback) {successCallback(resp, full)}; + }; + opts.success = successFun; + + if (opts.descending) { + thisViewId = view + (opts.startkey ? JSON.stringify(opts.startkey) : ""); + } else { + thisViewId = view + (opts.endkey ? JSON.stringify(opts.endkey) : ""); + } + // $.log(["thisViewId",thisViewId]) + // for query we'll set keys + if (thisViewId == $$(elem).lastViewId) { + // we only want the rows newer than changesKey + var hk = $$(elem).highKey; + if (hk !== undefined) { + if (opts.descending) { + opts.endkey = hk; + // opts.inclusive_end = false; + } else { + opts.startkey = hk; + } + } + // $.log("add view rows", opts) + if (!$$(elem).inFlight) { + $$(elem).inFlight = true; + app.view(view, opts); + } + } else { + // full refresh + // $.log("new view stuff") + full = true; + $$(elem).lastViewId = thisViewId; + $$(elem).highKey = undefined; + $$(elem).inFlight = true; + app.view(view, opts); + } + }; + + // only start one changes listener per db + function followChanges(app) { + var dbName = app.db.name, changeEvent = function(resp) { + $("body").trigger("evently-changes-"+dbName, [resp]); + }; + if (!$.evently.changesDBs[dbName]) { + if (app.db.changes) { + // new api in jquery.couch.js 1.0 + app.db.changes(null, $.evently.changesOpts).onChange(changeEvent); + } else { + // in case you are still on CouchDB 0.11 ;) deprecated. + connectToChanges(app, changeEvent); + } + $.evently.changesDBs[dbName] = true; + } + } + $.evently.followChanges = followChanges; + // deprecated. use db.changes() from jquery.couch.js + // this does not have an api for closing changes request. + function connectToChanges(app, fun, update_seq) { + function changesReq(seq) { + var url = app.db.uri+"_changes?heartbeat=10000&feed=longpoll&since="+seq; + if ($.evently.changesOpts.include_docs) { + url = url + "&include_docs=true"; + } + $.ajax({ + url: url, + contentType: "application/json", + dataType: "json", + complete: function(req) { + var resp = $.httpData(req, "json"); + fun(resp); + connectToChanges(app, fun, resp.last_seq); + } + }); + }; + if (update_seq) { + changesReq(update_seq); + } else { + app.db.info({success: function(db_info) { + changesReq(db_info.update_seq); + }}); + } + }; + +})(jQuery); diff --git a/vendor/couchapp/_attachments/jquery.mustache.js b/vendor/couchapp/_attachments/jquery.mustache.js new file mode 100644 index 0000000..5aa67de --- /dev/null +++ b/vendor/couchapp/_attachments/jquery.mustache.js @@ -0,0 +1,346 @@ +/* +Shameless port of a shameless port +@defunkt => @janl => @aq + +See http://github.com/defunkt/mustache for more info. +*/ + +;(function($) { + +/* + mustache.js — Logic-less templates in JavaScript + + See http://mustache.github.com/ for more info. +*/ + +var Mustache = function() { + var Renderer = function() {}; + + Renderer.prototype = { + otag: "{{", + ctag: "}}", + pragmas: {}, + buffer: [], + pragmas_implemented: { + "IMPLICIT-ITERATOR": true + }, + context: {}, + + render: function(template, context, partials, in_recursion) { + // reset buffer & set context + if(!in_recursion) { + this.context = context; + this.buffer = []; // TODO: make this non-lazy + } + + // fail fast + if(!this.includes("", template)) { + if(in_recursion) { + return template; + } else { + this.send(template); + return; + } + } + + template = this.render_pragmas(template); + var html = this.render_section(template, context, partials); + if(in_recursion) { + return this.render_tags(html, context, partials, in_recursion); + } + + this.render_tags(html, context, partials, in_recursion); + }, + + /* + Sends parsed lines + */ + send: function(line) { + if(line != "") { + this.buffer.push(line); + } + }, + + /* + Looks for %PRAGMAS + */ + render_pragmas: function(template) { + // no pragmas + if(!this.includes("%", template)) { + return template; + } + + var that = this; + var regex = new RegExp(this.otag + "%([\\w-]+) ?([\\w]+=[\\w]+)?" + + this.ctag); + return template.replace(regex, function(match, pragma, options) { + if(!that.pragmas_implemented[pragma]) { + throw({message: + "This implementation of mustache doesn't understand the '" + + pragma + "' pragma"}); + } + that.pragmas[pragma] = {}; + if(options) { + var opts = options.split("="); + that.pragmas[pragma][opts[0]] = opts[1]; + } + return ""; + // ignore unknown pragmas silently + }); + }, + + /* + Tries to find a partial in the curent scope and render it + */ + render_partial: function(name, context, partials) { + name = this.trim(name); + if(!partials || partials[name] === undefined) { + throw({message: "unknown_partial '" + name + "'"}); + } + if(typeof(context[name]) != "object") { + return this.render(partials[name], context, partials, true); + } + return this.render(partials[name], context[name], partials, true); + }, + + /* + Renders inverted (^) and normal (#) sections + */ + render_section: function(template, context, partials) { + if(!this.includes("#", template) && !this.includes("^", template)) { + return template; + } + + var that = this; + // CSW - Added "+?" so it finds the tighest bound, not the widest + var regex = new RegExp(this.otag + "(\\^|\\#)\\s*(.+)\\s*" + this.ctag + + "\n*([\\s\\S]+?)" + this.otag + "\\/\\s*\\2\\s*" + this.ctag + + "\\s*", "mg"); + + // for each {{#foo}}{{/foo}} section do... + return template.replace(regex, function(match, type, name, content) { + var value = that.find(name, context); + if(type == "^") { // inverted section + if(!value || that.is_array(value) && value.length === 0) { + // false or empty list, render it + return that.render(content, context, partials, true); + } else { + return ""; + } + } else if(type == "#") { // normal section + if(that.is_array(value)) { // Enumerable, Let's loop! + return that.map(value, function(row) { + return that.render(content, that.create_context(row), + partials, true); + }).join(""); + } else if(that.is_object(value)) { // Object, Use it as subcontext! + return that.render(content, that.create_context(value), + partials, true); + } else if(typeof value === "function") { + // higher order section + return value.call(context, content, function(text) { + return that.render(text, context, partials, true); + }); + } else if(value) { // boolean section + return that.render(content, context, partials, true); + } else { + return ""; + } + } + }); + }, + + /* + Replace {{foo}} and friends with values from our view + */ + render_tags: function(template, context, partials, in_recursion) { + // tit for tat + var that = this; + + var new_regex = function() { + return new RegExp(that.otag + "(=|!|>|\\{|%)?([^\\/#\\^]+?)\\1?" + + that.ctag + "+", "g"); + }; + + var regex = new_regex(); + var tag_replace_callback = function(match, operator, name) { + switch(operator) { + case "!": // ignore comments + return ""; + case "=": // set new delimiters, rebuild the replace regexp + that.set_delimiters(name); + regex = new_regex(); + return ""; + case ">": // render partial + return that.render_partial(name, context, partials); + case "{": // the triple mustache is unescaped + return that.find(name, context); + default: // escape the value + return that.escape(that.find(name, context)); + } + }; + var lines = template.split("\n"); + for(var i = 0; i < lines.length; i++) { + lines[i] = lines[i].replace(regex, tag_replace_callback, this); + if(!in_recursion) { + this.send(lines[i]); + } + } + + if(in_recursion) { + return lines.join("\n"); + } + }, + + set_delimiters: function(delimiters) { + var dels = delimiters.split(" "); + this.otag = this.escape_regex(dels[0]); + this.ctag = this.escape_regex(dels[1]); + }, + + escape_regex: function(text) { + // thank you Simon Willison + if(!arguments.callee.sRE) { + var specials = [ + '/', '.', '*', '+', '?', '|', + '(', ')', '[', ']', '{', '}', '\\' + ]; + arguments.callee.sRE = new RegExp( + '(\\' + specials.join('|\\') + ')', 'g' + ); + } + return text.replace(arguments.callee.sRE, '\\$1'); + }, + + /* + find `name` in current `context`. That is find me a value + from the view object + */ + find: function(name, context) { + name = this.trim(name); + + // Checks whether a value is thruthy or false or 0 + function is_kinda_truthy(bool) { + return bool === false || bool === 0 || bool; + } + + var value; + if(is_kinda_truthy(context[name])) { + value = context[name]; + } else if(is_kinda_truthy(this.context[name])) { + value = this.context[name]; + } + + if(typeof value === "function") { + return value.apply(context); + } + if(value !== undefined) { + return value; + } + // silently ignore unkown variables + return ""; + }, + + // Utility methods + + /* includes tag */ + includes: function(needle, haystack) { + return haystack.indexOf(this.otag + needle) != -1; + }, + + /* + Does away with nasty characters + */ + escape: function(s) { + s = String(s === null ? "" : s); + return s.replace(/&(?!\w+;)|["<>\\]/g, function(s) { + switch(s) { + case "&": return "&"; + case "\\": return "\\\\"; + case '"': return '\"'; + case "<": return "<"; + case ">": return ">"; + default: return s; + } + }); + }, + + // by @langalex, support for arrays of strings + create_context: function(_context) { + if(this.is_object(_context)) { + return _context; + } else { + var iterator = "."; + if(this.pragmas["IMPLICIT-ITERATOR"]) { + iterator = this.pragmas["IMPLICIT-ITERATOR"].iterator; + } + var ctx = {}; + ctx[iterator] = _context; + return ctx; + } + }, + + is_object: function(a) { + return a && typeof a == "object"; + }, + + is_array: function(a) { + return Object.prototype.toString.call(a) === '[object Array]'; + }, + + /* + Gets rid of leading and trailing whitespace + */ + trim: function(s) { + return s.replace(/^\s*|\s*$/g, ""); + }, + + /* + Why, why, why? Because IE. Cry, cry cry. + */ + map: function(array, fn) { + if (typeof array.map == "function") { + return array.map(fn); + } else { + var r = []; + var l = array.length; + for(var i = 0; i < l; i++) { + r.push(fn(array[i])); + } + return r; + } + } + }; + + return({ + name: "mustache.js", + version: "0.3.1-dev", + + /* + Turns a template and view into HTML + */ + to_html: function(template, view, partials, send_fun) { + var renderer = new Renderer(); + if(send_fun) { + renderer.send = send_fun; + } + renderer.render(template, view, partials); + if(!send_fun) { + return renderer.buffer.join("\n"); + } + }, + escape : function(text) { + return new Renderer().escape(text); + } + }); +}(); + + $.mustache = function(template, view, partials) { + return Mustache.to_html(template, view, partials); + }; + + $.mustache.escape = function(text) { + return Mustache.escape(text); + }; + +})(jQuery); diff --git a/vendor/couchapp/_attachments/jquery.pathbinder.js b/vendor/couchapp/_attachments/jquery.pathbinder.js new file mode 100644 index 0000000..89f0dd4 --- /dev/null +++ b/vendor/couchapp/_attachments/jquery.pathbinder.js @@ -0,0 +1,174 @@ +(function($) { + // functions for handling the path + // thanks sammy.js + var PATH_REPLACER = "([^\/]+)", + PATH_NAME_MATCHER = /:([\w\d]+)/g, + QUERY_STRING_MATCHER = /\?([^#]*)$/, + SPLAT_MATCHER = /(\*)/, + SPLAT_REPLACER = "(.+)", + _currentPath, + _lastPath, + _pathInterval; + + function hashChanged() { + _currentPath = getPath(); + // if path is actually changed from what we thought it was, then react + if (_lastPath != _currentPath) { + _lastPath = _currentPath; + return triggerOnPath(_currentPath); + } + } + + $.pathbinder = { + changeFuns : [], + paths : [], + begin : function(defaultPath) { + // this should trigger the defaultPath if there's not a path in the URL + // otherwise it should trigger the URL's path + $(function() { + var loadPath = getPath(); + if (loadPath) { + triggerOnPath(loadPath); + } else { + goPath(defaultPath); + triggerOnPath(defaultPath); + } + }) + }, + go : function(path) { + goPath(path); + triggerOnPath(path); + }, + currentPath : function() { + return getPath(); + }, + onChange : function (fun) { + $.pathbinder.changeFuns.push(fun); + } + }; + + function pollPath(every) { + function hashCheck() { + _currentPath = getPath(); + // path changed if _currentPath != _lastPath + if (_lastPath != _currentPath) { + setTimeout(function() { + $(window).trigger('hashchange'); + }, 1); + } + }; + hashCheck(); + _pathInterval = setInterval(hashCheck, every); + $(window).bind('unload', function() { + clearInterval(_pathInterval); + }); + } + + function triggerOnPath(path) { + path = path.replace(/^#/,''); + $.pathbinder.changeFuns.forEach(function(fun) {fun(path)}); + var pathSpec, path_params, params = {}, param_name, param; + for (var i=0; i < $.pathbinder.paths.length; i++) { + pathSpec = $.pathbinder.paths[i]; + // $.log("pathSpec", pathSpec); + if ((path_params = pathSpec.matcher.exec(path)) !== null) { + // $.log("path_params", path_params); + path_params.shift(); + for (var j=0; j < path_params.length; j++) { + param_name = pathSpec.param_names[j]; + param = decodeURIComponent(path_params[j]); + if (param_name) { + params[param_name] = param; + } else { + if (!params.splat) params.splat = []; + params.splat.push(param); + } + }; + pathSpec.callback(params); + // return true; // removed this to allow for multi match + } + }; + }; + + // bind the event + $(function() { + if ('onhashchange' in window) { + // we have a native event + } else { + pollPath(10); + } + // setTimeout(hashChanged,50); + $(window).bind('hashchange', hashChanged); + }); + + function registerPath(pathSpec) { + $.pathbinder.paths.push(pathSpec); + }; + + function setPath(pathSpec, params) { + var newPath = $.mustache(pathSpec.template, params); + goPath(newPath); + }; + + function goPath(newPath) { + if (newPath) { + // $.log("goPath", newPath) + window.location = '#'+newPath; + } + _lastPath = getPath(); + }; + + function getPath() { + var matches = window.location.toString().match(/^[^#]*(#.+)$/); + return matches ? matches[1] : ''; + }; + + function makePathSpec(path, callback) { + var param_names = []; + var template = ""; + + PATH_NAME_MATCHER.lastIndex = 0; + + while ((path_match = PATH_NAME_MATCHER.exec(path)) !== null) { + param_names.push(path_match[1]); + } + + return { + param_names : param_names, + matcher : new RegExp("^" + path.replace( + PATH_NAME_MATCHER, PATH_REPLACER).replace( + SPLAT_MATCHER, SPLAT_REPLACER) + "/?$"), + template : path.replace(PATH_NAME_MATCHER, function(a, b) { + return '{{'+b+'}}'; + }).replace(SPLAT_MATCHER, '{{splat}}'), + callback : callback + }; + }; + + $.fn.pathbinder = function(name, paths, options) { + options = options || {}; + var self = $(this), pathList = paths.split(/\n/); + $.each(pathList, function() { + var path = this; + if (path) { + // $.log("bind path", path); + var pathSpec = makePathSpec(path, function(params) { + // $.log("path cb", name, path, self) + // $.log("trigger path: "+path+" params: ", params); + self.trigger(name, [params]); + }); + // set the path when the event triggered through other means + if (options.bindPath) { + self.bind(name, function(ev, params) { + params = params || {}; + // $.log("set path", name, pathSpec) + setPath(pathSpec, params); + }); + } + // trigger when the path matches + registerPath(pathSpec); + } + }); + }; +})(jQuery); + \ No newline at end of file diff --git a/vendor/couchapp/_attachments/loader.js b/vendor/couchapp/_attachments/loader.js new file mode 100644 index 0000000..86b2ed7 --- /dev/null +++ b/vendor/couchapp/_attachments/loader.js @@ -0,0 +1,17 @@ + +function couchapp_load(scripts) { + for (var i=0; i < scripts.length; i++) { + document.write('