comparison roundup/roundupdb.py @ 6008:2b53c310089f

flake8 cleanup formatting plus type comparisons replaced with isinstance removal of unused imports bare except -> except Exception:
author John Rouillard <rouilj@ieee.org>
date Sat, 28 Dec 2019 14:51:50 -0500
parents 71c68961d9f4
children 8497bf3f23a1
comparison
equal deleted inserted replaced
6007:e27a240430b8 6008:2b53c310089f
18 18
19 """Extending hyperdb with types specific to issue-tracking. 19 """Extending hyperdb with types specific to issue-tracking.
20 """ 20 """
21 __docformat__ = 'restructuredtext' 21 __docformat__ = 'restructuredtext'
22 22
23 import re, os, smtplib, socket, time 23 import time
24 import base64, mimetypes 24 import base64, mimetypes
25 import os.path
26 import logging 25 import logging
27 from email import encoders 26 from email import encoders
28 from email.parser import FeedParser 27 from email.parser import FeedParser
29 from email.utils import formataddr 28 from email.utils import formataddr
30 from email.header import Header 29 from email.header import Header
52 # remember the journal uid for the current journaltag so that: 51 # remember the journal uid for the current journaltag so that:
53 # a. we don't have to look it up every time we need it, and 52 # a. we don't have to look it up every time we need it, and
54 # b. if the journaltag disappears during a transaction, we don't barf 53 # b. if the journaltag disappears during a transaction, we don't barf
55 # (eg. the current user edits their username) 54 # (eg. the current user edits their username)
56 journal_uid = None 55 journal_uid = None
56
57 def getuid(self): 57 def getuid(self):
58 """Return the id of the "user" node associated with the user 58 """Return the id of the "user" node associated with the user
59 that owns this connection to the hyperdatabase.""" 59 that owns this connection to the hyperdatabase."""
60 if self.journaltag is None: 60 if self.journaltag is None:
61 return None 61 return None
130 except ValueError as e: 130 except ValueError as e:
131 username = props['username'] 131 username = props['username']
132 # Try to make error message less cryptic to the user. 132 # Try to make error message less cryptic to the user.
133 if str(e) == 'node with key "%s" exists' % username: 133 if str(e) == 'node with key "%s" exists' % username:
134 raise ValueError( 134 raise ValueError(
135 _("Username '%s' already exists."%username)) 135 _("Username '%s' already exists." % username))
136 else: 136 else:
137 raise 137 raise
138 138
139 # clear the props from the otk database 139 # clear the props from the otk database
140 self.getOTKManager().destroy(otk) 140 self.getOTKManager().destroy(otk)
141 # commit cl.create (and otk changes) 141 # commit cl.create (and otk changes)
142 self.commit() 142 self.commit()
143 143
144 return userid 144 return userid
145
146 145
147 def log_debug(self, msg, *args, **kwargs): 146 def log_debug(self, msg, *args, **kwargs):
148 """Log a message with level DEBUG.""" 147 """Log a message with level DEBUG."""
149 148
150 logger = self.get_logger() 149 logger = self.get_logger()
170 """ Backends may keep a cache. 169 """ Backends may keep a cache.
171 It must be cleared at end of commit and rollback methods. 170 It must be cleared at end of commit and rollback methods.
172 We allow to register user-defined cache-clearing routines 171 We allow to register user-defined cache-clearing routines
173 that are called by this routine. 172 that are called by this routine.
174 """ 173 """
175 if getattr (self, 'cache_callbacks', None) : 174 if getattr(self, 'cache_callbacks', None):
176 for method, param in self.cache_callbacks: 175 for method, param in self.cache_callbacks:
177 method(param) 176 method(param)
178 177
179 def registerClearCacheCallback(self, method, param = None): 178 def registerClearCacheCallback(self, method, param=None):
180 """ Register a callback method for clearing the cache. 179 """ Register a callback method for clearing the cache.
181 It is called with the given param as the only parameter. 180 It is called with the given param as the only parameter.
182 Even if the parameter is not specified, the method has to 181 Even if the parameter is not specified, the method has to
183 accept a single parameter. 182 accept a single parameter.
184 """ 183 """
185 if not getattr (self, 'cache_callbacks', None) : 184 if not getattr(self, 'cache_callbacks', None):
186 self.cache_callbacks = [] 185 self.cache_callbacks = []
187 self.cache_callbacks.append ((method, param)) 186 self.cache_callbacks.append((method, param))
188 187
189 188
190 class DetectorError(RuntimeError): 189 class DetectorError(RuntimeError):
191 """ Raised by detectors that want to indicate that something's amiss 190 """ Raised by detectors that want to indicate that something's amiss
192 """ 191 """
193 pass 192 pass
193
194 194
195 # deviation from spec - was called IssueClass 195 # deviation from spec - was called IssueClass
196 class IssueClass: 196 class IssueClass:
197 """This class is intended to be mixed-in with a hyperdb backend 197 """This class is intended to be mixed-in with a hyperdb backend
198 implementation. The backend should provide a mechanism that 198 implementation. The backend should provide a mechanism that
236 The given text is saved as the body of the message and the node is 236 The given text is saved as the body of the message and the node is
237 appended to the "messages" field of the specified issue. 237 appended to the "messages" field of the specified issue.
238 """ 238 """
239 239
240 def nosymessage(self, issueid, msgid, oldvalues, whichnosy='nosy', 240 def nosymessage(self, issueid, msgid, oldvalues, whichnosy='nosy',
241 from_address=None, cc=[], bcc=[], cc_emails = [], 241 from_address=None, cc=[], bcc=[], cc_emails=[],
242 bcc_emails = [], subject=None, 242 bcc_emails=[], subject=None,
243 note_filter = None, add_headers={}): 243 note_filter=None, add_headers={}):
244 """Send a message to the members of an issue's nosy list. 244 """Send a message to the members of an issue's nosy list.
245 245
246 The message is sent only to users on the nosy list who are not 246 The message is sent only to users on the nosy list who are not
247 already on the "recipients" list for the message. 247 already on the "recipients" list for the message.
248 248
298 else: 298 else:
299 # "system message" 299 # "system message"
300 authid = None 300 authid = None
301 recipients = [] 301 recipients = []
302 302
303 sendto = dict (plain = [], crypt = []) 303 sendto = dict(plain=[], crypt=[])
304 bcc_sendto = dict (plain = [], crypt = []) 304 bcc_sendto = dict(plain=[], crypt=[])
305 seen_message = {} 305 seen_message = {}
306 for recipient in recipients: 306 for recipient in recipients:
307 seen_message[recipient] = 1 307 seen_message[recipient] = 1
308 308
309 def add_recipient(userid, to): 309 def add_recipient(userid, to):
338 # anonymous 338 # anonymous
339 if (good_recipient(authid) and 339 if (good_recipient(authid) and
340 (self.db.config.MESSAGES_TO_AUTHOR == 'yes' or 340 (self.db.config.MESSAGES_TO_AUTHOR == 'yes' or
341 (self.db.config.MESSAGES_TO_AUTHOR == 'new' and not oldvalues) or 341 (self.db.config.MESSAGES_TO_AUTHOR == 'new' and not oldvalues) or
342 (self.db.config.MESSAGES_TO_AUTHOR == 'nosy' and authid in 342 (self.db.config.MESSAGES_TO_AUTHOR == 'nosy' and authid in
343 self.get(issueid, whichnosy)))): 343 self.get(issueid, whichnosy)))):
344 add_recipient(authid, sendto) 344 add_recipient(authid, sendto)
345 345
346 if authid: 346 if authid:
347 seen_message[authid] = 1 347 seen_message[authid] = 1
348 348
350 for userid in cc + self.get(issueid, whichnosy): 350 for userid in cc + self.get(issueid, whichnosy):
351 if good_recipient(userid): 351 if good_recipient(userid):
352 add_recipient(userid, sendto) 352 add_recipient(userid, sendto)
353 seen_message[userid] = 1 353 seen_message[userid] = 1
354 if encrypt and not pgproles: 354 if encrypt and not pgproles:
355 sendto['crypt'].extend (cc_emails) 355 sendto['crypt'].extend(cc_emails)
356 else: 356 else:
357 sendto['plain'].extend (cc_emails) 357 sendto['plain'].extend(cc_emails)
358 358
359 # now deal with bcc people. 359 # now deal with bcc people.
360 for userid in bcc: 360 for userid in bcc:
361 if good_recipient(userid): 361 if good_recipient(userid):
362 add_recipient(userid, bcc_sendto) 362 add_recipient(userid, bcc_sendto)
363 seen_message[userid] = 1 363 seen_message[userid] = 1
364 if encrypt and not pgproles: 364 if encrypt and not pgproles:
365 bcc_sendto['crypt'].extend (bcc_emails) 365 bcc_sendto['crypt'].extend(bcc_emails)
366 else: 366 else:
367 bcc_sendto['plain'].extend (bcc_emails) 367 bcc_sendto['plain'].extend(bcc_emails)
368 368
369 if oldvalues: 369 if oldvalues:
370 note = self.generateChangeNote(issueid, oldvalues) 370 note = self.generateChangeNote(issueid, oldvalues)
371 else: 371 else:
372 note = self.generateCreateNote(issueid) 372 note = self.generateCreateNote(issueid)
385 self.send_message(issueid, msgid, note, sendto['plain'], 385 self.send_message(issueid, msgid, note, sendto['plain'],
386 from_address, bcc_sendto['plain'], 386 from_address, bcc_sendto['plain'],
387 subject, add_headers=add_headers) 387 subject, add_headers=add_headers)
388 if sendto['crypt'] or bcc_sendto['crypt']: 388 if sendto['crypt'] or bcc_sendto['crypt']:
389 self.send_message(issueid, msgid, note, sendto['crypt'], 389 self.send_message(issueid, msgid, note, sendto['crypt'],
390 from_address, bcc_sendto['crypt'], subject, crypt=True, 390 from_address, bcc_sendto['crypt'], subject,
391 add_headers=add_headers) 391 crypt=True, add_headers=add_headers)
392 392
393 # backwards compatibility - don't remove 393 # backwards compatibility - don't remove
394 sendmessage = nosymessage 394 sendmessage = nosymessage
395 395
396 def encrypt_to(self, message, sendto): 396 def encrypt_to(self, message, sendto):
407 # only first key per email 407 # only first key per email
408 k = ctx.op_keylist_next() 408 k = ctx.op_keylist_next()
409 if k is not None: 409 if k is not None:
410 keys.append(k) 410 keys.append(k)
411 else: 411 else:
412 msg = _('No key for "%(adr)s" in keyring')%locals() 412 msg = _('No key for "%(adr)s" in keyring') % locals()
413 raise MessageSendError(msg) 413 raise MessageSendError(msg)
414 ctx.op_keylist_end() 414 ctx.op_keylist_end()
415 ctx.op_encrypt(keys, 1, plain, cipher) 415 ctx.op_encrypt(keys, 1, plain, cipher)
416 cipher.seek(0,0) 416 cipher.seek(0, 0)
417 msg = MIMEMultipart('encrypted', boundary=None, _subparts=None, 417 msg = MIMEMultipart('encrypted', boundary=None, _subparts=None,
418 protocol="application/pgp-encrypted") 418 protocol="application/pgp-encrypted")
419 part = MIMEBase('application', 'pgp-encrypted') 419 part = MIMEBase('application', 'pgp-encrypted')
420 part.set_payload("Version: 1\r\n") 420 part.set_payload("Version: 1\r\n")
421 msg.attach(part) 421 msg.attach(part)
422 part = MIMEBase('application', 'octet-stream') 422 part = MIMEBase('application', 'octet-stream')
423 part.set_payload(cipher.read()) 423 part.set_payload(cipher.read())
424 msg.attach(part) 424 msg.attach(part)
425 return msg 425 return msg
426 426
427 def send_message(self, issueid, msgid, note, sendto, from_address=None, 427 def send_message(self, issueid, msgid, note, sendto, from_address=None,
428 bcc_sendto=[], subject=None, crypt=False, add_headers={}): 428 bcc_sendto=[], subject=None, crypt=False,
429 add_headers={}):
429 '''Actually send the nominated message from this issue to the sendto 430 '''Actually send the nominated message from this issue to the sendto
430 recipients, with the note appended. It's possible to add 431 recipients, with the note appended. It's possible to add
431 headers to the message with the add_headers variable. 432 headers to the message with the add_headers variable.
432 ''' 433 '''
433 users = self.db.user 434 users = self.db.user
443 444
444 # make up a messageid if there isn't one (web edit) 445 # make up a messageid if there isn't one (web edit)
445 if not messageid: 446 if not messageid:
446 # this is an old message that didn't get a messageid, so 447 # this is an old message that didn't get a messageid, so
447 # create one 448 # create one
448 messageid = "<%s.%s.%s%s@%s>"%(time.time(), 449 messageid = "<%s.%s.%s%s@%s>" % (time.time(),
449 b2s(base64.b32encode(random_.token_bytes(10))), 450 b2s(base64.b32encode(random_.token_bytes(10))),
450 self.classname, issueid, self.db.config['MAIL_DOMAIN']) 451 self.classname, issueid, self.db.config['MAIL_DOMAIN'])
451 if msgid is not None: 452 if msgid is not None:
452 messages.set(msgid, messageid=messageid) 453 messages.set(msgid, messageid=messageid)
453 454
454 # compose title 455 # compose title
455 cn = self.classname 456 cn = self.classname
456 title = self.get(issueid, 'title') or '%s message copy'%cn 457 title = self.get(issueid, 'title') or '%s message copy' % cn
457 458
458 # figure author information 459 # figure author information
459 if msgid: 460 if msgid:
460 authid = messages.get(msgid, 'author') 461 authid = messages.get(msgid, 'author')
461 else: 462 else:
464 if not authname: 465 if not authname:
465 authname = users.get(authid, 'username', '') 466 authname = users.get(authid, 'username', '')
466 authaddr = users.get(authid, 'address', '') 467 authaddr = users.get(authid, 'address', '')
467 468
468 if authaddr and self.db.config.MAIL_ADD_AUTHOREMAIL: 469 if authaddr and self.db.config.MAIL_ADD_AUTHOREMAIL:
469 authaddr = " <%s>" % formataddr( ('',authaddr) ) 470 authaddr = " <%s>" % formataddr(('', authaddr))
470 elif authaddr: 471 elif authaddr:
471 authaddr = "" 472 authaddr = ""
472 473
473 # make the message body 474 # make the message body
474 m = [''] 475 m = ['']
479 480
480 # add author information 481 # add author information
481 if authid and self.db.config.MAIL_ADD_AUTHORINFO: 482 if authid and self.db.config.MAIL_ADD_AUTHORINFO:
482 if msgid and len(self.get(issueid, 'messages')) == 1: 483 if msgid and len(self.get(issueid, 'messages')) == 1:
483 m.append(_("New submission from %(authname)s%(authaddr)s:") 484 m.append(_("New submission from %(authname)s%(authaddr)s:")
484 % locals()) 485 % locals())
485 elif msgid: 486 elif msgid:
486 m.append(_("%(authname)s%(authaddr)s added the comment:") 487 m.append(_("%(authname)s%(authaddr)s added the comment:")
487 % locals()) 488 % locals())
488 else: 489 else:
489 m.append(_("Change by %(authname)s%(authaddr)s:") % locals()) 490 m.append(_("Change by %(authname)s%(authaddr)s:") % locals())
490 m.append('') 491 m.append('')
491 492
492 # add the content 493 # add the content
493 if msgid is not None: 494 if msgid is not None:
494 m.append(messages.get(msgid, 'content', '')) 495 m.append(messages.get(msgid, 'content', ''))
495 496
496 # get the files for this message 497 # get the files for this message
497 message_files = [] 498 message_files = []
498 if msgid : 499 if msgid:
499 for fileid in messages.get(msgid, 'files') : 500 for fileid in messages.get(msgid, 'files'):
500 # check the attachment size 501 # check the attachment size
501 filesize = self.db.filesize('file', fileid, None) 502 filesize = self.db.filesize('file', fileid, None)
502 if filesize <= self.db.config.NOSY_MAX_ATTACHMENT_SIZE: 503 if filesize <= self.db.config.NOSY_MAX_ATTACHMENT_SIZE:
503 message_files.append(fileid) 504 message_files.append(fileid)
504 else: 505 else:
505 base = self.db.config.TRACKER_WEB 506 base = self.db.config.TRACKER_WEB
506 link = "".join((base, files.classname, fileid)) 507 link = "".join((base, files.classname, fileid))
507 filename = files.get(fileid, 'name') 508 filename = files.get(fileid, 'name')
508 m.append(_("File '%(filename)s' not attached - " 509 m.append(_("File '%(filename)s' not attached - "
509 "you can download it from %(link)s.") % locals()) 510 "you can download it from %(link)s.") %
511 locals())
510 512
511 # add the change note 513 # add the change note
512 if note: 514 if note:
513 m.append(note) 515 m.append(note)
514 516
533 from_tag = getattr(self.db.config, 'EMAIL_FROM_TAG', '') 535 from_tag = getattr(self.db.config, 'EMAIL_FROM_TAG', '')
534 if from_tag: 536 if from_tag:
535 from_tag = ' ' + from_tag 537 from_tag = ' ' + from_tag
536 538
537 if subject is None: 539 if subject is None:
538 subject = '[%s%s] %s'%(cn, issueid, title) 540 subject = '[%s%s] %s' % (cn, issueid, title)
539 541
540 author = (authname + from_tag, from_address) 542 author = (authname + from_tag, from_address)
541 543
542 # send an individual message per recipient? 544 # send an individual message per recipient?
543 if self.db.config.NOSY_EMAIL_SENDING != 'single': 545 if self.db.config.NOSY_EMAIL_SENDING != 'single':
546 sendto = [sendto] 548 sendto = [sendto]
547 549
548 # tracker sender info 550 # tracker sender info
549 tracker_name = s2u(self.db.config.TRACKER_NAME) 551 tracker_name = s2u(self.db.config.TRACKER_NAME)
550 tracker_name = nice_sender_header(tracker_name, from_address, 552 tracker_name = nice_sender_header(tracker_name, from_address,
551 charset) 553 charset)
552 554
553 # now send one or more messages 555 # now send one or more messages
554 # TODO: I believe we have to create a new message each time as we 556 # TODO: I believe we have to create a new message each time as we
555 # can't fiddle the recipients in the message ... worth testing 557 # can't fiddle the recipients in the message ... worth testing
556 # and/or fixing some day 558 # and/or fixing some day
559 # create the message 561 # create the message
560 mailer = Mailer(self.db.config) 562 mailer = Mailer(self.db.config)
561 563
562 message = mailer.get_standard_message(multipart=message_files) 564 message = mailer.get_standard_message(multipart=message_files)
563 565
564 # set reply-to as requested by config option TRACKER_REPLYTO_ADDRESS 566 # set reply-to as requested by config option
567 # TRACKER_REPLYTO_ADDRESS
565 replyto_config = self.db.config.TRACKER_REPLYTO_ADDRESS 568 replyto_config = self.db.config.TRACKER_REPLYTO_ADDRESS
566 if replyto_config: 569 if replyto_config:
567 if replyto_config == "AUTHOR": 570 if replyto_config == "AUTHOR":
568 # note that authaddr at this point is already surrounded by < >, so 571 # note that authaddr at this point is already
569 # get the original address from the db as nice_send_header adds < > 572 # surrounded by < >, so get the original address
570 replyto_addr = nice_sender_header(authname, users.get(authid, 'address', ''), charset) 573 # from the db as nice_send_header adds < >
574 replyto_addr = nice_sender_header(authname,
575 users.get(authid, 'address', ''), charset)
571 else: 576 else:
572 replyto_addr = replyto_config 577 replyto_addr = replyto_config
573 else: 578 else:
574 replyto_addr = tracker_name 579 replyto_addr = tracker_name
575 message['Reply-To'] = replyto_addr 580 message['Reply-To'] = replyto_addr
608 values = self.get(issueid, propname) 613 values = self.get(issueid, propname)
609 if not values: 614 if not values:
610 continue 615 continue
611 values = [cl.get(v, label) for v in values] 616 values = [cl.get(v, label) for v in values]
612 values = ', '.join(values) 617 values = ', '.join(values)
613 header = "X-Roundup-%s-%s"%(self.classname, propname) 618 header = "X-Roundup-%s-%s" % (self.classname, propname)
614 try: 619 try:
615 values.encode('ascii') 620 values.encode('ascii')
616 message[header] = values 621 message[header] = values
617 except UnicodeError: 622 except UnicodeError:
618 message[header] = Header(values, charset) 623 message[header] = Header(values, charset)
619 624
620 # Add header for main id number to make filtering 625 # Add header for main id number to make filtering
621 # email easier than extracting from subject line. 626 # email easier than extracting from subject line.
622 header = "X-Roundup-%s-Id"%(self.classname) 627 header = "X-Roundup-%s-Id" % (self.classname)
623 values = issueid 628 values = issueid
624 try: 629 try:
625 values.encode('ascii') 630 values.encode('ascii')
626 message[header] = values 631 message[header] = values
627 except UnicodeError: 632 except UnicodeError:
685 main, sub = mime_type.split('/') 690 main, sub = mime_type.split('/')
686 part = MIMEBase(main, sub) 691 part = MIMEBase(main, sub)
687 part.set_payload(content) 692 part.set_payload(content)
688 encoders.encode_base64(part) 693 encoders.encode_base64(part)
689 cd = 'Content-Disposition' 694 cd = 'Content-Disposition'
690 part[cd] = 'attachment;\n filename="%s"'%name 695 part[cd] = 'attachment;\n filename="%s"' % name
691 message.attach(part) 696 message.attach(part)
692 697
693 else: 698 else:
694 message.set_payload(body, message.get_charset()) 699 message.set_payload(body, message.get_charset())
695 700
696 if crypt: 701 if crypt:
697 send_msg = self.encrypt_to (message, sendto) 702 send_msg = self.encrypt_to(message, sendto)
698 else: 703 else:
699 send_msg = message 704 send_msg = message
700 mailer.set_message_attributes(send_msg, sendto, subject, author) 705 mailer.set_message_attributes(send_msg, sendto, subject, author)
701 if crypt: 706 if crypt:
702 send_msg ['Message-Id'] = message ['Message-Id'] 707 send_msg['Message-Id'] = message['Message-Id']
703 send_msg ['Reply-To'] = message ['Reply-To'] 708 send_msg['Reply-To'] = message['Reply-To']
704 if message.get ('In-Reply-To'): 709 if message.get('In-Reply-To'):
705 send_msg ['In-Reply-To'] = message ['In-Reply-To'] 710 send_msg['In-Reply-To'] = message['In-Reply-To']
706 711
707 if sendto: 712 if sendto:
708 mailer.smtp_send(sendto, send_msg.as_string()) 713 mailer.smtp_send(sendto, send_msg.as_string())
709 if first: 714 if first:
710 if crypt: 715 if crypt:
711 # send individual bcc mails, otherwise receivers can 716 # send individual bcc mails, otherwise receivers can
712 # deduce bcc recipients from keys in message 717 # deduce bcc recipients from keys in message
713 for bcc in bcc_sendto: 718 for bcc in bcc_sendto:
714 send_msg = self.encrypt_to (message, [bcc]) 719 send_msg = self.encrypt_to(message, [bcc])
715 send_msg ['Message-Id'] = message ['Message-Id'] 720 send_msg['Message-Id'] = message['Message-Id']
716 send_msg ['Reply-To'] = message ['Reply-To'] 721 send_msg['Reply-To'] = message['Reply-To']
717 if message.get ('In-Reply-To'): 722 if message.get('In-Reply-To'):
718 send_msg ['In-Reply-To'] = message ['In-Reply-To'] 723 send_msg['In-Reply-To'] = message['In-Reply-To']
719 mailer.smtp_send([bcc], send_msg.as_string()) 724 mailer.smtp_send([bcc], send_msg.as_string())
720 elif bcc_sendto: 725 elif bcc_sendto:
721 mailer.smtp_send(bcc_sendto, send_msg.as_string()) 726 mailer.smtp_send(bcc_sendto, send_msg.as_string())
722 first = False 727 first = False
723 728
725 ''' Add a signature to the e-mail with some useful information 730 ''' Add a signature to the e-mail with some useful information
726 ''' 731 '''
727 # simplistic check to see if the url is valid, 732 # simplistic check to see if the url is valid,
728 # then append a trailing slash if it is missing 733 # then append a trailing slash if it is missing
729 base = self.db.config.TRACKER_WEB 734 base = self.db.config.TRACKER_WEB
730 if (not isinstance(base , type('')) or 735 if (not isinstance(base, type('')) or
731 not (base.startswith('http://') or base.startswith('https://'))): 736 not (base.startswith('http://') or base.startswith('https://'))):
732 web = "Configuration Error: TRACKER_WEB isn't a " \ 737 web = "Configuration Error: TRACKER_WEB isn't a " \
733 "fully-qualified URL" 738 "fully-qualified URL"
734 else: 739 else:
735 if not base.endswith('/'): 740 if not base.endswith('/'):
736 base = base + '/' 741 base = base + '/'
737 web = base + self.classname + issueid 742 web = base + self.classname + issueid
738 743
739 # ensure the email address is properly quoted 744 # ensure the email address is properly quoted
740 email = formataddr((self.db.config.TRACKER_NAME, 745 email = formataddr((self.db.config.TRACKER_NAME,
741 self.db.config.TRACKER_EMAIL)) 746 self.db.config.TRACKER_EMAIL))
742 747
743 line = '_' * max(len(web)+2, len(email)) 748 line = '_' * max(len(web)+2, len(email))
744 return '\n%s\n%s\n<%s>\n%s'%(line, email, web, line) 749 return '\n%s\n%s\n<%s>\n%s' % (line, email, web, line)
745
746 750
747 def generateCreateNote(self, issueid): 751 def generateCreateNote(self, issueid):
748 """Generate a create note that lists initial property values 752 """Generate a create note that lists initial property values
749 """ 753 """
750 cn = self.classname 754 cn = self.classname
781 value = ', '.join(value) 785 value = ', '.join(value)
782 else: 786 else:
783 value = str(value) 787 value = str(value)
784 if '\n' in value: 788 if '\n' in value:
785 value = '\n'+self.indentChangeNoteValue(value) 789 value = '\n'+self.indentChangeNoteValue(value)
786 m.append('%s: %s'%(propname, value)) 790 m.append('%s: %s' % (propname, value))
787 m.insert(0, '----------') 791 m.insert(0, '----------')
788 m.insert(0, '') 792 m.insert(0, '')
789 return '\n'.join(m) 793 return '\n'.join(m)
790 794
791 def generateChangeNote(self, issueid, oldvalues): 795 def generateChangeNote(self, issueid, oldvalues):
792 """Generate a change note that lists property changes 796 """Generate a change note that lists property changes
793 """ 797 """
794 if not isinstance(oldvalues, type({})): 798 if not isinstance(oldvalues, type({})):
795 raise TypeError("'oldvalues' must be dict-like, not %s."% 799 raise TypeError("'oldvalues' must be dict-like, not %s." %
796 type(oldvalues)) 800 type(oldvalues))
797 801
798 cn = self.classname 802 cn = self.classname
799 cl = self.db.classes[cn] 803 cl = self.db.classes[cn]
800 changed = {} 804 changed = {}
801 props = cl.getprops(protected=0) 805 props = cl.getprops(protected=0)
802 806
803 # determine what changed 807 # determine what changed
804 for key in oldvalues.keys(): 808 for key in oldvalues.keys():
805 if key in ['files','messages']: 809 if key in ['files', 'messages']:
806 continue 810 continue
807 if key in ('actor', 'activity', 'creator', 'creation'): 811 if key in ('actor', 'activity', 'creator', 'creation'):
808 continue 812 continue
809 # not all keys from oldvalues might be available in database 813 # not all keys from oldvalues might be available in database
810 # this happens when property was deleted 814 # this happens when property was deleted
814 continue 818 continue
815 # the old value might be non existent 819 # the old value might be non existent
816 # this happens when property was added 820 # this happens when property was added
817 try: 821 try:
818 old_value = oldvalues[key] 822 old_value = oldvalues[key]
819 if type(new_value) is type([]): 823 if isinstance(new_value, type([])):
820 new_value.sort() 824 new_value.sort()
821 old_value.sort() 825 old_value.sort()
822 if new_value != old_value: 826 if new_value != old_value:
823 changed[key] = old_value 827 changed[key] = old_value
824 except: 828 except Exception:
825 changed[key] = new_value 829 changed[key] = new_value
826 830
827 # list the changes 831 # list the changes
828 m = [] 832 m = []
829 changed_items = sorted(changed.items()) 833 changed_items = sorted(changed.items())
843 value = '' 847 value = ''
844 if oldvalue: 848 if oldvalue:
845 oldvalue = link.get(oldvalue, key) 849 oldvalue = link.get(oldvalue, key)
846 else: 850 else:
847 oldvalue = '' 851 oldvalue = ''
848 change = '%s -> %s'%(oldvalue, value) 852 change = '%s -> %s' % (oldvalue, value)
849 elif isinstance(prop, hyperdb.Multilink): 853 elif isinstance(prop, hyperdb.Multilink):
850 change = '' 854 change = ''
851 if value is None: value = [] 855 if value is None: value = []
852 if oldvalue is None: oldvalue = [] 856 if oldvalue is None: oldvalue = []
853 l = [] 857 l = []
860 l.append(link.get(entry, key)) 864 l.append(link.get(entry, key))
861 else: 865 else:
862 l.append(entry) 866 l.append(entry)
863 if l: 867 if l:
864 l.sort() 868 l.sort()
865 change = '+%s'%(', '.join(l)) 869 change = '+%s' % (', '.join(l))
866 l = [] 870 l = []
867 # check for removals 871 # check for removals
868 for entry in oldvalue: 872 for entry in oldvalue:
869 if entry in value: continue 873 if entry in value: continue
870 if key: 874 if key:
871 l.append(link.get(entry, key)) 875 l.append(link.get(entry, key))
872 else: 876 else:
873 l.append(entry) 877 l.append(entry)
874 if l: 878 if l:
875 l.sort() 879 l.sort()
876 change += ' -%s'%(', '.join(l)) 880 change += ' -%s' % (', '.join(l))
877 else: 881 else:
878 change = '%s -> %s'%(oldvalue, value) 882 change = '%s -> %s' % (oldvalue, value)
879 if '\n' in change: 883 if '\n' in change:
880 value = self.indentChangeNoteValue(str(value)) 884 value = self.indentChangeNoteValue(str(value))
881 oldvalue = self.indentChangeNoteValue(str(oldvalue)) 885 oldvalue = self.indentChangeNoteValue(str(oldvalue))
882 change = _('\nNow:\n%(new)s\nWas:\n%(old)s') % { 886 change = _('\nNow:\n%(new)s\nWas:\n%(old)s') % {
883 "new": value, "old": oldvalue} 887 "new": value, "old": oldvalue}
884 m.append('%s: %s'%(propname, change)) 888 m.append('%s: %s' % (propname, change))
885 if m: 889 if m:
886 m.insert(0, '----------') 890 m.insert(0, '----------')
887 m.insert(0, '') 891 m.insert(0, '')
888 return '\n'.join(m) 892 return '\n'.join(m)
889 893
890 def indentChangeNoteValue(self, text): 894 def indentChangeNoteValue(self, text):
891 lines = text.rstrip('\n').split('\n') 895 lines = text.rstrip('\n').split('\n')
892 lines = [ ' '+line for line in lines ] 896 lines = [' '+line for line in lines]
893 return '\n'.join(lines) 897 return '\n'.join(lines)
894 898
895 # vim: set filetype=python sts=4 sw=4 et si : 899 # vim: set filetype=python sts=4 sw=4 et si :

Roundup Issue Tracker: http://roundup-tracker.org/