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""" 

11Implements all commands which deal with message keywords. 

12""" 

13 

14import re 

15 

16from distro_tracker.core.models import ( 

17 EmailSettings, 

18 Keyword, 

19 PackageName, 

20 Subscription, 

21 UserEmail 

22) 

23from distro_tracker.core.utils import get_or_none 

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

25 

26__all__ = ( 

27 'ViewDefaultKeywordsCommand', 

28 'ViewPackageKeywordsCommand', 

29 'SetDefaultKeywordsCommand', 

30 'SetPackageKeywordsCommand', 

31 'KeywordCommandMixin', 

32) 

33 

34 

35class KeywordCommandMixin(object): 

36 """ 

37 A mixin including some utility methods for commands which handle keywords. 

38 """ 

39 def error_not_subscribed(self, email, package_name): 

40 """ 

41 Helper returns an error saying the user is not subscribed to the 

42 package. 

43 

44 :param email: The email of the user which is not subscribed to the 

45 package. 

46 :param package_name: The name of the package the user is not subscribed 

47 to. 

48 """ 

49 self.error('%s is not subscribed to the package %s', 

50 email, package_name) 

51 

52 def get_subscription(self, email, package_name): 

53 """ 

54 Helper method returning a 

55 :py:class:`Subscription <distro_tracker.core.models.Subscription>` 

56 instance for the given package and user. 

57 It logs any errors found while retrieving this instance, such as the 

58 user not being subscribed to the given package. 

59 

60 :param email: The email of the user. 

61 :param package_name: The name of the package. 

62 """ 

63 user_email = get_or_none(UserEmail, email__iexact=email) 

64 email_settings = get_or_none(EmailSettings, user_email=user_email) 

65 if not user_email or not email_settings: 

66 self.error_not_subscribed(email, package_name) 

67 return 

68 

69 package = get_or_none(PackageName, name=package_name) 

70 if not package: 

71 self.error('Package %s does not exist', package_name) 

72 return 

73 

74 subscription = get_or_none(Subscription, 

75 package=package, 

76 email_settings=email_settings) 

77 if not subscription: 

78 self.error_not_subscribed(email, package_name) 

79 

80 return subscription 

81 

82 def keyword_name_to_object(self, keyword_name): 

83 """ 

84 Takes a keyword name and returns a 

85 :py:class:`Keyword <distro_tracker.core.models.Keyword>` object with 

86 the given name if it exists. If not, a warning is added to the commands' 

87 output. 

88 

89 :param keyword_name: The name of the keyword to be retrieved. 

90 :rtype: :py:class:`Keyword <distro_tracker.core.models.Keyword>` or 

91 ``None`` 

92 """ 

93 keyword = get_or_none(Keyword, name=keyword_name) 

94 if not keyword: 

95 self.warning('%s is not a valid keyword', keyword_name) 

96 return keyword 

97 

98 def add_keywords(self, keywords, manager): 

99 """ 

100 Adds the keywords given in the iterable ``keywords`` to the ``manager`` 

101 

102 :param keywords: The keywords to be added to the ``manager`` 

103 :type keywords: any iterable containing 

104 :py:class:`Keyword <distro_tracker.core.models.Keyword>` instances 

105 

106 :param manager: The manager to which the keywords should be added. 

107 :type manager: :py:class:`Manager <django.db.models.Manager>` 

108 """ 

109 for keyword_name in keywords: 

110 keyword = self.keyword_name_to_object(keyword_name) 

111 if keyword: 

112 manager.add(keyword) 

113 

114 def remove_keywords(self, keywords, manager): 

115 """ 

116 Removes the keywords given in the iterable ``keywords`` from the 

117 ``manager``. 

118 

119 :param keywords: The keywords to be removed from the ``manager`` 

120 :type keywords: any iterable containing 

121 :py:class:`Keyword <distro_tracker.core.models.Keyword>` instances 

122 

123 :param manager: The manager from which the keywords should be removed. 

124 :type manager: :py:class:`Manager <django.db.models.Manager>` 

125 """ 

126 for keyword_name in keywords: 

127 keyword = self.keyword_name_to_object(keyword_name) 

128 if keyword: 128 ↛ 126line 128 didn't jump to line 126, because the condition on line 128 was never false

