diff --git a/qutebrowser/browser/qutescheme.py b/qutebrowser/browser/qutescheme.py index 018582abf..e5b581fca 100644 --- a/qutebrowser/browser/qutescheme.py +++ b/qutebrowser/browser/qutescheme.py @@ -24,12 +24,12 @@ Module attributes: _HANDLERS: The handlers registered via decorators. """ +import json import sys import time -import datetime import urllib.parse -from PyQt5.QtCore import QUrlQuery +from PyQt5.QtCore import QUrl, QUrlQuery import qutebrowser from qutebrowser.utils import (version, utils, jinja, log, message, docutils, @@ -165,83 +165,77 @@ def qute_bookmarks(_url): @add_handler('history') # noqa def qute_history(url): - """Handler for qute:history. Display history.""" - # Get current date from query parameter, if not given choose today. - curr_date = datetime.date.today() - try: - query_date = QUrlQuery(url).queryItemValue("date") - if query_date: - curr_date = datetime.datetime.strptime(query_date, "%Y-%m-%d") - curr_date = curr_date.date() - except ValueError: - log.misc.debug("Invalid date passed to qute:history: " + query_date) + """Handler for qute:history. Display and serve history.""" + def history_iter(start_time, reverse=False): + """Iterate through the history and get items we're interested. - one_day = datetime.timedelta(days=1) - next_date = curr_date + one_day - prev_date = curr_date - one_day - - def history_iter(reverse): - """Iterate through the history and get items we're interested in.""" - curr_timestamp = time.mktime(curr_date.timetuple()) + Keyword arguments: + reverse -- whether to reverse the history_dict before iterating. + start_time -- select history starting from this timestamp. + """ history = objreg.get('web-history').history_dict.values() if reverse: history = reversed(history) + end_time = start_time - 86400.0 # end is 24hrs earlier than start + for item in history: - # If we can't apply the reverse performance trick below, - # at least continue as early as possible with old items. - # This gets us down from 550ms to 123ms with 500k old items on my - # machine. - if item.atime < curr_timestamp and not reverse: - continue + # Abort/continue as early as possible + item_newer = item.atime > start_time + item_older = item.atime < end_time + if reverse: + # history_dict is reversed, we are going back in history. + # so: + # abort if item is older than start_time+24hr + # skip if item is newer than start + if item_older: + return + if item_newer: + continue + else: + # history_dict is not reversed, we are going forward in history. + # so: + # abort if item is newer than start_time + # skip if item is older than start_time+24hrs + if item_older: + continue + if item_newer: + return - # Convert timestamp - try: - item_atime = datetime.datetime.fromtimestamp(item.atime) - except (ValueError, OSError, OverflowError): - log.misc.debug("Invalid timestamp {}.".format(item.atime)) - continue - - if reverse and item_atime.date() < curr_date: - # If we could reverse the history in-place, and this entry is - # older than today, only older entries will follow, so we can - # abort here. - return - - # Skip items not on curr_date + # Skip items not within start_time and end_time # Skip redirects # Skip qute:// links + is_in_window = item.atime > end_time and item.atime <= start_time is_internal = item.url.scheme() == 'qute' - is_not_today = item_atime.date() != curr_date - if item.redirect or is_internal or is_not_today: + if item.redirect or is_internal or not is_in_window: continue # Use item's url as title if there's no title. item_url = item.url.toDisplayString() item_title = item.title if item.title else item_url - display_atime = item_atime.strftime("%X") + item_time = int(item.atime) - yield (item_url, item_title, display_atime) + yield {"url": item_url, "title": item_title, "time": item_time} - if sys.hexversion >= 0x03050000: - # On Python >= 3.5 we can reverse the ordereddict in-place and thus - # apply an additional performance improvement in history_iter. - # On my machine, this gets us down from 550ms to 72us with 500k old - # items. - history = list(history_iter(reverse=True)) + if QUrl(url).path() == '/data': + # Use start_time in query or current time. + start_time = QUrlQuery(url).queryItemValue("start_time") + start_time = float(start_time) if start_time else time.time() + + if sys.hexversion >= 0x03050000: + # On Python >= 3.5 we can reverse the ordereddict in-place and thus + # apply an additional performance improvement in history_iter. + # On my machine, this gets us down from 550ms to 72us with 500k old + # items. + history = list(history_iter(start_time, reverse=True)) + else: + # On Python 3.4, we can't do that, so we'd need to copy the entire + # history to a list. There, filter first and then reverse it here. + history = reversed(list(history_iter(start_time, reverse=False))) + + return 'text/html', json.dumps(history) else: - # On Python 3.4, we can't do that, so we'd need to copy the entire - # history to a list. There, filter first and then reverse it here. - history = reversed(list(history_iter(reverse=False))) - - html = jinja.render('history.html', - title='History', - history=history, - curr_date=curr_date, - next_date=next_date, - prev_date=prev_date, - today=datetime.date.today()) - return 'text/html', html + return 'text/html', jinja.render('history.html', title='History') @add_handler('pyeval') diff --git a/qutebrowser/html/history.html b/qutebrowser/html/history.html index 94f82182f..ba64316e4 100644 --- a/qutebrowser/html/history.html +++ b/qutebrowser/html/history.html @@ -16,43 +16,165 @@ td.time { white-space: nowrap; } +table { + margin-bottom: 30px; +} + .date { - color: #888; - font-size: 14pt; - padding-left: 25px; -} - -.pagination-link { - display: inline-block; - margin-bottom: 10px; - margin-top: 10px; - padding-right: 10px; -} - -.pagination-link > a { - color: #333; + color: #555; + font-size: 12pt; + padding-bottom: 15px; font-weight: bold; + text-align: left; +} + +.session-separator { + color: #aaa; + height: 40px; + text-align: center; +} + +{% endblock %} + +{% block script %} +/** + * Container for global stuff + */ +var global = { + // The last history item that was seen. + lastItem: null, + // The cutoff interval for session-separator (30 minutes) + SESSION_CUTOFF: 30*60 +}; + +/** + * Finds or creates the session table>tbody to which item with given date + * should be added. + * + * @param {Date} date - the date of the item being added. + */ +var getSessionNode = function(date) { + var histContainer = document.getElementById('hist-container'); + + // Find/create table + var tableId = "hist-" + date.getDate() + date.getMonth() + date.getYear(); + var table = document.getElementById(tableId); + if (table === null) { + table = document.createElement("table"); + table.id = tableId; + + caption = document.createElement("caption"); + caption.className = "date"; + var options = {weekday: 'long', year: 'numeric', month: 'long', day: 'numeric'}; + caption.innerHTML = date.toLocaleDateString('en-US', options); + table.appendChild(caption); + + // Add table to page + histContainer.appendChild(table); + } + + // Find/create tbody + var tbody = table.lastChild; + if (tbody.tagName !== "TBODY") { // this is the caption + tbody = document.createElement("tbody"); + table.appendChild(tbody); + } + + // Create session-separator and new tbody if necessary + if (tbody.lastChild !== null && global.lastItem !== null) { + lastItemDate = new Date(parseInt(global.lastItem.time)*1000); + var interval = (lastItemDate.getTime() - date.getTime())/1000.00; + if (interval > global.SESSION_CUTOFF) { + // Add session-separator + var sessionSeparator = document.createElement('td'); + sessionSeparator.className = "session-separator"; + sessionSeparator.colSpan = 2; + sessionSeparator.innerHTML = "§" + table.appendChild(document.createElement('tr')); + table.lastChild.appendChild(sessionSeparator); + + // Create new tbody + tbody = document.createElement("tbody"); + table.appendChild(tbody); + } + } + + return tbody; +} + +/** + * Given a history item, create and return for it. + * param {string} itemUrl - The url for this item + * param {string} itemTitle - The title for this item + * param {string} itemTime - The formatted time for this item + */ +var makeHistoryRow = function(itemUrl, itemTitle, itemTime) { + var row = document.createElement('tr'); + + var title = document.createElement('td'); + title.className = "title"; + var link = document.createElement('a'); + link.href = itemUrl; + link.innerHTML = itemTitle; + title.appendChild(link); + + var time = document.createElement('td'); + time.className = "time"; + time.innerHTML = itemTime; + + row.appendChild(title); + row.appendChild(time); + + return row; +} + +var getJSON = function(url, callback) { + var xhr = new XMLHttpRequest(); + xhr.open('GET', url, true); + xhr.responseType = 'json'; + xhr.onload = function() { + var status = xhr.status; + callback(status, xhr.response); + }; + xhr.send(); +}; + +/** + * Load new history. + */ +var loadHistory = function() { + url = "qute://history/data"; + if (global.lastItem !== null) { + startTime = parseInt(global.lastItem.time) - 1; + url = "qute://history/data?start_time=" + startTime.toString(); + } + + getJSON(url, function(status, history) { + if (history !== undefined) { + for (item of history) { + atime = new Date(parseInt(item.time)*1000); + var session = getSessionNode(atime); + var row = makeHistoryRow(item.url, item.title, atime.toLocaleTimeString()); + session.appendChild(row) + global.lastItem = item; + } + } + }); } {% endblock %} {% block content %} +

