diff --git a/swh/web/ui/apidoc.py b/swh/web/ui/apidoc.py --- a/swh/web/ui/apidoc.py +++ b/swh/web/ui/apidoc.py @@ -21,6 +21,7 @@ ts = 'timestamp' int = 'integer' + str = 'string' path = 'path' sha1 = 'sha1' uuid = 'uuid' @@ -225,7 +226,8 @@ # Build example endpoint URL if 'args' in env: defaults = {arg['name']: arg['default'] for arg in env['args']} - env['example'] = url_for(f.__name__, **defaults) + example = url_for(f.__name__, **defaults) + env['example'] = re.sub(r'(.*)\?.*', r'\1', example) # Prepare and send to mimetype selector if it's not a doc request if re.match(route_re, request.url) and not kwargs['noargs']: diff --git a/swh/web/ui/backend.py b/swh/web/ui/backend.py --- a/swh/web/ui/backend.py +++ b/swh/web/ui/backend.py @@ -80,17 +80,18 @@ return res[0] -def origin_get(origin_id): - """Return information about the origin with id origin_id. +def origin_get(origin): + """Return information about the origin matching dict origin. Args: - origin_id: origin's identifier + origin: origin's dict with keys either 'id' or + ('type' AND 'url') Returns: Origin information as dict. """ - return main.storage().origin_get({'id': origin_id}) + return main.storage().origin_get(origin) def person_get(person_id): diff --git a/swh/web/ui/renderers.py b/swh/web/ui/renderers.py --- a/swh/web/ui/renderers.py +++ b/swh/web/ui/renderers.py @@ -6,10 +6,10 @@ import re import yaml import json -import sys from docutils.core import publish_parts from docutils.writers.html4css1 import Writer, HTMLTranslator +from inspect import cleandoc from flask import request, Response, render_template from flask import g @@ -136,30 +136,7 @@ api. """ - def trim(docstring): - """Correctly trim triple-quoted docstrings, taking into account - first-line indentation inconsistency. - Source: https://www.python.org/dev/peps/pep-0257/#handling-docstring-indentation # noqa - """ - if not docstring: - return '' - lines = docstring.expandtabs().splitlines() - indent = sys.maxsize - for line in lines[1:]: - stripped = line.lstrip() - if stripped: - indent = min(indent, len(line) - len(stripped)) - trimmed = [lines[0].strip()] - if indent < sys.maxsize: - for line in lines[1:]: - trimmed.append(line[indent:].rstrip()) - while trimmed and not trimmed[-1]: - trimmed.pop() - while trimmed and not trimmed[0]: - trimmed.pop(0) - return '\n'.join(trimmed) - - docstring = trim(docstring) + docstring = cleandoc(docstring) return publish_parts(docstring, writer=DOCSTRING_WRITER)['html_body'] diff --git a/swh/web/ui/service.py b/swh/web/ui/service.py --- a/swh/web/ui/service.py +++ b/swh/web/ui/service.py @@ -73,17 +73,18 @@ return converters.from_origin(origin) -def lookup_origin(origin_id): - """Return information about the origin with id origin_id. +def lookup_origin(origin): + """Return information about the origin matching dict origin. Args: - origin_id as string + origin: origin's dict with keys either 'id' or + ('type' AND 'url') Returns: origin information as dict. """ - return backend.origin_get(origin_id) + return backend.origin_get(origin) def lookup_person(person_id): diff --git a/swh/web/ui/templates/origin.html b/swh/web/ui/templates/origin.html --- a/swh/web/ui/templates/origin.html +++ b/swh/web/ui/templates/origin.html @@ -12,7 +12,7 @@ -
Details on origin {{ origin_id }}: +
Details on origin {{ origin['id'] }}:
@@ -34,7 +34,7 @@
{% endif %}
- + {% endif %} {% endblock %} diff --git a/swh/web/ui/tests/test_backend.py b/swh/web/ui/tests/test_backend.py --- a/swh/web/ui/tests/test_backend.py +++ b/swh/web/ui/tests/test_backend.py @@ -174,7 +174,7 @@ self.storage.content_missing_per_sha1.assert_called_with(sha1s_bin) @istest - def origin_get(self): + def origin_get_by_id(self): # given self.storage.origin_get = MagicMock(return_value={ 'id': 'origin-id', @@ -184,7 +184,7 @@ 'type': 'ftp'}) # when - actual_origin = backend.origin_get('origin-id') + actual_origin = backend.origin_get({'id': 'origin-id'}) # then self.assertEqual(actual_origin, {'id': 'origin-id', @@ -196,6 +196,31 @@ self.storage.origin_get.assert_called_with({'id': 'origin-id'}) @istest + def origin_get_by_type_url(self): + # given + self.storage.origin_get = MagicMock(return_value={ + 'id': 'origin-id', + 'lister': 'uuid-lister', + 'project': 'uuid-project', + 'url': 'ftp://some/url/to/origin', + 'type': 'ftp'}) + + # when + actual_origin = backend.origin_get({'type': 'ftp', + 'url': 'ftp://some/url/to/origin'}) + + # then + self.assertEqual(actual_origin, {'id': 'origin-id', + 'lister': 'uuid-lister', + 'project': 'uuid-project', + 'url': 'ftp://some/url/to/origin', + 'type': 'ftp'}) + + self.storage.origin_get.assert_called_with( + {'type': 'ftp', + 'url': 'ftp://some/url/to/origin'}) + + @istest def person_get(self): # given self.storage.person_get = MagicMock(return_value=[{ diff --git a/swh/web/ui/tests/test_service.py b/swh/web/ui/tests/test_service.py --- a/swh/web/ui/tests/test_service.py +++ b/swh/web/ui/tests/test_service.py @@ -362,7 +362,7 @@ 'type': 'ftp'}) # when - actual_origin = service.lookup_origin('origin-id') + actual_origin = service.lookup_origin({'id': 'origin-id'}) # then self.assertEqual(actual_origin, {'id': 'origin-id', @@ -371,7 +371,7 @@ 'url': 'ftp://some/url/to/origin', 'type': 'ftp'}) - mock_backend.origin_get.assert_called_with('origin-id') + mock_backend.origin_get.assert_called_with({'id': 'origin-id'}) @patch('swh.web.ui.service.backend') @istest diff --git a/swh/web/ui/tests/views/test_api.py b/swh/web/ui/tests/views/test_api.py --- a/swh/web/ui/tests/views/test_api.py +++ b/swh/web/ui/tests/views/test_api.py @@ -298,7 +298,7 @@ } # when - rv = self.app.get('/api/1/search/sha1:blah/') + rv = self.app.get('/api/1/content/search/sha1:blah/') self.assertEquals(rv.status_code, 200) self.assertEquals(rv.mimetype, 'application/json') @@ -320,7 +320,7 @@ } # when - rv = self.app.get('/api/1/search/sha1:halb/', + rv = self.app.get('/api/1/content/search/sha1:halb/', headers={'Accept': 'application/yaml'}) self.assertEquals(rv.status_code, 200) @@ -345,7 +345,7 @@ } # when - rv = self.app.get('/api/1/search/sha1:halb/') + rv = self.app.get('/api/1/content/search/sha1:halb/') self.assertEquals(rv.status_code, 200) self.assertEquals(rv.mimetype, 'application/json') @@ -527,7 +527,7 @@ @patch('swh.web.ui.views.api.service') @istest - def api_origin(self, mock_service): + def api_origin_by_id(self, mock_service): # given stub_origin = { 'id': 1234, @@ -548,7 +548,34 @@ response_data = json.loads(rv.data.decode('utf-8')) self.assertEquals(response_data, stub_origin) - mock_service.lookup_origin.assert_called_with(1234) + mock_service.lookup_origin.assert_called_with({'id': 1234}) + + @patch('swh.web.ui.views.api.service') + @istest + def api_origin_by_type_url(self, mock_service): + # given + stub_origin = { + 'id': 1234, + 'lister': 'uuid-lister-0', + 'project': 'uuid-project-0', + 'url': 'ftp://some/url/to/origin/0', + 'type': 'ftp' + } + mock_service.lookup_origin.return_value = stub_origin + + # when + rv = self.app.get('/api/1/origin/ftp/url/ftp://some/url/to/origin/0/') + + # then + self.assertEquals(rv.status_code, 200) + self.assertEquals(rv.mimetype, 'application/json') + + response_data = json.loads(rv.data.decode('utf-8')) + self.assertEquals(response_data, stub_origin) + + mock_service.lookup_origin.assert_called_with( + {'url': 'ftp://some/url/to/origin/0', + 'type': 'ftp'}) @patch('swh.web.ui.views.api.service') @istest @@ -567,7 +594,7 @@ 'error': 'Origin with id 4321 not found.' }) - mock_service.lookup_origin.assert_called_with(4321) + mock_service.lookup_origin.assert_called_with({'id': 4321}) @patch('swh.web.ui.views.api.service') @istest diff --git a/swh/web/ui/tests/views/test_browse.py b/swh/web/ui/tests/views/test_browse.py --- a/swh/web/ui/tests/views/test_browse.py +++ b/swh/web/ui/tests/views/test_browse.py @@ -43,7 +43,7 @@ @istest def search_default(self): # when - rv = self.client.get('/search/') + rv = self.client.get('/content/search/') self.assertEqual(rv.status_code, 200) self.assertEqual(self.get_context_variable('message'), '') @@ -62,7 +62,7 @@ 'search_stats': {'nbfiles': 1, 'pct': 100}} # when - rv = self.client.get('/search/?q=sha1:456') + rv = self.client.get('/content/search/?q=sha1:456') self.assertEqual(rv.status_code, 200) self.assertEqual(self.get_context_variable('message'), '') @@ -81,7 +81,7 @@ mock_api.api_search.side_effect = BadInputExc('error msg') # when - rv = self.client.get('/search/?q=sha1_git:789') + rv = self.client.get('/content/search/?q=sha1_git:789') self.assertEqual(rv.status_code, 200) self.assertEqual(self.get_context_variable('message'), 'error msg') @@ -102,7 +102,7 @@ 'search_stats': {'nbfiles': 1, 'pct': 100}} # when - rv = self.client.get('/search/?q=sha1:123') + rv = self.client.get('/content/search/?q=sha1:123') self.assertEqual(rv.status_code, 200) self.assertEqual(self.get_context_variable('message'), '') @@ -127,7 +127,7 @@ 'error bad input') # when (mock_request completes the post request) - rv = self.client.post('/search/') + rv = self.client.post('/content/search/') # then self.assertEqual(rv.status_code, 200) @@ -156,7 +156,7 @@ 'found': False}]} # when (mock_request completes the post request) - rv = self.client.post('/search/') + rv = self.client.post('/content/search/') # then self.assertEqual(rv.status_code, 200) @@ -193,7 +193,7 @@ 'found': True}]} # when (mock_request completes the post request) - rv = self.client.post('/search/') + rv = self.client.post('/content/search/') # then self.assertEqual(rv.status_code, 200) @@ -551,6 +551,22 @@ class OriginView(test_app.SWHViewTestCase): render_template = False + def setUp(self): + + def url_for_test(fn, **args): + if fn == 'browse_revision_with_origin': + return '/browse/revision/origin/%s/' % args['origin_id'] + elif fn == 'api_origin_visits': + return '/api/1/stat/visits/%s/' % args['origin_id'] + + self.url_for_test = url_for_test + + self.stub_origin = {'type': 'git', + 'lister': None, + 'project': None, + 'url': 'rsync://some/url', + 'id': 426} + @patch('swh.web.ui.views.browse.api') @istest def browse_origin_ko_not_found(self, mock_api): @@ -563,12 +579,12 @@ # then self.assertEqual(rv.status_code, 200) self.assert_template_used('origin.html') - self.assertEqual(self.get_context_variable('origin_id'), 1) + self.assertIsNone(self.get_context_variable('origin')) self.assertEqual( self.get_context_variable('message'), 'Not found!') - mock_api.api_origin.assert_called_once_with(1) + mock_api.api_origin.assert_called_once_with(1, None, None) @patch('swh.web.ui.views.browse.api') @istest @@ -582,28 +598,19 @@ # then self.assertEqual(rv.status_code, 200) self.assert_template_used('origin.html') - self.assertEqual(self.get_context_variable('origin_id'), 426) + self.assertIsNone(self.get_context_variable('origin')) - mock_api.api_origin.assert_called_once_with(426) + mock_api.api_origin.assert_called_once_with(426, None, None) @patch('swh.web.ui.views.browse.api') @patch('swh.web.ui.views.browse.url_for') @istest - def browse_origin_found(self, mock_url_for, mock_api): + def browse_origin_found_id(self, mock_url_for, mock_api): # given - def url_for_test(fn, **args): - if fn == 'browse_revision_with_origin': - return '/browse/revision/origin/%s/' % args['origin_id'] - elif fn == 'api_origin_visits': - return '/api/1/stat/visits/%s/' % args['origin_id'] - mock_url_for.side_effect = url_for_test - mock_origin = {'type': 'git', - 'lister': None, - 'project': None, - 'url': 'rsync://some/url', - 'id': 426} - mock_api.api_origin.return_value = mock_origin + mock_url_for.side_effect = self.url_for_test + + mock_api.api_origin.return_value = self.stub_origin # when rv = self.client.get('/browse/origin/426/') @@ -611,14 +618,38 @@ # then self.assertEqual(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) + self.assertEqual(self.get_context_variable('origin'), self.stub_origin) + self.assertEqual(self.get_context_variable('browse_url'), + '/browse/revision/origin/426/') + self.assertEqual(self.get_context_variable('visit_url'), + '/api/1/stat/visits/426/') + + mock_api.api_origin.assert_called_once_with(426, None, None) + + @patch('swh.web.ui.views.browse.api') + @patch('swh.web.ui.views.browse.url_for') + @istest + def browse_origin_found_url_type(self, mock_url_for, mock_api): + # given + + mock_url_for.side_effect = self.url_for_test + + mock_api.api_origin.return_value = self.stub_origin + + # when + rv = self.client.get('/browse/origin/git/url/rsync://some/url/') + + # then + self.assertEqual(rv.status_code, 200) + self.assert_template_used('origin.html') + self.assertEqual(self.get_context_variable('origin'), self.stub_origin) self.assertEqual(self.get_context_variable('browse_url'), '/browse/revision/origin/426/') self.assertEqual(self.get_context_variable('visit_url'), '/api/1/stat/visits/426/') - mock_api.api_origin.assert_called_once_with(426) + mock_api.api_origin.assert_called_once_with(None, 'git', + 'rsync://some/url') class PersonView(test_app.SWHViewTestCase): diff --git a/swh/web/ui/views/api.py b/swh/web/ui/views/api.py --- a/swh/web/ui/views/api.py +++ b/swh/web/ui/views/api.py @@ -40,9 +40,9 @@ return sorted(date_gen) -@app.route('/api/1/search/', methods=['POST']) -@app.route('/api/1/search//') -@doc.route('/api/1/search/') +@app.route('/api/1/content/search/', methods=['POST']) +@app.route('/api/1/content/search//') +@doc.route('/api/1/content/search/') @doc.arg('q', default='sha1:adc83b19e793491b1c6ea0fd8b46cd9f32e592fc', argtype=doc.argtypes.algo_and_hash, @@ -153,21 +153,46 @@ @app.route('/api/1/origin//') +@app.route('/api/1/origin//url//') @doc.route('/api/1/origin/') @doc.arg('origin_id', default=1, argtype=doc.argtypes.int, argdoc="The origin's SWH origin_id.") +@doc.arg('origin_type', + default='git', + argtype=doc.argtypes.str, + argdoc="The origin's type (git, svn..)") +@doc.arg('origin_url', + default='https://github.com/hylang/hy', + argtype=doc.argtypes.path, + argdoc="The origin's URL.") @doc.raises(exc=doc.excs.notfound, doc='Raised if origin_id does not correspond to an origin in SWH') @doc.returns(rettype=doc.rettypes.dict, retdoc='The metadata of the origin identified by origin_id') -def api_origin(origin_id): - """Return information about origin with id origin_id. +def api_origin(origin_id=None, origin_type=None, origin_url=None): + """Return information about the origin matching the passed criteria. + + Criteria may be: + - An SWH-specific ID, if you already know it + - An origin type and its URL, if you do not have the origin's SWH + identifier """ + ori_dict = { + 'id': origin_id, + 'type': origin_type, + 'url': origin_url + } + ori_dict = {k: v for k, v in ori_dict.items() if ori_dict[k]} + if 'id' in ori_dict: + error_msg = 'Origin with id %s not found.' % ori_dict['id'] + else: + error_msg = 'Origin with type %s and URL %s not found' % ( + ori_dict['type'], ori_dict['url']) return _api_lookup( - origin_id, lookup_fn=service.lookup_origin, - error_msg_if_not_found='Origin with id %s not found.' % origin_id) + ori_dict, lookup_fn=service.lookup_origin, + error_msg_if_not_found=error_msg) @app.route('/api/1/person//') diff --git a/swh/web/ui/views/browse.py b/swh/web/ui/views/browse.py --- a/swh/web/ui/views/browse.py +++ b/swh/web/ui/views/browse.py @@ -27,7 +27,7 @@ return render_template('api.html', **env) -@app.route('/search/', methods=['GET', 'POST']) +@app.route('/content/search/', methods=['GET', 'POST']) def search(): """Search for hashes in swh-storage. @@ -241,22 +241,30 @@ return render_template('directory.html', **env) +@app.route('/browse/origin//url//') @app.route('/browse/origin//') -def browse_origin(origin_id): - """Browse origin with id id. +def browse_origin(origin_id=None, origin_type=None, origin_url=None): + """Browse origin matching given criteria - either origin_id or + origin_type and origin_path. + Args: + - origin_id: origin's swh identifier + - origin_type: origin's type + - origin_url: origin's URL """ - - browse_url = url_for('browse_revision_with_origin', origin_id=origin_id) - visit_url = url_for('api_origin_visits', origin_id=origin_id) - - env = {'browse_url': browse_url, - 'visit_url': visit_url, - 'origin_id': origin_id, + # URLs for the calendar JS plugin + env = {'browse_url': None, + 'visit_url': None, 'origin': None} try: - env['origin'] = api.api_origin(origin_id) + origin = api.api_origin(origin_id, origin_type, origin_url) + env['origin'] = origin + env['browse_url'] = url_for('browse_revision_with_origin', + origin_id=origin['id']) + env['visit_url'] = url_for('api_origin_visits', + origin_id=origin['id']) + except (NotFoundExc, BadInputExc) as e: env['message'] = str(e)