129 manager.remove(keyword) 

130 

131 def set_keywords(self, keywords, manager): 

132 """ 

133 Sets the keywords given in the iterable ``keywords`` to the ``manager`` 

134 so that they are the only keywords it contains. 

135 

136 :param keywords: The keywords to be set to the ``manager`` 

137 :type keywords: any iterable containing 

138 :py:class:`Keyword <distro_tracker.core.models.Keyword>` instances 

139 

140 :param manager: The manager to which the keywords should be added. 

141 :type manager: :py:class:`Manager <django.db.models.Manager>` 

142 """ 

143 manager.clear() 

144 self.add_keywords(keywords, manager) 

145 

146 OPERATIONS = { 

147 '+': add_keywords, 

148 '-': remove_keywords, 

149 '=': set_keywords, 

150 } 

151 """ 

152 Maps symbols to operations. When the symbol is found in a keyword command 

153 the given operation is called. 

154 

155 - '+': :py:meth:`add_keywords` 

156 - '-': :py:meth:`remove_keywords` 

157 - '=': :py:meth:`set_keywords` 

158 """ 

159 

160 

161class ViewDefaultKeywordsCommand(Command, KeywordCommandMixin): 

162 """ 

163 Implementation of the keyword command which handles displaying a list 

164 of the user's default keywords. 

165 """ 

166 META = { 

167 'position': 10, 

168 'name': 'view-default-keywords', 

169 'aliases': ['keyword', 'tag', 'keywords', 'tags'], 

170 'description': '''keyword [<email>] 

171 Tells you the keywords you are accepting by default for packages 

172 with no specific keywords set. 

173 

174 Each mail sent through the Distro Tracker is associated 

175 to a keyword and you receive only the mails associated to keywords 

176 you are accepting. 

177 You may select a different set of keywords for each package.''' 

178 } 

179 REGEX_LIST = ( 

180 r'(?:\s+(?P<email>\S+@\S+))?$', 

181 ) 

182 

183 def __init__(self, email): 

184 super(ViewDefaultKeywordsCommand, self).__init__() 

185 self.email = email 

186 

187 def get_command_text(self): 

188 return super(ViewDefaultKeywordsCommand, self).get_command_text( 

189 self.email) 

190 

191 def handle(self): 

192 if not self.validate_email(self.email): 

193 self.warning('%s is not a valid email.', self.email) 

194 return 

195 

196 user_email, _ = UserEmail.objects.get_or_create(email=self.email) 

197 email_settings, _ = \ 

198 EmailSettings.objects.get_or_create(user_email=user_email) 

199 self.reply("Here's the default list of accepted keywords for %s:", 

200 self.email) 

201 self.list_reply(sorted( 

202 keyword.name for keyword in email_settings.default_keywords.all())) 

203 

204 

205class ViewPackageKeywordsCommand(Command, KeywordCommandMixin): 

206 """ 

207 Implementation of the keyword command version which handles listing 

208 all keywords associated to a package for a particular user. 

209 """ 

210 META = { 

211 'position': 11, 

212 'name': 'view-package-keywords', 

213 'aliases': ['keyword', 'keywords', 'tag', 'tags'], 

214 'description': '''keyword <srcpackage> [<email>] 

215 Tells you the keywords you are accepting for the given package. 

216 

217 Each mail sent through Distro Tracker is associated 

218 to a keyword and you receive only the mails associated to keywords 

219 you are accepting. 

220 You may select a different set of keywords for each package.''' 

221 } 

222 REGEX_LIST = ( 

223 r'\s+(?P<package>\S+)(?:\s+(?P<email>\S+@\S+))?$', 

224 ) 

225 

226 def __init__(self, package, email): 

227 super(ViewPackageKeywordsCommand, self).__init__() 

228 self.package = package 

229 self.email = email 

230 

231 def get_command_text(self): 

232 return super(ViewPackageKeywordsCommand, self).get_command_text( 

233 self.package, 

234 self.email) 

235 

236 def handle(self): 

237 subscription = self.get_subscription(self.email, self.package) 

238 if not subscription: 

239 return 

240 

241 self.reply( 

242 "Here's the list of accepted keywords associated to package") 

243 self.reply('%s for %s', self.package, self.email) 

