Commit 68af0f8094802e278a34d1fc1595bb7a9904ba9f

Add pagination to OCD search results, a different URL for oauth, and fixes

  - Add pagination to OCD search results.
  - Change server code to have a different endpoint (redirect URI of swtstore)
    to process the OAuth authentication.
  - Fix bug when entering input which is not a URL, the app would throw 500;
    now it handles that error.
  - Fix overlay not showing for the entire page.
  • Diff rendering mode:
  • inline
  • side by side

swtr/server.py

1# -*- coding: utf-8 -*-1# -*- coding: utf-8 -*-
22
3import flask
4from flask import session
3from flask import Flask, session, request, make_response, url_for, redirect,\
4 render_template, jsonify, abort
5import lxml.html5import lxml.html
6import requests6import requests
7import json7import json
12import config12import config
1313
1414
15app = flask.Flask(__name__)
15app = Flask(__name__)
16app.config['SECRET_KEY'] = config.secret_key16app.config['SECRET_KEY'] = config.secret_key
1717
1818
19@app.route('/', methods=['GET'])19@app.route('/', methods=['GET'])
20def index():20def index():
21 auth_tok = None
22 # check if ?code param is there
23 if flask.request.args.get('code'):
24 # prepare the payload
25 payload = {
26 'scopes': 'email sweet',
27 'client_secret': config.app_secret,
28 'code': flask.request.args.get('code'),
29 'redirect_uri': config.redirect_uri,
30 'grant_type': 'authorization_code',
31 'client_id': config.app_id
32 }
33 # token exchange endpoint
34 oauth_token_x_endpoint = config.swtstoreURL + '/oauth/token'
35 resp = requests.post(oauth_token_x_endpoint, data=payload)
36 auth_tok = json.loads(resp.text)
37 print 'recvd auth token from swtstore'
38 print auth_tok
3921
40 if 'error' in auth_tok:
41 print auth_tok['error']
42 return flask.make_response(auth_tok['error'], 200)
43
44 # set sessions et al
45 session['auth_tok'] = auth_tok
46 session['auth_tok']['issued'] = datetime.utcnow()
47
48 # else if session is already existing..
22 # if auth_tok is in session already..
49 if 'auth_tok' in session:23 if 'auth_tok' in session:
50 auth_tok = session['auth_tok']24 auth_tok = session['auth_tok']
25
51 # check if it has expired26 # check if it has expired
52 oauth_token_expires_in_endpoint = config.swtstoreURL +\27 oauth_token_expires_in_endpoint = config.swtstoreURL +\
53 '/oauth/token-expires-in'28 '/oauth/token-expires-in'
54 resp = requests.get(oauth_token_expires_in_endpoint)29 resp = requests.get(oauth_token_expires_in_endpoint)
55 expires_in = json.loads(resp.text)['expires_in']30 expires_in = json.loads(resp.text)['expires_in']
31
56 # added for backwared compatibility. previous session stores did not32 # added for backwared compatibility. previous session stores did not
57 # have issued key33 # have issued key
58 try:34 try:
5050
51 #print 'existing tokens'51 #print 'existing tokens'
52 #print auth_tok52 #print auth_tok
53 return flask.render_template('index.html',
54 access_token=auth_tok['access_token'],
55 refresh_token=auth_tok['refresh_token'],
56 config=config,
57 url=flask.request.args.get('where'))
53 return render_template('index.html',
54 access_token=auth_tok['access_token'],
55 refresh_token=auth_tok['refresh_token'],
56 config=config,
57 url=request.args.get('where'))
5858
5959
60@app.route('/authenticate', methods=['GET'])
61def authenticateWithOAuth():
62 auth_tok = None
63 code = request.args.get('code')
64
65 # prepare the payload
66 payload = {
67 'scopes': 'email sweet',
68 'client_secret': config.app_secret,
69 'code': code,
70 'redirect_uri': config.redirect_uri,
71 'grant_type': 'authorization_code',
72 'client_id': config.app_id
73 }
74
75 # token exchange endpoint
76 oauth_token_x_endpoint = config.swtstoreURL + '/oauth/token'
77 resp = requests.post(oauth_token_x_endpoint, data=payload)
78 auth_tok = json.loads(resp.text)
79 print 'recvd auth token from swtstore'
80 print auth_tok
81
82 if 'error' in auth_tok:
83 print auth_tok['error']
84 return make_response(auth_tok['error'], 200)
85
86 # set sessions et al
87 session['auth_tok'] = auth_tok
88 session['auth_tok']['issued'] = datetime.utcnow()
89 return redirect(url_for('index'))
90
91
60# endpoint to search the Open Cuultur Data APIs92# endpoint to search the Open Cuultur Data APIs
61# takes in `query`, `size`, and `from` parameters in query string93# takes in `query`, `size`, and `from` parameters in query string
62# returns a JSON response94# returns a JSON response
63@app.route('/search/ocd', methods=['GET'])95@app.route('/search/ocd', methods=['GET'])
64def searchOCD():96def searchOCD():
65 query = flask.request.args.get('query')
97 query = request.args.get('query')
66 #collection = flask.request.args.get('collection')98 #collection = flask.request.args.get('collection')
67 size = flask.request.args.get('size') or 10
68 offset = flask.request.args.get('from') or 0
99 size = request.args.get('size') or 9
100 offset = request.args.get('from') or 0
69101
70 # if query parameter is not passed, return bad request.102 # if query parameter is not passed, return bad request.
71 if not query:103 if not query:
72 flask.abort(400)
104 abort(400)
73105
74 payload = {106 payload = {
75 'query': query,107 'query': query,
118 resp = requests.post('http://api.opencultuurdata.nl/v0/search',118 resp = requests.post('http://api.opencultuurdata.nl/v0/search',
119 data=json.dumps(payload))119 data=json.dumps(payload))
120120
121 response = flask.make_response()
121 response = make_response()
122 response.data = json.dumps(resp.json())122 response.data = json.dumps(resp.json())
123 response.headers['Content-type'] = 'application/json'123 response.headers['Content-type'] = 'application/json'
124 return response124 return response
128@app.route('/resolve-ocd-media', methods=['GET'])128@app.route('/resolve-ocd-media', methods=['GET'])
129def resolveOCDMediaURLs():129def resolveOCDMediaURLs():
130130
131 media_hash = flask.request.args.get('hash') or None
131 media_hash = request.args.get('hash') or None
132132
133 if not media_hash:133 if not media_hash:
134 flask.abort(400)
134 abort(400)
135135
136 resp = requests.get('http://api.opencultuurdata.nl/v0/resolve/' +136 resp = requests.get('http://api.opencultuurdata.nl/v0/resolve/' +
137 media_hash)137 media_hash)
138138
139 return flask.jsonify(url=resp.url)
139 return jsonify(url=resp.url)
140140
141141
142@app.route('/media-type', methods=['GET'])142@app.route('/media-type', methods=['GET'])
143def getMediaType():143def getMediaType():
144144
145 where = flask.request.args.get('where') or None
145 where = request.args.get('where') or None
146146
147 if not where:147 if not where:
148 flask.abort(400)
148 abort(400)
149149
150 resp = requests.get(where)150 resp = requests.get(where)
151 content = resp.text151 content = resp.text
152152
153 if imghdr.what('ignore', content) is None:153 if imghdr.what('ignore', content) is None:
154 return flask.jsonify({'type': 'html'})
154 return jsonify({'type': 'html'})
155 else:155 else:
156 return flask.jsonify({'type': 'image'})
156 return jsonify({'type': 'image'})
157157
158158
159@app.route('/annotate', methods=['GET'])159@app.route('/annotate', methods=['GET'])
160def annotate():160def annotate():
161 # img = urllib2.urlopen(flask.request.args['where']).read()161 # img = urllib2.urlopen(flask.request.args['where']).read()
162 request = requests.get(flask.request.args['where'])
163 content = request.text
162 where = request.args.get('where')
163 response = requests.get(where)
164 content = response.text
164 if imghdr.what('ignore', content) is None:165 if imghdr.what('ignore', content) is None:
165 root = lxml.html.parse(StringIO.StringIO(content)).getroot()166 root = lxml.html.parse(StringIO.StringIO(content)).getroot()
166 root.make_links_absolute(flask.request.args['where'],
167 root.make_links_absolute(where,
167 resolve_base_href=True)168 resolve_base_href=True)
168169
169 addScript("//code.jquery.com/jquery-1.11.0.min.js", root)170 addScript("//code.jquery.com/jquery-1.11.0.min.js", root)
170 addScript(flask.url_for('static', filename="js/annotator-full.min.js"),
171 addScript(url_for('static', filename="js/annotator-full.min.js"),
171 root)172 root)
172173
173 addCSS(flask.url_for('static', filename='css/annotator.min.css'), root)
174 addCSS(flask.url_for('static', filename='css/swtmaker.css'), root)
175 addCSS(flask.url_for('static', filename='css/bootstrap.min.css'), root)
176 addScript(flask.url_for('static',
177 filename="js/lib/underscore-1.5.2.min.js"),
174 addCSS(url_for('static', filename='css/annotator.min.css'), root)
175 addCSS(url_for('static', filename='css/swtmaker.css'), root)
176 addCSS(url_for('static', filename='css/bootstrap.min.css'), root)
177 addScript(url_for('static',
178 filename="js/lib/underscore-1.5.2.min.js"),
178 root)179 root)
179 addScript(flask.url_for('static',
180 filename="js/lib/backbone-1.0.0.min.js"),
180 addScript(url_for('static',
181 filename="js/lib/backbone-1.0.0.min.js"),
181 root)182 root)
182 addCSS(flask.url_for('static', filename='css/annotorious.css'), root)
183 addScript(flask.url_for('static',
184 filename="js/annotorious.okfn.0.3.js"),
183 addCSS(url_for('static', filename='css/annotorious.css'), root)
184 addScript(url_for('static',
185 filename="js/annotorious.okfn.0.3.js"),
185 root)186 root)
186187
187 if 'auth_tok' in session:188 if 'auth_tok' in session:
213 config.redirect_uri)213 config.redirect_uri)
214 configScript.set("type", "text/javascript")214 configScript.set("type", "text/javascript")
215215
216 addScript(flask.url_for('static', filename="js/oauth.js"), root)
216 addScript(url_for('static', filename="js/oauth.js"), root)
217217
218 addScript(flask.url_for('static', filename="js/app.js"), root)
218 addScript(url_for('static', filename="js/app.js"), root)
219219
220 response = flask.make_response()
220 response = make_response()
221 response.data = lxml.html.tostring(root)221 response.data = lxml.html.tostring(root)
222 return response222 return response
223223
227 else:227 else:
228 auth_tok = {'access_token': '', 'refresh_token': ''}228 auth_tok = {'access_token': '', 'refresh_token': ''}
229229
230 return flask.render_template('index.html',
231 access_token=auth_tok['access_token'],
232 refresh_token=auth_tok['refresh_token'],
233 config=config,
234 url=flask.request.args.get('where'))
230 return render_template('index.html',
231 access_token=auth_tok['access_token'],
232 refresh_token=auth_tok['refresh_token'],
233 config=config,
234 url=request.args.get('where'))
235235
236236
237def addScript(src, el):237def addScript(src, el):

