launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #01183
[Merge] lp:~lifeless/launchpad/cp into lp:~launchpad-pqm/launchpad/production-devel
Robert Collins has proposed merging lp:~lifeless/launchpad/cp into lp:~launchpad-pqm/launchpad/production-devel.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
More fixes from stable.
--
https://code.launchpad.net/~lifeless/launchpad/cp/+merge/36406
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~lifeless/launchpad/cp into lp:~launchpad-pqm/launchpad/production-devel.
=== modified file '.bzrignore'
--- .bzrignore 2010-09-05 20:29:40 +0000
+++ .bzrignore 2010-09-23 02:21:04 +0000
@@ -78,3 +78,5 @@
lib/canonical/buildd/debian/*
lib/canonical/buildd/launchpad-files/*
*.pt.py
+.project
+.pydevproject
=== modified file 'lib/canonical/launchpad/browser/launchpad.py'
--- lib/canonical/launchpad/browser/launchpad.py 2010-08-24 10:45:57 +0000
+++ lib/canonical/launchpad/browser/launchpad.py 2010-09-23 02:21:04 +0000
@@ -514,19 +514,39 @@
'foo' can be the unique name of the branch, or any of the aliases for
the branch.
+ If 'foo' resolves to an ICanHasLinkedBranch instance but the linked
+ branch is not yet set, redirect back to the referring page with a
+ suitable notification message.
+ If 'foo' is completely invalid, redirect back to the referring page
+ with a suitable error message.
"""
+
+ # The default url to go to will be back to the referring page (in
+ # the case that there is an error resolving the branch url)
+ url = self.request.getHeader('referer')
path = '/'.join(self.request.stepstogo)
try:
- branch_data = getUtility(IBranchLookup).getByLPPath(path)
- except (CannotHaveLinkedBranch, NoLinkedBranch, InvalidNamespace,
- InvalidProductName):
- raise NotFoundError
- branch, trailing = branch_data
- if branch is None:
- raise NotFoundError
- url = canonical_url(branch)
- if trailing is not None:
- url = urlappend(url, trailing)
+ # first check for a valid branch url
+ try:
+ branch_data = getUtility(IBranchLookup).getByLPPath(path)
+ branch, trailing = branch_data
+ url = canonical_url(branch)
+ if trailing is not None:
+ url = urlappend(url, trailing)
+
+ except (NoLinkedBranch):
+ # a valid ICanHasLinkedBranch target exists but there's no
+ # branch or it's not visible
+ self.request.response.addNotification(
+ "The target %s does not have a linked branch." % path)
+
+ except (CannotHaveLinkedBranch, InvalidNamespace,
+ InvalidProductName, NotFoundError) as e:
+ error_msg = str(e)
+ if error_msg == '':
+ error_msg = "Invalid branch lp:%s." % path
+ self.request.response.addErrorNotification(error_msg)
+
return self.redirectSubTree(url)
@stepto('+builds')
=== modified file 'lib/canonical/launchpad/browser/tests/test_launchpad.py'
--- lib/canonical/launchpad/browser/tests/test_launchpad.py 2010-08-20 20:31:18 +0000
+++ lib/canonical/launchpad/browser/tests/test_launchpad.py 2010-09-23 02:21:04 +0000
@@ -1,7 +1,7 @@
# Copyright 2009 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
-"""Tests for traversal from the root branch object.."""
+"""Tests for traversal from the root branch object."""
__metaclass__ = type
@@ -13,17 +13,21 @@
from canonical.launchpad.browser.launchpad import LaunchpadRootNavigation
from canonical.launchpad.interfaces.account import AccountStatus
+from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
from canonical.launchpad.webapp import canonical_url
+from canonical.launchpad.webapp.interfaces import BrowserNotificationLevel
from canonical.launchpad.webapp.servers import LaunchpadTestRequest
from canonical.launchpad.webapp.url import urlappend
from canonical.testing.layers import DatabaseFunctionalLayer
from lp.app.errors import GoneError
+from lp.code.interfaces.linkedbranch import ICanHasLinkedBranch
from lp.registry.interfaces.person import (
IPersonSet,
PersonVisibility,
)
from lp.testing import (
login_person,
+ person_logged_in,
TestCaseWithFactory,
)
from lp.testing.views import create_view
@@ -31,6 +35,48 @@
class TraversalMixin:
+ def _validateNotificationContext(
+ self, request, notification=None,
+ level=BrowserNotificationLevel.INFO):
+ """Check the browser notifications associated with the request.
+
+ Ensure that the notification instances attached to the request match
+ the expected values for text and type.
+
+ :param notification: The exact notification text to validate. If None
+ then we don't care what the notification text is, so long as there
+ is some.
+ : param level: the required notification level
+ """
+
+ notifications = request.notifications
+ if notification is None:
+ self.assertEquals(len(notifications), 0)
+ return
+ self.assertEqual(len(notifications), 1)
+ self.assertEquals(notifications[0].level, level)
+ self.assertEqual(notification, notifications[0].message)
+
+ def assertDisplaysNotification(
+ self, path, notification=None,
+ level=BrowserNotificationLevel.INFO):
+ """Assert that an invalid path redirects back to referrer.
+
+ The request object is expected to have a notification message to
+ display to the user to explain the reason for the error.
+
+ :param path: The path to check
+ :param notification: The exact notification text to validate. If None
+ then we don't care what the notification text is, so long as there
+ is some.
+ : param level: the required notification level
+ """
+
+ redirection = self.traverse(path)
+ self.assertIs(redirection.target, None)
+ self._validateNotificationContext(
+ redirection.request, notification, level)
+
def assertNotFound(self, path):
self.assertRaises(NotFound, self.traverse, path)
@@ -58,8 +104,8 @@
class TestBranchTraversal(TestCaseWithFactory, TraversalMixin):
"""Branches are traversed to from IPersons. Test we can reach them.
- This class tests the `PersonNavigation` class to see that we can traverse
- to branches from such objects.
+ This class tests the `LaunchpadRootNavigation` class to see that we can
+ traverse to branches from URLs of the form +branch/xxxx.
"""
layer = DatabaseFunctionalLayer
@@ -75,26 +121,107 @@
def test_no_such_unique_name(self):
# Traversing to /+branch/<unique_name> where 'unique_name' is for a
- # branch that doesn't exist will generate a 404.
+ # branch that doesn't exist will display an error message.
branch = self.factory.makeAnyBranch()
- self.assertNotFound(branch.unique_name + 'wibble')
+ bad_name = branch.unique_name + 'wibble'
+ requiredMessage = "No such branch: '%s'." % (branch.name+"wibble")
+ self.assertDisplaysNotification(
+ bad_name, requiredMessage,
+ BrowserNotificationLevel.ERROR)
+
+ def test_private_branch(self):
+ # If an attempt is made to access a private branch, display an error.
+ branch = self.factory.makeProductBranch()
+ branch_unique_name = branch.unique_name
+ naked_product = removeSecurityProxy(branch.product)
+ ICanHasLinkedBranch(naked_product).setBranch(branch)
+ removeSecurityProxy(branch).private = True
+
+ any_user = self.factory.makePerson()
+ login_person(any_user)
+ requiredMessage = "No such branch: '%s'." % branch_unique_name
+ self.assertDisplaysNotification(
+ branch_unique_name,
+ requiredMessage,
+ BrowserNotificationLevel.ERROR)
def test_product_alias(self):
# Traversing to /+branch/<product> redirects to the page for the
# branch that is the development focus branch for that product.
branch = self.factory.makeProductBranch()
- product = removeSecurityProxy(branch.product)
- product.development_focus.branch = branch
- self.assertRedirects(product.name, canonical_url(branch))
+ naked_product = removeSecurityProxy(branch.product)
+ ICanHasLinkedBranch(naked_product).setBranch(branch)
+ self.assertRedirects(naked_product.name, canonical_url(branch))
+
+ def test_private_branch_for_product(self):
+ # If the development focus of a product is private, display a
+ # message telling the user there is no linked branch.
+ branch = self.factory.makeProductBranch()
+ naked_product = removeSecurityProxy(branch.product)
+ ICanHasLinkedBranch(naked_product).setBranch(branch)
+ removeSecurityProxy(branch).private = True
+
+ any_user = self.factory.makePerson()
+ login_person(any_user)
+ requiredMessage = (u"The target %s does not have a linked branch."
+ % naked_product.name)
+ self.assertDisplaysNotification(
+ naked_product.name,
+ requiredMessage,
+ BrowserNotificationLevel.NOTICE)
def test_nonexistent_product(self):
- # Traversing to /+branch/<no-such-product> generates a 404.
- self.assertNotFound('non-existent')
+ # Traversing to /+branch/<no-such-product> displays an error message.
+ non_existent = 'non-existent'
+ requiredMessage = u"No such product: '%s'." % non_existent
+ self.assertDisplaysNotification(
+ non_existent, requiredMessage,
+ BrowserNotificationLevel.ERROR)
def test_product_without_dev_focus(self):
- # Traversing to a product without a development focus generates a 404.
+ # Traversing to a product without a development focus displays a
+ # user message on the same page.
product = self.factory.makeProduct()
- self.assertNotFound(product.name)
+ requiredMessage = (u"The target %s does not have a linked branch."
+ % product.name)
+ self.assertDisplaysNotification(
+ product.name,
+ requiredMessage,
+ BrowserNotificationLevel.NOTICE)
+
+ def test_distro_package_alias(self):
+ # Traversing to /+branch/<distro>/<sourcepackage package> redirects
+ # to the page for the branch that is the development focus branch
+ # for that package.
+ sourcepackage = self.factory.makeSourcePackage()
+ branch = self.factory.makePackageBranch(sourcepackage=sourcepackage)
+ distro_package = sourcepackage.distribution_sourcepackage
+ ubuntu_branches = getUtility(ILaunchpadCelebrities).ubuntu_branches
+ registrant = ubuntu_branches.teamowner
+ target = ICanHasLinkedBranch(distro_package)
+ with person_logged_in(registrant):
+ target.setBranch(branch, registrant)
+ self.assertRedirects("%s" % target.bzr_path, canonical_url(branch))
+
+ def test_private_branch_for_distro_package(self):
+ # If the development focus of a distro package is private, display a
+ # message telling the user there is no linked branch.
+ sourcepackage = self.factory.makeSourcePackage()
+ branch = self.factory.makePackageBranch(
+ sourcepackage=sourcepackage, private=True)
+ distro_package = sourcepackage.distribution_sourcepackage
+ ubuntu_branches = getUtility(ILaunchpadCelebrities).ubuntu_branches
+ registrant = ubuntu_branches.teamowner
+ with person_logged_in(registrant):
+ ICanHasLinkedBranch(distro_package).setBranch(branch, registrant)
+
+ any_user = self.factory.makePerson()
+ login_person(any_user)
+ path = ICanHasLinkedBranch(distro_package).bzr_path
+ requiredMessage = (u"The target %s does not have a linked branch."
+ % path)
+ self.assertDisplaysNotification(
+ path, requiredMessage, BrowserNotificationLevel.NOTICE)
def test_trailing_path_redirect(self):
# If there are any trailing path segments after the branch identifier,
@@ -106,33 +233,63 @@
def test_product_series_redirect(self):
# Traversing to /+branch/<product>/<series> redirects to the branch
# for that series, if there is one.
- branch = self.factory.makeProductBranch()
- product = branch.product
- series = self.factory.makeProductSeries(product=product)
- removeSecurityProxy(series).branch = branch
+ branch = self.factory.makeBranch()
+ series = self.factory.makeProductSeries(branch=branch)
self.assertRedirects(
- '%s/%s' % (product.name, series.name), canonical_url(branch))
+ ICanHasLinkedBranch(series).bzr_path, canonical_url(branch))
def test_nonexistent_product_series(self):
- # /+branch/<product>/<series> generates a 404 if there is no such
- # series.
+ # /+branch/<product>/<series> displays an error message if there is
+ # no such series.
product = self.factory.makeProduct()
- self.assertNotFound('%s/nonexistent' % product.name)
+ non_existent = 'nonexistent'
+ requiredMessage = u"No such product series: '%s'." % non_existent
+ self.assertDisplaysNotification(
+ '%s/%s' % (product.name, non_existent),
+ requiredMessage,
+ BrowserNotificationLevel.ERROR)
def test_no_branch_for_series(self):
- # If there's no branch for a product series, generate a 404.
+ # If there's no branch for a product series, display a
+ # message telling the user there is no linked branch.
series = self.factory.makeProductSeries()
- self.assertNotFound('%s/%s' % (series.product.name, series.name))
+ path = ICanHasLinkedBranch(series).bzr_path
+ requiredMessage = ("The target %s does not have a linked branch."
+ % path)
+ self.assertDisplaysNotification(
+ path, requiredMessage, BrowserNotificationLevel.NOTICE)
+
+ def test_private_branch_for_series(self):
+ # If the development focus of a product series is private, display a
+ # message telling the user there is no linked branch.
+ branch = self.factory.makeBranch(private=True)
+ series = self.factory.makeProductSeries(branch=branch)
+
+ any_user = self.factory.makePerson()
+ login_person(any_user)
+ path = ICanHasLinkedBranch(series).bzr_path
+ requiredMessage = (u"The target %s does not have a linked branch."
+ % path)
+ self.assertDisplaysNotification(
+ path, requiredMessage, BrowserNotificationLevel.NOTICE)
def test_too_short_branch_name(self):
- # 404 if the thing following +branch is a unique name that's too short
- # to be a real unique name.
+ # error notification if the thing following +branch is a unique name
+ # that's too short to be a real unique name.
owner = self.factory.makePerson()
- self.assertNotFound('~%s' % owner.name)
+ requiredMessage = (u"Cannot understand namespace name: '%s'"
+ % owner.name)
+ self.assertDisplaysNotification(
+ '~%s' % owner.name,
+ requiredMessage,
+ BrowserNotificationLevel.ERROR)
def test_invalid_product_name(self):
- # 404 if the thing following +branch has an invalid product name.
- self.assertNotFound('a')
+ # error notification if the thing following +branch has an invalid
+ # product name.
+ self.assertDisplaysNotification(
+ 'a', u"Invalid name for product: a.",
+ BrowserNotificationLevel.ERROR)
class TestPersonTraversal(TestCaseWithFactory, TraversalMixin):
=== modified file 'lib/canonical/launchpad/database/message.py'
--- lib/canonical/launchpad/database/message.py 2010-08-29 21:24:47 +0000
+++ lib/canonical/launchpad/database/message.py 2010-09-23 02:21:04 +0000
@@ -82,6 +82,7 @@
from lp.app.errors import NotFoundError
from lp.registry.interfaces.person import validate_public_person
from lp.services.job.model.job import Job
+from lp.services.propertycache import cachedproperty
# this is a hard limit on the size of email we will be willing to store in
# the database.
@@ -158,10 +159,14 @@
"""See IMessage."""
return self.owner
- @property
+ @cachedproperty
def text_contents(self):
"""See IMessage."""
- bits = [unicode(chunk) for chunk in self if chunk.content]
+ return Message.chunks_text(self.chunks)
+
+ @classmethod
+ def chunks_text(cls, chunks):
+ bits = [unicode(chunk) for chunk in chunks if chunk.content]
return '\n\n'.join(bits)
# XXX flacoste 2006-09-08: Bogus attribute only present so that
=== modified file 'lib/canonical/launchpad/interfaces/emailaddress.py'
--- lib/canonical/launchpad/interfaces/emailaddress.py 2010-08-20 20:31:18 +0000
+++ lib/canonical/launchpad/interfaces/emailaddress.py 2010-09-23 02:21:04 +0000
@@ -115,7 +115,7 @@
def destroySelf():
"""Destroy this email address and any associated subscriptions.
-
+
:raises UndeletableEmailAddress: When the email address is a person's
preferred one or a hosted mailing list's address.
"""
@@ -153,4 +153,3 @@
Return None if there is no such email address.
"""
-
=== modified file 'lib/canonical/launchpad/interfaces/validation.py'
--- lib/canonical/launchpad/interfaces/validation.py 2010-08-22 18:31:30 +0000
+++ lib/canonical/launchpad/interfaces/validation.py 2010-09-23 02:21:04 +0000
@@ -19,7 +19,7 @@
'validate_new_distrotask',
'valid_upstreamtask',
'valid_password',
- 'validate_date_interval'
+ 'validate_date_interval',
]
from cgi import escape
@@ -31,10 +31,16 @@
from canonical.launchpad import _
from canonical.launchpad.interfaces.launchpad import ILaunchBag
+from canonical.launchpad.interfaces.account import IAccount
+from canonical.launchpad.interfaces.emailaddress import (
+ IEmailAddress,
+ IEmailAddressSet,
+ )
from canonical.launchpad.validators import LaunchpadValidationError
from canonical.launchpad.validators.cve import valid_cve
from canonical.launchpad.validators.email import valid_email
from canonical.launchpad.validators.url import valid_absolute_url
+from canonical.launchpad.webapp import canonical_url
from canonical.launchpad.webapp.menu import structured
from lp.app.errors import NotFoundError
@@ -124,6 +130,7 @@
scheme (for instance, http:// for a web URL), and ensure the
URL uses either http, https or ftp.""")))
+
def valid_branch_url(branch_url):
"""Returns True if web_ref is a valid download URL, or raises a
LaunchpadValidationError.
@@ -188,6 +195,28 @@
"${email} isn't a valid email address.",
mapping={'email': email}))
+def _check_email_availability(email):
+ email_address = getUtility(IEmailAddressSet).getByEmail(email)
+ if email_address is not None:
+ # The email already exists; determine what has it.
+ if email_address.person is not None:
+ person = email_address.person
+ message = _('${email} is already registered in Launchpad and is '
+ 'associated with <a href="${url}">${person}</a>.',
+ mapping={'email': escape(email),
+ 'url': canonical_url(person),
+ 'person': escape(person.displayname)})
+ elif email_address.account is not None:
+ account = email_address.account
+ message = _('${email} is already registered in Launchpad and is '
+ 'associated with the ${account} account.',
+ mapping={'email': escape(email),
+ 'account': escape(account.displayname)})
+ else:
+ message = _('${email} is already registered in Launchpad.',
+ mapping={'email': escape(email)})
+ raise LaunchpadValidationError(structured(message))
+
def validate_new_team_email(email):
"""Check that the given email is valid and not registered to
@@ -195,16 +224,9 @@
"""
from canonical.launchpad.webapp import canonical_url
from canonical.launchpad.interfaces import IEmailAddressSet
+
_validate_email(email)
- email_address = getUtility(IEmailAddressSet).getByEmail(email)
- if email_address is not None:
- person = email_address.person
- message = _('${email} is already registered in Launchpad and is '
- 'associated with <a href="${url}">${team}</a>.',
- mapping={'email': escape(email),
- 'url': canonical_url(person),
- 'team': escape(person.displayname)})
- raise LaunchpadValidationError(structured(message))
+ _check_email_availability(email)
return True
=== modified file 'lib/lp/blueprints/browser/specificationtarget.py'
--- lib/lp/blueprints/browser/specificationtarget.py 2010-08-24 10:45:57 +0000
+++ lib/lp/blueprints/browser/specificationtarget.py 2010-09-23 02:21:04 +0000
@@ -326,6 +326,12 @@
return self.context.specifications(filter=filter)
@cachedproperty
+ def specs_batched(self):
+ navigator = BatchNavigator(self.specs, self.request, size=500)
+ navigator.setHeadings('specification', 'specifications')
+ return navigator
+
+ @cachedproperty
def spec_count(self):
return self.specs.count()
=== modified file 'lib/lp/blueprints/browser/tests/test_specificationtarget.py'
--- lib/lp/blueprints/browser/tests/test_specificationtarget.py 2010-08-20 20:31:18 +0000
+++ lib/lp/blueprints/browser/tests/test_specificationtarget.py 2010-09-23 02:21:04 +0000
@@ -3,8 +3,8 @@
__metaclass__ = type
-import unittest
-
+from canonical.launchpad.webapp.batching import BatchNavigator
+from canonical.launchpad.testing.pages import find_tag_by_id
from canonical.testing.layers import DatabaseFunctionalLayer
from lp.blueprints.interfaces.specificationtarget import (
IHasSpecifications,
@@ -15,7 +15,10 @@
login_person,
TestCaseWithFactory,
)
-from lp.testing.views import create_view
+from lp.testing.views import (
+ create_view,
+ create_initialized_view,
+ )
class TestRegisterABlueprintButtonView(TestCaseWithFactory):
@@ -86,12 +89,43 @@
self.assertFalse(
'<div id="involvement" class="portlet involvement">' in view())
-
-def test_suite():
- suite = unittest.TestSuite()
- suite.addTest(unittest.TestLoader().loadTestsFromName(__name__))
- return suite
-
-
-if __name__ == '__main__':
- unittest.TextTestRunner().run(test_suite())
+ def test_specs_batch(self):
+ # Some pages turn up in very large contexts and patch. E.g.
+ # Distro:+assignments which uses SpecificationAssignmentsView, a
+ # subclass.
+ person = self.factory.makePerson()
+ view = create_initialized_view(person, name='+assignments')
+ self.assertIsInstance(view.specs_batched, BatchNavigator)
+
+ def test_batch_headings(self):
+ person = self.factory.makePerson()
+ view = create_initialized_view(person, name='+assignments')
+ navigator = view.specs_batched
+ self.assertEqual('specification', navigator._singular_heading)
+ self.assertEqual('specifications', navigator._plural_heading)
+
+ def test_batch_size(self):
+ # Because +assignments is meant to provide an overview, we default to
+ # 500 as the default batch size.
+ person = self.factory.makePerson()
+ view = create_initialized_view(person, name='+assignments')
+ navigator = view.specs_batched
+ self.assertEqual(500, view.specs_batched.default_size)
+
+
+class TestAssignments(TestCaseWithFactory):
+
+ layer = DatabaseFunctionalLayer
+
+ def test_assignments_are_batched(self):
+ product = self.factory.makeProduct()
+ spec1 = self.factory.makeSpecification(product=product)
+ spec2 = self.factory.makeSpecification(product=product)
+ view = create_initialized_view(product, name='+assignments',
+ query_string="batch=1")
+ content = view.render()
+ self.assertEqual('next',
+ find_tag_by_id(content, 'upper-batch-nav-batchnav-next')['class'])
+ self.assertEqual('next',
+ find_tag_by_id(content, 'lower-batch-nav-batchnav-next')['class'])
+
=== modified file 'lib/lp/blueprints/model/specification.py'
--- lib/lp/blueprints/model/specification.py 2010-08-31 19:38:30 +0000
+++ lib/lp/blueprints/model/specification.py 2010-09-23 02:21:04 +0000
@@ -713,50 +713,39 @@
"""
# Circular import.
from lp.registry.model.person import Person
- assignee = ClassAlias(Person, "assignee")
- approver = ClassAlias(Person, "approver")
- drafter = ClassAlias(Person, "drafter")
- origin = [Specification,
- LeftJoin(assignee, Specification.assignee==assignee.id),
- LeftJoin(approver, Specification.approver==approver.id),
- LeftJoin(drafter, Specification.drafter==drafter.id),
- ]
- columns = [Specification, assignee, approver, drafter]
- for alias in (assignee, approver, drafter):
- validity_info = Person._validity_queries(person_table=alias)
+ def cache_people(rows):
+ # Find the people we need:
+ person_ids = set()
+ for spec in rows:
+ person_ids.add(spec.assigneeID)
+ person_ids.add(spec.approverID)
+ person_ids.add(spec.drafterID)
+ person_ids.discard(None)
+ if not person_ids:
+ return
+ # Query those people
+ origin = [Person]
+ columns = [Person]
+ validity_info = Person._validity_queries()
origin.extend(validity_info["joins"])
columns.extend(validity_info["tables"])
- # We only need one decorators list: all the decorators will be
- # bound the same, and having a single list lets us more easily call
- # the right one.
decorators = validity_info["decorators"]
- columns = tuple(columns)
- results = Store.of(self).using(*origin).find(
- columns,
+ personset = Store.of(self).using(*origin).find(
+ tuple(columns),
+ Person.id.is_in(person_ids),
+ )
+ for row in personset:
+ person = row[0]
+ index = 1
+ for decorator in decorators:
+ column = row[index]
+ index += 1
+ decorator(person, column)
+ results = Store.of(self).find(
+ Specification,
SQL(query),
)
- def cache_person(person, row, start_index):
- """apply caching decorators to person."""
- index = start_index
- for decorator in decorators:
- column = row[index]
- index += 1
- decorator(person, column)
- return index
- def cache_validity(row):
- # Assignee
- person = row[1]
- # After drafter
- index = 4
- index = cache_person(person, row, index)
- # approver
- person = row[2]
- index = cache_person(person, row, index)
- # drafter
- person = row[3]
- index = cache_person(person, row, index)
- return row[0]
- return DecoratedResultSet(results, cache_validity)
+ return DecoratedResultSet(results, pre_iter_hook=cache_people)
@property
def valid_specifications(self):
=== modified file 'lib/lp/blueprints/templates/specificationtarget-assignments.pt'
--- lib/lp/blueprints/templates/specificationtarget-assignments.pt 2009-09-22 15:02:41 +0000
+++ lib/lp/blueprints/templates/specificationtarget-assignments.pt 2010-09-23 02:21:04 +0000
@@ -31,6 +31,7 @@
sign off on the specification.
</p>
+ <tal:navigation replace="structure view/specs_batched/@@+navigation-links-upper" />
<table class="listing sortable" id="work">
<thead>
<tr>
@@ -43,8 +44,8 @@
<th>Approver</th>
</tr>
</thead>
- <tbody class="lesser">
- <tr tal:repeat="spec view/specs">
+ <tbody class="lesser" tal:define="specs view/specs_batched/currentBatch">
+ <tr tal:repeat="spec specs">
<td>
<span class="sortkey" tal:content="spec/priority/sortkey" />
<span tal:content="spec/priority/title"
@@ -91,6 +92,7 @@
</tr>
</tbody>
</table>
+ <tal:navigation replace="structure view/specs_batched/@@+navigation-links-lower" />
</div>
</div>
=== modified file 'lib/lp/bugs/model/bug.py'
--- lib/lp/bugs/model/bug.py 2010-09-09 17:02:33 +0000
+++ lib/lp/bugs/model/bug.py 2010-09-23 02:21:04 +0000
@@ -58,6 +58,7 @@
SQLRaw,
Union,
)
+from storm.info import ClassAlias
from storm.store import (
EmptyResultSet,
Store,
@@ -426,21 +427,101 @@
@property
def indexed_messages(self):
"""See `IMessageTarget`."""
+ return self._indexed_messages(include_content=True)
+
+ def _indexed_messages(self, include_content=False, include_parents=True):
+ """Get the bugs messages, indexed.
+
+ :param include_content: If True retrieve the content for the messages
+ too.
+ :param include_parents: If True retrieve the object for parent messages
+ too. If False the parent attribute will be *forced* to None to
+ reduce database lookups.
+ """
+ # Make all messages be 'in' the main bugtask.
inside = self.default_bugtask
- messages = list(self.messages)
- message_set = set(messages)
-
- indexed_messages = []
- for index, message in enumerate(messages):
- if message.parent not in message_set:
- parent = None
+ store = Store.of(self)
+ message_by_id = {}
+ if include_parents:
+ def to_messages(rows):
+ return [row[0] for row in rows]
+ else:
+ def to_messages(rows):
+ return rows
+ def eager_load_owners(messages):
+ # Because we may have multiple owners, we spend less time in storm
+ # with very large bugs by not joining and instead querying a second
+ # time. If this starts to show high db time, we can left outer join
+ # instead.
+ owner_ids = set(message.ownerID for message in messages)
+ owner_ids.discard(None)
+ if not owner_ids:
+ return
+ list(store.find(Person, Person.id.is_in(owner_ids)))
+ def eager_load_content(messages):
+ # To avoid the complexity of having multiple rows per
+ # message, or joining in the database (though perhaps in
+ # future we should do that), we do a single separate query
+ # for the message content.
+ message_ids = set(message.id for message in messages)
+ chunks = store.find(
+ MessageChunk, MessageChunk.messageID.is_in(message_ids))
+ chunks.order_by(MessageChunk.id)
+ chunk_map = {}
+ for chunk in chunks:
+ message_chunks = chunk_map.setdefault(chunk.messageID, [])
+ message_chunks.append(chunk)
+ for message in messages:
+ if message.id not in chunk_map:
+ continue
+ cache = IPropertyCache(message)
+ cache.text_contents = Message.chunks_text(
+ chunk_map[message.id])
+ def eager_load(rows, slice_info):
+ messages = to_messages(rows)
+ eager_load_owners(messages)
+ if include_content:
+ eager_load_content(messages)
+ def index_message(row, index):
+ # convert row to an IndexedMessage
+ if include_parents:
+ message, parent = row
+ if parent is not None:
+ # If there is an IndexedMessage available as parent, use
+ # that to reduce on-demand parent lookups.
+ parent = message_by_id.get(parent.id, parent)
else:
- parent = message.parent
-
- indexed_message = IndexedMessage(message, inside, index, parent)
- indexed_messages.append(indexed_message)
-
- return indexed_messages
+ message = row
+ parent = None # parent attribute is not going to be accessed.
+ result = IndexedMessage(message, inside, index, parent)
+ # This message may be the parent for another: stash it to permit
+ # use.
+ message_by_id[message.id] = result
+ return result
+ # There is possibly some nicer way to do this in storm, but
+ # this is a lot easier to figure out.
+ if include_parents:
+ ParentMessage = ClassAlias(Message, name="parent_message")
+ tables = SQL("""
+Message left outer join
+message as parent_message on (
+ message.parent=parent_message.id and
+ parent_message.id in (
+ select bugmessage.message from bugmessage where bugmessage.bug=%s)),
+BugMessage""" % sqlvalues(self.id))
+ results = store.using(tables).find(
+ (Message, ParentMessage),
+ BugMessage.bugID == self.id,
+ BugMessage.messageID == Message.id,
+ )
+ else:
+ results = store.find(Message,
+ BugMessage.bugID == self.id,
+ BugMessage.messageID == Message.id,
+ )
+ results.order_by(Message.datecreated, Message.id)
+ return DecoratedResultSet(results, index_message,
+ pre_iter_hook=eager_load, slice_info=True)
@property
def displayname(self):
@@ -1785,7 +1866,7 @@
when attachments is evaluated, not when the resultset is processed).
"""
message_to_indexed = {}
- for message in self.indexed_messages:
+ for message in self._indexed_messages(include_parents=False):
message_to_indexed[message.id] = message
def set_indexed_message(row):
attachment = row[0]
=== modified file 'lib/lp/bugs/stories/structural-subscriptions/xx-bug-subscriptions.txt'
--- lib/lp/bugs/stories/structural-subscriptions/xx-bug-subscriptions.txt 2010-05-21 04:38:42 +0000
+++ lib/lp/bugs/stories/structural-subscriptions/xx-bug-subscriptions.txt 2010-09-23 02:21:04 +0000
@@ -194,3 +194,33 @@
>>> anon_browser.open('http://launchpad.dev/bzr')
>>> print anon_browser.title
Bazaar Version Control System in Launchpad
+
+== Distribution with a bug supervisor ==
+
+If a distribution has a bug supervisor only that team or members of it can
+subscribe to all of the distribution's bugs.
+
+First, check the page content for a distribution without a bug supervisor.
+
+ >>> browser.open('http://bugs.launchpad.dev/ubuntu/+subscribe')
+ >>> text_contents = extract_text(find_main_content(browser.contents))
+ >>> "You can choose to receive an e-mail every time" in text_contents
+ True
+
+Set a bug supervisor for Ubuntu.
+
+ >>> from zope.security.proxy import removeSecurityProxy
+ >>> login('foo.bar@xxxxxxxxxxxxx')
+ >>> ubuntu = removeSecurityProxy(getUtility(IDistributionSet).get(1))
+ >>> guadamen = getUtility(IPersonSet).getByName('guadamen')
+ >>> ubuntu.bug_supervisor = guadamen
+ >>> flush_database_updates()
+ >>> logout()
+
+Second, check that the page content for a distribution with a bug supervisor
+contains a message about not being able to subscribe.
+
+ >>> browser.open('http://bugs.launchpad.dev/ubuntu/+subscribe')
+ >>> text_contents = extract_text(find_main_content(browser.contents))
+ >>> "You are unable to subscribe to bug reports about" in text_contents
+ True
=== modified file 'lib/lp/bugs/tests/test_bugs_webservice.py'
--- lib/lp/bugs/tests/test_bugs_webservice.py 2010-09-02 19:30:03 +0000
+++ lib/lp/bugs/tests/test_bugs_webservice.py 2010-09-23 02:21:04 +0000
@@ -143,7 +143,7 @@
rendered_comment, self.expected_comment_html)
-class TestBugAttachments(TestCaseWithFactory):
+class TestBugScaling(TestCaseWithFactory):
layer = LaunchpadFunctionalLayer
@@ -165,12 +165,12 @@
collector = QueryCollector()
collector.register()
self.addCleanup(collector.unregister)
- url = '/bugs/%d/attachments' % self.bug.id
+ url = '/bugs/%d/attachments?ws.size=75' % self.bug.id
#First request
store.flush()
store.reset()
response = webservice.get(url)
- self.assertThat(collector, HasQueryCount(LessThan(24)))
+ self.assertThat(collector, HasQueryCount(LessThan(21)))
with_2_count = collector.count
self.failUnlessEqual(response.status, 200)
login(USER_EMAIL)
@@ -181,7 +181,44 @@
store.flush()
store.reset()
response = webservice.get(url)
- self.assertThat(collector, HasQueryCount(Equals(with_2_count+1)))
+ self.assertThat(collector, HasQueryCount(Equals(with_2_count)))
+
+ def test_messages_query_counts_constant(self):
+ # XXX Robert Collins 2010-09-15 bug=619017
+ # This test may be thrown off by the reference bug. To get around the
+ # problem, flush and reset are called on the bug storm cache before
+ # each call to the webservice. When lp's storm is updated to release
+ # the committed fix for this bug, please see about updating this test.
+ login(USER_EMAIL)
+ bug = self.factory.makeBug()
+ store = Store.of(bug)
+ self.factory.makeBugComment(bug)
+ self.factory.makeBugComment(bug)
+ self.factory.makeBugComment(bug)
+ person = self.factory.makePerson()
+ webservice = LaunchpadWebServiceCaller(
+ 'launchpad-library', 'salgado-change-anything')
+ collector = QueryCollector()
+ collector.register()
+ self.addCleanup(collector.unregister)
+ url = '/bugs/%d/messages?ws.size=75' % bug.id
+ #First request
+ store.flush()
+ store.reset()
+ response = webservice.get(url)
+ self.assertThat(collector, HasQueryCount(LessThan(21)))
+ with_2_count = collector.count
+ self.failUnlessEqual(response.status, 200)
+ login(USER_EMAIL)
+ for i in range(50):
+ self.factory.makeBugComment(bug)
+ self.factory.makeBugAttachment(bug)
+ logout()
+ #Second request
+ store.flush()
+ store.reset()
+ response = webservice.get(url)
+ self.assertThat(collector, HasQueryCount(Equals(with_2_count)))
class TestBugMessages(TestCaseWithFactory):
=== modified file 'lib/lp/bugs/tests/test_searchtasks_webservice.py'
--- lib/lp/bugs/tests/test_searchtasks_webservice.py 2010-08-20 21:48:35 +0000
+++ lib/lp/bugs/tests/test_searchtasks_webservice.py 2010-09-23 02:21:04 +0000
@@ -5,8 +5,6 @@
__metaclass__ = type
-from unittest import TestLoader
-
from canonical.launchpad.ftests import login
from lp.testing import TestCaseWithFactory
from canonical.launchpad.testing.pages import LaunchpadWebServiceCaller
@@ -37,7 +35,3 @@
response = self.webservice.named_get('/mebuntu/inkanyamba',
'searchTasks', api_version='devel').jsonBody()
self.assertEqual(response['total_size'], 1)
-
-
-def test_suite():
- return TestLoader().loadTestsFromName(__name__)
=== modified file 'lib/lp/code/stories/branches/xx-branchmergeproposals.txt'
--- lib/lp/code/stories/branches/xx-branchmergeproposals.txt 2010-05-27 02:19:27 +0000
+++ lib/lp/code/stories/branches/xx-branchmergeproposals.txt 2010-09-23 02:21:04 +0000
@@ -543,7 +543,6 @@
>>> nopriv_browser.open(bmp_url)
>>> print_bugs_and_specs(nopriv_browser)
- Linked bug reports and blueprints
Bug #...: Bug for linking Undecided New
=== modified file 'lib/lp/code/templates/branchmergeproposal-index.pt'
--- lib/lp/code/templates/branchmergeproposal-index.pt 2010-07-15 13:57:39 +0000
+++ lib/lp/code/templates/branchmergeproposal-index.pt 2010-09-23 02:21:04 +0000
@@ -46,6 +46,10 @@
#proposal-summary td {
padding-left: 0.5em;
}
+ .related-bugs-list {
+ padding-left: 20px;
+ padding-bottom: 10px;
+ }
/* A page-specific fix for inline text are editing to line up box. */
#edit-description .yui-ieditor-input { top: 0; }
#edit-commit_message .yui-ieditor-input { top: 0; }
@@ -138,6 +142,18 @@
</tal:has-description>
</div>
+ <div class="yui-g" tal:condition="view/has_bug_or_spec">
+ <div id="related-bugs-and-blueprints" class="related-bugs-list"
+ tal:define="branch context/source_branch">
+ <div class="actions">
+ <metal:bug-branch-links use-macro="branch/@@+macros/bug-branch-links"/>
+ </div>
+ <div class="actions">
+ <metal:spec-branch-links use-macro="branch/@@+macros/spec-branch-links"/>
+ </div>
+ </div>
+ </div>
+
<div class="yui-g">
<tal:not-logged-in condition="not: view/user">
<div align="center" id="add-comment-login-first">
@@ -176,36 +192,19 @@
</tal:logged-in>
<div class="yui-g">
- <div class="yui-u first">
- <div id="source-revisions"
- tal:condition="not: context/queue_status/enumvalue:MERGED">
-
- <tal:history-available condition="context/source_branch/revision_count"
- define="branch context/source_branch;
- revisions view/unlanded_revisions">
- <h2>Unmerged revisions</h2>
- <metal:landing-target use-macro="branch/@@+macros/branch-revisions"/>
- </tal:history-available>
-
- <tal:remote-branch condition="context/source_branch/branch_type/enumvalue:REMOTE">
- <h2>Unmerged revisions</h2>
- <p>Recent revisions are not available due to the source branch being remote.</p>
- </tal:remote-branch>
- </div>
- </div>
-
- <div class="yui-u" tal:condition="view/has_bug_or_spec">
- <div id="related-bugs-and-blueprints"
- tal:define="branch context/source_branch">
- <h2>Linked bug reports and blueprints</h2>
- <div class="actions">
-
- <metal:bug-branch-links use-macro="branch/@@+macros/bug-branch-links"/>
- </div>
- <div class="actions">
- <metal:spec-branch-links use-macro="branch/@@+macros/spec-branch-links"/>
- </div>
- </div>
+ <div id="source-revisions"
+ tal:condition="not: context/queue_status/enumvalue:MERGED">
+
+ <tal:history-available condition="context/source_branch/revision_count"
+ define="branch context/source_branch;
+ revisions view/unlanded_revisions">
+ <h2>Unmerged revisions</h2>
+ <metal:landing-target use-macro="branch/@@+macros/branch-revisions"/>
+ </tal:history-available>
+ <tal:remote-branch condition="context/source_branch/branch_type/enumvalue:REMOTE">
+ <h2>Unmerged revisions</h2>
+ <p>Recent revisions are not available due to the source branch being remote.</p>
+ </tal:remote-branch>
</div>
</div>
=== modified file 'lib/lp/registry/browser/person.py'
--- lib/lp/registry/browser/person.py 2010-09-19 21:23:58 +0000
+++ lib/lp/registry/browser/person.py 2010-09-23 02:21:04 +0000
@@ -4835,6 +4835,11 @@
"'%s' doesn't seem to be a valid email address." % newemail)
return self.errors
+ # XXX j.c.sackett 2010-09-15 bug=628247, 576757 There is a validation
+ # system set up for this that is almost identical in
+ # canonical.launchpad.interfaces.validation, called
+ # _check_email_available or similar. It would be really nice if we
+ # could merge that code somehow with this.
email = getUtility(IEmailAddressSet).getByEmail(newemail)
person = self.context
if email is not None:
@@ -4846,20 +4851,32 @@
"detected it as being yours. If it was detected by our "
"system, it's probably shown on this page and is waiting "
"to be confirmed as yours." % newemail)
- else:
+ elif email.person is not None:
owner = email.person
owner_name = urllib.quote(owner.name)
merge_url = (
'%s/+requestmerge?field.dupe_person=%s'
% (canonical_url(getUtility(IPersonSet)), owner_name))
- self.addError(
- structured(
- "The email address '%s' is already registered to "
- '<a href="%s">%s</a>. If you think that is a '
- 'duplicated account, you can <a href="%s">merge it'
- "</a> into your account.",
- newemail, canonical_url(owner), owner.displayname,
- merge_url))
+ self.addError(structured(
+ "The email address '%s' is already registered to "
+ '<a href="%s">%s</a>. If you think that is a '
+ 'duplicated account, you can <a href="%s">merge it'
+ "</a> into your account.",
+ newemail,
+ canonical_url(owner),
+ owner.displayname,
+ merge_url))
+ elif email.account is not None:
+ account = email.account
+ self.addError(structured(
+ "The email address '%s' is already registered to an "
+ "account, %s.",
+ newemail,
+ account.displayname))
+ else:
+ self.addError(structured(
+ "The email address '%s' is already registered.",
+ newemail))
return self.errors
@action(_("Add"), name="add_email", validator=validate_action_add_email)
=== modified file 'lib/lp/registry/browser/structuralsubscription.py'
--- lib/lp/registry/browser/structuralsubscription.py 2010-08-24 10:45:57 +0000
+++ lib/lp/registry/browser/structuralsubscription.py 2010-09-23 02:21:04 +0000
@@ -172,6 +172,10 @@
"""Return True, if the current user is subscribed."""
return self.isSubscribed(self.user)
+ def userCanAlter(self):
+ if self.context.userCanAlterBugSubscription(self.user, self.user):
+ return True
+
@action(u'Save these changes', name='save')
def save_action(self, action, data):
"""Process the subscriptions submitted by the user."""
=== modified file 'lib/lp/registry/browser/tests/test_person_view.py'
--- lib/lp/registry/browser/tests/test_person_view.py 2010-09-02 19:28:35 +0000
+++ lib/lp/registry/browser/tests/test_person_view.py 2010-09-23 02:21:04 +0000
@@ -5,13 +5,16 @@
import transaction
from zope.component import getUtility
+from zope.security.proxy import removeSecurityProxy
from canonical.config import config
from canonical.launchpad.ftests import (
ANONYMOUS,
login,
)
+from canonical.launchpad.interfaces.authtoken import LoginTokenType
from canonical.launchpad.interfaces.account import AccountStatus
+from canonical.launchpad.interfaces.logintoken import ILoginTokenSet
from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
from canonical.launchpad.webapp.servers import LaunchpadTestRequest
from canonical.testing import (
@@ -29,7 +32,10 @@
)
from lp.registry.interfaces.karma import IKarmaCacheManager
-from lp.registry.interfaces.person import PersonVisibility
+from lp.registry.interfaces.person import (
+ PersonVisibility,
+ IPersonSet,
+ )
from lp.registry.interfaces.teammembership import (
ITeamMembershipSet,
TeamMembershipStatus,
@@ -207,7 +213,8 @@
def setUp(self):
TestCaseWithFactory.setUp(self)
- self.person = self.factory.makePerson()
+ self.valid_email_address = self.factory.getUniqueEmailAddress()
+ self.person = self.factory.makePerson(email=self.valid_email_address)
login_person(self.person)
self.ppa = self.factory.makeArchive(owner=self.person)
self.view = PersonEditView(
@@ -256,6 +263,94 @@
self.view.initialize()
self.assertFalse(self.view.form_fields['name'].for_display)
+ def test_add_email_good_data(self):
+ email_address = self.factory.getUniqueEmailAddress()
+ form = {
+ 'field.VALIDATED_SELECTED': self.valid_email_address,
+ 'field.VALIDATED_SELECTED-empty-marker': 1,
+ 'field.actions.add_email': 'Add',
+ 'field.newemail': email_address,
+ }
+ view = create_initialized_view(self.person, "+editemails", form=form)
+
+ # If everything worked, there should now be a login token to validate
+ # this email address for this user.
+ token = getUtility(ILoginTokenSet).searchByEmailRequesterAndType(
+ email_address,
+ self.person,
+ LoginTokenType.VALIDATEEMAIL)
+ self.assertTrue(token is not None)
+
+ def test_add_email_address_taken(self):
+ email_address = self.factory.getUniqueEmailAddress()
+ account = self.factory.makeAccount(
+ displayname='deadaccount',
+ email=email_address,
+ status=AccountStatus.NOACCOUNT)
+ form = {
+ 'field.VALIDATED_SELECTED': self.valid_email_address,
+ 'field.VALIDATED_SELECTED-empty-marker': 1,
+ 'field.actions.add_email': 'Add',
+ 'field.newemail': email_address,
+ }
+ view = create_initialized_view(self.person, "+editemails", form=form)
+ error_msg = view.errors[0]
+ expected_msg = ("The email address '%s' is already registered to an "
+ "account, deadaccount." % email_address)
+ self.assertEqual(expected_msg, error_msg)
+
+
+class TestTeamCreationView(TestCaseWithFactory):
+
+ layer = DatabaseFunctionalLayer
+
+ def setUp(self):
+ super(TestTeamCreationView, self).setUp()
+ person = self.factory.makePerson()
+ login_person(person)
+
+ def test_team_creation_good_data(self):
+ form = {
+ 'field.actions.create': 'Create Team',
+ 'field.contactemail': 'contactemail@xxxxxxxxxxx',
+ 'field.displayname': 'liberty-land',
+ 'field.name': 'libertyland',
+ 'field.renewal_policy': 'NONE',
+ 'field.renewal_policy-empty-marker': 1,
+ 'field.subscriptionpolicy': 'RESTRICTED',
+ 'field.subscriptionpolicy-empty-marker': 1,
+ }
+ person_set = getUtility(IPersonSet)
+ view = create_initialized_view(
+ person_set, '+newteam', form=form)
+ team = person_set.getByName('libertyland')
+ self.assertTrue(team is not None)
+ self.assertEqual('libertyland', team.name)
+
+ def test_validate_email_catches_taken_emails(self):
+ email_address = self.factory.getUniqueEmailAddress()
+ account = self.factory.makeAccount(
+ displayname='libertylandaccount',
+ email=email_address,
+ status=AccountStatus.NOACCOUNT)
+ form = {
+ 'field.actions.create': 'Create Team',
+ 'field.contactemail': email_address,
+ 'field.displayname': 'liberty-land',
+ 'field.name': 'libertyland',
+ 'field.renewal_policy': 'NONE',
+ 'field.renewal_policy-empty-marker': 1,
+ 'field.subscriptionpolicy': 'RESTRICTED',
+ 'field.subscriptionpolicy-empty-marker': 1,
+ }
+ person_set = getUtility(IPersonSet)
+ view = create_initialized_view(person_set, '+newteam', form=form)
+ expected_msg = ('%s is already registered in Launchpad and is '
+ 'associated with the libertylandaccount '
+ 'account.' % email_address)
+ error_msg = view.errors[0].errors[0]
+ self.assertEqual(expected_msg, error_msg)
+
class TestPersonParticipationView(TestCaseWithFactory):
@@ -602,6 +697,7 @@
self.assertEqual(
'This account is already deactivated.', view.errors[0])
+
class TestTeamInvitationView(TestCaseWithFactory):
"""Tests for TeamInvitationView."""
=== modified file 'lib/lp/registry/templates/structural-subscriptions-manage.pt'
--- lib/lp/registry/templates/structural-subscriptions-manage.pt 2009-12-05 18:37:28 +0000
+++ lib/lp/registry/templates/structural-subscriptions-manage.pt 2010-09-23 02:21:04 +0000
@@ -8,6 +8,20 @@
>
<body>
<div metal:fill-slot="main">
+ <tal:no_permissions condition="not: view/userCanAlter|nothing">
+ <p>
+ You are unable to subscribe to bug reports about <span
+ tal:replace="context/title">this item</span> as it generates
+ a high amount of bug activity which results in more e-mails than
+ most users can handle.
+ </p>
+ <p>
+ If you really want to subscribe to <span
+ tal:replace="context/title">this item</span> bug mail than
+ please contact its bug supervisor.
+ </p>
+ </tal:no_permissions>
+ <tal:has_permissions condition="view/userCanAlter|nothing">
<p>
You can choose to receive an e-mail every time someone reports or
changes a public bug associated with
@@ -19,6 +33,7 @@
time.
</p>
<div metal:use-macro="context/@@launchpad_form/form" />
+ </tal:has_permissions>
</div>
<div metal:fill-slot="side">
<div tal:replace="structure context/@@+portlet-malone-bugmail-filtering-faq"/>
=== modified file 'lib/lp/services/database/prejoin.py'
--- lib/lp/services/database/prejoin.py 2010-08-20 20:31:18 +0000
+++ lib/lp/services/database/prejoin.py 2010-09-23 02:21:04 +0000
@@ -1,7 +1,11 @@
# Copyright 2009 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
-"""Prejoin for Storm."""
+"""Prejoin for Storm.
+
+XXX bug=632150 This is duplicated with DecoratedResultSet. please use that.
+RBC 20100907.
+"""
__metaclass__ = type
__all__ = ['prejoin']
=== modified file 'lib/lp/soyuz/scripts/ftpmaster.py'
--- lib/lp/soyuz/scripts/ftpmaster.py 2010-09-16 21:13:12 +0000
+++ lib/lp/soyuz/scripts/ftpmaster.py 2010-09-23 02:21:04 +0000
@@ -75,6 +75,7 @@
Mostly used to describe errors in the initialisation of this object.
"""
+
class TagFileNotFound(Exception):
"""Raised when an archive tag file could not be found."""
@@ -126,6 +127,7 @@
def architectures(self):
return dict([(a.architecturetag, a)
for a in self.distroseries.architectures])
+
@property
def components(self):
return dict([(c.name, c) for c in self.distroseries.components])
@@ -223,7 +225,6 @@
for architecture in self.architectures:
self.buildArchNBS(component, architecture)
-
def buildArchNBS(self, component, architecture):
"""Build NBS per architecture.
@@ -257,7 +258,7 @@
source = m.group(1)
version = m.group(2)
- if not self.bin_pkgs.has_key(package):
+ if package not in self.bin_pkgs:
self.nbs.setdefault(source, {})
self.nbs[source].setdefault(package, {})
self.nbs[source][package][version] = ""
@@ -265,7 +266,7 @@
if architecture != "all":
self.arch_any.setdefault(package, "0")
if apt_pkg.VersionCompare(
- version,self.arch_any[package]) < 1:
+ version, self.arch_any[package]) < 1:
self.arch_any[package] = version
finally:
# close fd and remove temporary file used to store uncompressed
@@ -273,7 +274,6 @@
temp_fd.close()
os.unlink(temp_filename)
-
def buildASBA(self):
"""Build the group of 'all superseded by any' binaries."""
self.logger.debug("Building all superseded by any list (ASBA):")
@@ -281,7 +281,6 @@
for architecture in self.architectures:
self.buildArchASBA(component, architecture)
-
def buildArchASBA(self, component, architecture):
"""Build ASBA per architecture.
@@ -315,7 +314,7 @@
version = m.group(2)
if architecture == "all":
- if (self.arch_any.has_key(package) and
+ if (package in self.arch_any and
apt_pkg.VersionCompare(
version, self.arch_any[package]) > -1):
self.asba.setdefault(source, {})
@@ -483,6 +482,7 @@
Currently used for auxiliary storage in PubSourceChecker.
"""
+
def __init__(self, name, version, arch, component, section, priority):
self.name = name
self.version = version
@@ -521,6 +521,7 @@
return "\n".join(report)
+
class PubBinaryDetails:
"""Store the component, section and priority of binary packages and, for
each binary package the most frequent component, section and priority.
@@ -537,6 +538,7 @@
- correct_sections: same as correct_components, but for sections
- correct_priorities: same as correct_components, but for priorities
"""
+
def __init__(self):
self.components = {}
self.sections = {}
@@ -591,6 +593,7 @@
Receive the source publication data and its binaries and perform
a group of heuristic consistency checks.
"""
+
def __init__(self, name, version, component, section, urgency):
self.name = name
self.version = version
@@ -655,7 +658,8 @@
def renderReport(self):
"""Render a formatted report for the publication group.
- Return None if no issue was annotated or an formatted string including:
+ Return None if no issue was annotated or an formatted string
+ including:
SourceName_Version Component/Section/Urgency | # bin
<BINREPORTS>
@@ -719,7 +723,7 @@
ftype = filenameToContentType(filename)
try:
- alias_id = getUtility(ILibrarianClient).addFile(
+ alias_id = getUtility(ILibrarianClient).addFile(
filename, flen, fd, contentType=ftype)
except UploadFailed, info:
raise ChrootManagerError("Librarian upload failed: %s" % info)
@@ -845,7 +849,8 @@
origin: a dictionary similar to 'files' but where the values
contain information for download files to be synchronized
logger: a logger
- downloader: a callable that fetchs URLs, 'downloader(url, destination)'
+ downloader: a callable that fetchs URLs,
+ 'downloader(url, destination)'
todistro: target distribution object
"""
self.files = files
=== modified file 'lib/lp/soyuz/scripts/processaccepted.py'
--- lib/lp/soyuz/scripts/processaccepted.py 2010-09-17 00:53:33 +0000
+++ lib/lp/soyuz/scripts/processaccepted.py 2010-09-23 02:21:04 +0000
@@ -7,6 +7,7 @@
__all__ = [
'close_bugs',
'close_bugs_for_queue_item',
+ 'close_bugs_for_sourcepackagerelease',
'close_bugs_for_sourcepublication',
'get_bugs_from_changes_file',
'ProcessAccepted',
@@ -16,6 +17,7 @@
import sys
from zope.component import getUtility
+from zope.security.proxy import removeSecurityProxy
from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
from canonical.launchpad.webapp.errorlog import (
@@ -165,6 +167,14 @@
janitor = getUtility(ILaunchpadCelebrities).janitor
for bug in bugs_to_close:
+ # We need to remove the security proxy here because the bug
+ # might be private and if this code is called via someone using
+ # the +queue page they will get an OOPS. Ideally, we should
+ # migrate this code to the Job system though, but that's a lot
+ # of work. If you don't do that and you're changing stuff in
+ # here, BE CAREFUL with the unproxied bug object and look at
+ # what you're doing with it that might violate security.
+ bug = removeSecurityProxy(bug)
edited_task = bug.setStatus(
target=source_release.sourcepackage,
status=BugTaskStatus.FIXRELEASED,
=== modified file 'lib/lp/soyuz/scripts/tests/test_queue.py'
--- lib/lp/soyuz/scripts/tests/test_queue.py 2010-08-27 12:21:25 +0000
+++ lib/lp/soyuz/scripts/tests/test_queue.py 2010-09-23 02:21:04 +0000
@@ -3,6 +3,8 @@
"""queue tool base class tests."""
+from __future__ import with_statement
+
__metaclass__ = type
__all__ = [
'upload_bar_source',
@@ -12,6 +14,7 @@
import hashlib
import os
import shutil
+from StringIO import StringIO
import tempfile
from unittest import (
TestCase,
@@ -19,6 +22,7 @@
)
from zope.component import getUtility
+from zope.security.interfaces import ForbiddenAttribute
from zope.security.proxy import removeSecurityProxy
from canonical.config import config
@@ -33,7 +37,10 @@
fillLibrarianFile,
)
from canonical.librarian.utils import filechunks
-from canonical.testing import LaunchpadZopelessLayer
+from canonical.testing import (
+ DatabaseFunctionalLayer,
+ LaunchpadZopelessLayer,
+ )
from lp.archiveuploader.nascentupload import NascentUpload
from lp.archiveuploader.tests import (
datadir,
@@ -42,7 +49,10 @@
mock_logger_quiet,
)
from lp.bugs.interfaces.bug import IBugSet
-from lp.bugs.interfaces.bugtask import IBugTaskSet
+from lp.bugs.interfaces.bugtask import (
+ BugTaskStatus,
+ IBugTaskSet,
+ )
from lp.registry.interfaces.distribution import IDistributionSet
from lp.registry.interfaces.person import IPersonSet
from lp.registry.interfaces.pocket import PackagePublishingPocket
@@ -56,6 +66,9 @@
from lp.soyuz.interfaces.archive import (
IArchiveSet,
)
+from lp.soyuz.scripts.processaccepted import (
+ close_bugs_for_sourcepackagerelease,
+ )
from lp.soyuz.interfaces.queue import (
IPackageUploadSet,
)
@@ -64,6 +77,11 @@
CommandRunnerError,
name_queue_map,
)
+from lp.testing import (
+ celebrity_logged_in,
+ person_logged_in,
+ TestCaseWithFactory,
+ )
class TestQueueBase(TestCase):
@@ -926,6 +944,36 @@
'override binary pmount', component_name='partner')
+class TestQueuePageClosingBugs(TestCaseWithFactory):
+ # The distroseries +queue page can close bug when accepting
+ # packages. Unit tests for that belong here.
+
+ layer = DatabaseFunctionalLayer
+
+ def test_close_bugs_for_sourcepackagerelease_with_private_bug(self):
+ # lp.soyuz.scripts.processaccepted.close_bugs_for_sourcepackagerelease
+ # should work with private bugs where the person using the queue
+ # page doesn't have access to it.
+ changes_file_template = "Format: 1.7\nLaunchpad-bugs-fixed: %s\n"
+ # changelog_entry is required for an assertion inside the function
+ # we're testing.
+ spr = self.factory.makeSourcePackageRelease(changelog_entry="blah")
+ archive_admin = self.factory.makePerson()
+ bug = self.factory.makeBug(private=True)
+ bug_task = self.factory.makeBugTask(target=spr.sourcepackage, bug=bug)
+ changes = StringIO(changes_file_template % bug.id)
+
+ with person_logged_in(archive_admin):
+ # The archive admin user can't normally see this bug.
+ self.assertRaises(ForbiddenAttribute, bug, 'status')
+ # But the bug closure should work.
+ close_bugs_for_sourcepackagerelease(spr, changes)
+
+ # Verify it was closed.
+ with celebrity_logged_in("admin"):
+ self.assertEqual(bug_task.status, BugTaskStatus.FIXRELEASED)
+
+
class TestQueueToolInJail(TestQueueBase):
layer = LaunchpadZopelessLayer
dbuser = config.uploadqueue.dbuser
=== modified file 'lib/lp/soyuz/scripts/tests/test_sync_source.py'
--- lib/lp/soyuz/scripts/tests/test_sync_source.py 2010-09-16 21:13:12 +0000
+++ lib/lp/soyuz/scripts/tests/test_sync_source.py 2010-09-23 02:21:04 +0000
@@ -124,7 +124,7 @@
on disk.
"""
files = {
- 'foo': {'md5sum': 'dd21ab16f950f7ac4f9c78ef1498eee1', 'size': 15}
+ 'foo': {'md5sum': 'dd21ab16f950f7ac4f9c78ef1498eee1', 'size': 15},
}
origin = {}
sync_source = self._getSyncSource(files, origin)
@@ -138,7 +138,7 @@
def testCheckDownloadedFilesWrongMD5(self):
"""Expect SyncSourceError to be raised due the wrong MD5."""
files = {
- 'foo': {'md5sum': 'duhhhhh', 'size': 15}
+ 'foo': {'md5sum': 'duhhhhh', 'size': 15},
}
origin = {}
sync_source = self._getSyncSource(files, origin)
@@ -154,7 +154,7 @@
def testCheckDownloadedFilesWrongSize(self):
"""Expect SyncSourceError to be raised due the wrong size."""
files = {
- 'foo': {'md5sum': 'dd21ab16f950f7ac4f9c78ef1498eee1', 'size': 10}
+ 'foo': {'md5sum': 'dd21ab16f950f7ac4f9c78ef1498eee1', 'size': 10},
}
origin = {}
sync_source = self._getSyncSource(files, origin)
@@ -209,7 +209,8 @@
def testFetchLibrarianFilesOK(self):
"""Probe fetchLibrarianFiles.
- Seek on files published from librarian and download matching filenames.
+ Seek on files published from librarian and download matching
+ filenames.
"""
files = {
'netapplet_1.0.0.orig.tar.gz': {},
@@ -359,7 +360,7 @@
os.unlink(expected_changesfile)
def testSyncSourceRunV3(self):
- """Try a simple sync-source.py run with a version 3 source format
+ """Try a simple sync-source.py run with a version 3 source format
package.
It will run in a special tree prepared to cope with sync-source
@@ -386,7 +387,7 @@
self.assertEqual(
err.splitlines(),
['W: Could not find blacklist file on '
- '/srv/launchpad.net/dak/sync-blacklist.txt',
+ '/srv/launchpad.net/dak/sync-blacklist.txt',
'INFO - <sample1_1.0.orig-component3.tar.gz: cached>',
'INFO - <sample1_1.0-1.dsc: cached>',
'INFO - <sample1_1.0-1.debian.tar.gz: cached>',
=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py 2010-09-10 03:20:48 +0000
+++ lib/lp/testing/factory.py 2010-09-23 02:21:04 +0000
@@ -881,7 +881,7 @@
return product
def makeProductSeries(self, product=None, name=None, owner=None,
- summary=None, date_created=None):
+ summary=None, date_created=None, branch=None):
"""Create and return a new ProductSeries."""
if product is None:
product = self.makeProduct()
@@ -895,7 +895,7 @@
# so we remove the security proxy before creating the series.
naked_product = removeSecurityProxy(product)
series = naked_product.newSeries(
- owner=owner, name=name, summary=summary)
+ owner=owner, name=name, summary=summary, branch=branch)
if date_created is not None:
series.datecreated = date_created
return ProxyFactory(series)
@@ -2537,6 +2537,7 @@
source_package_recipe_build=None,
dscsigningkey=None,
user_defined_fields=None,
+ changelog_entry=None,
homepage=None):
"""Make a `SourcePackageRelease`."""
if distroseries is None:
@@ -2594,7 +2595,7 @@
build_conflicts_indep=build_conflicts_indep,
architecturehintlist=architecturehintlist,
changelog=None,
- changelog_entry=None,
+ changelog_entry=changelog_entry,
dsc=None,
copyright=self.getUniqueString(),
dscsigningkey=dscsigningkey,
=== modified file 'scripts/ftpmaster-tools/sync-source.py'
--- scripts/ftpmaster-tools/sync-source.py 2010-09-16 21:13:12 +0000
+++ scripts/ftpmaster-tools/sync-source.py 2010-09-23 02:21:04 +0000
@@ -18,15 +18,7 @@
import apt_pkg
import commands
-try:
- from debian.deb822 import Dsc
-except ImportError:
- # In older versions of python-debian the main package was named
- # debian_bundle
- # XXX: Remove this when an up to date version of python-debian lands in
- # the PPA or Ubuntu. Maverick will be the first release that has an
- # up to date version of python-debian.
- from debian_bundle.deb822 import Dsc
+from debian.deb822 import Dsc
import errno
import optparse
@@ -92,7 +84,7 @@
return md5sum
-def reject (str, prefix="Rejected: "):
+def reject(str, prefix="Rejected: "):
global reject_message
if str:
reject_message += prefix + str + "\n"
@@ -338,69 +330,6 @@
source, source_component, binary, current_version,
current_component)
-def split_gpg_and_payload(sequence):
- """Return a (gpg_pre, payload, gpg_post) tuple
-
- Each element of the returned tuple is a list of lines (with trailing
- whitespace stripped).
- """
- # XXX JRV 20100211: Copied from deb822.py in python-debian. When
- # Launchpad switches to Lucid this copy should be removed.
- # bug=520508
-
- gpg_pre_lines = []
- lines = []
- gpg_post_lines = []
- state = 'SAFE'
- gpgre = re.compile(r'^-----(?P<action>BEGIN|END) PGP (?P<what>[^-]+)-----$')
- blank_line = re.compile('^$')
- first_line = True
-
- for line in sequence:
- line = line.strip('\r\n')
-
- # skip initial blank lines, if any
- if first_line:
- if blank_line.match(line):
- continue
- else:
- first_line = False
-
- m = gpgre.match(line)
-
- if not m:
- if state == 'SAFE':
- if not blank_line.match(line):
- lines.append(line)
- else:
- if not gpg_pre_lines:
- # There's no gpg signature, so we should stop at
- # this blank line
- break
- elif state == 'SIGNED MESSAGE':
- if blank_line.match(line):
- state = 'SAFE'
- else:
- gpg_pre_lines.append(line)
- elif state == 'SIGNATURE':
- gpg_post_lines.append(line)
- else:
- if m.group('action') == 'BEGIN':
- state = m.group('what')
- elif m.group('action') == 'END':
- gpg_post_lines.append(line)
- break
- if not blank_line.match(line):
- if not lines:
- gpg_pre_lines.append(line)
- else:
- gpg_post_lines.append(line)
-
- if len(lines):
- return (gpg_pre_lines, lines, gpg_post_lines)
- else:
- raise EOFError('only blank lines found in input')
-
def import_dsc(dsc_filename, suite, previous_version, signing_rules,
files_from_librarian, requested_by, origin, current_sources,
@@ -410,10 +339,7 @@
if signing_rules.startswith("must be signed"):
dsc_file.seek(0)
- # XXX JRV 20100211: When Launchpad starts depending on Lucid,
- # use dsc.split_gpg_and_payload() instead.
- # bug=520508
- (gpg_pre, payload, gpg_post) = split_gpg_and_payload(dsc_file)
+ (gpg_pre, payload, gpg_post) = Dsc.split_gpg_and_payload(dsc_file)
if gpg_pre == [] and gpg_post == []:
dak_utils.fubar("signature required for %s but not present"
% dsc_filename)
Follow ups