instances.py 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837
  1. ########################################################################
  2. # Searx-qt - Lightweight desktop application for SearX.
  3. # Copyright (C) 2020 CYBERDEViL
  4. #
  5. # This file is part of Searx-qt.
  6. #
  7. # Searx-qt is free software: you can redistribute it and/or modify
  8. # it under the terms of the GNU General Public License as published by
  9. # the Free Software Foundation, either version 3 of the License, or
  10. # (at your option) any later version.
  11. #
  12. # Searx-qt is distributed in the hope that it will be useful,
  13. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. # GNU General Public License for more details.
  16. #
  17. # You should have received a copy of the GNU General Public License
  18. # along with this program. If not, see <https://www.gnu.org/licenses/>.
  19. #
  20. ########################################################################
  21. from urllib.parse import urlparse
  22. import random
  23. from operator import itemgetter
  24. from PyQt5.QtCore import (
  25. QObject,
  26. pyqtSignal,
  27. QAbstractTableModel,
  28. QVariant,
  29. Qt
  30. )
  31. from searxqt.core.instances import Instance, Stats2
  32. from searxqt.core.engines import Stats2Engines, EnginesModel
  33. from searxqt.utils.string import boolToStr, listToStr
  34. from searxqt.thread import Thread, ThreadManagerProto
  35. from searxqt.translations import _, timeToString
  36. class InstancesModelTypes:
  37. NotDefined = 0
  38. Stats2 = 1
  39. User = 2
  40. class PersistentEnginesModel(EnginesModel, QObject):
  41. changed = pyqtSignal()
  42. def __init__(self, enginesModel=None, parent=None):
  43. EnginesModel.__init__(self)
  44. QObject.__init__(self, parent)
  45. self._currentModel = None
  46. if enginesModel:
  47. self.setModel(enginesModel)
  48. def hasModel(self):
  49. return False if self._currentModel is None else True
  50. def setModel(self, enginesModel):
  51. if self._currentModel:
  52. self._currentModel.deleteLater()
  53. self._currentModel.changed.disconnect(self.changed)
  54. self._currentModel = enginesModel
  55. self._currentModel.changed.connect(self.changed)
  56. self._data = self._currentModel.data()
  57. self.changed.emit()
  58. class UserEnginesModel(EnginesModel, QObject):
  59. changed = pyqtSignal()
  60. def __init__(self, handler, parent=None):
  61. QObject.__init__(self, parent)
  62. EnginesModel.__init__(self, handler)
  63. handler.changed.connect(self.changed)
  64. class Stats2EnginesModel(Stats2Engines, QObject):
  65. changed = pyqtSignal()
  66. def __init__(self, handler, parent=None):
  67. """
  68. @param handler: Object containing engines data.
  69. @type handler: searxqt.models.instances.Stats2Model
  70. """
  71. QObject.__init__(self, parent)
  72. Stats2Engines.__init__(self, handler)
  73. handler.changed.connect(self.changed)
  74. class Stats2Model(Stats2, ThreadManagerProto):
  75. changed = pyqtSignal()
  76. def __init__(self, requestsHandler, parent=None):
  77. """
  78. @param requestsHandler:
  79. @type requestsHandler: core.requests.RequestsHandler
  80. """
  81. Stats2.__init__(self, requestsHandler)
  82. ThreadManagerProto.__init__(self, parent=parent)
  83. # ThreadManagerProto override
  84. def currentJobStr(self):
  85. if self.hasActiveJobs():
  86. return _("<b>Updating data from:</b> {0}").format(self.URL)
  87. return ""
  88. def setData(self, data):
  89. Stats2.setData(self, data)
  90. self.changed.emit()
  91. def updateInstances(self):
  92. if self._thread:
  93. return
  94. self._thread = Thread(
  95. Stats2.updateInstances,
  96. args=[self],
  97. parent=self
  98. )
  99. self._thread.finished.connect(
  100. self.__updateInstancesThreadFinished
  101. )
  102. self.threadStarted.emit()
  103. self._thread.start()
  104. def __updateInstancesThreadFinished(self):
  105. if self._thread.result():
  106. self.changed.emit()
  107. self._thread.finished.disconnect(
  108. self.__updateInstancesThreadFinished
  109. )
  110. # Wait before deleting because the `finished` signal is emited
  111. # from the thread itself, so this method could be called before the
  112. # thread is actually finished and result in a crash.
  113. self._thread.wait()
  114. self._thread.deleteLater()
  115. self._thread = None
  116. self.threadFinished.emit()
  117. class InstanceModel(Instance):
  118. def __init__(self, url, data, parent=None):
  119. Instance.__init__(self, url, data)
  120. class UserInstanceModel(InstanceModel, QObject):
  121. def __init__(self, url, data, source='user', parent=None):
  122. QObject.__init__(self, parent=parent)
  123. InstanceModel.__init__(self, url, data, source)
  124. @property
  125. def lastUpdated(self):
  126. return self._data.get('lastUpdated', 0)
  127. class InstancesModel(QObject):
  128. InstanceType = InstanceModel
  129. Type = InstancesModelTypes.NotDefined
  130. changed = pyqtSignal()
  131. def __init__(self, handler=None, parent=None):
  132. """
  133. """
  134. QObject.__init__(self, parent=parent)
  135. self._instances = {}
  136. self._modelHandler = handler
  137. if handler:
  138. self._instances = handler.instances
  139. handler.changed.connect(self.changed)
  140. def __contains__(self, url):
  141. return bool(url in self._instances)
  142. def __getitem__(self, url):
  143. return self.InstanceType(url, self._instances[url])
  144. def __str__(self): return str([url for url in self._instances])
  145. def __repr__(self): return str(self)
  146. def __len__(self): return len(self._instances)
  147. def data(self):
  148. return self._instances
  149. def items(self):
  150. return [
  151. (url, self.InstanceType(url, data))
  152. for url, data in self._instances.items()
  153. ]
  154. def keys(self): return self._instances.keys()
  155. def values(self):
  156. return [
  157. self.InstanceType(url, data)
  158. for url, data in self._instances.items()
  159. ]
  160. def copy(self):
  161. return self._instances.copy()
  162. class PersistentInstancesModel(InstancesModel):
  163. """ This will either hold a Stats2InstancesModel or a UserInstancesModel
  164. It can be switched during run-time.
  165. This ensures that no references are made to the underlying model
  166. outside of this object.
  167. """
  168. typeChanged = pyqtSignal(int) # InstancesModelTypes
  169. def __init__(self, instancesModel=None, parent=None):
  170. InstancesModel.__init__(self, parent=parent)
  171. self.__currentModel = None
  172. self._modelHandler = None # Object that manages the model
  173. if instancesModel:
  174. self.setModel(instancesModel)
  175. def hasModel(self):
  176. return False if self.__currentModel is None else True
  177. def hasHandler(self):
  178. return False if self._modelHandler is None else True
  179. def handler(self):
  180. # Do not store references to the returned object!
  181. return self._modelHandler
  182. def setModel(self, instancesModel):
  183. if self.__currentModel:
  184. self.__currentModel.changed.disconnect(self.changed)
  185. self.__currentModel.deleteLater()
  186. self.InstanceType = instancesModel.InstanceType
  187. self.__currentModel = instancesModel
  188. self.__currentModel.changed.connect(self.changed)
  189. self._instances = self.__currentModel.data()
  190. self._modelHandler = instancesModel._modelHandler
  191. if self.Type != instancesModel.Type:
  192. self.Type = instancesModel.Type
  193. self.typeChanged.emit(self.Type)
  194. self.changed.emit()
  195. class UserInstancesModel(InstancesModel, QObject):
  196. InstanceType = UserInstanceModel
  197. Type = InstancesModelTypes.User
  198. def __init__(self, handler, parent=None):
  199. InstancesModel.__init__(self, handler, parent=parent)
  200. class Stats2InstancesModel(InstancesModel):
  201. Type = InstancesModelTypes.Stats2
  202. def __init__(self, handler, parent=None):
  203. """
  204. @param handler:
  205. @type handler: Stats2Model
  206. """
  207. InstancesModel.__init__(self, handler, parent=parent)
  208. class InstanceModelFilter(QObject):
  209. changed = pyqtSignal()
  210. def __init__(self, model, parent=None):
  211. QObject.__init__(self, parent=parent)
  212. """
  213. @type model: searxqt.models.instances.PersistentInstancesModel
  214. """
  215. self._model = model
  216. self._current = model.copy()
  217. self._filter = {
  218. 'ext': [],
  219. 'skipExt': [],
  220. 'version': [],
  221. 'skipVersion': [],
  222. 'url': [],
  223. 'skipUrl': [],
  224. 'asnPrivacy': True,
  225. 'ipv6': False,
  226. 'engines': []
  227. }
  228. self._model.changed.connect(self.apply)
  229. def loadSettings(self, data):
  230. defaultAsnPrivacy = bool(self._model.Type != InstancesModelTypes.User)
  231. self.updateKwargs(
  232. {
  233. 'ext': data.get('ext', []),
  234. 'skipExt': data.get('skipExt', []),
  235. 'version': data.get('version', []),
  236. 'skipVersion': data.get('skipVersion', []),
  237. 'url': data.get('url', []),
  238. 'skipUrl': data.get('skipUrl', []),
  239. 'asnPrivacy': data.get('asnPrivacy', defaultAsnPrivacy),
  240. 'ipv6': data.get('ipv6', False)
  241. # 'engines': data.get('engines', []) # TODO
  242. }
  243. )
  244. def saveSettings(self):
  245. return self.filter()
  246. def filter(self): return self._filter
  247. def parentModel(self): return self._model
  248. def updateKwargs(self, kwargs, commit=True):
  249. for key in kwargs:
  250. self._filter[key] = kwargs[key]
  251. if commit:
  252. self.apply()
  253. def apply(self):
  254. self._current.clear()
  255. for url, instance in self._model.items():
  256. if url not in self._filter['url']: # Url whitelisted
  257. # Url blacklist
  258. if self._filter['skipUrl']:
  259. if instance.url in self._filter['skipUrl']:
  260. continue
  261. # Network
  262. if self._filter['skipExt'] or self._filter['ext']:
  263. parsedUrl = urlparse(instance.url)
  264. extSplit = parsedUrl.hostname.split('.')
  265. ext = extSplit[(len(extSplit)-1)]
  266. if self._filter['skipExt']:
  267. if (ext in self._filter['skipExt']):
  268. continue
  269. if self._filter['ext']:
  270. if (ext not in self._filter['ext']):
  271. continue
  272. # Version
  273. if self._filter['skipVersion'] or self._filter['version']:
  274. if self._filter['version']:
  275. if instance.version not in self._filter['version']:
  276. continue
  277. # ASN privacy
  278. if self._filter['asnPrivacy']:
  279. if instance.network.asnPrivacy != 0:
  280. continue
  281. # IPv6
  282. if self._filter['ipv6']:
  283. if not instance.network.ipv6:
  284. continue
  285. # Engines
  286. if self._filter['engines']:
  287. found = False
  288. for engine in self._filter['engines']:
  289. for e in instance.engines:
  290. if e.name == engine:
  291. found = True
  292. break
  293. if not found:
  294. continue
  295. self._current.update({url: instance})
  296. self.changed.emit()
  297. def __contains__(self, url): return bool(url in self._current)
  298. def __iter__(self): return iter(self._current)
  299. def __getitem__(self, url): return self._current[url]
  300. def __str__(self): return str([url for url in self])
  301. def __repr__(self): return str(self)
  302. def __len__(self): return len(self._current)
  303. def items(self): return self._current.items()
  304. def keys(self): return self._current.keys()
  305. def values(self): return self._current.values()
  306. def copy(self): return self._current.copy()
  307. class InstanceSelecterModel(QObject):
  308. optionsChanged = pyqtSignal()
  309. instanceChanged = pyqtSignal(str) # instance url
  310. def __init__(self, model, parent=None):
  311. QObject.__init__(self, parent=parent)
  312. """
  313. @type model: InstancesModelFilter
  314. """
  315. self._model = model
  316. self._currentInstanceUrl = ''
  317. self._model.changed.connect(self.__modelChanged)
  318. def __modelChanged(self):
  319. """ This can happen after example blacklisting all instances.
  320. """
  321. if self.currentUrl and self.currentUrl not in self._model:
  322. self.currentUrl = ""
  323. @property
  324. def currentUrl(self): return self._currentInstanceUrl
  325. @currentUrl.setter
  326. def currentUrl(self, url):
  327. self._currentInstanceUrl = url
  328. self.instanceChanged.emit(url)
  329. def loadSettings(self, data):
  330. self.currentUrl = data.get('currentInstance', '')
  331. self.instanceChanged.emit(self.currentUrl)
  332. def saveSettings(self):
  333. return {
  334. 'currentInstance': self.currentUrl
  335. }
  336. def getRandomInstances(self, amount=10):
  337. """ Returns a list of random instance urls.
  338. """
  339. return random.sample(list(self._model.keys()),
  340. min(amount, len(self._model.keys())))
  341. def randomInstance(self):
  342. if self._model.keys():
  343. self.currentUrl = random.choice(list(self._model.keys()))
  344. return self.currentUrl
  345. class EnginesTableModel(QAbstractTableModel):
  346. """ Model used to display engines with their data in a QTableView and
  347. for adding/removing engines to/from categories.
  348. """
  349. def __init__(self, enginesModel, parent):
  350. """
  351. @param enginesModel: Contains data about all engines.
  352. @type enginesModel: searxqt.models.instances.EnginesModel
  353. """
  354. QAbstractTableModel.__init__(self, parent)
  355. self._model = enginesModel # contains all engines
  356. self._userModel = None # see self.setUserModel method
  357. self._columns = [
  358. _('Enabled'),
  359. _('Name'),
  360. _('Categories'),
  361. _('Language support'),
  362. _('Paging'),
  363. _('SafeSearch'),
  364. _('Shortcut'),
  365. _('Time-range support')
  366. ]
  367. self._keyIndex = []
  368. self._catFilter = ""
  369. self._sort = (0, None)
  370. self.__genKeyIndexes()
  371. def setUserModel(self, model):
  372. """
  373. @param model: User category model
  374. @type model: searxqt.models.search.UserCategoryModel
  375. """
  376. self.layoutAboutToBeChanged.emit()
  377. self._userModel = model
  378. self.layoutChanged.emit()
  379. self.reSort()
  380. def __genKeyIndexes(self):
  381. self._keyIndex.clear()
  382. if self._catFilter:
  383. self._keyIndex = [
  384. key for key, engine in self._model.items()
  385. if self._catFilter in engine.categories
  386. ]
  387. else:
  388. self._keyIndex = list(self._model.keys())
  389. def setCatFilter(self, catKey=""):
  390. """ Filter engines on category.
  391. """
  392. self.layoutAboutToBeChanged.emit()
  393. self._catFilter = catKey
  394. self.__genKeyIndexes()
  395. self.reSort()
  396. self.layoutChanged.emit()
  397. def getValueByKey(self, key, columnIndex):
  398. if columnIndex == 0:
  399. if self._userModel:
  400. return boolToStr(bool(key in self._userModel.engines))
  401. return boolToStr(False)
  402. elif columnIndex == 1:
  403. return key
  404. elif columnIndex == 2:
  405. return listToStr(self._model[key].categories)
  406. elif columnIndex == 3:
  407. return boolToStr(self._model[key].languageSupport)
  408. elif columnIndex == 4:
  409. return boolToStr(self._model[key].paging)
  410. elif columnIndex == 5:
  411. return boolToStr(self._model[key].safesearch)
  412. elif columnIndex == 6:
  413. return self._model[key].shortcut
  414. elif columnIndex == 7:
  415. return boolToStr(self._model[key].timeRangeSupport)
  416. def __sort(self, columnIndex, order=Qt.AscendingOrder):
  417. unsortedList = [
  418. [key, self.getValueByKey(key, columnIndex)]
  419. for key in self._keyIndex
  420. ]
  421. reverse = False if order == Qt.AscendingOrder else True
  422. sortedList = sorted(
  423. unsortedList,
  424. key=itemgetter(1),
  425. reverse=reverse
  426. )
  427. self._keyIndex.clear()
  428. for key, value in sortedList:
  429. self._keyIndex.append(key)
  430. def reSort(self):
  431. if self._sort is not None:
  432. self.sort(self._sort[0], self._sort[1])
  433. """ QAbstractTableModel reimplementations below
  434. """
  435. def rowCount(self, parent): return len(self._keyIndex)
  436. def columnCount(self, parent):
  437. return len(self._columns)
  438. def headerData(self, col, orientation, role):
  439. if orientation == Qt.Horizontal and role == Qt.DisplayRole:
  440. return QVariant(self._columns[col])
  441. return QVariant()
  442. def sort(self, columnIndex, order=Qt.AscendingOrder):
  443. self.layoutAboutToBeChanged.emit()
  444. self._sort = (columnIndex, order) # backup current sorting
  445. self.__sort(columnIndex, order=order)
  446. self.layoutChanged.emit()
  447. def setData(self, index, value, role):
  448. if not index.isValid():
  449. return False
  450. if role == Qt.CheckStateRole:
  451. if self._userModel is not None:
  452. key = self._keyIndex[index.row()]
  453. if value:
  454. self._userModel.addEngine(key)
  455. else:
  456. self._userModel.removeEngine(key)
  457. self.reSort()
  458. return True
  459. return False
  460. def data(self, index, role):
  461. if not index.isValid():
  462. return QVariant()
  463. if role == Qt.DisplayRole:
  464. key = self._keyIndex[index.row()]
  465. return self.getValueByKey(key, index.column())
  466. elif index.column() == 0 and role == Qt.CheckStateRole:
  467. if self._userModel is not None:
  468. key = self._keyIndex[index.row()]
  469. if key in self._userModel.engines:
  470. return Qt.Checked
  471. return Qt.Unchecked
  472. return QVariant()
  473. def flags(self, index):
  474. flags = (
  475. Qt.ItemIsSelectable |
  476. Qt.ItemIsEnabled |
  477. Qt.ItemNeverHasChildren
  478. )
  479. if index.column() == 0:
  480. flags = flags | Qt.ItemIsUserCheckable
  481. return flags
  482. class InstanceTableModel(QAbstractTableModel):
  483. """ `InstancesModel` -> `QAbstractTableModel` adapter model
  484. """
  485. class Column:
  486. def __init__(self, name, route, type_):
  487. self._name = name
  488. self._route = route
  489. self._type = type_
  490. @property
  491. def type(self): return self._type
  492. @property
  493. def name(self): return self._name
  494. @property
  495. def route(self): return self._route
  496. def __init__(self, instancesModel, parent):
  497. """
  498. @param instancesModel: Resource model
  499. @type instancesModel: InstancesModel
  500. """
  501. QAbstractTableModel.__init__(self, parent)
  502. self._model = instancesModel
  503. self._currentModelType = instancesModel.parentModel().Type
  504. self._keyIndex = [] # [key, key, ..]
  505. self.__currentSorting = (0, Qt.AscendingOrder)
  506. self._columns = [
  507. InstanceTableModel.Column('url', 'url', str),
  508. InstanceTableModel.Column('version', 'version', str),
  509. InstanceTableModel.Column('engines', 'engines', list),
  510. InstanceTableModel.Column('tls.version', 'tls.version', str),
  511. InstanceTableModel.Column(
  512. 'tls.cert.version',
  513. 'tls.certificate.version',
  514. int),
  515. InstanceTableModel.Column(
  516. 'tls.countryName',
  517. 'tls.certificate.issuer.countryName',
  518. str),
  519. InstanceTableModel.Column(
  520. 'tls.commonName',
  521. 'tls.certificate.issuer.commonName',
  522. str),
  523. InstanceTableModel.Column(
  524. 'tls.organizationName',
  525. 'tls.certificate.issuer.organizationName',
  526. str),
  527. InstanceTableModel.Column(
  528. 'network.asnPrivacy',
  529. 'network.asnPrivacy',
  530. str),
  531. InstanceTableModel.Column(
  532. 'network.ipv6',
  533. 'network.ipv6',
  534. bool),
  535. InstanceTableModel.Column('network.ips', 'network.ips', dict)
  536. ]
  537. instancesModel.changed.connect(self.__resourceModelChanged)
  538. instancesModel.parentModel().typeChanged.connect(
  539. self.__modelTypeChanged
  540. )
  541. def __modelTypeChanged(self, newType):
  542. previousType = self._currentModelType
  543. if (previousType != InstancesModelTypes.User and
  544. newType == InstancesModelTypes.User):
  545. self._columns.append(
  546. InstanceTableModel.Column(
  547. 'lastUpdated',
  548. 'lastUpdated',
  549. int
  550. )
  551. )
  552. elif (previousType == InstancesModelTypes.User and
  553. newType != InstancesModelTypes.User):
  554. del self._columns[-1]
  555. self._currentModelType = newType
  556. def __genKeyIndexes(self):
  557. self._keyIndex.clear()
  558. for key in self._model:
  559. self._keyIndex.append(key)
  560. def __resourceModelChanged(self):
  561. self.sort(*self.__currentSorting)
  562. def getColumns(self): return self._columns
  563. def getByIndex(self, index):
  564. """ Returns a Instance it's URL by index.
  565. @param index: Index of the instance it's url you like to get.
  566. @type index: int
  567. @return: Instance url
  568. @rtype: str
  569. """
  570. return self._keyIndex[index]
  571. def getByUrl(self, url):
  572. """ Returns a Instancs it's current index by url
  573. @param url: Url of the instance you want to get the current
  574. index of.
  575. @type url: str
  576. @returns: Instance index.
  577. @rtype: int
  578. """
  579. return self._keyIndex.index(url)
  580. def getPropertyValueByIndex(self, index, route):
  581. obj = self._model[self.getByIndex(index)]
  582. return self.getPropertyValue(obj, route)
  583. def getPropertyValue(self, obj, route):
  584. """ Returns the `Instance` it's desired property.
  585. @param obj: instance object
  586. @type obj: Instance
  587. @param route: traversel path to value through properties.
  588. @type route: str
  589. """
  590. routes = route.split('.')
  591. propValue = None
  592. for propName in routes:
  593. propValue = getattr(obj, propName)
  594. obj = propValue
  595. return propValue
  596. """ QAbstractTableModel reimplementations below
  597. """
  598. def rowCount(self, parent): return len(self._model)
  599. def columnCount(self, parent): return len(self._columns)
  600. def sort(self, col, order=Qt.AscendingOrder):
  601. self.layoutAboutToBeChanged.emit()
  602. route = self._columns[col].route
  603. unsortedList = []
  604. for url, instance in self._model.items():
  605. value = str(
  606. self.getPropertyValue(
  607. instance,
  608. route
  609. )
  610. )
  611. unsortedList.append([url, value])
  612. reverse = False if order == Qt.AscendingOrder else True
  613. sortedList = sorted(
  614. unsortedList,
  615. key=itemgetter(1),
  616. reverse=reverse
  617. )
  618. self._keyIndex.clear()
  619. for url, value in sortedList:
  620. self._keyIndex.append(url)
  621. self.__currentSorting = (col, order)
  622. self.layoutChanged.emit()
  623. def headerData(self, col, orientation, role):
  624. if orientation == Qt.Horizontal and role == Qt.DisplayRole:
  625. return QVariant(self._columns[col].name)
  626. return QVariant()
  627. def data(self, index, role):
  628. if not index.isValid():
  629. return QVariant()
  630. if role == Qt.DisplayRole:
  631. value = self.getPropertyValueByIndex(
  632. index.row(),
  633. self._columns[index.column()].route)
  634. if index.column() == 2: # engines
  635. newStr = ''
  636. for engine in value:
  637. if newStr:
  638. newStr += ', {0}'.format(engine.name)
  639. else:
  640. newStr = engine.name
  641. return newStr
  642. elif index.column() == 10: # ips
  643. return str(value)
  644. elif index.column() == 11: # userInstances lastUpdated
  645. return timeToString(value)
  646. return value
  647. return QVariant()