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