instances.py 33 KB

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