launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #01107
[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 qa-passed revisions from stable.
Revision 11547 can be deployed: qa-ok
bug 597738 where-bugs-are-tracked UI
Revision 11548 can be deployed: qa-ok
bug 413174 better exceptions for API clients
Revision 11549 can be deployed: qa-ok
bug 159146 OOPS setting answer contact
Revision 11550 can be deployed: qa-ok
bug 636420 allow anonymous access to project release files
Revision 11551 can be deployed: qa-ok
bug 611274 remove an inappropriate oops raising in checkwatches
Revision 11552 can be deployed: qa-ok
bug 638420 turn on project configuration bars
Revision 11553 can be deployed: qa-ok
no-qa
Revision 11554 can be deployed: qa-ok
no-qa
Revision 11555 can be deployed: qa-ok
no-qa
--
https://code.launchpad.net/~lifeless/launchpad/cp/+merge/35965
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 'lib/canonical/launchpad/components/decoratedresultset.py'
--- lib/canonical/launchpad/components/decoratedresultset.py 2010-08-25 19:23:13 +0000
+++ lib/canonical/launchpad/components/decoratedresultset.py 2010-09-19 21:32:44 +0000
@@ -9,6 +9,7 @@
]
from lazr.delegates import delegates
+from storm import Undef
from storm.zope.interfaces import IResultSet
from zope.security.proxy import removeSecurityProxy
@@ -33,25 +34,32 @@
"""
delegates(IResultSet, context='result_set')
- def __init__(self, result_set, result_decorator=None, pre_iter_hook=None):
+ def __init__(self, result_set, result_decorator=None, pre_iter_hook=None,
+ slice_info=False):
"""
:param result_set: The original result set to be decorated.
:param result_decorator: The method with which individual results
will be passed through before being returned.
:param pre_iter_hook: The method to be called (with the 'result_set')
immediately before iteration starts.
+ :param slice_info: If True pass information about the slice parameters
+ to the result_decorator and pre_iter_hook. any() and similar
+ methods will cause None to be supplied.
"""
self.result_set = result_set
self.result_decorator = result_decorator
self.pre_iter_hook = pre_iter_hook
+ self.slice_info = slice_info
- def decorate_or_none(self, result):
+ def decorate_or_none(self, result, row_index=None):
"""Decorate a result or return None if the result is itself None"""
if result is None:
return None
else:
if self.result_decorator is None:
return result
+ elif self.slice_info:
+ return self.result_decorator(result, row_index)
else:
return self.result_decorator(result)
@@ -62,7 +70,8 @@
"""
new_result_set = self.result_set.copy(*args, **kwargs)
return DecoratedResultSet(
- new_result_set, self.result_decorator, self.pre_iter_hook)
+ new_result_set, self.result_decorator, self.pre_iter_hook,
+ self.slice_info)
def config(self, *args, **kwargs):
"""See `IResultSet`.
@@ -79,10 +88,25 @@
"""
# Execute/evaluate the result set query.
results = list(self.result_set.__iter__(*args, **kwargs))
+ if self.slice_info:
+ # Calculate slice data
+ start = self.result_set._offset
+ if start is Undef:
+ start = 0
+ stop = start + len(results)
+ result_slice = slice(start, stop)
if self.pre_iter_hook is not None:
- self.pre_iter_hook(results)
- for value in results:
- yield self.decorate_or_none(value)
+ if self.slice_info:
+ self.pre_iter_hook(results, result_slice)
+ else:
+ self.pre_iter_hook(results)
+ if self.slice_info:
+ start = result_slice.start
+ for offset, value in enumerate(results):
+ yield self.decorate_or_none(value, offset + start)
+ else:
+ for value in results:
+ yield self.decorate_or_none(value)
def __getitem__(self, *args, **kwargs):
"""See `IResultSet`.
@@ -94,7 +118,8 @@
naked_value = removeSecurityProxy(value)
if IResultSet.providedBy(naked_value):
return DecoratedResultSet(
- value, self.result_decorator, self.pre_iter_hook)
+ value, self.result_decorator, self.pre_iter_hook,
+ self.slice_info)
else:
return self.decorate_or_none(value)
@@ -137,4 +162,5 @@
"""
new_result_set = self.result_set.order_by(*args, **kwargs)
return DecoratedResultSet(
- new_result_set, self.result_decorator, self.pre_iter_hook)
+ new_result_set, self.result_decorator, self.pre_iter_hook,
+ self.slice_info)
=== modified file 'lib/canonical/launchpad/configure.zcml'
--- lib/canonical/launchpad/configure.zcml 2010-08-17 13:58:57 +0000
+++ lib/canonical/launchpad/configure.zcml 2010-09-19 21:32:44 +0000
@@ -31,8 +31,6 @@
<include package="lp.translations" />
<include package="lp.testopenid" />
<include package="lp.blueprints" />
- <include package="lp.services.comments" />
- <include package="lp.services.fields" />
<include package="lp.vostok" />
<browser:url
=== modified file 'lib/canonical/launchpad/doc/decoratedresultset.txt'
--- lib/canonical/launchpad/doc/decoratedresultset.txt 2009-07-23 17:51:28 +0000
+++ lib/canonical/launchpad/doc/decoratedresultset.txt 2010-09-19 21:32:44 +0000
@@ -1,30 +1,17 @@
= DecoratedResultSet =
-Within Soyuz (and possibly other areas of Launchpad?) there are a
-number of content classes which do not actually correspond directly to
-a database table. For example, a `DistroSeriesBinaryPackage` is really
-just a representation of a `BinaryPackageName` within a certain
-DistroSeries. Nonetheless, this representation is presented (via views)
-to users as a useful reference point for obtaining particular package
-releases and/or architectures.
-
-A problem arises however when attempting to present a search result of
-such content. For example, when a user searches for all packages within
-the Ubuntu Intrepid `DistroSeries` that include the letter 'l', it is
-actually the `DistroSeriesPackageCache` that is searched, but the
-results need to be presented back to the browser as a set of
-`DistroSeriesBinaryPackage`s. In the past this was achieved by using a
-list comprehension on the complete result set to convert each result
-into the expected DSBP. This in-memory list was then passed to the view.
-But given that views usually paginate results, they are only interested
-in 20 or so items at a time, so there is a huge waste of resources (DB
-and memory primarily, but some CPU).
-
-The purpose of the `DecoratedResultSet` is to allow such content
-classes to pass an un-evaluated result set to the view so that the view
-can add limits to the query *before* it is evaluated (batching), while
-still ensuring that when the query is evaluated the result is converted
-into the expected content class.
+Within Launchpad we often want to return related data for a ResultSet
+but not have every call site know how that data is structured - they
+should not need to know how to go about loading related Persons for
+a BugMessage, or how to calculate whether a Person is valid. Nor do
+we want to return a less capable object - that prohibits late slicing
+and sorting.
+
+DecoratedResultSet permits some preprocessing of Storm ResultSet
+objects at the time the query executes. This can be used to present
+content classes which are not backed directly in the database, to
+eager load multiple related tables and present just one in the result,
+and so on.
First, we'll create the un-decorated result set of all distributions:
@@ -38,7 +25,8 @@
== Creating the decorator method ==
We create a decorator method that we want to be applied to any
-results obtained from our undecorated result set:
+results obtained from our undecorated result set. For instance,
+we can turn a model object into a string:
>>> def result_decorator(distribution):
... return "Dist name is: %s" % distribution.name
@@ -150,3 +138,47 @@
> original value : 4
decorated result: 8
+
+== Calculating row numbers ==
+
+DecoratedResultSet can inform its hooks about slice data if slice_info=True is
+passed.
+
+ >>> def pre_iter(rows, slice):
+ ... print "pre iter", len(rows), slice.start, slice.stop
+ >>> def decorate(row, row_index):
+ ... print "row", row.id, row_index
+ >>> _ = result_set.order_by(Distribution.id)
+ >>> drs = DecoratedResultSet(
+ ... result_set, decorate, pre_iter, slice_info=True)
+
+We need enough rows to play with:
+
+ >>> drs.count()
+ 7
+
+ >>> _ = list(drs[1:3])
+ pre iter 2 1 3
+ row 2 1
+ row 3 2
+
+Half open slicing is supported too:
+
+ >>> _ = list(drs[:3])
+ pre iter 3 0 3
+ row 1 0
+ row 2 1
+ row 3 2
+
+ >>> _ = list(drs[2:])
+ pre iter 5 2 7
+ row 3 2
+ row 4 3
+ row 5 4
+ row 7 5
+ row 8 6
+
+And of course empty slices:
+
+ >>> _ = list(drs[3:3])
+ pre iter 0 3 3
=== modified file 'lib/canonical/launchpad/helpers.py'
--- lib/canonical/launchpad/helpers.py 2010-08-20 20:31:18 +0000
+++ lib/canonical/launchpad/helpers.py 2010-09-19 21:32:44 +0000
@@ -25,18 +25,18 @@
from zope.security.interfaces import ForbiddenAttribute
import canonical
-from canonical.launchpad.interfaces import (
- ILaunchBag,
+from canonical.launchpad.interfaces import ILaunchBag
+from lp.services.geoip.interfaces import (
IRequestLocalLanguages,
IRequestPreferredLanguages,
)
-# pylint: disable-msg=W0102
+
def text_replaced(text, replacements, _cache={}):
"""Return a new string with text replaced according to the dict provided.
- The keys of the dict are substrings to find, the values are what to replace
- found substrings with.
+ The keys of the dict are substrings to find, the values are what to
+ replace found substrings with.
:arg text: An unicode or str to do the replacement.
:arg replacements: A dictionary with the replacements that should be done
@@ -78,13 +78,16 @@
# Make a copy of the replacements dict, as it is mutable, but we're
# keeping a cached reference to it.
replacements_copy = dict(replacements)
+
def matchobj_replacer(matchobj):
return replacements_copy[matchobj.group()]
+
regexsub = re.compile(join_char.join(L)).sub
+
def replacer(s):
return regexsub(matchobj_replacer, s)
+
_cache[cachekey] = replacer
-
return _cache[cachekey](text)
@@ -98,7 +101,7 @@
def join_lines(*lines):
"""Concatenate a list of strings, adding a newline at the end of each."""
- return ''.join([ x + '\n' for x in lines ])
+ return ''.join([x + '\n' for x in lines])
def string_to_tarfile(s):
@@ -169,7 +172,7 @@
in the database.
"""
# All chars should be lower case, underscores and spaces become dashes.
- return text_replaced(invalid_name.lower(), {'_': '-', ' ':'-'})
+ return text_replaced(invalid_name.lower(), {'_': '-', ' ': '-'})
def browserLanguages(request):
@@ -193,8 +196,7 @@
p = subprocess.Popen(
command, env=env, stdin=subprocess.PIPE,
- stdout=subprocess.PIPE, stderr=subprocess.STDOUT
- )
+ stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
(output, nothing) = p.communicate(input)
return output
@@ -247,7 +249,7 @@
3: {'.': ' (!) ',
'@': ' (at) '},
4: {'.': ' {dot} ',
- '@': ' {at} '}
+ '@': ' {at} '},
}
@@ -444,12 +446,13 @@
>>> filenameToContentType('test.tgz')
'application/octet-stream'
"""
- ftmap = {".dsc": "text/plain",
- ".changes": "text/plain",
- ".deb": "application/x-debian-package",
- ".udeb": "application/x-debian-package",
- ".txt": "text/plain",
- ".txt.gz": "text/plain", # For the build master logs
+ ftmap = {".dsc": "text/plain",
+ ".changes": "text/plain",
+ ".deb": "application/x-debian-package",
+ ".udeb": "application/x-debian-package",
+ ".txt": "text/plain",
+ # For the build master logs
+ ".txt.gz": "text/plain",
}
for ending in ftmap:
if fname.endswith(ending):
=== modified file 'lib/canonical/launchpad/interfaces/__init__.py'
--- lib/canonical/launchpad/interfaces/__init__.py 2010-08-20 12:42:25 +0000
+++ lib/canonical/launchpad/interfaces/__init__.py 2010-09-19 21:32:44 +0000
@@ -71,7 +71,6 @@
from lp.bugs.interfaces.externalbugtracker import *
from lp.registry.interfaces.featuredproject import *
from lp.soyuz.interfaces.files import *
-from canonical.launchpad.interfaces.geoip import *
from lp.registry.interfaces.gpg import *
from canonical.launchpad.interfaces.gpghandler import *
from lp.hardwaredb.interfaces.hwdb import *
=== modified file 'lib/canonical/launchpad/security.py'
--- lib/canonical/launchpad/security.py 2010-09-10 16:21:21 +0000
+++ lib/canonical/launchpad/security.py 2010-09-19 21:32:44 +0000
@@ -418,6 +418,11 @@
user)
+class ViewProductReleaseFile(AnonymousAuthorization):
+ """Anyone can view an IProductReleaseFile."""
+ usedfor = IProductReleaseFile
+
+
class AdminDistributionMirrorByDistroOwnerOrMirrorAdminsOrAdmins(
AuthorizationBase):
permission = 'launchpad.Admin'
=== modified file 'lib/canonical/launchpad/tests/test_helpers.py'
--- lib/canonical/launchpad/tests/test_helpers.py 2010-08-20 20:31:18 +0000
+++ lib/canonical/launchpad/tests/test_helpers.py 2010-09-19 21:32:44 +0000
@@ -183,8 +183,8 @@
>>> from zope.app.testing.placelesssetup import setUp, tearDown
>>> from zope.app.testing import ztapi
>>> from zope.i18n.interfaces import IUserPreferredLanguages
- >>> from canonical.launchpad.interfaces import IRequestPreferredLanguages
- >>> from canonical.launchpad.interfaces import IRequestLocalLanguages
+ >>> from lp.services.geoip.interfaces import IRequestPreferredLanguages
+ >>> from lp.services.geoip.interfaces import IRequestLocalLanguages
>>> from canonical.launchpad.helpers import preferred_or_request_languages
First, test with a person who has a single preferred language.
=== modified file 'lib/canonical/launchpad/webapp/errorlog.py'
--- lib/canonical/launchpad/webapp/errorlog.py 2010-09-12 11:43:36 +0000
+++ lib/canonical/launchpad/webapp/errorlog.py 2010-09-19 21:32:44 +0000
@@ -324,7 +324,7 @@
# Check today
oopsid, filename = self.log_namer._findHighestSerialFilename(time=now)
if filename is None:
- # Check yesterday
+ # Check yesterday, we may have just passed midnight.
yesterday = now - datetime.timedelta(days=1)
oopsid, filename = self.log_namer._findHighestSerialFilename(
time=yesterday)
@@ -484,6 +484,7 @@
info.
:param now: The datetime to use as the current time. Will be
determined if not supplied. Useful for testing.
+ :return: The ErrorReport created.
"""
return self._raising(
info, request=request, now=now, informational=True)
=== modified file 'lib/canonical/launchpad/zcml/configure.zcml'
--- lib/canonical/launchpad/zcml/configure.zcml 2010-08-11 15:47:27 +0000
+++ lib/canonical/launchpad/zcml/configure.zcml 2010-09-19 21:32:44 +0000
@@ -37,7 +37,6 @@
<!-- Event configuration -->
<!-- Special Utilities -->
- <include file="geoip.zcml" />
<include file="gpghandler.zcml" />
<include file="searchservice.zcml" />
=== modified file 'lib/canonical/widgets/location.py'
--- lib/canonical/widgets/location.py 2010-07-15 10:55:27 +0000
+++ lib/canonical/widgets/location.py 2010-09-19 21:32:44 +0000
@@ -24,12 +24,12 @@
from canonical.config import config
from canonical.launchpad import _
-from canonical.launchpad.interfaces.geoip import IGeoIPRecord
-from lp.registry.interfaces.location import IObjectWithLocation
from canonical.launchpad.validators import LaunchpadValidationError
from canonical.launchpad.webapp.interfaces import (
ILaunchBag, IMultiLineWidgetLayout)
from canonical.launchpad.webapp.tales import ObjectImageDisplayAPI
+from lp.registry.interfaces.location import IObjectWithLocation
+from lp.services.geoip.interfaces import IGeoIPRecord
class ILocationWidget(IInputWidget, IBrowserWidget, IMultiLineWidgetLayout):
=== modified file 'lib/lp/answers/browser/questiontarget.py'
--- lib/lp/answers/browser/questiontarget.py 2010-09-03 13:32:42 +0000
+++ lib/lp/answers/browser/questiontarget.py 2010-09-19 21:32:44 +0000
@@ -507,22 +507,6 @@
question.sourcepackagename.name)
@property
- def ubuntu_packages(self):
- """The Ubuntu `IDistributionSourcePackage`s linked to the context.
-
- If the context is an `IProduct` and it has `IPackaging` links to
- Ubuntu, a list is returned. Otherwise None is returned
- """
- if IProduct.providedBy(self.context):
- ubuntu = getUtility(ILaunchpadCelebrities).ubuntu
- packages = [
- package for package in self.context.distrosourcepackages
- if package.distribution == ubuntu]
- if len(packages) > 0:
- return packages
- return None
-
- @property
def can_configure_answers(self):
"""Can the user configure answers for the `IQuestionTarget`."""
target = self.context
=== modified file 'lib/lp/answers/browser/tests/test_questiontarget.py'
--- lib/lp/answers/browser/tests/test_questiontarget.py 2010-09-06 09:11:43 +0000
+++ lib/lp/answers/browser/tests/test_questiontarget.py 2010-09-19 21:32:44 +0000
@@ -135,30 +135,6 @@
self.assertViewTemplate(question_set, 'question-listing.pt')
-class TestSearchQuestionsView_ubuntu_packages(TestSearchQuestionsView):
- """Test the behaviour of SearchQuestionsView.ubuntu_packages."""
-
- def test_nonproduct_ubuntu_packages(self):
- distribution = self.factory.makeDistribution()
- view = create_initialized_view(distribution, '+questions')
- packages = view.ubuntu_packages
- self.assertEqual(None, packages)
-
- def test_product_ubuntu_packages_unlinked(self):
- product = self.factory.makeProduct()
- view = create_initialized_view(product, '+questions')
- packages = view.ubuntu_packages
- self.assertEqual(None, packages)
-
- def test_product_ubuntu_packages_linked(self):
- product = self.factory.makeProduct()
- self.linkPackage(product, 'cow')
- view = create_initialized_view(product, '+questions')
- packages = view.ubuntu_packages
- self.assertEqual(1, len(packages))
- self.assertEqual('cow', packages[0].name)
-
-
class TestSearchQuestionsViewUnknown(TestSearchQuestionsView):
"""Test the behaviour of SearchQuestionsView unknown support."""
=== modified file 'lib/lp/answers/templates/unknown-support.pt'
--- lib/lp/answers/templates/unknown-support.pt 2010-08-05 17:14:05 +0000
+++ lib/lp/answers/templates/unknown-support.pt 2010-09-19 21:32:44 +0000
@@ -21,9 +21,9 @@
</p>
<p id="ubuntu-support"
- tal:define="packages view/ubuntu_packages"
+ tal:define="packages context/ubuntu_packages | nothing"
tal:condition="packages">
- <tal:project replace="context/displayname" /> questions are also
+ <tal:project replace="context/displayname" /> questions are
tracked in: <tal:packages repeat="package packages">
<tal:package replace="structure package/fmt:link" /><tal:comma
condition="not:repeat/package/end">, </tal:comma></tal:packages>.
@@ -31,10 +31,9 @@
<p id="configure-support"
tal:condition="view/can_configure_answers">
- Launchpad allows your project to track questions and create FAQs.
- <br /><a class="sprite maybe"
+ <a class="sprite maybe"
href="https://help.launchpad.net/Answers">Getting started
- with support tracking in Launchpad</a>.
+ tracking questions and FAQs in Launchpad</a>.
<br /><a tal:replace="structure context/menu:overview/configure_answers/fmt:link" />
</p>
</div>
=== modified file 'lib/lp/bugs/browser/bugtarget.py'
--- lib/lp/bugs/browser/bugtarget.py 2010-09-03 03:12:39 +0000
+++ lib/lp/bugs/browser/bugtarget.py 2010-09-19 21:32:44 +0000
@@ -1252,13 +1252,18 @@
bug_statuses_to_show.append(BugTaskStatus.FIXRELEASED)
@property
- def uses_launchpad_bugtracker(self):
- """Whether this distro or product tracks bugs in launchpad.
-
- :returns: boolean
+ def can_have_external_bugtracker(self):
+ return (IProduct.providedBy(self.context)
+ or IProductSeries.providedBy(self.context))
+
+ @property
+ def bug_tracking_usage(self):
+ """Whether the context tracks bugs in launchpad.
+
+ :returns: ServiceUsage enum value
"""
service_usage = IServiceUsage(self.context)
- return service_usage.bug_tracking_usage == ServiceUsage.LAUNCHPAD
+ return service_usage.bug_tracking_usage
@property
def external_bugtracker(self):
@@ -1278,7 +1283,7 @@
:returns: str which may contain HTML.
"""
- if self.uses_launchpad_bugtracker:
+ if self.bug_tracking_usage == ServiceUsage.LAUNCHPAD:
return 'Launchpad'
elif self.external_bugtracker:
return BugTrackerFormatterAPI(self.external_bugtracker).link(None)
=== modified file 'lib/lp/bugs/model/bugtask.py'
--- lib/lp/bugs/model/bugtask.py 2010-09-03 03:12:39 +0000
+++ lib/lp/bugs/model/bugtask.py 2010-09-19 21:32:44 +0000
@@ -159,6 +159,7 @@
from lp.soyuz.model.publishing import SourcePackagePublishingHistory
from lp.soyuz.model.sourcepackagerelease import SourcePackageRelease
+
debbugsseveritymap = {
None: BugTaskImportance.UNDECIDED,
'wishlist': BugTaskImportance.WISHLIST,
=== modified file 'lib/lp/bugs/stories/bugs/xx-front-page-info.txt'
--- lib/lp/bugs/stories/bugs/xx-front-page-info.txt 2010-05-08 15:29:56 +0000
+++ lib/lp/bugs/stories/bugs/xx-front-page-info.txt 2010-09-19 21:32:44 +0000
@@ -19,7 +19,8 @@
>>> anon_browser.open('http://bugs.launchpad.dev/test-project')
>>> uses_malone_p = find_tag_by_id(anon_browser.contents, 'no-malone')
>>> print extract_text(uses_malone_p)
- Simple Test Project does not use Launchpad for bug tracking.
+ Test-project must be configured in order for Launchpad to forward bugs to
+ the project's developers.
Only users who have permission to do so can enable bug tracking
for a project.
@@ -53,7 +54,7 @@
>>> bug_list = find_tag_by_id(
... anon_browser.contents, 'no-bugs-filed')
>>> print extract_text(bug_list)
- There are currently no bugs filed against Project Uses Malone.
+ There are currently no bugs filed against Uses-malone.
Since there are no bugs at all filed for the project, no search box is
shown.
@@ -100,7 +101,7 @@
>>> bug_list = find_tag_by_id(
... anon_browser.contents, 'no-bugs-filed')
>>> print extract_text(bug_list)
- There are currently no open bugs filed against Project Uses Malone.
+ There are currently no open bugs filed against Uses-malone.
But since the project has a bug, the search box is still visible.
@@ -112,8 +113,7 @@
Advanced search
Projects that use an external bug tracker will list the tracker on a
-bugs home page in addition to the message that the project does not
-use Launchpad for bug tracking.
+bugs home page.
>>> login('foo.bar@xxxxxxxxxxxxx')
>>> some_tracker = factory.makeBugTracker(
@@ -121,9 +121,22 @@
>>> test_project.bugtracker = some_tracker
>>> logout()
>>> anon_browser.open('http://bugs.launchpad.dev/test-project')
- >>> uses_malone_p = find_tag_by_id(anon_browser.contents, 'no-malone')
- >>> print extract_text(uses_malone_p)
- Simple Test Project does not use Launchpad for bug tracking.
>>> tracker_text = find_tag_by_id(anon_browser.contents, 'bugtracker')
>>> print extract_text(tracker_text)
Bugs are tracked in tracker.example.com/.
+
+Projects that are linked to an Ubuntu distro source package and that
+don't use Launchpad for bug tracking will inform the user that a bug can
+be reported on the project's source packages.
+
+ >>> login('foo.bar@xxxxxxxxxxxxx')
+ >>> factory.makePackagingLink(
+ ... productseries=test_project.development_focus,
+ ... sourcepackagename='test-project-package',
+ ... in_ubuntu=True)
+ >>> logout()
+ >>> anon_browser.open('http://bugs.launchpad.dev/test-project')
+ >>> print extract_text(
+ ... find_tag_by_id(anon_browser.contents, 'also-in-ubuntu'))
+ Ubuntu also tracks bugs for packages derived from this project:
+ test-project-package in ubuntu.
=== modified file 'lib/lp/bugs/templates/bugtarget-bugs.pt'
--- lib/lp/bugs/templates/bugtarget-bugs.pt 2010-08-04 11:01:15 +0000
+++ lib/lp/bugs/templates/bugtarget-bugs.pt 2010-09-19 21:32:44 +0000
@@ -10,12 +10,15 @@
i18n:domain="malone"
>
<metal:block fill-slot="head_epilogue">
+ <meta tal:condition="not: view/bug_tracking_usage/enumvalue:LAUNCHPAD"
+ name="robots" content="noindex,nofollow" />
<style type="text/css">
p#more-hot-bugs {float:right; margin-top:7px;}
</style>
</metal:block>
<body>
- <tal:side metal:fill-slot="side" condition="view/uses_launchpad_bugtracker">
+ <tal:side metal:fill-slot="side"
+ condition="view/bug_tracking_usage/enumvalue:LAUNCHPAD">
<div id="involvement" class="portlet">
<ul class="involvement">
<li style="border: none">
@@ -95,7 +98,8 @@
- <tal:uses_malone condition="view/uses_launchpad_bugtracker">
+ <tal:uses_launchpad_bugtracker
+ condition="view/bug_tracking_usage/enumvalue:LAUNCHPAD">
<tal:has_bugtasks condition="context/has_bugtasks">
<div class="search-box" style="margin-bottom:2em">
<metal:search
@@ -158,31 +162,65 @@
<tal:no_hot_bugs condition="not: view/hot_bugs_info/bugtasks">
<p id="no-bugs-filed"><strong>There are currently no
<span tal:condition="context/has_bugtasks">open</span> bugs filed
- against <tal:project_title replace="context/title" />.</strong></p>
+ against <tal:displayname replace="context/displayname" />.</strong></p>
<p id="no-bugs-report"><a href="+filebug">Report a bug.</a></p>
</tal:no_hot_bugs>
- </tal:uses_malone>
-
- <tal:not_uses_malone condition="not: view/uses_launchpad_bugtracker"
- tal:define ="configure_bugtracker context/menu:overview/configure_bugtracker | nothing">
- <p id="no-malone"><strong><tal:project_title replace="context/title" /> does not use Launchpad for
- bug tracking.</strong></p>
- <p tal:condition="view/external_bugtracker"
- id="bugtracker"><strong>Bugs are tracked in
- <tal:bugtracker replace="structure view/bugtracker" />.</strong>
- </p>
-
- <p tal:condition="context/required:launchpad.Edit"
- id="no-malone-edit"
- >
- <a tal:condition="configure_bugtracker"
- tal:replace="structure configure_bugtracker/fmt:link"/>
- <a tal:condition="not: configure_bugtracker"
- tal:attributes="href string:${context/fmt:url/+edit}">
- Enable bug tracking.</a>
- </p>
- </tal:not_uses_malone>
+ </tal:uses_launchpad_bugtracker>
+
+ <p id="no-malone"
+ tal:condition="view/bug_tracking_usage/enumvalue:UNKNOWN">
+ <strong tal:condition="view/can_have_external_bugtracker">
+ <tal:displayname replace="context/displayname" />
+ must be configured in order for Launchpad to forward bugs to
+ the project's developers.
+ </strong>
+ <strong tal:condition="not: view/can_have_external_bugtracker">
+ <tal:displayname replace="context/displayname" />
+ does not use Launchpad for bug tracking.
+ </strong>
+ </p>
+
+ <p tal:condition="view/external_bugtracker"
+ id="bugtracker">
+ <strong>Bugs are tracked in
+ <tal:bugtracker replace="structure view/bugtracker" />.
+ </strong>
+ </p>
+
+ <tal:also_in_ubuntu
+ condition="not: view/bug_tracking_usage/enumvalue:LAUNCHPAD">
+ <p tal:define="packages context/ubuntu_packages | nothing"
+ tal:condition="packages"
+ id="also-in-ubuntu">
+ Ubuntu
+ <tal:also condition="view/external_bugtracker">also</tal:also>
+ tracks bugs for packages derived from this project:
+ <tal:packages repeat="package packages">
+ <span style="white-space: nowrap"
+ tal:content="structure package/fmt:link" /><tal:comma
+ condition="not:repeat/package/end">,</tal:comma></tal:packages>.
+ </p>
+ </tal:also_in_ubuntu>
+
+ <div
+ tal:condition="not: view/bug_tracking_usage/enumvalue:LAUNCHPAD"
+ tal:define="configure_bugtracker context/menu:overview/configure_bugtracker | nothing">
+ <a class="sprite maybe"
+ href="https://help.launchpad.net/Bugs">Getting started
+ with bug tracking in Launchpad</a>.
+
+ <p tal:condition="context/required:launchpad.Edit"
+ id="no-malone-edit"
+ >
+ <a tal:condition="configure_bugtracker"
+ tal:replace="structure configure_bugtracker/fmt:link"/>
+ <a class="sprite edit"
+ tal:condition="not: configure_bugtracker"
+ tal:attributes="href string:${context/fmt:url/+edit}">
+ Enable bug tracking.</a>
+ </p>
+ </div>
</div><!-- main -->
</body>
=== modified file 'lib/lp/registry/browser/distribution.py'
--- lib/lp/registry/browser/distribution.py 2010-09-10 13:29:42 +0000
+++ lib/lp/registry/browser/distribution.py 2010-09-19 21:32:44 +0000
@@ -48,10 +48,6 @@
from canonical.launchpad.components.decoratedresultset import (
DecoratedResultSet,
)
-from canonical.launchpad.components.request_country import (
- ipaddress_from_request,
- request_country,
- )
from canonical.launchpad.helpers import english_list
from canonical.launchpad.webapp import (
action,
@@ -109,6 +105,10 @@
)
from lp.registry.interfaces.product import IProduct
from lp.registry.interfaces.series import SeriesStatus
+from lp.services.geoip.helpers import (
+ ipaddress_from_request,
+ request_country,
+ )
from lp.services.propertycache import cachedproperty
from lp.soyuz.browser.packagesearch import PackageSearchViewBase
from lp.soyuz.enums import ArchivePurpose
=== modified file 'lib/lp/registry/browser/person.py'
--- lib/lp/registry/browser/person.py 2010-09-17 00:53:33 +0000
+++ lib/lp/registry/browser/person.py 2010-09-19 21:32:44 +0000
@@ -159,7 +159,6 @@
IEmailAddress,
IEmailAddressSet,
)
-from canonical.launchpad.interfaces.geoip import IRequestPreferredLanguages
from canonical.launchpad.interfaces.gpghandler import (
GPGKeyNotFoundError,
IGPGHandler,
@@ -300,6 +299,7 @@
IWikiNameSet,
)
from lp.services.fields import LocationField
+from lp.services.geoip.interfaces import IRequestPreferredLanguages
from lp.services.openid.adapters.openid import CurrentOpenIDEndPoint
from lp.services.openid.browser.openiddiscovery import (
XRDSContentNegotiationMixin,
=== modified file 'lib/lp/registry/browser/productseries.py'
--- lib/lp/registry/browser/productseries.py 2010-09-09 18:21:55 +0000
+++ lib/lp/registry/browser/productseries.py 2010-09-19 21:32:44 +0000
@@ -280,12 +280,29 @@
usedfor = IProductSeries
facet = 'overview'
links = [
- 'edit', 'delete', 'driver', 'link_branch', 'ubuntupkg',
- 'create_milestone', 'create_release', 'rdf', 'subscribe',
+ 'configure_bugtracker',
+ 'create_milestone',
+ 'create_release',
+ 'delete',
+ 'driver',
+ 'edit',
+ 'link_branch',
+ 'rdf',
'set_branch',
+ 'subscribe',
+ 'ubuntupkg',
]
@enabled_with_permission('launchpad.Edit')
+ def configure_bugtracker(self):
+ text = 'Configure bug tracker'
+ summary = 'Specify where bugs are tracked for this project'
+ return Link(
+ canonical_url(self.context.product,
+ view_name='+configure-bugtracker'),
+ text, summary, icon='edit')
+
+ @enabled_with_permission('launchpad.Edit')
def edit(self):
"""Return a link to edit this series."""
text = 'Change details'
=== modified file 'lib/lp/registry/browser/tests/pillar-views.txt'
--- lib/lp/registry/browser/tests/pillar-views.txt 2010-09-10 13:29:42 +0000
+++ lib/lp/registry/browser/tests/pillar-views.txt 2010-09-19 21:32:44 +0000
@@ -153,35 +153,6 @@
<span class="sprite no">...
</table>
-Until other supporting code lands, the progress bar is not going to be
-shown on lpnet.
-
- >>> # Pretend that we're on launchpad.net:
- >>> from canonical.config import config
- >>> from textwrap import dedent
- >>> test_data = dedent("""
- ... [launchpad]
- ... is_lpnet: True
- ... """)
- >>> config.push('test_data', test_data)
-
-The progress bar is not shown on lpnet.
-
- >>> view = create_view(product, '+get-involved')
- >>> rendered = view.render()
- >>> print find_tag_by_id(rendered, 'progressbar')
- None
-
-Neither are the indicator sprites.
-
- >>> 'sprite' in find_tag_by_id(rendered, 'configuration_links')
- False
-
- >>> # Restore the previous config:
- >>> config_data = config.pop('test_data')
- >>> print config.launchpad.is_lpnet
- False
-
Project groups are supported too, but they only display the
applications used by their products.
=== modified file 'lib/lp/registry/doc/product.txt'
--- lib/lp/registry/doc/product.txt 2010-09-10 13:29:42 +0000
+++ lib/lp/registry/doc/product.txt 2010-09-19 21:32:44 +0000
@@ -225,6 +225,11 @@
>>> [(sp.name, sp.distribution.name) for sp in alsa.distrosourcepackages]
[(u'alsa-utils', u'debian'), (u'alsa-utils', u'ubuntu')]
+For convenience, you can get just the distro source packages for Ubuntu.
+
+ >>> [(sp.name, sp.distribution.name) for sp in alsa.ubuntu_packages]
+ [(u'alsa-utils', u'ubuntu')]
+
The date_next_suggest_packaging attribute records the date when Launchpad can
resume suggesting Ubuntu packages that the project provides. A value of None
means that no user has ever confirmed that that the project has no Ubuntu
@@ -714,6 +719,7 @@
... print series.name
trunk
+
Changing ownership
==================
=== modified file 'lib/lp/registry/interfaces/product.py'
--- lib/lp/registry/interfaces/product.py 2010-08-31 00:02:42 +0000
+++ lib/lp/registry/interfaces/product.py 2010-09-19 21:32:44 +0000
@@ -644,6 +644,9 @@
distrosourcepackages = Attribute(_("List of distribution packages for "
"this product"))
+ ubuntu_packages = Attribute(
+ _("List of distribution packages for this product in Ubuntu"))
+
series = exported(
doNotSnapshot(
CollectionField(value_type=Object(schema=IProductSeries))))
=== modified file 'lib/lp/registry/model/distributionsourcepackage.py'
--- lib/lp/registry/model/distributionsourcepackage.py 2010-08-26 08:02:08 +0000
+++ lib/lp/registry/model/distributionsourcepackage.py 2010-09-19 21:32:44 +0000
@@ -34,8 +34,6 @@
Unicode,
)
from storm.store import EmptyResultSet
-from zope.component import getUtility
-from zope.error.interfaces import IErrorReportingUtility
from zope.interface import implements
from canonical.database.sqlbase import sqlvalues
@@ -113,13 +111,6 @@
def __set__(self, obj, value):
if obj._self_in_database is None:
- # Log an oops without raising an error.
- exception = AssertionError(
- "DistributionSourcePackage record should have been created "
- "earlier in the database for distro=%s, sourcepackagename=%s"
- % (obj.distribution.name, obj.sourcepackagename.name))
- getUtility(IErrorReportingUtility).raising(
- (exception.__class__, exception, None))
spph = Store.of(obj.distribution).find(
SourcePackagePublishingHistory,
SourcePackagePublishingHistory.distroseriesID ==
=== modified file 'lib/lp/registry/model/milestone.py'
--- lib/lp/registry/model/milestone.py 2010-08-30 22:08:25 +0000
+++ lib/lp/registry/model/milestone.py 2010-09-19 21:32:44 +0000
@@ -36,6 +36,7 @@
sqlvalues,
)
from canonical.launchpad.webapp.sorting import expand_numbers
+from lazr.restful.error import expose
from lp.app.errors import NotFoundError
from lp.blueprints.model.specification import Specification
from lp.bugs.interfaces.bugtarget import IHasBugs
@@ -116,6 +117,13 @@
return result.order_by(self._milestone_order)
+class MultipleProductReleases(Exception):
+ """Raised when a second ProductRelease is created for a milestone."""
+
+ def __init__(self, msg='A milestone can only have one ProductRelease.'):
+ super(MultipleProductReleases, self).__init__(msg)
+
+
class Milestone(SQLBase, StructuralSubscriptionTargetMixin, HasBugsBase):
implements(IHasBugs, IMilestone)
@@ -201,8 +209,7 @@
changelog=None, release_notes=None):
"""See `IMilestone`."""
if self.product_release is not None:
- raise AssertionError(
- 'A milestone can only have one ProductRelease.')
+ raise expose(MultipleProductReleases())
release = ProductRelease(
owner=owner,
changelog=changelog,
=== modified file 'lib/lp/registry/model/product.py'
--- lib/lp/registry/model/product.py 2010-09-03 11:05:21 +0000
+++ lib/lp/registry/model/product.py 2010-09-19 21:32:44 +0000
@@ -17,6 +17,7 @@
import operator
from lazr.delegates import delegates
+from lazr.restful.error import expose
import pytz
from sqlobject import (
BoolCol,
@@ -48,6 +49,9 @@
SQLBase,
sqlvalues,
)
+from canonical.launchpad.components.decoratedresultset import (
+ DecoratedResultSet,
+ )
from canonical.launchpad.interfaces.launchpad import (
IHasIcon,
IHasLogo,
@@ -64,6 +68,7 @@
MAIN_STORE,
)
from canonical.launchpad.webapp.sorting import sorted_version_numbers
+
from lp.answers.interfaces.faqtarget import IFAQTarget
from lp.answers.interfaces.questioncollection import (
QUESTION_STATUS_DEFAULT_SEARCH,
@@ -146,6 +151,7 @@
from lp.registry.model.productlicense import ProductLicense
from lp.registry.model.productrelease import ProductRelease
from lp.registry.model.productseries import ProductSeries
+from lp.registry.model.sourcepackagename import SourcePackageName
from lp.registry.model.structuralsubscription import (
StructuralSubscriptionTargetMixin,
)
@@ -271,6 +277,13 @@
tables=[ProductLicense]))
+class UnDeactivateable(Exception):
+ """Raised when a project is requested to deactivate but can not."""
+
+ def __init__(self, msg):
+ super(UnDeactivateable, self).__init__(msg)
+
+
class Product(SQLBase, BugTargetBase, MakesAnnouncements,
HasSpecificationsMixin, HasSprintsMixin,
KarmaContextMixin, BranchVisibilityPolicyMixin,
@@ -440,9 +453,9 @@
# Validate deactivation.
if self.active == True and value == False:
if len(self.sourcepackages) > 0:
- raise AssertionError(
+ raise expose(UnDeactivateable(
'This project cannot be deactivated since it is '
- 'linked to source packages.')
+ 'linked to source packages.'))
return value
active = BoolCol(dbName='active', notNull=True, default=True,
@@ -829,28 +842,40 @@
(x.sourcepackagename.name, x.distroseries.name,
x.distroseries.distribution.name))
- @property
+ @cachedproperty
def distrosourcepackages(self):
- from lp.registry.model.distributionsourcepackage \
- import DistributionSourcePackage
- clause = """ProductSeries.id=Packaging.productseries AND
- ProductSeries.product = %s
- """ % sqlvalues(self.id)
- clauseTables = ['ProductSeries']
- ret = Packaging.select(clause, clauseTables,
- prejoins=["sourcepackagename", "distroseries.distribution"])
- distros = set()
- dsps = []
- for packaging in ret:
- distro = packaging.distroseries.distribution
- if distro in distros:
- continue
- distros.add(distro)
- dsps.append(DistributionSourcePackage(
- sourcepackagename=packaging.sourcepackagename,
- distribution=distro))
- return sorted(dsps, key=lambda x:
- (x.sourcepackagename.name, x.distribution.name))
+ from lp.registry.model.distributionsourcepackage import (
+ DistributionSourcePackage,
+ )
+ store = IStore(Packaging)
+ origin = [
+ Packaging,
+ Join(SourcePackageName,
+ Packaging.sourcepackagename == SourcePackageName.id),
+ Join(ProductSeries, Packaging.productseries == ProductSeries.id),
+ Join(DistroSeries, Packaging.distroseries == DistroSeries.id),
+ Join(Distribution, DistroSeries.distribution == Distribution.id),
+ ]
+ result = store.using(*origin).find(
+ (SourcePackageName, Distribution),
+ ProductSeries.product == self)
+ result = result.order_by(SourcePackageName.name, Distribution.name)
+ result.config(distinct=True)
+
+ return [
+ DistributionSourcePackage(
+ sourcepackagename=sourcepackagename,
+ distribution=distro)
+ for sourcepackagename, distro in result]
+
+ @cachedproperty
+ def ubuntu_packages(self):
+ """The Ubuntu `IDistributionSourcePackage`s linked to the `IProduct`.
+ """
+ ubuntu = getUtility(ILaunchpadCelebrities).ubuntu
+ return [
+ package for package in self.distrosourcepackages
+ if package.distribution == ubuntu]
@property
def bugtargetdisplayname(self):
=== modified file 'lib/lp/registry/stories/webservice/xx-project-registry.txt'
--- lib/lp/registry/stories/webservice/xx-project-registry.txt 2010-07-20 17:50:45 +0000
+++ lib/lp/registry/stories/webservice/xx-project-registry.txt 2010-09-19 21:32:44 +0000
@@ -568,7 +568,9 @@
>>> project_collection = webservice.named_get(
... "/projects", "search", text="Apache").jsonBody()
- >>> projects = [project['display_name'] for project in project_collection['entries']]
+ >>> projects = [
+ ... project['display_name']
+ ... for project in project_collection['entries']]
>>> for project_name in sorted(projects):
... print project_name
Derby
@@ -1016,9 +1018,9 @@
... date_released='2000-01-01T01:01:01+00:00Z',
... changelog='Added 5,000 features.')
>>> print response
- HTTP/1.1 500 Internal Server Error
+ HTTP/1.1 400 Bad Request
...
- AssertionError: A milestone can only have one ProductRelease.
+ MultipleProductReleases: A milestone can only have one ProductRelease.
Project release entries
@@ -1228,6 +1230,13 @@
>>> print_self_link_of_entries(ff_100_files)
http://.../firefox/1.0/1.0.0/+file/filename2.txt
+Anonymous users can access project release files.
+
+ >>> release_files = anon_webservice.get(
+ ... '/firefox/1.0/1.0.0/files').jsonBody()
+ >>> print_self_link_of_entries(release_files)
+ http://.../firefox/1.0/1.0.0/+file/filename2.txt
+
Commercial subscriptions
------------------------
@@ -1252,7 +1261,6 @@
>>> logout()
- >>> from lazr.restful.interfaces import IRepresentationCache
>>> ws_uncache(mmm)
>>> mmm = webservice.get("/mega-money-maker").jsonBody()
>>> print mmm['display_name']
=== modified file 'lib/lp/registry/templates/pillar-involvement-portlet.pt'
--- lib/lp/registry/templates/pillar-involvement-portlet.pt 2010-08-04 12:34:28 +0000
+++ lib/lp/registry/templates/pillar-involvement-portlet.pt 2010-09-19 21:32:44 +0000
@@ -3,7 +3,6 @@
xmlns:i18n="http://xml.zope.org/namespaces/i18n"
id="involvement" class="portlet"
tal:condition="view/has_involvement"
- tal:define="is_lpnet modules/canonical.config/config/launchpad/is_lpnet;"
>
<h2>Get Involved</h2>
@@ -35,7 +34,6 @@
<tal:editor condition="context/required:launchpad.Edit"
define="registration view/registration_completeness">
- <tal:not_lpnet condition="not:is_lpnet">
<tal:show_configuration condition="registration">
<h2>Configuration Progress</h2>
<div class="centered" id="progressbar">
@@ -54,7 +52,6 @@
</table>
</div>
</tal:show_configuration>
- </tal:not_lpnet>
<table style="width: 100%; padding-top: 1em"
tal:condition="view/configuration_links"
@@ -65,11 +62,9 @@
<a tal:replace="structure link_status/link/fmt:link" />
</td>
- <tal:not_lpnet condition="not:is_lpnet">
<td style="text-align: right;" tal:condition="registration">
<tal:yes-no replace="structure link_status/configured/image:boolean" />
</td>
- </tal:not_lpnet>
</tr>
</tal:item>
=== modified file 'lib/lp/registry/tests/test_milestone.py'
--- lib/lp/registry/tests/test_milestone.py 2010-08-20 20:31:18 +0000
+++ lib/lp/registry/tests/test_milestone.py 2010-09-19 21:32:44 +0000
@@ -15,6 +15,7 @@
logout,
)
from canonical.testing import LaunchpadFunctionalLayer
+
from lp.app.errors import NotFoundError
from lp.registry.interfaces.distribution import IDistributionSet
from lp.registry.interfaces.milestone import IMilestoneSet
@@ -79,6 +80,7 @@
all_visible_milestones_ids,
[1, 2, 3])
+
def test_suite():
"""Return the test suite for the tests in this module."""
return unittest.TestLoader().loadTestsFromName(__name__)
=== modified file 'lib/lp/registry/tests/test_product.py'
--- lib/lp/registry/tests/test_product.py 2010-09-03 06:06:40 +0000
+++ lib/lp/registry/tests/test_product.py 2010-09-19 21:32:44 +0000
@@ -28,7 +28,7 @@
License,
)
from lp.registry.model.commercialsubscription import CommercialSubscription
-from lp.registry.model.product import Product
+from lp.registry.model.product import Product, UnDeactivateable
from lp.registry.model.productlicense import ProductLicense
from lp.testing import TestCaseWithFactory
@@ -48,7 +48,7 @@
source_package.setPackaging(
product.development_focus, self.factory.makePerson())
self.assertRaises(
- AssertionError,
+ UnDeactivateable,
setattr, product, 'active', False)
def test_deactivation_success(self):
=== modified file 'lib/lp/registry/tests/test_project.py'
--- lib/lp/registry/tests/test_project.py 2010-08-20 20:31:18 +0000
+++ lib/lp/registry/tests/test_project.py 2010-09-19 21:32:44 +0000
@@ -6,11 +6,22 @@
import unittest
from zope.component import getUtility
+from lazr.restfulclient.errors import ClientError
+from canonical.launchpad.testing.pages import LaunchpadWebServiceCaller
from canonical.launchpad.ftests import login
-from canonical.testing import LaunchpadFunctionalLayer
+from canonical.launchpad.webapp.errorlog import globalErrorUtility
+from canonical.testing import (
+ LaunchpadFunctionalLayer,
+ DatabaseFunctionalLayer,
+ )
from lp.registry.interfaces.projectgroup import IProjectGroupSet
-from lp.testing import TestCaseWithFactory
+from lp.soyuz.enums import ArchivePurpose
+from lp.testing import (
+ celebrity_logged_in,
+ launchpadlib_for,
+ TestCaseWithFactory,
+ )
class ProjectGroupSearchTestCase(TestCaseWithFactory):
@@ -99,5 +110,27 @@
self.assertEqual(self.project3, results[0])
+class TestLaunchpadlibAPI(TestCaseWithFactory):
+ layer = DatabaseFunctionalLayer
+
+ def test_inappropriate_deactivation_does_not_cause_an_OOPS(self):
+ # Make sure a 400 error and not an OOPS is returned when a ValueError
+ # is raised when trying to deactivate a project that has source
+ # releases.
+ last_oops = globalErrorUtility.getLastOopsReport()
+ launchpad = launchpadlib_for("test", "salgado", "WRITE_PUBLIC")
+
+ project = launchpad.projects['evolution']
+ project.active = False
+ e = self.assertRaises(ClientError, project.lp_save)
+
+ # no OOPS was generated as a result of the exception
+ self.assertNoNewOops(last_oops)
+ self.assertEqual(400, e.response.status)
+ self.assertIn(
+ 'This project cannot be deactivated since it is linked to source '
+ 'packages.', e.content)
+
+
def test_suite():
return unittest.TestLoader().loadTestsFromName(__name__)
=== modified file 'lib/lp/registry/tests/test_project_milestone.py'
--- lib/lp/registry/tests/test_project_milestone.py 2010-08-20 20:31:18 +0000
+++ lib/lp/registry/tests/test_project_milestone.py 2010-09-19 21:32:44 +0000
@@ -10,12 +10,19 @@
from storm.store import Store
from zope.component import getUtility
+import pytz
from canonical.launchpad.ftests import (
login,
syncUpdate,
)
-from canonical.testing import LaunchpadFunctionalLayer
+from canonical.launchpad.webapp.errorlog import globalErrorUtility
+from canonical.testing import (
+ LaunchpadFunctionalLayer,
+ DatabaseFunctionalLayer,
+ )
+from lazr.restfulclient.errors import ClientError
+
from lp.blueprints.interfaces.specification import (
ISpecificationSet,
SpecificationDefinitionStatus,
@@ -30,6 +37,11 @@
from lp.registry.interfaces.person import IPersonSet
from lp.registry.interfaces.product import IProductSet
from lp.registry.interfaces.projectgroup import IProjectGroupSet
+from lp.registry.model.milestone import MultipleProductReleases
+from lp.testing import (
+ launchpadlib_for,
+ TestCaseWithFactory,
+ )
class ProjectMilestoneTest(unittest.TestCase):
@@ -197,7 +209,7 @@
spec = specset.new(
name='%s-specification' % product_name,
title='Title %s specification' % product_name,
- specurl='http://www.example.com/spec/%s' %product_name ,
+ specurl='http://www.example.com/spec/%s' % product_name,
summary='summary',
definition_status=SpecificationDefinitionStatus.APPROVED,
priority=SpecificationPriority.HIGH,
@@ -317,6 +329,48 @@
self._createProductSeriesBugtask('evolution', 'trunk', '1.1')
+class TestDuplicateProductReleases(TestCaseWithFactory):
+ layer = DatabaseFunctionalLayer
+
+ def test_inappropriate_release_raises(self):
+ # A milestone that already has a ProductRelease can not be given
+ # another one.
+ login('foo.bar@xxxxxxxxxxxxx')
+ product_set = getUtility(IProductSet)
+ product = product_set['evolution']
+ series = product.getSeries('trunk')
+ milestone = series.newMilestone(name='1.1', dateexpected=None)
+ now = datetime.now(pytz.UTC)
+ milestone.createProductRelease(1, now)
+ self.assertRaises(MultipleProductReleases,
+ milestone.createProductRelease, 1, now)
+ try:
+ milestone.createProductRelease(1, now)
+ except MultipleProductReleases, e:
+ self.assert_(
+ str(e), 'A milestone can only have one ProductRelease.')
+
+ def test_inappropriate_deactivation_does_not_cause_an_OOPS(self):
+ # Make sure a 400 error and not an OOPS is returned when an exception
+ # is raised when trying to create a product release when a milestone
+ # already has one.
+ last_oops = globalErrorUtility.getLastOopsReport()
+ launchpad = launchpadlib_for("test", "salgado", "WRITE_PUBLIC")
+
+ project = launchpad.projects['evolution']
+ milestone = project.getMilestone(name='2.1.6')
+ now = datetime.now(pytz.UTC)
+
+ e = self.assertRaises(
+ ClientError, milestone.createProductRelease, date_released=now)
+
+ # no OOPS was generated as a result of the exception
+ self.assertNoNewOops(last_oops)
+ self.assertEqual(400, e.response.status)
+ self.assertIn(
+ 'A milestone can only have one ProductRelease.', e.content)
+
+
def test_suite():
"""Return the test suite for the tests in this module."""
return unittest.TestLoader().loadTestsFromName(__name__)
=== modified file 'lib/lp/services/apachelogparser/base.py'
--- lib/lp/services/apachelogparser/base.py 2010-08-30 17:12:59 +0000
+++ lib/lp/services/apachelogparser/base.py 2010-09-19 21:32:44 +0000
@@ -11,13 +11,13 @@
from zope.component import getUtility
from canonical.config import config
-from canonical.launchpad.interfaces.geoip import IGeoIP
from canonical.launchpad.webapp.interfaces import (
DEFAULT_FLAVOR,
IStoreSelector,
MAIN_STORE,
)
from lp.services.apachelogparser.model.parsedapachelog import ParsedApacheLog
+from lp.services.geoip.interfaces import IGeoIP
parser = apachelog.parser(apachelog.formats['extended'])
=== modified file 'lib/lp/services/configure.zcml'
--- lib/lp/services/configure.zcml 2010-08-20 12:11:28 +0000
+++ lib/lp/services/configure.zcml 2010-09-19 21:32:44 +0000
@@ -6,8 +6,11 @@
<adapter factory=".propertycache.get_default_cache"/>
<adapter factory=".propertycache.PropertyCacheManager"/>
<adapter factory=".propertycache.DefaultPropertyCacheManager"/>
+ <include package=".comments" />
<include package=".database" />
<include package=".features" />
+ <include package=".fields" />
+ <include package=".geoip" />
<include package=".inlinehelp" file="meta.zcml" />
<include package=".job" />
<include package=".memcache" />
=== added directory 'lib/lp/services/geoip'
=== added file 'lib/lp/services/geoip/__init__.py'
--- lib/lp/services/geoip/__init__.py 1970-01-01 00:00:00 +0000
+++ lib/lp/services/geoip/__init__.py 2010-09-19 21:32:44 +0000
@@ -0,0 +1,3 @@
+# Copyright 2010 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+"""Launchpad integration of the GEOIP library."""
=== renamed file 'lib/canonical/launchpad/zcml/geoip.zcml' => 'lib/lp/services/geoip/configure.zcml'
--- lib/canonical/launchpad/zcml/geoip.zcml 2009-07-13 18:15:02 +0000
+++ lib/lp/services/geoip/configure.zcml 2010-09-19 21:32:44 +0000
@@ -1,4 +1,4 @@
-<!-- Copyright 2009 Canonical Ltd. This software is licensed under the
+<!-- Copyright 2009-2010 Canonical Ltd. This software is licensed under the
GNU Affero General Public License version 3 (see the file LICENSE).
-->
@@ -9,34 +9,34 @@
xmlns:zope="http://namespaces.zope.org/zope"
i18n_domain="launchpad">
- <class class="canonical.launchpad.utilities.geoip.GeoIP">
- <allow interface="canonical.launchpad.interfaces.IGeoIP" />
+ <class class="lp.services.geoip.model.GeoIP">
+ <allow interface="lp.services.geoip.interfaces.IGeoIP" />
</class>
<securedutility
- class="canonical.launchpad.utilities.geoip.GeoIP"
- provides="canonical.launchpad.interfaces.IGeoIP">
- <allow interface="canonical.launchpad.interfaces.IGeoIP" />
+ class="lp.services.geoip.model.GeoIP"
+ provides="lp.services.geoip.interfaces.IGeoIP">
+ <allow interface="lp.services.geoip.interfaces.IGeoIP" />
</securedutility>
<adapter
for="zope.publisher.interfaces.browser.IBrowserRequest"
- factory="canonical.launchpad.utilities.geoip.RequestLocalLanguages"
- provides="canonical.launchpad.interfaces.IRequestLocalLanguages" />
-
- <adapter
- for="zope.publisher.interfaces.browser.IBrowserRequest"
- factory="canonical.launchpad.utilities.geoip.RequestPreferredLanguages"
- provides="canonical.launchpad.interfaces.IRequestPreferredLanguages" />
-
- <adapter
- for="zope.publisher.interfaces.browser.IBrowserRequest"
- factory="canonical.launchpad.components.request_country.request_country"
- provides="canonical.launchpad.interfaces.ICountry" />
-
- <adapter
- for="zope.publisher.interfaces.browser.IBrowserRequest"
- factory="canonical.launchpad.utilities.geoip.GeoIPRequest"
- provides="canonical.launchpad.interfaces.IGeoIPRecord" />
+ factory="lp.services.geoip.model.RequestLocalLanguages"
+ provides="lp.services.geoip.interfaces.IRequestLocalLanguages" />
+
+ <adapter
+ for="zope.publisher.interfaces.browser.IBrowserRequest"
+ factory="lp.services.geoip.model.RequestPreferredLanguages"
+ provides="lp.services.geoip.interfaces.IRequestPreferredLanguages" />
+
+ <adapter
+ for="zope.publisher.interfaces.browser.IBrowserRequest"
+ factory="lp.services.geoip.helpers.request_country"
+ provides="lp.services.worlddata.interfaces.country.ICountry" />
+
+ <adapter
+ for="zope.publisher.interfaces.browser.IBrowserRequest"
+ factory="lp.services.geoip.model.GeoIPRequest"
+ provides="lp.services.geoip.interfaces.IGeoIPRecord" />
</configure>
=== added directory 'lib/lp/services/geoip/doc'
=== renamed file 'lib/canonical/launchpad/doc/geoip.txt' => 'lib/lp/services/geoip/doc/geoip.txt'
--- lib/canonical/launchpad/doc/geoip.txt 2009-04-17 10:32:16 +0000
+++ lib/lp/services/geoip/doc/geoip.txt 2010-09-19 21:32:44 +0000
@@ -1,10 +1,11 @@
-= GeoIP =
+GeoIP
+=====
GeoIP allows us to guess the location of a user based on his IP address.
Our IGeoIP utility provides a couple methods to get location information
from a given IP address.
- >>> from canonical.launchpad.interfaces.geoip import IGeoIP
+ >>> from lp.services.geoip.interfaces import IGeoIP
>>> geoip = getUtility(IGeoIP)
The getCountryByAddr() method will return the country of the given IP
@@ -80,14 +81,13 @@
request's originating IP address.
>>> from canonical.launchpad.webapp.servers import LaunchpadTestRequest
- >>> from canonical.launchpad.interfaces.geoip import IGeoIPRecord
+ >>> from lp.services.geoip.interfaces import IGeoIPRecord
>>> request = LaunchpadTestRequest()
Since our request won't have an originating IP address, we'll use that
same South African IP address.
- >>> from canonical.launchpad.components.request_country import (
- ... ipaddress_from_request)
+ >>> from lp.services.geoip.helpers import ipaddress_from_request
>>> print ipaddress_from_request(request)
None
>>> geoip_request = IGeoIPRecord(request)
=== renamed file 'lib/canonical/launchpad/components/request_country.py' => 'lib/lp/services/geoip/helpers.py'
--- lib/canonical/launchpad/components/request_country.py 2010-08-20 20:31:18 +0000
+++ lib/lp/services/geoip/helpers.py 2010-09-19 21:32:44 +0000
@@ -7,10 +7,14 @@
from zope.component import getUtility
-from canonical.launchpad.interfaces import IGeoIP
-
-
-__all__ = ['request_country', 'ipaddress_from_request']
+from lp.services.geoip.interfaces import IGeoIP
+
+
+__all__ = [
+ 'request_country',
+ 'ipaddress_from_request',
+ ]
+
def request_country(request):
"""Adapt a request to the country in which the request was made.
@@ -26,8 +30,10 @@
return getUtility(IGeoIP).getCountryByAddr(ipaddress)
return None
+
_ipaddr_re = re.compile('\d\d?\d?\.\d\d?\d?\.\d\d?\d?\.\d\d?\d?')
+
def ipaddress_from_request(request):
"""Determine the IP address for this request.
@@ -67,8 +73,7 @@
ipaddresses = [
addr for addr in ipaddresses
if not (addr.startswith('127.')
- or _ipaddr_re.search(addr) is None)
- ]
+ or _ipaddr_re.search(addr) is None)]
if ipaddresses:
# If we have more than one, have a guess.
=== renamed file 'lib/canonical/launchpad/interfaces/geoip.py' => 'lib/lp/services/geoip/interfaces.py'
=== renamed file 'lib/canonical/launchpad/utilities/geoip.py' => 'lib/lp/services/geoip/model.py'
--- lib/canonical/launchpad/utilities/geoip.py 2010-08-24 10:45:57 +0000
+++ lib/lp/services/geoip/model.py 2010-09-19 21:32:44 +0000
@@ -16,10 +16,8 @@
from zope.interface import implements
from canonical.config import config
-from canonical.launchpad.components.request_country import (
- ipaddress_from_request,
- )
-from canonical.launchpad.interfaces.geoip import (
+from lp.services.geoip.helpers import ipaddress_from_request
+from lp.services.geoip.interfaces import (
IGeoIP,
IGeoIPRecord,
IRequestLocalLanguages,
@@ -144,7 +142,7 @@
codes = IUserPreferredLanguages(self.request).getPreferredLanguages()
languageset = getUtility(ILanguageSet)
- languages = []
+ languages = set()
for code in codes:
# We need to ensure that the code received contains only ASCII
@@ -164,7 +162,7 @@
continue
code = languageset.canonicalise_language_code(code)
try:
- languages.append(languageset[code])
+ languages.add(languageset[code])
except KeyError:
pass
=== added directory 'lib/lp/services/geoip/tests'
=== added file 'lib/lp/services/geoip/tests/__init__.py'
=== renamed file 'lib/canonical/launchpad/components/tests/test_request_country.py' => 'lib/lp/services/geoip/tests/test_doc.py'
--- lib/canonical/launchpad/components/tests/test_request_country.py 2010-08-20 20:31:18 +0000
+++ lib/lp/services/geoip/tests/test_doc.py 2010-09-19 21:32:44 +0000
@@ -1,19 +1,23 @@
-# Copyright 2009 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2010 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
-"""Module docstring goes here."""
+"""Test GEOIP documentation."""
__metaclass__ = type
+import os
from doctest import DocTestSuite
-import unittest
+
+from canonical.testing.layers import LaunchpadFunctionalLayer
+from lp.services.testing import build_test_suite
+
+
+here = os.path.dirname(os.path.realpath(__file__))
def test_suite():
- import canonical.launchpad.components.request_country
- return DocTestSuite(canonical.launchpad.components.request_country)
-
-if __name__ == '__main__':
- DEFAULT = test_suite()
- unittest.main(defaultTest='DEFAULT')
-
+ import lp.services.geoip.helpers
+ inline_doctest = DocTestSuite(lp.services.geoip.helpers)
+ suite = build_test_suite(here, {}, layer=LaunchpadFunctionalLayer)
+ suite.addTest(inline_doctest)
+ return suite
=== renamed file 'lib/canonical/launchpad/components/ftests/test_request_country.py' => 'lib/lp/services/geoip/tests/test_request_country.py'
--- lib/canonical/launchpad/components/ftests/test_request_country.py 2010-08-20 20:31:18 +0000
+++ lib/lp/services/geoip/tests/test_request_country.py 2010-09-19 21:32:44 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2010 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Functional tests for request_country"""
@@ -6,12 +6,12 @@
import unittest
-from canonical.launchpad.components.request_country import request_country
from canonical.launchpad.ftests import (
ANONYMOUS,
login,
logout,
)
+from lp.services.geoip.helpers import request_country
from canonical.testing import LaunchpadFunctionalLayer
=== modified file 'lib/lp/services/job/tests/test_runner.py'
--- lib/lp/services/job/tests/test_runner.py 2010-08-20 20:31:18 +0000
+++ lib/lp/services/job/tests/test_runner.py 2010-09-19 21:32:44 +0000
@@ -149,7 +149,6 @@
def test_runAll_skips_lease_failures(self):
"""Ensure runAll skips jobs whose leases can't be acquired."""
- last_oops = errorlog.globalErrorUtility.getLastOopsReport()
job_1, job_2 = self.makeTwoJobs()
job_2.job.acquireLease()
runner = JobRunner([job_1, job_2])
@@ -158,8 +157,7 @@
self.assertEqual(JobStatus.WAITING, job_2.job.status)
self.assertEqual([job_1], runner.completed_jobs)
self.assertEqual([job_2], runner.incomplete_jobs)
- new_last_oops = errorlog.globalErrorUtility.getLastOopsReport()
- self.assertEqual(last_oops.id, new_last_oops.id)
+ self.assertEqual([], self.oopses)
def test_runAll_reports_oopses(self):
"""When an error is encountered, report an oops and continue."""
@@ -177,7 +175,7 @@
self.assertEqual(JobStatus.FAILED, job_1.job.status)
self.assertEqual(JobStatus.COMPLETED, job_2.job.status)
reporter = errorlog.globalErrorUtility
- oops = reporter.getLastOopsReport()
+ oops = self.oopses[-1]
self.assertIn('Fake exception. Foobar, I say!', oops.tb_text)
self.assertEqual(1, len(oops.req_vars))
self.assertEqual("{'foo': 'bar'}", oops.req_vars[0][1])
@@ -195,7 +193,7 @@
runner = JobRunner([job_1, job_2])
runner.runAll()
reporter = getUtility(IErrorReportingUtility)
- oops = reporter.getLastOopsReport()
+ oops = self.oopses[-1]
self.assertEqual(1, len(oops.req_vars))
self.assertEqual("{'foo': 'bar'}", oops.req_vars[0][1])
@@ -231,7 +229,7 @@
runner.runAll()
(notification,) = pop_notifications()
reporter = errorlog.globalErrorUtility
- oops = reporter.getLastOopsReport()
+ oops = self.oopses[-1]
self.assertIn(
'Launchpad encountered an internal error during the following'
' operation: appending a string to a list. It was logged with id'
@@ -257,10 +255,8 @@
job_1.user_error_types = (ExampleError,)
job_1.error_recipients = ['jrandom@xxxxxxxxxxx']
runner = JobRunner([job_1, job_2])
- reporter = errorlog.globalErrorUtility
- old_oops = reporter.getLastOopsReport()
runner.runAll()
- self.assertNoNewOops(old_oops)
+ self.assertEqual([], self.oopses)
notifications = pop_notifications()
self.assertEqual(1, len(notifications))
body = notifications[0].get_payload(decode=True)
=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py 2010-09-09 20:12:02 +0000
+++ lib/lp/testing/factory.py 2010-09-19 21:32:44 +0000
@@ -182,6 +182,10 @@
from lp.registry.interfaces.mailinglistsubscription import (
MailingListAutoSubscribePolicy,
)
+from lp.registry.interfaces.packaging import (
+ IPackagingUtil,
+ PackagingType,
+ )
from lp.registry.interfaces.person import (
IPerson,
IPersonSet,
@@ -993,6 +997,29 @@
removeSecurityProxy(branch).reviewer = reviewer
return branch
+ def makePackagingLink(self, productseries=None, sourcepackagename=None,
+ distroseries=None, packaging_type=None, owner=None,
+ in_ubuntu=False):
+ if productseries is None:
+ productseries = self.makeProduct().development_focus
+ if sourcepackagename is None or isinstance(sourcepackagename, str):
+ sourcepackagename = self.makeSourcePackageName(sourcepackagename)
+ if distroseries is None:
+ distribution = None
+ if in_ubuntu:
+ distribution = getUtility(ILaunchpadCelebrities).ubuntu
+ distroseries = self.makeDistroSeries(distribution=distribution)
+ if packaging_type is None:
+ packaging_type = PackagingType.PRIME
+ if owner is None:
+ owner = self.makePerson()
+ return getUtility(IPackagingUtil).createPackaging(
+ productseries=productseries,
+ sourcepackagename=sourcepackagename,
+ distroseries=distroseries,
+ packaging=packaging_type,
+ owner=owner)
+
def makePackageBranch(self, sourcepackage=None, distroseries=None,
sourcepackagename=None, **kwargs):
"""Make a package branch on an arbitrary package.
=== modified file 'lib/lp/testing/menu.py'
--- lib/lp/testing/menu.py 2010-09-12 11:43:36 +0000
+++ lib/lp/testing/menu.py 2010-09-19 21:32:44 +0000
@@ -11,7 +11,7 @@
def check_menu_links(menu):
context = menu.context
for link in menu.iterlinks():
- if link.target.startswith('/'):
+ if link.target.startswith(('/', 'http://')):
# The context is not the context of this target.
continue
if '?' in link.target:
=== modified file 'lib/lp/translations/browser/translations.py'
--- lib/lp/translations/browser/translations.py 2010-08-24 10:45:57 +0000
+++ lib/lp/translations/browser/translations.py 2010-09-19 21:32:44 +0000
@@ -19,7 +19,6 @@
from canonical.config import config
from canonical.launchpad import helpers
-from canonical.launchpad.interfaces.geoip import IRequestPreferredLanguages
from canonical.launchpad.interfaces.launchpad import (
ILaunchpadCelebrities,
IRosettaApplication,
@@ -35,6 +34,7 @@
from canonical.launchpad.webapp.interfaces import ILaunchpadRoot
from lp.registry.interfaces.person import IPersonSet
from lp.registry.interfaces.product import IProductSet
+from lp.services.geoip.interfaces import IRequestPreferredLanguages
from lp.services.propertycache import cachedproperty
from lp.services.worlddata.interfaces.country import ICountry
from lp.translations.publisher import TranslationsLayer
=== modified file 'lib/lp/translations/doc/preferred-languages.txt'
--- lib/lp/translations/doc/preferred-languages.txt 2009-10-22 11:55:51 +0000
+++ lib/lp/translations/doc/preferred-languages.txt 2010-09-19 21:32:44 +0000
@@ -3,17 +3,27 @@
in ASCII we just skip them.
>>> from canonical.launchpad.webapp.servers import LaunchpadTestRequest
- >>> from canonical.launchpad.utilities.geoip import (
- ... RequestPreferredLanguages)
+ >>> from lp.services.geoip.model import RequestPreferredLanguages
>>> langs = {'HTTP_ACCEPT_LANGUAGE': 'pt_BR, Espa\xf1ol'}
>>> request = LaunchpadTestRequest(**langs)
>>> [l.code
... for l in RequestPreferredLanguages(request).getPreferredLanguages()]
[u'pt_BR']
-
+
>>> langs = {'HTTP_ACCEPT_LANGUAGE': u'pt_BR, Espa\xf1ol'}
>>> request = LaunchpadTestRequest(**langs)
>>> [l.code
... for l in RequestPreferredLanguages(request).getPreferredLanguages()]
[u'pt_BR']
+
+The getPreferredLanguages() method returns unique codes.
+
+ >>> langs = {
+ ... 'HTTP_ACCEPT_LANGUAGE': 'en-US,en;q=0.9,de-CH;q=0.8,de;q=0.6,'
+ ... 'en-GB;q=0.4,en-us;q=0.3,en;q=0.1'
+ ... }
+ >>> request = LaunchpadTestRequest(**langs)
+ >>> [l.code
+ ... for l in RequestPreferredLanguages(request).getPreferredLanguages()]
+ [u'en', u'en_GB', u'de']
=== modified file 'utilities/rocketfuel-mp-status'
--- utilities/rocketfuel-mp-status 2010-07-16 10:14:48 +0000
+++ utilities/rocketfuel-mp-status 2010-09-19 21:32:44 +0000
@@ -11,8 +11,11 @@
'. ~/.rocketfuel-env.sh && echo $LP_TRUNK_NAME')
projpath = commands.getoutput(
'. ~/.rocketfuel-env.sh && echo $LP_PROJECT_PATH')
-os.chdir(projpath)
-branches = os.listdir('.')
+if len(sys.argv) > 1:
+ branches = sys.argv[1:]
+else:
+ os.chdir(projpath)
+ branches = os.listdir('.')
lp_branches = launchpad.branches
for lb in branches:
=== modified file 'versions.cfg'
--- versions.cfg 2010-09-07 08:17:36 +0000
+++ versions.cfg 2010-09-19 21:32:44 +0000
@@ -31,7 +31,7 @@
lazr.delegates = 1.2.0
lazr.enum = 1.1.2
lazr.lifecycle = 1.1
-lazr.restful = 0.11.3
+lazr.restful = 0.13.0
lazr.restfulclient = 0.10.0
lazr.smtptest = 1.1
lazr.testing = 0.1.1