1# Copyright 2013 The Distro Tracker Developers 

2# See the COPYRIGHT file at the top-level directory of this distribution and 

3# at https://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 https://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""" 

12Defines and implements all Distro Tracker control commands. 

13""" 

14 

15import inspect 

16import sys 

17 

18from django.conf import settings 

19 

20from distro_tracker.core.utils import distro_tracker_render_to_string 

21from distro_tracker.mail.control.commands.base import Command 

22from distro_tracker.mail.control.commands.keywords import ( # noqa 

23 ViewDefaultKeywordsCommand, 

24 ViewPackageKeywordsCommand, 

25 SetDefaultKeywordsCommand, 

26 SetPackageKeywordsCommand, 

27) 

28from distro_tracker.mail.control.commands.teams import ( # noqa 

29 JoinTeam, 

30 LeaveTeam, 

31 ListTeamPackages, 

32 WhichTeams, 

33) 

34from distro_tracker.mail.control.commands.misc import ( # noqa 

35 SubscribeCommand, 

36 UnsubscribeCommand, 

37 WhichCommand, 

38 WhoCommand, 

39 QuitCommand, 

40 UnsubscribeallCommand, 

41) 

42from distro_tracker.mail.control.commands.confirmation import ( # noqa 

43 ConfirmCommand 

44) 

45 

46MAX_ALLOWED_ERRORS = settings.DISTRO_TRACKER_MAX_ALLOWED_ERRORS_CONTROL_COMMANDS 

47 

48 

49class HelpCommand(Command): 

50 """ 

51 Displays help for all the other commands -- their description. 

52 """ 

53 META = { 

54 'description': '''help 

55 Shows all available commands''', 

56 'name': 'help', 

57 'position': 5, 

58 } 

59 

60 REGEX_LIST = ( 

61 r'$', 

62 ) 

63 

64 def handle(self): 

65 self.reply(distro_tracker_render_to_string('control/help.txt', { 

66 'descriptions': [ 

67 command.META.get('description', '') 

68 for command in UNIQUE_COMMANDS 

69 ], 

70 })) 

71 

72 

73UNIQUE_COMMANDS = sorted( 

74 (klass 

75 for _, klass in inspect.getmembers(sys.modules[__name__], inspect.isclass) 

76 if klass != Command and issubclass(klass, Command)), 

77 key=lambda cmd: cmd.META.get('position', float('inf')) 

78) 

79""" 

80A list of all :py:class:`Command` that are defined. 

81""" 

82 

83 

84class CommandFactory(object): 

85 """ 

86 Creates instances of 

87 :py:class:`Command <distro_tracker.mail.control.commands.base.Command>` 

88 classes based on the given context. 

89 

90 Context is used to fill in parameters when the command has not found 

91 it in the given command line. 

92 """ 

93 def __init__(self, context): 

94 #: A dict which is used to fill in parameters' values when they are not 

95 #: found in the command line. 

96 self.context = context 

97 

98 def get_command_function(self, line): 

99 """ 

100 Returns a function which executes the functionality of the command 

101 which corresponds to the given arguments. 

102 

103 :param line: The line for which a command function should be returned. 

104 :type line: string 

105 

106 :returns: A callable which when called executes the functionality of a 

107 command matching the given line. 

108 :rtype: :py:class:`Command 

109 <distro_tracker.mail.control.commands.base.Command>` subclass 

110 """ 

111 for cmd in UNIQUE_COMMANDS: 

112 # Command exists 

113 match = cmd.match_line(line) 

114 if not match: 

115 continue 

116 kwargs = match.groupdict() 

117 if not kwargs: 

118 # No named patterns found, pass them in the order they were 

119 # matched. 

120 args = match.groups() 

121 return cmd(*args) 

122 else: 

123 # Update the arguments which weren't matched from the given 

124 # context, if available. 

125 kwargs.update({ 

126 key: value 

127 for key, value in self.context.items() 

128 if key in kwargs and not kwargs[key] and value 

129 }) 

130 command = cmd(**kwargs) 

131 command.context = dict(self.context.items()) 

132 return command 

133 

134 

135class CommandProcessor(object): 

136 """ 

