|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2009 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing the history filter model. |
|
8 """ |
|
9 |
|
10 from __future__ import unicode_literals |
|
11 |
|
12 from PyQt5.QtCore import Qt, QDateTime, QModelIndex, QAbstractProxyModel |
|
13 |
|
14 from .HistoryModel import HistoryModel |
|
15 |
|
16 |
|
17 class HistoryData(object): |
|
18 """ |
|
19 Class storing some history data. |
|
20 """ |
|
21 def __init__(self, offset, frequency=0): |
|
22 """ |
|
23 Constructor |
|
24 |
|
25 @param offset tail offset (integer) |
|
26 @param frequency frequency (integer) |
|
27 """ |
|
28 self.tailOffset = offset |
|
29 self.frequency = frequency |
|
30 |
|
31 def __eq__(self, other): |
|
32 """ |
|
33 Special method implementing equality. |
|
34 |
|
35 @param other reference to the object to check against (HistoryData) |
|
36 @return flag indicating equality (boolean) |
|
37 """ |
|
38 return self.tailOffset == other.tailOffset and \ |
|
39 (self.frequency == -1 or other.frequency == -1 or |
|
40 self.frequency == other.frequency) |
|
41 |
|
42 def __lt__(self, other): |
|
43 """ |
|
44 Special method determining less relation. |
|
45 |
|
46 Note: Like the actual history entries the index mapping is sorted in |
|
47 reverse order by offset |
|
48 |
|
49 @param other reference to the history data object to compare against |
|
50 (HistoryEntry) |
|
51 @return flag indicating less (boolean) |
|
52 """ |
|
53 return self.tailOffset > other.tailOffset |
|
54 |
|
55 |
|
56 class HistoryFilterModel(QAbstractProxyModel): |
|
57 """ |
|
58 Class implementing the history filter model. |
|
59 """ |
|
60 FrequencyRole = HistoryModel.MaxRole + 1 |
|
61 MaxRole = FrequencyRole |
|
62 |
|
63 def __init__(self, sourceModel, parent=None): |
|
64 """ |
|
65 Constructor |
|
66 |
|
67 @param sourceModel reference to the source model (QAbstractItemModel) |
|
68 @param parent reference to the parent object (QObject) |
|
69 """ |
|
70 super(HistoryFilterModel, self).__init__(parent) |
|
71 |
|
72 self.__loaded = False |
|
73 self.__filteredRows = [] |
|
74 self.__historyDict = {} |
|
75 self.__scaleTime = QDateTime() |
|
76 |
|
77 self.setSourceModel(sourceModel) |
|
78 |
|
79 def historyContains(self, url): |
|
80 """ |
|
81 Public method to check the history for an entry. |
|
82 |
|
83 @param url URL to check for (string) |
|
84 @return flag indicating success (boolean) |
|
85 """ |
|
86 self.__load() |
|
87 return url in self.__historyDict |
|
88 |
|
89 def historyLocation(self, url): |
|
90 """ |
|
91 Public method to get the row number of an entry in the source model. |
|
92 |
|
93 @param url URL to check for (tring) |
|
94 @return row number in the source model (integer) |
|
95 """ |
|
96 self.__load() |
|
97 if url not in self.__historyDict: |
|
98 return 0 |
|
99 |
|
100 return self.sourceModel().rowCount() - self.__historyDict[url] |
|
101 |
|
102 def data(self, index, role=Qt.DisplayRole): |
|
103 """ |
|
104 Public method to get data from the model. |
|
105 |
|
106 @param index index of history entry to get data for (QModelIndex) |
|
107 @param role data role (integer) |
|
108 @return history entry data |
|
109 """ |
|
110 if role == self.FrequencyRole and index.isValid(): |
|
111 return self.__filteredRows[index.row()].frequency |
|
112 |
|
113 return QAbstractProxyModel.data(self, index, role) |
|
114 |
|
115 def setSourceModel(self, sourceModel): |
|
116 """ |
|
117 Public method to set the source model. |
|
118 |
|
119 @param sourceModel reference to the source model (QAbstractItemModel) |
|
120 """ |
|
121 if self.sourceModel() is not None: |
|
122 self.sourceModel().modelReset.disconnect(self.__sourceReset) |
|
123 self.sourceModel().dataChanged.disconnect(self.__sourceDataChanged) |
|
124 self.sourceModel().rowsInserted.disconnect( |
|
125 self.__sourceRowsInserted) |
|
126 self.sourceModel().rowsRemoved.disconnect(self.__sourceRowsRemoved) |
|
127 |
|
128 super(HistoryFilterModel, self).setSourceModel(sourceModel) |
|
129 |
|
130 if self.sourceModel() is not None: |
|
131 self.__loaded = False |
|
132 self.sourceModel().modelReset.connect(self.__sourceReset) |
|
133 self.sourceModel().dataChanged.connect(self.__sourceDataChanged) |
|
134 self.sourceModel().rowsInserted.connect(self.__sourceRowsInserted) |
|
135 self.sourceModel().rowsRemoved.connect(self.__sourceRowsRemoved) |
|
136 |
|
137 def __sourceDataChanged(self, topLeft, bottomRight): |
|
138 """ |
|
139 Private slot to handle the change of data of the source model. |
|
140 |
|
141 @param topLeft index of top left data element (QModelIndex) |
|
142 @param bottomRight index of bottom right data element (QModelIndex) |
|
143 """ |
|
144 self.dataChanged.emit( |
|
145 self.mapFromSource(topLeft), self.mapFromSource(bottomRight)) |
|
146 |
|
147 def headerData(self, section, orientation, role=Qt.DisplayRole): |
|
148 """ |
|
149 Public method to get the header data. |
|
150 |
|
151 @param section section number (integer) |
|
152 @param orientation header orientation (Qt.Orientation) |
|
153 @param role data role (integer) |
|
154 @return header data |
|
155 """ |
|
156 return self.sourceModel().headerData(section, orientation, role) |
|
157 |
|
158 def recalculateFrequencies(self): |
|
159 """ |
|
160 Public method to recalculate the frequencies. |
|
161 """ |
|
162 self.__sourceReset() |
|
163 |
|
164 def __sourceReset(self): |
|
165 """ |
|
166 Private slot to handle a reset of the source model. |
|
167 """ |
|
168 self.beginResetModel() |
|
169 self.__loaded = False |
|
170 self.endResetModel() |
|
171 |
|
172 def rowCount(self, parent=QModelIndex()): |
|
173 """ |
|
174 Public method to determine the number of rows. |
|
175 |
|
176 @param parent index of parent (QModelIndex) |
|
177 @return number of rows (integer) |
|
178 """ |
|
179 self.__load() |
|
180 if parent.isValid(): |
|
181 return 0 |
|
182 return len(self.__historyDict) |
|
183 |
|
184 def columnCount(self, parent=QModelIndex()): |
|
185 """ |
|
186 Public method to get the number of columns. |
|
187 |
|
188 @param parent index of parent (QModelIndex) |
|
189 @return number of columns (integer) |
|
190 """ |
|
191 return self.sourceModel().columnCount(self.mapToSource(parent)) |
|
192 |
|
193 def mapToSource(self, proxyIndex): |
|
194 """ |
|
195 Public method to map an index to the source model index. |
|
196 |
|
197 @param proxyIndex reference to a proxy model index (QModelIndex) |
|
198 @return source model index (QModelIndex) |
|
199 """ |
|
200 self.__load() |
|
201 sourceRow = self.sourceModel().rowCount() - proxyIndex.internalId() |
|
202 return self.sourceModel().index(sourceRow, proxyIndex.column()) |
|
203 |
|
204 def mapFromSource(self, sourceIndex): |
|
205 """ |
|
206 Public method to map an index to the proxy model index. |
|
207 |
|
208 @param sourceIndex reference to a source model index (QModelIndex) |
|
209 @return proxy model index (QModelIndex) |
|
210 """ |
|
211 self.__load() |
|
212 url = sourceIndex.data(HistoryModel.UrlStringRole) |
|
213 if url not in self.__historyDict: |
|
214 return QModelIndex() |
|
215 |
|
216 sourceOffset = self.sourceModel().rowCount() - sourceIndex.row() |
|
217 |
|
218 try: |
|
219 row = self.__filteredRows.index(HistoryData(sourceOffset, -1)) |
|
220 except ValueError: |
|
221 return QModelIndex() |
|
222 |
|
223 return self.createIndex(row, sourceIndex.column(), sourceOffset) |
|
224 |
|
225 def index(self, row, column, parent=QModelIndex()): |
|
226 """ |
|
227 Public method to create an index. |
|
228 |
|
229 @param row row number for the index (integer) |
|
230 @param column column number for the index (integer) |
|
231 @param parent index of the parent item (QModelIndex) |
|
232 @return requested index (QModelIndex) |
|
233 """ |
|
234 self.__load() |
|
235 if row < 0 or row >= self.rowCount(parent) or \ |
|
236 column < 0 or column >= self.columnCount(parent): |
|
237 return QModelIndex() |
|
238 |
|
239 return self.createIndex(row, column, |
|
240 self.__filteredRows[row].tailOffset) |
|
241 |
|
242 def parent(self, index): |
|
243 """ |
|
244 Public method to get the parent index. |
|
245 |
|
246 @param index index of item to get parent (QModelIndex) |
|
247 @return index of parent (QModelIndex) |
|
248 """ |
|
249 return QModelIndex() |
|
250 |
|
251 def __load(self): |
|
252 """ |
|
253 Private method to load the model data. |
|
254 """ |
|
255 if self.__loaded: |
|
256 return |
|
257 |
|
258 self.__filteredRows = [] |
|
259 self.__historyDict = {} |
|
260 self.__scaleTime = QDateTime.currentDateTime() |
|
261 |
|
262 for sourceRow in range(self.sourceModel().rowCount()): |
|
263 idx = self.sourceModel().index(sourceRow, 0) |
|
264 url = idx.data(HistoryModel.UrlStringRole) |
|
265 if url not in self.__historyDict: |
|
266 sourceOffset = self.sourceModel().rowCount() - sourceRow |
|
267 self.__filteredRows.append( |
|
268 HistoryData(sourceOffset, self.__frequencyScore(idx))) |
|
269 self.__historyDict[url] = sourceOffset |
|
270 else: |
|
271 # the url is known already, so just update the frequency score |
|
272 row = self.__filteredRows.index( |
|
273 HistoryData(self.__historyDict[url], -1)) |
|
274 self.__filteredRows[row].frequency += \ |
|
275 self.__frequencyScore(idx) |
|
276 |
|
277 self.__loaded = True |
|
278 |
|
279 def __sourceRowsInserted(self, parent, start, end): |
|
280 """ |
|
281 Private slot to handle the insertion of data in the source model. |
|
282 |
|
283 @param parent reference to the parent index (QModelIndex) |
|
284 @param start start row (integer) |
|
285 @param end end row (integer) |
|
286 """ |
|
287 if start == end and start == 0: |
|
288 if not self.__loaded: |
|
289 return |
|
290 |
|
291 idx = self.sourceModel().index(start, 0, parent) |
|
292 url = idx.data(HistoryModel.UrlStringRole) |
|
293 currentFrequency = 0 |
|
294 if url in self.__historyDict: |
|
295 row = self.__filteredRows.index( |
|
296 HistoryData(self.__historyDict[url], -1)) |
|
297 currentFrequency = self.__filteredRows[row].frequency |
|
298 self.beginRemoveRows(QModelIndex(), row, row) |
|
299 del self.__filteredRows[row] |
|
300 del self.__historyDict[url] |
|
301 self.endRemoveRows() |
|
302 |
|
303 self.beginInsertRows(QModelIndex(), 0, 0) |
|
304 self.__filteredRows.insert( |
|
305 0, HistoryData( |
|
306 self.sourceModel().rowCount(), |
|
307 self.__frequencyScore(idx) + currentFrequency)) |
|
308 self.__historyDict[url] = self.sourceModel().rowCount() |
|
309 self.endInsertRows() |
|
310 |
|
311 def __sourceRowsRemoved(self, parent, start, end): |
|
312 """ |
|
313 Private slot to handle the removal of data in the source model. |
|
314 |
|
315 @param parent reference to the parent index (QModelIndex) |
|
316 @param start start row (integer) |
|
317 @param end end row (integer) |
|
318 """ |
|
319 self.__sourceReset() |
|
320 |
|
321 def removeRows(self, row, count, parent=QModelIndex()): |
|
322 """ |
|
323 Public method to remove entries from the model. |
|
324 |
|
325 @param row row of the first entry to remove (integer) |
|
326 @param count number of entries to remove (integer) |
|
327 @param parent index of the parent entry (QModelIndex) |
|
328 @return flag indicating successful removal (boolean) |
|
329 """ |
|
330 if row < 0 or \ |
|
331 count <= 0 or \ |
|
332 row + count > self.rowCount(parent) or \ |
|
333 parent.isValid(): |
|
334 return False |
|
335 |
|
336 lastRow = row + count - 1 |
|
337 self.sourceModel().rowsRemoved.disconnect(self.__sourceRowsRemoved) |
|
338 self.beginRemoveRows(parent, row, lastRow) |
|
339 oldCount = self.rowCount() |
|
340 start = self.sourceModel().rowCount() - \ |
|
341 self.__filteredRows[row].tailOffset |
|
342 end = self.sourceModel().rowCount() - \ |
|
343 self.__filteredRows[lastRow].tailOffset |
|
344 self.sourceModel().removeRows(start, end - start + 1) |
|
345 self.endRemoveRows() |
|
346 self.sourceModel().rowsRemoved.connect(self.__sourceRowsRemoved) |
|
347 self.__loaded = False |
|
348 if oldCount - count != self.rowCount(): |
|
349 self.beginResetModel() |
|
350 self.endResetModel() |
|
351 return True |
|
352 |
|
353 def __frequencyScore(self, sourceIndex): |
|
354 """ |
|
355 Private method to calculate the frequency score. |
|
356 |
|
357 @param sourceIndex index of the source model (QModelIndex) |
|
358 @return frequency score (integer) |
|
359 """ |
|
360 loadTime = \ |
|
361 self.sourceModel().data(sourceIndex, HistoryModel.DateTimeRole) |
|
362 days = loadTime.daysTo(self.__scaleTime) |
|
363 |
|
364 if days <= 1: |
|
365 return 100 |
|
366 elif days < 8: # within the last week |
|
367 return 90 |
|
368 elif days < 15: # within the last two weeks |
|
369 return 70 |
|
370 elif days < 31: # within the last month |
|
371 return 50 |
|
372 elif days < 91: # within the last 3 months |
|
373 return 30 |
|
374 else: |
|
375 return 10 |