swtr/static/css/swtmaker.css

22 margin 0 20px;22 margin 0 20px;
23 padding: 10px;23 padding: 10px;
24 text-align: center;24 text-align: center;
25 z-index: 100001;
25}26}
2627
27#img-annotation-wrapper, #ocd-results {28#img-annotation-wrapper, #ocd-results {
78 font-size: 1.3em;78 font-size: 1.3em;
79 min-height: 50px;79 min-height: 50px;
80 text-align: center;80 text-align: center;
81 z-index: 100001;
81}82}
82#app-overlay {83#app-overlay {
83 position: absolute;
84 top: 60px;
84 position: fixed;
85 top: 0;
85 left: 0;86 left: 0;
86 display: none;87 display: none;
87 width: 100%;88 width: 100%;
88 height: 800px;
89 height: 100%;
89 z-index: 10000;90 z-index: 10000;
90 background-color: rgba(255, 255, 255, 0.7);91 background-color: rgba(255, 255, 255, 0.7);
91}92}

swtr/static/js/swtmaker.js

66 throw Error('"where" option must be passed to get sweets of a URI');66 throw Error('"where" option must be passed to get sweets of a URI');
67 return false;67 return false;
68 }68 }
69 /*if(!swtr.access_token) {
70 throw new Error('Access Token required to get query that API');
71 }*/
72 // setting up params69 // setting up params
73 var where = options.where,70 var where = options.where,
74 who = options.who || null;71 who = options.who || null;
230 cleanUp: function() {230 cleanUp: function() {
231 //console.log('cleaning up');231 //console.log('cleaning up');
232 $(this.el).hide();232 $(this.el).hide();
233 if(swtr.appView.source === 'ocd') {
234 $('#img-annotation-wrapper').hide();
235 $('#ocd-results').show();
236 }
237 }233 }
238 });234 });
239235
299 this.loadURL(input);299 this.loadURL(input);
300 }300 }
301 },301 },
302 // load a URL for annotation (can be of image or html resource for now)
302 loadURL: function(url, type) {303 loadURL: function(url, type) {
303 console.log('loadURL()');
304 //console.log('loadURL()');
304 if(this.source !== 'ocd') {305 if(this.source !== 'ocd') {
305 $('#ocd-results').hide();306 $('#ocd-results').hide();
306 }307 }
307 $('#img-annotation-wrapper').show();308 $('#img-annotation-wrapper').show();
308 if(!url) {
309 if(!url || !url.match(/http/)) {
310 this.helpview.step(13);
309 return false;311 return false;
310 }312 }
311 // if type is given explicitly; we load it as such.313 // if type is given explicitly; we load it as such.
605 var self = this;605 var self = this;
606 $('#img-annotation-wrapper').hide();606 $('#img-annotation-wrapper').hide();
607 $('#ocd-results').show();607 $('#ocd-results').show();
608 $('#ocd-results').append('Loading..');
608 $('#ocd-results').html('<h4 style="text-align: center;">Loading..</h4>');
609 $.ajax({609 $.ajax({
610 type: 'GET',610 type: 'GET',
611 url: '/search/ocd',611 url: '/search/ocd',
612 data: {query: input},612 data: {query: input},
613 success: function(data) {613 success: function(data) {
614 self.ocdView = new OCDView({model: data.hits.hits});
614 self.ocdView = new OCDView({
615 query: input,
616 data: data,
617 model: data.hits.hits
618 });
615 }619 }
616 });620 });
617 }621 }
618 });622 });
619623
620 var OCDView = Backbone.View.extend({624 var OCDView = Backbone.View.extend({
621 el: $('#ocd-results'),
625 el: $('#ocd-view'),
622 events: {626 events: {
623 'click .ocd-item a': 'onClickImg'
627 'click .ocd-item a': 'onImgClick',
628 'click .pager li': 'onPagerClick'
624 },629 },
625 initialize: function() {
630 initialize: function(opts) {
631 this.data = opts.data || {};
632 this.query = opts.query || '';
633 this.size = 9; // num of items per page
634 this.page = 0;
626 this.item_template = _.template($('#ocd-item-template').html());635 this.item_template = _.template($('#ocd-item-template').html());
636 this.base_template = _.template($('#ocd-view-base-template').html());
627 this.render();637 this.render();
628 },638 },
629 render: function() {639 render: function() {
630 var $row_el;640 var $row_el;
631 this.$el.html('');641 this.$el.html('');
642 if(!this.model.length) {
643 this.$el.html('No results could be found from your query.');
644 return;
645 }
646 this.$el.html(this.base_template());
647 var $el = $('#ocd-results');
632 _.each(this.model, function(item, idx) {648 _.each(this.model, function(item, idx) {
633 // put every 3 items in a row649 // put every 3 items in a row
634 if(idx % 3 === 0) {650 if(idx % 3 === 0) {
635 $row_el = $('<div class="row"></div>');651 $row_el = $('<div class="row"></div>');
636 this.$el.append($row_el);
652 $el.append($row_el);
637 }653 }
638 $row_el.append(this.item_template({654 $row_el.append(this.item_template({
639 title: item._source.title,655 title: item._source.title,
658 }));658 }));
659 }, this);659 }, this);
660 this.resolveOCDURLs();660 this.resolveOCDURLs();
661 this.appendTotal();
661 },662 },
663 appendTotal: function() {
664 $('#ocd-total-results').html(this.data.hits.total + ' results found.');
665 },
662 // resolve the OCD media URLs666 // resolve the OCD media URLs
663 resolveOCDURLs: function() {667 resolveOCDURLs: function() {
664 var self = this;668 var self = this;
674 });674 });
675 });675 });
676 },676 },
677 onClickImg: function(event) {
677 rerender: function(data) {
678 this.data = data;
679 this.model = data.hits.hits;
680 this.render();
681 },
682 onPagerClick: function(event) {
678 event.preventDefault();683 event.preventDefault();
684 var elem = $(event.currentTarget);
685 var self = this;
686 if(elem.hasClass('next')) {
687 if((this.page + 1) * this.size >= this.data.hits.total) {
688 console.log('no next page to go to');
689 return false;
690 }
691 console.log('clicked next');
692 this.search({
693 query: this.query,
694 from: (this.page + 1) * this.size
695 }, function(resp) {
696 console.log('reached next page');
697 self.page = self.page + 1;
698 self.rerender(resp);
699 });
700 }
701 else if (elem.hasClass('previous')) {
702 if(this.page <= 0) {
703 console.log('no prev page to go to');
704 return false;
705 }
706 console.log('clicked prev');
707 this.search({
708 query: this.query,
709 from: (this.page - 1) * this.size
710 }, function(resp) {
711 console.log('reached prev page');
712 self.page = self.page - 1;
713 self.rerender(resp);
714 });
715 }
716 return false;
717 },
718 onImgClick: function(event) {
719 event.preventDefault();
679 // TODO: init the image anno720 // TODO: init the image anno
680 var url = $(event.currentTarget).find('img').attr('src');721 var url = $(event.currentTarget).find('img').attr('src');
681 swtr.appView.loadURL(url, 'image');722 swtr.appView.loadURL(url, 'image');
682 return false;723 return false;
724 },
725 search: function(data, cb) {
726 swtr.appView.$overlay.show();
727 var self = this;
728 $.ajax({
729 type: 'GET',
730 url: '/search/ocd',
731 data: data,
732 success: function(resp) {
733 swtr.appView.$overlay.hide();
734 cb(resp);
735 }
736 });
683 }737 }
684 });738 });
685739
752 switch (n) {752 switch (n) {
753 case 0 : text = 'Getting annotations..';753 case 0 : text = 'Getting annotations..';
754 break;754 break;
755 case 1: text = 'Enter the URL of an image or web page below, and start annotating!';
755 case 1: text = 'Enter URL of an image or web page below, and start annotating!';
756 break;756 break;
757 case 2: text = 'Annotate the image, or see other annotations';757 case 2: text = 'Annotate the image, or see other annotations';
758 break;758 break;
775 case 11: text = 'Search in <a href="http://www.opencultuurdata.nl/">Open Cuultur Data API</a>';775 case 11: text = 'Search in <a href="http://www.opencultuurdata.nl/">Open Cuultur Data API</a>';
776 break;776 break;
777 case 12: text = 'Analyzing the resource type..';777 case 12: text = 'Analyzing the resource type..';
778 break;
779 case 13: text = 'This does not seem to be a URL. Please enter a valid URL.';
778 break;780 break;
779 }781 }
780 $(this.el).html(text);782 $(this.el).html(text);

