New qute:history page.

This commit is contained in:
Imran Sobir 2017-02-26 17:07:30 +05:00
parent 469445e816
commit 845f21b275
4 changed files with 251 additions and 179 deletions

View File

@ -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')

View File

@ -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 = "&#167;"
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 <tr> 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 %}
<h1>Browsing history</h1>
<div id="hist-container"></div>
<script>
window.onload = function() {
loadHistory();
<h1>Browsing history <span class="date">{{curr_date.strftime("%a, %d %B %Y")}}</span></h1>
<table>
<tbody>
{% for url, title, time in history %}
<tr>
<td class="title"><a href="{{url}}">{{title}}</a></td>
<td class="time">{{time}}</td>
</tr>
{% endfor %}
</tbody>
</table>
<span class="pagination-link"><a href="qute://history/?date={{prev_date.strftime("%Y-%m-%d")}}" rel="prev">Previous</a></span>
{% if today >= next_date %}
<span class="pagination-link"><a href="qute://history/?date={{next_date.strftime("%Y-%m-%d")}}" rel="next">Next</a></span>
{% endif %}
window.onscroll = function(ev) {
if ((window.innerHeight + window.scrollY) >= document.body.scrollHeight) {
loadHistory();
}
};
};
</script>
{% endblock %}

View File

@ -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"

View File

@ -17,8 +17,8 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
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 = "<span class=\"date\">{}</span>".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 = "<span class=\"date\">{}</span>".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