12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091 |
- ########################################################################
- # Searx-Qt - Lightweight desktop application for Searx.
- # Copyright (C) 2020-2022 CYBERDEViL
- #
- # This file is part of Searx-Qt.
- #
- # Searx-Qt is free software: you can redistribute it and/or modify
- # it under the terms of the GNU General Public License as published by
- # the Free Software Foundation, either version 3 of the License, or
- # (at your option) any later version.
- #
- # Searx-Qt is distributed in the hope that it will be useful,
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- # GNU General Public License for more details.
- #
- # You should have received a copy of the GNU General Public License
- # along with this program. If not, see <https://www.gnu.org/licenses/>.
- #
- ########################################################################
- from copy import deepcopy
- import random
- from operator import itemgetter
- from PyQt5.QtCore import (
- QObject,
- pyqtSignal,
- QAbstractTableModel,
- QTimer,
- QVariant,
- Qt
- )
- from searxqt.core.instances import Instance, Stats2
- from searxqt.core.engines import Stats2Engines, EnginesModel
- from searxqt.core.instanceVersions import (
- InstanceVersion,
- VersionFlags
- )
- from searxqt.utils.string import boolToStr, listToStr
- from searxqt.utils.time import nowInMinutes
- from searxqt.thread import Thread, ThreadManagerProto
- from searxqt.translations import _, timeToString
- from searxqt.core import log
- class InstancesModelTypes:
- NotDefined = 0
- Stats2 = 1
- User = 2
- class PersistentEnginesModel(EnginesModel, QObject):
- changed = pyqtSignal()
- def __init__(self, enginesModel=None, parent=None):
- EnginesModel.__init__(self)
- QObject.__init__(self, parent)
- self._currentModel = None
- if enginesModel:
- self.setModel(enginesModel)
- def hasModel(self):
- return False if self._currentModel is None else True
- def setModel(self, enginesModel):
- if self._currentModel:
- self._currentModel.deleteLater()
- self._currentModel.changed.disconnect(self.changed)
- self._currentModel = enginesModel
- self._currentModel.changed.connect(self.changed)
- self._data = self._currentModel.data()
- self.changed.emit()
- class UserEnginesModel(EnginesModel, QObject):
- changed = pyqtSignal()
- def __init__(self, handler, parent=None):
- QObject.__init__(self, parent)
- EnginesModel.__init__(self, handler)
- handler.changed.connect(self.changed)
- class Stats2EnginesModel(Stats2Engines, QObject):
- changed = pyqtSignal()
- def __init__(self, handler, parent=None):
- """
- @param handler: Object containing engines data.
- @type handler: searxqt.models.instances.Stats2Model
- """
- QObject.__init__(self, parent)
- Stats2Engines.__init__(self, handler)
- handler.changed.connect(self.changed)
- class Stats2Model(Stats2, ThreadManagerProto):
- changed = pyqtSignal()
- # int core.requests.ErrorType, str errorMsg
- updateFinished = pyqtSignal(int, str)
- def __init__(self, requestsHandler, parent=None):
- """
- @param requestsHandler:
- @type requestsHandler: core.requests.RequestsHandler
- """
- Stats2.__init__(self, requestsHandler)
- ThreadManagerProto.__init__(self, parent=parent)
- # ThreadManagerProto override
- def currentJobStr(self):
- if self.hasActiveJobs():
- url = self.URL
- return _(f"<b>Updating data from:</b> {url}")
- return ""
- def setData(self, data):
- Stats2.setData(self, data)
- self.changed.emit()
- def updateInstances(self):
- if self._thread:
- log.warning('Instances already being updated.', self)
- return False
- self._thread = Thread(
- Stats2.updateInstances,
- args=[self],
- parent=self
- )
- self._thread.finished.connect(
- self.__updateInstancesThreadFinished
- )
- self.threadStarted.emit()
- self._thread.start()
- return True
- def __updateInstancesThreadFinished(self):
- result = self._thread.result()
- if result:
- log.info('Instances updated!', self)
- self.changed.emit()
- else:
- log.error(f'Updating instances failed! Error: {result.error()}', self)
- self._thread.finished.disconnect(
- self.__updateInstancesThreadFinished
- )
- # Wait before deleting because the `finished` signal is emited
- # from the thread itself, so this method could be called before the
- # thread is actually finished and result in a crash.
- self._thread.wait()
- self._thread.deleteLater()
- self._thread = None
- self.threadFinished.emit()
- self.updateFinished.emit(result.errorType(), result.error())
- class InstanceModel(Instance):
- def __init__(self, url, data, parent=None):
- Instance.__init__(self, url, data)
- class UserInstanceModel(InstanceModel, QObject):
- def __init__(self, url, data, parent=None):
- QObject.__init__(self, parent=parent)
- InstanceModel.__init__(self, url, data, parent=parent)
- @property
- def lastUpdated(self):
- return self._data.get('lastUpdated', 0)
- class Stats2InstanceModel(InstanceModel, QObject):
- def __init__(self, url, data, parent=None):
- QObject.__init__(self, parent=parent)
- InstanceModel.__init__(self, url, data, parent=parent)
- @property
- def analytics(self):
- """ When this is True, the instance has known tracking.
- @return: True when instance has known tracking, False otherwise.
- @rtype: bool
- """
- return self._data.get('analytics', False)
- class InstancesModel(QObject):
- InstanceType = InstanceModel
- Type = InstancesModelTypes.NotDefined
- changed = pyqtSignal()
- def __init__(self, handler=None, parent=None):
- """
- """
- QObject.__init__(self, parent=parent)
- self._instances = {}
- self._modelHandler = handler
- if handler:
- self._instances = handler.instances
- handler.changed.connect(self.changed)
- def __contains__(self, url):
- return bool(url in self._instances)
- def __getitem__(self, url):
- return self.InstanceType(url, self._instances[url])
- def __str__(self): return str([url for url in self._instances])
- def __repr__(self): return str(self)
- def __len__(self): return len(self._instances)
- def data(self):
- return self._instances
- def items(self):
- return [
- (url, self.InstanceType(url, data))
- for url, data in self._instances.items()
- ]
- def keys(self): return self._instances.keys()
- def values(self):
- return [
- self.InstanceType(url, data)
- for url, data in self._instances.items()
- ]
- def copy(self):
- return self._instances.copy()
- class PersistentInstancesModel(InstancesModel):
- """ This will either hold a Stats2InstancesModel or a UserInstancesModel
- It can be switched during run-time.
- This ensures that no references are made to the underlying model
- outside of this object.
- """
- typeChanged = pyqtSignal(int) # InstancesModelTypes
- def __init__(self, instancesModel=None, parent=None):
- InstancesModel.__init__(self, parent=parent)
- self.__currentModel = None
- self._modelHandler = None # Object that manages the model
- if instancesModel:
- self.setModel(instancesModel)
- def hasModel(self):
- return False if self.__currentModel is None else True
- def hasHandler(self):
- return False if self._modelHandler is None else True
- def handler(self):
- # Do not store references to the returned object!
- return self._modelHandler
- def setModel(self, instancesModel):
- if self.__currentModel:
- self.__currentModel.changed.disconnect(self.changed)
- self.__currentModel.deleteLater()
- self.InstanceType = instancesModel.InstanceType
- self.__currentModel = instancesModel
- self.__currentModel.changed.connect(self.changed)
- self._instances = self.__currentModel.data()
- self._modelHandler = instancesModel._modelHandler
- if self.Type != instancesModel.Type:
- self.Type = instancesModel.Type
- self.typeChanged.emit(self.Type)
- self.changed.emit()
- class UserInstancesModel(InstancesModel, QObject):
- InstanceType = UserInstanceModel
- Type = InstancesModelTypes.User
- def __init__(self, handler, parent=None):
- InstancesModel.__init__(self, handler, parent=parent)
- class Stats2InstancesModel(InstancesModel):
- InstanceType = Stats2InstanceModel
- Type = InstancesModelTypes.Stats2
- def __init__(self, handler, parent=None):
- """
- @param handler:
- @type handler: Stats2Model
- """
- InstancesModel.__init__(self, handler, parent=parent)
- class InstanceModelFilter(QObject):
- changed = pyqtSignal()
- VERSION_FILTER_TEMPLATE = {
- 'min': "", # just the version string
- 'git': True, # Allow git versions by default
- 'dirty': False, # Don't allow dirty versions by default
- 'extra': False, # Don't allow 'extra' versions by default
- 'unknown': False, # Don't allow unknown git commits by default
- 'invalid': False # Don't allow invalid versions by default
- }
- def __init__(self, model, parent=None):
- QObject.__init__(self, parent=parent)
- """
- @type model: searxqt.models.instances.PersistentInstancesModel
- """
- self._model = model
- self._current = model.copy()
- self._filter = {
- 'networkTypes': [],
- 'version': deepcopy(self.VERSION_FILTER_TEMPLATE),
- 'whitelist': [],
- # key: url (str), value: (time (uint), reason (str))
- 'blacklist': {},
- 'asnPrivacy': True,
- 'ipv6': False,
- 'engines': [],
- # A dict for temp blacklisting a instance url. This won't be stored on
- # disk, only in RAM. So on restart of searx-qt this will be empty.
- # It is used to put failing instances on a timeout.
- # key: instance url, value: tuple (QTimer object, str reason)
- 'timeout': {},
- # Skip instances that have analytics set to True
- 'analytics': True
- }
- self._model.changed.connect(self.apply)
- @property
- def timeoutList(self):
- return self._filter['timeout']
- @property
- def blacklist(self):
- return self._filter['blacklist']
- @property
- def whitelist(self):
- return self._filter['whitelist']
- def __delInstanceFromTimeout(self, url):
- """ Internal method for removing instance from timeout and apply the
- filter to the model afterwards.
- @param url: Instance URL to remove from timeout.
- @type url: str
- """
- self.delInstanceFromTimeout(url)
- self.apply()
- def putInstanceOnTimeout(self, url, duration=0, reason=''):
- """ Put a instance url on a timeout.
- When 'duration' is '0' it won't timeout, and the url will be
- blacklisted until restart of searx-qt or possible manual removal
- from the list.
- @param url: Instance url
- @type url: str
- @param duration: The duration of the blacklist in minutes.
- @type duration: int
- """
- timer = None
- if duration:
- timer = QTimer(self)
- timer.setSingleShot(True)
- timer.timeout.connect(
- lambda url=url: self.__delInstanceFromTimeout(url)
- )
- timer.start(duration * 60000)
- self.timeoutList.update({url: (timer, reason)})
- def delInstanceFromTimeout(self, url):
- """ Remove instance url from timeout.
- @param url: Instance URL to remove from timeout.
- @type url: str
- """
- if self.timeoutList[url][0]:
- # a QTimer is set, delete it.
- self.timeoutList[url][0].deleteLater() # Delete the QTimer.
- del self.timeoutList[url] # Remove from filter.
- def putInstanceOnBlacklist(self, url, reason=''):
- """ Put instance url on blacklist.
- @param url: Instance URL to blacklist.
- @type url: str
- @param reason: Optional reason for the blacklisting.
- @type reason: str
- """
- if url not in self.blacklist:
- self.blacklist.update({url: (nowInMinutes(), reason)})
- def delInstanceFromBlacklist(self, url):
- """ Delete instance url from blacklist.
- @param url: Instance URL remove from blacklist.
- @type url: str
- """
- del self.blacklist[url]
- def putInstanceOnWhitelist(self, url):
- """ Put instance url from whitelist.
- @param url: Instance URL to whitelist.
- @type url: str
- """
- if url not in self.whitelist:
- self.whitelist.append(url)
- def delInstanceFromWhitelist(self, url):
- """ Delete instance url from whitelist.
- @param url: Instance URL remove from whitelist.
- @type url: str
- """
- self.whitelist.remove(url)
- def loadSettings(self, data):
- defaultAsnPrivacy = bool(self._model.Type != InstancesModelTypes.User)
- defaultAnalytics = bool(self._model.Type != InstancesModelTypes.User)
- # Clear the temporary blacklist which maybe populated when switched
- # from profile.
- for timer, reason in self.timeoutList.values():
- if timer:
- timer.deleteLater()
- self.timeoutList.clear()
- # Restore timeouts
- timeouts = data.get('timeout', {})
- for url in timeouts:
- until, reason = timeouts[url]
- delta = until - nowInMinutes()
- if delta > 0:
- self.putInstanceOnTimeout(url, delta, reason)
- self.updateKwargs(
- {
- 'networkTypes': data.get('networkTypes', []),
- 'version': data.get(
- 'version',
- deepcopy(self.VERSION_FILTER_TEMPLATE)
- ),
- 'whitelist': data.get('whitelist', []),
- 'blacklist': data.get('blacklist', {}),
- 'asnPrivacy': data.get('asnPrivacy', defaultAsnPrivacy),
- 'ipv6': data.get('ipv6', False),
- 'analytics': data.get('analytics', defaultAnalytics)
- }
- )
- def saveSettings(self):
- filter_ = self.filter()
- # Store timeouts
- timeout = {}
- for url in self.timeoutList:
- timer, reason = self.timeoutList[url]
- if timer:
- until = nowInMinutes() + int((timer.remainingTime() / 1000) / 60)
- timeout.update({url: (until, reason)})
- return {
- 'networkTypes': filter_['networkTypes'],
- 'version': filter_['version'],
- 'whitelist': self.whitelist,
- 'blacklist': self.blacklist,
- 'asnPrivacy': filter_['asnPrivacy'],
- 'ipv6': filter_['ipv6'],
- 'timeout': timeout,
- 'analytics': filter_['analytics']
- }
- def filter(self): return self._filter
- def parentModel(self): return self._model
- def updateKwargs(self, kwargs, commit=True):
- for key in kwargs:
- if type(self._filter[key]) is dict: # TODO this is stupid
- for key2 in kwargs[key]:
- self._filter[key][key2] = kwargs[key][key2]
- else:
- self._filter[key] = kwargs[key]
- if commit:
- self.apply()
- def apply(self):
- self._current.clear()
- minimumVersion = InstanceVersion(self._filter['version']['min'])
- for url, instance in self._model.items():
- # Skip temporary blacklisted instances.
- if url in self.timeoutList:
- continue
- if url not in self.whitelist: # Url whitelisted
- # Url blacklist
- if self.blacklist:
- if instance.url in self.blacklist:
- continue
- # Stats2 only.
- if self._model.Type == InstancesModelTypes.Stats2:
- # Analytics
- if self._filter['analytics']:
- if instance.analytics:
- continue
- # Network
- if self._filter['networkTypes']:
- if (
- instance.networkType not in
- self._filter['networkTypes']
- ):
- continue
- # Version
- instanceVersion = instance.version
- # Filter out instances with an invalid version, maybe its
- # malformed or the format may be unknown to us.
- if not self._filter['version']['invalid']:
- if not instanceVersion.isValid():
- continue
- ## Minimum version
- if minimumVersion.isValid():
- # Cannot compare date-version with a semantic-version, so
- # filter out the other.
- if instanceVersion.type() != minimumVersion.type():
- continue
- # Filter out instances that don't meet the minimum version.
- if instance.version < minimumVersion:
- continue
- ## Non-development versions
- if not self._filter['version']['git']:
- # Condition where the evaluated instance it's version is a
- # git version (development) and the git checkbox is
- # unchecked, so we want to filter it out.
- if (instanceVersion.flags() & VersionFlags.Git):
- continue
- ## Development versions
- else:
- ## Dirty development versions
- if not self._filter['version']['dirty']:
- # Filter out instances with 'dirty' flag when filter is not
- # enabled.
- if (instanceVersion.flags() & VersionFlags.Dirty):
- continue
- ## Extra development versions
- if not self._filter['version']['extra']:
- # Filter out instances with 'extra' flag when filter is not
- # enabled.
- if (instanceVersion.flags() & VersionFlags.Extra):
- continue
- ## Extra development versions
- if not self._filter['version']['unknown']:
- # Filter out instances with 'unknown' flag when filter is not
- # enabled.
- if (instanceVersion.flags() & VersionFlags.Unknown):
- continue
- # ASN privacy
- if self._filter['asnPrivacy']:
- if instance.network.asnPrivacy != 0:
- continue
- # IPv6
- if self._filter['ipv6']:
- if not instance.network.ipv6:
- continue
- # Engines
- if self._filter['engines']:
- # TODO when engine(s) are set and also a language, we should
- # check if the engine has language support before allowing
- # it.
- #
- # TODO when engine(s) are set and also a time-range we
- # should check if the engine has time-range support.
- #
- # When the user has set specific search engines set to be
- # searched on we filter out all instanes that don't atleast
- # support one of the set engines available.
- found = False
- for engine in self._filter['engines']:
- for e in instance.engines:
- if e.name == engine:
- found = True
- break
- if not found:
- # This instance doesn't have one of the set engines so
- # we filter it out.
- continue
- self._current.update({url: instance})
- self.changed.emit()
- def __contains__(self, url): return bool(url in self._current)
- def __iter__(self): return iter(self._current)
- def __getitem__(self, url): return self._current[url]
- def __str__(self): return str([url for url in self])
- def __repr__(self): return str(self)
- def __len__(self): return len(self._current)
- def items(self): return self._current.items()
- def keys(self): return self._current.keys()
- def values(self): return self._current.values()
- def copy(self): return self._current.copy()
- class InstanceSelecterModel(QObject):
- optionsChanged = pyqtSignal()
- instanceChanged = pyqtSignal(str) # instance url
- def __init__(self, model, parent=None):
- QObject.__init__(self, parent=parent)
- """
- @type model: InstancesModelFilter
- """
- self._model = model
- self._currentInstanceUrl = ''
- self._model.changed.connect(self.__modelChanged)
- def __modelChanged(self):
- """ This can happen after example blacklisting all instances.
- """
- if self.currentUrl and self.currentUrl not in self._model:
- self.currentUrl = ""
- @property
- def currentUrl(self): return self._currentInstanceUrl
- @currentUrl.setter
- def currentUrl(self, url):
- self._currentInstanceUrl = url
- self.instanceChanged.emit(url)
- def loadSettings(self, data):
- self.currentUrl = data.get('currentInstance', '')
- self.instanceChanged.emit(self.currentUrl)
- def saveSettings(self):
- return {
- 'currentInstance': self.currentUrl
- }
- def getRandomInstances(self, amount=10):
- """ Returns a list of random instance urls.
- """
- return random.sample(list(self._model.keys()),
- min(amount, len(self._model.keys())))
- def randomInstance(self):
- if self._model.keys():
- self.currentUrl = random.choice(list(self._model.keys()))
- return self.currentUrl
- class EnginesTableModel(QAbstractTableModel):
- """ Model used to display engines with their data in a QTableView and
- for adding/removing engines to/from categories.
- """
- def __init__(self, enginesModel, parent):
- """
- @param enginesModel: Contains data about all engines.
- @type enginesModel: searxqt.models.instances.EnginesModel
- """
- QAbstractTableModel.__init__(self, parent)
- self._model = enginesModel # contains all engines
- self._userModel = None # see self.setUserModel method
- self._columns = [
- _('Enabled'),
- _('Name'),
- _('Categories'),
- _('Language support'),
- _('Paging'),
- _('SafeSearch'),
- _('Shortcut'),
- _('Time-range support')
- ]
- self._keyIndex = []
- self._catFilter = ""
- self._sort = (0, None)
- self.__genKeyIndexes()
- def setUserModel(self, model):
- """
- @param model: User category model
- @type model: searxqt.models.search.UserCategoryModel
- """
- self.layoutAboutToBeChanged.emit()
- self._userModel = model
- self.layoutChanged.emit()
- self.reSort()
- def __genKeyIndexes(self):
- self._keyIndex.clear()
- if self._catFilter:
- self._keyIndex = [
- key for key, engine in self._model.items()
- if self._catFilter in engine.categories
- ]
- else:
- self._keyIndex = list(self._model.keys())
- def setCatFilter(self, catKey=""):
- """ Filter engines on category.
- """
- self.layoutAboutToBeChanged.emit()
- self._catFilter = catKey
- self.__genKeyIndexes()
- self.reSort()
- self.layoutChanged.emit()
- def getValueByKey(self, key, columnIndex):
- if columnIndex == 0:
- if self._userModel:
- return boolToStr(bool(key in self._userModel.engines))
- return boolToStr(False)
- elif columnIndex == 1:
- return key
- elif columnIndex == 2:
- return listToStr(self._model[key].categories)
- elif columnIndex == 3:
- return boolToStr(self._model[key].languageSupport)
- elif columnIndex == 4:
- return boolToStr(self._model[key].paging)
- elif columnIndex == 5:
- return boolToStr(self._model[key].safesearch)
- elif columnIndex == 6:
- return self._model[key].shortcut
- elif columnIndex == 7:
- return boolToStr(self._model[key].timeRangeSupport)
- def __sort(self, columnIndex, order=Qt.AscendingOrder):
- unsortedList = [
- [key, self.getValueByKey(key, columnIndex)]
- for key in self._keyIndex
- ]
- reverse = False if order == Qt.AscendingOrder else True
- sortedList = sorted(
- unsortedList,
- key=itemgetter(1),
- reverse=reverse
- )
- self._keyIndex.clear()
- for key, value in sortedList:
- self._keyIndex.append(key)
- def reSort(self):
- if self._sort is not None:
- self.sort(self._sort[0], self._sort[1])
- """ QAbstractTableModel reimplementations below
- """
- def rowCount(self, parent): return len(self._keyIndex)
- def columnCount(self, parent):
- return len(self._columns)
- def headerData(self, col, orientation, role):
- if orientation == Qt.Horizontal and role == Qt.DisplayRole:
- return QVariant(self._columns[col])
- return QVariant()
- def sort(self, columnIndex, order=Qt.AscendingOrder):
- self.layoutAboutToBeChanged.emit()
- self._sort = (columnIndex, order) # backup current sorting
- self.__sort(columnIndex, order=order)
- self.layoutChanged.emit()
- def setData(self, index, value, role):
- if not index.isValid():
- return False
- if role == Qt.CheckStateRole:
- if self._userModel is not None:
- key = self._keyIndex[index.row()]
- if value:
- self._userModel.addEngine(key)
- else:
- self._userModel.removeEngine(key)
- self.reSort()
- return True
- return False
- def data(self, index, role):
- if not index.isValid():
- return QVariant()
- if role == Qt.DisplayRole:
- key = self._keyIndex[index.row()]
- return self.getValueByKey(key, index.column())
- elif index.column() == 0 and role == Qt.CheckStateRole:
- if self._userModel is not None:
- key = self._keyIndex[index.row()]
- if key in self._userModel.engines:
- return Qt.Checked
- return Qt.Unchecked
- return QVariant()
- def flags(self, index):
- flags = (
- Qt.ItemIsSelectable |
- Qt.ItemIsEnabled |
- Qt.ItemNeverHasChildren
- )
- if index.column() == 0:
- flags = flags | Qt.ItemIsUserCheckable
- return flags
- class InstanceTableModel(QAbstractTableModel):
- """ `InstancesModel` -> `QAbstractTableModel` adapter model
- """
- class Column:
- def __init__(self, name, route, type_):
- self._name = name
- self._route = route
- self._type = type_
- @property
- def type(self): return self._type
- @property
- def name(self): return self._name
- @property
- def route(self): return self._route
- def __init__(self, instancesModel, parent):
- """
- @param instancesModel: Resource model
- @type instancesModel: InstancesModel
- """
- QAbstractTableModel.__init__(self, parent)
- self._model = instancesModel
- self._currentModelType = instancesModel.parentModel().Type
- self._keyIndex = [] # [key, key, ..]
- self.__currentSorting = (0, Qt.AscendingOrder)
- self._columns = [
- InstanceTableModel.Column('url', 'url', str),
- InstanceTableModel.Column('version', 'version', str),
- InstanceTableModel.Column('engines', 'engines', list),
- InstanceTableModel.Column('tls.version', 'tls.version', str),
- InstanceTableModel.Column(
- 'tls.cert.version',
- 'tls.certificate.version',
- int),
- InstanceTableModel.Column(
- 'tls.countryName',
- 'tls.certificate.issuer.countryName',
- str),
- InstanceTableModel.Column(
- 'tls.commonName',
- 'tls.certificate.issuer.commonName',
- str),
- InstanceTableModel.Column(
- 'tls.organizationName',
- 'tls.certificate.issuer.organizationName',
- str),
- InstanceTableModel.Column(
- 'network.asnPrivacy',
- 'network.asnPrivacy',
- str),
- InstanceTableModel.Column(
- 'network.ipv6',
- 'network.ipv6',
- bool),
- InstanceTableModel.Column('network.ips', 'network.ips', dict),
- InstanceTableModel.Column('analytics', 'analytics', bool) # stats2
- ]
- instancesModel.changed.connect(self.__resourceModelChanged)
- instancesModel.parentModel().typeChanged.connect(
- self.__modelTypeChanged
- )
- def __modelTypeChanged(self, newType):
- previousType = self._currentModelType
- if (previousType != InstancesModelTypes.User and
- newType == InstancesModelTypes.User):
- del self._columns[-1]
- self._columns.append(
- InstanceTableModel.Column('lastUpdated', 'lastUpdated', int))
- elif (previousType == InstancesModelTypes.User and
- newType != InstancesModelTypes.User):
- del self._columns[-1]
- self._columns.append(
- InstanceTableModel.Column('analytics', 'analytics', bool))
- self._currentModelType = newType
- def __genKeyIndexes(self):
- self._keyIndex.clear()
- for key in self._model:
- self._keyIndex.append(key)
- def __resourceModelChanged(self):
- self.sort(*self.__currentSorting)
- def getColumns(self): return self._columns
- def getByIndex(self, index):
- """ Returns a Instance it's URL by index.
- @param index: Index of the instance it's url you like to get.
- @type index: int
- @return: Instance url
- @rtype: str
- """
- return self._keyIndex[index]
- def getByUrl(self, url):
- """ Returns a Instancs it's current index by url
- @param url: Url of the instance you want to get the current
- index of.
- @type url: str
- @returns: Instance index.
- @rtype: int
- """
- return self._keyIndex.index(url)
- def getPropertyValueByIndex(self, index, route):
- obj = self._model[self.getByIndex(index)]
- return self.getPropertyValue(obj, route)
- def getPropertyValue(self, obj, route):
- """ Returns the `Instance` it's desired property.
- @param obj: instance object
- @type obj: Instance
- @param route: traversel path to value through properties.
- @type route: str
- """
- routes = route.split('.')
- propValue = None
- for propName in routes:
- propValue = getattr(obj, propName)
- obj = propValue
- return propValue
- """ QAbstractTableModel reimplementations below
- """
- def rowCount(self, parent): return len(self._model)
- def columnCount(self, parent): return len(self._columns)
- def sort(self, col, order=Qt.AscendingOrder):
- self.layoutAboutToBeChanged.emit()
- route = self._columns[col].route
- unsortedList = []
- for url, instance in self._model.items():
- value = str(
- self.getPropertyValue(
- instance,
- route
- )
- )
- unsortedList.append([url, value])
- reverse = False if order == Qt.AscendingOrder else True
- sortedList = sorted(
- unsortedList,
- key=itemgetter(1),
- reverse=reverse
- )
- self._keyIndex.clear()
- for url, value in sortedList:
- self._keyIndex.append(url)
- self.__currentSorting = (col, order)
- self.layoutChanged.emit()
- def headerData(self, col, orientation, role):
- if orientation == Qt.Horizontal and role == Qt.DisplayRole:
- return QVariant(self._columns[col].name)
- return QVariant()
- def data(self, index, role):
- if not index.isValid():
- return QVariant()
- if role == Qt.DisplayRole:
- value = self.getPropertyValueByIndex(
- index.row(),
- self._columns[index.column()].route)
- if index.column() == 1: # version
- return str(value)
- elif index.column() == 2: # engines
- newStr = ''
- for engine in value:
- if newStr:
- newStr += ', {0}'.format(engine.name)
- else:
- newStr = engine.name
- return newStr
- elif index.column() == 10: # ips
- return str(value)
- # stats2 profile type specific
- elif self._model.parentModel().Type == InstancesModelTypes.Stats2:
- if index.column() == 11: # analytics
- return str(value)
- # user profile type specific
- else:
- if index.column() == 11: # userInstances lastUpdated
- return timeToString(value)
- return value
- return QVariant()
|