swtr/templates/index.html

76 <img src="" id="annotatable-img" class="img-responsive" alt="Annotation Workbench">76 <img src="" id="annotatable-img" class="img-responsive" alt="Annotation Workbench">
77 {% endif %}77 {% endif %}
78 </div>78 </div>
79 <div id="ocd-results"></div>
79 <div id="ocd-view"></div>
80 </div> <!-- end fourth row -->80 </div> <!-- end fourth row -->
8181
82 <div id="sweet-list-wrapper">82 <div id="sweet-list-wrapper">
142 <li class="custom-popup-line"><span>Related Links: </span><%= Links %></li>142 <li class="custom-popup-line"><span>Related Links: </span><%= Links %></li>
143 </ul>143 </ul>
144 </span>144 </span>
145 </script>
146 <script type="text/template" id="ocd-view-base-template">
147 <div id="ocd-hud">
148 <span id="ocd-total-results"></span>
149 </div>
150 <div id="ocd-results"></div>
151 <ul class="pager">
152 <li class="previous"> <a href="#">&laquo; Previous </a></li>
153 <li class="next"> <a href="#"> Next &raquo; </a></li>
154 </ul>
145 </script>155 </script>
146 <script type="text/template" id="ocd-item-template">156 <script type="text/template" id="ocd-item-template">
147 <div class="col-sm-6 col-md-4 ocd-item">157 <div class="col-sm-6 col-md-4 ocd-item">