diff --git a/swh/web/ui/templates/origin.html b/swh/web/ui/templates/origin.html
new file mode 100644
index 000000000..b557c19c9
--- /dev/null
+++ b/swh/web/ui/templates/origin.html
@@ -0,0 +1,22 @@
+{% extends "layout.html" %}
+{% block title %}Origin{% endblock %}
+{% block content %}
+
+{% if message is not none %}
+ {{ message }}
+{% endif %}
+
+{% if origin is not none %}
+
Details on origin {{ origin_id }}:
+ {% for key in ['type', 'lister', 'projet', 'url'] %}
+ {% if origin[key] is not none %}
+
+
{{ key }}
+
{{ origin[key] }}
+
+ {% endif %}
+ {% endfor %}
+
+{% endif %}
+
+{% endblock %}
diff --git a/swh/web/ui/tests/test_views.py b/swh/web/ui/tests/test_views.py
index 4dd19375a..ef40b22fb 100644
--- a/swh/web/ui/tests/test_views.py
+++ b/swh/web/ui/tests/test_views.py
@@ -1,433 +1,490 @@
# 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
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('messages'), [])
self.assertEqual(self.get_context_variable('filename'), None)
self.assertEqual(self.get_context_variable('file'), None)
self.assert_template_used('upload_and_search.html')
@patch('swh.web.ui.views.service')
@istest
def search_get_query_hash_not_found(self, mock_service):
# given
mock_service.lookup_hash.return_value = {'found': None}
# when
rv = self.client.get('/search/?q=sha1:456')
self.assertEquals(rv.status_code, 200)
self.assertEqual(self.get_context_variable('q'), 'sha1:456')
self.assertEqual(self.get_context_variable('messages'),
['Content with hash sha1:456 not found!'])
self.assertEqual(self.get_context_variable('filename'), None)
self.assertEqual(self.get_context_variable('file'), None)
self.assert_template_used('upload_and_search.html')
mock_service.lookup_hash.assert_called_once_with('sha1:456')
@patch('swh.web.ui.views.service')
@istest
def search_get_query_hash_bad_input(self, mock_service):
# given
mock_service.lookup_hash.side_effect = BadInputExc('error msg')
# when
rv = self.client.get('/search/?q=sha1_git:789')
self.assertEquals(rv.status_code, 200)
self.assertEqual(self.get_context_variable('q'), 'sha1_git:789')
self.assertEqual(self.get_context_variable('messages'),
['error msg'])
self.assertEqual(self.get_context_variable('filename'), None)
self.assertEqual(self.get_context_variable('file'), None)
self.assert_template_used('upload_and_search.html')
mock_service.lookup_hash.assert_called_once_with('sha1_git:789')
@patch('swh.web.ui.views.service')
@istest
def search_get_query_hash_found(self, mock_service):
# given
mock_service.lookup_hash.return_value = {'found': True}
# when
rv = self.client.get('/search/?q=sha1:123')
self.assertEquals(rv.status_code, 200)
self.assertEqual(self.get_context_variable('q'), 'sha1:123')
self.assertEqual(self.get_context_variable('messages'),
['Content with hash sha1:123 found!'])
self.assertEqual(self.get_context_variable('filename'), None)
self.assertEqual(self.get_context_variable('file'), None)
self.assert_template_used('upload_and_search.html')
mock_service.lookup_hash.assert_called_once_with('sha1:123')
@patch('swh.web.ui.views.service')
@istest
def search_post_query_hash_not_found(self, mock_service):
# given
mock_service.lookup_hash.return_value = {'found': None}
# when
rv = self.client.get('/search/?q=sha1:456')
self.assertEquals(rv.status_code, 200)
self.assertEqual(self.get_context_variable('q'), 'sha1:456')
self.assertEqual(self.get_context_variable('messages'),
['Content with hash sha1:456 not found!'])
self.assertEqual(self.get_context_variable('filename'), None)
self.assertEqual(self.get_context_variable('file'), None)
self.assert_template_used('upload_and_search.html')
mock_service.lookup_hash.assert_called_once_with('sha1:456')
@patch('swh.web.ui.views.service')
@istest
def search_post_query_hash_bad_input(self, mock_service):
# given
mock_service.lookup_hash.side_effect = BadInputExc('error msg!')
# when
rv = self.client.post('/search/', data=dict(q='sha1_git:987'))
self.assertEquals(rv.status_code, 200)
self.assertEqual(self.get_context_variable('q'), 'sha1_git:987')
self.assertEqual(self.get_context_variable('messages'),
['error msg!'])
self.assertEqual(self.get_context_variable('filename'), None)
self.assertEqual(self.get_context_variable('file'), None)
self.assert_template_used('upload_and_search.html')
mock_service.lookup_hash.assert_called_once_with('sha1_git:987')
@patch('swh.web.ui.views.service')
@istest
def search_post_query_hash_found(self, mock_service):
# given
mock_service.lookup_hash.return_value = {'found': True}
# when
rv = self.client.post('/search/', data=dict(q='sha1:321'))
self.assertEquals(rv.status_code, 200)
self.assertEqual(self.get_context_variable('q'), 'sha1:321')
self.assertEqual(self.get_context_variable('messages'),
['Content with hash sha1:321 found!'])
self.assertEqual(self.get_context_variable('filename'), None)
self.assertEqual(self.get_context_variable('file'), None)
self.assert_template_used('upload_and_search.html')
mock_service.lookup_hash.assert_called_once_with('sha1:321')
@patch('swh.web.ui.views.service')
@patch('swh.web.ui.views.request')
@istest
def search_post_upload_and_hash_bad_input(self, mock_request,
mock_service):
# given
mock_request.data = {}
mock_request.method = 'POST'
mock_request.files = dict(filename='foobar')
mock_service.upload_and_search.side_effect = BadInputExc(
'error bad input')
# when (mock_request completes the post request)
rv = self.client.post('/search/')
# then
self.assertEquals(rv.status_code, 200)
self.assertEqual(self.get_context_variable('messages'),
['error bad input'])
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_upload_and_hash_not_found(self, mock_request,
mock_service):
# given
mock_request.data = {}
mock_request.method = 'POST'
mock_request.files = dict(filename='foobar')
mock_service.upload_and_search.return_value = {'filename': 'foobar',
'sha1': 'blahhash',
'found': False}
# when (mock_request completes the post request)
rv = self.client.post('/search/')
# then
self.assertEquals(rv.status_code, 200)
self.assertEqual(self.get_context_variable('messages'),
["File foobar with hash blahhash not found!"])
self.assertEqual(self.get_context_variable('filename'), 'foobar')
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_upload_and_hash_found(self, mock_request, mock_service):
# given
mock_request.data = {}
mock_request.method = 'POST'
mock_request.files = dict(filename='foobar')
mock_service.upload_and_search.return_value = {'filename': 'foobar',
'sha1': '123456789',
'found': True}
# when (mock_request completes the post request)
rv = self.client.post('/search/')
# then
self.assertEquals(rv.status_code, 200)
self.assertEqual(self.get_context_variable('messages'),
["File foobar with hash 123456789 found!"])
self.assertEqual(self.get_context_variable('filename'), 'foobar')
self.assertEqual(self.get_context_variable('sha1'), '123456789')
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')
+
+ @patch('swh.web.ui.views.service')
+ @istest
+ def browse_origin_not_found(self, mock_service):
+ # given
+ mock_service.lookup_origin.return_value = None
+
+ # when
+ rv = self.client.get('/browse/origin/1/')
+
+ # then
+ self.assertEquals(rv.status_code, 200)
+ self.assert_template_used('origin.html')
+ self.assertEqual(self.get_context_variable('origin_id'), 1)
+ self.assertEqual(
+ self.get_context_variable('message'),
+ 'Origin 1 not found!')
+
+ mock_service.lookup_origin.assert_called_once_with(1)
+
+ @patch('swh.web.ui.views.service')
+ @istest
+ def browse_origin_found(self, mock_service):
+ # given
+ mock_origin = {'type': 'git',
+ 'lister': None,
+ 'project': None,
+ 'url': 'rsync://some/url',
+ 'id': 426}
+ mock_service.lookup_origin.return_value = mock_origin
+
+ # when
+ rv = self.client.get('/browse/origin/426/')
+
+ # then
+ self.assertEquals(rv.status_code, 200)
+ self.assert_template_used('origin.html')
+ self.assertEqual(self.get_context_variable('origin_id'), 426)
+ self.assertEqual(self.get_context_variable('origin'), mock_origin)
+
+ mock_service.lookup_origin.assert_called_once_with(426)
+
+ @patch('swh.web.ui.views.service')
+ @istest
+ def browse_origin_bad_input(self, mock_service):
+ # given
+ mock_service.lookup_origin.side_effect = BadInputExc('wrong input')
+
+ # when
+ rv = self.client.get('/browse/origin/426/')
+
+ # then
+ self.assertEquals(rv.status_code, 200)
+ self.assert_template_used('origin.html')
+ self.assertEqual(self.get_context_variable('origin_id'), 426)
+
+ mock_service.lookup_origin.assert_called_once_with(426)
diff --git a/swh/web/ui/views.py b/swh/web/ui/views.py
index b4202b5e3..fbd66222e 100644
--- a/swh/web/ui/views.py
+++ b/swh/web/ui/views.py
@@ -1,237 +1,259 @@
# 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/', methods=['GET', 'POST'])
@set_renderers(HTMLRenderer)
def search():
"""Search for hashes in swh-storage.
One form to submit either:
- hash query to look up in swh storage
- some file content to upload, compute its hash and look it up in swh
storage
- both
Returns:
dict representing data to look for in swh storage.
The following keys are returned:
- file: File submitted for upload
- filename: Filename submitted for upload
- q: Query on hash to look for
- message: Message detailing if data has been found or not.
"""
env = {'filename': None,
'q': None,
'file': None}
data = None
q = env['q']
file = env['file']
if request.method == 'GET':
data = request.args
elif request.method == 'POST':
data = request.data
# could either be a query for sha1 hash
q = data.get('q')
# or hash and search a file
file = request.files.get('filename')
messages = []
if q:
env['q'] = q
try:
r = service.lookup_hash(q)
messages.append('Content with hash %s%sfound!' % (
q, ' ' if r.get('found') else ' not '))
except BadInputExc as e:
messages.append(str(e))
if file:
env['file'] = file
try:
uploaded_content = service.upload_and_search(file)
filename = uploaded_content['filename']
sha1 = uploaded_content['sha1']
found = uploaded_content['found']
messages.append('File %s with hash %s%sfound!' % (
filename, sha1, ' ' if found else ' not '))
env.update({
'filename': filename,
'sha1': sha1,
})
except BadInputExc as e:
messages.append(str(e))
env['q'] = q if q else ''
env['messages'] = messages
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/')
# @app.route('/browse/content//')
@set_renderers(HTMLRenderer)
def content_with_origin(q='sha1:4320781056e5a735a39de0b8c229aea224590052'):
"""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/')
@app.route('/browse/directory//')
@set_renderers(HTMLRenderer)
def browse_directory(sha1_git='828da2b80e41aa958b2c98526f4a1d2cc7d298b7'):
"""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)
+
+
+@app.route('/browse/origin/')
+@app.route('/browse/origin//')
+@set_renderers(HTMLRenderer)
+def browse_origin(origin_id=1):
+ """Browse origin with id id.
+
+ """
+ env = {'origin_id': origin_id,
+ 'origin': None}
+
+ try:
+ ori = service.lookup_origin(origin_id)
+ if ori:
+ env.update({'origin': ori})
+ else:
+ env.update({'message': 'Origin %s not found!' % origin_id})
+ except BadInputExc as e:
+ env.update({'message': str(e)})
+
+ return render_template('origin.html', **env)