__init__.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251
  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. Defines and implements all Distro Tracker control commands.
  12. """
  13. from __future__ import unicode_literals
  14. from django.conf import settings
  15. import sys
  16. import inspect
  17. from distro_tracker.core.utils import distro_tracker_render_to_string
  18. from distro_tracker.mail.control.commands.base import Command
  19. from distro_tracker.mail.control.commands.keywords import ( # noqa
  20. ViewDefaultKeywordsCommand,
  21. ViewPackageKeywordsCommand,
  22. SetDefaultKeywordsCommand,
  23. SetPackageKeywordsCommand,
  24. )
  25. from distro_tracker.mail.control.commands.teams import ( # noqa
  26. JoinTeam,
  27. LeaveTeam,
  28. ListTeamPackages,
  29. WhichTeams,
  30. )
  31. from distro_tracker.mail.control.commands.misc import ( # noqa
  32. SubscribeCommand,
  33. UnsubscribeCommand,
  34. WhichCommand,
  35. WhoCommand,
  36. QuitCommand,
  37. UnsubscribeallCommand,
  38. )
  39. from distro_tracker.mail.control.commands.confirmation import ( # noqa
  40. ConfirmCommand
  41. )
  42. MAX_ALLOWED_ERRORS = settings.DISTRO_TRACKER_MAX_ALLOWED_ERRORS_CONTROL_COMMANDS
  43. class HelpCommand(Command):
  44. """
  45. Displays help for all the other commands -- their description.
  46. """
  47. META = {
  48. 'description': '''help
  49. Shows all available commands''',
  50. 'name': 'help',
  51. 'position': 5,
  52. }
  53. REGEX_LIST = (
  54. r'$',
  55. )
  56. def handle(self):
  57. self.reply(distro_tracker_render_to_string('control/help.txt', {
  58. 'descriptions': [
  59. command.META.get('description', '')
  60. for command in UNIQUE_COMMANDS
  61. ],
  62. }))
  63. UNIQUE_COMMANDS = sorted(
  64. (klass
  65. for _, klass in inspect.getmembers(sys.modules[__name__], inspect.isclass)
  66. if klass != Command and issubclass(klass, Command)),
  67. key=lambda cmd: cmd.META.get('position', float('inf'))
  68. )
  69. """
  70. A list of all :py:class:`Command` that are defined.
  71. """
  72. class CommandFactory(object):
  73. """
  74. Creates instances of
  75. :py:class:`Command <distro_tracker.mail.control.commands.base.Command>`
  76. classes based on the given context.
  77. Context is used to fill in parameters when the command has not found
  78. it in the given command line.
  79. """
  80. def __init__(self, context):
  81. #: A dict which is used to fill in parameters' values when they are not
  82. #: found in the command line.
  83. self.context = context
  84. def get_command_function(self, line):
  85. """
  86. Returns a function which executes the functionality of the command
  87. which corresponds to the given arguments.
  88. :param line: The line for which a command function should be returned.
  89. :type line: string
  90. :returns: A callable which when called executes the functionality of a
  91. command matching the given line.
  92. :rtype: :py:class:`Command
  93. <distro_tracker.mail.control.commands.base.Command>` subclass
  94. """
  95. for cmd in UNIQUE_COMMANDS:
  96. # Command exists
  97. match = cmd.match_line(line)
  98. if not match:
  99. continue
  100. kwargs = match.groupdict()
  101. if not kwargs:
  102. # No named patterns found, pass them in the order they were
  103. # matched.
  104. args = match.groups()
  105. return cmd(*args)
  106. else:
  107. # Update the arguments which weren't matched from the given
  108. # context, if available.
  109. kwargs.update({
  110. key: value
  111. for key, value in self.context.items()
  112. if key in kwargs and not kwargs[key] and value
  113. })
  114. command = cmd(**kwargs)
  115. command.context = dict(self.context.items())
  116. return command
  117. class CommandProcessor(object):
  118. """
  119. A class which performs command processing.
  120. """
  121. def __init__(self, factory, confirmed=False):
  122. """
  123. :param factory: Used to obtain
  124. :py:class:`Command
  125. <distro_tracker.mail.control.commands.base.Command>` instances
  126. from command text which is processed.
  127. :type factory: :py:class`CommandFactory` instance
  128. :param confirmed: Indicates whether the commands being executed have
  129. already been confirmed or if those which require confirmation will
  130. be added to the set of commands requiring confirmation.
  131. :type confirmed: Boolean
  132. """
  133. self.factory = factory
  134. self.confirmed = confirmed
  135. self.confirmation_set = None
  136. self.out = []
  137. self.errors = 0
  138. self.processed = set()
  139. def echo_command(self, line):
  140. """
  141. Echoes the line to the command processing output. The line is quoted in
  142. the output.
  143. :param line: The line to be echoed back to the output.
  144. """
  145. self.out.append('> ' + line)
  146. def output(self, text):
  147. """
  148. Include the given line in the command processing output.
  149. :param line: The line of text to be included in the output.
  150. """
  151. self.out.append(text)
  152. def run_command(self, command):
  153. """
  154. Runs the given command.
  155. :param command: The command to be ran.
  156. :type command: :py:class:`Command
  157. <distro_tracker.mail.control.commands.base.Command>`
  158. """
  159. if command.get_command_text() not in self.processed:
  160. # Only process the command if it was not previously processed.
  161. if getattr(command, 'needs_confirmation', False):
  162. command.is_confirmed = self.confirmed
  163. command.confirmation_set = self.confirmation_set
  164. # Now run the command
  165. command_output = command()
  166. if not command_output:
  167. command_output = ''
  168. self.output(command_output)
  169. self.processed.add(command.get_command_text())
  170. def process(self, lines):
  171. """
  172. Processes all the given lines of text which are interpreted as
  173. commands.
  174. :param lines: A list of strings each representing a single line which
  175. is to be regarded as a command.
  176. """
  177. if self.errors == MAX_ALLOWED_ERRORS:
  178. return
  179. for line in lines:
  180. line = line.strip()
  181. self.echo_command(line)
  182. if not line or line.startswith('#'):
  183. continue
  184. command = self.factory.get_command_function(line)
  185. if not command:
  186. self.errors += 1
  187. if self.errors == MAX_ALLOWED_ERRORS:
  188. self.output(
  189. '{MAX_ALLOWED_ERRORS} lines '
  190. 'without commands: stopping.'.format(
  191. MAX_ALLOWED_ERRORS=MAX_ALLOWED_ERRORS))
  192. return
  193. else:
  194. self.run_command(command)
  195. if isinstance(command, QuitCommand):
  196. return
  197. def is_success(self):
  198. """
  199. Checks whether any command was successfully processed.
  200. :returns True: when at least one command is successfully executed
  201. :returns False: when no commands were successfully executed
  202. :rtype: Boolean
  203. """
  204. # Send a response only if there were some commands processed
  205. if self.processed:
  206. return True
  207. else:
  208. return False
  209. def get_output(self):
  210. """
  211. Returns the resulting output of processing all given commands.
  212. :rtype: string
  213. """
  214. return '\n'.join(self.out)