diff --git a/debian/control b/debian/control index 164d0f44..64a105e5 100644 --- a/debian/control +++ b/debian/control @@ -1,21 +1,22 @@ Source: swh-web-ui Maintainer: Software Heritage developers Section: python Priority: optional Build-Depends: debhelper (>= 9), dh-python, python3-all, python3-nose, python3-setuptools, python3-swh.core, python3-swh.storage (>= 0.0.19~), - python3-vcversioner + python3-vcversioner, + python3-blinker Standards-Version: 3.9.6 Homepage: https://forge.softwareheritage.org/diffusion/DWUI/ Package: python3-swh.web.ui Architecture: all Depends: python3-swh.storage (>= 0.0.19~), ${misc:Depends}, ${python3:Depends} Description: Software Heritage Web UI diff --git a/requirements.txt b/requirements.txt index ddacba43..5b0e0c1c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,12 +1,13 @@ # Add here external Python modules dependencies, one per line. Module names # should match https://pypi.python.org/pypi names. For the full spec or # dependency lines, see https://pip.readthedocs.org/en/1.1/requirements.html # Runtime dependencies Flask Flask-API swh.core swh.storage >= 0.0.19 # Test dependencies Flask-Testing +blinker diff --git a/swh/web/ui/tests/test_views.py b/swh/web/ui/tests/test_views.py index a57d1e82..f9e5ee20 100644 --- a/swh/web/ui/tests/test_views.py +++ b/swh/web/ui/tests/test_views.py @@ -1,37 +1,92 @@ # 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_1(self): + @istest + def search_default(self): # when rv = self.client.get('/search') - self.assertEquals(rv.status_code, 200) # check this api - self.assertRegexpMatches(rv.data, b'name=q value=>') + 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') + + @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.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.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!') + ) - # @istest - def search_2(self): # when - rv = self.client.get('/search?q=one-hash-to-look-for:another-one') + rv = self.client.get('/search?q=sha1:invalid-hash') + + self.assertEquals(rv.status_code, 200) + self.assert_template_used('search.html') + self.assertEqual(self.get_context_variable('q'), 'sha1:invalid-hash') + self.assertEqual(self.get_context_variable('message'), + 'Invalid query!') - self.assertEquals(rv.status_code, 200) # check this api - self.assertRegexpMatches( - rv.data, - b'name=q value=one-hash-to-look-for:another-one') + mock_service.lookup_hash.assert_called_once_with('sha1:invalid-hash') diff --git a/swh/web/ui/views.py b/swh/web/ui/views.py index e6455268..e34da7f1 100644 --- a/swh/web/ui/views.py +++ b/swh/web/ui/views.py @@ -1,233 +1,233 @@ # 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, url_for 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 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') @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: - env['message'] = 'Error: invalid query string' + q, + ' ' if r['found'] == True else ' not ' + ) + except BadInputExc as e: + env['message'] = str(e) return render_template('search.html', **env) @app.route('/uploadnsearch', methods=['GET', 'POST']) @set_renderers(HTMLRenderer) def uploadnsearch(): """Upload and search for hashes in swh-storage. """ env = {'filename': None, 'message': '', 'found': None} if request.method == 'POST': file = request.files['filename'] try: filename, sha1, found = service.upload_and_search(file) 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: env['message'] = 'Error: invalid query string' return render_template('upload_and_search.html', **env) def _origin_seen(hash, 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 """ if data is None: return 'Content with hash %s is unknown as of now.' % hash 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'.""" % (hash, 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) found = content['found'] if not found: message = "Hash %s was not found." % content['algo'] else: origin = service.lookup_hash_origin(q) message = _origin_seen(hash, origin) except BadInputExc as e: # do not like it but do not duplicate code message = 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 is None: message = 'Content with %s not found.' message = 'Content %s' % content['sha1'] env['content'] = content except BadInputExc as e: message = e env['message'] = message return render_template('display_content.html', **env) def prepare_directory_listing(files): """Given a list of dictionary files, return a view ready dictionary. """ ls = [] for entry in files: new_entry = {} if entry['type'] == 'dir': new_entry['link'] = url_for('browse_directory', sha1_git=entry['target']) else: new_entry['link'] = url_for('show_content', q=entry['sha1']) new_entry['name'] = entry['name'] ls.append(new_entry) return ls @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: files = service.lookup_directory(sha1_git) if not files: message = "Directory %s was not found." % sha1_git else: message = "Listing for directory %s:" % sha1_git env['ls'] = prepare_directory_listing(files) except BadInputExc as e: # do not like it but do not duplicate code message = e env['message'] = message return render_template('directory.html', **env)