rules.py 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807
  1. # Copyright 2013-2015 The Distro Tracker Developers
  2. # See the COPYRIGHT file at the top-level directory of this distribution and
  3. # at http://deb.li/DTAuthors
  4. #
  5. # This file is part of Distro Tracker. It is subject to the license terms
  6. # in the LICENSE file found in the top-level directory of this
  7. # distribution and at http://deb.li/DTLicense. No part of Distro Tracker,
  8. # including this file, may be copied, modified, propagated, or distributed
  9. # except according to the terms contained in the LICENSE file.
  10. from __future__ import unicode_literals
  11. import os.path
  12. import re
  13. import requests
  14. from django import forms
  15. from django.utils.http import urlencode, urlquote
  16. from django.utils.safestring import mark_safe
  17. from django.conf import settings
  18. from distro_tracker.core.models import PackageBugStats
  19. from distro_tracker.core.models import EmailNews
  20. from distro_tracker.core.models import PackageName
  21. from distro_tracker.core.models import SourcePackageName
  22. from distro_tracker.core.models import BinaryPackageBugStats
  23. from distro_tracker.core.models import PackageExtractedInfo
  24. from distro_tracker.core.models import UserEmail
  25. from distro_tracker.core.utils import get_decoded_message_payload
  26. from distro_tracker.core.utils import get_or_none
  27. from distro_tracker.core.utils.http import HttpCache
  28. from distro_tracker.core.utils.packages import package_hashdir
  29. from distro_tracker.mail import mail_news
  30. from .models import DebianContributor
  31. from distro_tracker.vendor.common import PluginProcessingError
  32. from distro_tracker.vendor.debian.tracker_tasks import UpdateNewQueuePackages
  33. def _simplify_pkglist(pkglist, multi_allowed=True, default=None):
  34. """Replace a single-list item by its sole item. A longer list is left
  35. as-is (provided multi_allowed is True). An empty list returns the default
  36. value."""
  37. if len(pkglist) == 1 and pkglist[0]:
  38. return pkglist[0]
  39. elif len(pkglist) > 1 and multi_allowed:
  40. return pkglist
  41. return default
  42. def _classify_bts_message(msg, package, keyword):
  43. bts_package = msg.get('X-Debian-PR-Source',
  44. msg.get('X-Debian-PR-Package', ''))
  45. pkglist = re.split(r'\s+', bts_package.strip())
  46. # Don't override default package assignation when we find multiple package
  47. # associated to the mail, otherwise we will send multiple copies of a mail
  48. # that we already receive multiple times
  49. multi_allowed = package is None
  50. pkg_result = _simplify_pkglist(pkglist, multi_allowed=multi_allowed,
  51. default=package)
  52. # We override the package/keyword only...
  53. if package is None: # When needed, because we don't have a suggestion
  54. override_suggestion = True
  55. else: # Or when package suggestion matches the one found in the header
  56. override_suggestion = package == pkg_result
  57. if override_suggestion:
  58. package = pkg_result
  59. if override_suggestion or keyword is None:
  60. debian_pr_message = msg.get('X-Debian-PR-Message', '')
  61. if debian_pr_message.startswith('transcript'):
  62. keyword = 'bts-control'
  63. else:
  64. keyword = 'bts'
  65. return (package, keyword)
  66. def _classify_dak_message(msg, package, keyword):
  67. package = msg.get('X-Debian-Package', package)
  68. subject = msg.get('Subject', '')
  69. xdak = msg.get('X-DAK', '')
  70. body = _get_message_body(msg)
  71. if re.search(r'^Accepted|ACCEPTED', subject):
  72. if re.search(r'^Accepted.*\(.*source.*\)', subject):
  73. mail_news.create_news(msg, package, create_package=True)
  74. if re.search(r'\.dsc\s*$', body, flags=re.MULTILINE):
  75. keyword = 'upload-source'
  76. else:
  77. keyword = 'upload-binary'
  78. else:
  79. keyword = 'archive'
  80. if xdak == 'dak rm':
  81. # Find all lines giving information about removed source packages
  82. re_rmline = re.compile(r"^\s*(\S+)\s*\|\s*(\S+)\s*\|.*source", re.M)
  83. source_removals = re_rmline.findall(body)
  84. pkglist = []
  85. for pkgname, version in source_removals:
  86. pkglist.append(pkgname)
  87. create_dak_rm_news(msg, pkgname, version=version, body=body)
  88. package = _simplify_pkglist(pkglist, default=package)
  89. return (package, keyword)
  90. def classify_message(msg, package, keyword):
  91. # Default values for git commit notifications
  92. xgitrepo = msg.get('X-Git-Repo')
  93. if xgitrepo:
  94. if not package:
  95. if xgitrepo.endswith('.git'):
  96. xgitrepo = xgitrepo[:-4]
  97. package = os.path.basename(xgitrepo)
  98. if not keyword:
  99. keyword = 'vcs'
  100. xloop = msg.get_all('X-Loop', ())
  101. xdebian = msg.get_all('X-Debian', ())
  102. testing_watch = msg.get('X-Testing-Watch-Package')
  103. bts_match = 'owner@bugs.debian.org' in xloop
  104. dak_match = 'DAK' in xdebian
  105. if bts_match: # This is a mail of the Debian bug tracking system
  106. package, keyword = _classify_bts_message(msg, package, keyword)
  107. elif dak_match:
  108. package, keyword = _classify_dak_message(msg, package, keyword)
  109. elif testing_watch:
  110. package = testing_watch
  111. keyword = 'summary'
  112. mail_news.create_news(msg, package)
  113. # Converts old PTS keywords into new ones
  114. legacy_mapping = {
  115. 'katie-other': 'archive',
  116. 'buildd': 'build',
  117. 'ddtp': 'translation',
  118. 'cvs': 'vcs',
  119. }
  120. if keyword in legacy_mapping:
  121. keyword = legacy_mapping[keyword]
  122. return (package, keyword)
  123. def add_new_headers(received_message, package_name, keyword):
  124. """
  125. Debian adds the following new headers:
  126. - X-Debian-Package
  127. - X-Debian
  128. :param received_message: The original received package message
  129. :type received_message: :py:class:`email.message.Message`
  130. :param package_name: The name of the package for which the message was
  131. intended
  132. :type package_name: string
  133. :param keyword: The keyword with which the message is tagged.
  134. :type keyword: string
  135. """
  136. new_headers = [
  137. ('X-Debian-Package', package_name),
  138. ('X-Debian', 'tracker.debian.org'),
  139. ('X-PTS-Package', package_name), # for compat with old PTS
  140. ('X-PTS-Keyword', keyword), # for compat with old PTS
  141. ]
  142. return new_headers
  143. def approve_default_message(msg):
  144. """
  145. Debian approves a default message only if it has a X-Bugzilla-Product
  146. header.
  147. :param msg: The original received package message
  148. :type msg: :py:class:`email.message.Message`
  149. """
  150. return 'X-Bugzilla-Product' in msg
  151. def _get_message_body(msg):
  152. """
  153. Returns the message body, joining together all parts into one string.
  154. :param msg: The original received package message
  155. :type msg: :py:class:`email.message.Message`
  156. """
  157. return '\n'.join(get_decoded_message_payload(part)
  158. for part in msg.walk() if not part.is_multipart())
  159. def get_pseudo_package_list():
  160. """
  161. Existing pseudo packages for Debian are obtained from
  162. `BTS <https://bugs.debian.org/pseudo-packages.maintainers>`_
  163. """
  164. PSEUDO_PACKAGE_LIST_URL = (
  165. 'https://bugs.debian.org/pseudo-packages.maintainers'
  166. )
  167. cache = HttpCache(settings.DISTRO_TRACKER_CACHE_DIRECTORY)
  168. if not cache.is_expired(PSEUDO_PACKAGE_LIST_URL):
  169. return
  170. response, updated = cache.update(PSEUDO_PACKAGE_LIST_URL)
  171. try:
  172. response.raise_for_status()
  173. except requests.exceptions.HTTPError:
  174. raise PluginProcessingError()
  175. if not updated:
  176. return
  177. return [
  178. line.split(None, 1)[0]
  179. for line in response.text.splitlines()
  180. ]
  181. def get_package_information_site_url(package_name, source_package=False,
  182. repository=None, version=None):
  183. """
  184. Return a link pointing to more information about a package in a
  185. given repository.
  186. """
  187. BASE_URL = 'https://packages.debian.org/'
  188. PU_URL = 'https://release.debian.org/proposed-updates/'
  189. SOURCE_PACKAGE_URL_TEMPLATES = {
  190. 'repository': BASE_URL + 'source/{repo}/{package}',
  191. 'no-repository': BASE_URL + 'src:{package}',
  192. 'pu': PU_URL + '{targetsuite}.html#{package}_{version}',
  193. }
  194. BINARY_PACKAGE_URL_TEMPLATES = {
  195. 'repository': BASE_URL + '{repo}/{package}',
  196. 'no-repository': BASE_URL + '{package}',
  197. 'pu': '',
  198. }
  199. params = {'package': package_name}
  200. if repository:
  201. suite = repository['suite'] or repository['codename']
  202. if suite.endswith('proposed-updates'):
  203. url_type = 'pu'
  204. params['version'] = version
  205. params['targetsuite'] = suite.replace('-proposed-updates', '')\
  206. .replace('proposed-updates', 'stable')
  207. else:
  208. url_type = 'repository'
  209. params['repo'] = suite
  210. else:
  211. url_type = 'no-repository'
  212. if source_package:
  213. template = SOURCE_PACKAGE_URL_TEMPLATES[url_type]
  214. else:
  215. template = BINARY_PACKAGE_URL_TEMPLATES[url_type]
  216. return template.format(**params)
  217. def get_developer_information_url(developer_email):
  218. """
  219. The function returns a URL which displays extra information about a
  220. developer, given his email.
  221. """
  222. URL_TEMPLATE = 'https://qa.debian.org/developer.php?email={email}'
  223. return URL_TEMPLATE.format(email=urlquote(developer_email))
  224. def get_external_version_information_urls(package_name):
  225. """
  226. The function returns a list of external Web resources which provide
  227. additional information about the versions of a package.
  228. """
  229. return [
  230. {
  231. 'url': 'https://qa.debian.org/madison.php?package={package}'.format(
  232. package=package_name),
  233. 'description': 'more versions can be listed by madison',
  234. },
  235. {
  236. 'url': 'http://snapshot.debian.org/package/{package}/'.format(
  237. package=package_name),
  238. 'description': 'old versions available from snapshot.debian.org',
  239. }
  240. ]
  241. def get_maintainer_extra(developer_email, package_name=None):
  242. """
  243. The function returns a list of additional items that are to be
  244. included in the general panel next to the maintainer. This includes:
  245. - Whether the maintainer agrees with lowthreshold NMU
  246. - Whether the maintainer is a Debian Maintainer
  247. """
  248. developer = get_or_none(DebianContributor,
  249. email__email__iexact=developer_email)
  250. extra = []
  251. _add_dmd_entry(extra, developer_email)
  252. if developer and developer.agree_with_low_threshold_nmu:
  253. extra.append({
  254. 'display': 'LowNMU',
  255. 'description': 'maintainer agrees with Low Threshold NMU',
  256. 'link': 'https://wiki.debian.org/LowThresholdNmu',
  257. })
  258. _add_dm_entry(extra, developer, package_name)
  259. return extra
  260. def get_uploader_extra(developer_email, package_name=None):
  261. """
  262. The function returns a list of additional items that are to be
  263. included in the general panel next to an uploader. This includes:
  264. - Whether the uploader is a DebianMaintainer
  265. """
  266. developer = get_or_none(DebianContributor,
  267. email__email__iexact=developer_email)
  268. extra = []
  269. _add_dmd_entry(extra, developer_email)
  270. _add_dm_entry(extra, developer, package_name)
  271. return extra
  272. def _add_dmd_entry(extra, email):
  273. extra.append({
  274. 'display': 'DMD',
  275. 'description': 'UDD\'s Debian Maintainer Dashboard',
  276. 'link': 'https://udd.debian.org/dmd/?{email}#todo'.format(
  277. email=urlquote(email)
  278. )
  279. })
  280. def _add_dm_entry(extra, developer, package_name):
  281. if package_name and developer and developer.is_debian_maintainer:
  282. if package_name in developer.allowed_packages:
  283. extra.append({'display': 'dm'})
  284. def allow_package(stanza):
  285. """
  286. The function provides a way for vendors to exclude some packages from being
  287. saved in the database.
  288. In Debian's case, this is done for packages where the ``Extra-Source-Only``
  289. is set since those packages are in the repository only for various
  290. compliance reasons.
  291. :param stanza: The raw package entry from a ``Sources`` file.
  292. :type stanza: case-insensitive dict
  293. """
  294. return 'Extra-Source-Only' not in stanza
  295. def get_bug_tracker_url(package_name, package_type, category_name):
  296. """
  297. Returns a URL to the BTS for the given package for the given bug category
  298. name.
  299. The following categories are recognized for Debian's implementation:
  300. - ``all`` - all bugs for the package
  301. - ``all-merged`` - all bugs, including the merged ones
  302. - ``rc`` - release critical bugs
  303. - ``rc-merged`` - release critical bugs, including the merged ones
  304. - ``normal`` - bugs tagged as normal and important
  305. - ``normal`` - bugs tagged as normal and important, including the merged
  306. ones
  307. - ``wishlist`` - bugs tagged as wishlist and minor
  308. - ``wishlist-merged`` - bugs tagged as wishlist and minor, including the
  309. merged ones
  310. - ``fixed`` - bugs tagged as fixed and pending
  311. - ``fixed-merged`` - bugs tagged as fixed and pending, including the merged
  312. ones
  313. :param package_name: The name of the package for which the BTS link should
  314. be provided.
  315. :param package_type: The type of the package for which the BTS link should
  316. be provided. For Debian this is one of: ``source``, ``pseudo``,
  317. ``binary``.
  318. :param category_name: The name of the bug category for which the BTS
  319. link should be provided. It is one of the categories listed above.
  320. :rtype: :class:`string` or ``None`` if there is no BTS bug for the given
  321. category.
  322. """
  323. URL_PARAMETERS = {
  324. 'all': (
  325. ('repeatmerged', 'no'),
  326. ),
  327. 'rc': (
  328. ('archive', 'no'),
  329. ('pend-exc', 'pending-fixed'),
  330. ('pend-exc', 'fixed'),
  331. ('pend-exc', 'done'),
  332. ('sev-inc', 'critical'),
  333. ('sev-inc', 'grave'),
  334. ('sev-inc', 'serious'),
  335. ('repeatmerged', 'no'),
  336. ),
  337. 'normal': (
  338. ('archive', 'no'),
  339. ('pend-exc', 'pending-fixed'),
  340. ('pend-exc', 'fixed'),
  341. ('pend-exc', 'done'),
  342. ('sev-inc', 'important'),
  343. ('sev-inc', 'normal'),
  344. ('repeatmerged', 'no'),
  345. ),
  346. 'wishlist': (
  347. ('archive', 'no'),
  348. ('pend-exc', 'pending-fixed'),
  349. ('pend-exc', 'fixed'),
  350. ('pend-exc', 'done'),
  351. ('sev-inc', 'minor'),
  352. ('sev-inc', 'wishlist'),
  353. ('repeatmerged', 'no'),
  354. ),
  355. 'fixed': (
  356. ('archive', 'no'),
  357. ('pend-inc', 'pending-fixed'),
  358. ('pend-inc', 'fixed'),
  359. ('repeatmerged', 'no'),
  360. ),
  361. 'patch': (
  362. ('include', 'tags:patch'),
  363. ('exclude', 'tags:pending'),
  364. ('pend-exc', 'done'),
  365. ('repeatmerged', 'no'),
  366. ),
  367. 'help': (
  368. ('tag', 'help'),
  369. ('pend-exc', 'pending-fixed'),
  370. ('pend-exc', 'fixed'),
  371. ('pend-exc', 'done'),
  372. ),
  373. 'newcomer': (
  374. ('tag', 'newcomer'),
  375. ('pend-exc', 'pending-fixed'),
  376. ('pend-exc', 'fixed'),
  377. ('pend-exc', 'done'),
  378. ),
  379. 'all-merged': (
  380. ('repeatmerged', 'yes'),
  381. ),
  382. 'rc-merged': (
  383. ('archive', 'no'),
  384. ('pend-exc', 'pending-fixed'),
  385. ('pend-exc', 'fixed'),
  386. ('pend-exc', 'done'),
  387. ('sev-inc', 'critical'),
  388. ('sev-inc', 'grave'),
  389. ('sev-inc', 'serious'),
  390. ('repeatmerged', 'yes'),
  391. ),
  392. 'normal-merged': (
  393. ('archive', 'no'),
  394. ('pend-exc', 'pending-fixed'),
  395. ('pend-exc', 'fixed'),
  396. ('pend-exc', 'done'),
  397. ('sev-inc', 'important'),
  398. ('sev-inc', 'normal'),
  399. ('repeatmerged', 'yes'),
  400. ),
  401. 'wishlist-merged': (
  402. ('archive', 'no'),
  403. ('pend-exc', 'pending-fixed'),
  404. ('pend-exc', 'fixed'),
  405. ('pend-exc', 'done'),
  406. ('sev-inc', 'minor'),
  407. ('sev-inc', 'wishlist'),
  408. ('repeatmerged', 'yes'),
  409. ),
  410. 'fixed-merged': (
  411. ('archive', 'no'),
  412. ('pend-inc', 'pending-fixed'),
  413. ('pend-inc', 'fixed'),
  414. ('repeatmerged', 'yes'),
  415. ),
  416. 'patch-merged': (
  417. ('include', 'tags:patch'),
  418. ('exclude', 'tags:pending'),
  419. ('pend-exc', 'done'),
  420. ('repeatmerged', 'yes'),
  421. ),
  422. }
  423. if category_name not in URL_PARAMETERS:
  424. return
  425. domain = 'https://bugs.debian.org/'
  426. query_parameters = URL_PARAMETERS[category_name]
  427. if package_type == 'source':
  428. query_parameters += (('src', package_name),)
  429. elif package_type == 'binary':
  430. if category_name == 'all':
  431. # All bugs for a binary package don't follow the same pattern as
  432. # the rest of the URLs.
  433. return domain + package_name
  434. query_parameters += (('which', 'pkg'),)
  435. query_parameters += (('data', package_name),)
  436. return (
  437. domain +
  438. 'cgi-bin/pkgreport.cgi?' +
  439. urlencode(query_parameters)
  440. )
  441. def get_bug_panel_stats(package_name):
  442. """
  443. Returns bug statistics which are to be displayed in the bugs panel
  444. (:class:`BugsPanel <distro_tracker.core.panels.BugsPanel>`).
  445. Debian wants to include the merged bug count for each bug category
  446. (but only if the count is different than non-merged bug count) so this
  447. function is used in conjunction with a custom bug panel template which
  448. displays this bug count in parentheses next to the non-merged count.
  449. Each bug category count (merged and non-merged) is linked to a URL in the
  450. BTS which displays more information about the bugs found in that category.
  451. A verbose name is included for each of the categories.
  452. The function includes a URL to a bug history graph which is displayed in
  453. the rendered template.
  454. """
  455. bug_stats = get_or_none(PackageBugStats, package__name=package_name)
  456. if not bug_stats:
  457. return
  458. # Map category names to their bug panel display names and descriptions
  459. category_descriptions = {
  460. 'rc': {
  461. 'display_name': 'RC',
  462. 'description': 'Release Critical',
  463. },
  464. 'normal': {
  465. 'display_name': 'I&N',
  466. 'description': 'Important and Normal',
  467. },
  468. 'wishlist': {
  469. 'display_name': 'M&W',
  470. 'description': 'Minor and Wishlist',
  471. },
  472. 'fixed': {
  473. 'display_name': 'F&P',
  474. 'description': 'Fixed and Pending',
  475. },
  476. 'newcomer': {
  477. 'display_name': 'newcomer',
  478. 'description': 'newcomer',
  479. 'link': 'https://wiki.debian.org/qa.debian.org/GiftTag',
  480. }
  481. }
  482. # Some bug categories should not be included in the count.
  483. exclude_from_count = ('newcomer',)
  484. stats = bug_stats.stats
  485. categories = []
  486. total, total_merged = 0, 0
  487. # From all known bug stats, extract only the ones relevant for the panel
  488. for category in stats:
  489. category_name = category['category_name']
  490. if category_name not in category_descriptions.keys():
  491. continue
  492. # Add main bug count
  493. category_stats = {
  494. 'category_name': category['category_name'],
  495. 'bug_count': category['bug_count'],
  496. }
  497. # Add merged bug count
  498. if 'merged_count' in category:
  499. if category['merged_count'] != category['bug_count']:
  500. category_stats['merged'] = {
  501. 'bug_count': category['merged_count'],
  502. }
  503. # Add descriptions
  504. category_stats.update(category_descriptions[category_name])
  505. categories.append(category_stats)
  506. # Keep a running total of all and all-merged bugs
  507. if category_name not in exclude_from_count:
  508. total += category['bug_count']
  509. total_merged += category.get('merged_count', 0)
  510. # Add another "category" with the bug totals.
  511. all_category = {
  512. 'category_name': 'all',
  513. 'display_name': 'all',
  514. 'bug_count': total,
  515. }
  516. if total != total_merged:
  517. all_category['merged'] = {
  518. 'bug_count': total_merged,
  519. }
  520. # The totals are the first displayed row.
  521. categories.insert(0, all_category)
  522. # Add URLs for all categories
  523. for category in categories:
  524. # URL for the non-merged category
  525. url = get_bug_tracker_url(
  526. package_name, 'source', category['category_name'])
  527. category['url'] = url
  528. # URL for the merged category
  529. if 'merged' in category:
  530. url_merged = get_bug_tracker_url(
  531. package_name, 'source', category['category_name'] + '-merged')
  532. category['merged']['url'] = url_merged
  533. # Debian also includes a custom graph of bug history
  534. graph_url = (
  535. 'https://qa.debian.org/data/bts/graphs/'
  536. '{package_hash}/{package_name}.png'
  537. )
  538. # Final context variables which are available in the template
  539. return {
  540. 'categories': categories,
  541. 'graph_url': graph_url.format(
  542. package_hash=package_hashdir(package_name),
  543. package_name=package_name),
  544. }
  545. def get_binary_package_bug_stats(binary_name):
  546. """
  547. Returns the bug statistics for the given binary package.
  548. Debian's implementation filters out some of the stored bug category stats.
  549. It also provides a different, more verbose, display name for each of them.
  550. The included categories and their names are:
  551. - rc - critical, grave serious
  552. - normal - important and normal
  553. - wishlist - wishlist and minor
  554. - fixed - pending and fixed
  555. """
  556. stats = get_or_none(BinaryPackageBugStats, package__name=binary_name)
  557. if stats is None:
  558. return
  559. category_descriptions = {
  560. 'rc': {
  561. 'display_name': 'critical, grave and serious',
  562. },
  563. 'normal': {
  564. 'display_name': 'important and normal',
  565. },
  566. 'wishlist': {
  567. 'display_name': 'wishlist and minor',
  568. },
  569. 'fixed': {
  570. 'display_name': 'pending and fixed',
  571. },
  572. }
  573. def extend_category(category, extra_parameters):
  574. category.update(extra_parameters)
  575. return category
  576. # Filter the bug stats to only include some categories and add a custom
  577. # display name for each of them.
  578. return [
  579. extend_category(category,
  580. category_descriptions[category['category_name']])
  581. for category in stats.stats
  582. if category['category_name'] in category_descriptions.keys()
  583. ]
  584. def create_dak_rm_news(message, package, body=None, version=''):
  585. if not body:
  586. body = get_decoded_message_payload(message)
  587. suite = re.search(r"have been removed from (\S+):", body).group(1)
  588. title = "Removed {ver} from {suite}".format(ver=version, suite=suite)
  589. return mail_news.create_news(message, package, title=title)
  590. def _create_news_from_dak_email(message):
  591. x_dak = message['X-DAK']
  592. katie = x_dak.split()[1]
  593. if katie != 'rm':
  594. # Only rm mails are processed.
  595. return
  596. body = get_decoded_message_payload(message)
  597. if not body:
  598. # The content cannot be decoded.
  599. return
  600. # Find all lines giving information about removed source packages
  601. re_rmline = re.compile(r"^\s*(\S+)\s*\|\s*(\S+)\s*\|.*source", re.M)
  602. source_removals = re_rmline.findall(body)
  603. # Add a news item for each source removal.
  604. created_news = []
  605. for removal in source_removals:
  606. package_name, version = removal
  607. created_news.append(
  608. create_dak_rm_news(message, package_name, body=body,
  609. version=version))
  610. return created_news
  611. def create_news_from_email_message(message):
  612. """
  613. In Debian's implementation, this function creates news when the received
  614. mail's origin is either the testing watch or katie.
  615. """
  616. subject = message.get("Subject", None)
  617. if not subject:
  618. return
  619. subject_words = subject.split()
  620. # Source upload?
  621. if len(subject_words) > 1 and subject_words[0] in ('Accepted', 'Installed'):
  622. if 'source' not in subject:
  623. # Only source uploads should be considered.
  624. return
  625. package_name = subject_words[1]
  626. package, _ = PackageName.objects.get_or_create(name=package_name)
  627. return [EmailNews.objects.create_email_news(message, package)]
  628. # DAK rm?
  629. elif 'X-DAK' in message:
  630. return _create_news_from_dak_email(message)
  631. # Testing Watch?
  632. elif 'X-Testing-Watch-Package' in message:
  633. package_name = message['X-Testing-Watch-Package']
  634. package = get_or_none(SourcePackageName, name=package_name)
  635. if not package:
  636. # This package is not tracked
  637. return
  638. title = message.get('Subject', '')
  639. if not title:
  640. title = 'Testing Watch Message'
  641. return [
  642. EmailNews.objects.create_email_news(
  643. title=title,
  644. message=message,
  645. package=package,
  646. created_by='Britney')
  647. ]
  648. def get_extra_versions(package):
  649. """
  650. :returns: The versions of the package found in the NEW queue.
  651. """
  652. try:
  653. info = package.packageextractedinfo_set.get(
  654. key=UpdateNewQueuePackages.EXTRACTED_INFO_KEY)
  655. except PackageExtractedInfo.DoesNotExist:
  656. return
  657. version_url_template = 'https://ftp-master.debian.org/new/{pkg}_{ver}.html'
  658. return [
  659. {
  660. 'version': ver['version'],
  661. 'repository_shorthand': 'NEW/' + dist,
  662. 'version_link': version_url_template.format(
  663. pkg=package.name, ver=ver['version']),
  664. 'repository_link': 'https://ftp-master.debian.org/new.html',
  665. }
  666. for dist, ver in info.value.items()
  667. ]
  668. def pre_login(form):
  669. """
  670. If the user has a @debian.org email associated, don't let him log in
  671. directly through local authentication.
  672. """
  673. username = form.cleaned_data.get('username')
  674. if not username:
  675. return
  676. user_email = get_or_none(UserEmail, email__iexact=username)
  677. emails = [username]
  678. if user_email and user_email.user:
  679. emails += [x.email for x in user_email.user.emails.all()]
  680. if any(email.endswith('@debian.org') for email in emails):
  681. raise forms.ValidationError(mark_safe(
  682. "Your account has a @debian.org email address associated. "
  683. "To log in to the package tracker, you must use a SSL client "
  684. "certificate generated on "
  685. "<a href='https://sso.debian.org/spkac/enroll/'>"
  686. "sso.debian.org</a> (click on the link!)."))
  687. def post_logout(request, user, next_url=None):
  688. """
  689. If the user is authenticated via the SSO, sign him out at the SSO level too.
  690. """
  691. if request.META.get('REMOTE_USER'):
  692. if next_url is None:
  693. next_url = 'https://' + settings.DISTRO_TRACKER_FQDN
  694. elif next_url.startswith('/'):
  695. next_url = 'https://' + settings.DISTRO_TRACKER_FQDN + next_url
  696. return (
  697. 'https://sso.debian.org/cgi-bin/dacs/dacs_signout?' + urlencode({
  698. 'SIGNOUT_HANDLER': next_url
  699. })
  700. )