137 A class which performs command processing. 

138 """ 

139 def __init__(self, factory, confirmed=False): 

140 """ 

141 :param factory: Used to obtain 

142 :py:class:`Command 

143 <distro_tracker.mail.control.commands.base.Command>` instances 

144 from command text which is processed. 

145 :type factory: :py:class`CommandFactory` instance 

146 :param confirmed: Indicates whether the commands being executed have 

147 already been confirmed or if those which require confirmation will 

148 be added to the set of commands requiring confirmation. 

149 :type confirmed: Boolean 

150 """ 

151 self.factory = factory 

152 self.confirmed = confirmed 

153 self.confirmation_set = None 

154 

155 self.out = [] 

156 self.errors = 0 

157 self.processed = set() 

158 

159 def echo_command(self, line): 

160 """ 

161 Echoes the line to the command processing output. The line is quoted in 

162 the output. 

163 

164 :param line: The line to be echoed back to the output. 

165 """ 

166 self.out.append('> ' + line) 

167 

168 def output(self, text): 

169 """ 

170 Include the given line in the command processing output. 

171 

172 :param line: The line of text to be included in the output. 

173 """ 

174 self.out.append(text) 

175 

176 def run_command(self, command): 

177 """ 

178 Runs the given command. 

179 

180 :param command: The command to be ran. 

181 :type command: :py:class:`Command 

182 <distro_tracker.mail.control.commands.base.Command>` 

183 """ 

184 if command.get_command_text() not in self.processed: 

185 # Only process the command if it was not previously processed. 

186 if getattr(command, 'needs_confirmation', False): 

187 command.is_confirmed = self.confirmed 

188 command.confirmation_set = self.confirmation_set 

189 # Now run the command 

190 command_output = command() 

191 if not command_output: 191 ↛ 192line 191 didn't jump to line 192, because the condition on line 191 was never true

192 command_output = '' 

193 self.output(command_output) 

194 self.processed.add(command.get_command_text()) 

195 

196 def process(self, lines): 

197 """ 

198 Processes all the given lines of text which are interpreted as 

199 commands. 

200 

201 :param lines: A list of strings each representing a single line which 

202 is to be regarded as a command. 

203 """ 

204 if self.errors == MAX_ALLOWED_ERRORS: 204 ↛ 205line 204 didn't jump to line 205, because the condition on line 204 was never true

205 return 

206 

207 for line in lines: 

208 line = line.strip() 

209 self.echo_command(line) 

210 

211 if not line or line.startswith('#'): 

212 continue 

213 command = self.factory.get_command_function(line) 

214 

215 if not command: 

216 self.errors += 1 

217 if self.errors == MAX_ALLOWED_ERRORS: 

218 self.output( 

219 '{MAX_ALLOWED_ERRORS} lines ' 

220 'without commands: stopping.'.format( 

221 MAX_ALLOWED_ERRORS=MAX_ALLOWED_ERRORS)) 

222 return 

223 else: 

224 self.run_command(command) 

225 

226 if isinstance(command, QuitCommand): 

227 return 

228 

229 def is_success(self): 

230 """ 

231 Checks whether any command was successfully processed. 

232 

233 :returns True: when at least one command is successfully executed 

234 :returns False: when no commands were successfully executed 

235 :rtype: Boolean 

236 """ 

237 # Send a response only if there were some commands processed 

238 if self.processed: 

239 return True 

240 else: 

241 return False 

242 

243 def get_output(self): 

244 """ 

245 Returns the resulting output of processing all given commands. 

246 

247 :rtype: string 

248 """ 

249 return '\n'.join(self.out)