Only complete most recent atime for url.

The history completion query is extended to pick only the most recent item for
a given url.

The tests in test_models now check for ordering of elements.
This commit is contained in:
Ryan Roden-Corrent 2017-04-18 07:48:11 -04:00
parent 9d4888a772
commit 71191f10a2
7 changed files with 107 additions and 56 deletions

View File

@ -187,7 +187,7 @@ def qute_bookmarks(_url):
def history_data(start_time): def history_data(start_time):
"""Return history data """Return history data.
Arguments: Arguments:
start_time -- select history starting from this timestamp. start_time -- select history starting from this timestamp.

View File

@ -30,7 +30,7 @@ class SqlCategory(QSqlQueryModel):
"""Wraps a SqlQuery for use as a completion category.""" """Wraps a SqlQuery for use as a completion category."""
def __init__(self, name, *, sort_by=None, sort_order=None, select='*', def __init__(self, name, *, sort_by=None, sort_order=None, select='*',
where=None, parent=None): where=None, group_by=None, parent=None):
"""Create a new completion category backed by a sql table. """Create a new completion category backed by a sql table.
Args: Args:
@ -46,6 +46,7 @@ class SqlCategory(QSqlQueryModel):
self._sort_order = sort_order self._sort_order = sort_order
self._select = select self._select = select
self._where = where self._where = where
self._group_by = group_by
self.set_pattern('', [0]) self.set_pattern('', [0])
def set_pattern(self, pattern, columns_to_filter): def set_pattern(self, pattern, columns_to_filter):
@ -67,6 +68,9 @@ class SqlCategory(QSqlQueryModel):
if self._where: if self._where:
querystr += ' and ' + self._where querystr += ' and ' + self._where
if self._group_by:
querystr += ' group by {}'.format(self._group_by)
if self._sort_by: if self._sort_by:
assert self._sort_order in ['asc', 'desc'] assert self._sort_order in ['asc', 'desc']
querystr += ' order by {} {}'.format(self._sort_by, querystr += ' order by {} {}'.format(self._sort_by,

View File

@ -74,9 +74,9 @@ def url():
model.add_category(listcategory.ListCategory('Bookmarks', bookmarks)) model.add_category(listcategory.ListCategory('Bookmarks', bookmarks))
timefmt = config.get('completion', 'timestamp-format') timefmt = config.get('completion', 'timestamp-format')
select_time = "strftime('{}', atime, 'unixepoch')".format(timefmt) select_time = "strftime('{}', max(atime), 'unixepoch')".format(timefmt)
hist_cat = sqlcategory.SqlCategory( hist_cat = sqlcategory.SqlCategory(
'History', sort_order='desc', sort_by='atime', 'History', sort_order='desc', sort_by='atime', group_by='url',
select='url, title, {}'.format(select_time), where='not redirect') select='url, title, {}'.format(select_time), where='not redirect')
model.add_category(hist_cat) model.add_category(hist_cat)
return model return model

View File

@ -259,7 +259,7 @@ def bookmark_manager_stub(stubs):
@pytest.fixture @pytest.fixture
def session_manager_stub(stubs): def session_manager_stub(stubs):
"""Fixture which provides a fake web-history object.""" """Fixture which provides a fake session-manager object."""
stub = stubs.SessionManagerStub() stub = stubs.SessionManagerStub()
objreg.register('session-manager', stub) objreg.register('session-manager', stub)
yield stub yield stub

View File

@ -518,24 +518,6 @@ class QuickmarkManagerStub(UrlMarkManagerStub):
self.delete(key) self.delete(key)
class WebHistoryStub(QObject):
"""Stub for the web-history object."""
add_completion_item = pyqtSignal(history.Entry)
cleared = pyqtSignal()
def __init__(self, parent=None):
super().__init__(parent)
self.history_dict = collections.OrderedDict()
def __iter__(self):
return iter(self.history_dict.values())
def __len__(self):
return len(self.history_dict)
class HostBlockerStub: class HostBlockerStub:
"""Stub for the host-blocker object.""" """Stub for the host-blocker object."""

View File

@ -43,18 +43,18 @@ def _check_completions(model, expected):
... ...
} }
""" """
actual = {}
assert model.rowCount() == len(expected) assert model.rowCount() == len(expected)
for i in range(0, model.rowCount()): for i in range(0, model.rowCount()):
catidx = model.index(i, 0) catidx = model.index(i, 0)
catname = model.data(catidx) catname = model.data(catidx)
assert catname in expected actual[catname] = []
expected_cat = expected[catname]
assert model.rowCount(catidx) == len(expected_cat)
for j in range(model.rowCount(catidx)): for j in range(model.rowCount(catidx)):
name = model.data(model.index(j, 0, parent=catidx)) name = model.data(model.index(j, 0, parent=catidx))
desc = model.data(model.index(j, 1, parent=catidx)) desc = model.data(model.index(j, 1, parent=catidx))
misc = model.data(model.index(j, 2, parent=catidx)) misc = model.data(model.index(j, 2, parent=catidx))
assert (name, desc, misc) in expected_cat actual[catname].append((name, desc, misc))
assert actual == expected
# sanity-check the column_widths # sanity-check the column_widths
assert len(model.column_widths) == 3 assert len(model.column_widths) == 3
assert sum(model.column_widths) == 100 assert sum(model.column_widths) == 100
@ -155,18 +155,26 @@ def bookmarks(bookmark_manager_stub):
@pytest.fixture @pytest.fixture
def web_history(stubs, init_sql): def web_history_stub(stubs, init_sql):
return sql.SqlTable("History", ['url', 'title', 'atime', 'redirect'])
@pytest.fixture
def web_history(web_history_stub, init_sql):
"""Pre-populate the web-history database.""" """Pre-populate the web-history database."""
table = sql.SqlTable("History", ['url', 'title', 'atime', 'redirect']) web_history_stub.insert(['http://some-redirect.example.com', 'redirect',
table.insert(['http://some-redirect.example.com', 'redirect', datetime(2016, 9, 5).timestamp(), True])
datetime(2016, 9, 5).timestamp(), True]) web_history_stub.insert(['http://qutebrowser.org', 'qutebrowser',
table.insert(['http://qutebrowser.org', 'qutebrowser', datetime(2015, 9, 5).timestamp(), False])
datetime(2015, 9, 5).timestamp(), False]) web_history_stub.insert(['https://python.org', 'Welcome to Python.org',
table.insert(['https://python.org', 'Welcome to Python.org', datetime(2016, 2, 8).timestamp(), False])
datetime(2016, 3, 8).timestamp(), False]) web_history_stub.insert(['https://python.org', 'Welcome to Python.org',
table.insert(['https://github.com', 'https://github.com', datetime(2016, 3, 8).timestamp(), False])
datetime(2016, 5, 1).timestamp(), False]) web_history_stub.insert(['https://python.org', 'Welcome to Python.org',
return table datetime(2014, 3, 8).timestamp(), False])
web_history_stub.insert(['https://github.com', 'https://github.com',
datetime(2016, 5, 1).timestamp(), False])
return web_history_stub
def test_command_completion(qtmodeltester, monkeypatch, stubs, config_stub, def test_command_completion(qtmodeltester, monkeypatch, stubs, config_stub,
@ -186,15 +194,16 @@ def test_command_completion(qtmodeltester, monkeypatch, stubs, config_stub,
'rr': 'roll', 'rr': 'roll',
'ro': 'rock'}) 'ro': 'rock'})
model = miscmodels.command() model = miscmodels.command()
model.set_pattern('')
qtmodeltester.data_display_may_return_none = True qtmodeltester.data_display_may_return_none = True
qtmodeltester.check(model) qtmodeltester.check(model)
_check_completions(model, { _check_completions(model, {
"Commands": [ "Commands": [
('stop', 'stop qutebrowser', 's'),
('drop', 'drop all user data', ''), ('drop', 'drop all user data', ''),
('roll', 'never gonna give you up', 'rr'),
('rock', "Alias for 'roll'", 'ro'), ('rock', "Alias for 'roll'", 'ro'),
('roll', 'never gonna give you up', 'rr'),
('stop', 'stop qutebrowser', 's'),
] ]
}) })
@ -214,23 +223,24 @@ def test_help_completion(qtmodeltester, monkeypatch, stubs, key_config_stub):
_patch_cmdutils(monkeypatch, stubs, module + '.cmdutils') _patch_cmdutils(monkeypatch, stubs, module + '.cmdutils')
_patch_configdata(monkeypatch, stubs, module + '.configdata.DATA') _patch_configdata(monkeypatch, stubs, module + '.configdata.DATA')
model = miscmodels.helptopic() model = miscmodels.helptopic()
model.set_pattern('')
qtmodeltester.data_display_may_return_none = True qtmodeltester.data_display_may_return_none = True
qtmodeltester.check(model) qtmodeltester.check(model)
_check_completions(model, { _check_completions(model, {
"Commands": [ "Commands": [
(':stop', 'stop qutebrowser', 's'),
(':drop', 'drop all user data', ''), (':drop', 'drop all user data', ''),
(':roll', 'never gonna give you up', 'rr'),
(':hide', '', ''), (':hide', '', ''),
(':roll', 'never gonna give you up', 'rr'),
(':stop', 'stop qutebrowser', 's'),
], ],
"Settings": [ "Settings": [
('general->time', 'Is an illusion.', None), ('general->time', 'Is an illusion.', None),
('general->volume', 'Goes to 11', None), ('general->volume', 'Goes to 11', None),
('searchengines->DEFAULT', '', None),
('ui->gesture', 'Waggle your hands to control qutebrowser', None), ('ui->gesture', 'Waggle your hands to control qutebrowser', None),
('ui->mind', 'Enable mind-control ui (experimental)', None), ('ui->mind', 'Enable mind-control ui (experimental)', None),
('ui->voice', 'Whether to respond to voice commands', None), ('ui->voice', 'Whether to respond to voice commands', None),
('searchengines->DEFAULT', '', None),
] ]
}) })
@ -238,6 +248,7 @@ def test_help_completion(qtmodeltester, monkeypatch, stubs, key_config_stub):
def test_quickmark_completion(qtmodeltester, quickmarks): def test_quickmark_completion(qtmodeltester, quickmarks):
"""Test the results of quickmark completion.""" """Test the results of quickmark completion."""
model = miscmodels.quickmark() model = miscmodels.quickmark()
model.set_pattern('')
qtmodeltester.data_display_may_return_none = True qtmodeltester.data_display_may_return_none = True
qtmodeltester.check(model) qtmodeltester.check(model)
@ -253,14 +264,15 @@ def test_quickmark_completion(qtmodeltester, quickmarks):
def test_bookmark_completion(qtmodeltester, bookmarks): def test_bookmark_completion(qtmodeltester, bookmarks):
"""Test the results of bookmark completion.""" """Test the results of bookmark completion."""
model = miscmodels.bookmark() model = miscmodels.bookmark()
model.set_pattern('')
qtmodeltester.data_display_may_return_none = True qtmodeltester.data_display_may_return_none = True
qtmodeltester.check(model) qtmodeltester.check(model)
_check_completions(model, { _check_completions(model, {
"Bookmarks": [ "Bookmarks": [
('http://qutebrowser.org', 'qutebrowser | qutebrowser', None),
('https://github.com', 'GitHub', None), ('https://github.com', 'GitHub', None),
('https://python.org', 'Welcome to Python.org', None), ('https://python.org', 'Welcome to Python.org', None),
('http://qutebrowser.org', 'qutebrowser | qutebrowser', None),
] ]
}) })
@ -273,22 +285,24 @@ def test_url_completion(qtmodeltester, config_stub, web_history, quickmarks,
- quickmarks, bookmarks, and urls are included - quickmarks, bookmarks, and urls are included
- entries are sorted by access time - entries are sorted by access time
- redirect entries are not included - redirect entries are not included
- only the most recent entry is included for each url
""" """
config_stub.data['completion'] = {'timestamp-format': '%Y-%m-%d'} config_stub.data['completion'] = {'timestamp-format': '%Y-%m-%d'}
model = urlmodel.url() model = urlmodel.url()
model.set_pattern('')
qtmodeltester.data_display_may_return_none = True qtmodeltester.data_display_may_return_none = True
qtmodeltester.check(model) qtmodeltester.check(model)
_check_completions(model, { _check_completions(model, {
"Quickmarks": [ "Quickmarks": [
('https://wiki.archlinux.org', 'aw', None),
('https://duckduckgo.com', 'ddg', None), ('https://duckduckgo.com', 'ddg', None),
('https://wiki.archlinux.org', 'aw', None),
('https://wikipedia.org', 'wiki', None), ('https://wikipedia.org', 'wiki', None),
], ],
"Bookmarks": [ "Bookmarks": [
('http://qutebrowser.org', 'qutebrowser | qutebrowser', None),
('https://github.com', 'GitHub', None), ('https://github.com', 'GitHub', None),
('https://python.org', 'Welcome to Python.org', None), ('https://python.org', 'Welcome to Python.org', None),
('http://qutebrowser.org', 'qutebrowser | qutebrowser', None),
], ],
"History": [ "History": [
('https://github.com', 'https://github.com', '2016-05-01'), ('https://github.com', 'https://github.com', '2016-05-01'),
@ -298,17 +312,48 @@ def test_url_completion(qtmodeltester, config_stub, web_history, quickmarks,
}) })
@pytest.mark.parametrize('url, title, pattern, rowcount', [
('example.com', 'Site Title', '', 1),
('example.com', 'Site Title', 'ex', 1),
('example.com', 'Site Title', 'am', 1),
('example.com', 'Site Title', 'com', 1),
('example.com', 'Site Title', 'ex com', 1),
('example.com', 'Site Title', 'com ex', 0),
('example.com', 'Site Title', 'ex foo', 0),
('example.com', 'Site Title', 'foo com', 0),
('example.com', 'Site Title', 'exm', 0),
('example.com', 'Site Title', 'Si Ti', 1),
('example.com', 'Site Title', 'Ti Si', 0),
('example.com', '', 'foo', 0),
('foo_bar', '', '_', 1),
('foobar', '', '_', 0),
('foo%bar', '', '%', 1),
('foobar', '', '%', 0),
])
def test_url_completion_pattern(config_stub, web_history_stub,
quickmark_manager_stub, bookmark_manager_stub,
url, title, pattern, rowcount):
"""Test that url completion filters by url and title."""
config_stub.data['completion'] = {'timestamp-format': '%Y-%m-%d'}
web_history_stub.insert([url, title, 0, False])
model = urlmodel.url()
model.set_pattern(pattern)
# 2, 0 is History
assert model.rowCount(model.index(2, 0)) == rowcount
def test_url_completion_delete_bookmark(qtmodeltester, config_stub, def test_url_completion_delete_bookmark(qtmodeltester, config_stub,
web_history, quickmarks, bookmarks, web_history, quickmarks, bookmarks,
qtbot): qtbot):
"""Test deleting a bookmark from the url completion model.""" """Test deleting a bookmark from the url completion model."""
config_stub.data['completion'] = {'timestamp-format': '%Y-%m-%d'} config_stub.data['completion'] = {'timestamp-format': '%Y-%m-%d'}
model = urlmodel.url() model = urlmodel.url()
model.set_pattern('')
qtmodeltester.data_display_may_return_none = True qtmodeltester.data_display_may_return_none = True
qtmodeltester.check(model) qtmodeltester.check(model)
# delete item (1, 0) -> (bookmarks, 'https://github.com' ) # delete item (1, 1) -> (bookmarks, 'https://github.com')
view = _mock_view_index(model, 1, 0, qtbot) view = _mock_view_index(model, 1, 1, qtbot)
model.delete_cur_item(view) model.delete_cur_item(view)
assert 'https://github.com' not in bookmarks.marks assert 'https://github.com' not in bookmarks.marks
assert 'https://python.org' in bookmarks.marks assert 'https://python.org' in bookmarks.marks
@ -321,11 +366,12 @@ def test_url_completion_delete_quickmark(qtmodeltester, config_stub,
"""Test deleting a bookmark from the url completion model.""" """Test deleting a bookmark from the url completion model."""
config_stub.data['completion'] = {'timestamp-format': '%Y-%m-%d'} config_stub.data['completion'] = {'timestamp-format': '%Y-%m-%d'}
model = urlmodel.url() model = urlmodel.url()
model.set_pattern('')
qtmodeltester.data_display_may_return_none = True qtmodeltester.data_display_may_return_none = True
qtmodeltester.check(model) qtmodeltester.check(model)
# delete item (0, 1) -> (quickmarks, 'ddg' ) # delete item (0, 0) -> (quickmarks, 'ddg' )
view = _mock_view_index(model, 0, 1, qtbot) view = _mock_view_index(model, 0, 0, qtbot)
model.delete_cur_item(view) model.delete_cur_item(view)
assert 'aw' in quickmarks.marks assert 'aw' in quickmarks.marks
assert 'ddg' not in quickmarks.marks assert 'ddg' not in quickmarks.marks
@ -338,6 +384,7 @@ def test_url_completion_delete_history(qtmodeltester, config_stub,
"""Test that deleting a history entry is a noop.""" """Test that deleting a history entry is a noop."""
config_stub.data['completion'] = {'timestamp-format': '%Y-%m-%d'} config_stub.data['completion'] = {'timestamp-format': '%Y-%m-%d'}
model = urlmodel.url() model = urlmodel.url()
model.set_pattern('')
qtmodeltester.data_display_may_return_none = True qtmodeltester.data_display_may_return_none = True
qtmodeltester.check(model) qtmodeltester.check(model)
@ -350,13 +397,14 @@ def test_url_completion_delete_history(qtmodeltester, config_stub,
def test_session_completion(qtmodeltester, session_manager_stub): def test_session_completion(qtmodeltester, session_manager_stub):
session_manager_stub.sessions = ['default', '1', '2'] session_manager_stub.sessions = ['default', '1', '2']
model = miscmodels.session() model = miscmodels.session()
model.set_pattern('')
qtmodeltester.data_display_may_return_none = True qtmodeltester.data_display_may_return_none = True
qtmodeltester.check(model) qtmodeltester.check(model)
_check_completions(model, { _check_completions(model, {
"Sessions": [('default', None, None), "Sessions": [('1', None, None),
('1', None, None), ('2', None, None),
('2', None, None)] ('default', None, None)]
}) })
@ -371,6 +419,7 @@ def test_tab_completion(qtmodeltester, fake_web_tab, app_stub, win_registry,
fake_web_tab(QUrl('https://wiki.archlinux.org'), 'ArchWiki', 0), fake_web_tab(QUrl('https://wiki.archlinux.org'), 'ArchWiki', 0),
] ]
model = miscmodels.buffer() model = miscmodels.buffer()
model.set_pattern('')
qtmodeltester.data_display_may_return_none = True qtmodeltester.data_display_may_return_none = True
qtmodeltester.check(model) qtmodeltester.check(model)
@ -398,6 +447,7 @@ def test_tab_completion_delete(qtmodeltester, fake_web_tab, qtbot, app_stub,
fake_web_tab(QUrl('https://wiki.archlinux.org'), 'ArchWiki', 0), fake_web_tab(QUrl('https://wiki.archlinux.org'), 'ArchWiki', 0),
] ]
model = miscmodels.buffer() model = miscmodels.buffer()
model.set_pattern('')
qtmodeltester.data_display_may_return_none = True qtmodeltester.data_display_may_return_none = True
qtmodeltester.check(model) qtmodeltester.check(model)
@ -415,14 +465,15 @@ def test_setting_section_completion(qtmodeltester, monkeypatch, stubs):
_patch_config_section_desc(monkeypatch, stubs, _patch_config_section_desc(monkeypatch, stubs,
module + '.configdata.SECTION_DESC') module + '.configdata.SECTION_DESC')
model = configmodel.section() model = configmodel.section()
model.set_pattern('')
qtmodeltester.data_display_may_return_none = True qtmodeltester.data_display_may_return_none = True
qtmodeltester.check(model) qtmodeltester.check(model)
_check_completions(model, { _check_completions(model, {
"Sections": [ "Sections": [
('general', 'General/miscellaneous options.', None), ('general', 'General/miscellaneous options.', None),
('ui', 'General options related to the user interface.', None),
('searchengines', 'Definitions of search engines ...', None), ('searchengines', 'Definitions of search engines ...', None),
('ui', 'General options related to the user interface.', None),
] ]
}) })
@ -435,6 +486,7 @@ def test_setting_option_completion(qtmodeltester, monkeypatch, stubs,
'mind': 'on', 'mind': 'on',
'voice': 'sometimes'}} 'voice': 'sometimes'}}
model = configmodel.option('ui') model = configmodel.option('ui')
model.set_pattern('')
qtmodeltester.data_display_may_return_none = True qtmodeltester.data_display_may_return_none = True
qtmodeltester.check(model) qtmodeltester.check(model)
@ -457,6 +509,7 @@ def test_setting_option_completion_valuelist(qtmodeltester, monkeypatch, stubs,
} }
} }
model = configmodel.option('searchengines') model = configmodel.option('searchengines')
model.set_pattern('')
qtmodeltester.data_display_may_return_none = True qtmodeltester.data_display_may_return_none = True
qtmodeltester.check(model) qtmodeltester.check(model)
@ -471,6 +524,7 @@ def test_setting_value_completion(qtmodeltester, monkeypatch, stubs,
_patch_configdata(monkeypatch, stubs, module + '.configdata.DATA') _patch_configdata(monkeypatch, stubs, module + '.configdata.DATA')
config_stub.data = {'general': {'volume': '0'}} config_stub.data = {'general': {'volume': '0'}}
model = configmodel.value('general', 'volume') model = configmodel.value('general', 'volume')
model.set_pattern('')
qtmodeltester.data_display_may_return_none = True qtmodeltester.data_display_may_return_none = True
qtmodeltester.check(model) qtmodeltester.check(model)
@ -503,16 +557,17 @@ def test_bind_completion(qtmodeltester, monkeypatch, stubs, config_stub,
'rr': 'roll', 'rr': 'roll',
'ro': 'rock'}) 'ro': 'rock'})
model = miscmodels.bind('s') model = miscmodels.bind('s')
model.set_pattern('')
qtmodeltester.data_display_may_return_none = True qtmodeltester.data_display_may_return_none = True
qtmodeltester.check(model) qtmodeltester.check(model)
_check_completions(model, { _check_completions(model, {
"Commands": [ "Commands": [
('stop', 'stop qutebrowser', 's'),
('drop', 'drop all user data', ''), ('drop', 'drop all user data', ''),
('hide', '', ''), ('hide', '', ''),
('roll', 'never gonna give you up', 'rr'),
('rock', "Alias for 'roll'", 'ro'), ('rock', "Alias for 'roll'", 'ro'),
('roll', 'never gonna give you up', 'rr'),
('stop', 'stop qutebrowser', 's'),
] ]
}) })

View File

@ -149,6 +149,16 @@ def test_where():
_validate(cat, [('foo', 'bar', False)]) _validate(cat, [('foo', 'bar', False)])
def test_group():
table = sql.SqlTable('Foo', ['a', 'b'])
table.insert(['foo', 1])
table.insert(['bar', 3])
table.insert(['foo', 2])
table.insert(['bar', 0])
cat = sqlcategory.SqlCategory('Foo', select='a, max(b)', group_by='a')
_validate(cat, [('bar', 3), ('foo', 2)])
def test_entry(): def test_entry():
table = sql.SqlTable('Foo', ['a', 'b', 'c']) table = sql.SqlTable('Foo', ['a', 'b', 'c'])
assert hasattr(table.Entry, 'a') assert hasattr(table.Entry, 'a')