--- a/setup.py +++ b/setup.py @@ -7,6 +7,7 @@ requires = [ 'Flask', + 'requests' ] setup(name='Swtr', --- /dev/null +++ b/swtr.explain @@ -1 +1,59 @@ +== High-level components and data flow of the swtr application == + +> The document is written in an informal way. + +Table of contents +1. Introduction +2. Design +3. Configuration +4. Deployment + +1. Introduction + +swtr is the cannonical swt maker + swt swagger application. +That means, the swtr application can be used to - +a) interact with resources on an existing web page(like +text, images, video etc), annotate them, and make swts out of the annotations +and post the swts to a swt store. +b) fetch existing swts about an existing web page and display/render them based +on a combination of - the type of swt (the context), and configuration +parameters set by the user. + +2. Design + +swtr is designed entirely to be a client-side javascript app. +the components of the swtr app can be divided like- +i) text annotation module - an interface to the user to select and annotate +text. + 1. creates an interface for the user to select any text on the webpage. + 2. when user has selected text - gives two options - edit/renarrate or comment/annotate. + 3.1 if user selects comment, the module provides a text box to enter user's + comment, keeping the selected text by the user in context. + 3.2 if the user selects edit, the module provides an editor keeping the + selected text by the user in context. + 4. In both the above case, the editor window has options to save the changes + or cancel them. + 5.1 If the user cancels, all the changes made by the user is discarded and + previous version of the page is kept. + 5.2 If the user saves her changes, the new edit or comment is updated in the + DOM of the browser to visually reflect the changes, and also the changes + are cached in a local data store. + +ii) image annotation module - an interface to the user to select arbitrary +parts of an image in rectange shape and add textual comments to them. Later +this can be extended to add support for audio, video comments. + 1. provides an interface to click on any image on the webpage and bring it to + center focus to make it annotatable. We use the annotorious project to do + the image annotation. + 2. Once an annotation to the image has been made, the module can provide the + user with options to cancel or save the changes. + 3.1 If the user cancels, all the changes made by the user is discarded (i.e + annotations removed). + 3.2 If the user saves her changes, this module retrieves the annotations via + the annotorious' API and stores them in the local data store. + +iii) map annotation module +iv) controller +v) swt maker +vi) swt swagger --- a/swtr/server.py +++ b/swtr/server.py @@ -1,16 +1,51 @@ #!/usr/bin/python import flask +from flask import session import conf +import requests +import json app = flask.Flask(__name__) +app.config['secret_key'] = "asdkasdiq2jedmaid0q2238uwadscksnc" +app.secret_key = "asdkasdiq2jedmaid0q2238uwadscksnc" + +appID = 'YrYc9oMO7fT0avRUAtbRO1cLvoOUUI08BAuqOAJc' +appSecret = 'r9BIYjYOPotMQUOoI98DmH7Eu1M4zg6cMeLay7LOlSsrF1KhKZ' + @app.route('/', methods=['GET']) def index(): - return flask.render_template('index.html', - url=flask.request.args.get('where'), - conf=conf) + auth_tok = None + if flask.request.args.get('code'): + payload = { + 'scopes': 'email sweet', + 'client_secret': appSecret, + 'code': flask.request.args.get('code'), + 'redirect_uri': 'http://localhost:5000/', + 'grant_type': 'authorization_code', + 'client_id': appID + } + resp = requests.post('http://localhost:5001/oauth/token', data=payload) + auth_tok = json.loads(resp.text) + print auth_tok + if auth_tok.has_key('error'): + print auth_tok['error'] + return flask.make_response(auth_tok['error'], 200) + session['auth_tok'] = auth_tok + + if 'auth_tok' in session: + auth_tok = session['auth_tok'] + else: + auth_tok = {'access_token': '', 'refresh_token': ''} + + print auth_tok + return flask.render_template('index.html', + access_token=auth_tok['access_token'], + refresh_token=auth_tok['refresh_token'], + url=flask.request.args.get('where'), + conf=conf) if __name__ == '__main__': app.run(debug=conf.debug, host=conf.HOST, port=conf.PORT) --- /dev/null +++ b/swtr/static/js/lib/mozilla.persona.include.js @@ -1 +1,9 @@ +/** + * Uncompressed source can be found at https://login.persona.org/include.orig.js + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +(function(){var a;navigator.mozId?navigator.id=navigator.mozId:function(){var a,b=function(){function f(a){return Array.isArray?Array.isArray(a):a.constructor.toString().indexOf("Array")!=-1}function e(a,b,d){var e=c[b][d];for(var f=0;f1)throw"scope may not contain double colons: '::'"}var k=function(){var a="",b="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";for(var c=0;c<5;c++)a+=b.charAt(Math.floor(Math.random()*b.length));return a}(),l={},m={},n={},o=!1,p=[],q=function(a,b,c){var d=!1,e=!1;return{origin:b,invoke:function(b,d){if(!n[a])throw"attempting to invoke a callback of a nonexistent transaction: "+a;var e=!1;for(var f=0;f0)for(var k=0;k=0;e--)try{if(c[e].location.href.indexOf(d)===0&&c[e].name===b)return c[e]}catch(f){}return}function i(a){/^https?:\/\//.test(a)||(a=window.location.href);var b=/^(https?:\/\/[\-_a-zA-Z\.0-9:]+)/.exec(a);return b?b[1]:a}function h(){return window.JSON&&window.JSON.stringify&&window.JSON.parse&&window.postMessage}function g(){try{var a=navigator.userAgent;return a.indexOf("Fennec/")!=-1||a.indexOf("Firefox/")!=-1&&a.indexOf("Android")!=-1}catch(b){}return!1}function f(){var a=-1,b=navigator.userAgent;if(navigator.appName==="Microsoft Internet Explorer"){var c=new RegExp("MSIE ([0-9]{1,}[.0-9]{0,})");c.exec(b)!=null&&(a=parseFloat(RegExp.$1))}else if(b.indexOf("Trident")>-1){var c=new RegExp("rv:([0-9]{2,2}[.0-9]{0,})");c.exec(b)!==null&&(a=parseFloat(RegExp.$1))}return a>=8}function e(a,b,c){a.detachEvent?a.detachEvent("on"+b,c):a.removeEventListener&&a.removeEventListener(b,c,!1)}function d(a,b,c){a.attachEvent?a.attachEvent("on"+b,c):a.addEventListener&&a.addEventListener(b,c,!1)}var b="__winchan_relay_frame",c="die",k=f();return h()?{open:function(f,h){function s(a){if(a.origin===m)try{var b=JSON.parse(a.data);b.a==="ready"?n.postMessage(q,m):b.a==="error"?(r(),h&&(h(b.d),h=null)):b.a==="response"&&(r(),h&&(h(null,b.d),h=null))}catch(c){}}function r(){l&&document.body.removeChild(l),l=a,p&&(p=clearInterval(p)),e(window,"message",s),e(window,"unload",r);if(o)try{o.close()}catch(b){n.postMessage(c,m)}o=n=a}if(!h)throw"missing required callback argument";var j;f.url||(j="missing required 'url' parameter"),f.relay_url||(j="missing required 'relay_url' parameter"),j&&setTimeout(function(){h(j)},0),f.window_name||(f.window_name=null);if(!f.window_features||g())f.window_features=a;var l,m=i(f.url);if(m!==i(f.relay_url))return setTimeout(function(){h("invalid arguments: origin of url and relay_url must match")},0);var n;k&&(l=document.createElement("iframe"),l.setAttribute("src",f.relay_url),l.style.display="none",l.setAttribute("name",b),document.body.appendChild(l),n=l.contentWindow);var o=window.open(f.url,f.window_name,f.window_features);n||(n=o);var p=setInterval(function(){o&&o.closed&&(r(),h&&(h("unknown closed window"),h=null))},500),q=JSON.stringify({a:"request",d:f.params});d(window,"unload",r),d(window,"message",s);return{close:r,focus:function(){if(o)try{o.focus()}catch(a){}}}},onOpen:function(b){function l(a){if(a.data===c)try{window.close()}catch(b){}}function i(c){var d;try{d=JSON.parse(c.data)}catch(g){}!!d&&d.a==="request"&&(e(window,"message",i),f=c.origin,b&&setTimeout(function(){b(f,d.d,function(c){b=a,h({a:"response",d:c})})},0))}function h(a){a=JSON.stringify(a),k?g.doPost(a,f):g.postMessage(a,f)}var f="*",g=k?j():window.opener;if(!g)throw"can't find relay frame";d(k?g:window,"message",i),d(k?g:window,"message",l);try{h({a:"ready"})}catch(m){d(g,"load",function(a){h({a:"ready"})})}var n=function(){try{e(k?g:window,"message",l)}catch(c){}b&&h({a:"error",d:"client closed window"}),b=a;try{window.close()}catch(d){}};d(window,"unload",n);return{detach:function(){e(window,"unload",n)}}}}:{open:function(a,b,c,d){setTimeout(function(){d("unsupported browser")},0)},onOpen:function(a){setTimeout(function(){a("unsupported browser")},0)}}}();var c=function(){function l(){return c}function k(){c=g()||h()||i()||j();return!c}function j(){if(!(window.JSON&&window.JSON.stringify&&window.JSON.parse))return"JSON_NOT_SUPPORTED"}function i(){if(!a.postMessage)return"POSTMESSAGE_NOT_SUPPORTED"}function h(){try{var b="localStorage"in a&&a.localStorage!==null;if(b)a.localStorage.setItem("test","true"),a.localStorage.removeItem("test");else return"LOCALSTORAGE_NOT_SUPPORTED"}catch(c){return"LOCALSTORAGE_DISABLED"}}function g(){return f()}function f(){var a=e(),b=a>-1&&a<8;if(b)return"BAD_IE_VERSION"}function e(){var a=-1;if(b.appName=="Microsoft Internet Explorer"){var c=b.userAgent,d=new RegExp("MSIE ([0-9]{1,}[.0-9]{0,})");d.exec(c)!=null&&(a=parseFloat(RegExp.$1))}return a}function d(c,d){b=c,a=d}var a=window,b=navigator,c;return{setTestEnv:d,isSupported:k,getNoSupportReason:l}}();navigator.id||(navigator.id={});if(!navigator.id.request||navigator.id._shimmed){var d="https://login.persona.org",e=navigator.userAgent,f=e.indexOf("Fennec/")!=-1||e.indexOf("Firefox/")!=-1&&e.indexOf("Android")!=-1,g=f?a:"menubar=0,location=1,resizable=1,scrollbars=1,status=0,width=700,height=375",h=e.match(/CriOS/)||e.match(/Windows Phone/),i="WATCH_NEEDED",j="__persona_dialog",k,l={login:null,logout:null,match:null,ready:null},m,n=a;function o(b){b!==!0;if(n===a)n=b;else if(n!=b)throw new Error("you cannot combine the navigator.id.watch() API with navigator.id.getVerifiedEmail() or navigator.id.get()this site should instead use navigator.id.request() and navigator.id.watch()")}var p,q=!1,r=c.isSupported();function s(a){document.addEventListener?document.addEventListener("DOMContentLoaded",function b(){document.removeEventListener("DOMContentLoaded",b),a()},!1):document.attachEvent&&document.readyState&&document.attachEvent("onreadystatechange",function c(){var b=document.readyState;if(b==="loaded"||b==="complete"||b==="interactive")document.detachEvent("onreadystatechange",c),a()})}function t(){if(!!r){var c=window.document;if(!c.body){q||(s(t),q=!0);return}try{if(!p){var e=c.createElement("iframe");e.style.display="none",c.body.appendChild(e),e.src=d+"/communication_iframe",p=b.build({window:e.contentWindow,origin:d,scope:"mozid_ni",onReady:function(){p.call({method:"loaded",success:function(){l.ready&&l.ready()},error:function(){}})}}),p.bind("logout",function(a,b){l.logout&&l.logout()}),p.bind("login",function(a,b){l.login&&l.login(b)}),p.bind("match",function(a,b){l.match&&l.match()}),u(m)&&p.notify({method:"loggedInUser",params:m})}}catch(f){p=a}}}function u(a){return typeof a!="undefined"}function v(a){try{console.warn(a)}catch(b){}}function w(a,b){if(u(a[b])){v(b+" has been deprecated");return!0}}function x(a,b,c){if(u(a[b])&&u(a[c]))throw new Error("you cannot supply *both* "+b+" and "+c);w(a,b)&&(a[c]=a[b],delete a[b])}function y(a){if(typeof a=="object"){if(a.onlogin&&typeof a.onlogin!="function"||a.onlogout&&typeof a.onlogout!="function"||a.onmatch&&typeof a.onmatch!="function"||a.onready&&typeof a.onready!="function")throw new Error("non-function where function expected in parameters to navigator.id.watch()");if(!a.onlogin)throw new Error("'onlogin' is a required argument to navigator.id.watch()");if(!a.onlogout)throw new Error("'onlogout' is a required argument to navigator.id.watch()");l.login=a.onlogin||null,l.logout=a.onlogout||null,l.match=a.onmatch||null,l.ready=a.onready||null,x(a,"loggedInEmail","loggedInUser"),m=a.loggedInUser,t()}}var z;function A(){var a=z;a==="request"&&(l.ready?a="watch_with_onready":a="watch_without_onready");return a}function B(b){function r(){if(p)return p.call({method:"redirect_flow",params:JSON.stringify(b),success:function(){window.location=d+"/sign_in"}})}function n(){var a=c.getNoSupportReason();if(!a&&!e)return i}function m(){return c.isSupported()&&e}w(b,"requiredEmail"),x(b,"tosURL","termsOfService"),x(b,"privacyURL","privacyPolicy"),b.termsOfService&&!b.privacyPolicy&&v("termsOfService ignored unless privacyPolicy also defined"),b.privacyPolicy&&!b.termsOfService&&v("privacyPolicy ignored unless termsOfService also defined"),b.rp_api=A();var e=!h||z==="request";z=null,b.start_time=(new Date).getTime();if(k)try{k.focus()}catch(f){}else{if(!m()){var o=n(),q="unsupported_dialog";o==="LOCALSTORAGE_DISABLED"?q="cookies_disabled":o===i&&(q="unsupported_dialog_without_watch"),k=window.open(d+"/"+q,j,g);return}p&&p.notify({method:"dialog_running"});if(h)return r();k=WinChan.open({url:d+"/sign_in",relay_url:d+"/relay",window_features:g,window_name:j,params:{method:"get",params:b}},function(c,d){if(p){!c&&d&&d.email&&p.notify({method:"loggedInUser",params:d.email});var e=!(c||d&&d.assertion);p.notify({method:"dialog_complete",params:e})}k=a;if(!c&&d&&d.assertion)try{l.login&&l.login(d.assertion)}catch(f){console.log(f);throw f}if(c==="client closed window"||!d)b&&b.oncancel&&b.oncancel(),delete b.oncancel})}}navigator.id={request:function(a){if(this!=navigator.id)throw new Error("all navigator.id calls must be made on the navigator.id object");if(!l.login)throw new Error("navigator.id.watch must be called before navigator.id.request");a=a||{},o(!1),z="request",a.returnTo||(a.returnTo=document.location.pathname);return B(a)},watch:function(a){if(this!=navigator.id)throw new Error("all navigator.id calls must be made on the navigator.id object");o(!1),y(a)},logout:function(a){if(this!=navigator.id)throw new Error("all navigator.id calls must be made on the navigator.id object");t(),p&&p.notify({method:"logout"}),typeof a=="function"&&(v("navigator.id.logout callback argument has been deprecated."),setTimeout(a,0))},get:function(b,c){var d={};c=c||{},d.privacyPolicy=c.privacyPolicy||a,d.termsOfService=c.termsOfService||a,d.privacyURL=c.privacyURL||a,d.tosURL=c.tosURL||a,d.siteName=c.siteName||a,d.siteLogo=c.siteLogo||a,d.backgroundColor=c.backgroundColor||a,d.experimental_emailHint=c.experimental_emailHint||a,z=z||"get";w(c,"silent")?b&&setTimeout(function(){b(null)},0):(o(!0),y({onlogin:function(a){b&&(b(a),b=null)},onlogout:function(){}}),d.oncancel=function(){b&&(b(null),b=null),l.login=l.logout=l.match=l.ready=null},B(d))},getVerifiedEmail:function(a){v("navigator.id.getVerifiedEmail has been deprecated"),o(!0),z="getVerifiedEmail",navigator.id.get(a)},_shimmed:!0}}}()})() --- /dev/null +++ b/swtr/static/js/oauth.js @@ -1 +1,53 @@ +(function(window) { + + var Oauth = function(options) { + // all necessary params provided? + if(!options.hasOwnProperty('app_id') || !options.hasOwnProperty('app_secret') || + !options.hasOwnProperty('redirect_uri') || !options.hasOwnProperty('scopes') || + !options.hasOwnProperty('endpoint')) { + + throw new Error('All of the - app_id, app_secret, redirect_uri, scopes'+ + ', endpoint - parameters must be provided'); + return; + } + // check for string types + for(key in options) { + if(typeof options[key] !== 'string') { + throw new Error('All parameters should be of string type'); + return false; + } + } + + this.app_id = options.app_id; + this.app_secret = options.app_secret; + this.redirect_uri = options.redirect_uri; + this.scopes = options.scopes.split(','); + this.endpoint = options.endpoint; + // if scopes returns empty array - that means scopes was not provided in + // correct format - i.e comma seperated values + if(!this.scopes.length) { + throw new Error('scopes paramater must be a string of comma seperated scopes'); + return; + } + + return this; + }; + + // callback function to click handler + Oauth.prototype.login = function(event) { + event.preventDefault(); + this.authorize(); + }; + + Oauth.prototype.authorize = function() { + var qs = 'scope=' + this.scopes.join('+') + '&' + + 'redirect_uri=' + encodeURIComponent(this.redirect_uri) + '&' + + 'response_type=code&'+ + 'client_id=' + this.app_id; + + window.location.href = this.endpoint + '?' + qs; + }; + + window.Oauth = Oauth; +})(window); --- a/swtr/static/js/swtmaker.js +++ b/swtr/static/js/swtmaker.js @@ -10,12 +10,28 @@ $.ajaxSetup({ xhrFields: { + // we need this to send cookies to cross-domain requests withCredentials: true }, + //some browsers won't make cross-domain ajax until it is explicitly set crossDomain: true }); + swtr.handleOAuth(); }; + swtr.handleOAuth = function() { + if(swtr.access_token) { + $('#signinview').html('Signing you in..'); + $.ajax({ + url: 'http://localhost:5001/api/users/me?access_token='+ + swtr.access_token, + success: function(data) { + swtr.appView.userLoggedIn(data.username); + } + }); + } + }; + /* Model for Image Annotation Sweets */ var ImgAnnoSwt = Backbone.Model.extend({ defaults: { @@ -51,10 +67,14 @@ throw Error('"where" option must be passed to get sweets of a URI'); return false; } + if(!swtr.access_token) { + throw new Error('Access Token required to get query that API'); + } // setting up params var where = options.where, who = options.who || null; - url = swtr.swtstoreURL() + swtr.endpoints.get + '?where=' + where; + url = swtr.swtstoreURL() + swtr.endpoints.get + '?where=' + + encodeURIComponent(where) + '&access_token=' + swtr.access_token; if(who) { url += '&who=' + who; } @@ -83,8 +103,16 @@ var new_sweets = this.getNew(); var dummy_collection = new Backbone.Collection(new_sweets); + if(!swtr.access_token) { + throw new Error('Access Token is required to sweet'); + return; + } + + var url = swtr.swtstoreURL() + swtr.endpoints.post + + '?access_token=' + swtr.access_token; + this.sync('create', dummy_collection, { - url: swtr.swtstoreURL() + swtr.endpoints.post, + url: url, success: function() { if(typeof options.success === 'function') { options.success.apply(this, arguments); @@ -166,8 +194,8 @@ swtr.appView.$overlay.hide(); swtr.appView.helpview.step(6); }, - error: function(jqxhr, error) { - console.log(jqxhr, error); + error: function(jqxhr, error, text) { + console.log(jqxhr, error, text); } }); this.cleanUp(); @@ -202,6 +230,14 @@ else { this.helpview.step(1); } + + this.oauth = new Oauth({ + app_id: 'YrYc9oMO7fT0avRUAtbRO1cLvoOUUI08BAuqOAJc', + app_secret: 'r9BIYjYOPotMQUOoI98DmH7Eu1M4zg6cMeLay7LOlSsrF1KhKZ', + endpoint: 'http://localhost:5001/oauth/authorize', + redirect_uri: 'http://localhost:5000/', + scopes: 'email,sweet' + }); }, setImage: function() { anno.reset(); @@ -229,7 +265,9 @@ swtr.sweets.getAll({ where: this.imgURL, success: function(data) { + console.log(data); if(_.isArray(data)) { + console.log('data is array'); swtr.sweets.add(data); _.each(data, function(swt) { swt.how['editable'] = false; @@ -276,16 +314,7 @@ }, getSignInCredentials: function(event) { event.preventDefault(); - navigator.id.request(); - /*if(swtr.who === 'Guest' && !$('#username').length) { - var template = _.template($('#signin-credentials-template').html()); - $('#signin-msg').html(template()); - } - else if($('#username').length && $('#username').val()) { - var username = $('#username').val(); - var password = $('#password').val(); - this.signIn(username, password); - }*/ + this.oauth.authorize(); return false; }, signIn: function(username, password) { @@ -311,9 +340,9 @@ } }); }, - userLoggedIn: function(userData) { - swtr.who = userData.username; - var text = 'You are signed in as ' + swtr.who+ ''; + userLoggedIn: function(token) { + swtr.who = token; + var text = 'You are signed in as ' + swtr.who + ''; $('#signinview').html(text); }, userLoggedOut: function() { @@ -363,7 +392,7 @@ //swtr.AppView = AppView; // Persona callbacks - navigator.id.watch({ + /*navigator.id.watch({ //when an user logs in onlogin: function(assertion) { //verify assertion and login the user @@ -394,7 +423,7 @@ } }); } - }); + });*/ })(swtr); --- a/swtr/templates/index.html +++ b/swtr/templates/index.html @@ -52,8 +52,10 @@ - + +