instances.py 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974
  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 copy import deepcopy
  22. import random
  23. from operator import itemgetter
  24. from PyQt5.QtCore import (
  25. QObject,
  26. pyqtSignal,
  27. QAbstractTableModel,
  28. QTimer,
  29. QVariant,
  30. Qt
  31. )
  32. from searxqt.core.instances import Instance, Stats2
  33. from searxqt.core.engines import Stats2Engines, EnginesModel
  34. from searxqt.core.instanceVersions import (
  35. InstanceVersionTypes,
  36. parseVersionString,
  37. InstanceVersionGtEqCmp
  38. )
  39. from searxqt.utils.string import boolToStr, listToStr
  40. from searxqt.utils.time import nowInMinutes
  41. from searxqt.thread import Thread, ThreadManagerProto
  42. from searxqt.translations import _, timeToString
  43. class InstancesModelTypes:
  44. NotDefined = 0
  45. Stats2 = 1
  46. User = 2
  47. class PersistentEnginesModel(EnginesModel, QObject):
  48. changed = pyqtSignal()
  49. def __init__(self, enginesModel=None, parent=None):
  50. EnginesModel.__init__(self)
  51. QObject.__init__(self, parent)
  52. self._currentModel = None
  53. if enginesModel:
  54. self.setModel(enginesModel)
  55. def hasModel(self):
  56. return False if self._currentModel is None else True
  57. def setModel(self, enginesModel):
  58. if self._currentModel:
  59. self._currentModel.deleteLater()
  60. self._currentModel.changed.disconnect(self.changed)
  61. self._currentModel = enginesModel
  62. self._currentModel.changed.connect(self.changed)
  63. self._data = self._currentModel.data()
  64. self.changed.emit()
  65. class UserEnginesModel(EnginesModel, QObject):
  66. changed = pyqtSignal()
  67. def __init__(self, handler, parent=None):
  68. QObject.__init__(self, parent)
  69. EnginesModel.__init__(self, handler)
  70. handler.changed.connect(self.changed)
  71. class Stats2EnginesModel(Stats2Engines, QObject):
  72. changed = pyqtSignal()
  73. def __init__(self, handler, parent=None):
  74. """
  75. @param handler: Object containing engines data.
  76. @type handler: searxqt.models.instances.Stats2Model
  77. """
  78. QObject.__init__(self, parent)
  79. Stats2Engines.__init__(self, handler)
  80. handler.changed.connect(self.changed)
  81. class Stats2Model(Stats2, ThreadManagerProto):
  82. changed = pyqtSignal()
  83. def __init__(self, requestsHandler, parent=None):
  84. """
  85. @param requestsHandler:
  86. @type requestsHandler: core.requests.RequestsHandler
  87. """
  88. Stats2.__init__(self, requestsHandler)
  89. ThreadManagerProto.__init__(self, parent=parent)
  90. # ThreadManagerProto override
  91. def currentJobStr(self):
  92. if self.hasActiveJobs():
  93. return _("<b>Updating data from:</b> {0}").format(self.URL)
  94. return ""
  95. def setData(self, data):
  96. Stats2.setData(self, data)
  97. self.changed.emit()
  98. def updateInstances(self):
  99. if self._thread:
  100. return
  101. self._thread = Thread(
  102. Stats2.updateInstances,
  103. args=[self],
  104. parent=self
  105. )
  106. self._thread.finished.connect(
  107. self.__updateInstancesThreadFinished
  108. )
  109. self.threadStarted.emit()
  110. self._thread.start()
  111. def __updateInstancesThreadFinished(self):
  112. if self._thread.result():
  113. self.changed.emit()
  114. self._thread.finished.disconnect(
  115. self.__updateInstancesThreadFinished
  116. )
  117. # Wait before deleting because the `finished` signal is emited
  118. # from the thread itself, so this method could be called before the
  119. # thread is actually finished and result in a crash.
  120. self._thread.wait()
  121. self._thread.deleteLater()
  122. self._thread = None
  123. self.threadFinished.emit()
  124. class InstanceModel(Instance):
  125. def __init__(self, url, data, parent=None):
  126. Instance.__init__(self, url, data)
  127. class UserInstanceModel(InstanceModel, QObject):
  128. def __init__(self, url, data, parent=None):
  129. QObject.__init__(self, parent=parent)
  130. InstanceModel.__init__(self, url, data, parent=parent)
  131. @property
  132. def lastUpdated(self):
  133. return self._data.get('lastUpdated', 0)
  134. class InstancesModel(QObject):
  135. InstanceType = InstanceModel
  136. Type = InstancesModelTypes.NotDefined
  137. changed = pyqtSignal()
  138. def __init__(self, handler=None, parent=None):
  139. """
  140. """
  141. QObject.__init__(self, parent=parent)
  142. self._instances = {}
  143. self._modelHandler = handler
  144. if handler:
  145. self._instances = handler.instances
  146. handler.changed.connect(self.changed)
  147. def __contains__(self, url):
  148. return bool(url in self._instances)
  149. def __getitem__(self, url):
  150. return self.InstanceType(url, self._instances[url])
  151. def __str__(self): return str([url for url in self._instances])
  152. def __repr__(self): return str(self)
  153. def __len__(self): return len(self._instances)
  154. def data(self):
  155. return self._instances
  156. def items(self):
  157. return [
  158. (url, self.InstanceType(url, data))
  159. for url, data in self._instances.items()
  160. ]
  161. def keys(self): return self._instances.keys()
  162. def values(self):
  163. return [
  164. self.InstanceType(url, data)
  165. for url, data in self._instances.items()
  166. ]
  167. def copy(self):
  168. return self._instances.copy()
  169. class PersistentInstancesModel(InstancesModel):
  170. """ This will either hold a Stats2InstancesModel or a UserInstancesModel
  171. It can be switched during run-time.
  172. This ensures that no references are made to the underlying model
  173. outside of this object.
  174. """
  175. typeChanged = pyqtSignal(int) # InstancesModelTypes
  176. def __init__(self, instancesModel=None, parent=None):
  177. InstancesModel.__init__(self, parent=parent)
  178. self.__currentModel = None
  179. self._modelHandler = None # Object that manages the model
  180. if instancesModel:
  181. self.setModel(instancesModel)
  182. def hasModel(self):
  183. return False if self.__currentModel is None else True
  184. def hasHandler(self):
  185. return False if self._modelHandler is None else True
  186. def handler(self):
  187. # Do not store references to the returned object!
  188. return self._modelHandler
  189. def setModel(self, instancesModel):
  190. if self.__currentModel:
  191. self.__currentModel.changed.disconnect(self.changed)
  192. self.__currentModel.deleteLater()
  193. self.InstanceType = instancesModel.InstanceType
  194. self.__currentModel = instancesModel
  195. self.__currentModel.changed.connect(self.changed)
  196. self._instances = self.__currentModel.data()
  197. self._modelHandler = instancesModel._modelHandler
  198. if self.Type != instancesModel.Type:
  199. self.Type = instancesModel.Type
  200. self.typeChanged.emit(self.Type)
  201. self.changed.emit()
  202. class UserInstancesModel(InstancesModel, QObject):
  203. InstanceType = UserInstanceModel
  204. Type = InstancesModelTypes.User
  205. def __init__(self, handler, parent=None):
  206. InstancesModel.__init__(self, handler, parent=parent)
  207. class Stats2InstancesModel(InstancesModel):
  208. Type = InstancesModelTypes.Stats2
  209. def __init__(self, handler, parent=None):
  210. """
  211. @param handler:
  212. @type handler: Stats2Model
  213. """
  214. InstancesModel.__init__(self, handler, parent=parent)
  215. class InstanceModelFilter(QObject):
  216. changed = pyqtSignal()
  217. VERSION_FILTER_TEMPLATE = {
  218. 'min': "", # just the version string
  219. 'git': True,
  220. 'unknown': False
  221. }
  222. def __init__(self, model, parent=None):
  223. QObject.__init__(self, parent=parent)
  224. """
  225. @type model: searxqt.models.instances.PersistentInstancesModel
  226. """
  227. self._model = model
  228. self._current = model.copy()
  229. self._filter = {
  230. 'networkTypes': [],
  231. 'version': deepcopy(self.VERSION_FILTER_TEMPLATE),
  232. 'url': [], # whitelist
  233. # key: url (str), value: (time (uint), reason (str))
  234. 'skipUrl': {}, # blacklist
  235. 'asnPrivacy': True,
  236. 'ipv6': False,
  237. 'engines': [],
  238. # A dict for temp blacklisting a instance url. This won't be stored on
  239. # disk, only in RAM. So on restart of searx-qt this will be empty.
  240. # It is used to put failing instances on a timeout.
  241. # key: instance url, value: tuple (QTimer object, str reason)
  242. 'timeout': {}
  243. }
  244. self._model.changed.connect(self.apply)
  245. def putInstanceOnTimeout(self, url, duration=0, reason=''):
  246. """ Put a instance url on a temporary blacklist.
  247. When 'duration' is '0' it won't timeout, and the url will be
  248. blacklisted until restart of searx-qt or possible manual removal
  249. from the list.
  250. @param url: Instance url
  251. @type url: str
  252. @param duration: The duration of the blacklist in minutes.
  253. @type duration: int
  254. """
  255. timer = None
  256. if duration:
  257. timer = QTimer(self)
  258. timer.setSingleShot(True)
  259. timer.timeout.connect(
  260. lambda url=url: self.delInstanceFromTimeout(url)
  261. )
  262. timer.start(duration * 60000)
  263. self._filter['timeout'].update({url: (timer, reason)})
  264. self.apply()
  265. def delInstanceFromTimeout(self, url):
  266. """ Remove url from temporary blacklist.
  267. """
  268. if self._filter['timeout'][url][0]:
  269. # a QTimer is set, delete it.
  270. self._filter['timeout'][url][0].deleteLater() # Delete the QTimer.
  271. del self._filter['timeout'][url] # Remove from filter.
  272. self.apply() # Re-apply the filter.
  273. def putInstanceOnBlacklist(self, url, reason=''):
  274. """ TODO reason is unused!
  275. """
  276. if url not in self._filter['skipUrl']:
  277. self._filter['skipUrl'].update({url: (nowInMinutes(), reason)})
  278. def delInstanceFromBlacklist(self, url):
  279. del self._filter['skipUrl'][url]
  280. def loadSettings(self, data):
  281. defaultAsnPrivacy = bool(self._model.Type != InstancesModelTypes.User)
  282. # Clear the temporary blacklist which maybe populated when switched
  283. # from profile.
  284. for timer, reason in self._filter['timeout'].values():
  285. if timer:
  286. timer.deleteLater()
  287. self._filter['timeout'].clear()
  288. # Restore timeouts
  289. timeouts = data.get('timeout', {})
  290. for url in timeouts:
  291. until, reason = timeouts[url]
  292. delta = until - nowInMinutes()
  293. if delta > 0:
  294. self.putInstanceOnTimeout(url, delta, reason)
  295. self.updateKwargs(
  296. {
  297. 'networkTypes': data.get('networkTypes', []),
  298. 'version': data.get(
  299. 'version',
  300. deepcopy(self.VERSION_FILTER_TEMPLATE)
  301. ),
  302. 'url': data.get('url', []),
  303. 'skipUrl': data.get('skipUrl', {}),
  304. 'asnPrivacy': data.get('asnPrivacy', defaultAsnPrivacy),
  305. 'ipv6': data.get('ipv6', False)
  306. }
  307. )
  308. def saveSettings(self):
  309. filter_ = self.filter()
  310. # Store timeouts
  311. timeout = {}
  312. for url in filter_['timeout']:
  313. timer, reason = filter_['timeout'][url]
  314. if timer:
  315. until = nowInMinutes() + int((timer.remainingTime() / 1000) / 60)
  316. timeout.update({url: (until, reason)})
  317. return {
  318. 'networkTypes': filter_['networkTypes'],
  319. 'version': filter_['version'],
  320. 'url': filter_['url'],
  321. 'skipUrl': filter_['skipUrl'],
  322. 'asnPrivacy': filter_['asnPrivacy'],
  323. 'ipv6': filter_['ipv6'],
  324. 'timeout': timeout
  325. }
  326. def filter(self): return self._filter
  327. def parentModel(self): return self._model
  328. def updateKwargs(self, kwargs, commit=True):
  329. for key in kwargs:
  330. self._filter[key] = kwargs[key]
  331. if commit:
  332. self.apply()
  333. def apply(self):
  334. self._current.clear()
  335. minimumVersion = parseVersionString(self._filter['version']['min'])
  336. for url, instance in self._model.items():
  337. # Skip temporary blacklisted instances.
  338. if url in self._filter['timeout']:
  339. continue
  340. if url not in self._filter['url']: # Url whitelisted
  341. # Url blacklist
  342. if self._filter['skipUrl']:
  343. if instance.url in self._filter['skipUrl']:
  344. continue
  345. # Network
  346. if self._filter['networkTypes']:
  347. if (
  348. instance.networkType not in
  349. self._filter['networkTypes']
  350. ):
  351. continue
  352. # Version
  353. if minimumVersion.type != InstanceVersionTypes.No:
  354. # Filter out instances with a truly unknown version, a
  355. # empty string or unknown formatted (malformed?) version
  356. # string. This doesn't filter out versions with a valid
  357. # semantic version string plus '-unknown' as suffix.
  358. if instance.version.type == InstanceVersionTypes.No:
  359. continue
  360. # Condition where the evaluated instance it's version is a
  361. # git version (development) and the git checkbox is
  362. # unchecked, so we want to filter it out.
  363. if (
  364. not self._filter['version']['git'] and
  365. instance.version.type == InstanceVersionTypes.Git
  366. ):
  367. continue
  368. # Condition where the evaluated instance it's version has
  369. # the '-unknown' suffix after a valid semantic version.
  370. # This is a git version (development) with a unknown hash,
  371. # probably from a fork.
  372. if (
  373. not self._filter['version']['unknown'] and
  374. instance.version.type == InstanceVersionTypes.Unknown
  375. ):
  376. continue
  377. # Filter out instances that don't meet the minimum version.
  378. if not InstanceVersionGtEqCmp(
  379. instance.version,
  380. minimumVersion
  381. ):
  382. continue
  383. # ASN privacy
  384. if self._filter['asnPrivacy']:
  385. if instance.network.asnPrivacy != 0:
  386. continue
  387. # IPv6
  388. if self._filter['ipv6']:
  389. if not instance.network.ipv6:
  390. continue
  391. # Engines
  392. if self._filter['engines']:
  393. # TODO when engine(s) are set and also a language, we should
  394. # check if the engine has language support before allowing
  395. # it.
  396. #
  397. # TODO when engine(s) are set and also a time-range we
  398. # should check if the engine has time-range support.
  399. #
  400. # When the user has set specific search engines set to be
  401. # searched on we filter out all instanes that don't atleast
  402. # support one of the set engines available.
  403. found = False
  404. for engine in self._filter['engines']:
  405. for e in instance.engines:
  406. if e.name == engine:
  407. found = True
  408. break
  409. if not found:
  410. # This instance doesn't have one of the set engines so
  411. # we filter it out.
  412. continue
  413. self._current.update({url: instance})
  414. self.changed.emit()
  415. def __contains__(self, url): return bool(url in self._current)
  416. def __iter__(self): return iter(self._current)
  417. def __getitem__(self, url): return self._current[url]
  418. def __str__(self): return str([url for url in self])
  419. def __repr__(self): return str(self)
  420. def __len__(self): return len(self._current)
  421. def items(self): return self._current.items()
  422. def keys(self): return self._current.keys()
  423. def values(self): return self._current.values()
  424. def copy(self): return self._current.copy()
  425. class InstanceSelecterModel(QObject):
  426. optionsChanged = pyqtSignal()
  427. instanceChanged = pyqtSignal(str) # instance url
  428. def __init__(self, model, parent=None):
  429. QObject.__init__(self, parent=parent)
  430. """
  431. @type model: InstancesModelFilter
  432. """
  433. self._model = model
  434. self._currentInstanceUrl = ''
  435. self._model.changed.connect(self.__modelChanged)
  436. def __modelChanged(self):
  437. """ This can happen after example blacklisting all instances.
  438. """
  439. if self.currentUrl and self.currentUrl not in self._model:
  440. self.currentUrl = ""
  441. @property
  442. def currentUrl(self): return self._currentInstanceUrl
  443. @currentUrl.setter
  444. def currentUrl(self, url):
  445. self._currentInstanceUrl = url
  446. self.instanceChanged.emit(url)
  447. def loadSettings(self, data):
  448. self.currentUrl = data.get('currentInstance', '')
  449. self.instanceChanged.emit(self.currentUrl)
  450. def saveSettings(self):
  451. return {
  452. 'currentInstance': self.currentUrl
  453. }
  454. def getRandomInstances(self, amount=10):
  455. """ Returns a list of random instance urls.
  456. """
  457. return random.sample(list(self._model.keys()),
  458. min(amount, len(self._model.keys())))
  459. def randomInstance(self):
  460. if self._model.keys():
  461. self.currentUrl = random.choice(list(self._model.keys()))
  462. return self.currentUrl
  463. class EnginesTableModel(QAbstractTableModel):
  464. """ Model used to display engines with their data in a QTableView and
  465. for adding/removing engines to/from categories.
  466. """
  467. def __init__(self, enginesModel, parent):
  468. """
  469. @param enginesModel: Contains data about all engines.
  470. @type enginesModel: searxqt.models.instances.EnginesModel
  471. """
  472. QAbstractTableModel.__init__(self, parent)
  473. self._model = enginesModel # contains all engines
  474. self._userModel = None # see self.setUserModel method
  475. self._columns = [
  476. _('Enabled'),
  477. _('Name'),
  478. _('Categories'),
  479. _('Language support'),
  480. _('Paging'),
  481. _('SafeSearch'),
  482. _('Shortcut'),
  483. _('Time-range support')
  484. ]
  485. self._keyIndex = []
  486. self._catFilter = ""
  487. self._sort = (0, None)
  488. self.__genKeyIndexes()
  489. def setUserModel(self, model):
  490. """
  491. @param model: User category model
  492. @type model: searxqt.models.search.UserCategoryModel
  493. """
  494. self.layoutAboutToBeChanged.emit()
  495. self._userModel = model
  496. self.layoutChanged.emit()
  497. self.reSort()
  498. def __genKeyIndexes(self):
  499. self._keyIndex.clear()
  500. if self._catFilter:
  501. self._keyIndex = [
  502. key for key, engine in self._model.items()
  503. if self._catFilter in engine.categories
  504. ]
  505. else:
  506. self._keyIndex = list(self._model.keys())
  507. def setCatFilter(self, catKey=""):
  508. """ Filter engines on category.
  509. """
  510. self.layoutAboutToBeChanged.emit()
  511. self._catFilter = catKey
  512. self.__genKeyIndexes()
  513. self.reSort()
  514. self.layoutChanged.emit()
  515. def getValueByKey(self, key, columnIndex):
  516. if columnIndex == 0:
  517. if self._userModel:
  518. return boolToStr(bool(key in self._userModel.engines))
  519. return boolToStr(False)
  520. elif columnIndex == 1:
  521. return key
  522. elif columnIndex == 2:
  523. return listToStr(self._model[key].categories)
  524. elif columnIndex == 3:
  525. return boolToStr(self._model[key].languageSupport)
  526. elif columnIndex == 4:
  527. return boolToStr(self._model[key].paging)
  528. elif columnIndex == 5:
  529. return boolToStr(self._model[key].safesearch)
  530. elif columnIndex == 6:
  531. return self._model[key].shortcut
  532. elif columnIndex == 7:
  533. return boolToStr(self._model[key].timeRangeSupport)
  534. def __sort(self, columnIndex, order=Qt.AscendingOrder):
  535. unsortedList = [
  536. [key, self.getValueByKey(key, columnIndex)]
  537. for key in self._keyIndex
  538. ]
  539. reverse = False if order == Qt.AscendingOrder else True
  540. sortedList = sorted(
  541. unsortedList,
  542. key=itemgetter(1),
  543. reverse=reverse
  544. )
  545. self._keyIndex.clear()
  546. for key, value in sortedList:
  547. self._keyIndex.append(key)
  548. def reSort(self):
  549. if self._sort is not None:
  550. self.sort(self._sort[0], self._sort[1])
  551. """ QAbstractTableModel reimplementations below
  552. """
  553. def rowCount(self, parent): return len(self._keyIndex)
  554. def columnCount(self, parent):
  555. return len(self._columns)
  556. def headerData(self, col, orientation, role):
  557. if orientation == Qt.Horizontal and role == Qt.DisplayRole:
  558. return QVariant(self._columns[col])
  559. return QVariant()
  560. def sort(self, columnIndex, order=Qt.AscendingOrder):
  561. self.layoutAboutToBeChanged.emit()
  562. self._sort = (columnIndex, order) # backup current sorting
  563. self.__sort(columnIndex, order=order)
  564. self.layoutChanged.emit()
  565. def setData(self, index, value, role):
  566. if not index.isValid():
  567. return False
  568. if role == Qt.CheckStateRole:
  569. if self._userModel is not None:
  570. key = self._keyIndex[index.row()]
  571. if value:
  572. self._userModel.addEngine(key)
  573. else:
  574. self._userModel.removeEngine(key)
  575. self.reSort()
  576. return True
  577. return False
  578. def data(self, index, role):
  579. if not index.isValid():
  580. return QVariant()
  581. if role == Qt.DisplayRole:
  582. key = self._keyIndex[index.row()]
  583. return self.getValueByKey(key, index.column())
  584. elif index.column() == 0 and role == Qt.CheckStateRole:
  585. if self._userModel is not None:
  586. key = self._keyIndex[index.row()]
  587. if key in self._userModel.engines:
  588. return Qt.Checked
  589. return Qt.Unchecked
  590. return QVariant()
  591. def flags(self, index):
  592. flags = (
  593. Qt.ItemIsSelectable |
  594. Qt.ItemIsEnabled |
  595. Qt.ItemNeverHasChildren
  596. )
  597. if index.column() == 0:
  598. flags = flags | Qt.ItemIsUserCheckable
  599. return flags
  600. class InstanceTableModel(QAbstractTableModel):
  601. """ `InstancesModel` -> `QAbstractTableModel` adapter model
  602. """
  603. class Column:
  604. def __init__(self, name, route, type_):
  605. self._name = name
  606. self._route = route
  607. self._type = type_
  608. @property
  609. def type(self): return self._type
  610. @property
  611. def name(self): return self._name
  612. @property
  613. def route(self): return self._route
  614. def __init__(self, instancesModel, parent):
  615. """
  616. @param instancesModel: Resource model
  617. @type instancesModel: InstancesModel
  618. """
  619. QAbstractTableModel.__init__(self, parent)
  620. self._model = instancesModel
  621. self._currentModelType = instancesModel.parentModel().Type
  622. self._keyIndex = [] # [key, key, ..]
  623. self.__currentSorting = (0, Qt.AscendingOrder)
  624. self._columns = [
  625. InstanceTableModel.Column('url', 'url', str),
  626. InstanceTableModel.Column('version', 'version', str),
  627. InstanceTableModel.Column('engines', 'engines', list),
  628. InstanceTableModel.Column('tls.version', 'tls.version', str),
  629. InstanceTableModel.Column(
  630. 'tls.cert.version',
  631. 'tls.certificate.version',
  632. int),
  633. InstanceTableModel.Column(
  634. 'tls.countryName',
  635. 'tls.certificate.issuer.countryName',
  636. str),
  637. InstanceTableModel.Column(
  638. 'tls.commonName',
  639. 'tls.certificate.issuer.commonName',
  640. str),
  641. InstanceTableModel.Column(
  642. 'tls.organizationName',
  643. 'tls.certificate.issuer.organizationName',
  644. str),
  645. InstanceTableModel.Column(
  646. 'network.asnPrivacy',
  647. 'network.asnPrivacy',
  648. str),
  649. InstanceTableModel.Column(
  650. 'network.ipv6',
  651. 'network.ipv6',
  652. bool),
  653. InstanceTableModel.Column('network.ips', 'network.ips', dict)
  654. ]
  655. instancesModel.changed.connect(self.__resourceModelChanged)
  656. instancesModel.parentModel().typeChanged.connect(
  657. self.__modelTypeChanged
  658. )
  659. def __modelTypeChanged(self, newType):
  660. previousType = self._currentModelType
  661. if (previousType != InstancesModelTypes.User and
  662. newType == InstancesModelTypes.User):
  663. self._columns.append(
  664. InstanceTableModel.Column(
  665. 'lastUpdated',
  666. 'lastUpdated',
  667. int
  668. )
  669. )
  670. elif (previousType == InstancesModelTypes.User and
  671. newType != InstancesModelTypes.User):
  672. del self._columns[-1]
  673. self._currentModelType = newType
  674. def __genKeyIndexes(self):
  675. self._keyIndex.clear()
  676. for key in self._model:
  677. self._keyIndex.append(key)
  678. def __resourceModelChanged(self):
  679. self.sort(*self.__currentSorting)
  680. def getColumns(self): return self._columns
  681. def getByIndex(self, index):
  682. """ Returns a Instance it's URL by index.
  683. @param index: Index of the instance it's url you like to get.
  684. @type index: int
  685. @return: Instance url
  686. @rtype: str
  687. """
  688. return self._keyIndex[index]
  689. def getByUrl(self, url):
  690. """ Returns a Instancs it's current index by url
  691. @param url: Url of the instance you want to get the current
  692. index of.
  693. @type url: str
  694. @returns: Instance index.
  695. @rtype: int
  696. """
  697. return self._keyIndex.index(url)
  698. def getPropertyValueByIndex(self, index, route):
  699. obj = self._model[self.getByIndex(index)]
  700. return self.getPropertyValue(obj, route)
  701. def getPropertyValue(self, obj, route):
  702. """ Returns the `Instance` it's desired property.
  703. @param obj: instance object
  704. @type obj: Instance
  705. @param route: traversel path to value through properties.
  706. @type route: str
  707. """
  708. routes = route.split('.')
  709. propValue = None
  710. for propName in routes:
  711. propValue = getattr(obj, propName)
  712. obj = propValue
  713. return propValue
  714. """ QAbstractTableModel reimplementations below
  715. """
  716. def rowCount(self, parent): return len(self._model)
  717. def columnCount(self, parent): return len(self._columns)
  718. def sort(self, col, order=Qt.AscendingOrder):
  719. self.layoutAboutToBeChanged.emit()
  720. route = self._columns[col].route
  721. unsortedList = []
  722. for url, instance in self._model.items():
  723. value = str(
  724. self.getPropertyValue(
  725. instance,
  726. route
  727. )
  728. )
  729. unsortedList.append([url, value])
  730. reverse = False if order == Qt.AscendingOrder else True
  731. sortedList = sorted(
  732. unsortedList,
  733. key=itemgetter(1),
  734. reverse=reverse
  735. )
  736. self._keyIndex.clear()
  737. for url, value in sortedList:
  738. self._keyIndex.append(url)
  739. self.__currentSorting = (col, order)
  740. self.layoutChanged.emit()
  741. def headerData(self, col, orientation, role):
  742. if orientation == Qt.Horizontal and role == Qt.DisplayRole:
  743. return QVariant(self._columns[col].name)
  744. return QVariant()
  745. def data(self, index, role):
  746. if not index.isValid():
  747. return QVariant()
  748. if role == Qt.DisplayRole:
  749. value = self.getPropertyValueByIndex(
  750. index.row(),
  751. self._columns[index.column()].route)
  752. if index.column() == 1: # version
  753. return str(value)
  754. elif index.column() == 2: # engines
  755. newStr = ''
  756. for engine in value:
  757. if newStr:
  758. newStr += ', {0}'.format(engine.name)
  759. else:
  760. newStr = engine.name
  761. return newStr
  762. elif index.column() == 10: # ips
  763. return str(value)
  764. elif index.column() == 11: # userInstances lastUpdated
  765. return timeToString(value)
  766. return value
  767. return QVariant()