diff --git a/swh/web/ui/templates/home.html b/swh/web/ui/templates/home.html index 558c14cdd..2e462bc62 100644 --- a/swh/web/ui/templates/home.html +++ b/swh/web/ui/templates/home.html @@ -1,9 +1,8 @@ {% extends "layout.html" %} {% block title %}Home{% endblock %} {% block content %} {% endblock %} diff --git a/swh/web/ui/templates/upload_and_search.html b/swh/web/ui/templates/upload_and_search.html index 8bd9832c4..30a94891c 100644 --- a/swh/web/ui/templates/upload_and_search.html +++ b/swh/web/ui/templates/upload_and_search.html @@ -1,21 +1,34 @@ {% extends "layout.html" %} -{% block title %}Upload, hash and search{% endblock %} +{% block title %}Search or Upload/Hash/Search{% endblock %} {% block content %} -{% if message is not none %}
-
+
+ +
+ +
+
+ Or upload some file, we'll hash and search it: +
- +
+ {% if message is not none %} +
+
{{ message | safe }}
+ {% endif %} +
-
{{ message | safe }}
-{% endif %} {% endblock %} diff --git a/swh/web/ui/tests/test_views.py b/swh/web/ui/tests/test_views.py index cb5e92e60..f44098611 100644 --- a/swh/web/ui/tests/test_views.py +++ b/swh/web/ui/tests/test_views.py @@ -1,388 +1,381 @@ # Copyright (C) 2015 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU Affero General Public License version 3, or any later version # See top-level LICENSE file for more information from nose.tools import istest from swh.web.ui.tests import test_app from unittest.mock import patch, MagicMock from swh.web.ui.exc import BadInputExc class ViewTestCase(test_app.SWHViewTestCase): render_template = False @istest def info(self): # when rv = self.client.get('/about') self.assertEquals(rv.status_code, 200) self.assert_template_used('about.html') self.assertIn(b'About', rv.data) @istest def search_default(self): # when rv = self.client.get('/search') self.assertEquals(rv.status_code, 200) self.assertEqual(self.get_context_variable('q'), '') self.assertEqual(self.get_context_variable('message'), '') - self.assert_template_used('search.html') + self.assertEqual(self.get_context_variable('filename'), None) + self.assertEqual(self.get_context_variable('found'), None) + self.assert_template_used('upload_and_search.html') @patch('swh.web.ui.views.service') @istest def search_content_found(self, mock_service): # given mock_service.lookup_hash.return_value = { 'found': True, 'algo': 'sha1' } # when rv = self.client.get('/search?q=sha1:123') self.assertEquals(rv.status_code, 200) - self.assert_template_used('search.html') + self.assert_template_used('upload_and_search.html') self.assertEqual(self.get_context_variable('q'), 'sha1:123') self.assertEqual(self.get_context_variable('message'), 'Content with hash sha1:123 found!') mock_service.lookup_hash.assert_called_once_with('sha1:123') @patch('swh.web.ui.views.service') @istest def search_content_not_found(self, mock_service): # given mock_service.lookup_hash.return_value = { 'found': False, 'algo': 'sha1' } # when rv = self.client.get('/search?q=sha1:456') self.assertEquals(rv.status_code, 200) - self.assert_template_used('search.html') + self.assert_template_used('upload_and_search.html') self.assertEqual(self.get_context_variable('q'), 'sha1:456') self.assertEqual(self.get_context_variable('message'), 'Content with hash sha1:456 not found!') mock_service.lookup_hash.assert_called_once_with('sha1:456') @patch('swh.web.ui.views.service') @istest def search_content_invalid_query(self, mock_service): # given mock_service.lookup_hash = MagicMock( side_effect=BadInputExc('Invalid query!') ) # when rv = self.client.get('/search?q=sha1:invalid-hash') self.assertEquals(rv.status_code, 200) - self.assert_template_used('search.html') + self.assert_template_used('upload_and_search.html') self.assertEqual(self.get_context_variable('q'), 'sha1:invalid-hash') self.assertEqual(self.get_context_variable('message'), 'Invalid query!') mock_service.lookup_hash.assert_called_once_with('sha1:invalid-hash') + @patch('swh.web.ui.views.service') + @patch('swh.web.ui.views.request') + @istest + def search_post_not_found(self, mock_request, mock_service): + # given + mock_request.args = {} + mock_request.method = 'POST' + mock_request.files = dict(filename='foobar') + mock_service.upload_and_search.return_value = {'filename': 'foobar', + 'sha1': 'blahhash', + 'found': False} + + # when + # the mock mock_request completes the post request + rv = self.client.post('/search') + + # then + self.assertEquals(rv.status_code, 200) + self.assertEqual(self.get_context_variable('message'), + 'The file foobar with hash blahhash has not been' + ' found.') + self.assertEqual(self.get_context_variable('filename'), 'foobar') + self.assertEqual(self.get_context_variable('found'), False) + self.assertEqual(self.get_context_variable('sha1'), 'blahhash') + self.assert_template_used('upload_and_search.html') + + mock_service.upload_and_search.called = True + + @patch('swh.web.ui.views.service') + @patch('swh.web.ui.views.request') + @istest + def search_post_found(self, mock_request, mock_service): + # given + mock_request.args = {} + mock_request.method = 'POST' + mock_request.files = dict(filename='foobar') + mock_service.upload_and_search.return_value = {'filename': 'foobar', + 'sha1': 'hash-blah', + 'found': True} + + # when + # the mock mock_request completes the post request + rv = self.client.post('/search') + + # then + self.assertEquals(rv.status_code, 200) + self.assertEqual(self.get_context_variable('message'), + 'The file foobar with hash hash-blah has been found.') + self.assertEqual(self.get_context_variable('filename'), 'foobar') + self.assertEqual(self.get_context_variable('found'), True) + self.assertEqual(self.get_context_variable('sha1'), 'hash-blah') + self.assert_template_used('upload_and_search.html') + + mock_service.upload_and_search.called = True + + @patch('swh.web.ui.views.service') + @patch('swh.web.ui.views.request') + @istest + def search_post_bad_input(self, mock_request, mock_service): + # given + mock_request.method = 'POST' + mock_request.args = {} + mock_request.files = dict(filename='foobar') + mock_service.upload_and_search.side_effect = BadInputExc( + 'Invalid hash') + + # when + # the mock mock_request completes the post request + rv = self.client.post('/search') + + # then + self.assertEquals(rv.status_code, 200) + self.assertEqual(self.get_context_variable('message'), 'Invalid hash') + self.assert_template_used('upload_and_search.html') + + mock_service.upload_and_search.called = True + @patch('swh.web.ui.views.service') @istest def show_content(self, mock_service): # given stub_content_raw = { 'sha1': 'sha1-hash', 'data': b'some-data' } mock_service.lookup_content_raw.return_value = stub_content_raw # when rv = self.client.get('/browse/content/sha1:sha1-hash/raw') self.assertEquals(rv.status_code, 200) self.assert_template_used('display_content.html') self.assertEqual(self.get_context_variable('message'), 'Content sha1-hash') self.assertEqual(self.get_context_variable('content'), stub_content_raw) mock_service.lookup_content_raw.assert_called_once_with( 'sha1:sha1-hash') @patch('swh.web.ui.views.service') @istest def show_content_not_found(self, mock_service): # given mock_service.lookup_content_raw.return_value = None # when rv = self.client.get('/browse/content/sha1:sha1-unknown/raw') self.assertEquals(rv.status_code, 200) self.assert_template_used('display_content.html') self.assertEqual(self.get_context_variable('message'), 'Content with sha1:sha1-unknown not found.') self.assertEqual(self.get_context_variable('content'), None) mock_service.lookup_content_raw.assert_called_once_with( 'sha1:sha1-unknown') @patch('swh.web.ui.views.service') @istest def show_content_invalid_hash(self, mock_service): # given mock_service.lookup_content_raw.side_effect = BadInputExc( 'Invalid hash') # when rv = self.client.get('/browse/content/sha2:sha1-invalid/raw') self.assertEquals(rv.status_code, 200) self.assert_template_used('display_content.html') self.assertEqual(self.get_context_variable('message'), 'Invalid hash') self.assertEqual(self.get_context_variable('content'), None) mock_service.lookup_content_raw.assert_called_once_with( 'sha2:sha1-invalid') @patch('swh.web.ui.views.service') @patch('swh.web.ui.utils') @istest def browse_directory_bad_input(self, mock_utils, mock_service): # given mock_service.lookup_directory.side_effect = BadInputExc('Invalid hash') # when rv = self.client.get('/browse/directory/sha2-invalid') # then self.assertEquals(rv.status_code, 200) self.assert_template_used('directory.html') self.assertEqual(self.get_context_variable('message'), 'Invalid hash') self.assertEqual(self.get_context_variable('files'), []) mock_service.lookup_directory.assert_called_once_with( 'sha2-invalid') @patch('swh.web.ui.views.service') @patch('swh.web.ui.utils') @istest def browse_directory_empty_result(self, mock_utils, mock_service): # given mock_service.lookup_directory.return_value = None # when rv = self.client.get('/browse/directory/some-sha1') # then self.assertEquals(rv.status_code, 200) self.assert_template_used('directory.html') self.assertEqual(self.get_context_variable('message'), 'Directory some-sha1 not found.') self.assertEqual(self.get_context_variable('files'), []) mock_service.lookup_directory.assert_called_once_with( 'some-sha1') @patch('swh.web.ui.views.service') @patch('swh.web.ui.views.utils') @istest def browse_directory(self, mock_utils, mock_service): # given stub_directory_ls = [ {'type': 'dir', 'target': '123', 'name': 'some-dir-name'}, {'type': 'file', 'sha1': '654', 'name': 'some-filename'}, {'type': 'dir', 'target': '987', 'name': 'some-other-dirname'} ] mock_service.lookup_directory.return_value = stub_directory_ls stub_directory_map = [ {'link': '/path/to/url/dir/123', 'name': 'some-dir-name'}, {'link': '/path/to/url/file/654', 'name': 'some-filename'}, {'link': '/path/to/url/dir/987', 'name': 'some-other-dirname'} ] mock_utils.prepare_directory_listing.return_value = stub_directory_map # when rv = self.client.get('/browse/directory/some-sha1') # then print(self.templates) self.assertEquals(rv.status_code, 200) self.assert_template_used('directory.html') self.assertEqual(self.get_context_variable('message'), 'Listing for directory some-sha1:') self.assertEqual(self.get_context_variable('files'), stub_directory_map) mock_service.lookup_directory.assert_called_once_with( 'some-sha1') mock_utils.prepare_directory_listing.assert_called_once_with( stub_directory_ls) @patch('swh.web.ui.views.service') @istest def content_with_origin_content_not_found(self, mock_service): # given mock_service.lookup_hash.return_value = {'found': False} # when rv = self.client.get('/browse/content/sha256:some-sha256') # then self.assertEquals(rv.status_code, 200) self.assert_template_used('content.html') self.assertEqual(self.get_context_variable('message'), 'Hash sha256:some-sha256 was not found.') mock_service.lookup_hash.assert_called_once_with( 'sha256:some-sha256') mock_service.lookup_hash_origin.called = False @patch('swh.web.ui.views.service') @istest def content_with_origin_bad_input(self, mock_service): # given mock_service.lookup_hash.side_effect = BadInputExc('Invalid hash') # when rv = self.client.get('/browse/content/sha256:some-sha256') # then self.assertEquals(rv.status_code, 200) self.assert_template_used('content.html') self.assertEqual( self.get_context_variable('message'), 'Invalid hash') mock_service.lookup_hash.assert_called_once_with( 'sha256:some-sha256') mock_service.lookup_hash_origin.called = False @patch('swh.web.ui.views.service') @istest def content_with_origin(self, mock_service): # given mock_service.lookup_hash.return_value = {'found': True} mock_service.lookup_hash_origin.return_value = { 'origin_type': 'ftp', 'origin_url': '/some/url', 'revision': 'revision-hash', 'branch': 'master', 'path': '/path/to', } # when rv = self.client.get('/browse/content/sha256:some-sha256') # then self.assertEquals(rv.status_code, 200) self.assert_template_used('content.html') self.assertEqual( self.get_context_variable('message'), "The content with hash sha256:some-sha256 has been seen on " + "origin with type 'ftp'\n" + "at url '/some/url'. The revision was identified at " + "'revision-hash' on branch 'master'.\n" + "The file's path referenced was '/path/to'.") mock_service.lookup_hash.assert_called_once_with( 'sha256:some-sha256') mock_service.lookup_hash_origin.assert_called_once_with( 'sha256:some-sha256') - - @istest - def uploadnsearch_get(self): - # when - rv = self.client.get('/uploadnsearch') - - # then - self.assertEquals(rv.status_code, 200) - self.assertEqual(self.get_context_variable('message'), '') - self.assertEqual(self.get_context_variable('filename'), None) - self.assertEqual(self.get_context_variable('found'), None) - self.assert_template_used('upload_and_search.html') - - @patch('swh.web.ui.views.service') - @patch('swh.web.ui.views.request') - @istest - def uploadnsearch_post_not_found(self, mock_request, mock_service): - # given - mock_request.method = 'POST' - mock_request.files = dict(filename='foobar') - mock_service.upload_and_search.return_value = {'filename': 'foobar', - 'sha1': 'blahhash', - 'found': False} - - # when - # the mock mock_request completes the post request - rv = self.client.post('/uploadnsearch') - - # then - self.assertEquals(rv.status_code, 200) - self.assertEqual(self.get_context_variable('message'), - 'The file foobar with hash blahhash has not been' - ' found.') - self.assertEqual(self.get_context_variable('filename'), 'foobar') - self.assertEqual(self.get_context_variable('found'), False) - self.assertEqual(self.get_context_variable('sha1'), 'blahhash') - self.assert_template_used('upload_and_search.html') - - mock_service.upload_and_search.called = True - - @patch('swh.web.ui.views.service') - @patch('swh.web.ui.views.request') - @istest - def uploadnsearch_post_found(self, mock_request, mock_service): - # given - mock_request.method = 'POST' - mock_request.files = dict(filename='foobar') - mock_service.upload_and_search.return_value = {'filename': 'foobar', - 'sha1': 'hash-blah', - 'found': True} - - # when - # the mock mock_request completes the post request - rv = self.client.post('/uploadnsearch') - - # then - self.assertEquals(rv.status_code, 200) - self.assertEqual(self.get_context_variable('message'), - 'The file foobar with hash hash-blah has been found.') - self.assertEqual(self.get_context_variable('filename'), 'foobar') - self.assertEqual(self.get_context_variable('found'), True) - self.assertEqual(self.get_context_variable('sha1'), 'hash-blah') - self.assert_template_used('upload_and_search.html') - - mock_service.upload_and_search.called = True - - @patch('swh.web.ui.views.service') - @patch('swh.web.ui.views.request') - @istest - def uploadnsearch_post_bad_input(self, mock_request, mock_service): - # given - mock_request.method = 'POST' - mock_request.files = dict(filename='foobar') - mock_service.upload_and_search.side_effect = BadInputExc( - 'Invalid hash') - - # when - # the mock mock_request completes the post request - rv = self.client.post('/uploadnsearch') - - # then - self.assertEquals(rv.status_code, 200) - self.assertEqual(self.get_context_variable('message'), 'Invalid hash') - self.assert_template_used('upload_and_search.html') - - mock_service.upload_and_search.called = True diff --git a/swh/web/ui/views.py b/swh/web/ui/views.py index 547649300..e0829f9f8 100644 --- a/swh/web/ui/views.py +++ b/swh/web/ui/views.py @@ -1,218 +1,211 @@ # Copyright (C) 2015 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU Affero General Public License version 3, or any later version # See top-level LICENSE file for more information from flask import render_template, flash, request from flask.ext.api.decorators import set_renderers from flask.ext.api.renderers import HTMLRenderer from swh.core.hashutil import ALGORITHMS from swh.web.ui import service, utils from swh.web.ui.exc import BadInputExc from swh.web.ui.main import app hash_filter_keys = ALGORITHMS @app.route('/') @set_renderers(HTMLRenderer) def homepage(): """Home page """ flash('This Web app is still work in progress, use at your own risk', 'warning') # return redirect(url_for('about')) return render_template('home.html') @app.route('/about') @set_renderers(HTMLRenderer) def about(): return render_template('about.html') -@app.route('/search') +@app.route('/search', methods=['GET', 'POST']) @set_renderers(HTMLRenderer) def search(): """Search for hashes in swh-storage. """ - q = request.args.get('q', '') - env = {'q': q, 'message': ''} - - try: - if q: - r = service.lookup_hash(q) - env['message'] = 'Content with hash %s%sfound!' % ( - q, - ' ' if r['found'] == True else ' not ' - ) - except BadInputExc as e: - env['message'] = str(e) - - return render_template('search.html', **env) - + env = {'filename': None, 'message': '', 'found': None, 'q': ''} -@app.route('/uploadnsearch', methods=['GET', 'POST']) -@set_renderers(HTMLRenderer) -def uploadnsearch(): - """Upload and search for hashes in swh-storage. + q = request.args.get('q', '') + if q: + env = {'q': q, 'message': ''} - """ - env = {'filename': None, 'message': '', 'found': None} + try: + if q: + r = service.lookup_hash(q) + env['message'] = 'Content with hash %s%sfound!' % ( + q, + ' ' if r['found'] == True else ' not ' + ) + env['q'] = q + except BadInputExc as e: + env['message'] = str(e) - if request.method == 'POST': + elif request.method == 'POST': file = request.files['filename'] try: uploaded_content = service.upload_and_search(file) filename = uploaded_content['filename'] sha1 = uploaded_content['sha1'] found = uploaded_content['found'] message = 'The file %s with hash %s has%sbeen found.' % ( filename, sha1, ' ' if found else ' not ') env.update({ 'filename': filename, 'sha1': sha1, 'found': found, 'message': message }) except BadInputExc as e: env['message'] = str(e) return render_template('upload_and_search.html', **env) def _origin_seen(q, data): """Given an origin, compute a message string with the right information. Args: origin: a dictionary with keys: - origin: a dictionary with type and url keys - occurrence: a dictionary with a validity range Returns: Message as a string """ origin_type = data['origin_type'] origin_url = data['origin_url'] revision = data['revision'] branch = data['branch'] path = data['path'] return """The content with hash %s has been seen on origin with type '%s' at url '%s'. The revision was identified at '%s' on branch '%s'. The file's path referenced was '%s'.""" % (q, origin_type, origin_url, revision, branch, path) @app.route('/browse/content/') @set_renderers(HTMLRenderer) def content_with_origin(q): """Show content information. Args: - q: query string of the form with `algo_hash` in sha1, sha1_git, sha256. This means that several different URLs (at least one per HASH_ALGO) will point to the same content sha: the sha with 'hash' format Returns: The content's information at for a given checksum. """ env = {'q': q} try: content = service.lookup_hash(q) if not content.get('found'): message = "Hash %s was not found." % q else: origin = service.lookup_hash_origin(q) message = _origin_seen(q, origin) except BadInputExc as e: # do not like it but do not duplicate code message = str(e) env['message'] = message return render_template('content.html', **env) @app.route('/browse/content//raw') @set_renderers(HTMLRenderer) def show_content(q): """Given a hash and a checksum, display the content's raw data. Args: q is of the form algo_hash:hash with algo_hash in (sha1, sha1_git, sha256) Returns: Information on one possible origin for such content. Raises: BadInputExc in case of unknown algo_hash or bad hash NotFoundExc if the content is not found. """ env = {} try: content = service.lookup_content_raw(q) if content: # FIXME: will break if not utf-8 content['data'] = content['data'].decode('utf-8') message = 'Content %s' % content['sha1'] else: message = 'Content with %s not found.' % q except BadInputExc as e: message = str(e) content = None env['message'] = message env['content'] = content return render_template('display_content.html', **env) @app.route('/browse/directory/') @set_renderers(HTMLRenderer) def browse_directory(sha1_git): """Show directory information. Args: - sha1_git: the directory's sha1 git identifier. Returns: The content's information at sha1_git """ env = {'sha1_git': sha1_git} try: directory_files = service.lookup_directory(sha1_git) if directory_files: message = "Listing for directory %s:" % sha1_git files = utils.prepare_directory_listing(directory_files) else: message = "Directory %s not found." % sha1_git files = [] except BadInputExc as e: # do not like it but do not duplicate code message = str(e) files = [] env['message'] = message env['files'] = files return render_template('directory.html', **env)