You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@bloodhound.apache.org by rj...@apache.org on 2014/02/13 06:08:08 UTC
svn commit: r1567849 [8/17] - in /bloodhound/vendor/trac: 1.0-stable/
current/ current/contrib/ current/contrib/cgi-bin/
current/contrib/workflow/ current/doc/ current/doc/utils/
current/sample-plugins/ current/sample-plugins/permissions/ current/sampl...
Modified: bloodhound/vendor/trac/current/trac/tests/functional/testenv.py
URL: http://svn.apache.org/viewvc/bloodhound/vendor/trac/current/trac/tests/functional/testenv.py?rev=1567849&r1=1567848&r2=1567849&view=diff
==============================================================================
--- bloodhound/vendor/trac/current/trac/tests/functional/testenv.py (original)
+++ bloodhound/vendor/trac/current/trac/tests/functional/testenv.py Thu Feb 13 05:08:02 2014
@@ -1,24 +1,40 @@
-#!/usr/bin/python
# -*- coding: utf-8 -*-
#
+# Copyright (C) 2008-2013 Edgewall Software
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.org/wiki/TracLicense.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://trac.edgewall.org/log/.
+
"""Object for creating and destroying a Trac environment for testing purposes.
Provides some Trac environment-wide utility functions, and a way to call
:command:`trac-admin` without it being on the path."""
+import locale
import os
-import time
-import signal
+import re
import sys
-import errno
-import locale
+import time
from subprocess import call, Popen, PIPE, STDOUT
from trac.env import open_environment
from trac.test import EnvironmentStub, get_dburi
-from trac.tests.functional.compat import rmtree
+from trac.tests.compat import rmtree
from trac.tests.functional import logfile
from trac.tests.functional.better_twill import tc, ConnectError
-from trac.util.compat import close_fds
+from trac.util import terminate
+from trac.util.compat import close_fds, wait_for_file_mtime_change
+from trac.util.text import to_utf8
+
+try:
+ from configobj import ConfigObj
+except ImportError:
+ ConfigObj = None
# TODO: refactor to support testing multiple frontends, backends
# (and maybe repositories and authentication).
@@ -33,6 +49,7 @@ from trac.util.compat import close_fds
# (those need to test search escaping, among many other things like long
# paths in browser and unicode chars being allowed/translating...)
+
class FunctionalTestEnvironment(object):
"""Common location for convenience functions that work with the test
environment on Trac. Subclass this and override some methods if you are
@@ -125,10 +142,11 @@ class FunctionalTestEnvironment(object):
if call([sys.executable,
os.path.join(self.trac_src, 'contrib', 'htpasswd.py'), "-c",
"-b", self.htpasswd, "admin", "admin"], close_fds=close_fds,
- cwd=self.command_cwd):
+ cwd=self.command_cwd):
raise Exception('Unable to setup admin password')
self.adduser('user')
- self._tracadmin('permission', 'add', 'admin', 'TRAC_ADMIN')
+ self.adduser('joe')
+ self.grant_perm('admin', 'TRAC_ADMIN')
# Setup Trac logging
env = self.get_trac_environment()
env.config.set('logging', 'log_type', 'file')
@@ -140,23 +158,89 @@ class FunctionalTestEnvironment(object):
def adduser(self, user):
"""Add a user to the environment. The password will be set to the
same as username."""
+ user = to_utf8(user)
if call([sys.executable, os.path.join(self.trac_src, 'contrib',
'htpasswd.py'), '-b', self.htpasswd,
user, user], close_fds=close_fds, cwd=self.command_cwd):
raise Exception('Unable to setup password for user "%s"' % user)
+ def deluser(self, user):
+ """Delete a user from the environment."""
+ user = to_utf8(user)
+ self._tracadmin('session', 'delete', user)
+ if call([sys.executable, os.path.join(self.trac_src, 'contrib',
+ 'htpasswd.py'), '-D', self.htpasswd, user],
+ close_fds=close_fds, cwd=self.command_cwd):
+ raise Exception('Unable to remove password for user "%s"' % user)
+
+ def grant_perm(self, user, perm):
+ """Grant permission(s) to specified user. A single permission may
+ be specified as a string, or multiple permissions may be
+ specified as a list or tuple of strings."""
+ if isinstance(perm, (list, tuple)):
+ self._tracadmin('permission', 'add', user, *perm)
+ else:
+ self._tracadmin('permission', 'add', user, perm)
+ # We need to force an environment reset, as this is necessary
+ # for the permission change to take effect: grant only
+ # invalidates the `DefaultPermissionStore._all_permissions`
+ # cache, but the `DefaultPermissionPolicy.permission_cache` is
+ # unaffected.
+ self.get_trac_environment().config.touch()
+
+ def revoke_perm(self, user, perm):
+ """Revoke permission(s) from specified user. A single permission
+ may be specified as a string, or multiple permissions may be
+ specified as a list or tuple of strings."""
+ if isinstance(perm, (list, tuple)):
+ self._tracadmin('permission', 'remove', user, *perm)
+ else:
+ self._tracadmin('permission', 'remove', user, perm)
+ # Force an environment reset (see grant_perm above)
+ self.get_trac_environment().config.touch()
+
+ def set_config(self, *args):
+ """Calls trac-admin to get the value for the given option
+ in `trac.ini`."""
+ self._tracadmin('config', 'set', *args)
+
+ def get_config(self, *args):
+ """Calls trac-admin to set the value for the given option
+ in `trac.ini`."""
+ return self._tracadmin('config', 'get', *args)
+
+ def remove_config(self, *args):
+ """Calls trac-admin to remove the value for the given option
+ in `trac.ini`."""
+ return self._tracadmin('config', 'remove', *args)
+
def _tracadmin(self, *args):
"""Internal utility method for calling trac-admin"""
proc = Popen([sys.executable, os.path.join(self.trac_src, 'trac',
- 'admin', 'console.py'), self.tracdir]
- + list(args), stdout=PIPE, stderr=STDOUT,
- close_fds=close_fds, cwd=self.command_cwd)
- out = proc.communicate()[0]
+ 'admin', 'console.py'), self.tracdir],
+ stdin=PIPE, stdout=PIPE, stderr=STDOUT,
+ close_fds=close_fds, cwd=self.command_cwd)
+ if args:
+ if any('\n' in arg for arg in args):
+ raise Exception(
+ "trac-admin in interactive mode doesn't support "
+ "arguments with newline characters: %r" % (args,))
+ # Don't quote first token which is sub-command name
+ input = ' '.join(('"%s"' % to_utf8(arg) if idx else arg)
+ for idx, arg in enumerate(args))
+ else:
+ input = None
+ out = proc.communicate(input=input)[0]
if proc.returncode:
print(out)
logfile.write(out)
- raise Exception('Failed with exitcode %s running trac-admin ' \
- 'with %r' % (proc.returncode, args))
+ raise Exception("Failed while running trac-admin with arguments %r.\n"
+ "Exitcode: %s \n%s"
+ % (args, proc.returncode, out))
+ else:
+ # trac-admin is started in interactive mode, so we strip away
+ # everything up to the to the interactive prompt
+ return re.split(r'\r?\nTrac \[[^]]+\]> ', out, 2)[1]
def start(self):
"""Starts the webserver, and waits for it to come up."""
@@ -177,8 +261,7 @@ class FunctionalTestEnvironment(object):
server = Popen(args + options + [self.tracdir],
stdout=logfile, stderr=logfile,
close_fds=close_fds,
- cwd=self.command_cwd,
- )
+ cwd=self.command_cwd)
self.pid = server.pid
# Verify that the url is ok
timeout = 30
@@ -199,17 +282,7 @@ class FunctionalTestEnvironment(object):
FIXME: probably needs a nicer way to exit for coverage to work
"""
if self.pid:
- if os.name == 'nt':
- # Untested
- res = call(["taskkill", "/f", "/pid", str(self.pid)],
- stdin=PIPE, stdout=PIPE, stderr=PIPE)
- else:
- os.kill(self.pid, signal.SIGTERM)
- try:
- os.waitpid(self.pid, 0)
- except OSError, e:
- if e.errno != errno.ESRCH:
- raise
+ terminate(self)
def restart(self):
"""Restarts the webserver"""
@@ -224,13 +297,70 @@ class FunctionalTestEnvironment(object):
"""Default to no repository"""
return "''" # needed for Python 2.3 and 2.4 on win32
- def call_in_workdir(self, args, environ=None):
+ def call_in_dir(self, dir, args, environ=None):
proc = Popen(args, stdout=PIPE, stderr=logfile,
- close_fds=close_fds, cwd=self.work_dir(), env=environ)
+ close_fds=close_fds, cwd=dir, env=environ)
(data, _) = proc.communicate()
if proc.wait():
raise Exception('Unable to run command %s in %s' %
- (args, self.work_dir()))
-
+ (args, dir))
logfile.write(data)
return data
+
+ def enable_authz_permpolicy(self, authz_content, filename=None):
+ """Enables the Authz permissions policy. The `authz_content` will
+ be written to `filename`, and may be specified in a triple-quoted
+ string.
+ '''
+ [wiki:WikiStart@*]
+ * = WIKI_VIEW
+ [wiki:PrivatePage@*]
+ john = WIKI_VIEW
+ * = !WIKI_VIEW
+ '''
+ `authz_content` may also be a dictionary of dictionaries specifying
+ the sections and key/value pairs of each section, however this form
+ should only be used when the order of the entries in the file is not
+ important, as the order cannot be known.
+ {'wiki:WikiStart@*': {'*': 'WIKI_VIEW'},
+ 'wiki:PrivatePage@*': {'john': 'WIKI_VIEW', '*': '!WIKI_VIEW'},
+ }
+ The `filename` parameter is optional, and if omitted a filename will
+ be generated by computing a hash of `authz_content`, prefixed with
+ "authz-".
+ """
+ if not ConfigObj:
+ raise ImportError("Can't enable authz permissions policy. " +
+ "ConfigObj not installed.")
+ if filename is None:
+ from hashlib import md5
+ filename = 'authz-' + md5(str(authz_content)).hexdigest()[0:9]
+ env = self.get_trac_environment()
+ permission_policies = env.config.get('trac', 'permission_policies')
+ env.config.set('trac', 'permission_policies',
+ 'AuthzPolicy, ' + permission_policies)
+ authz_file = self.tracdir + '/conf/' + filename
+ if isinstance(authz_content, basestring):
+ authz_content = [line.strip() for line in
+ authz_content.strip().splitlines()]
+ authz_config = ConfigObj(authz_content, encoding='utf8',
+ write_empty_values=True, indent_type='')
+ authz_config.filename = authz_file
+ wait_for_file_mtime_change(authz_file)
+ authz_config.write()
+ env.config.set('authz_policy', 'authz_file', authz_file)
+ env.config.set('components', 'tracopt.perm.authz_policy.*', 'enabled')
+ env.config.save()
+
+ def disable_authz_permpolicy(self):
+ """Disables the Authz permission policy."""
+ env = self.get_trac_environment()
+ permission_policies = env.config.get('trac', 'permission_policies')
+ pp_list = [p.strip() for p in permission_policies.split(',')]
+ if 'AuthzPolicy' in pp_list:
+ pp_list.remove('AuthzPolicy')
+ permission_policies = ', '.join(pp_list)
+ env.config.set('trac', 'permission_policies', permission_policies)
+ env.config.remove('authz_policy', 'authz_file')
+ env.config.remove('components', 'tracopt.perm.authz_policy.*')
+ env.config.save()
Modified: bloodhound/vendor/trac/current/trac/tests/functional/tester.py
URL: http://svn.apache.org/viewvc/bloodhound/vendor/trac/current/trac/tests/functional/tester.py?rev=1567849&r1=1567848&r2=1567849&view=diff
==============================================================================
--- bloodhound/vendor/trac/current/trac/tests/functional/tester.py (original)
+++ bloodhound/vendor/trac/current/trac/tests/functional/tester.py Thu Feb 13 05:08:02 2014
@@ -1,19 +1,34 @@
-#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2008-2013 Edgewall Software
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.org/wiki/TracLicense.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://trac.edgewall.org/log/.
+
"""The :class:`FunctionalTester` object provides a higher-level interface to
working with a Trac environment to make test cases more succinct.
"""
+import re
+
from trac.tests.functional import internal_error
from trac.tests.functional.better_twill import tc, b
from trac.tests.contentgen import random_page, random_sentence, random_word, \
random_unique_camel
-from trac.util.text import unicode_quote
+from trac.util.text import to_utf8, unicode_quote
try:
from cStringIO import StringIO
except ImportError:
from StringIO import StringIO
+
class FunctionalTester(object):
"""Provides a library of higher-level operations for interacting with a
test environment.
@@ -34,10 +49,11 @@ class FunctionalTester(object):
def login(self, username):
"""Login as the given user"""
+ username = to_utf8(username)
tc.add_auth("", self.url, username, username)
self.go_to_front()
tc.find("Login")
- tc.follow("Login")
+ tc.follow(r"\bLogin\b")
# We've provided authentication info earlier, so this should
# redirect back to the base url.
tc.find("logged in as %s" % username)
@@ -47,8 +63,9 @@ class FunctionalTester(object):
def logout(self):
"""Logout"""
- tc.follow("Logout")
+ tc.submit('logout', 'logout')
tc.notfind(internal_error)
+ tc.notfind('logged in as')
def create_ticket(self, summary=None, info=None):
"""Create a new (random) ticket in the test environment. Returns
@@ -63,10 +80,10 @@ class FunctionalTester(object):
`summary` and `description` default to randomly-generated values.
"""
self.go_to_front()
- tc.follow('New Ticket')
+ tc.follow(r"\bNew Ticket\b")
tc.notfind(internal_error)
- if summary == None:
- summary = random_sentence(4)
+ if summary is None:
+ summary = random_sentence(5)
tc.formvalue('propertyform', 'field_summary', summary)
tc.formvalue('propertyform', 'field_description', random_page())
if info:
@@ -75,17 +92,12 @@ class FunctionalTester(object):
tc.submit('submit')
# we should be looking at the newly created ticket
tc.url(self.url + '/ticket/%s' % (self.ticketcount + 1))
+ tc.notfind(internal_error)
# Increment self.ticketcount /after/ we've verified that the ticket
# was created so a failure does not trigger spurious later
# failures.
self.ticketcount += 1
- # verify the ticket creation event shows up in the timeline
- self.go_to_timeline()
- tc.formvalue('prefs', 'ticket', True)
- tc.submit()
- tc.find('Ticket.*#%s.*created' % self.ticketcount)
-
return self.ticketcount
def quickjump(self, search):
@@ -94,51 +106,91 @@ class FunctionalTester(object):
tc.submit()
tc.notfind(internal_error)
+ def go_to_url(self, url):
+ tc.go(url)
+ tc.url(re.escape(url))
+ tc.notfind(internal_error)
+
def go_to_front(self):
"""Go to the Trac front page"""
- tc.go(self.url)
- tc.url(self.url)
- tc.notfind(internal_error)
+ self.go_to_url(self.url)
- def go_to_ticket(self, ticketid):
- """Surf to the page for the given ticket ID. Assumes ticket
- exists."""
- ticket_url = self.url + "/ticket/%s" % ticketid
- tc.go(ticket_url)
- tc.url(ticket_url)
+ def go_to_ticket(self, ticketid=None):
+ """Surf to the page for the given ticket ID, or to the NewTicket page
+ if `ticketid` is not specified or is `None`. If `ticketid` is
+ specified, it assumes the ticket exists."""
+ if ticketid is not None:
+ ticket_url = self.url + '/ticket/%s' % ticketid
+ else:
+ ticket_url = self.url + '/newticket'
+ self.go_to_url(ticket_url)
+ tc.url(ticket_url + '$')
+
+ def go_to_wiki(self, name, version=None):
+ """Surf to the wiki page. By default this will be the latest version
+ of the page.
- def go_to_wiki(self, name):
- """Surf to the page for the given wiki page."""
+ :param name: name of the wiki page.
+ :param version: version of the wiki page.
+ """
# Used to go based on a quickjump, but if the wiki pagename isn't
# camel case, that won't work.
wiki_url = self.url + '/wiki/%s' % name
- tc.go(wiki_url)
- tc.url(wiki_url)
+ if version:
+ wiki_url += '?version=%s' % version
+ self.go_to_url(wiki_url)
def go_to_timeline(self):
"""Surf to the timeline page."""
self.go_to_front()
- tc.follow('Timeline')
+ tc.follow(r"\bTimeline\b")
tc.url(self.url + '/timeline')
+ def go_to_view_tickets(self, href='report'):
+ """Surf to the View Tickets page. By default this will be the Reports
+ page, but 'query' can be specified for the `href` argument to support
+ non-default configurations."""
+ self.go_to_front()
+ tc.follow(r"\bView Tickets\b")
+ tc.url(self.url + '/' + href.lstrip('/'))
+
def go_to_query(self):
"""Surf to the custom query page."""
self.go_to_front()
- tc.follow('View Tickets')
- tc.follow('Custom Query')
+ tc.follow(r"\bView Tickets\b")
+ tc.follow(r"\bCustom Query\b")
tc.url(self.url + '/query')
- def go_to_admin(self):
- """Surf to the webadmin page."""
+ def go_to_admin(self, panel_label=None):
+ """Surf to the webadmin page. Continue surfing to a specific
+ admin page if `panel_label` is specified."""
self.go_to_front()
- tc.follow('\\bAdmin\\b')
+ tc.follow(r"\bAdmin\b")
+ tc.url(self.url + '/admin')
+ if panel_label is not None:
+ tc.follow(r"\b%s\b" % panel_label)
def go_to_roadmap(self):
"""Surf to the roadmap page."""
self.go_to_front()
- tc.follow('\\bRoadmap\\b')
+ tc.follow(r"\bRoadmap\b")
tc.url(self.url + '/roadmap')
+ def go_to_milestone(self, name):
+ """Surf to the specified milestone page. Assumes milestone exists."""
+ self.go_to_roadmap()
+ tc.follow(r"\bMilestone: %s\b" % name)
+ tc.url(self.url + '/milestone/%s' % name)
+
+ def go_to_preferences(self, panel_label=None):
+ """Surf to the preferences page. Continue surfing to a specific
+ preferences panel if `panel_label` is specified."""
+ self.go_to_front()
+ tc.follow(r"\bPreferences\b")
+ tc.url(self.url + '/prefs')
+ if panel_label is not None:
+ tc.follow(r"\b%s\b" % panel_label)
+
def add_comment(self, ticketid, comment=None):
"""Adds a comment to the given ticket ID, assumes ticket exists."""
self.go_to_ticket(ticketid)
@@ -152,35 +204,16 @@ class FunctionalTester(object):
tc.url(self.url + '/ticket/%s(?:#comment:.*)?$' % ticketid)
return comment
- def attach_file_to_ticket(self, ticketid, data=None, tempfilename=None,
+ def attach_file_to_ticket(self, ticketid, data=None, filename=None,
description=None, replace=False,
content_type=None):
"""Attaches a file to the given ticket id, with random data if none is
provided. Assumes the ticket exists.
"""
- if data is None:
- data = random_page()
- if description is None:
- description = random_sentence()
- if tempfilename is None:
- tempfilename = random_word()
-
self.go_to_ticket(ticketid)
- # set the value to what it already is, so that twill will know we
- # want this form.
- tc.formvalue('attachfile', 'action', 'new')
- tc.submit()
- tc.url(self.url + "/attachment/ticket/" \
- "%s/\\?action=new&attachfilebutton=Attach\\+file" % ticketid)
- fp = StringIO(data)
- tc.formfile('attachment', 'attachment', tempfilename,
- content_type=content_type, fp=fp)
- tc.formvalue('attachment', 'description', description)
- if replace:
- tc.formvalue('attachment', 'replace', True)
- tc.submit()
- tc.url(self.url + '/attachment/ticket/%s/$' % ticketid)
- return tempfilename
+ return self._attach_file_to_resource('ticket', ticketid, data,
+ filename, description,
+ replace, content_type)
def clone_ticket(self, ticketid):
"""Create a clone of the given ticket id using the clone button."""
@@ -194,57 +227,66 @@ class FunctionalTester(object):
tc.url(self.url + "/ticket/%s" % self.ticketcount)
return self.ticketcount
- def create_wiki_page(self, page, content=None):
- """Creates the specified wiki page, with random content if none is
- provided.
+ def create_wiki_page(self, name=None, content=None, comment=None):
+ """Creates a wiki page, with a random unique CamelCase name if none
+ is provided, random content if none is provided and a random comment
+ if none is provided. Returns the name of the wiki page.
"""
- if content == None:
+ if name is None:
+ name = random_unique_camel()
+ if content is None:
content = random_page()
- page_url = self.url + "/wiki/" + page
- tc.go(page_url)
- tc.url(page_url)
- tc.find("The page %s does not exist." % page)
- tc.formvalue('modifypage', 'action', 'edit')
- tc.submit()
- tc.url(page_url + '\\?action=edit')
+ self.go_to_wiki(name)
+ tc.find("The page %s does not exist." % name)
- tc.formvalue('edit', 'text', content)
- tc.submit('save')
- tc.url(page_url+'$')
+ self.edit_wiki_page(name, content, comment)
# verify the event shows up in the timeline
self.go_to_timeline()
tc.formvalue('prefs', 'wiki', True)
tc.submit()
- tc.find(page + ".*created")
+ tc.find(name + ".*created")
- def attach_file_to_wiki(self, name, data=None, tempfilename=None):
+ self.go_to_wiki(name)
+
+ return name
+
+ def edit_wiki_page(self, name, content=None, comment=None):
+ """Edits a wiki page, with random content is none is provided.
+ and a random comment if none is provided. Returns the content.
+ """
+ if content is None:
+ content = random_page()
+ if comment is None:
+ comment = random_sentence()
+ self.go_to_wiki(name)
+ tc.formvalue('modifypage', 'action', 'edit')
+ tc.submit()
+ tc.formvalue('edit', 'text', content)
+ tc.formvalue('edit', 'comment', comment)
+ tc.submit('save')
+ page_url = self.url + '/wiki/%s' % name
+ tc.url(page_url+'$')
+
+ return content
+
+ def attach_file_to_wiki(self, name, data=None, filename=None,
+ description=None, replace=False,
+ content_type=None):
"""Attaches a file to the given wiki page, with random content if none
is provided. Assumes the wiki page exists.
"""
- if data == None:
- data = random_page()
- if tempfilename is None:
- tempfilename = random_word()
+
self.go_to_wiki(name)
- # set the value to what it already is, so that twill will know we
- # want this form.
- tc.formvalue('attachfile', 'action', 'new')
- tc.submit()
- tc.url(self.url + "/attachment/wiki/" \
- "%s/\\?action=new&attachfilebutton=Attach\\+file" % name)
- fp = StringIO(data)
- tc.formfile('attachment', 'attachment', tempfilename, fp=fp)
- tc.formvalue('attachment', 'description', random_sentence())
- tc.submit()
- tc.url(self.url + '/attachment/wiki/%s/$' % name)
- return tempfilename
+ return self._attach_file_to_resource('wiki', name, data,
+ filename, description,
+ replace, content_type)
def create_milestone(self, name=None, due=None):
"""Creates the specified milestone, with a random name if none is
provided. Returns the name of the milestone.
"""
- if name == None:
+ if name is None:
name = random_unique_camel()
milestone_url = self.url + "/admin/ticket/milestones"
tc.go(milestone_url)
@@ -260,32 +302,51 @@ class FunctionalTester(object):
tc.find(name)
# Make sure it's on the roadmap.
- tc.follow('Roadmap')
+ tc.follow(r"\bRoadmap\b")
tc.url(self.url + "/roadmap")
tc.find('Milestone:.*%s' % name)
- tc.follow(name)
+ tc.follow(r"\b%s\b" % name)
tc.url('%s/milestone/%s' % (self.url, unicode_quote(name)))
if not due:
tc.find('No date set')
return name
- def create_component(self, name=None, user=None):
+ def attach_file_to_milestone(self, name, data=None, filename=None,
+ description=None, replace=False,
+ content_type=None):
+ """Attaches a file to the given milestone, with random content if none
+ is provided. Assumes the milestone exists.
+ """
+
+ self.go_to_milestone(name)
+ return self._attach_file_to_resource('milestone', name, data,
+ filename, description,
+ replace, content_type)
+
+ def create_component(self, name=None, owner=None, description=None):
"""Creates the specified component, with a random camel-cased name if
none is provided. Returns the name."""
- if name == None:
+ if name is None:
name = random_unique_camel()
component_url = self.url + "/admin/ticket/components"
tc.go(component_url)
tc.url(component_url)
tc.formvalue('addcomponent', 'name', name)
- if user != None:
- tc.formvalue('addcomponent', 'owner', user)
+ if owner is not None:
+ tc.formvalue('addcomponent', 'owner', owner)
tc.submit()
# Verify the component appears in the component list
tc.url(component_url)
tc.find(name)
tc.notfind(internal_error)
+ if description is not None:
+ tc.follow(r"\b%s\b" % name)
+ tc.formvalue('modcomp', 'description', description)
+ tc.submit('save')
+ tc.url(component_url)
+ tc.find("Your changes have been saved.")
+ tc.notfind(internal_error)
# TODO: verify the component shows up in the newticket page
return name
@@ -294,7 +355,7 @@ class FunctionalTester(object):
``severity``, etc). If no name is given, a unique random word is used.
The name is returned.
"""
- if name == None:
+ if name is None:
name = random_unique_camel()
priority_url = self.url + "/admin/ticket/" + kind
tc.go(priority_url)
@@ -326,12 +387,12 @@ class FunctionalTester(object):
"""Create a new version. The name defaults to a random camel-cased
word if not provided."""
version_admin = self.url + "/admin/ticket/versions"
- if name == None:
+ if name is None:
name = random_unique_camel()
tc.go(version_admin)
tc.url(version_admin)
tc.formvalue('addversion', 'name', name)
- if releasetime != None:
+ if releasetime is not None:
tc.formvalue('addversion', 'time', releasetime)
tc.submit()
tc.url(version_admin)
@@ -342,7 +403,7 @@ class FunctionalTester(object):
def create_report(self, title, query, description):
"""Create a new report with the given title, query, and description"""
self.go_to_front()
- tc.follow('View Tickets')
+ tc.follow(r"\bView Tickets\b")
tc.formvalue('create_report', 'action', 'new') # select the right form
tc.submit()
tc.find('New Report')
@@ -364,3 +425,29 @@ class FunctionalTester(object):
tc.formvalue('propertyform', 'milestone', milestone)
tc.submit('submit')
# TODO: verify the change occurred.
+
+ def _attach_file_to_resource(self, realm, name, data=None,
+ filename=None, description=None,
+ replace=False, content_type=None):
+ """Attaches a file to a resource. Assumes the resource exists and
+ has already been navigated to."""
+
+ if data is None:
+ data = random_page()
+ if description is None:
+ description = random_sentence()
+ if filename is None:
+ filename = random_word()
+
+ tc.submit('attachfilebutton', 'attachfile')
+ tc.url(self.url + r'/attachment/%s/%s/\?action=new$' % (realm, name))
+ fp = StringIO(data)
+ tc.formfile('attachment', 'attachment', filename,
+ content_type=content_type, fp=fp)
+ tc.formvalue('attachment', 'description', description)
+ if replace:
+ tc.formvalue('attachment', 'replace', True)
+ tc.submit()
+ tc.url(self.url + r'/attachment/%s/%s/$' % (realm, name))
+
+ return filename
Modified: bloodhound/vendor/trac/current/trac/tests/notification.py
URL: http://svn.apache.org/viewvc/bloodhound/vendor/trac/current/trac/tests/notification.py?rev=1567849&r1=1567848&r2=1567849&view=diff
==============================================================================
--- bloodhound/vendor/trac/current/trac/tests/notification.py (original)
+++ bloodhound/vendor/trac/current/trac/tests/notification.py Thu Feb 13 05:08:02 2014
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
#
-# Copyright (C) 2005-2009 Edgewall Software
+# Copyright (C) 2005-2013 Edgewall Software
# Copyright (C) 2005-2006 Emmanuel Blot <em...@free.fr>
# All rights reserved.
#
@@ -19,24 +19,30 @@
# classes to run SMTP notification tests
#
+import base64
+import os
+import quopri
+import re
import socket
import string
import threading
-import re
-import base64
-import quopri
+import unittest
+from trac.config import ConfigurationError
+from trac.notification import SendmailEmailSender, SmtpEmailSender
+from trac.test import EnvironmentStub
LF = '\n'
CR = '\r'
-email_re = re.compile(r"([\w\d_\.\-])+\@(([\w\d\-])+\.)+([\w\d]{2,4})+")
+SMTP_TEST_PORT = 7000 + os.getpid() % 1000
+email_re = re.compile(r'([\w\d_\.\-])+\@(([\w\d\-])+\.)+([\w\d]{2,4})+')
header_re = re.compile(r'^=\?(?P<charset>[\w\d\-]+)\?(?P<code>[qb])\?(?P<value>.*)\?=$')
class SMTPServerInterface:
"""
- A base class for the imlementation of an application specific SMTP
- Server. Applications should subclass this and overide these
+ A base class for the implementation of an application specific SMTP
+ Server. Applications should subclass this and override these
methods, which by default do nothing.
A method is defined for each RFC821 command. For each of these
@@ -44,7 +50,7 @@ class SMTPServerInterface:
client. The 'data' method is called after all of the client DATA
is received.
- If a method returns 'None', then a '250 OK'message is
+ If a method returns 'None', then a '250 OK' message is
automatically sent to the client. If a subclass returns a non-null
string then it is returned instead.
"""
@@ -67,10 +73,10 @@ class SMTPServerInterface:
def reset(self, args):
return None
+
#
# Some helper functions for manipulating from & to addresses etc.
#
-
def strip_address(address):
"""
Strip the leading & trailing <> from an address. Handy for
@@ -80,6 +86,7 @@ def strip_address(address):
end = string.index(address, '>')
return address[start:end]
+
def split_to(address):
"""
Return 'address' as undressed (host, fulladdress) tuple.
@@ -88,7 +95,7 @@ def split_to(address):
start = string.index(address, '<') + 1
sep = string.index(address, '@') + 1
end = string.index(address, '>')
- return (address[sep:end], address[start:end],)
+ return address[sep:end], address[start:end]
#
@@ -133,13 +140,13 @@ class SMTPServerEngine:
lump = self.socket.recv(1024)
if len(lump):
data += lump
- if (len(data) >= 2) and data[-2:] == '\r\n':
+ if len(data) >= 2 and data[-2:] == '\r\n':
completeLine = 1
if self.state != SMTPServerEngine.ST_DATA:
rsp, keep = self.do_command(data)
else:
rsp = self.do_data(data)
- if rsp == None:
+ if rsp is None:
continue
self.socket.send(rsp + "\r\n")
if keep == 0:
@@ -171,28 +178,28 @@ class SMTPServerEngine:
keep = 0
elif cmd == "MAIL":
if self.state != SMTPServerEngine.ST_HELO:
- return ("503 Bad command sequence", 1)
+ return "503 Bad command sequence", 1
self.state = SMTPServerEngine.ST_MAIL
rv = self.impl.mail_from(data[5:])
elif cmd == "RCPT":
if (self.state != SMTPServerEngine.ST_MAIL) and \
(self.state != SMTPServerEngine.ST_RCPT):
- return ("503 Bad command sequence", 1)
+ return "503 Bad command sequence", 1
self.state = SMTPServerEngine.ST_RCPT
rv = self.impl.rcpt_to(data[5:])
elif cmd == "DATA":
if self.state != SMTPServerEngine.ST_RCPT:
- return ("503 Bad command sequence", 1)
+ return "503 Bad command sequence", 1
self.state = SMTPServerEngine.ST_DATA
self.data_accum = ""
- return ("354 OK, Enter data, terminated with a \\r\\n.\\r\\n", 1)
+ return "354 OK, Enter data, terminated with a \\r\\n.\\r\\n", 1
else:
- return ("505 Eh? WTF was that?", 1)
+ return "505 Eh? WTF was that?", 1
if rv:
- return (rv, keep)
+ return rv, keep
else:
- return("250 OK", keep)
+ return "250 OK", keep
def do_data(self, data):
"""
@@ -227,7 +234,7 @@ class SMTPServer:
self._socket_service = None
def serve(self, impl):
- while ( self._resume ):
+ while self._resume:
try:
nsd = self._socket.accept()
except socket.error:
@@ -273,11 +280,11 @@ class SMTPServerStore(SMTPServerInterfac
def mail_from(self, args):
if args.lower().startswith('from:'):
- self.sender = strip_address(args[5:].replace('\r\n','').strip())
+ self.sender = strip_address(args[5:].replace('\r\n', '').strip())
def rcpt_to(self, args):
if args.lower().startswith('to:'):
- rcpt = args[3:].replace('\r\n','').strip()
+ rcpt = args[3:].replace('\r\n', '').strip()
self.recipients.append(strip_address(rcpt))
def data(self, args):
@@ -300,12 +307,12 @@ class SMTPThreadedServer(threading.Threa
def __init__(self, port):
self.port = port
self.server = SMTPServer(port)
- self.store = SMTPServerStore()
+ self.store = SMTPServerStore()
threading.Thread.__init__(self)
def run(self):
# run from within the SMTP server thread
- self.server.serve(impl = self.store)
+ self.server.serve(impl=self.store)
def start(self):
# run from the main thread
@@ -356,19 +363,19 @@ def decode_header(header):
# header does not seem to be MIME-encoded
if not mo:
return header
- # attempts to decode the hedear,
- # following the specified MIME endoding and charset
+ # attempts to decode the header,
+ # following the specified MIME encoding and charset
try:
encoding = mo.group('code').lower()
- if encoding == 'q':
+ if encoding == 'q':
val = quopri.decodestring(mo.group('value'), header=True)
elif encoding == 'b':
val = base64.decodestring(mo.group('value'))
else:
- raise AssertionError, "unsupported encoding: %s" % encoding
+ raise AssertionError("unsupported encoding: %s" % encoding)
header = unicode(val, mo.group('charset'))
except Exception, e:
- raise AssertionError, e
+ raise AssertionError(e)
return header
@@ -384,19 +391,19 @@ def parse_smtp_message(msg):
# last line does not contain the final line ending
msg += '\r\n'
for line in msg.splitlines(True):
- if body != None:
+ if body is not None:
# append current line to the body
if line[-2] == CR:
body += line[0:-2]
body += '\n'
else:
- raise AssertionError, "body misses CRLF: %s (0x%x)" \
- % (line, ord(line[-1]))
+ raise AssertionError("body misses CRLF: %s (0x%x)"
+ % (line, ord(line[-1])))
else:
if line[-2] != CR:
# RFC822 requires CRLF at end of field line
- raise AssertionError, "header field misses CRLF: %s (0x%x)" \
- % (line, ord(line[-1]))
+ raise AssertionError("header field misses CRLF: %s (0x%x)"
+ % (line, ord(line[-1])))
# discards CR
line = line[0:-2]
if line.strip() == '':
@@ -405,11 +412,11 @@ def parse_smtp_message(msg):
else:
val = None
if line[0] in ' \t':
- # continution of the previous line
+ # continuation of the previous line
if not lh:
# unexpected multiline
- raise AssertionError, \
- "unexpected folded line: %s" % line
+ raise AssertionError("unexpected folded line: %s"
+ % line)
val = decode_header(line.strip(' \t'))
# appends the current line to the previous one
if not isinstance(headers[lh], tuple):
@@ -420,14 +427,52 @@ def parse_smtp_message(msg):
# splits header name from value
(h, v) = line.split(':', 1)
val = decode_header(v.strip())
- if headers.has_key(h):
+ if h in headers:
if isinstance(headers[h], tuple):
headers[h] += val
else:
headers[h] = (headers[h], val)
else:
headers[h] = val
- # stores the last header (for multilines headers)
+ # stores the last header (for multi-line headers)
lh = h
# returns the headers and the message body
- return (headers, body)
+ return headers, body
+
+
+class SendmailEmailSenderTestCase(unittest.TestCase):
+
+ def setUp(self):
+ self.env = EnvironmentStub()
+
+ def test_sendmail_path_not_found_raises(self):
+ sender = SendmailEmailSender(self.env)
+ self.env.config.set('notification', 'sendmail_path',
+ os.path.join(os.path.dirname(__file__),
+ 'sendmail'))
+ self.assertRaises(ConfigurationError, sender.send,
+ 'admin@domain.com', ['foo@domain.com'], "")
+
+
+class SmtpEmailSenderTestCase(unittest.TestCase):
+
+ def setUp(self):
+ self.env = EnvironmentStub()
+
+ def test_smtp_server_not_found_raises(self):
+ sender = SmtpEmailSender(self.env)
+ self.env.config.set('notification', 'smtp_server', 'localhost')
+ self.env.config.set('notification', 'smtp_port', '65536')
+ self.assertRaises(ConfigurationError, sender.send,
+ 'admin@domain.com', ['foo@domain.com'], "")
+
+
+def suite():
+ suite = unittest.TestSuite()
+ suite.addTest(unittest.makeSuite(SendmailEmailSenderTestCase))
+ suite.addTest(unittest.makeSuite(SmtpEmailSenderTestCase))
+ return suite
+
+
+if __name__ == '__main__':
+ unittest.main(defaultTest='suite')
Modified: bloodhound/vendor/trac/current/trac/tests/perm.py
URL: http://svn.apache.org/viewvc/bloodhound/vendor/trac/current/trac/tests/perm.py?rev=1567849&r1=1567848&r2=1567849&view=diff
==============================================================================
--- bloodhound/vendor/trac/current/trac/tests/perm.py (original)
+++ bloodhound/vendor/trac/current/trac/tests/perm.py Thu Feb 13 05:08:02 2014
@@ -1,3 +1,16 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2004-2013 Edgewall Software
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.org/wiki/TracLicense.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://trac.edgewall.org/log/.
+
from trac import perm
from trac.core import *
from trac.test import EnvironmentStub
@@ -21,10 +34,10 @@ class DefaultPermissionStoreTestCase(uni
[('john', 'WIKI_MODIFY'),
('john', 'REPORT_ADMIN'),
('kate', 'TICKET_CREATE')])
- self.assertEquals(['REPORT_ADMIN', 'WIKI_MODIFY'],
- sorted(self.store.get_user_permissions('john')))
- self.assertEquals(['TICKET_CREATE'],
- self.store.get_user_permissions('kate'))
+ self.assertEqual(['REPORT_ADMIN', 'WIKI_MODIFY'],
+ sorted(self.store.get_user_permissions('john')))
+ self.assertEqual(['TICKET_CREATE'],
+ self.store.get_user_permissions('kate'))
def test_simple_group(self):
self.env.db_transaction.executemany(
@@ -32,8 +45,8 @@ class DefaultPermissionStoreTestCase(uni
[('dev', 'WIKI_MODIFY'),
('dev', 'REPORT_ADMIN'),
('john', 'dev')])
- self.assertEquals(['REPORT_ADMIN', 'WIKI_MODIFY'],
- sorted(self.store.get_user_permissions('john')))
+ self.assertEqual(['REPORT_ADMIN', 'WIKI_MODIFY'],
+ sorted(self.store.get_user_permissions('john')))
def test_nested_groups(self):
self.env.db_transaction.executemany(
@@ -42,8 +55,8 @@ class DefaultPermissionStoreTestCase(uni
('dev', 'REPORT_ADMIN'),
('admin', 'dev'),
('john', 'admin')])
- self.assertEquals(['REPORT_ADMIN', 'WIKI_MODIFY'],
- sorted(self.store.get_user_permissions('john')))
+ self.assertEqual(['REPORT_ADMIN', 'WIKI_MODIFY'],
+ sorted(self.store.get_user_permissions('john')))
def test_mixed_case_group(self):
self.env.db_transaction.executemany(
@@ -52,8 +65,8 @@ class DefaultPermissionStoreTestCase(uni
('Dev', 'REPORT_ADMIN'),
('Admin', 'Dev'),
('john', 'Admin')])
- self.assertEquals(['REPORT_ADMIN', 'WIKI_MODIFY'],
- sorted(self.store.get_user_permissions('john')))
+ self.assertEqual(['REPORT_ADMIN', 'WIKI_MODIFY'],
+ sorted(self.store.get_user_permissions('john')))
def test_builtin_groups(self):
self.env.db_transaction.executemany(
@@ -61,10 +74,10 @@ class DefaultPermissionStoreTestCase(uni
[('authenticated', 'WIKI_MODIFY'),
('authenticated', 'REPORT_ADMIN'),
('anonymous', 'TICKET_CREATE')])
- self.assertEquals(['REPORT_ADMIN', 'TICKET_CREATE', 'WIKI_MODIFY'],
- sorted(self.store.get_user_permissions('john')))
- self.assertEquals(['TICKET_CREATE'],
- self.store.get_user_permissions('anonymous'))
+ self.assertEqual(['REPORT_ADMIN', 'TICKET_CREATE', 'WIKI_MODIFY'],
+ sorted(self.store.get_user_permissions('john')))
+ self.assertEqual(['TICKET_CREATE'],
+ self.store.get_user_permissions('anonymous'))
def test_get_all_permissions(self):
self.env.db_transaction.executemany(
@@ -76,7 +89,7 @@ class DefaultPermissionStoreTestCase(uni
('dev', 'REPORT_ADMIN'),
('john', 'dev')]
for res in self.store.get_all_permissions():
- self.failIf(res not in expected)
+ self.assertFalse(res not in expected)
class TestPermissionRequestor(Component):
@@ -130,7 +143,7 @@ class PermissionSystemTestCase(unittest.
expected = [('bob', 'TEST_CREATE'),
('jane', 'TEST_ADMIN')]
for res in self.perm.get_all_permissions():
- self.failIf(res not in expected)
+ self.assertFalse(res not in expected)
def test_expand_actions_iter_7467(self):
# Check that expand_actions works with iterators (#7467)
@@ -146,6 +159,8 @@ class PermissionCacheTestCase(unittest.T
self.env = EnvironmentStub(enable=[perm.DefaultPermissionStore,
perm.DefaultPermissionPolicy,
TestPermissionRequestor])
+ self.env.config.set('trac', 'permission_policies',
+ 'DefaultPermissionPolicy')
self.perm_system = perm.PermissionSystem(self.env)
# by-pass DefaultPermissionPolicy cache:
perm.DefaultPermissionPolicy.CACHE_EXPIRY = -1
@@ -157,14 +172,14 @@ class PermissionCacheTestCase(unittest.T
self.env.reset_db()
def test_contains(self):
- self.assertEqual(True, 'TEST_MODIFY' in self.perm)
- self.assertEqual(True, 'TEST_ADMIN' in self.perm)
- self.assertEqual(False, 'TRAC_ADMIN' in self.perm)
+ self.assertTrue('TEST_MODIFY' in self.perm)
+ self.assertTrue('TEST_ADMIN' in self.perm)
+ self.assertFalse('TRAC_ADMIN' in self.perm)
def test_has_permission(self):
- self.assertEqual(True, self.perm.has_permission('TEST_MODIFY'))
- self.assertEqual(True, self.perm.has_permission('TEST_ADMIN'))
- self.assertEqual(False, self.perm.has_permission('TRAC_ADMIN'))
+ self.assertTrue(self.perm.has_permission('TEST_MODIFY'))
+ self.assertTrue(self.perm.has_permission('TEST_ADMIN'))
+ self.assertFalse(self.perm.has_permission('TRAC_ADMIN'))
def test_require(self):
self.perm.require('TEST_MODIFY')
@@ -259,13 +274,16 @@ class PermissionPolicyTestCase(unittest.
self.assertEqual(self.policy.results,
{('testuser', 'TEST_MODIFY'): True,
('testuser', 'TEST_ADMIN'): None})
+
+
def suite():
suite = unittest.TestSuite()
- suite.addTest(unittest.makeSuite(DefaultPermissionStoreTestCase, 'test'))
- suite.addTest(unittest.makeSuite(PermissionSystemTestCase, 'test'))
- suite.addTest(unittest.makeSuite(PermissionCacheTestCase, 'test'))
- suite.addTest(unittest.makeSuite(PermissionPolicyTestCase, 'test'))
+ suite.addTest(unittest.makeSuite(DefaultPermissionStoreTestCase))
+ suite.addTest(unittest.makeSuite(PermissionSystemTestCase))
+ suite.addTest(unittest.makeSuite(PermissionCacheTestCase))
+ suite.addTest(unittest.makeSuite(PermissionPolicyTestCase))
return suite
+
if __name__ == '__main__':
- unittest.main()
+ unittest.main(defaultTest='suite')
Modified: bloodhound/vendor/trac/current/trac/tests/resource.py
URL: http://svn.apache.org/viewvc/bloodhound/vendor/trac/current/trac/tests/resource.py?rev=1567849&r1=1567848&r2=1567849&view=diff
==============================================================================
--- bloodhound/vendor/trac/current/trac/tests/resource.py (original)
+++ bloodhound/vendor/trac/current/trac/tests/resource.py Thu Feb 13 05:08:02 2014
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
#
-# Copyright (C) 2007-2009 Edgewall Software
+# Copyright (C) 2007-2013 Edgewall Software
# All rights reserved.
#
# This software is licensed as described in the file COPYING, which
@@ -45,7 +45,7 @@ class ResourceTestCase(unittest.TestCase
def suite():
suite = unittest.TestSuite()
suite.addTest(doctest.DocTestSuite(resource))
- suite.addTest(unittest.makeSuite(ResourceTestCase, 'test'))
+ suite.addTest(unittest.makeSuite(ResourceTestCase))
return suite
if __name__ == '__main__':
Modified: bloodhound/vendor/trac/current/trac/tests/wikisyntax.py
URL: http://svn.apache.org/viewvc/bloodhound/vendor/trac/current/trac/tests/wikisyntax.py?rev=1567849&r1=1567848&r2=1567849&view=diff
==============================================================================
--- bloodhound/vendor/trac/current/trac/tests/wikisyntax.py (original)
+++ bloodhound/vendor/trac/current/trac/tests/wikisyntax.py Thu Feb 13 05:08:02 2014
@@ -1,4 +1,18 @@
-import os
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006-2013 Edgewall Software
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.org/wiki/TracLicense.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://trac.edgewall.org/log/.
+
+from __future__ import with_statement
+
import shutil
import tempfile
import unittest
@@ -116,12 +130,14 @@ attachment:file.txt?format=raw
def attachment_setup(tc):
import trac.ticket.api
import trac.wiki.api
- tc.env.path = os.path.join(tempfile.gettempdir(), 'trac-tempenv')
- os.mkdir(tc.env.path)
- attachment = Attachment(tc.env, 'wiki', 'WikiStart')
- attachment.insert('file.txt', tempfile.TemporaryFile(), 0)
+ tc.env.path = tempfile.mkdtemp(prefix='trac-tempenv-')
+ with tc.env.db_transaction as db:
+ db("INSERT INTO wiki (name,version) VALUES ('SomePage/SubPage',1)")
+ db("INSERT INTO ticket (id) VALUES (123)")
attachment = Attachment(tc.env, 'ticket', 123)
attachment.insert('file.txt', tempfile.TemporaryFile(), 0)
+ attachment = Attachment(tc.env, 'wiki', 'WikiStart')
+ attachment.insert('file.txt', tempfile.TemporaryFile(), 0)
attachment = Attachment(tc.env, 'wiki', 'SomePage/SubPage')
attachment.insert('foo.txt', tempfile.TemporaryFile(), 0)
@@ -188,4 +204,3 @@ def suite():
if __name__ == '__main__':
unittest.main(defaultTest='suite')
-
Modified: bloodhound/vendor/trac/current/trac/ticket/__init__.py
URL: http://svn.apache.org/viewvc/bloodhound/vendor/trac/current/trac/ticket/__init__.py?rev=1567849&r1=1567848&r2=1567849&view=diff
==============================================================================
--- bloodhound/vendor/trac/current/trac/ticket/__init__.py (original)
+++ bloodhound/vendor/trac/current/trac/ticket/__init__.py Thu Feb 13 05:08:02 2014
@@ -1,3 +1,16 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2005-2013 Edgewall Software
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.org/wiki/TracLicense.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://trac.edgewall.org/log/.
+
from trac.ticket.api import *
from trac.ticket.default_workflow import *
from trac.ticket.model import *
Modified: bloodhound/vendor/trac/current/trac/ticket/admin.py
URL: http://svn.apache.org/viewvc/bloodhound/vendor/trac/current/trac/ticket/admin.py?rev=1567849&r1=1567848&r2=1567849&view=diff
==============================================================================
--- bloodhound/vendor/trac/current/trac/ticket/admin.py (original)
+++ bloodhound/vendor/trac/current/trac/ticket/admin.py Thu Feb 13 05:08:02 2014
@@ -15,7 +15,9 @@ from __future__ import with_statement
from datetime import datetime
-from trac.admin import *
+from trac.admin.api import AdminCommandError, IAdminCommandProvider, \
+ IAdminPanelProvider, console_date_format, \
+ console_datetime_format, get_console_locale
from trac.core import *
from trac.perm import PermissionSystem
from trac.resource import ResourceNotFound
@@ -40,16 +42,14 @@ class TicketAdminPanel(Component):
# and don't use it whenever using them as field names (after
# a call to `.lower()`)
-
# IAdminPanelProvider methods
def get_admin_panels(self, req):
- if 'TICKET_ADMIN' in req.perm:
+ if 'TICKET_ADMIN' in req.perm('admin', 'ticket/' + self._type):
yield ('ticket', _('Ticket System'), self._type,
gettext(self._label[1]))
def render_admin_panel(self, req, cat, page, version):
- req.perm.require('TICKET_ADMIN')
# Trap AssertionErrors and convert them to TracErrors
try:
return self._render_admin_panel(req, cat, page, version)
@@ -147,7 +147,7 @@ class ComponentAdminPanel(TicketAdminPan
req.redirect(req.href.admin(cat, page))
data = {'view': 'list',
- 'components': model.Component.select(self.env),
+ 'components': list(model.Component.select(self.env)),
'default': default}
if self.config.getbool('ticket', 'restrict_owner'):
@@ -170,7 +170,7 @@ class ComponentAdminPanel(TicketAdminPan
yield ('component list', '',
'Show available components',
None, self._do_list)
- yield ('component add', '<name> <owner>',
+ yield ('component add', '<name> [owner]',
'Add a new component',
self._complete_add, self._do_add)
yield ('component rename', '<name> <newname>',
@@ -209,7 +209,7 @@ class ComponentAdminPanel(TicketAdminPan
for c in model.Component.select(self.env)],
[_('Name'), _('Owner')])
- def _do_add(self, name, owner):
+ def _do_add(self, name, owner=None):
component = model.Component(self.env)
component.name = name
component.owner = owner
@@ -237,21 +237,19 @@ class MilestoneAdminPanel(TicketAdminPan
# IAdminPanelProvider methods
def get_admin_panels(self, req):
- if 'MILESTONE_VIEW' in req.perm:
+ if 'MILESTONE_VIEW' in req.perm('admin', 'ticket/' + self._type):
return TicketAdminPanel.get_admin_panels(self, req)
- return iter([])
# TicketAdminPanel methods
def _render_admin_panel(self, req, cat, page, milestone):
- req.perm.require('MILESTONE_VIEW')
-
+ perm = req.perm('admin', 'ticket/' + self._type)
# Detail view?
if milestone:
mil = model.Milestone(self.env, milestone)
if req.method == 'POST':
if req.args.get('save'):
- req.perm.require('MILESTONE_MODIFY')
+ perm.require('MILESTONE_MODIFY')
mil.name = name = req.args.get('name')
mil.due = mil.completed = None
due = req.args.get('duedate', '')
@@ -268,7 +266,7 @@ class MilestoneAdminPanel(TicketAdminPan
_('Invalid Completion Date'))
mil.description = req.args.get('description', '')
try:
- mil.update()
+ mil.update(author=req.authname)
except self.env.db_exc.IntegrityError:
raise TracError(_('The milestone "%(name)s" already '
'exists.', name=name))
@@ -285,7 +283,7 @@ class MilestoneAdminPanel(TicketAdminPan
if req.method == 'POST':
# Add Milestone
if req.args.get('add') and req.args.get('name'):
- req.perm.require('MILESTONE_CREATE')
+ perm.require('MILESTONE_CREATE')
name = req.args.get('name')
try:
mil = model.Milestone(self.env, name=name)
@@ -308,7 +306,7 @@ class MilestoneAdminPanel(TicketAdminPan
# Remove milestone
elif req.args.get('remove'):
- req.perm.require('MILESTONE_DELETE')
+ perm.require('MILESTONE_DELETE')
sel = req.args.get('sel')
if not sel:
raise TracError(_('No milestone selected'))
@@ -352,6 +350,10 @@ class MilestoneAdminPanel(TicketAdminPan
# IAdminCommandProvider methods
def get_admin_commands(self):
+ hints = {
+ 'datetime': get_datetime_format_hint(get_console_locale(self.env)),
+ 'iso8601': get_datetime_format_hint('iso8601'),
+ }
yield ('milestone list', '',
"Show milestones",
None, self._do_list)
@@ -364,20 +366,22 @@ class MilestoneAdminPanel(TicketAdminPan
yield ('milestone due', '<name> <due>',
"""Set milestone due date
- The <due> date must be specified in the "%s" format.
+ The <due> date must be specified in the "%(datetime)s"
+ or "%(iso8601)s" (ISO 8601) format.
Alternatively, "now" can be used to set the due date to the
current time. To remove the due date from a milestone, specify
an empty string ("").
- """ % console_date_format_hint,
+ """ % hints,
self._complete_name, self._do_due)
yield ('milestone completed', '<name> <completed>',
"""Set milestone complete date
- The <completed> date must be specified in the "%s" format.
+ The <completed> date must be specified in the "%(datetime)s"
+ or "%(iso8601)s" (ISO 8601) format.
Alternatively, "now" can be used to set the completion date to
the current time. To remove the completion date from a
milestone, specify an empty string ("").
- """ % console_date_format_hint,
+ """ % hints,
self._complete_name, self._do_completed)
yield ('milestone remove', '<name>',
"Remove milestone",
@@ -402,23 +406,26 @@ class MilestoneAdminPanel(TicketAdminPan
milestone = model.Milestone(self.env)
milestone.name = name
if due is not None:
- milestone.due = parse_date(due, hint='datetime')
+ milestone.due = parse_date(due, hint='datetime',
+ locale=get_console_locale(self.env))
milestone.insert()
def _do_rename(self, name, newname):
milestone = model.Milestone(self.env, name)
milestone.name = newname
- milestone.update()
+ milestone.update(author=getuser())
def _do_due(self, name, due):
milestone = model.Milestone(self.env, name)
- milestone.due = due and parse_date(due, hint='datetime')
+ milestone.due = due and parse_date(due, hint='datetime',
+ locale=get_console_locale(self.env))
milestone.update()
def _do_completed(self, name, completed):
milestone = model.Milestone(self.env, name)
- milestone.completed = completed and parse_date(completed,
- hint='datetime')
+ milestone.completed = completed and \
+ parse_date(completed, hint='datetime',
+ locale=get_console_locale(self.env))
milestone.update()
def _do_remove(self, name):
@@ -510,7 +517,7 @@ class VersionAdminPanel(TicketAdminPanel
req.redirect(req.href.admin(cat, page))
data = {'view': 'list',
- 'versions': model.Version.select(self.env),
+ 'versions': list(model.Version.select(self.env)),
'default': default}
Chrome(self.env).add_jquery_ui(req)
@@ -523,6 +530,10 @@ class VersionAdminPanel(TicketAdminPanel
# IAdminCommandProvider methods
def get_admin_commands(self):
+ hints = {
+ 'datetime': get_datetime_format_hint(get_console_locale(self.env)),
+ 'iso8601': get_datetime_format_hint('iso8601'),
+ }
yield ('version list', '',
"Show versions",
None, self._do_list)
@@ -535,11 +546,12 @@ class VersionAdminPanel(TicketAdminPanel
yield ('version time', '<name> <time>',
"""Set version date
- The <time> must be specified in the "%s" format. Alternatively,
- "now" can be used to set the version date to the current time.
- To remove the date from a version, specify an empty string
- ("").
- """ % console_date_format_hint,
+ The <time> must be specified in the "%(datetime)s"
+ or "%(iso8601)s" (ISO 8601) format.
+ Alternatively, "now" can be used to set the version date to
+ the current time. To remove the date from a version, specify
+ an empty string ("").
+ """ % hints,
self._complete_name, self._do_time)
yield ('version remove', '<name>',
"Remove version",
@@ -562,7 +574,9 @@ class VersionAdminPanel(TicketAdminPanel
version = model.Version(self.env)
version.name = name
if time is not None:
- version.time = time and parse_date(time, hint='datetime')
+ version.time = time and \
+ parse_date(time, hint='datetime',
+ locale=get_console_locale(self.env))
version.insert()
def _do_rename(self, name, newname):
@@ -572,7 +586,9 @@ class VersionAdminPanel(TicketAdminPanel
def _do_time(self, name, time):
version = model.Version(self.env, name)
- version.time = time and parse_date(time, hint='datetime')
+ version.time = time and \
+ parse_date(time, hint='datetime',
+ locale=get_console_locale(self.env))
version.update()
def _do_remove(self, name):
Modified: bloodhound/vendor/trac/current/trac/ticket/api.py
URL: http://svn.apache.org/viewvc/bloodhound/vendor/trac/current/trac/ticket/api.py?rev=1567849&r1=1567848&r2=1567849&view=diff
==============================================================================
--- bloodhound/vendor/trac/current/trac/ticket/api.py (original)
+++ bloodhound/vendor/trac/current/trac/ticket/api.py Thu Feb 13 05:08:02 2014
@@ -491,24 +491,44 @@ class TicketSystem(Component):
cnum, realm, id = elts
if cnum != 'description' and cnum and not cnum[0].isdigit():
realm, id, cnum = elts # support old comment: style
+ id = as_int(id, None)
resource = formatter.resource(realm, id)
else:
resource = formatter.resource
cnum = target
- if resource and resource.realm == 'ticket':
- id = as_int(resource.id, None)
- if id is not None:
- href = "%s#comment:%s" % (formatter.href.ticket(resource.id),
- cnum)
- title = _("Comment %(cnum)s for Ticket #%(id)s", cnum=cnum,
- id=resource.id)
- if 'TICKET_VIEW' in formatter.perm(resource):
- for status, in self.env.db_query(
- "SELECT status FROM ticket WHERE id=%s", (id,)):
- return tag.a(label, href=href, title=title,
- class_=status)
- return tag.a(label, href=href, title=title)
+ if resource and resource.id and resource.realm == 'ticket' and \
+ cnum and (all(c.isdigit() for c in cnum) or cnum == 'description'):
+ href = title = class_ = None
+ if self.resource_exists(resource):
+ from trac.ticket.model import Ticket
+ ticket = Ticket(self.env, resource.id)
+ if cnum != 'description' and not ticket.get_change(cnum):
+ title = _("ticket comment does not exist")
+ class_ = 'missing ticket'
+ elif 'TICKET_VIEW' in formatter.perm(resource):
+ href = formatter.href.ticket(resource.id) + \
+ "#comment:%s" % cnum
+ if resource.id != formatter.resource.id:
+ if cnum == 'description':
+ title = _("Description for Ticket #%(id)s",
+ id=resource.id)
+ else:
+ title = _("Comment %(cnum)s for Ticket #%(id)s",
+ cnum=cnum, id=resource.id)
+ class_ = ticket['status'] + ' ticket'
+ else:
+ title = _("Description") if cnum == 'description' \
+ else _("Comment %(cnum)s",
+ cnum=cnum)
+ class_ = 'ticket'
+ else:
+ title = _("no permission to view ticket")
+ class_ = 'forbidden ticket'
+ else:
+ title = _("ticket does not exist")
+ class_ = 'missing ticket'
+ return tag.a(label, class_=class_, href=href, title=title)
return label
# IResourceManager methods
Modified: bloodhound/vendor/trac/current/trac/ticket/default_workflow.py
URL: http://svn.apache.org/viewvc/bloodhound/vendor/trac/current/trac/ticket/default_workflow.py?rev=1567849&r1=1567848&r2=1567849&view=diff
==============================================================================
--- bloodhound/vendor/trac/current/trac/ticket/default_workflow.py (original)
+++ bloodhound/vendor/trac/current/trac/ticket/default_workflow.py Thu Feb 13 05:08:02 2014
@@ -226,7 +226,8 @@ Read TracWorkflow for more information (
this_action = self.actions[action]
status = this_action['newstate']
operations = this_action['operations']
- current_owner = ticket._old.get('owner', ticket['owner'] or '(none)')
+ current_owner_or_empty = ticket._old.get('owner', ticket['owner'])
+ current_owner = current_owner_or_empty or '(none)'
if not (Chrome(self.env).show_email_addresses
or 'EMAIL_VIEW' in req.perm(ticket.resource)):
format_user = obfuscate_email_address
@@ -245,7 +246,7 @@ Read TracWorkflow for more information (
id = 'action_%s_reassign_owner' % action
selected_owner = req.args.get(id, req.authname)
- if this_action.has_key('set_owner'):
+ if 'set_owner' in this_action:
owners = [x.strip() for x in
this_action['set_owner'].split(',')]
elif self.config.getbool('ticket', 'restrict_owner'):
@@ -261,7 +262,7 @@ Read TracWorkflow for more information (
owner=tag.input(type='text', id=id,
name=id, value=owner)))
hints.append(_("The owner will be changed from "
- "%(current_owner)s",
+ "%(current_owner)s to the specified user",
current_owner=current_owner))
elif len(owners) == 1:
owner = tag.input(type='hidden', id=id, name=id,
@@ -289,7 +290,7 @@ Read TracWorkflow for more information (
"to %(authname)s", current_owner=current_owner,
authname=req.authname))
if 'set_resolution' in operations:
- if this_action.has_key('set_resolution'):
+ if 'set_resolution' in this_action:
resolutions = [x.strip() for x in
this_action['set_resolution'].split(',')]
else:
@@ -323,10 +324,16 @@ Read TracWorkflow for more information (
control.append(_('as %(status)s ',
status= ticket._old.get('status',
ticket['status'])))
+ if len(operations) == 1:
+ hints.append(_("The owner will remain %(current_owner)s",
+ current_owner=current_owner)
+ if current_owner_or_empty else
+ _("The ticket will remain with no owner"))
else:
if status != '*':
hints.append(_("Next status will be '%(name)s'", name=status))
- return (this_action['name'], tag(*control), '. '.join(hints) + ".")
+ return (this_action['name'], tag(*control), '. '.join(hints) + '.'
+ if hints else '')
def get_ticket_changes(self, req, ticket, action):
this_action = self.actions[action]
Modified: bloodhound/vendor/trac/current/trac/ticket/model.py
URL: http://svn.apache.org/viewvc/bloodhound/vendor/trac/current/trac/ticket/model.py?rev=1567849&r1=1567848&r2=1567849&view=diff
==============================================================================
--- bloodhound/vendor/trac/current/trac/ticket/model.py (original)
+++ bloodhound/vendor/trac/current/trac/ticket/model.py Thu Feb 13 05:08:02 2014
@@ -1003,23 +1003,20 @@ class Milestone(object):
def delete(self, retarget_to=None, author=None, db=None):
"""Delete the milestone.
+ :param author: the author of the change
+
+ :since 1.0.2: the `retarget_to` parameter is deprecated and tickets
+ should moved to another milestone by calling `move_tickets` before
+ `delete`.
+
:since 1.0: the `db` parameter is no longer needed and will be removed
in version 1.1.1
"""
with self.env.db_transaction as db:
self.env.log.info("Deleting milestone %s", self.name)
db("DELETE FROM milestone WHERE name=%s", (self.name,))
-
- # Retarget/reset tickets associated with this milestone
- now = datetime.now(utc)
- tkt_ids = [int(row[0]) for row in
- db("SELECT id FROM ticket WHERE milestone=%s",
- (self.name,))]
- for tkt_id in tkt_ids:
- ticket = Ticket(self.env, tkt_id, db)
- ticket['milestone'] = retarget_to
- comment = "Milestone %s deleted" % self.name # don't translate
- ticket.save_changes(author, comment, now)
+ # Don't translate ticket comment (comment:40:ticket:5658)
+ self.move_tickets(retarget_to, author, "Milestone deleted")
self._old['name'] = None
del self.cache.milestones
TicketSystem(self.env).reset_ticket_fields()
@@ -1049,7 +1046,7 @@ class Milestone(object):
for listener in TicketSystem(self.env).milestone_change_listeners:
listener.milestone_created(self)
- def update(self, db=None):
+ def update(self, db=None, author=None):
"""Update the milestone.
:since 1.0: the `db` parameter is no longer needed and will be removed
@@ -1061,33 +1058,65 @@ class Milestone(object):
old = self._old.copy()
with self.env.db_transaction as db:
- old_name = old['name']
- self.env.log.info("Updating milestone '%s'", self.name)
+ if self.name != old['name']:
+ # Update milestone field in tickets
+ self.move_tickets(self.name, author, "Milestone renamed")
+ TicketSystem(self.env).reset_ticket_fields()
+ # Reparent attachments
+ Attachment.reparent_all(self.env, 'milestone', old['name'],
+ 'milestone', self.name)
+
+ self.env.log.info("Updating milestone '%s'", old['name'])
db("""UPDATE milestone
SET name=%s, due=%s, completed=%s, description=%s
WHERE name=%s
""", (self.name, to_utimestamp(self.due),
to_utimestamp(self.completed),
- self.description, old_name))
+ self.description, old['name']))
self.checkin()
- if self.name != old_name:
- # Update milestone field in tickets
- self.env.log.info("Updating milestone field of all tickets "
- "associated with milestone '%s'", self.name)
- db("UPDATE ticket SET milestone=%s WHERE milestone=%s",
- (self.name, old_name))
- TicketSystem(self.env).reset_ticket_fields()
-
- # Reparent attachments
- Attachment.reparent_all(self.env, 'milestone', old_name,
- 'milestone', self.name)
-
old_values = dict((k, v) for k, v in old.iteritems()
if getattr(self, k) != v)
for listener in TicketSystem(self.env).milestone_change_listeners:
listener.milestone_changed(self, old_values)
+ def move_tickets(self, new_milestone, author, comment=None,
+ exclude_closed=False):
+ """Move tickets associated with this milestone to another
+ milestone.
+
+ :param new_milestone: milestone to which the tickets are moved
+ :param author: author of the change
+ :param comment: comment that is inserted into moved tickets. The
+ string should not be translated.
+ :param exclude_closed: whether tickets with status closed should be
+ excluded
+
+ :return: a list of ids of tickets that were moved
+ """
+ # Check if milestone exists, but if the milestone is being renamed
+ # the new milestone won't exist in the cache yet so skip the test
+ if new_milestone and new_milestone != self.name:
+ if not self.cache.fetchone(new_milestone):
+ raise ResourceNotFound(
+ _("Milestone %(name)s does not exist.",
+ name=new_milestone), _("Invalid milestone name"))
+ now = datetime.now(utc)
+ with self.env.db_transaction as db:
+ sql = "SELECT id FROM ticket WHERE milestone=%s"
+ if exclude_closed:
+ sql += " AND status != 'closed'"
+ tkt_ids = [int(row[0]) for row in db(sql, (self._old['name'],))]
+ if tkt_ids:
+ self.env.log.info("Moving tickets associated with milestone "
+ "'%s' to milestone '%s'", self._old['name'],
+ new_milestone)
+ for tkt_id in tkt_ids:
+ ticket = Ticket(self.env, tkt_id)
+ ticket['milestone'] = new_milestone
+ ticket.save_changes(author, comment, now)
+ return tkt_ids
+
@classmethod
def select(cls, env, include_completed=True, db=None):
"""
Modified: bloodhound/vendor/trac/current/trac/ticket/notification.py
URL: http://svn.apache.org/viewvc/bloodhound/vendor/trac/current/trac/ticket/notification.py?rev=1567849&r1=1567848&r2=1567849&view=diff
==============================================================================
--- bloodhound/vendor/trac/current/trac/ticket/notification.py (original)
+++ bloodhound/vendor/trac/current/trac/ticket/notification.py Thu Feb 13 05:08:02 2014
@@ -27,7 +27,8 @@ from trac.config import *
from trac.notification import NotifyEmail
from trac.ticket.api import TicketSystem
from trac.util.datefmt import to_utimestamp
-from trac.util.text import obfuscate_email_address, text_width, wrap
+from trac.util.text import obfuscate_email_address, shorten_line, \
+ text_width, wrap
from trac.util.translation import deactivate, reactivate
@@ -57,7 +58,7 @@ class TicketNotificationSystem(Component
''(since 0.11)''""")
batch_subject_template = Option('notification', 'batch_subject_template',
- '$prefix Batch modify: $tickets_descr',
+ '$prefix Batch modify: $tickets_descr',
"""Like ticket_subject_template but for batch modifications.
By default, the template is `$prefix Batch modify: $tickets_descr`.
@@ -73,10 +74,16 @@ class TicketNotificationSystem(Component
US-ASCII characters. This is expected by CJK users. ''(since
0.12.2)''""")
+
def get_ticket_notification_recipients(env, config, tktid, prev_cc):
- notify_reporter = config.getbool('notification', 'always_notify_reporter')
- notify_owner = config.getbool('notification', 'always_notify_owner')
- notify_updater = config.getbool('notification', 'always_notify_updater')
+ """Returns the notifications recipients.
+
+ :since 1.0.3: the `config` parameter is no longer used.
+ """
+ section = env.config['notification']
+ notify_reporter = section.getbool('always_notify_reporter')
+ notify_owner = section.getbool('always_notify_owner')
+ notify_updater = section.getbool('always_notify_updater')
ccrecipients = prev_cc
torecipients = []
@@ -306,13 +313,13 @@ class TicketNotifyEmail(NotifyEmail):
width_l = self.COLS - width_r - 1
sep = width_l * '-' + '+' + width_r * '-'
txt = sep + '\n'
- cell_tmp = [u'', u'']
+ vals_lr = ([], [])
big = []
i = 0
width_lr = [width_l, width_r]
for f in [f for f in fields if f['name'] != 'description']:
fname = f['name']
- if not tkt.values.has_key(fname):
+ if fname not in tkt.values:
continue
fval = tkt[fname] or ''
if fname in ['owner', 'reporter']:
@@ -324,15 +331,36 @@ class TicketNotifyEmail(NotifyEmail):
# __str__ method won't be called.
str_tmp = u'%s: %s' % (f['label'], unicode(fval))
idx = i % 2
- cell_tmp[idx] += wrap(str_tmp, width_lr[idx] - 2 + 2 * idx,
- (width[2 * idx]
- - self.get_text_width(f['label'])
- + 2 * idx) * ' ',
- 2 * ' ', '\n', self.ambiwidth)
- cell_tmp[idx] += '\n'
+ initial_indent = ' ' * (width[2 * idx] -
+ self.get_text_width(f['label']) +
+ 2 * idx)
+ wrapped = wrap(str_tmp, width_lr[idx] - 2 + 2 * idx,
+ initial_indent, ' ', '\n', self.ambiwidth)
+ vals_lr[idx].append(wrapped.splitlines())
i += 1
- cell_l = cell_tmp[0].splitlines()
- cell_r = cell_tmp[1].splitlines()
+ if len(vals_lr[0]) > len(vals_lr[1]):
+ vals_lr[1].append([])
+
+ cell_l = []
+ cell_r = []
+ for i in xrange(len(vals_lr[0])):
+ vals_l = vals_lr[0][i]
+ vals_r = vals_lr[1][i]
+ vals_diff = len(vals_l) - len(vals_r)
+ diff = len(cell_l) - len(cell_r)
+ if diff > 0:
+ # add padding to right side if needed
+ if vals_diff < 0:
+ diff += vals_diff
+ cell_r.extend([''] * max(diff, 0))
+ elif diff < 0:
+ # add padding to left side if needed
+ if vals_diff > 0:
+ diff += vals_diff
+ cell_l.extend([''] * max(-diff, 0))
+ cell_l.extend(vals_l)
+ cell_r.extend(vals_r)
+
for i in range(max(len(cell_l), len(cell_r))):
if i >= len(cell_l):
cell_l.append(width_l * ' ')
@@ -385,7 +413,7 @@ class TicketNotifyEmail(NotifyEmail):
def get_recipients(self, tktid):
(torecipients, ccrecipients, reporter, owner) = \
get_ticket_notification_recipients(self.env, self.config,
- tktid, self.prev_cc)
+ tktid, self.prev_cc)
self.reporter = reporter
self.owner = owner
return (torecipients, ccrecipients)
@@ -425,6 +453,7 @@ class TicketNotifyEmail(NotifyEmail):
else:
return obfuscate_email_address(text)
+
class BatchTicketNotifyEmail(NotifyEmail):
"""Notification of ticket batch modifications."""
@@ -443,7 +472,6 @@ class BatchTicketNotifyEmail(NotifyEmail
def _notify(self, tickets, new_values, comment, action, author):
self.tickets = tickets
- changes_body = ''
self.reporter = ''
self.owner = ''
changes_descr = '\n'.join(['%s to %s' % (prop, val)
@@ -475,8 +503,8 @@ class BatchTicketNotifyEmail(NotifyEmail
'tickets_descr': tickets_descr,
'env': self.env,
}
-
- return template.generate(**data).render('text', encoding=None).strip()
+ subj = template.generate(**data).render('text', encoding=None).strip()
+ return shorten_line(subj)
def get_recipients(self, tktids):
alltorecipients = []
@@ -484,7 +512,7 @@ class BatchTicketNotifyEmail(NotifyEmail
for t in tktids:
(torecipients, ccrecipients, reporter, owner) = \
get_ticket_notification_recipients(self.env, self.config,
- t, [])
+ t, [])
alltorecipients.extend(torecipients)
allccrecipients.extend(ccrecipients)
return (list(set(alltorecipients)), list(set(allccrecipients)))