New qute:history page.
This commit is contained in:
parent
469445e816
commit
845f21b275
@ -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')
|
||||
|
@ -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 <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 %}
|
||||
|
@ -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"
|
||||
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user