tools.py 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. # Part of Odoo. See LICENSE file for full copyright and licensing details.
  2. import contextlib
  3. import re
  4. import werkzeug.urls
  5. from lxml import etree
  6. from unittest.mock import Mock, MagicMock, patch
  7. from werkzeug.exceptions import NotFound
  8. from werkzeug.test import EnvironBuilder
  9. import odoo
  10. from odoo.tests.common import HttpCase, HOST
  11. from odoo.tools.misc import DotDict, frozendict
  12. @contextlib.contextmanager
  13. def MockRequest(
  14. env, *, path='/mockrequest', routing=True, multilang=True,
  15. context=frozendict(), cookies=frozendict(), country_code=None,
  16. website=None, remote_addr=HOST, environ_base=None,
  17. # website_sale
  18. sale_order_id=None, website_sale_current_pl=None,
  19. ):
  20. lang_code = context.get('lang', env.context.get('lang', 'en_US'))
  21. env = env(context=dict(context, lang=lang_code))
  22. request = Mock(
  23. # request
  24. httprequest=Mock(
  25. host='localhost',
  26. path=path,
  27. app=odoo.http.root,
  28. environ=dict(
  29. EnvironBuilder(
  30. path=path,
  31. base_url=HttpCase.base_url(),
  32. environ_base=environ_base,
  33. ).get_environ(),
  34. REMOTE_ADDR=remote_addr,
  35. ),
  36. cookies=cookies,
  37. referrer='',
  38. remote_addr=remote_addr,
  39. ),
  40. type='http',
  41. future_response=odoo.http.FutureResponse(),
  42. params={},
  43. redirect=env['ir.http']._redirect,
  44. session=DotDict(
  45. odoo.http.get_default_session(),
  46. geoip={'country_code': country_code},
  47. sale_order_id=sale_order_id,
  48. website_sale_current_pl=website_sale_current_pl,
  49. ),
  50. geoip={},
  51. db=env.registry.db_name,
  52. env=env,
  53. registry=env.registry,
  54. cr=env.cr,
  55. uid=env.uid,
  56. context=env.context,
  57. lang=env['res.lang']._lang_get(lang_code),
  58. website=website,
  59. render=lambda *a, **kw: '<MockResponse>',
  60. )
  61. if website:
  62. request.website_routing = website.id
  63. # The following code mocks match() to return a fake rule with a fake
  64. # 'routing' attribute (routing=True) or to raise a NotFound
  65. # exception (routing=False).
  66. #
  67. # router = odoo.http.root.get_db_router()
  68. # rule, args = router.bind(...).match(path)
  69. # # arg routing is True => rule.endpoint.routing == {...}
  70. # # arg routing is False => NotFound exception
  71. router = MagicMock()
  72. match = router.return_value.bind.return_value.match
  73. if routing:
  74. match.return_value[0].routing = {
  75. 'type': 'http',
  76. 'website': True,
  77. 'multilang': multilang
  78. }
  79. else:
  80. match.side_effect = NotFound
  81. def update_context(**overrides):
  82. request.context = dict(request.context, **overrides)
  83. request.update_context = update_context
  84. with contextlib.ExitStack() as s:
  85. odoo.http._request_stack.push(request)
  86. s.callback(odoo.http._request_stack.pop)
  87. s.enter_context(patch('odoo.http.root.get_db_router', router))
  88. yield request
  89. # Fuzzy matching tools
  90. def distance(s1="", s2="", limit=4):
  91. """
  92. Limited Levenshtein-ish distance (inspired from Apache text common)
  93. Note: this does not return quick results for simple cases (empty string, equal strings)
  94. those checks should be done outside loops that use this function.
  95. :param s1: first string
  96. :param s2: second string
  97. :param limit: maximum distance to take into account, return -1 if exceeded
  98. :return: number of character changes needed to transform s1 into s2 or -1 if this exceeds the limit
  99. """
  100. BIG = 100000 # never reached integer
  101. if len(s1) > len(s2):
  102. s1, s2 = s2, s1
  103. l1 = len(s1)
  104. l2 = len(s2)
  105. if l2 - l1 > limit:
  106. return -1
  107. boundary = min(l1, limit) + 1
  108. p = [i if i < boundary else BIG for i in range(0, l1 + 1)]
  109. d = [BIG for _ in range(0, l1 + 1)]
  110. for j in range(1, l2 + 1):
  111. j2 = s2[j - 1]
  112. d[0] = j
  113. range_min = max(1, j - limit)
  114. range_max = min(l1, j + limit)
  115. if range_min > 1:
  116. d[range_min - 1] = BIG
  117. for i in range(range_min, range_max + 1):
  118. if s1[i - 1] == j2:
  119. d[i] = p[i - 1]
  120. else:
  121. d[i] = 1 + min(d[i - 1], p[i], p[i - 1])
  122. p, d = d, p
  123. return p[l1] if p[l1] <= limit else -1
  124. def similarity_score(s1, s2):
  125. """
  126. Computes a score that describes how much two strings are matching.
  127. :param s1: first string
  128. :param s2: second string
  129. :return: float score, the higher the more similar
  130. pairs returning non-positive scores should be considered non similar
  131. """
  132. dist = distance(s1, s2)
  133. if dist == -1:
  134. return -1
  135. set1 = set(s1)
  136. score = len(set1.intersection(s2)) / len(set1)
  137. score -= dist / len(s1)
  138. score -= len(set1.symmetric_difference(s2)) / (len(s1) + len(s2))
  139. return score
  140. def text_from_html(html_fragment, collapse_whitespace=False):
  141. """
  142. Returns the plain non-tag text from an html
  143. :param html_fragment: document from which text must be extracted
  144. :return: text extracted from the html
  145. """
  146. # lxml requires one single root element
  147. tree = etree.fromstring('<p>%s</p>' % html_fragment, etree.XMLParser(recover=True))
  148. content = ' '.join(tree.itertext())
  149. if collapse_whitespace:
  150. content = re.sub('\\s+', ' ', content).strip()
  151. return content
  152. def get_base_domain(url, strip_www=False):
  153. """
  154. Returns the domain of a given url without the scheme and the www. and the
  155. final '/' if any.
  156. :param url: url from which the domain must be extracted
  157. :param strip_www: if True, strip the www. from the domain
  158. :return: domain of the url
  159. """
  160. if not url:
  161. return ''
  162. url = werkzeug.urls.url_parse(url).netloc
  163. if strip_www and url.startswith('www.'):
  164. url = url[4:]
  165. return url