Browsing history

+
+ {% endblock %} diff --git a/tests/end2end/features/history.feature b/tests/end2end/features/history.feature index bc7f537e6..2df4ada21 100644 --- a/tests/end2end/features/history.feature +++ b/tests/end2end/features/history.feature @@ -77,7 +77,7 @@ Feature: Page history Scenario: Listing history When I open data/numbers/3.txt And I open data/numbers/4.txt - And I open qute:history + And I open qute://history/data Then the page should contain the plaintext "3.txt" Then the page should contain the plaintext "4.txt" diff --git a/tests/unit/browser/test_qutescheme.py b/tests/unit/browser/test_qutescheme.py index 5ceecb7f8..6cdeb13f2 100644 --- a/tests/unit/browser/test_qutescheme.py +++ b/tests/unit/browser/test_qutescheme.py @@ -17,8 +17,8 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . -import datetime -import collections +import json +import time from PyQt5.QtCore import QUrl import pytest @@ -27,30 +27,26 @@ from qutebrowser.browser import history, qutescheme from qutebrowser.utils import objreg -Dates = collections.namedtuple('Dates', ['yesterday', 'today', 'tomorrow']) - - class TestHistoryHandler: """Test the qute://history endpoint.""" @pytest.fixture - def dates(self): - one_day = datetime.timedelta(days=1) - today = datetime.datetime.today() - tomorrow = today + one_day - yesterday = today - one_day - return Dates(yesterday, today, tomorrow) + def entries(self): + """Create fake history entries.""" + # create 12 history items spaced 6 hours apart, starting from now + entry_count = 12 + interval = 6 * 60 * 60 + self.now = time.time() - @pytest.fixture - def entries(self, dates): - today = history.Entry(atime=str(dates.today.timestamp()), - url=QUrl('www.today.com'), title='today') - tomorrow = history.Entry(atime=str(dates.tomorrow.timestamp()), - url=QUrl('www.tomorrow.com'), title='tomorrow') - yesterday = history.Entry(atime=str(dates.yesterday.timestamp()), - url=QUrl('www.yesterday.com'), title='yesterday') - return Dates(yesterday, today, tomorrow) + items = [] + for i in range(entry_count): + entry_atime = int(self.now - i * interval) + entry = history.Entry(atime=str(entry_atime), + url=QUrl("www.x.com/" + str(i)), title="Page " + str(i)) + items.insert(0, entry) + + return items @pytest.fixture def fake_web_history(self, fake_save_manager, tmpdir): @@ -62,78 +58,38 @@ class TestHistoryHandler: @pytest.fixture(autouse=True) def fake_history(self, fake_web_history, entries): - """Create fake history for three different days.""" - fake_web_history._add_entry(entries.yesterday) - fake_web_history._add_entry(entries.today) - fake_web_history._add_entry(entries.tomorrow) + """Create fake history.""" + for item in entries: + fake_web_history._add_entry(item) fake_web_history.save() - def test_history_without_query(self): - """Ensure qute://history shows today's history without any query.""" - _mimetype, data = qutescheme.qute_history(QUrl("qute://history")) - key = "{}".format( - datetime.date.today().strftime("%a, %d %B %Y")) - assert key in data - - def test_history_with_bad_query(self): - """Ensure qute://history shows today's history with bad query.""" - url = QUrl("qute://history?date=204-blaah") + @pytest.mark.parametrize("start_time_offset, expected_item_count", [ + (0, 4), + (24*60*60, 4), + (48*60*60, 4), + (72*60*60, 0) + ]) + def test_qutehistory_data(self, start_time_offset, expected_item_count): + """Ensure qute://history/data returns correct items.""" + start_time = int(self.now) - start_time_offset + url = QUrl("qute://history/data?start_time=" + str(start_time)) _mimetype, data = qutescheme.qute_history(url) - key = "{}".format( - datetime.date.today().strftime("%a, %d %B %Y")) - assert key in data + items = json.loads(data) - def test_history_today(self): - """Ensure qute://history shows history for today.""" - url = QUrl("qute://history") - _mimetype, data = qutescheme.qute_history(url) - assert "today" in data - assert "tomorrow" not in data - assert "yesterday" not in data + assert len(items) == expected_item_count - def test_history_yesterday(self, dates): - """Ensure qute://history shows history for yesterday.""" - url = QUrl("qute://history?date=" + - dates.yesterday.strftime("%Y-%m-%d")) - _mimetype, data = qutescheme.qute_history(url) - assert "today" not in data - assert "tomorrow" not in data - assert "yesterday" in data + end_time = start_time - 24*60*60 + for item in items: + assert item['time'] <= start_time + assert item['time'] > end_time - def test_history_tomorrow(self, dates): - """Ensure qute://history shows history for tomorrow.""" - url = QUrl("qute://history?date=" + - dates.tomorrow.strftime("%Y-%m-%d")) - _mimetype, data = qutescheme.qute_history(url) - assert "today" not in data - assert "tomorrow" in data - assert "yesterday" not in data - - def test_no_next_link_to_future(self, dates): - """Ensure there's no next link pointing to the future.""" - url = QUrl("qute://history") - _mimetype, data = qutescheme.qute_history(url) - assert "Next" not in data - - url = QUrl("qute://history?date=" + - dates.tomorrow.strftime("%Y-%m-%d")) - _mimetype, data = qutescheme.qute_history(url) - assert "Next" not in data - - def test_qute_history_benchmark(self, dates, entries, fake_web_history, - benchmark): - for i in range(100000): + def test_qute_history_benchmark(self, fake_web_history, benchmark): + for t in range(100000): # one history per second entry = history.Entry( - atime=str(dates.yesterday.timestamp()), - url=QUrl('www.yesterday.com/{}'.format(i)), - title='yesterday') + atime=str(self.now - t), + url=QUrl('www.x.com/{}'.format(t)), + title='x at {}'.format(t)) fake_web_history._add_entry(entry) - fake_web_history._add_entry(entries.today) - fake_web_history._add_entry(entries.tomorrow) - url = QUrl("qute://history") + url = QUrl("qute://history/data?start_time={}".format(self.now)) _mimetype, data = benchmark(qutescheme.qute_history, url) - - assert "today" in data - assert "tomorrow" not in data - assert "yesterday" not in data