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