1# -*- coding: utf-8 -*-
2
3# Copyright (c) 2009 - 2021 Detlev Offenbach <detlev@die-offenbachs.de>
4#
5
6"""
7Module implementing the history filter model.
8"""
9
10from PyQt5.QtCore import Qt, QDateTime, QModelIndex, QAbstractProxyModel
11
12from .HistoryModel import HistoryModel
13
14
15class HistoryData:
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 (
37            self.tailOffset == other.tailOffset and
38            (self.frequency == -1 or other.frequency == -1 or
39             self.frequency == other.frequency)
40        )
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
56class 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().__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.ItemDataRole.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().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,
148                   role=Qt.ItemDataRole.DisplayRole):
149        """
150        Public method to get the header data.
151
152        @param section section number (integer)
153        @param orientation header orientation (Qt.Orientation)
154        @param role data role (Qt.ItemDataRole)
155        @return header data
156        """
157        return self.sourceModel().headerData(section, orientation, role)
158
159    def recalculateFrequencies(self):
160        """
161        Public method to recalculate the frequencies.
162        """
163        self.__sourceReset()
164
165    def __sourceReset(self):
166        """
167        Private slot to handle a reset of the source model.
168        """
169        self.beginResetModel()
170        self.__loaded = False
171        self.endResetModel()
172
173    def rowCount(self, parent=None):
174        """
175        Public method to determine the number of rows.
176
177        @param parent index of parent (QModelIndex)
178        @return number of rows (integer)
179        """
180        if parent is None:
181            parent = QModelIndex()
182
183        self.__load()
184        if parent.isValid():
185            return 0
186        return len(self.__historyDict)
187
188    def columnCount(self, parent=None):
189        """
190        Public method to get the number of columns.
191
192        @param parent index of parent (QModelIndex)
193        @return number of columns (integer)
194        """
195        if parent is None:
196            parent = QModelIndex()
197
198        return self.sourceModel().columnCount(self.mapToSource(parent))
199
200    def mapToSource(self, proxyIndex):
201        """
202        Public method to map an index to the source model index.
203
204        @param proxyIndex reference to a proxy model index (QModelIndex)
205        @return source model index (QModelIndex)
206        """
207        self.__load()
208        sourceRow = self.sourceModel().rowCount() - proxyIndex.internalId()
209        return self.sourceModel().index(sourceRow, proxyIndex.column())
210
211    def mapFromSource(self, sourceIndex):
212        """
213        Public method to map an index to the proxy model index.
214
215        @param sourceIndex reference to a source model index (QModelIndex)
216        @return proxy model index (QModelIndex)
217        """
218        self.__load()
219        url = sourceIndex.data(HistoryModel.UrlStringRole)
220        if url not in self.__historyDict:
221            return QModelIndex()
222
223        sourceOffset = self.sourceModel().rowCount() - sourceIndex.row()
224
225        try:
226            row = self.__filteredRows.index(HistoryData(sourceOffset, -1))
227        except ValueError:
228            return QModelIndex()
229
230        return self.createIndex(row, sourceIndex.column(), sourceOffset)
231
232    def index(self, row, column, parent=None):
233        """
234        Public method to create an index.
235
236        @param row row number for the index (integer)
237        @param column column number for the index (integer)
238        @param parent index of the parent item (QModelIndex)
239        @return requested index (QModelIndex)
240        """
241        if parent is None:
242            parent = QModelIndex()
243
244        self.__load()
245        if (
246            row < 0 or
247            row >= self.rowCount(parent) or
248            column < 0 or
249            column >= self.columnCount(parent)
250        ):
251            return QModelIndex()
252
253        return self.createIndex(row, column,
254                                self.__filteredRows[row].tailOffset)
255
256    def parent(self, index):
257        """
258        Public method to get the parent index.
259
260        @param index index of item to get parent (QModelIndex)
261        @return index of parent (QModelIndex)
262        """
263        return QModelIndex()
264
265    def __load(self):
266        """
267        Private method to load the model data.
268        """
269        if self.__loaded:
270            return
271
272        self.__filteredRows = []
273        self.__historyDict = {}
274        self.__scaleTime = QDateTime.currentDateTime()
275
276        for sourceRow in range(self.sourceModel().rowCount()):
277            idx = self.sourceModel().index(sourceRow, 0)
278            url = idx.data(HistoryModel.UrlStringRole)
279            if url not in self.__historyDict:
280                sourceOffset = self.sourceModel().rowCount() - sourceRow
281                self.__filteredRows.append(
282                    HistoryData(sourceOffset, self.__frequencyScore(idx)))
283                self.__historyDict[url] = sourceOffset
284            else:
285                # the url is known already, so just update the frequency score
286                row = self.__filteredRows.index(
287                    HistoryData(self.__historyDict[url], -1))
288                self.__filteredRows[row].frequency += self.__frequencyScore(
289                    idx)
290
291        self.__loaded = True
292
293    def __sourceRowsInserted(self, parent, start, end):
294        """
295        Private slot to handle the insertion of data in the source model.
296
297        @param parent reference to the parent index (QModelIndex)
298        @param start start row (integer)
299        @param end end row (integer)
300        """
301        if start == end and start == 0:
302            if not self.__loaded:
303                return
304
305            idx = self.sourceModel().index(start, 0, parent)
306            url = idx.data(HistoryModel.UrlStringRole)
307            currentFrequency = 0
308            if url in self.__historyDict:
309                row = self.__filteredRows.index(
310                    HistoryData(self.__historyDict[url], -1))
311                currentFrequency = self.__filteredRows[row].frequency
312                self.beginRemoveRows(QModelIndex(), row, row)
313                del self.__filteredRows[row]
314                del self.__historyDict[url]
315                self.endRemoveRows()
316
317            self.beginInsertRows(QModelIndex(), 0, 0)
318            self.__filteredRows.insert(
319                0, HistoryData(
320                    self.sourceModel().rowCount(),
321                    self.__frequencyScore(idx) + currentFrequency))
322            self.__historyDict[url] = self.sourceModel().rowCount()
323            self.endInsertRows()
324
325    def __sourceRowsRemoved(self, parent, start, end):
326        """
327        Private slot to handle the removal of data in the source model.
328
329        @param parent reference to the parent index (QModelIndex)
330        @param start start row (integer)
331        @param end end row (integer)
332        """
333        self.__sourceReset()
334
335    def removeRows(self, row, count, parent=None):
336        """
337        Public method to remove entries from the model.
338
339        @param row row of the first entry to remove (integer)
340        @param count number of entries to remove (integer)
341        @param parent index of the parent entry (QModelIndex)
342        @return flag indicating successful removal (boolean)
343        """
344        if parent is None:
345            parent = QModelIndex()
346
347        if (
348            row < 0 or
349            count <= 0 or
350            row + count > self.rowCount(parent) or
351            parent.isValid()
352        ):
353            return False
354
355        lastRow = row + count - 1
356        self.sourceModel().rowsRemoved.disconnect(self.__sourceRowsRemoved)
357        self.beginRemoveRows(parent, row, lastRow)
358        oldCount = self.rowCount()
359        start = (
360            self.sourceModel().rowCount() -
361            self.__filteredRows[row].tailOffset
362        )
363        end = (
364            self.sourceModel().rowCount() -
365            self.__filteredRows[lastRow].tailOffset
366        )
367        self.sourceModel().removeRows(start, end - start + 1)
368        self.endRemoveRows()
369        self.sourceModel().rowsRemoved.connect(self.__sourceRowsRemoved)
370        self.__loaded = False
371        if oldCount - count != self.rowCount():
372            self.beginResetModel()
373            self.endResetModel()
374        return True
375
376    def __frequencyScore(self, sourceIndex):
377        """
378        Private method to calculate the frequency score.
379
380        @param sourceIndex index of the source model (QModelIndex)
381        @return frequency score (integer)
382        """
383        loadTime = self.sourceModel().data(
384            sourceIndex, HistoryModel.DateTimeRole)
385        days = loadTime.daysTo(self.__scaleTime)
386
387        if days <= 1:
388            return 100
389        elif days < 8:      # within the last week
390            return 90
391        elif days < 15:     # within the last two weeks
392            return 70
393        elif days < 31:     # within the last month
394            return 50
395        elif days < 91:     # within the last 3 months
396            return 30
397        else:
398            return 10
399