instances.py 34 KB

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