244 self.list_reply(sorted( 

245 keyword.name for keyword in subscription.keywords.all())) 

246 

247 

248class SetDefaultKeywordsCommand(Command, KeywordCommandMixin): 

249 """ 

250 Implementation of the keyword command which handles modifying a user's 

251 list of default keywords. 

252 """ 

253 META = { 

254 'position': 12, 

255 'name': 'set-default-keywords', 

256 'aliases': ['keyword', 'keywords', 'tag', 'tags'], 

257 'description': '''keyword [<email>] {+|-|=} <list of keywords> 

258 Accept (+) or refuse (-) mails associated to the given keyword(s). 

259 Define the list (=) of accepted keywords. 

260 These keywords are applied for subscriptions where no specific 

261 keyword set is given.''' 

262 } 

263 REGEX_LIST = ( 

264 (r'(?:\s+(?P<email>\S+@\S+))?\s+(?P<operation>[-+=])' 

265 r'\s+(?P<keywords>\S+(?:\s+\S+)*)$'), 

266 ) 

267 

268 def __init__(self, email, operation, keywords): 

269 super(SetDefaultKeywordsCommand, self).__init__() 

270 self.email = email 

271 self.operation = operation 

272 self.keywords = keywords 

273 

274 def get_command_text(self): 

275 return super(SetDefaultKeywordsCommand, self).get_command_text( 

276 self.email, 

277 self.operation, 

278 self.keywords) 

279 

280 def handle(self): 

281 if not self.validate_email(self.email): 

282 self.warning('%s is not a valid email.', self.email) 

283 return 

284 

285 keywords = re.split(r'[,\s]+', self.keywords) 

286 user_email, _ = UserEmail.objects.get_or_create(email=self.email) 

287 email_settings, _ = \ 

288 EmailSettings.objects.get_or_create(user_email=user_email) 

289 

290 operation_method = self.OPERATIONS[self.operation] 

291 operation_method(self, keywords, email_settings.default_keywords) 

292 

293 self.reply("Here's the new default list of accepted keywords for %s :", 

294 self.email) 

295 self.list_reply(sorted( 

296 keyword.name for keyword in email_settings.default_keywords.all() 

297 )) 

298 

299 

300class SetPackageKeywordsCommand(Command, KeywordCommandMixin): 

301 """ 

302 Implementation of the keyword command version which modifies subscription 

303 specific keywords. 

304 """ 

305 META = { 

306 'name': 'set-package-keywords', 

307 'aliases': ['keyword', 'keywords', 'tag', 'tags'], 

308 'position': 13, 

309 'description': ( 

310 '''keyword <srcpackage> [<email>] {+|-|=} <list of keywords> 

311 Accept (+) or refuse (-) mails associated to the given keyword(s) for the 

312 given package. 

313 Define the list (=) of accepted keywords. 

314 These keywords take precedence over default keywords.''') 

315 } 

316 REGEX_LIST = ( 

317 (r'\s+(?P<package>\S+)(?:\s+(?P<email>\S+@\S+))?\s+' 

318 r'(?P<operation>[-+=])\s+(?P<keywords>\S+(?:\s+\S+)*)$'), 

319 ) 

320 

321 def __init__(self, package, email, operation, keywords): 

322 super(SetPackageKeywordsCommand, self).__init__() 

323 self.package = package 

324 self.email = email 

325 self.operation = operation 

326 self.keywords = keywords 

327 

328 def get_command_text(self): 

329 return super(SetPackageKeywordsCommand, self).get_command_text( 

330 self.package, 

331 self.email, 

332 self.operation, 

333 self.keywords) 

334 

335 def handle(self): 

336 """ 

337 Actual implementation of the keyword command version which handles 

338 subscription specific keywords. 

339 """ 

340 keywords = re.split(r'[,\s]+', self.keywords) 

341 subscription = self.get_subscription(self.email, self.package) 

342 if not subscription: 

343 return 

344 

345 operation_method = self.OPERATIONS[self.operation] 

346 operation_method(self, keywords, subscription.keywords) 

347 

348 self.reply( 

349 "Here's the new list of accepted keywords associated to package\n" 

350 "%s for %s :", self.package, self.email) 

351 self.list_reply(sorted( 

352 keyword.name for keyword in subscription.keywords.all()))