keywords.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
  1. # Copyright 2013 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. """
  11. Implements all commands which deal with message keywords.
  12. """
  13. from __future__ import unicode_literals
  14. from distro_tracker.mail.control.commands.base import Command
  15. from distro_tracker.core.models import (
  16. Subscription, UserEmail, EmailSettings, PackageName, Keyword)
  17. from distro_tracker.core.utils import get_or_none
  18. import re
  19. __all__ = (
  20. 'ViewDefaultKeywordsCommand',
  21. 'ViewPackageKeywordsCommand',
  22. 'SetDefaultKeywordsCommand',
  23. 'SetPackageKeywordsCommand',
  24. 'KeywordCommandMixin',
  25. )
  26. class KeywordCommandMixin(object):
  27. """
  28. A mixin including some utility methods for commands which handle keywords.
  29. """
  30. def error_not_subscribed(self, email, package_name):
  31. """
  32. Helper returns an error saying the user is not subscribed to the
  33. package.
  34. :param email: The email of the user which is not subscribed to the
  35. package.
  36. :param package_name: The name of the package the user is not subscribed
  37. to.
  38. """
  39. self.error('{email} is not subscribed to the package {package}'.format(
  40. email=email,
  41. package=package_name)
  42. )
  43. def get_subscription(self, email, package_name):
  44. """
  45. Helper method returning a
  46. :py:class:`Subscription <distro_tracker.core.models.Subscription>`
  47. instance for the given package and user.
  48. It logs any errors found while retrieving this instance, such as the
  49. user not being subscribed to the given package.
  50. :param email: The email of the user.
  51. :param package_name: The name of the package.
  52. """
  53. user_email = get_or_none(UserEmail, email__iexact=email)
  54. email_settings = get_or_none(EmailSettings, user_email=user_email)
  55. if not user_email or not email_settings:
  56. self.error_not_subscribed(email, package_name)
  57. return
  58. package = get_or_none(PackageName, name=package_name)
  59. if not package:
  60. self.error('Package {package} does not exist'.format(
  61. package=package_name))
  62. return
  63. subscription = get_or_none(Subscription,
  64. package=package,
  65. email_settings=email_settings)
  66. if not subscription:
  67. self.error_not_subscribed(email, package_name)
  68. return subscription
  69. def keyword_name_to_object(self, keyword_name):
  70. """
  71. Takes a keyword name and returns a
  72. :py:class:`Keyword <distro_tracker.core.models.Keyword>` object with
  73. the given name if it exists. If not, a warning is added to the commands'
  74. output.
  75. :param keyword_name: The name of the keyword to be retrieved.
  76. :rtype: :py:class:`Keyword <distro_tracker.core.models.Keyword>` or
  77. ``None``
  78. """
  79. keyword = get_or_none(Keyword, name=keyword_name)
  80. if not keyword:
  81. self.warn('{keyword} is not a valid keyword'.format(
  82. keyword=keyword_name))
  83. return keyword
  84. def add_keywords(self, keywords, manager):
  85. """
  86. Adds the keywords given in the iterable ``keywords`` to the ``manager``
  87. :param keywords: The keywords to be added to the ``manager``
  88. :type keywords: any iterable containing
  89. :py:class:`Keyword <distro_tracker.core.models.Keyword>` instances
  90. :param manager: The manager to which the keywords should be added.
  91. :type manager: :py:class:`Manager <django.db.models.Manager>`
  92. """
  93. for keyword_name in keywords:
  94. keyword = self.keyword_name_to_object(keyword_name)
  95. if keyword:
  96. manager.add(keyword)
  97. def remove_keywords(self, keywords, manager):
  98. """
  99. Removes the keywords given in the iterable ``keywords`` from the
  100. ``manager``.
  101. :param keywords: The keywords to be removed from the ``manager``
  102. :type keywords: any iterable containing
  103. :py:class:`Keyword <distro_tracker.core.models.Keyword>` instances
  104. :param manager: The manager from which the keywords should be removed.
  105. :type manager: :py:class:`Manager <django.db.models.Manager>`
  106. """
  107. for keyword_name in keywords:
  108. keyword = self.keyword_name_to_object(keyword_name)
  109. if keyword:
  110. manager.remove(keyword)
  111. def set_keywords(self, keywords, manager):
  112. """
  113. Sets the keywords given in the iterable ``keywords`` to the ``manager``
  114. so that they are the only keywords it contains.
  115. :param keywords: The keywords to be set to the ``manager``
  116. :type keywords: any iterable containing
  117. :py:class:`Keyword <distro_tracker.core.models.Keyword>` instances
  118. :param manager: The manager to which the keywords should be added.
  119. :type manager: :py:class:`Manager <django.db.models.Manager>`
  120. """
  121. manager.clear()
  122. self.add_keywords(keywords, manager)
  123. OPERATIONS = {
  124. '+': add_keywords,
  125. '-': remove_keywords,
  126. '=': set_keywords,
  127. }
  128. """
  129. Maps symbols to operations. When the symbol is found in a keyword command
  130. the given operation is called.
  131. - '+': :py:meth:`add_keywords`
  132. - '-': :py:meth:`remove_keywords`
  133. - '=': :py:meth:`set_keywords`
  134. """
  135. class ViewDefaultKeywordsCommand(Command, KeywordCommandMixin):
  136. """
  137. Implementation of the keyword command which handles displaying a list
  138. of the user's default keywords.
  139. """
  140. META = {
  141. 'position': 10,
  142. 'name': 'view-default-keywords',
  143. 'aliases': ['keyword', 'tag', 'keywords', 'tags'],
  144. 'description': '''keyword [<email>]
  145. Tells you the keywords you are accepting by default for packages
  146. with no specific keywords set.
  147. Each mail sent through the Distro Tracker is associated
  148. to a keyword and you receive only the mails associated to keywords
  149. you are accepting.
  150. You may select a different set of keywords for each package.'''
  151. }
  152. REGEX_LIST = (
  153. r'(?:\s+(?P<email>\S+@\S+))?$',
  154. )
  155. def __init__(self, email):
  156. super(ViewDefaultKeywordsCommand, self).__init__()
  157. self.email = email
  158. def get_command_text(self):
  159. return super(ViewDefaultKeywordsCommand, self).get_command_text(
  160. self.email)
  161. def handle(self):
  162. user_email, _ = UserEmail.objects.get_or_create(email=self.email)
  163. email_settings, _ = \
  164. EmailSettings.objects.get_or_create(user_email=user_email)
  165. self.reply(
  166. "Here's the default list of accepted keywords for {email}:".format(
  167. email=self.email))
  168. self.list_reply(sorted(
  169. keyword.name for keyword in email_settings.default_keywords.all()))
  170. class ViewPackageKeywordsCommand(Command, KeywordCommandMixin):
  171. """
  172. Implementation of the keyword command version which handles listing
  173. all keywords associated to a package for a particular user.
  174. """
  175. META = {
  176. 'position': 11,
  177. 'name': 'view-package-keywords',
  178. 'aliases': ['keyword', 'keywords', 'tag', 'tags'],
  179. 'description': '''keyword <srcpackage> [<email>]
  180. Tells you the keywords you are accepting for the given package.
  181. Each mail sent through Distro Tracker is associated
  182. to a keyword and you receive only the mails associated to keywords
  183. you are accepting.
  184. You may select a different set of keywords for each package.'''
  185. }
  186. REGEX_LIST = (
  187. r'\s+(?P<package>\S+)(?:\s+(?P<email>\S+@\S+))?$',
  188. )
  189. def __init__(self, package, email):
  190. super(ViewPackageKeywordsCommand, self).__init__()
  191. self.package = package
  192. self.email = email
  193. def get_command_text(self):
  194. return super(ViewPackageKeywordsCommand, self).get_command_text(
  195. self.package,
  196. self.email)
  197. def handle(self):
  198. subscription = self.get_subscription(self.email, self.package)
  199. if not subscription:
  200. return
  201. self.reply(
  202. "Here's the list of accepted keywords associated to package")
  203. self.reply('{package} for {user}'.format(package=self.package,
  204. user=self.email))
  205. self.list_reply(sorted(
  206. keyword.name for keyword in subscription.keywords.all()))
  207. class SetDefaultKeywordsCommand(Command, KeywordCommandMixin):
  208. """
  209. Implementation of the keyword command which handles modifying a user's
  210. list of default keywords.
  211. """
  212. META = {
  213. 'position': 12,
  214. 'name': 'set-default-keywords',
  215. 'aliases': ['keyword', 'keywords', 'tag', 'tags'],
  216. 'description': '''keyword [<email>] {+|-|=} <list of keywords>
  217. Accept (+) or refuse (-) mails associated to the given keyword(s).
  218. Define the list (=) of accepted keywords.
  219. These keywords are applied for subscriptions where no specific
  220. keyword set is given.'''
  221. }
  222. REGEX_LIST = (
  223. (r'(?:\s+(?P<email>\S+@\S+))?\s+(?P<operation>[-+=])'
  224. r'\s+(?P<keywords>\S+(?:\s+\S+)*)$'),
  225. )
  226. def __init__(self, email, operation, keywords):
  227. super(SetDefaultKeywordsCommand, self).__init__()
  228. self.email = email
  229. self.operation = operation
  230. self.keywords = keywords
  231. def get_command_text(self):
  232. return super(SetDefaultKeywordsCommand, self).get_command_text(
  233. self.email,
  234. self.operation,
  235. self.keywords)
  236. def handle(self):
  237. keywords = re.split('[,\s]+', self.keywords)
  238. user_email, _ = UserEmail.objects.get_or_create(email=self.email)
  239. email_settings, _ = \
  240. EmailSettings.objects.get_or_create(user_email=user_email)
  241. operation_method = self.OPERATIONS[self.operation]
  242. operation_method(self, keywords, email_settings.default_keywords)
  243. self.reply(
  244. "Here's the new default list of accepted keywords for "
  245. "{user} :".format(user=self.email))
  246. self.list_reply(sorted(
  247. keyword.name for keyword in email_settings.default_keywords.all()
  248. ))
  249. class SetPackageKeywordsCommand(Command, KeywordCommandMixin):
  250. """
  251. Implementation of the keyword command version which modifies subscription
  252. specific keywords.
  253. """
  254. META = {
  255. 'name': 'set-package-keywords',
  256. 'aliases': ['keyword', 'keywords', 'tag', 'tags'],
  257. 'position': 13,
  258. 'description': (
  259. '''keyword <srcpackage> [<email>] {+|-|=} <list of keywords>
  260. Accept (+) or refuse (-) mails associated to the given keyword(s) for the
  261. given package.
  262. Define the list (=) of accepted keywords.
  263. These keywords take precedence over default keywords.''')
  264. }
  265. REGEX_LIST = (
  266. (r'\s+(?P<package>\S+)(?:\s+(?P<email>\S+@\S+))?\s+'
  267. r'(?P<operation>[-+=])\s+(?P<keywords>\S+(?:\s+\S+)*)$'),
  268. )
  269. def __init__(self, package, email, operation, keywords):
  270. super(SetPackageKeywordsCommand, self).__init__()
  271. self.package = package
  272. self.email = email
  273. self.operation = operation
  274. self.keywords = keywords
  275. def get_command_text(self):
  276. return super(SetPackageKeywordsCommand, self).get_command_text(
  277. self.package,
  278. self.email,
  279. self.operation,
  280. self.keywords)
  281. def handle(self):
  282. """
  283. Actual implementation of the keyword command version which handles
  284. subscription specific keywords.
  285. """
  286. keywords = re.split('[,\s]+', self.keywords)
  287. subscription = self.get_subscription(self.email, self.package)
  288. if not subscription:
  289. return
  290. operation_method = self.OPERATIONS[self.operation]
  291. operation_method(self, keywords, subscription.keywords)
  292. self.reply(
  293. "Here's the new list of accepted keywords associated to package")
  294. self.reply('{package} for {user} :'.format(package=self.package,
  295. user=self.email))
  296. self.list_reply(sorted(
  297. keyword.name for keyword in subscription.keywords.all()))