1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

220

221

222

223

224

225

226

227

228

229

230

231

232

233

234

235

236

237

238

239

240

241

242

243

244

245

246

247

248

249

250

251

252

253

254

255

256

257

258

259

260

261

262

263

264

265

266

267

268

269

270

271

272

273

274

275

276

277

278

279

280

281

282

283

284

285

286

287

288

289

290

291

292

293

294

295

296

297

298

299

300

301

302

303

304

305

306

307

308

309

310

311

312

313

314

315

316

317

318

319

320

321

322

323

324

325

326

327

328

329

330

331

332

333

334

335

336

337

338

339

340

341

342

343

344

345

346

347

348

349

350

351

352

# Copyright 2013 The Distro Tracker Developers 

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

# at https://deb.li/DTAuthors 

# 

# This file is part of Distro Tracker. It is subject to the license terms 

# in the LICENSE file found in the top-level directory of this 

# distribution and at https://deb.li/DTLicense. No part of Distro Tracker, 

# including this file, may be copied, modified, propagated, or distributed 

# except according to the terms contained in the LICENSE file. 

""" 

Implements all commands which deal with message keywords. 

""" 

 

import re 

 

from distro_tracker.core.models import ( 

EmailSettings, 

Keyword, 

PackageName, 

Subscription, 

UserEmail 

) 

from distro_tracker.core.utils import get_or_none 

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

 

__all__ = ( 

'ViewDefaultKeywordsCommand', 

'ViewPackageKeywordsCommand', 

'SetDefaultKeywordsCommand', 

'SetPackageKeywordsCommand', 

'KeywordCommandMixin', 

) 

 

 

class KeywordCommandMixin(object): 

""" 

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

""" 

def error_not_subscribed(self, email, package_name): 

""" 

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

package. 

 

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

package. 

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

to. 

""" 

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

email, package_name) 

 

def get_subscription(self, email, package_name): 

""" 

Helper method returning a 

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

instance for the given package and user. 

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

user not being subscribed to the given package. 

 

:param email: The email of the user. 

:param package_name: The name of the package. 

""" 

user_email = get_or_none(UserEmail, email__iexact=email) 

email_settings = get_or_none(EmailSettings, user_email=user_email) 

if not user_email or not email_settings: 

self.error_not_subscribed(email, package_name) 

return 

 

package = get_or_none(PackageName, name=package_name) 

if not package: 

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

return 

 

subscription = get_or_none(Subscription, 

package=package, 

email_settings=email_settings) 

if not subscription: 

self.error_not_subscribed(email, package_name) 

 

return subscription 

 

def keyword_name_to_object(self, keyword_name): 

""" 

Takes a keyword name and returns a 

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

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

output. 

 

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

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

``None`` 

""" 

keyword = get_or_none(Keyword, name=keyword_name) 

if not keyword: 

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

return keyword 

 

def add_keywords(self, keywords, manager): 

""" 

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

 

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

:type keywords: any iterable containing 

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

 

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

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

""" 

for keyword_name in keywords: 

keyword = self.keyword_name_to_object(keyword_name) 

if keyword: 

manager.add(keyword) 

 

def remove_keywords(self, keywords, manager): 

""" 

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

``manager``. 

 

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

:type keywords: any iterable containing 

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

 

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

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

""" 

for keyword_name in keywords: 

keyword = self.keyword_name_to_object(keyword_name) 

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

manager.remove(keyword) 

 

def set_keywords(self, keywords, manager): 

""" 

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

so that they are the only keywords it contains. 

 

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

:type keywords: any iterable containing 

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

 

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

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

""" 

manager.clear() 

self.add_keywords(keywords, manager) 

 

OPERATIONS = { 

'+': add_keywords, 

'-': remove_keywords, 

'=': set_keywords, 

} 

""" 

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

the given operation is called. 

 

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

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

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

""" 

 

 

class ViewDefaultKeywordsCommand(Command, KeywordCommandMixin): 

""" 

Implementation of the keyword command which handles displaying a list 

of the user's default keywords. 

""" 

META = { 

'position': 10, 

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

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

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

Tells you the keywords you are accepting by default for packages 

with no specific keywords set. 

 

Each mail sent through the Distro Tracker is associated 

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

you are accepting. 

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

} 

REGEX_LIST = ( 

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

) 

 

def __init__(self, email): 

super(ViewDefaultKeywordsCommand, self).__init__() 

self.email = email 

 

def get_command_text(self): 

return super(ViewDefaultKeywordsCommand, self).get_command_text( 

self.email) 

 

def handle(self): 

if not self.validate_email(self.email): 

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

return 

 

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

email_settings, _ = \ 

EmailSettings.objects.get_or_create(user_email=user_email) 

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

self.email) 

self.list_reply(sorted( 

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

 

 

class ViewPackageKeywordsCommand(Command, KeywordCommandMixin): 

""" 

Implementation of the keyword command version which handles listing 

all keywords associated to a package for a particular user. 

""" 

META = { 

'position': 11, 

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

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

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

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

 

Each mail sent through Distro Tracker is associated 

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

you are accepting. 

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

} 

REGEX_LIST = ( 

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

) 

 

def __init__(self, package, email): 

super(ViewPackageKeywordsCommand, self).__init__() 

self.package = package 

self.email = email 

 

def get_command_text(self): 

return super(ViewPackageKeywordsCommand, self).get_command_text( 

self.package, 

self.email) 

 

def handle(self): 

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

if not subscription: 

return 

 

self.reply( 

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

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

self.list_reply(sorted( 

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

 

 

class SetDefaultKeywordsCommand(Command, KeywordCommandMixin): 

""" 

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

list of default keywords. 

""" 

META = { 

'position': 12, 

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

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

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

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

Define the list (=) of accepted keywords. 

These keywords are applied for subscriptions where no specific 

keyword set is given.''' 

} 

REGEX_LIST = ( 

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

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

) 

 

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

super(SetDefaultKeywordsCommand, self).__init__() 

self.email = email 

self.operation = operation 

self.keywords = keywords 

 

def get_command_text(self): 

return super(SetDefaultKeywordsCommand, self).get_command_text( 

self.email, 

self.operation, 

self.keywords) 

 

def handle(self): 

if not self.validate_email(self.email): 

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

return 

 

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

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

email_settings, _ = \ 

EmailSettings.objects.get_or_create(user_email=user_email) 

 

operation_method = self.OPERATIONS[self.operation] 

operation_method(self, keywords, email_settings.default_keywords) 

 

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

self.email) 

self.list_reply(sorted( 

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

)) 

 

 

class SetPackageKeywordsCommand(Command, KeywordCommandMixin): 

""" 

Implementation of the keyword command version which modifies subscription 

specific keywords. 

""" 

META = { 

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

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

'position': 13, 

'description': ( 

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

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

given package. 

Define the list (=) of accepted keywords. 

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

} 

REGEX_LIST = ( 

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

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

) 

 

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

super(SetPackageKeywordsCommand, self).__init__() 

self.package = package 

self.email = email 

self.operation = operation 

self.keywords = keywords 

 

def get_command_text(self): 

return super(SetPackageKeywordsCommand, self).get_command_text( 

self.package, 

self.email, 

self.operation, 

self.keywords) 

 

def handle(self): 

""" 

Actual implementation of the keyword command version which handles 

subscription specific keywords. 

""" 

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

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

if not subscription: 

return 

 

operation_method = self.OPERATIONS[self.operation] 

operation_method(self, keywords, subscription.keywords) 

 

self.reply( 

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

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

self.list_reply(sorted( 

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