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"""
14import re
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
26__all__ = (
27 'ViewDefaultKeywordsCommand',
28 'ViewPackageKeywordsCommand',
29 'SetDefaultKeywordsCommand',
30 'SetPackageKeywordsCommand',
31 'KeywordCommandMixin',
32)
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.
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)
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.
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
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
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)
80 return subscription
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.
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
98 def add_keywords(self, keywords, manager):
99 """
100 Adds the keywords given in the iterable ``keywords`` to the ``manager``
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
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)
114 def remove_keywords(self, keywords, manager):
115 """
116 Removes the keywords given in the iterable ``keywords`` from the
117 ``manager``.
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
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)
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.
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
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)
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.
155 - '+': :py:meth:`add_keywords`
156 - '-': :py:meth:`remove_keywords`
157 - '=': :py:meth:`set_keywords`
158 """
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.
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 )
183 def __init__(self, email):
184 super(ViewDefaultKeywordsCommand, self).__init__()
185 self.email = email
187 def get_command_text(self):
188 return super(ViewDefaultKeywordsCommand, self).get_command_text(
189 self.email)
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
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()))
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.
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 )
226 def __init__(self, package, email):
227 super(ViewPackageKeywordsCommand, self).__init__()
228 self.package = package
229 self.email = email
231 def get_command_text(self):
232 return super(ViewPackageKeywordsCommand, self).get_command_text(
233 self.package,
234 self.email)
236 def handle(self):
237 subscription = self.get_subscription(self.email, self.package)
238 if not subscription:
239 return
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()))
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 )
268 def __init__(self, email, operation, keywords):
269 super(SetDefaultKeywordsCommand, self).__init__()
270 self.email = email
271 self.operation = operation
272 self.keywords = keywords
274 def get_command_text(self):
275 return super(SetDefaultKeywordsCommand, self).get_command_text(
276 self.email,
277 self.operation,
278 self.keywords)
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
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)
290 operation_method = self.OPERATIONS[self.operation]
291 operation_method(self, keywords, email_settings.default_keywords)
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 ))
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 )
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
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)
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
345 operation_method = self.OPERATIONS[self.operation]
346 operation_method(self, keywords, subscription.keywords)
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()))