launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #00654
[Merge] lp:~jtv/launchpad/bug-617431 into lp:~launchpad-pqm/launchpad/production-devel
Jeroen T. Vermeulen has proposed merging lp:~jtv/launchpad/bug-617431 into lp:~launchpad-pqm/launchpad/production-devel.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers): code
Related bugs:
#617431 Can't download all .mo files as a single tarball
https://bugs.launchpad.net/bugs/617431
= Bug 617431 =
I'm hoping to get this cherry-picked, so proposing for production-devel.
Fixes breakage in the MO translations format exporter. The formater is supposed to convert translations (PO files) to MO format using /usr/bin/msgfmt, but pass through templates (POT files) in their original form. A silly mis-spelled attrivute in the latter bit of code, tragically untested, broke this. For the interested: "mime_type" vs. "content_type."
I'm adding a test for the template case, as well as what I hope is some proper verification of the MO output. There's also a doctest, but I didn't want to add to that.
No lint introduced. To test:
{{{
/bin/test -vvc -m lp.translations.utilities.tests -t export
}}}
To Q/A: request a full translations export of Gufw trunk, in MO format, and wait for the email to come in.
Jeroen
--
The attached diff has been truncated due to its size.
https://code.launchpad.net/~jtv/launchpad/bug-617431/+merge/32986
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~jtv/launchpad/bug-617431 into lp:~launchpad-pqm/launchpad/production-devel.
=== modified file '.bzrignore'
--- .bzrignore 2010-07-15 15:57:40 +0000
+++ .bzrignore 2010-08-18 11:41:24 +0000
@@ -56,7 +56,6 @@
.bazaar
.cache
.subversion
-lib/canonical/buildd/launchpad-files
.testrepository
.memcache.pid
./pipes
@@ -77,3 +76,4 @@
lib/canonical/launchpad-buildd_*_source.build
lib/canonical/launchpad-buildd_*_source.changes
lib/canonical/buildd/debian/*
+lib/canonical/buildd/launchpad-files/*
=== modified file 'README'
--- README 2010-04-06 20:07:30 +0000
+++ README 2010-08-18 11:41:24 +0000
@@ -1,4 +1,106 @@
-This is the top level project, that supplies the infrastructure for testing,
-and running launchpad.
-
-Documentation is in the doc directory or on the wiki.
+====================
+README for Launchpad
+====================
+
+Launchpad is an open source suite of tools that help people and teams to work
+together on software projects. Unlike many open source projects, Launchpad
+isn't something you install and run yourself (although you are welcome to do
+so), instead, contributors help make <https://launchpad.net> better.
+
+Launchpad is a project of Canonical <http://www.canonical.com> and has
+received many contributions from many wonderful people
+<https://dev.launchpad.net/Contributions>.
+
+If you want help using Launchpad, then please visit our help wiki at:
+
+ https://help.launchpad.net
+
+If you'd like to contribute to Launchpad, have a look at:
+
+ https://dev.launchpad.net
+
+Alternatively, have a poke around in the code, which you probably already know
+how to get if you are reading this file.
+
+
+Getting started
+===============
+
+There's a full guide for getting up-and-running with a development Launchpad
+environment at <https://dev.launchpad.net/Getting>. When you are ready to
+submit a patch, please consult <https://dev.launchpad.net/PatchSubmission>.
+
+Our bug tracker is at <https://bugs.launchpad.net/launchpad/> and you can get
+the source code any time by doing:
+
+ $ bzr branch lp:launchpad
+
+
+Navigating the tree
+-------------------
+
+The Launchpad tree is big, messy and changing. Sorry about that. Don't panic
+though, it can sense fear. Keep a firm grip on `grep` and pay attention to
+these important top-level folders:
+
+ bin/, utilities/
+ Where you will find scripts intended for developers and admins. There's
+ no rhyme or reason to what goes in bin/ and what goes in utilities/, so
+ take a look in both. bin/ will be empty in a fresh checkout, the actual
+ content lives in 'buildout-templates'.
+
+ configs/
+ Configuration files for various kinds of Launchpad instances.
+ 'development' and 'testrunner' are of particular interest to developers.
+
+ cronscripts/
+ Scripts that are run on actual production instances of Launchpad as
+ cronjobs.
+
+ daemons/
+ Entry points for various daemons that form part of Launchpad
+
+ database/
+ Our database schema, our sample data, and some other stuff that causes
+ fear.
+
+ doc/
+ General system-wide documentation. You can also find documentation on
+ <https://dev.launchpad.net>, in docstrings and in doctests.
+
+ lib/
+ Where the vast majority of the code lives, along with our templates, tests
+ and the bits of our documentation that are written as doctests. 'lp' and
+ 'canonical' are the two most interesting packages. Note that 'canonical'
+ is deprecated in favour of 'lp'. To learn more about how the 'lp' package
+ is laid out, take a look at its docstring.
+
+ Makefile
+ Ahh, bliss. The Makefile has all sorts of goodies. If you spend any
+ length of time hacking on Launchpad, you'll use it often. The most
+ important targets are 'make clean', 'make compile', 'make schema', 'make
+ run' and 'make run_all'.
+
+ scripts/
+ Scripts that are run on actual production instances of Launchpad,
+ generally triggered by some automatic process.
+
+
+You can spend years hacking on Launchpad full-time and not know what all of
+the files in the top-level directory are for. However, here's a guide to some
+of the ones that come up from time to time.
+
+ buildout-templates/
+ Templates that are generated into actual files, normally bin/ scripts,
+ when buildout is run. If you want to change the behaviour of bin/test,
+ look here.
+
+ bzrplugins/, optionalbzrplugins/
+ Bazaar plugins used in running Launchpad.
+
+ sourcecode/
+ A directory into which we symlink branches of some of Launchpad's
+ dependencies. Don't ask.
+
+You never have to care about 'benchmarks', 'override-includes' or
+'package-includes'.
=== modified file 'buildout-templates/bin/test.in'
--- buildout-templates/bin/test.in 2010-07-30 11:52:48 +0000
+++ buildout-templates/bin/test.in 2010-08-18 11:41:24 +0000
@@ -45,15 +45,6 @@
BUILD_DIR = ${buildout:directory|path-repr}
CUSTOM_SITE_DIR = ${scripts:parts-directory|path-repr}
-if os.getsid(0) == os.getsid(os.getppid()):
- # We need to become the process group leader so test_on_merge.py
- # can reap its children.
- #
- # Note that if setpgrp() is used to move a process from one
- # process group to another (as is done by some shells when
- # creating pipelines), then both process groups must be part of
- # the same session.
- os.setpgrp()
# Make tests run in a timezone no launchpad developers live in.
# Our tests need to run in any timezone.
=== modified file 'configs/development/apidoc-configure-normal.zcml'
--- configs/development/apidoc-configure-normal.zcml 2010-07-24 02:27:19 +0000
+++ configs/development/apidoc-configure-normal.zcml 2010-08-18 11:41:24 +0000
@@ -90,7 +90,6 @@
<apidoc:rootModule module="martian" />
<apidoc:rootModule module="manuel" />
<apidoc:rootModule module="chameleon" />
- <apidoc:rootModule module="bzrlib" />
<apidoc:rootModule module="storm" />
<apidoc:bookchapter
=== modified file 'database/schema/security.cfg'
--- database/schema/security.cfg 2010-08-10 05:49:16 +0000
+++ database/schema/security.cfg 2010-08-18 11:41:24 +0000
@@ -771,7 +771,7 @@
public.gpgkey = SELECT, INSERT, UPDATE
public.packagecopyrequest = SELECT, INSERT, UPDATE
public.packagediff = SELECT, INSERT, UPDATE
-public.packageset = SELECT
+public.packageset = SELECT, INSERT
public.packagesetgroup = SELECT
public.packagesetsources = SELECT, INSERT, UPDATE, DELETE
public.packagesetinclusion = SELECT, INSERT, UPDATE, DELETE
@@ -1053,7 +1053,10 @@
[uploader]
type=user
-groups=script
+groups=script,uploading
+
+[uploading]
+type=group
# Everything is keyed off an archive
public.archive = SELECT, INSERT, UPDATE
public.archivearch = SELECT, INSERT, UPDATE
=== renamed file 'daemons/buildd-slave-example.conf' => 'lib/canonical/buildd/buildd-slave-example.conf'
=== modified file 'lib/canonical/buildd/buildrecipe'
--- lib/canonical/buildd/buildrecipe 2010-07-08 20:18:39 +0000
+++ lib/canonical/buildd/buildrecipe 2010-08-18 11:41:24 +0000
@@ -10,6 +10,7 @@
import os
import os.path
import pwd
+import socket
from subprocess import call
import sys
@@ -155,6 +156,11 @@
os.environ["HOME"], "build-" + build_id, *extra)
if __name__ == '__main__':
+ # As of bzr 2.2, a defined identity is needed. In this case, we're using
+ # buildd@<hostname>.
+ hostname = socket.gethostname()
+ os.environ['BZR_EMAIL'] = 'buildd@%s' % hostname
+
builder = RecipeBuilder(*sys.argv[1:])
if builder.install() != 0:
sys.exit(RETCODE_FAILURE_INSTALL)
=== modified file 'lib/canonical/buildd/debian/changelog'
--- lib/canonical/buildd/debian/changelog 2010-08-05 21:12:36 +0000
+++ lib/canonical/buildd/debian/changelog 2010-08-18 11:41:24 +0000
@@ -1,9 +1,22 @@
-launchpad-buildd (68) UNRELEASED; urgency=low
+launchpad-buildd (68) hardy-cat; urgency=low
+ [ William Grant ]
* Take an 'arch_tag' argument, so the master can override the slave
architecture.
- -- William Grant <wgrant@xxxxxxxxxx> Sun, 01 Aug 2010 22:00:32 +1000
+ [ Jelmer Vernooij ]
+
+ * Explicitly use source format 1.0.
+ * Add LSB information to init script.
+ * Use debhelper >= 5 (available in dapper, not yet deprecated in
+ maverick).
+ * Fix spelling in description.
+ * Install example buildd configuration.
+
+ [ Paul Hummer ]
+ * Provide BZR_EMAIL for bzr 2.2 in the buildds LP#617072
+
+ -- LaMont Jones <lamont@xxxxxxxxxxxxx> Mon, 16 Aug 2010 13:25:09 -0600
launchpad-buildd (67) hardy-cat; urgency=low
=== added file 'lib/canonical/buildd/debian/compat'
--- lib/canonical/buildd/debian/compat 1970-01-01 00:00:00 +0000
+++ lib/canonical/buildd/debian/compat 2010-08-18 11:41:24 +0000
@@ -0,0 +1,1 @@
+5
=== modified file 'lib/canonical/buildd/debian/control'
--- lib/canonical/buildd/debian/control 2010-05-19 15:50:27 +0000
+++ lib/canonical/buildd/debian/control 2010-08-18 11:41:24 +0000
@@ -3,15 +3,15 @@
Priority: extra
Maintainer: Adam Conrad <adconrad@xxxxxxxxxx>
Standards-Version: 3.5.9
-Build-Depends-Indep: debhelper (>= 4)
+Build-Depends-Indep: debhelper (>= 5)
Package: launchpad-buildd
Section: misc
Architecture: all
-Depends: python-twisted-core, python-twisted-web, debootstrap, dpkg-dev, linux32, file, bzip2, sudo, ntpdate, adduser, apt-transport-https, lsb-release, apache2
+Depends: python-twisted-core, python-twisted-web, debootstrap, dpkg-dev, linux32, file, bzip2, sudo, ntpdate, adduser, apt-transport-https, lsb-release, apache2, ${misc:Depends}
Description: Launchpad buildd slave
This is the launchpad buildd slave package. It contains everything needed to
get a launchpad buildd going apart from the database manipulation required to
tell launchpad about the slave instance. If you are creating more than one
- slave instance on the same computer, be sure to give them independant configs
- and independant filecaches etc.
+ slave instance on the same computer, be sure to give them independent configs
+ and independent filecaches etc.
=== added file 'lib/canonical/buildd/debian/launchpad-buildd.examples'
--- lib/canonical/buildd/debian/launchpad-buildd.examples 1970-01-01 00:00:00 +0000
+++ lib/canonical/buildd/debian/launchpad-buildd.examples 2010-08-18 11:41:24 +0000
@@ -0,0 +1,1 @@
+buildd-slave-example.conf
=== modified file 'lib/canonical/buildd/debian/launchpad-buildd.init'
--- lib/canonical/buildd/debian/launchpad-buildd.init 2010-03-31 17:10:21 +0000
+++ lib/canonical/buildd/debian/launchpad-buildd.init 2010-08-18 11:41:24 +0000
@@ -8,6 +8,16 @@
#
# Author: Daniel Silverstone <daniel.silverstone@xxxxxxxxxxxxx>
+### BEGIN INIT INFO
+# Provides: launchpad_buildd
+# Required-Start: $local_fs $network $syslog $time
+# Required-Stop: $local_fs $network $syslog $time
+# Default-Start: 2 3 4 5
+# Default-Stop: 0 1 6
+# X-Interactive: false
+# Short-Description: Start/stop launchpad buildds
+### END INIT INFO
+
set -e
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
=== modified file 'lib/canonical/buildd/debian/rules'
--- lib/canonical/buildd/debian/rules 2010-07-18 08:49:02 +0000
+++ lib/canonical/buildd/debian/rules 2010-08-18 11:41:24 +0000
@@ -3,7 +3,6 @@
# Copyright 2009, 2010 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
-export DH_COMPAT=4
export DH_OPTIONS
# This is an incomplete debian rules file for making the launchpad-buildd deb
@@ -41,6 +40,7 @@
etc/launchpad-buildd \
usr/share/launchpad-buildd/canonical/launchpad/daemons \
usr/share/doc/launchpad-buildd
+ dh_installexamples
# Do installs here
touch $(pytarget)/../launchpad/__init__.py
@@ -89,10 +89,11 @@
clean:
dh_clean
-package:
- mkdir -p launchpad-files
+prepare:
install -m644 $(daemons)/buildd-slave.tac launchpad-files/buildd-slave.tac
cp ../launchpad/daemons/tachandler.py launchpad-files/tachandler.py
+
+package: prepare
debuild -uc -us -S
build:
=== added directory 'lib/canonical/buildd/debian/source'
=== added file 'lib/canonical/buildd/debian/source/format'
--- lib/canonical/buildd/debian/source/format 1970-01-01 00:00:00 +0000
+++ lib/canonical/buildd/debian/source/format 2010-08-18 11:41:24 +0000
@@ -0,0 +1,1 @@
+1.0
=== added directory 'lib/canonical/buildd/launchpad-files'
=== modified file 'lib/canonical/cachedproperty.py'
--- lib/canonical/cachedproperty.py 2010-01-11 21:29:33 +0000
+++ lib/canonical/cachedproperty.py 2010-08-18 11:41:24 +0000
@@ -3,10 +3,24 @@
"""Cached properties for situations where a property is computed once and
then returned each time it is asked for.
+
+The clear_cachedproperties function can be used to wipe the cache of properties
+from an instance.
"""
__metaclass__ = type
+__all__ = [
+ 'cache_property',
+ 'cachedproperty',
+ 'clear_cachedproperties',
+ 'clear_property',
+ ]
+
+from zope.security.proxy import removeSecurityProxy
+
+from canonical.lazr.utils import safe_hasattr
+
# XXX: JonathanLange 2010-01-11 bug=505731: Move this to lp.services.
def cachedproperty(attrname_or_fn):
@@ -17,6 +31,10 @@
If you don't provide a name, the mangled name of the property is used.
+ cachedproperty is not threadsafe - it should not be used on objects which
+ are shared across threads / external locking should be used on those
+ objects.
+
>>> class CachedPropertyTest(object):
...
... @cachedproperty('_foo_cache')
@@ -44,6 +62,10 @@
69
>>> cpt._bar_cached_value
69
+
+ Cached properties are listed on instances.
+ >>> sorted(cpt._cached_properties)
+ ['_bar_cached_value', '_foo_cache']
"""
if isinstance(attrname_or_fn, basestring):
@@ -54,8 +76,125 @@
attrname = '_%s_cached_value' % fn.__name__
return CachedProperty(attrname, fn)
+def cache_property(instance, attrname, value):
+ """Cache value on instance as attrname.
+
+ instance._cached_properties is updated with attrname.
+
+ >>> class CachedPropertyTest(object):
+ ...
+ ... @cachedproperty('_foo_cache')
+ ... def foo(self):
+ ... return 23
+ ...
+ >>> instance = CachedPropertyTest()
+ >>> cache_property(instance, '_foo_cache', 42)
+ >>> instance.foo
+ 42
+ >>> instance._cached_properties
+ ['_foo_cache']
+
+ Caching a new value does not duplicate the cache keys.
+
+ >>> cache_property(instance, '_foo_cache', 84)
+ >>> instance._cached_properties
+ ['_foo_cache']
+
+ And does update the cached value.
+
+ >>> instance.foo
+ 84
+ """
+ naked_instance = removeSecurityProxy(instance)
+ clear_property(naked_instance, attrname)
+ setattr(naked_instance, attrname, value)
+ cached_properties = getattr(naked_instance, '_cached_properties', [])
+ cached_properties.append(attrname)
+ naked_instance._cached_properties = cached_properties
+
+
+def clear_property(instance, attrname):
+ """Remove a cached attribute from instance.
+
+ The attribute name is removed from instance._cached_properties.
+
+ If the property is not cached, nothing happens.
+
+ :seealso clear_cachedproperties: For clearing all cached items at once.
+
+ >>> class CachedPropertyTest(object):
+ ...
+ ... @cachedproperty('_foo_cache')
+ ... def foo(self):
+ ... return 23
+ ...
+ >>> instance = CachedPropertyTest()
+ >>> instance.foo
+ 23
+ >>> clear_property(instance, '_foo_cache')
+ >>> instance._cached_properties
+ []
+ >>> is_cached(instance, '_foo_cache')
+ False
+ >>> clear_property(instance, '_foo_cache')
+ """
+ naked_instance = removeSecurityProxy(instance)
+ if not is_cached(naked_instance, attrname):
+ return
+ delattr(naked_instance, attrname)
+ naked_instance._cached_properties.remove(attrname)
+
+
+def clear_cachedproperties(instance):
+ """Clear cached properties from an object.
+
+ >>> class CachedPropertyTest(object):
+ ...
+ ... @cachedproperty('_foo_cache')
+ ... def foo(self):
+ ... return 23
+ ...
+ >>> instance = CachedPropertyTest()
+ >>> instance.foo
+ 23
+ >>> instance._cached_properties
+ ['_foo_cache']
+ >>> clear_cachedproperties(instance)
+ >>> instance._cached_properties
+ []
+ >>> hasattr(instance, '_foo_cache')
+ False
+ """
+ naked_instance = removeSecurityProxy(instance)
+ cached_properties = getattr(naked_instance, '_cached_properties', [])
+ for property_name in cached_properties:
+ delattr(naked_instance, property_name)
+ naked_instance._cached_properties = []
+
+
+def is_cached(instance, attrname):
+ """Return True if attrname is cached on instance.
+
+ >>> class CachedPropertyTest(object):
+ ...
+ ... @cachedproperty('_foo_cache')
+ ... def foo(self):
+ ... return 23
+ ...
+ >>> instance = CachedPropertyTest()
+ >>> instance.foo
+ 23
+ >>> is_cached(instance, '_foo_cache')
+ True
+ >>> is_cached(instance, '_var_cache')
+ False
+ """
+ naked_instance = removeSecurityProxy(instance)
+ return safe_hasattr(naked_instance, attrname)
+
class CachedPropertyForAttr:
+ """Curry a decorator to provide arguments to the CachedProperty."""
def __init__(self, attrname):
self.attrname = attrname
@@ -66,18 +205,20 @@
class CachedProperty:
+ # Used to detect not-yet-cached properties.
+ sentinel = object()
+
def __init__(self, attrname, fn):
self.fn = fn
self.attrname = attrname
- self.marker = object()
def __get__(self, inst, cls=None):
if inst is None:
return self
- cachedresult = getattr(inst, self.attrname, self.marker)
- if cachedresult is self.marker:
+ cachedresult = getattr(inst, self.attrname, CachedProperty.sentinel)
+ if cachedresult is CachedProperty.sentinel:
result = self.fn(inst)
- setattr(inst, self.attrname, result)
+ cache_property(inst, self.attrname, result)
return result
else:
return cachedresult
=== modified file 'lib/canonical/configure.zcml'
--- lib/canonical/configure.zcml 2010-07-24 00:07:54 +0000
+++ lib/canonical/configure.zcml 2010-08-18 11:41:24 +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).
-->
@@ -21,6 +21,7 @@
<include package="lp.services.job" />
<include package="lp.services.memcache" />
<include package="lp.services.profile" />
+ <include package="lp.services.features" />
<include package="lp.services.scripts" />
<include package="lp.services.worlddata" />
<include package="lp.services.salesforce" />
=== modified file 'lib/canonical/database/sqlbase.py'
--- lib/canonical/database/sqlbase.py 2010-05-21 13:08:18 +0000
+++ lib/canonical/database/sqlbase.py 2010-08-18 11:41:24 +0000
@@ -30,6 +30,7 @@
from lazr.restful.interfaces import IRepresentationCache
+from canonical.cachedproperty import clear_cachedproperties
from canonical.config import config, dbconfig
from canonical.database.interfaces import ISQLBase
@@ -175,6 +176,9 @@
correct master Store.
"""
from canonical.launchpad.interfaces import IMasterStore
+ # Make it simple to write dumb-invalidators - initialised
+ # _cached_properties to a valid list rather than just-in-time creation.
+ self._cached_properties = []
store = IMasterStore(self.__class__)
# The constructor will fail if objects from a different Store
@@ -253,6 +257,14 @@
cache = getUtility(IRepresentationCache)
cache.delete(self)
+ def __storm_invalidated__(self):
+ """Flush cached properties."""
+ # Note this is not directly tested; but the entire test suite blows up
+ # awesomely if its broken : its entirely unclear where tests for this
+ # should be -- RBC 20100816.
+ clear_cachedproperties(self)
+
+
alreadyInstalledMsg = ("A ZopelessTransactionManager with these settings is "
"already installed. This is probably caused by calling initZopeless twice.")
=== modified file 'lib/canonical/launchpad/browser/librarian.py'
--- lib/canonical/launchpad/browser/librarian.py 2010-08-04 20:13:18 +0000
+++ lib/canonical/launchpad/browser/librarian.py 2010-08-18 11:41:24 +0000
@@ -32,7 +32,7 @@
IWebBrowserOriginatingRequest)
from canonical.launchpad.webapp.url import urlappend
from canonical.lazr.utils import get_current_browser_request
-from canonical.librarian.client import quote
+from canonical.librarian.client import url_path_quote
from canonical.librarian.interfaces import LibrarianServerError
from canonical.librarian.utils import filechunks, guess_librarian_encoding
@@ -216,5 +216,6 @@
parent_url = canonical_url(self.parent, request=request)
traversal_url = urlappend(parent_url, '+files')
url = urlappend(
- traversal_url, quote(self.context.filename.encode('utf-8')))
+ traversal_url,
+ url_path_quote(self.context.filename.encode('utf-8')))
return url
=== modified file 'lib/canonical/launchpad/browser/multistep.py'
--- lib/canonical/launchpad/browser/multistep.py 2010-05-12 19:06:17 +0000
+++ lib/canonical/launchpad/browser/multistep.py 2010-08-18 11:41:24 +0000
@@ -93,6 +93,14 @@
view.total_steps = self.total_steps
view.is_step = self.getIsStepDict()
self.step_number += 1
+
+ action_required = None
+ for name in self.request.form.keys():
+ if name.startswith('field.actions.'):
+ action_required = (name, self.request.form[name])
+ break
+
+ action_taken = view.action_taken
while view.next_step is not None:
view = view.next_step(self.context, self.request)
assert isinstance(view, StepView), 'Not a StepView: %s' % view
@@ -102,8 +110,19 @@
view.is_step = self.getIsStepDict()
self.step_number += 1
view.injectStepNameInRequest()
+ if view.action_taken is not None:
+ action_taken = view.action_taken
+
self.view = view
+ if action_required is not None and action_taken is None:
+ # This is mostly useful for catching tests that pass
+ # in invalid form data via a dictionary instead of
+ # using a test browser.
+ raise AssertionError(
+ 'MultiStepView did not find action for %s=%r'
+ % action_required)
+
def render(self):
return self.view.render()
=== modified file 'lib/canonical/launchpad/configure.zcml'
--- lib/canonical/launchpad/configure.zcml 2010-07-14 09:58:11 +0000
+++ lib/canonical/launchpad/configure.zcml 2010-08-18 11:41:24 +0000
@@ -32,6 +32,7 @@
<include package="lp.testopenid" />
<include package="lp.blueprints" />
<include package="lp.services.comments" />
+ <include package="lp.services.fields" />
<include package="lp.vostok" />
<browser:url
@@ -96,6 +97,12 @@
classes="LaunchpadRootNavigation"
/>
+ <browser:navigation
+ module="canonical.launchpad.browser"
+ classes="LaunchpadRootNavigation"
+ layer="zope.publisher.interfaces.xmlrpc.IXMLRPCRequest"
+ />
+
<!-- Register a handler to fix things up just before the application
starts (and after zcml has been processed). -->
<subscriber handler=".webapp.initialization.handle_process_start" />
=== modified file 'lib/canonical/launchpad/database/librarian.py'
--- lib/canonical/launchpad/database/librarian.py 2010-07-06 16:12:46 +0000
+++ lib/canonical/launchpad/database/librarian.py 2010-08-18 11:41:24 +0000
@@ -194,6 +194,7 @@
def __storm_invalidated__(self):
"""Make sure that the file is closed across transaction boundary."""
+ super(LibraryFileAlias, self).__storm_invalidated__()
self.close()
=== modified file 'lib/canonical/launchpad/doc/image-widget.txt'
--- lib/canonical/launchpad/doc/image-widget.txt 2009-08-21 17:43:28 +0000
+++ lib/canonical/launchpad/doc/image-widget.txt 2010-08-18 11:41:24 +0000
@@ -99,7 +99,7 @@
First, let's tell it to keep the existing image.
- >>> from canonical.launchpad.fields import KEEP_SAME_IMAGE
+ >>> from lp.services.fields import KEEP_SAME_IMAGE
>>> form = {'field.logo.action': 'keep'}
>>> widget = ImageChangeWidget(
... person_logo, LaunchpadTestRequest(form=form), edit_style)
@@ -260,7 +260,7 @@
used directly, since it specifies some constraints and defaults that are
specific to each image, so you must subclass it before using.
- >>> from canonical.launchpad.fields import (BaseImageUpload,
+ >>> from lp.services.fields import (BaseImageUpload,
... IconImageUpload)
Note: the .bind method here is fetching the field from the IPerson schema
@@ -317,7 +317,7 @@
#an attribute called gotchi_heading of the same object. Any class in which this
#field is used must provide a gotchi_heading attribute.
#
-# >>> from canonical.launchpad.fields import LargeImageUpload
+# >>> from lp.services.fields import LargeImageUpload
# >>> person_gotchi.__class__ == LargeImageUpload
# True
#
=== modified file 'lib/canonical/launchpad/doc/location-widget.txt'
--- lib/canonical/launchpad/doc/location-widget.txt 2010-07-15 10:55:27 +0000
+++ lib/canonical/launchpad/doc/location-widget.txt 2010-08-18 11:41:24 +0000
@@ -4,7 +4,7 @@
>>> from canonical.launchpad.webapp.servers import LaunchpadTestRequest
>>> from lp.registry.interfaces.person import IPersonSet
- >>> from canonical.launchpad.fields import LocationField
+ >>> from lp.services.fields import LocationField
>>> from canonical.widgets.location import LocationWidget
>>> salgado = getUtility(IPersonSet).getByName('salgado')
>>> field = LocationField(__name__='location', title=u'Location')
=== modified file 'lib/canonical/launchpad/doc/loginstatus-pages.txt'
--- lib/canonical/launchpad/doc/loginstatus-pages.txt 2010-03-01 19:35:48 +0000
+++ lib/canonical/launchpad/doc/loginstatus-pages.txt 2010-08-18 11:41:24 +0000
@@ -136,4 +136,4 @@
>>> print view()
<...
- ...<a href="http://launchpad.dev/~name16"...>...
+ ...<a href="/~name16"...>...
=== modified file 'lib/canonical/launchpad/doc/navigation.txt'
--- lib/canonical/launchpad/doc/navigation.txt 2010-08-02 02:13:52 +0000
+++ lib/canonical/launchpad/doc/navigation.txt 2010-08-18 11:41:24 +0000
@@ -234,7 +234,7 @@
== ZCML for browser:navigation ==
-The zcml processor `browser:navigation` processes navigation classes.
+The zcml processor `browser:navigation` registers navigation classes.
>>> class ThingSetView:
...
@@ -266,6 +266,14 @@
... </configure>
... """)
+Once registered, we can look the navigation up using getMultiAdapter().
+
+ >>> from zope.component import getMultiAdapter
+ >>> from canonical.launchpad.webapp.servers import LaunchpadTestRequest
+ >>> from zope.publisher.interfaces.browser import IBrowserPublisher
+ >>> navigation = getMultiAdapter(
+ ... (thingset, LaunchpadTestRequest()), IBrowserPublisher, name='')
+
This time, we get the view object for the page that was registered.
>>> navigation.publishTraverse(request, 'thingview')
=== modified file 'lib/canonical/launchpad/doc/stripped-text-widget.txt'
--- lib/canonical/launchpad/doc/stripped-text-widget.txt 2009-11-05 17:22:53 +0000
+++ lib/canonical/launchpad/doc/stripped-text-widget.txt 2010-08-18 11:41:24 +0000
@@ -4,7 +4,7 @@
The StrippedTextLine field strips the leading and trailing text from the
set value.
- >>> from canonical.launchpad.fields import StrippedTextLine
+ >>> from lp.services.fields import StrippedTextLine
>>> non_required_field = StrippedTextLine(
... __name__='field', title=u'Title', required=False)
=== modified file 'lib/canonical/launchpad/doc/tales-macro.txt'
--- lib/canonical/launchpad/doc/tales-macro.txt 2009-11-23 03:10:04 +0000
+++ lib/canonical/launchpad/doc/tales-macro.txt 2010-08-18 11:41:24 +0000
@@ -3,8 +3,9 @@
Launchpad has a 'macro:' TALES namespace that offers controls over the
layout of the page.
+ >>> from canonical.launchpad.webapp.servers import LaunchpadTestRequest
>>> class FakeView(object):
- ... pass
+ ... request = LaunchpadTestRequest()
Templates should start by specifying the kind of pagetype they use.
That's done by using the 'macro:page' traversal. That expression returns
@@ -50,6 +51,7 @@
returns False for older views that do not.
>>> class LPView(object):
+ ... request = LaunchpadTestRequest()
... def isBetaUser(self):
... return True
=== modified file 'lib/canonical/launchpad/emailtemplates/product-other-license.txt'
--- lib/canonical/launchpad/emailtemplates/product-other-license.txt 2010-05-12 19:06:17 +0000
+++ lib/canonical/launchpad/emailtemplates/product-other-license.txt 2010-08-18 11:41:24 +0000
@@ -28,8 +28,9 @@
questions.
Sometimes new projects are licensed as 'Other/Open Source' because the
-licensing decisions have not yet been made. If that is your situation we urge
-you to update the licensing in Launchpad as soon as you make that choice.
+licensing decisions have not yet been made. If that is your situation
+we urge you to update the licensing in Launchpad as soon as you make
+that choice.
If the license for your project needs to be corrected you can do so by
following the 'Change Details' link on your project's overview page.
=== modified file 'lib/canonical/launchpad/interfaces/account.py'
--- lib/canonical/launchpad/interfaces/account.py 2010-07-08 07:47:27 +0000
+++ lib/canonical/launchpad/interfaces/account.py 2010-08-18 11:41:24 +0000
@@ -24,7 +24,7 @@
from lazr.enum import DBEnumeratedType, DBItem
from canonical.launchpad import _
-from canonical.launchpad.fields import StrippedTextLine, PasswordField
+from lp.services.fields import StrippedTextLine, PasswordField
from lazr.restful.fields import CollectionField, Reference
=== modified file 'lib/canonical/launchpad/interfaces/authtoken.py'
--- lib/canonical/launchpad/interfaces/authtoken.py 2010-04-16 22:56:16 +0000
+++ lib/canonical/launchpad/interfaces/authtoken.py 2010-08-18 11:41:24 +0000
@@ -16,7 +16,7 @@
from zope.interface import Attribute, Interface
from canonical.launchpad import _
-from canonical.launchpad.fields import PasswordField
+from lp.services.fields import PasswordField
from lazr.enum import DBEnumeratedType, DBItem
=== modified file 'lib/canonical/launchpad/mailnotification.py'
--- lib/canonical/launchpad/mailnotification.py 2010-07-28 22:33:59 +0000
+++ lib/canonical/launchpad/mailnotification.py 2010-08-18 11:41:24 +0000
@@ -187,6 +187,7 @@
template % {'url': file_alias_url, 'error_msg': message},
headers={'X-Launchpad-Unhandled-Email': message})
+
def generate_bug_add_email(bug, new_recipients=False, reason=None,
subscribed_by=None, event_creator=None):
"""Generate a new bug notification from the given IBug.
@@ -237,7 +238,8 @@
if new_recipients:
if "assignee" in reason:
- contents += "You have been assigned a bug task for a %(visibility)s bug"
+ contents += (
+ "You have been assigned a bug task for a %(visibility)s bug")
if event_creator is not None:
contents += " by %(assigner)s"
content_substitutions['assigner'] = (
@@ -1051,9 +1053,9 @@
additions = u'\n'.join([
u'',
u'-- ',
- u'This message was sent from Launchpad by the user',
+ u'This message was sent from Launchpad by',
u'%s (%s)' % (sender_name, canonical_url(sender)),
- u'using %s.',
+ u'%s.',
u'For more information see',
u'https://help.launchpad.net/YourAccount/ContactingPeople',
])
=== modified file 'lib/canonical/launchpad/security.py'
--- lib/canonical/launchpad/security.py 2010-08-09 17:14:10 +0000
+++ lib/canonical/launchpad/security.py 2010-08-18 11:41:24 +0000
@@ -654,11 +654,12 @@
"""Verify that the user can view the team's membership.
Anyone can see a public team's membership. Only a team member or
- a Launchpad admin can view a private team.
+ commercial admin or a Launchpad admin can view a private team.
"""
if self.obj.team.visibility == PersonVisibility.PUBLIC:
return True
- if user.in_admin or user.inTeam(self.obj.team):
+ if (user.in_admin or user.in_commercial_admin
+ or user.inTeam(self.obj.team)):
return True
return False
@@ -725,12 +726,13 @@
def checkAuthenticated(self, user):
"""Verify that the user can view the team's membership.
- Anyone can see a public team's membership. Only a team member
- or a Launchpad admin can view a private team's members.
+ Anyone can see a public team's membership. Only a team member,
+ commercial admin, or a Launchpad admin can view a private team's
+ members.
"""
if self.obj.visibility == PersonVisibility.PUBLIC:
return True
- if user.in_admin or user.inTeam(self.obj):
+ if user.in_admin or user.in_commercial_admin or user.inTeam(self.obj):
return True
# We also grant visibility of the private team to administrators of
# other teams that have been invited to join the private team.
@@ -1171,12 +1173,12 @@
usedfor = ICodeImportMachine
-class EditSourcePackageRecipeBuilds(AuthorizationBase):
+class AdminSourcePackageRecipeBuilds(AuthorizationBase):
"""Control who can edit SourcePackageRecipeBuilds.
Access is restricted to members of ~bazaar-experts and Buildd Admins.
"""
- permission = 'launchpad.Edit'
+ permission = 'launchpad.Admin'
usedfor = ISourcePackageRecipeBuild
def checkAuthenticated(self, user):
=== modified file 'lib/canonical/launchpad/templates/launchpad-loginstatus.pt'
--- lib/canonical/launchpad/templates/launchpad-loginstatus.pt 2009-07-17 17:59:07 +0000
+++ lib/canonical/launchpad/templates/launchpad-loginstatus.pt 2010-08-18 11:41:24 +0000
@@ -9,7 +9,7 @@
<div id="logincontrol" tal:condition="view/logged_in">
<form action="+logout" method="post">
<input type="hidden" name="loggingout" value="1" />
- <a tal:replace="structure request/lp:person/fmt:link:mainsite" /> •
+ <a tal:replace="structure request/lp:person/fmt:name_link" /> •
<input type="submit" name="logout" value="Log Out" />
</form>
</div>
=== modified file 'lib/canonical/launchpad/utilities/gpghandler.py'
--- lib/canonical/launchpad/utilities/gpghandler.py 2010-05-15 17:43:59 +0000
+++ lib/canonical/launchpad/utilities/gpghandler.py 2010-08-18 11:41:24 +0000
@@ -163,10 +163,10 @@
# XXX: 2010-04-26, Salgado, bug=570244: This hack is needed
# for python2.5 compatibility. We should remove it when we no
# longer need to run on python2.5.
- if hasattr(e, 'message'):
+ if hasattr(e, 'strerror'):
+ msg = e.strerror
+ else:
msg = e.message
- else:
- msg = e.strerror
raise GPGVerificationError(msg)
else:
# store clearsigned signature
@@ -180,10 +180,10 @@
# XXX: 2010-04-26, Salgado, bug=570244: This hack is needed
# for python2.5 compatibility. We should remove it when we no
# longer need to run on python2.5.
- if hasattr(e, 'message'):
+ if hasattr(e, 'strerror'):
+ msg = e.strerror
+ else:
msg = e.message
- else:
- msg = e.strerror
raise GPGVerificationError(msg)
# XXX jamesh 2006-01-31:
=== modified file 'lib/canonical/launchpad/webapp/configure.zcml'
--- lib/canonical/launchpad/webapp/configure.zcml 2010-07-23 13:24:58 +0000
+++ lib/canonical/launchpad/webapp/configure.zcml 2010-08-18 11:41:24 +0000
@@ -711,6 +711,10 @@
/>
<adapter
+ factory="canonical.launchpad.webapp.tales.LaunchpadLayerToMainTemplateAdapter"
+ />
+
+ <adapter
factory="canonical.launchpad.webapp.snapshot.snapshot_sql_result" />
<!-- It also works for the legacy SQLObject interface. -->
<adapter
@@ -813,7 +817,7 @@
<!-- Define the widget used by PublicPersonChoice fields. -->
<view
type="zope.publisher.interfaces.browser.IBrowserRequest"
- for="canonical.launchpad.fields.PublicPersonChoice
+ for="lp.services.fields.PublicPersonChoice
canonical.launchpad.webapp.vocabulary.IHugeVocabulary"
provides="zope.app.form.interfaces.IInputWidget"
factory="canonical.widgets.popup.PersonPickerWidget"
=== modified file 'lib/canonical/launchpad/webapp/interaction.py'
--- lib/canonical/launchpad/webapp/interaction.py 2010-07-10 10:00:12 +0000
+++ lib/canonical/launchpad/webapp/interaction.py 2010-08-18 11:41:24 +0000
@@ -52,6 +52,7 @@
'setupInteraction',
'setupInteractionByEmail',
'setupInteractionForPerson',
+ 'Participation',
]
=== modified file 'lib/canonical/launchpad/webapp/launchpadform.py'
--- lib/canonical/launchpad/webapp/launchpadform.py 2010-06-23 23:07:10 +0000
+++ lib/canonical/launchpad/webapp/launchpadform.py 2010-08-18 11:41:24 +0000
@@ -74,6 +74,8 @@
actions = ()
+ action_taken = None
+
render_context = False
form_result = None
@@ -112,6 +114,7 @@
self.form_result = action.success(data)
if self.next_url:
self.request.response.redirect(self.next_url)
+ self.action_taken = action
def render(self):
"""Return the body of the response.
=== modified file 'lib/canonical/launchpad/webapp/login.py'
--- lib/canonical/launchpad/webapp/login.py 2010-07-24 17:43:17 +0000
+++ lib/canonical/launchpad/webapp/login.py 2010-08-18 11:41:24 +0000
@@ -254,15 +254,6 @@
'Did not expect multi-valued fields.')
params[key] = value[0]
- # XXX benji 2010-07-23 bug=608920
- # The production OpenID provider has some Django middleware that
- # generates a token used to prevent XSRF attacks and stuffs it into
- # every form. Unfortunately that includes forms that have off-site
- # targets and since our OpenID client verifies that no form values have
- # been injected as a security precaution, this breaks logging-in in
- # certain circumstances (see bug 597324). The best we can do at the
- # moment is to remove the token before invoking the OpenID library.
- params.pop('csrfmiddlewaretoken', None)
return params
def _get_requested_url(self, request):
=== modified file 'lib/canonical/launchpad/webapp/metazcml.py'
--- lib/canonical/launchpad/webapp/metazcml.py 2010-08-05 21:53:44 +0000
+++ lib/canonical/launchpad/webapp/metazcml.py 2010-08-18 11:41:24 +0000
@@ -19,7 +19,6 @@
from zope.interface import Interface, implements
from zope.publisher.interfaces.browser import (
IBrowserPublisher, IBrowserRequest, IDefaultBrowserLayer)
-from zope.publisher.interfaces.xmlrpc import IXMLRPCRequest
from zope.schema import TextLine
from zope.security.checker import Checker, CheckerPublic
from zope.security.interfaces import IPermission
@@ -193,6 +192,10 @@
class INavigationDirective(IGlueDirective):
"""Hook up traversal etc."""
+ layer = GlobalInterface(
+ title=u"The layer where this navigation is going to be available.",
+ required=False)
+
class IFeedsDirective(IGlueDirective):
"""Hook up feeds."""
@@ -267,7 +270,7 @@
layer=layer, class_=feedclass)
-def navigation(_context, module, classes):
+def navigation(_context, module, classes, layer=IDefaultBrowserLayer):
"""Handler for the `INavigationDirective`."""
if not inspect.ismodule(module):
raise TypeError("module attribute must be a module: %s, %s" %
@@ -281,19 +284,11 @@
for_ = [navclass.usedfor]
# Register the navigation as the traversal component.
- layer = IDefaultBrowserLayer
provides = IBrowserPublisher
name = ''
view(_context, factory, layer, name, for_,
permission=PublicPermission, provides=provides,
allowed_interface=[IBrowserPublisher])
- #view(_context, factory, layer, name, for_,
- # permission=PublicPermission, provides=provides)
-
- # Also register the navigation as a traversal component for XMLRPC.
- xmlrpc_layer = IXMLRPCRequest
- view(_context, factory, xmlrpc_layer, name, for_,
- permission=PublicPermission, provides=provides)
class InterfaceInstanceDispatcher:
=== modified file 'lib/canonical/launchpad/webapp/publication.py'
--- lib/canonical/launchpad/webapp/publication.py 2010-07-30 00:06:00 +0000
+++ lib/canonical/launchpad/webapp/publication.py 2010-08-18 11:41:24 +0000
@@ -60,6 +60,90 @@
METHOD_WRAPPER_TYPE = type({}.__setitem__)
+OFFSITE_POST_WHITELIST = ('/+storeblob', '/+request-token', '/+access-token',
+ '/+hwdb/+submit', '/+openid')
+
+
+def maybe_block_offsite_form_post(request):
+ """Check if an attempt was made to post a form from a remote site.
+
+ This is a cross-site request forgery (XSRF/CSRF) countermeasure.
+
+ The OffsiteFormPostError exception is raised if the following
+ holds true:
+ 1. the request method is POST *AND*
+ 2. a. the HTTP referer header is empty *OR*
+ b. the host portion of the referrer is not a registered vhost
+ """
+ if request.method != 'POST':
+ return
+ if (IOAuthSignedRequest.providedBy(request)
+ or not IBrowserRequest.providedBy(request)):
+ # We only want to check for the referrer header if we are
+ # in the middle of a request initiated by a web browser. A
+ # request to the web service (which is necessarily
+ # OAuth-signed) or a request that does not implement
+ # IBrowserRequest (such as an XML-RPC request) can do
+ # without a Referer.
+ return
+ if request['PATH_INFO'] in OFFSITE_POST_WHITELIST:
+ # XXX: jamesh 2007-11-23 bug=124421:
+ # Allow offsite posts to our TestOpenID endpoint. Ideally we'd
+ # have a better way of marking this URL as allowing offsite
+ # form posts.
+ #
+ # XXX gary 2010-03-09 bug=535122,538097
+ # The one-off exceptions are necessary because existing
+ # non-browser applications make requests to these URLs
+ # without providing a Referer. Apport makes POST requests
+ # to +storeblob without providing a Referer (bug 538097),
+ # and launchpadlib used to make POST requests to
+ # +request-token and +access-token without providing a
+ # Referer.
+ #
+ # XXX Abel Deuring 2010-04-09 bug=550973
+ # The HWDB client "checkbox" accesses /+hwdb/+submit without
+ # a referer. This will change in the version in Ubuntu 10.04,
+ # but Launchpad should support HWDB submissions from older
+ # Ubuntu versions during their support period.
+ #
+ # We'll have to keep an application's one-off exception
+ # until the application has been changed to send a
+ # Referer, and until we have no legacy versions of that
+ # application to support. For instance, we can't get rid
+ # of the apport exception until after Lucid's end-of-life
+ # date. We should be able to get rid of the launchpadlib
+ # exception after Karmic's end-of-life date.
+ return
+ if request['PATH_INFO'].startswith('/+openid-callback'):
+ # If this is a callback from an OpenID provider, we don't require an
+ # on-site referer (because the provider may be off-site). This
+ # exception was added as a result of bug 597324 (message #10 in
+ # particular).
+ return
+ referrer = request.getHeader('referer') # match HTTP spec misspelling
+ if not referrer:
+ raise NoReferrerError('No value for REFERER header')
+ # XXX: jamesh 2007-04-26 bug=98437:
+ # The Zope testing infrastructure sets a default (incorrect)
+ # referrer value of "localhost" or "localhost:9000" if no
+ # referrer is included in the request. We let it pass through
+ # here for the benefits of the tests. Web browsers send full
+ # URLs so this does not open us up to extra XSRF attacks.
+ if referrer in ['localhost', 'localhost:9000']:
+ return
+ # Extract the hostname from the referrer URI
+ try:
+ hostname = URI(referrer).host
+ except InvalidURIError:
+ hostname = None
+ if hostname not in allvhosts.hostnames:
+ raise OffsiteFormPostError(referrer)
+
+
+class ProfilingOops(Exception):
+ """Fake exception used to log OOPS information when profiling pages."""
+
class LoginRoot:
"""Object that provides IPublishTraverse to return only itself.
@@ -191,7 +275,7 @@
principal = self.getPrincipal(request)
request.setPrincipal(principal)
self.maybeRestrictToTeam(request)
- self.maybeBlockOffsiteFormPost(request)
+ maybe_block_offsite_form_post(request)
self.maybeNotifyReadOnlyMode(request)
def maybeNotifyReadOnlyMode(self, request):
@@ -295,77 +379,6 @@
uri = uri.replace(query=query_string)
return str(uri)
- def maybeBlockOffsiteFormPost(self, request):
- """Check if an attempt was made to post a form from a remote site.
-
- The OffsiteFormPostError exception is raised if the following
- holds true:
- 1. the request method is POST *AND*
- 2. a. the HTTP referer header is empty *OR*
- b. the host portion of the referrer is not a registered vhost
- """
- if request.method != 'POST':
- return
- # XXX: jamesh 2007-11-23 bug=124421:
- # Allow offsite posts to our TestOpenID endpoint. Ideally we'd
- # have a better way of marking this URL as allowing offsite
- # form posts.
- if request['PATH_INFO'] == '/+openid':
- return
- if (IOAuthSignedRequest.providedBy(request)
- or not IBrowserRequest.providedBy(request)
- or request['PATH_INFO'] in (
- '/+storeblob', '/+request-token', '/+access-token',
- '/+hwdb/+submit')):
- # We only want to check for the referrer header if we are
- # in the middle of a request initiated by a web browser. A
- # request to the web service (which is necessarily
- # OAuth-signed) or a request that does not implement
- # IBrowserRequest (such as an XML-RPC request) can do
- # without a Referer.
- #
- # XXX gary 2010-03-09 bug=535122,538097
- # The one-off exceptions are necessary because existing
- # non-browser applications make requests to these URLs
- # without providing a Referer. Apport makes POST requests
- # to +storeblob without providing a Referer (bug 538097),
- # and launchpadlib used to make POST requests to
- # +request-token and +access-token without providing a
- # Referer.
- #
- # XXX Abel Deuring 2010-04-09 bug=550973
- # The HWDB client "checkbox" accesses /+hwdb/+submit without
- # a referer. This will change in the version in Ubuntu 10.04,
- # but Launchpad should support HWDB submissions from older
- # Ubuntu versions during their support period.
- #
- # We'll have to keep an application's one-off exception
- # until the application has been changed to send a
- # Referer, and until we have no legacy versions of that
- # application to support. For instance, we can't get rid
- # of the apport exception until after Lucid's end-of-life
- # date. We should be able to get rid of the launchpadlib
- # exception after Karmic's end-of-life date.
- return
- referrer = request.getHeader('referer') # match HTTP spec misspelling
- if not referrer:
- raise NoReferrerError('No value for REFERER header')
- # XXX: jamesh 2007-04-26 bug=98437:
- # The Zope testing infrastructure sets a default (incorrect)
- # referrer value of "localhost" or "localhost:9000" if no
- # referrer is included in the request. We let it pass through
- # here for the benefits of the tests. Web browsers send full
- # URLs so this does not open us up to extra XSRF attacks.
- if referrer in ['localhost', 'localhost:9000']:
- return
- # Extract the hostname from the referrer URI
- try:
- hostname = URI(referrer).host
- except InvalidURIError:
- hostname = None
- if hostname not in allvhosts.hostnames:
- raise OffsiteFormPostError(referrer)
-
def constructPageID(self, view, context):
"""Given a view, figure out what its page ID should be.
=== modified file 'lib/canonical/launchpad/webapp/servers.py'
--- lib/canonical/launchpad/webapp/servers.py 2010-08-02 02:23:26 +0000
+++ lib/canonical/launchpad/webapp/servers.py 2010-08-18 11:41:24 +0000
@@ -79,6 +79,10 @@
from canonical.lazr.timeout import set_default_timeout_function
+from lp.services.features.flags import (
+ NullFeatureController,
+ )
+
class StepsToGo:
"""
@@ -838,6 +842,9 @@
self.needs_datetimepicker_iframe = False
self.needs_json = False
self.needs_gmap2 = False
+ # stub out the FeatureController that would normally be provided by
+ # the publication mechanism
+ self.features = NullFeatureController()
@property
def uuid(self):
=== modified file 'lib/canonical/launchpad/webapp/tales.py'
--- lib/canonical/launchpad/webapp/tales.py 2010-07-30 06:08:54 +0000
+++ lib/canonical/launchpad/webapp/tales.py 2010-08-18 11:41:24 +0000
@@ -22,7 +22,7 @@
from lazr.uri import URI
from zope.interface import Interface, Attribute, implements
-from zope.component import getUtility, queryAdapter, getMultiAdapter
+from zope.component import adapts, getUtility, queryAdapter, getMultiAdapter
from zope.app import zapi
from zope.publisher.browser import BrowserView
from zope.publisher.interfaces import IApplicationRequest
@@ -31,6 +31,7 @@
ITraversable, IPathAdapter, TraversalError)
from zope.security.interfaces import Unauthorized
from zope.security.proxy import isinstance as zope_isinstance
+from zope.schema import TextLine
import pytz
from z3c.ptcompat import ViewPageTemplateFile
@@ -41,6 +42,7 @@
ISprint, LicenseStatus)
from canonical.launchpad.interfaces.launchpad import (
IHasIcon, IHasLogo, IHasMugshot, IPrivacy)
+from canonical.launchpad.layers import LaunchpadLayer
import canonical.launchpad.pagetitles
from canonical.launchpad.webapp import canonical_url, urlappend
from canonical.launchpad.webapp.authorization import check_permission
@@ -1061,6 +1063,7 @@
'icon': 'icon',
'displayname': 'displayname',
'unique_displayname': 'unique_displayname',
+ 'name_link': 'nameLink',
}
final_traversable_names = {'local-time': 'local_time'}
@@ -1080,24 +1083,27 @@
"""
return super(PersonFormatterAPI, self).url(view_name, rootsite)
- def link(self, view_name, rootsite='mainsite'):
- """See `ObjectFormatterAPI`.
-
- Return an HTML link to the person's page containing an icon
- followed by the person's name. The default URL for a person is to
- the mainsite.
- """
+ def _makeLink(self, view_name, rootsite, text):
person = self._context
url = self.url(view_name, rootsite)
custom_icon = ObjectImageDisplayAPI(person)._get_custom_icon_url()
if custom_icon is None:
css_class = ObjectImageDisplayAPI(person).sprite_css()
return (u'<a href="%s" class="%s">%s</a>') % (
- url, css_class, cgi.escape(person.displayname))
+ url, css_class, cgi.escape(text))
else:
return (u'<a href="%s" class="bg-image" '
'style="background-image: url(%s)">%s</a>') % (
- url, custom_icon, cgi.escape(person.displayname))
+ url, custom_icon, cgi.escape(text))
+
+ def link(self, view_name, rootsite='mainsite'):
+ """See `ObjectFormatterAPI`.
+
+ Return an HTML link to the person's page containing an icon
+ followed by the person's name. The default URL for a person is to
+ the mainsite.
+ """
+ return self._makeLink(view_name, rootsite, self._context.displayname)
def displayname(self, view_name, rootsite=None):
"""Return the displayname as a string."""
@@ -1119,6 +1125,10 @@
else:
return '<img src="%s" width="14" height="14" />' % custom_icon
+ def nameLink(self, view_name):
+ """Return the Launchpad id of the person, linked to their profile."""
+ return self._makeLink(view_name, None, self._context.name)
+
class TeamFormatterAPI(PersonFormatterAPI):
"""Adapter for `ITeam` objects to a formatted string."""
@@ -2297,6 +2307,20 @@
return check_permission(name, self.context)
+class IMainTemplateFile(Interface):
+ path = TextLine(title=u'The absolute path to this main template.')
+
+
+class LaunchpadLayerToMainTemplateAdapter:
+ adapts(LaunchpadLayer)
+ implements(IMainTemplateFile)
+
+ def __init__(self, context):
+ here = os.path.dirname(os.path.realpath(__file__))
+ self.path = os.path.join(
+ here, '../../../lp/app/templates/base-layout.pt')
+
+
class PageMacroDispatcher:
"""Selects a macro, while storing information about page layout.
@@ -2316,12 +2340,15 @@
implements(ITraversable)
- base = ViewPageTemplateFile('../../../lp/app/templates/base-layout.pt')
-
def __init__(self, context):
# The context of this object is a view object.
self.context = context
+ @property
+ def base(self):
+ return ViewPageTemplateFile(
+ IMainTemplateFile(self.context.request).path)
+
def traverse(self, name, furtherPath):
if name == 'page':
if len(furtherPath) == 1:
=== modified file 'lib/canonical/launchpad/webapp/tests/cookie-authentication.txt'
--- lib/canonical/launchpad/webapp/tests/cookie-authentication.txt 2010-02-25 10:50:31 +0000
+++ lib/canonical/launchpad/webapp/tests/cookie-authentication.txt 2010-08-18 11:41:24 +0000
@@ -24,7 +24,7 @@
... fill_login_form_and_submit)
>>> fill_login_form_and_submit(browser, 'foo.bar@xxxxxxxxxxxxx', 'test')
>>> print extract_text(find_tag_by_id(browser.contents, 'logincontrol'))
- Foo Bar...
+ name16...
# Open a page again so that we see the cookie for a launchpad.dev request
# and not a testopenid.dev request (as above).
@@ -43,7 +43,7 @@
>>> browser.url
'http://launchpad.dev:8085/'
>>> print extract_text(find_tag_by_id(browser.contents, 'logincontrol'))
- Foo Bar...
+ name16...
>>> browser.cookies.getinfo(session_cookie_name)['domain']
'.launchpad.dev'
=== modified file 'lib/canonical/launchpad/webapp/tests/no-anonymous-session-cookies.txt'
--- lib/canonical/launchpad/webapp/tests/no-anonymous-session-cookies.txt 2010-05-15 17:43:59 +0000
+++ lib/canonical/launchpad/webapp/tests/no-anonymous-session-cookies.txt 2010-08-18 11:41:24 +0000
@@ -29,7 +29,7 @@
... fill_login_form_and_submit)
>>> fill_login_form_and_submit(browser, 'foo.bar@xxxxxxxxxxxxx', 'test')
>>> print extract_text(find_tag_by_id(browser.contents, 'logincontrol'))
- Foo Bar...
+ name16...
# Open a page again so that we see the cookie for a launchpad.dev request
# and not a testopenid.dev request (as above).
=== added file 'lib/canonical/launchpad/webapp/tests/test_base_template.py'
--- lib/canonical/launchpad/webapp/tests/test_base_template.py 1970-01-01 00:00:00 +0000
+++ lib/canonical/launchpad/webapp/tests/test_base_template.py 2010-08-18 11:41:24 +0000
@@ -0,0 +1,29 @@
+# Copyright 2010 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Tests for Launchpad's 'view/macro:page' TALES adapter."""
+
+__metaclass__ = type
+
+from zope.component import getMultiAdapter
+from zope.traversing.interfaces import IPathAdapter
+
+from canonical.launchpad.webapp.publisher import rootObject
+from canonical.launchpad.webapp.servers import LaunchpadTestRequest
+from canonical.testing.layers import FunctionalLayer
+
+from lp.testing import TestCase
+
+
+class TestPageMacroDispatcher(TestCase):
+
+ layer = FunctionalLayer
+
+ def test_base_template(self):
+ # Requests on the launchpad.dev vhost use the Launchpad base template.
+ root_view = getMultiAdapter(
+ (rootObject, LaunchpadTestRequest()), name='index.html')
+ adapter = getMultiAdapter([root_view], IPathAdapter, name='macro')
+ self.assertIn('lp/app/templates', adapter.base.filename)
+ # The base template defines a 'master' macro as the adapter expects.
+ self.assertIn('master', adapter.base.macros.keys())
=== modified file 'lib/canonical/launchpad/webapp/tests/test_login.py'
--- lib/canonical/launchpad/webapp/tests/test_login.py 2010-07-24 17:43:17 +0000
+++ lib/canonical/launchpad/webapp/tests/test_login.py 2010-08-18 11:41:24 +0000
@@ -6,7 +6,13 @@
__metaclass__ = type
-__all__ = []
+__all__ = [
+ 'FakeOpenIDConsumer',
+ 'FakeOpenIDResponse',
+ 'IAccountSet_getByOpenIDIdentifier_monkey_patched',
+ 'SRegResponse_fromSuccessResponse_stubbed',
+ 'fill_login_form_and_submit',
+ ]
from contextlib import contextmanager
from datetime import datetime, timedelta
@@ -65,6 +71,7 @@
class FakeConsumer:
"""An OpenID consumer that stashes away arguments for test instection."""
+
def complete(self, params, requested_url):
self.params = params
self.requested_url = requested_url
@@ -72,6 +79,7 @@
class FakeConsumerOpenIDCallbackView(OpenIDCallbackView):
"""An OpenID handler with fake consumer so arguments can be inspected."""
+
def _getConsumer(self):
self.fake_consumer = FakeConsumer()
return self.fake_consumer
@@ -211,24 +219,6 @@
view = OpenIDCallbackView(context=None, request=None)
self.assertRaises(ValueError, view._gather_params, request)
- def test_csrfmiddlewaretoken_is_ignored(self):
- # Show that the _gather_params filters out the errant
- # csrfmiddlewaretoken form field. See comment in _gather_params for
- # more info.
- request = LaunchpadTestRequest(
- SERVER_URL='http://example.com',
- QUERY_STRING='foo=bar',
- form={'starting_url': 'http://launchpad.dev/after-login',
- 'csrfmiddlewaretoken': '12345'},
- environ={'PATH_INFO': '/'})
- view = OpenIDCallbackView(context=None, request=None)
- params = view._gather_params(request)
- expected_params = {
- 'starting_url': 'http://launchpad.dev/after-login',
- 'foo': 'bar',
- }
- self.assertEquals(params, expected_params)
-
def test_get_requested_url(self):
# The OpenIDCallbackView needs to pass the currently-being-requested
# URL to the OpenID library. OpenIDCallbackView._get_requested_url
@@ -420,8 +410,10 @@
# the response and redirect to the starting_url specified in the
# OpenID response.
test_account = self.factory.makeAccount('Test account')
+
class StubbedOpenIDCallbackViewLoggedIn(StubbedOpenIDCallbackView):
account = test_account
+
view, html = self._createViewWithResponse(
test_account, response_status=FAILURE,
response_msg='Server denied check_authentication',
@@ -532,7 +524,7 @@
fill_login_form_and_submit(browser, 'test@xxxxxxxxxxxxx', 'test')
login_status = extract_text(
find_tag_by_id(browser.contents, 'logincontrol'))
- self.assertIn('Sample Person', login_status)
+ self.assertIn('name12', login_status)
# Now we look up (in urls_redirected_to) the +openid-callback URL that
# was used to complete the authentication and open it on a different
@@ -569,11 +561,13 @@
class FakeOpenIDConsumer:
+
def begin(self, url):
return FakeOpenIDRequest()
class StubbedOpenIDLogin(OpenIDLogin):
+
def _getConsumer(self):
return FakeOpenIDConsumer()
=== added file 'lib/canonical/launchpad/webapp/tests/test_navigation.py'
--- lib/canonical/launchpad/webapp/tests/test_navigation.py 1970-01-01 00:00:00 +0000
+++ lib/canonical/launchpad/webapp/tests/test_navigation.py 2010-08-18 11:41:24 +0000
@@ -0,0 +1,107 @@
+# Copyright 2010 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+__metaclass__ = type
+
+from zope.component import ComponentLookupError, getMultiAdapter
+from zope.configuration import xmlconfig
+from zope.interface import implements, Interface
+from zope.publisher.interfaces.browser import (
+ IBrowserPublisher, IDefaultBrowserLayer)
+from zope.testing.cleanup import cleanUp
+
+from canonical.launchpad.webapp import Navigation
+
+from lp.testing import TestCase
+
+
+class TestNavigationDirective(TestCase):
+
+ def test_default_layer(self):
+ # By default all navigation classes are registered for
+ # IDefaultBrowserLayer.
+ directive = """
+ <browser:navigation
+ module="%(this)s" classes="ThingNavigation"/>
+ """ % dict(this=this)
+ xmlconfig.string(zcml_configure % directive)
+ navigation = getMultiAdapter(
+ (Thing(), DefaultBrowserLayer()), IBrowserPublisher, name='')
+ self.assertIsInstance(navigation, ThingNavigation)
+
+ def test_specific_layer(self):
+ # If we specify a layer when registering a navigation class, it will
+ # only be available on that layer.
+ directive = """
+ <browser:navigation
+ module="%(this)s" classes="OtherThingNavigation"
+ layer="%(this)s.IOtherLayer" />
+ """ % dict(this=this)
+ xmlconfig.string(zcml_configure % directive)
+ self.assertRaises(
+ ComponentLookupError,
+ getMultiAdapter,
+ (Thing(), DefaultBrowserLayer()), IBrowserPublisher, name='')
+
+ navigation = getMultiAdapter(
+ (Thing(), OtherLayer()), IBrowserPublisher, name='')
+ self.assertIsInstance(navigation, OtherThingNavigation)
+
+ def test_multiple_navigations_for_single_context(self):
+ # It is possible to have multiple navigation classes for a given
+ # context class as long as they are registered for different layers.
+ directive = """
+ <browser:navigation
+ module="%(this)s" classes="ThingNavigation"/>
+ <browser:navigation
+ module="%(this)s" classes="OtherThingNavigation"
+ layer="%(this)s.IOtherLayer" />
+ """ % dict(this=this)
+ xmlconfig.string(zcml_configure % directive)
+
+ navigation = getMultiAdapter(
+ (Thing(), DefaultBrowserLayer()), IBrowserPublisher, name='')
+ other_navigation = getMultiAdapter(
+ (Thing(), OtherLayer()), IBrowserPublisher, name='')
+ self.assertNotEqual(navigation, other_navigation)
+
+ def tearDown(self):
+ TestCase.tearDown(self)
+ cleanUp()
+
+
+class DefaultBrowserLayer:
+ implements(IDefaultBrowserLayer)
+
+
+class IThing(Interface):
+ pass
+
+
+class Thing(object):
+ implements(IThing)
+
+
+class ThingNavigation(Navigation):
+ usedfor = IThing
+
+
+class OtherThingNavigation(Navigation):
+ usedfor = IThing
+
+
+class IOtherLayer(Interface):
+ pass
+
+
+class OtherLayer:
+ implements(IOtherLayer)
+
+
+this = "canonical.launchpad.webapp.tests.test_navigation"
+zcml_configure = """
+ <configure xmlns:browser="http://namespaces.zope.org/browser">
+ <include package="canonical.launchpad.webapp" file="meta.zcml" />
+ %s
+ </configure>
+ """
=== modified file 'lib/canonical/launchpad/webapp/tests/test_publication.py'
--- lib/canonical/launchpad/webapp/tests/test_publication.py 2010-05-27 07:53:38 +0000
+++ lib/canonical/launchpad/webapp/tests/test_publication.py 2010-08-18 11:41:24 +0000
@@ -10,6 +10,7 @@
import unittest
from contrib.oauth import OAuthRequest, OAuthSignatureMethod_PLAINTEXT
+from zope.interface import directlyProvides, noLongerProvides
from storm.database import STATE_DISCONNECTED, STATE_RECONNECT
from storm.exceptions import DisconnectionError
@@ -18,20 +19,25 @@
from zope.component import getUtility
from zope.error.interfaces import IErrorReportingUtility
from zope.publisher.interfaces import Retry
+from zope.publisher.interfaces.browser import IBrowserRequest
from canonical.config import dbconfig
from canonical.launchpad.database.emailaddress import EmailAddress
from canonical.launchpad.interfaces.lpstorm import IMasterStore
-from canonical.launchpad.interfaces.oauth import IOAuthConsumerSet
+from canonical.launchpad.interfaces.oauth import (
+ IOAuthConsumerSet, IOAuthSignedRequest)
from canonical.launchpad.ftests import ANONYMOUS, login
from canonical.launchpad.readonly import is_read_only
from canonical.launchpad.tests.readonly import (
remove_read_only_file, touch_read_only_file)
import canonical.launchpad.webapp.adapter as dbadapter
+from canonical.launchpad.webapp.vhosts import allvhosts
from canonical.launchpad.webapp.interfaces import (
- IStoreSelector, MAIN_STORE, MASTER_FLAVOR, OAuthPermission, SLAVE_FLAVOR)
+ IStoreSelector, MAIN_STORE, MASTER_FLAVOR, OAuthPermission, SLAVE_FLAVOR,
+ OffsiteFormPostError, NoReferrerError)
from canonical.launchpad.webapp.publication import (
- is_browser, LaunchpadBrowserPublication)
+ is_browser, LaunchpadBrowserPublication, maybe_block_offsite_form_post,
+ OFFSITE_POST_WHITELIST)
from canonical.launchpad.webapp.servers import (
LaunchpadTestRequest, WebServicePublication)
from canonical.testing import DatabaseFunctionalLayer, FunctionalLayer
@@ -312,6 +318,96 @@
self.assertFalse(is_browser(request))
+class TestBlockingOffsitePosts(TestCase):
+ """We are very particular about what form POSTs we will accept."""
+
+ def test_NoReferrerError(self):
+ # If this request is a POST and there is no referrer, an exception is
+ # raised.
+ request = LaunchpadTestRequest(
+ method='POST', environ=dict(PATH_INFO='/'))
+ self.assertRaises(
+ NoReferrerError, maybe_block_offsite_form_post, request)
+
+ def test_nonPOST_requests(self):
+ # If the request isn't a POST it is always allowed.
+ request = LaunchpadTestRequest(method='SOMETHING')
+ maybe_block_offsite_form_post(request)
+
+ def test_localhost_is_ok(self):
+ # we accept "localhost" and "localhost:9000" as valid referrers. See
+ # comments in the code as to why and for a related bug report.
+ request = LaunchpadTestRequest(
+ method='POST', environ=dict(PATH_INFO='/', REFERER='localhost'))
+ # this doesn't raise an exception
+ maybe_block_offsite_form_post(request)
+
+ def test_whitelisted_paths(self):
+ # There are a few whitelisted POST targets that don't require the
+ # referrer be LP. See comments in the code as to why and for related
+ # bug reports.
+ for path in OFFSITE_POST_WHITELIST:
+ request = LaunchpadTestRequest(
+ method='POST', environ=dict(PATH_INFO=path))
+ # this call shouldn't raise an exception
+ maybe_block_offsite_form_post(request)
+
+ def test_OAuth_signed_requests(self):
+ # Requests that are OAuth signed are allowed.
+ request = LaunchpadTestRequest(
+ method='POST', environ=dict(PATH_INFO='/'))
+ directlyProvides(request, IOAuthSignedRequest)
+ # this call shouldn't raise an exception
+ maybe_block_offsite_form_post(request)
+
+ def test_nonbrowser_requests(self):
+ # Requests that are from non-browsers are allowed.
+ class FakeNonBrowserRequest:
+ method = 'SOMETHING'
+
+ # this call shouldn't raise an exception
+ maybe_block_offsite_form_post(FakeNonBrowserRequest)
+
+ def test_onsite_posts(self):
+ # Other than the explicit execptions, all POSTs have to come from a
+ # known LP virtual host.
+ for hostname in allvhosts.hostnames:
+ referer = 'http://' + hostname + '/foo'
+ request = LaunchpadTestRequest(
+ method='POST', environ=dict(PATH_INFO='/', REFERER=referer))
+ # this call shouldn't raise an exception
+ maybe_block_offsite_form_post(request)
+
+ def test_offsite_posts(self):
+ # If a post comes from an unknown host an execption is raised.
+ disallowed_hosts = ['example.com', 'not-subdomain.launchpad.net']
+ for hostname in disallowed_hosts:
+ referer = 'http://' + hostname + '/foo'
+ request = LaunchpadTestRequest(
+ method='POST', environ=dict(PATH_INFO='/', REFERER=referer))
+ self.assertRaises(
+ OffsiteFormPostError, maybe_block_offsite_form_post, request)
+
+ def test_unparsable_referer(self):
+ # If a post has a referer that is unparsable as a URI an exception is
+ # raised.
+ referer = 'this is not a URI'
+ request = LaunchpadTestRequest(
+ method='POST', environ=dict(PATH_INFO='/', REFERER=referer))
+ self.assertRaises(
+ OffsiteFormPostError, maybe_block_offsite_form_post, request)
+
+
+ def test_openid_callback_with_query_string(self):
+ # An OpenId provider (OP) may post to the +openid-callback URL with a
+ # query string and without a referer. These posts need to be allowed.
+ path_info = u'/+openid-callback?starting_url=...'
+ request = LaunchpadTestRequest(
+ method='POST', environ=dict(PATH_INFO=path_info))
+ # this call shouldn't raise an exception
+ maybe_block_offsite_form_post(request)
+
+
def test_suite():
suite = unittest.TestLoader().loadTestsFromName(__name__)
return suite
=== modified file 'lib/canonical/launchpad/webapp/tests/test_tales.py'
--- lib/canonical/launchpad/webapp/tests/test_tales.py 2010-07-14 14:11:15 +0000
+++ lib/canonical/launchpad/webapp/tests/test_tales.py 2010-08-18 11:41:24 +0000
@@ -3,9 +3,14 @@
"""tales.py doctests."""
+from doctest import DocTestSuite
import unittest
-from doctest import DocTestSuite
+from zope.component import getAdapter
+from zope.traversing.interfaces import IPathAdapter
+
+from canonical.testing import DatabaseFunctionalLayer
+from lp.testing import TestCaseWithFactory
def test_requestapi():
@@ -98,6 +103,21 @@
"""
+class TestPersonFormatterAPI(TestCaseWithFactory):
+ """Tests for PersonFormatterAPI"""
+
+ layer = DatabaseFunctionalLayer
+
+ def test_nameLink(self):
+ """The nameLink links to the URL with the person name as the text."""
+ person = self.factory.makePerson()
+ formatter = getAdapter(person, IPathAdapter, 'fmt')
+ result = formatter.nameLink(None)
+ expected = '<a href="%s" class="sprite person">%s</a>' % (
+ formatter.url(), person.name)
+ self.assertEqual(expected, result)
+
+
def test_suite():
"""Return this module's doctest Suite. Unit tests are also run."""
=== modified file 'lib/canonical/launchpad/zcml/configure.zcml'
--- lib/canonical/launchpad/zcml/configure.zcml 2010-03-16 23:59:41 +0000
+++ lib/canonical/launchpad/zcml/configure.zcml 2010-08-18 11:41:24 +0000
@@ -30,7 +30,6 @@
<include file="temporaryblobstorage.zcml" />
<include file="webservice.zcml" />
- <include file="fields.zcml" />
<include file="widgets.zcml" />
<!-- System homepages -->
=== modified file 'lib/canonical/librarian/client.py'
--- lib/canonical/librarian/client.py 2010-08-03 17:20:07 +0000
+++ lib/canonical/librarian/client.py 2010-08-18 11:41:24 +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).
__metaclass__ = type
@@ -6,9 +6,10 @@
__all__ = [
'FileDownloadClient',
'FileUploadClient',
+ 'get_libraryfilealias_download_path',
'LibrarianClient',
'RestrictedLibrarianClient',
- 'quote',
+ 'url_path_quote',
]
@@ -34,6 +35,18 @@
LIBRARIAN_SERVER_DEFAULT_TIMEOUT, LibrarianServerError, UploadFailed)
+def url_path_quote(filename):
+ """Quote `filename` for use in a URL."""
+ # XXX RobertCollins 2004-09-21: Perhaps filenames with / in them
+ # should be disallowed?
+ return urllib.quote(filename).replace('/', '%2F')
+
+
+def get_libraryfilealias_download_path(aliasID, filename):
+ """Download path for a given `LibraryFileAlias` id and filename."""
+ return '/%d/%s' % (int(aliasID), url_path_quote(filename))
+
+
class FileUploadClient:
"""Simple blocking client for uploading to the librarian."""
@@ -226,18 +239,12 @@
status, ids = response.split()
contentID, aliasID = ids.split('/', 1)
- path = '/%d/%s' % (int(aliasID), quote(name))
+ path = get_libraryfilealias_download_path(aliasID, name)
return urljoin(self.download_url, path)
finally:
self._close()
-def quote(s):
- # XXX: Robert Collins 2004-09-21: Perhaps filenames with / in them
- # should be disallowed?
- return urllib.quote(s).replace('/', '%2F')
-
-
class _File:
"""A wrapper around a file like object that has security assertions"""
@@ -300,7 +307,8 @@
'Alias %d cannot be downloaded from this client.' % aliasID)
if lfa.deleted:
return None
- return '/%d/%s' % (aliasID, quote(lfa.filename.encode('utf-8')))
+ return get_libraryfilealias_download_path(
+ aliasID, lfa.filename.encode('utf-8'))
def getURLForAlias(self, aliasID):
"""Returns the url for talking to the librarian about the given
=== modified file 'lib/canonical/librarian/interfaces.py'
--- lib/canonical/librarian/interfaces.py 2010-04-20 14:27:26 +0000
+++ lib/canonical/librarian/interfaces.py 2010-08-18 11:41:24 +0000
@@ -1,9 +1,18 @@
-# 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).
# PyLint doesn't grok Zope interfaces.
# pylint: disable-msg=E0213
__metaclass__ = type
+__all__ = [
+ 'DownloadFailed',
+ 'IFileUploadClient',
+ 'ILibrarianClient',
+ 'IRestrictedLibrarianClient',
+ 'LibrarianServerError',
+ 'LIBRARIAN_SERVER_DEFAULT_TIMEOUT',
+ 'UploadFailed',
+ ]
import signal
@@ -35,14 +44,18 @@
# LibrarianServerError.
LIBRARIAN_SERVER_DEFAULT_TIMEOUT = 5
+
class IFileUploadClient(Interface):
+ """Upload API for the Librarian client."""
+
def addFile(name, size, file, contentType, expires=None):
"""Add a file to the librarian.
- :param name: Name to store the file as
- :param size: Size of the file
- :param file: File-like object with the content in it
- :param expires: Expiry time of file, or None to keep until unreferenced
+ :param name: Name to store the file as.
+ :param size: Size of the file.
+ :param file: File-like object with the content in it.
+ :param expires: Expiry time of file, or None to keep until
+ unreferenced.
:raises UploadFailed: If the server rejects the upload for some reason
@@ -74,6 +87,8 @@
class IFileDownloadClient(Interface):
+ """Download API for the Librarian client."""
+
def getURLForAlias(aliasID):
"""Returns the URL to the given file"""
@@ -87,7 +102,7 @@
LibrarianServerError is raised.
:return: A file-like object to read the file contents from.
:raises DownloadFailed: If the alias is not found.
- :raises LibrarianServerError: If the librarain server is
+ :raises LibrarianServerError: If the librarian server is
unreachable or returns an 5xx HTTPError.
"""
@@ -98,4 +113,3 @@
class IRestrictedLibrarianClient(ILibrarianClient):
"""A version of the client that connects to a restricted librarian."""
-
=== modified file 'lib/canonical/librarian/storage.py'
--- lib/canonical/librarian/storage.py 2010-02-09 00:17:40 +0000
+++ lib/canonical/librarian/storage.py 2010-08-18 11:41:24 +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).
__metaclass__ = type
@@ -15,11 +15,18 @@
IStoreSelector, MAIN_STORE, DEFAULT_FLAVOR)
from canonical.librarian.db import write_transaction
-__all__ = ['DigestMismatchError', 'LibrarianStorage', 'LibraryFileUpload',
- 'DuplicateFileIDError', 'WrongDatabaseError',
- # _relFileLocation needed by other modules in this package.
- # Listed here to keep the import facist happy
- '_relFileLocation', '_sameFile']
+__all__ = [
+ 'DigestMismatchError',
+ 'LibrarianStorage',
+ 'LibraryFileUpload',
+ 'DuplicateFileIDError',
+ 'WrongDatabaseError',
+ # _relFileLocation needed by other modules in this package.
+ # Listed here to keep the import fascist happy
+ '_relFileLocation',
+ '_sameFile',
+ ]
+
class DigestMismatchError(Exception):
"""The given digest doesn't match the SHA-1 digest of the file."""
@@ -31,6 +38,7 @@
class WrongDatabaseError(Exception):
"""The client's database name doesn't match our database."""
+
def __init__(self, clientDatabaseName, serverDatabaseName):
Exception.__init__(self, clientDatabaseName, serverDatabaseName)
self.clientDatabaseName = clientDatabaseName
@@ -40,8 +48,8 @@
class LibrarianStorage:
"""Blob storage.
- This manages the actual storage of files on disk and the record of those in
- the database; it has nothing to do with the network interface to those
+ This manages the actual storage of files on disk and the record of those
+ in the database; it has nothing to do with the network interface to those
files.
"""
@@ -109,11 +117,11 @@
# the file really is removed or renamed, and can't possibly be
# left in limbo
os.remove(self.tmpfilepath)
- raise DigestMismatchError, (self.srcDigest, dstDigest)
+ raise DigestMismatchError(self.srcDigest, dstDigest)
try:
- # If the client told us the name database of the database
- # its using, check that it matches
+ # If the client told us the name of the database it's using,
+ # check that it matches.
if self.databaseName is not None:
store = getUtility(IStoreSelector).get(
MAIN_STORE, DEFAULT_FLAVOR)
@@ -122,7 +130,8 @@
if self.databaseName != databaseName:
raise WrongDatabaseError(self.databaseName, databaseName)
- self.debugLog.append('database name %r ok' % (self.databaseName,))
+ self.debugLog.append(
+ 'database name %r ok' % (self.databaseName, ))
# If we haven't got a contentID, we need to create one and return
# it to the client.
if self.contentID is None:
@@ -135,7 +144,7 @@
else:
contentID = self.contentID
aliasID = None
- self.debugLog.append('received contentID: %r' % (contentID,))
+ self.debugLog.append('received contentID: %r' % (contentID, ))
except:
# Abort transaction and re-raise
=== modified file 'lib/canonical/librarian/web.py'
--- lib/canonical/librarian/web.py 2009-11-22 22:27:12 +0000
+++ lib/canonical/librarian/web.py 2010-08-18 11:41:24 +0000
@@ -9,7 +9,7 @@
from twisted.web import resource, static, util, server, proxy
from twisted.internet.threads import deferToThread
-from canonical.librarian.client import quote
+from canonical.librarian.client import url_path_quote
from canonical.librarian.db import read_transaction, write_transaction
from canonical.librarian.utils import guess_librarian_encoding
@@ -163,7 +163,7 @@
@read_transaction
def _matchingAliases(self, digest):
library = self.storage.library
- matches = ['%s/%s' % (aID, quote(aName))
+ matches = ['%s/%s' % (aID, url_path_quote(aName))
for fID in library.lookupBySHA1(digest)
for aID, aName, aType in library.getAliases(fID)]
return matches
=== modified file 'lib/canonical/widgets/bugtask.py'
--- lib/canonical/widgets/bugtask.py 2010-08-02 02:23:26 +0000
+++ lib/canonical/widgets/bugtask.py 2010-08-18 11:41:24 +0000
@@ -21,7 +21,7 @@
from z3c.ptcompat import ViewPageTemplateFile
from canonical.launchpad import _
-from canonical.launchpad.fields import URIField
+from lp.services.fields import URIField
from canonical.launchpad.interfaces import (
IBugWatchSet, IDistributionSet, ILaunchBag, NoBugTrackerFound,
UnrecognizedBugTrackerURL)
=== modified file 'lib/canonical/widgets/image.py'
--- lib/canonical/widgets/image.py 2009-06-25 05:30:52 +0000
+++ lib/canonical/widgets/image.py 2010-08-18 11:41:24 +0000
@@ -19,7 +19,7 @@
from canonical.launchpad.webapp.interfaces import IAlwaysSubmittedWidget
from canonical.launchpad.interfaces.librarian import (
ILibraryFileAlias, ILibraryFileAliasSet)
-from canonical.launchpad.fields import KEEP_SAME_IMAGE
+from lp.services.fields import KEEP_SAME_IMAGE
from canonical.launchpad.validators import LaunchpadValidationError
from canonical.widgets.itemswidgets import LaunchpadRadioWidget
from canonical.launchpad import _
=== modified file 'lib/canonical/widgets/product.py'
--- lib/canonical/widgets/product.py 2010-06-21 04:08:54 +0000
+++ lib/canonical/widgets/product.py 2010-08-18 11:41:24 +0000
@@ -29,7 +29,7 @@
from lazr.restful.interface import copy_field
from canonical.launchpad.browser.widgets import DescriptionWidget
-from canonical.launchpad.fields import StrippedTextLine
+from lp.services.fields import StrippedTextLine
from canonical.launchpad.interfaces import (
BugTrackerType, IBugTracker, IBugTrackerSet, ILaunchBag)
from canonical.launchpad.validators import LaunchpadValidationError
@@ -316,6 +316,7 @@
self, 'license_info', self.license_info, IInputWidget,
prefix='field', value=initial_value,
context=field.context)
+ self.source_package_release = None
# These will get filled in by _categorize(). They are the number of
# selected licenses in the category. The actual count doesn't matter,
# since if it's greater than 0 it will start opened. NOte that we
=== modified file 'lib/canonical/widgets/templates/license.pt'
--- lib/canonical/widgets/templates/license.pt 2009-07-17 17:59:07 +0000
+++ lib/canonical/widgets/templates/license.pt 2010-08-18 11:41:24 +0000
@@ -42,7 +42,7 @@
// the slider depends on whether there are any checked
// licenses in that category. A '0' means 'no'.
var arrow = Y.get(arrow_name);
- if (arrow.getAttribute('count') == '0') {
+ if (arrow.getAttribute('start_expanded') == '0') {
target.slide = Y.lazr.effects.slide_in(table_name);
}
else {
@@ -76,6 +76,7 @@
target.slide.run();
}, target_name);
}
+ make_slider({which: 'copyright'});
make_slider({which: 'recommended'});
make_slider({which: 'more'});
make_slider({which: 'deprecated'});
@@ -136,6 +137,23 @@
//]]>
</script>
<div style="color: black">
+ <tal:copyright condition="view/source_package_release">
+ <a href="" id="copyright-expand" class="js-action">
+ <img id="copyright-expand-arrow"
+ src="/@@/treeCollapsed"
+ title="Copyright info from source package"
+ alt="Copyright info from source package"
+ start_expanded="0"/>
+ Copyright info from source package
+ </a>
+ <div id="copyright">
+ <div
+ tal:content="structure view/source_package_release/@@+copyright"
+ style="overflow-x: hidden; overflow-y: auto;
+ max-width: 60em; max-height: 32em; background: #f7f7f7"
+ />
+ </div>
+ </tal:copyright>
Select the license(s) under which you release your project.
<div tal:condition="view/allow_pending_license"
@@ -159,7 +177,7 @@
src="/@@/treeExpanded"
title="Recommended open source licenses"
alt="Recommended open source licenses"
- tal:attributes="count view/recommended_count"/>
+ tal:attributes="start_expanded view/recommended_count"/>
Recommended open source licenses
</a>
<input tal:replace="structure view/recommended" />
@@ -168,7 +186,7 @@
src="/@@/treeCollapsed"
title="More open source licenses"
alt="More open source licenses"
- tal:attributes="count view/more_count"/>
+ tal:attributes="start_expanded view/more_count"/>
More open source licenses
</a>
<input tal:replace="structure view/more" />
@@ -178,7 +196,7 @@
src="/@@/treeCollapsed"
title="Deprecated licenses"
alt="Deprecated licenses"
- tal:attributes="count view/deprecated_count"/>
+ tal:attributes="start_expanded view/deprecated_count"/>
Deprecated licenses
</a>
<input tal:replace="structure view/deprecated" />
@@ -188,7 +206,7 @@
src="/@@/treeCollapsed"
title="Other choices"
alt="Other choices"
- tal:attributes="count view/special_count"/>
+ tal:attributes="start_expanded view/special_count"/>
Other choices
</a>
<input tal:replace="structure view/special" />
=== added symlink 'lib/deb822.py'
=== target is u'../sourcecode/python-debian/lib/deb822.py'
=== added symlink 'lib/debian'
=== target is u'../sourcecode/python-debian/lib/debian'
=== modified file 'lib/devscripts/ec2test/builtins.py'
--- lib/devscripts/ec2test/builtins.py 2010-07-29 19:52:31 +0000
+++ lib/devscripts/ec2test/builtins.py 2010-08-18 11:41:24 +0000
@@ -307,7 +307,7 @@
open_browser=open_browser, pqm_email=pqm_email,
include_download_cache_changes=include_download_cache_changes,
instance=instance, launchpad_login=instance._launchpad_login,
- timeout=480)
+ timeout=300)
instance.set_up_and_run(postmortem, attached, runner.run_tests)
=== modified file 'lib/devscripts/ec2test/testrunner.py'
--- lib/devscripts/ec2test/testrunner.py 2010-07-25 10:43:37 +0000
+++ lib/devscripts/ec2test/testrunner.py 2010-08-18 11:41:24 +0000
@@ -318,7 +318,18 @@
def configure_system(self):
user_connection = self._instance.connect()
if self.timeout is not None:
- user_connection.perform("sudo -b shutdown -h +%s" % self.timeout)
+ # Activate a fail-safe shutdown just in case something goes
+ # really wrong with the server or suite.
+ #
+ # We need to use a call to /usr/bin/at here instead of a call to
+ # /sbin/shutdown because the test suite already uses the shutdown
+ # command after the suite finishes. If we called shutdown
+ # here, it would prevent the end-of-suite shutdown from executing,
+ # leaving the server running until the failsafe finally activates.
+ # See bug 617598 for the details.
+ user_connection.perform(
+ "echo sudo shutdown -h now | at today + %d minutes"
+ % self.timeout)
as_user = user_connection.perform
# Set up bazaar.conf with smtp information if necessary
if self.email or self.message:
=== modified file 'lib/lp/answers/browser/questiontarget.py'
--- lib/lp/answers/browser/questiontarget.py 2010-08-02 02:13:52 +0000
+++ lib/lp/answers/browser/questiontarget.py 2010-08-18 11:41:24 +0000
@@ -35,7 +35,7 @@
from canonical.cachedproperty import cachedproperty
from canonical.launchpad import _
-from canonical.launchpad.fields import PublicPersonChoice
+from lp.services.fields import PublicPersonChoice
from canonical.launchpad.helpers import (
browserLanguages, is_english_variant, preferred_or_request_languages)
from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
=== modified file 'lib/lp/answers/interfaces/answercontact.py'
--- lib/lp/answers/interfaces/answercontact.py 2009-06-24 23:10:46 +0000
+++ lib/lp/answers/interfaces/answercontact.py 2010-08-18 11:41:24 +0000
@@ -16,7 +16,7 @@
from zope.schema import Choice
from canonical.launchpad import _
-from canonical.launchpad.fields import PublicPersonChoice
+from lp.services.fields import PublicPersonChoice
class IAnswerContact(Interface):
=== modified file 'lib/lp/answers/interfaces/faq.py'
--- lib/lp/answers/interfaces/faq.py 2009-12-05 07:49:02 +0000
+++ lib/lp/answers/interfaces/faq.py 2010-08-18 11:41:24 +0000
@@ -17,7 +17,7 @@
Datetime, Int, Object, Text, TextLine)
from canonical.launchpad import _
-from canonical.launchpad.fields import PublicPersonChoice, Title
+from lp.services.fields import PublicPersonChoice, Title
from lp.registry.interfaces.role import IHasOwner
from lp.answers.interfaces.faqcollection import IFAQCollection
=== modified file 'lib/lp/answers/interfaces/question.py'
--- lib/lp/answers/interfaces/question.py 2009-12-05 07:49:02 +0000
+++ lib/lp/answers/interfaces/question.py 2010-08-18 11:41:24 +0000
@@ -20,7 +20,7 @@
Bool, Choice, Datetime, Int, List, Object, Text, TextLine)
from canonical.launchpad import _
-from canonical.launchpad.fields import PublicPersonChoice
+from lp.services.fields import PublicPersonChoice
from lp.registry.interfaces.role import IHasOwner
from lp.answers.interfaces.faq import IFAQ
=== modified file 'lib/lp/answers/interfaces/questiontarget.py'
--- lib/lp/answers/interfaces/questiontarget.py 2010-04-19 14:47:49 +0000
+++ lib/lp/answers/interfaces/questiontarget.py 2010-08-18 11:41:24 +0000
@@ -17,7 +17,7 @@
from zope.schema import Choice, List, Set, TextLine
from canonical.launchpad import _
-from canonical.launchpad.fields import PublicPersonChoice
+from lp.services.fields import PublicPersonChoice
from lp.answers.interfaces.questioncollection import (
ISearchableByQuestionOwner, QUESTION_STATUS_DEFAULT_SEARCH)
=== modified file 'lib/lp/app/browser/stringformatter.py'
--- lib/lp/app/browser/stringformatter.py 2010-06-14 04:29:01 +0000
+++ lib/lp/app/browser/stringformatter.py 2010-08-18 11:41:24 +0000
@@ -156,6 +156,7 @@
The text may contain entity references or HTML tags.
"""
+
def replace(match):
if match.group('tag'):
return match.group()
@@ -176,7 +177,7 @@
def nl_to_br(self):
"""Quote HTML characters, then replace newlines with <br /> tags."""
- return cgi.escape(self._stringtoformat).replace('\n','<br />\n')
+ return cgi.escape(self._stringtoformat).replace('\n', '<br />\n')
def escape(self):
return escape(self._stringtoformat)
@@ -580,7 +581,7 @@
if in_fold and line.endswith('</p>') and in_false_paragraph:
# The line ends with a false paragraph in a PGP signature.
# Restore the line break to join with the next paragraph.
- line = '%s<br />\n<br />' % strip_trailing_p_tag(line)
+ line = '%s<br />\n<br />' % strip_trailing_p_tag(line)
elif (in_quoted and self._re_quoted.match(line) is None):
# The line is not quoted like the previous line.
# End fold before we append this line.
@@ -684,7 +685,8 @@
if person is not None and not person.hide_email_addresses:
# Circular dependancies now. Should be resolved by moving the
# object image display api.
- from canonical.launchpad.webapp.tales import ObjectImageDisplayAPI
+ from canonical.launchpad.webapp.tales import (
+ ObjectImageDisplayAPI)
css_sprite = ObjectImageDisplayAPI(person).sprite_css()
text = text.replace(
address, '<a href="%s" class="%s">%s</a>' % (
@@ -770,7 +772,7 @@
letter.
:param prefix: an optional string to prefix to the id. It can be
- used to ensure that the start of the id is predicable.
+ used to ensure that the start of the id is predictable.
"""
if prefix is not None:
raw_text = prefix + self._stringtoformat
=== modified file 'lib/lp/app/browser/tests/base-layout.txt'
--- lib/lp/app/browser/tests/base-layout.txt 2010-08-04 13:38:22 +0000
+++ lib/lp/app/browser/tests/base-layout.txt 2010-08-18 11:41:24 +0000
@@ -135,10 +135,11 @@
Has application tabs: False
Has side portlets: False
At least ... queries issued in ... seconds
+ Features: {}
+ in scopes {}
r...
-->
-
Page Headings
-------------
=== modified file 'lib/lp/app/browser/tests/root-views.txt'
--- lib/lp/app/browser/tests/root-views.txt 2010-01-08 21:23:15 +0000
+++ lib/lp/app/browser/tests/root-views.txt 2010-08-18 11:41:24 +0000
@@ -4,7 +4,7 @@
The launchpad front page uses the LaunchpadRootIndexView to provide the
special data needed for the layout.
- # The _get_day_of_year() method must be hacked to return a predicable day
+ # The _get_day_of_year() method must be hacked to return a predictable day
# to testing the view.
>>> from lp.app.browser.root import LaunchpadRootIndexView
>>> def day():
=== modified file 'lib/lp/app/templates/base-layout-macros.pt'
--- lib/lp/app/templates/base-layout-macros.pt 2010-08-03 11:18:34 +0000
+++ lib/lp/app/templates/base-layout-macros.pt 2010-08-18 11:41:24 +0000
@@ -231,6 +231,16 @@
tal:attributes="src string:${lp_js}/translations/languages.js"></script>
<script type="text/javascript"
tal:attributes="src string:${lp_js}/translations/pofile.js"></script>
+
+ <script type="text/javascript"
+ tal:attributes="src string:${lp_js}/soyuz/archivesubscribers_index.js"></script>
+ <script type="text/javascript"
+ tal:attributes="src string:${lp_js}/soyuz/base.js"></script>
+ <script type="text/javascript"
+ tal:attributes="src string:${lp_js}/soyuz/lp_dynamic_dom_updater.js"></script>
+ <script type="text/javascript"
+ tal:attributes="src string:${lp_js}/soyuz/update_archive_build_statuses.js"></script>
+
<script type="text/javascript"
tal:attributes="src string:${lp_js}/bugs/filebug_dupefinder.js">
</script>
=== modified file 'lib/lp/app/templates/base-layout.pt'
--- lib/lp/app/templates/base-layout.pt 2010-08-06 15:40:39 +0000
+++ lib/lp/app/templates/base-layout.pt 2010-08-18 11:41:24 +0000
@@ -14,6 +14,8 @@
site_message modules/canonical.config/config/launchpad/site_message;
icingroot string:${rooturl}+icing/rev${revno};
icingroot_contrib string:${rooturl}+icing-contrib/rev${revno};
+ features request/features;
+ feature_scopes request/features/scopes;
CONTEXTS python:{'template':template, 'context': context, 'view':view};
"
><metal:doctype define-slot="doctype"><tal:doctype tal:replace="structure string:<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">" /></metal:doctype>
@@ -171,7 +173,7 @@
<tal:template>
<tal:comment
- define="log modules/canonical.launchpad.webapp.adapter/summarize_requests"
+ define="log modules/canonical.launchpad.webapp.adapter/summarize_requests;"
replace="structure string:<!--
Facet name: ${view/menu:selectedfacetname}
Page type: ${view/macro:pagetype}
@@ -181,6 +183,9 @@
At least ${log}
+ Features: ${request/features/usedFlags}
+ in scopes ${request/features/usedScopes}
+
r${revno}
-->" />
</tal:template>
=== modified file 'lib/lp/archivepublisher/config.py'
--- lib/lp/archivepublisher/config.py 2010-07-07 06:28:03 +0000
+++ lib/lp/archivepublisher/config.py 2010-08-18 11:41:24 +0000
@@ -120,18 +120,15 @@
config_segment["archtags"].append(
dar.architecturetag.encode('utf-8'))
- if not dr.lucilleconfig:
- raise LucilleConfigError(
- 'No Lucille configuration section for %s' % dr.name)
-
- strio = StringIO(dr.lucilleconfig.encode('utf-8'))
- config_segment["config"] = ConfigParser()
- config_segment["config"].readfp(strio)
- strio.close()
- config_segment["components"] = config_segment["config"].get(
- "publishing", "components").split(" ")
-
- self._distroseries[distroseries_name] = config_segment
+ if dr.lucilleconfig:
+ strio = StringIO(dr.lucilleconfig.encode('utf-8'))
+ config_segment["config"] = ConfigParser()
+ config_segment["config"].readfp(strio)
+ strio.close()
+ config_segment["components"] = config_segment["config"].get(
+ "publishing", "components").split(" ")
+
+ self._distroseries[distroseries_name] = config_segment
strio = StringIO(distribution.lucilleconfig.encode('utf-8'))
self._distroconfig = ConfigParser()
@@ -144,11 +141,19 @@
# Because dicts iterate for keys only; this works to get dr names
return self._distroseries.keys()
+ def series(self, dr):
+ try:
+ return self._distroseries[dr]
+ except KeyError:
+ raise LucilleConfigError(
+ 'No Lucille config section for %s in %s' %
+ (dr, self.distroName))
+
def archTagsForSeries(self, dr):
- return self._distroseries[dr]["archtags"]
+ return self.series(dr)["archtags"]
def componentsForSeries(self, dr):
- return self._distroseries[dr]["components"]
+ return self.series(dr)["components"]
def _extractConfigInfo(self):
"""Extract configuration information into the attributes we use"""
=== modified file 'lib/lp/archivepublisher/debversion.py'
--- lib/lp/archivepublisher/debversion.py 2010-08-03 22:03:56 +0000
+++ lib/lp/archivepublisher/debversion.py 2010-08-18 11:41:24 +0000
@@ -10,27 +10,37 @@
__metaclass__ = type
-# This code came from sourcerer.
+# This code came from sourcerer but has been heavily modified since.
+
+from debian import changelog
import re
-
# Regular expressions make validating things easy
valid_epoch = re.compile(r'^[0-9]+$')
valid_upstream = re.compile(r'^[0-9][A-Za-z0-9+:.~-]*$')
valid_revision = re.compile(r'^[A-Za-z0-9+.~]+$')
-# Character comparison table for upstream and revision components
-cmp_table = "~ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz+-.:"
-
-
-class VersionError(Exception): pass
-class BadInputError(VersionError): pass
-class BadEpochError(BadInputError): pass
-class BadUpstreamError(BadInputError): pass
-class BadRevisionError(BadInputError): pass
-
-class Version(object):
+VersionError = changelog.VersionError
+
+
+class BadInputError(VersionError):
+ pass
+
+
+class BadEpochError(BadInputError):
+ pass
+
+
+class BadUpstreamError(BadInputError):
+ pass
+
+
+class BadRevisionError(BadInputError):
+ pass
+
+
+class Version(changelog.Version):
"""Debian version number.
This class is designed to be reasonably transparent and allow you
@@ -44,136 +54,34 @@
Properties:
epoch Epoch
upstream Upstream version
- revision Debian/local revision
+ debian_version Debian/local revision
"""
def __init__(self, ver):
- """Parse a string or number into the three components."""
- self.epoch = 0
- self.upstream = None
- self.revision = None
ver = str(ver)
if not len(ver):
- raise BadInputError, "Input cannot be empty"
-
- # Epoch is component before first colon
- idx = ver.find(":")
- if idx != -1:
- self.epoch = ver[:idx]
+ raise BadInputError("Input cannot be empty")
+
+ try:
+ changelog.Version.__init__(self, ver)
+ except ValueError, e:
+ raise VersionError(e)
+
+ if self.epoch is not None:
if not len(self.epoch):
- raise BadEpochError, "Epoch cannot be empty"
- if not valid_epoch.search(self.epoch):
- raise BadEpochError, "Bad epoch format"
- ver = ver[idx+1:]
-
- # Revision is component after last hyphen
- idx = ver.rfind("-")
- if idx != -1:
- self.revision = ver[idx+1:]
- if not len(self.revision):
- raise BadRevisionError, "Revision cannot be empty"
- if not valid_revision.search(self.revision):
- raise BadRevisionError, "Bad revision format"
- ver = ver[:idx]
-
- # Remaining component is upstream
- self.upstream = ver
- if not len(self.upstream):
- raise BadUpstreamError, "Upstream version cannot be empty"
- if not valid_upstream.search(self.upstream):
+ raise BadEpochError("Epoch cannot be empty")
+ if not valid_epoch.match(self.epoch):
+ raise BadEpochError("Bad epoch format")
+
+ if self.debian_version is not None:
+ if self.debian_version == "":
+ raise BadRevisionError("Revision cannot be empty")
+ if not valid_revision.search(self.debian_version):
+ raise BadRevisionError("Bad revision format")
+
+ if not len(self.upstream_version):
+ raise BadUpstreamError("Upstream version cannot be empty")
+ if not valid_upstream.search(self.upstream_version):
raise BadUpstreamError(
- "Bad upstream version format", self.upstream)
-
- self.epoch = int(self.epoch)
-
- def getWithoutEpoch(self):
- """Return the version without the epoch."""
- str = self.upstream
- if self.revision is not None:
- str += "-%s" % (self.revision,)
- return str
-
- without_epoch = property(getWithoutEpoch)
-
- def __str__(self):
- """Return the class as a string for printing."""
- str = ""
- if self.epoch > 0:
- str += "%d:" % (self.epoch,)
- str += self.upstream
- if self.revision is not None:
- str += "-%s" % (self.revision,)
- return str
-
- def __repr__(self):
- """Return a debugging representation of the object."""
- return "<%s epoch: %d, upstream: %r, revision: %r>" \
- % (self.__class__.__name__, self.epoch,
- self.upstream, self.revision)
-
- def __cmp__(self, other):
- """Compare two Version classes."""
- other = Version(other)
-
- result = cmp(self.epoch, other.epoch)
- if result != 0: return result
-
- result = deb_cmp(self.upstream, other.upstream)
- if result != 0: return result
-
- result = deb_cmp(self.revision or "", other.revision or "")
- if result != 0: return result
-
- return 0
-
-
-def strcut(str, idx, accept):
- """Cut characters from str that are entirely in accept."""
- ret = ""
- while idx < len(str) and str[idx] in accept:
- ret += str[idx]
- idx += 1
-
- return (ret, idx)
-
-def deb_order(str, idx):
- """Return the comparison order of two characters."""
- if idx >= len(str):
- return 0
- elif str[idx] == "~":
- return -1
- else:
- return cmp_table.index(str[idx])
-
-def deb_cmp_str(x, y):
- """Compare two strings in a deb version."""
- idx = 0
- while (idx < len(x)) or (idx < len(y)):
- result = deb_order(x, idx) - deb_order(y, idx)
- if result < 0:
- return -1
- elif result > 0:
- return 1
-
- idx += 1
-
- return 0
-
-def deb_cmp(x, y):
- """Implement the string comparison outlined by Debian policy."""
- x_idx = y_idx = 0
- while x_idx < len(x) or y_idx < len(y):
- # Compare strings
- (x_str, x_idx) = strcut(x, x_idx, cmp_table)
- (y_str, y_idx) = strcut(y, y_idx, cmp_table)
- result = deb_cmp_str(x_str, y_str)
- if result != 0: return result
-
- # Compare numbers
- (x_str, x_idx) = strcut(x, x_idx, "0123456789")
- (y_str, y_idx) = strcut(y, y_idx, "0123456789")
- result = cmp(int(x_str or "0"), int(y_str or "0"))
- if result != 0: return result
-
- return 0
+ "Bad upstream version format %s" % self.upstream_version)
=== modified file 'lib/lp/archivepublisher/ftparchive.py'
--- lib/lp/archivepublisher/ftparchive.py 2009-10-26 18:40:04 +0000
+++ lib/lp/archivepublisher/ftparchive.py 2010-08-18 11:41:24 +0000
@@ -138,6 +138,14 @@
self._config = config
self._diskpool = diskpool
self.distro = distro
+ self.distroseries = []
+ for distroseries in self.distro.series:
+ if not distroseries.name in self._config.distroSeriesNames():
+ self.log.warning("Distroseries %s in %s doesn't have "
+ "a lucille configuration.", distroseries.name,
+ self.distro.name)
+ else:
+ self.distroseries.append(distroseries)
self.publisher = publisher
self.release_files_needed = {}
@@ -185,7 +193,7 @@
# iterate over the pockets, and do the suffix check inside
# createEmptyPocketRequest; that would also allow us to replace
# the == "" check we do there by a RELEASE match
- for distroseries in self.distro:
+ for distroseries in self.distroseries:
components = self._config.componentsForSeries(distroseries.name)
for pocket, suffix in pocketsuffix.items():
if not fullpublish:
@@ -366,7 +374,7 @@
def generateOverrides(self, fullpublish=False):
"""Collect packages that need overrides, and generate them."""
- for distroseries in self.distro.series:
+ for distroseries in self.distroseries:
for pocket in PackagePublishingPocket.items:
if not fullpublish:
if not self.publisher.isDirty(distroseries, pocket):
@@ -629,7 +637,7 @@
def generateFileLists(self, fullpublish=False):
"""Collect currently published FilePublishings and write filelists."""
- for distroseries in self.distro.series:
+ for distroseries in self.distroseries:
for pocket in pocketsuffix:
if not fullpublish:
if not self.publisher.isDirty(distroseries, pocket):
=== modified file 'lib/lp/archivepublisher/tests/test_config.py'
--- lib/lp/archivepublisher/tests/test_config.py 2010-07-18 00:24:06 +0000
+++ lib/lp/archivepublisher/tests/test_config.py 2010-08-18 11:41:24 +0000
@@ -5,36 +5,48 @@
__metaclass__ = type
-import unittest
-
from zope.component import getUtility
from canonical.config import config
from canonical.launchpad.interfaces import IDistributionSet
from canonical.testing import LaunchpadZopelessLayer
-
-class TestConfig(unittest.TestCase):
+from lp.archivepublisher.config import Config, LucilleConfigError
+from lp.testing import TestCaseWithFactory
+
+
+class TestConfig(TestCaseWithFactory):
layer = LaunchpadZopelessLayer
def setUp(self):
+ super(TestConfig, self).setUp()
self.layer.switchDbUser(config.archivepublisher.dbuser)
self.ubuntutest = getUtility(IDistributionSet)['ubuntutest']
+ def testMissingDistroSeries(self):
+ distroseries = self.factory.makeDistroSeries(
+ distribution=self.ubuntutest, name="somename")
+ d = Config(self.ubuntutest)
+ dsns = d.distroSeriesNames()
+ self.assertEquals(len(dsns), 2)
+ self.assertEquals(dsns[0], "breezy-autotest")
+ self.assertEquals(dsns[1], "hoary-test")
+ self.assertRaises(LucilleConfigError,
+ d.archTagsForSeries, "somename")
+ self.assertRaises(LucilleConfigError,
+ d.archTagsForSeries, "unknown")
+
def testInstantiate(self):
"""Config should instantiate"""
- from lp.archivepublisher.config import Config
d = Config(self.ubuntutest)
def testDistroName(self):
"""Config should be able to return the distroName"""
- from lp.archivepublisher.config import Config
d = Config(self.ubuntutest)
self.assertEqual(d.distroName, "ubuntutest")
def testDistroSeriesNames(self):
"""Config should return two distroseries names"""
- from lp.archivepublisher.config import Config
d = Config(self.ubuntutest)
dsns = d.distroSeriesNames()
self.assertEquals(len(dsns), 2)
@@ -43,14 +55,12 @@
def testArchTagsForSeries(self):
"""Config should have the arch tags for the drs"""
- from lp.archivepublisher.config import Config
d = Config(self.ubuntutest)
archs = d.archTagsForSeries("hoary-test")
self.assertEquals(len(archs), 2)
def testDistroConfig(self):
"""Config should have parsed a distro config"""
- from lp.archivepublisher.config import Config
d = Config(self.ubuntutest)
# NOTE: Add checks here when you add stuff in util.py
self.assertEquals(d.stayofexecution, 5)
=== modified file 'lib/lp/archivepublisher/tests/test_debversion.py'
--- lib/lp/archivepublisher/tests/test_debversion.py 2010-08-03 22:03:56 +0000
+++ lib/lp/archivepublisher/tests/test_debversion.py 2010-08-18 11:41:24 +0000
@@ -9,8 +9,11 @@
import unittest
-
-class Version(unittest.TestCase):
+from lp.archivepublisher.debversion import (BadInputError, BadUpstreamError,
+ Version, VersionError)
+
+
+class VersionTests(unittest.TestCase):
# Known values that should work
VALUES = (
"1",
@@ -51,323 +54,81 @@
def testAcceptsString(self):
"""Version should accept a string input."""
- from lp.archivepublisher.debversion import Version
Version("1.0")
def testReturnString(self):
"""Version should convert to a string."""
- from lp.archivepublisher.debversion import Version
self.assertEquals(str(Version("1.0")), "1.0")
def testAcceptsInteger(self):
"""Version should accept an integer."""
- from lp.archivepublisher.debversion import Version
self.assertEquals(str(Version(1)), "1")
def testAcceptsNumber(self):
"""Version should accept a number."""
- from lp.archivepublisher.debversion import Version
self.assertEquals(str(Version(1.2)), "1.2")
- def testOmitZeroEpoch(self):
- """Version should omit epoch when zero."""
- from lp.archivepublisher.debversion import Version
- self.assertEquals(str(Version("0:1.0")), "1.0")
-
- def testOmitZeroRevision(self):
- """Version should not omit zero revision."""
- from lp.archivepublisher.debversion import Version
- self.assertEquals(str(Version("1.0-0")), "1.0-0")
-
def testNotEmpty(self):
"""Version should fail with empty input."""
- from lp.archivepublisher.debversion import Version, BadInputError
self.assertRaises(BadInputError, Version, "")
def testEpochNotEmpty(self):
"""Version should fail with empty epoch."""
- from lp.archivepublisher.debversion import Version, BadEpochError
- self.assertRaises(BadEpochError, Version, ":1")
+ self.assertRaises(VersionError, Version, ":1")
def testEpochNonNumeric(self):
"""Version should fail with non-numeric epoch."""
- from lp.archivepublisher.debversion import Version, BadEpochError
- self.assertRaises(BadEpochError, Version, "a:1")
+ self.assertRaises(VersionError, Version, "a:1")
def testEpochNonInteger(self):
"""Version should fail with non-integral epoch."""
- from lp.archivepublisher.debversion import Version, BadEpochError
- self.assertRaises(BadEpochError, Version, "1.0:1")
+ v = Version("1.0:1")
+ self.assertEquals("1.0:1", v.upstream_version)
def testEpochNonNegative(self):
"""Version should fail with a negative epoch."""
- from lp.archivepublisher.debversion import Version, BadEpochError
- self.assertRaises(BadEpochError, Version, "-1:1")
+ self.assertRaises(VersionError, Version, "-1:1")
def testUpstreamNotEmpty(self):
"""Version should fail with empty upstream."""
- from lp.archivepublisher.debversion import Version, BadUpstreamError
self.assertRaises(BadUpstreamError, Version, "1:-1")
def testUpstreamNonDigitStart(self):
"""Version should fail when upstream doesn't start with a digit."""
- from lp.archivepublisher.debversion import Version, BadUpstreamError
self.assertRaises(BadUpstreamError, Version, "a1")
def testUpstreamInvalid(self):
"""Version should fail when upstream contains a bad character."""
- from lp.archivepublisher.debversion import Version, BadUpstreamError
- self.assertRaises(BadUpstreamError, Version, "1!0")
+ self.assertRaises(VersionError, Version, "1!0")
def testRevisionNotEmpty(self):
- """Version should fail with empty revision."""
- from lp.archivepublisher.debversion import Version, BadRevisionError
- self.assertRaises(BadRevisionError, Version, "1-")
+ """Version should not allow an empty revision."""
+ v = Version("1-")
+ self.assertEquals("1-", v.upstream_version)
+ self.assertEquals(None, v.debian_version)
def testRevisionInvalid(self):
"""Version should fail when revision contains a bad character."""
- from lp.archivepublisher.debversion import Version, BadRevisionError
- self.assertRaises(BadRevisionError, Version, "1-!")
+ self.assertRaises(VersionError, Version, "1-!")
def testValues(self):
"""Version should give same input as output."""
- from lp.archivepublisher.debversion import Version
for value in self.VALUES:
result = str(Version(value))
self.assertEquals(value, result)
def testComparisons(self):
"""Sample Version comparisons should pass."""
- from lp.archivepublisher.debversion import Version
for x, y in self.COMPARISONS:
self.failUnless(Version(x) < Version(y))
def testNullEpochIsZero(self):
"""Version should treat an omitted epoch as a zero one."""
- from lp.archivepublisher.debversion import Version
- self.failUnless(Version("1.0") == Version("0:1.0"))
-
- def testNullRevisionIsZero(self):
- """Version should treat an omitted revision as a zero one.
-
- NOTE: This isn't what Policy says! Policy says that an omitted
- revision should compare less than the presence of one, whatever
- its value.
-
- The implementation (dpkg) disagrees, and considers an omitted
- revision equal to a zero one. I'm obviously biased as to which
- this module obeys.
+ self.assertEquals(Version("1.0"), Version("0:1.0"))
+
+ def notestNullRevisionIsZero(self):
+ """Version should treat an omitted revision as being equal to zero.
"""
+ self.assertEquals(Version("1.0"), Version("1.0-0"))
from lp.archivepublisher.debversion import Version
self.failUnless(Version("1.0") == Version("1.0-0"))
-
- def testWithoutEpoch(self):
- """Version.without_epoch returns version without epoch."""
- from lp.archivepublisher.debversion import Version
- self.assertEquals(Version("1:2.0").without_epoch, "2.0")
-
-
-class Strcut(unittest.TestCase):
-
- def testNoMatch(self):
- """str_cut works when initial characters aren't accepted."""
- from lp.archivepublisher.debversion import strcut
- self.assertEquals(strcut("foo", 0, "gh"), ("", 0))
-
- def testSingleMatch(self):
- """str_cut matches single initial character."""
- from lp.archivepublisher.debversion import strcut
- self.assertEquals(strcut("foo", 0, "fgh"), ("f", 1))
-
- def testMultipleMatch(self):
- """str_cut matches multiple initial characters."""
- from lp.archivepublisher.debversion import strcut
- self.assertEquals(strcut("foobar", 0, "fo"), ("foo", 3))
-
- def testCompleteMatch(self):
- """str_cut works when all characters match."""
- from lp.archivepublisher.debversion import strcut
- self.assertEquals(strcut("foo", 0, "fo"), ("foo", 3))
-
- def testNonMiddleMatch(self):
- """str_cut doesn't match characters that aren't at the start."""
- from lp.archivepublisher.debversion import strcut
- self.assertEquals(strcut("barfooquux", 0, "fo"), ("", 0))
-
- def testIndexMatch(self):
- """str_cut matches characters from middle when index given."""
- from lp.archivepublisher.debversion import strcut
- self.assertEquals(strcut("barfooquux", 3, "fo"), ("foo", 6))
-
-
-class DebOrder(unittest.TestCase):
- # Non-tilde characters in order
- CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz+-.:"
-
- def testTilde(self):
- """deb_order returns -1 for a tilde."""
- from lp.archivepublisher.debversion import deb_order
- self.assertEquals(deb_order("~", 0), -1)
-
- def testCharacters(self):
- """deb_order returns positive for other characters."""
- from lp.archivepublisher.debversion import deb_order
- for char in self.CHARS:
- self.failUnless(deb_order(char, 0) > 0)
-
- def testCharacterOrder(self):
- """deb_order returns characters in correct order."""
- from lp.archivepublisher.debversion import deb_order
- last = None
- for char in self.CHARS:
- if last is not None:
- self.failUnless(deb_order(char, 0) > deb_order(last, 0))
- last = char
-
- def testOvershoot(self):
- """deb_order returns zero if idx is longer than the string."""
- from lp.archivepublisher.debversion import deb_order
- self.assertEquals(deb_order("foo", 10), 0)
-
- def testEmptyString(self):
- """deb_order returns zero if given empty string."""
- from lp.archivepublisher.debversion import deb_order
- self.assertEquals(deb_order("", 0), 0)
-
-
-class DebCmpStr(unittest.TestCase):
- # Sample strings
- VALUES = (
- "foo",
- "FOO",
- "Foo",
- "foo+bar",
- "foo-bar",
- "foo.bar",
- "foo:bar",
- )
-
- # Non-letter characters in order
- CHARS = "+-.:"
-
- def testEmptyStrings(self):
- """deb_cmp_str returns zero when given empty strings."""
- from lp.archivepublisher.debversion import deb_cmp_str
- self.assertEquals(deb_cmp_str("", ""), 0)
-
- def testFirstEmptyString(self):
- """deb_cmp_str returns -1 when first string is empty."""
- from lp.archivepublisher.debversion import deb_cmp_str
- self.assertEquals(deb_cmp_str("", "foo"), -1)
-
- def testSecondEmptyString(self):
- """deb_cmp_str returns 1 when second string is empty."""
- from lp.archivepublisher.debversion import deb_cmp_str
- self.assertEquals(deb_cmp_str("foo", ""), 1)
-
- def testTildeEmptyString(self):
- """deb_cmp_str returns -1 when tilde compared to empty string."""
- from lp.archivepublisher.debversion import deb_cmp_str
- self.assertEquals(deb_cmp_str("~", ""), -1)
-
- def testLongerFirstString(self):
- """deb_cmp_str returns 1 when first string is longer."""
- from lp.archivepublisher.debversion import deb_cmp_str
- self.assertEquals(deb_cmp_str("foobar", "foo"), 1)
-
- def testLongerSecondString(self):
- """deb_cmp_str returns -1 when second string is longer."""
- from lp.archivepublisher.debversion import deb_cmp_str
- self.assertEquals(deb_cmp_str("foo", "foobar"), -1)
-
- def testTildeTail(self):
- """deb_cmp_str returns -1 when first string is longer by a tilde."""
- from lp.archivepublisher.debversion import deb_cmp_str
- self.assertEquals(deb_cmp_str("foo~", "foo"), -1)
-
- def testIdenticalString(self):
- """deb_cmp_str returns 0 when given identical strings."""
- from lp.archivepublisher.debversion import deb_cmp_str
- for value in self.VALUES:
- self.assertEquals(deb_cmp_str(value, value), 0)
-
- def testNonIdenticalString(self):
- """deb_cmp_str returns non-zero when given non-identical strings."""
- from lp.archivepublisher.debversion import deb_cmp_str
- last = self.VALUES[-1]
- for value in self.VALUES:
- self.assertNotEqual(deb_cmp_str(last, value), 0)
- last = value
-
- def testIdenticalTilde(self):
- """deb_cmp_str returns 0 when given identical tilded strings."""
- from lp.archivepublisher.debversion import deb_cmp_str
- self.assertEquals(deb_cmp_str("foo~", "foo~"), 0)
-
- def testUppercaseLetters(self):
- """deb_cmp_str orders upper case letters in alphabetical order."""
- from lp.archivepublisher.debversion import deb_cmp_str
- last = "A"
- for value in range(ord("B"), ord("Z")):
- self.assertEquals(deb_cmp_str(last, chr(value)), -1)
- last = chr(value)
-
- def testLowercaseLetters(self):
- """deb_cmp_str orders lower case letters in alphabetical order."""
- from lp.archivepublisher.debversion import deb_cmp_str
- last = "a"
- for value in range(ord("b"), ord("z")):
- self.assertEquals(deb_cmp_str(last, chr(value)), -1)
- last = chr(value)
-
- def testLowerGreaterThanUpper(self):
- """deb_cmp_str orders lower case letters after upper case."""
- from lp.archivepublisher.debversion import deb_cmp_str
- self.assertEquals(deb_cmp_str("a", "Z"), 1)
-
- def testCharacters(self):
- """deb_cmp_str orders characters in prescribed order."""
- from lp.archivepublisher.debversion import deb_cmp_str
- chars = list(self.CHARS)
- last = chars.pop(0)
- for char in chars:
- self.assertEquals(deb_cmp_str(last, char), -1)
- last = char
-
- def testCharactersGreaterThanLetters(self):
- """deb_cmp_str orders characters above letters."""
- from lp.archivepublisher.debversion import deb_cmp_str
- self.assertEquals(deb_cmp_str(self.CHARS[0], "z"), 1)
-
-
-class DebCmp(unittest.TestCase):
-
- def testEmptyString(self):
- """deb_cmp returns 0 for the empty string."""
- from lp.archivepublisher.debversion import deb_cmp
- self.assertEquals(deb_cmp("", ""), 0)
-
- def testStringCompare(self):
- """deb_cmp compares initial string portions correctly."""
- from lp.archivepublisher.debversion import deb_cmp
- self.assertEquals(deb_cmp("a", "b"), -1)
- self.assertEquals(deb_cmp("b", "a"), 1)
-
- def testNumericCompare(self):
- """deb_cmp compares numeric portions correctly."""
- from lp.archivepublisher.debversion import deb_cmp
- self.assertEquals(deb_cmp("foo1", "foo2"), -1)
- self.assertEquals(deb_cmp("foo2", "foo1"), 1)
- self.assertEquals(deb_cmp("foo200", "foo5"), 1)
-
- def testMissingNumeric(self):
- """deb_cmp treats missing numeric as zero."""
- from lp.archivepublisher.debversion import deb_cmp
- self.assertEquals(deb_cmp("foo", "foo0"), 0)
- self.assertEquals(deb_cmp("foo", "foo1"), -1)
- self.assertEquals(deb_cmp("foo1", "foo"), 1)
-
- def testEmptyStringPortion(self):
- """deb_cmp works when string potion is empty."""
- from lp.archivepublisher.debversion import deb_cmp
- self.assertEquals(deb_cmp("100", "foo100"), -1)
=== modified file 'lib/lp/archivepublisher/tests/test_ftparchive.py'
--- lib/lp/archivepublisher/tests/test_ftparchive.py 2010-07-18 00:24:06 +0000
+++ lib/lp/archivepublisher/tests/test_ftparchive.py 2010-08-18 11:41:24 +0000
@@ -15,7 +15,7 @@
from zope.component import getUtility
from canonical.config import config
-from canonical.launchpad.scripts.logger import QuietFakeLogger
+from canonical.launchpad.scripts.logger import BufferLogger, QuietFakeLogger
from canonical.testing import LaunchpadZopelessLayer
from lp.archivepublisher.config import Config
from lp.archivepublisher.diskpool import DiskPool
@@ -23,6 +23,7 @@
from lp.archivepublisher.publishing import Publisher
from lp.registry.interfaces.distribution import IDistributionSet
from lp.registry.interfaces.pocket import PackagePublishingPocket
+from lp.testing import TestCaseWithFactory
def sanitize_apt_ftparchive_Sources_output(text):
@@ -55,10 +56,11 @@
return self._result[i:j]
-class TestFTPArchive(unittest.TestCase):
+class TestFTPArchive(TestCaseWithFactory):
layer = LaunchpadZopelessLayer
def setUp(self):
+ super(TestFTPArchive, self).setUp()
self.layer.switchDbUser(config.archivepublisher.dbuser)
self._distribution = getUtility(IDistributionSet)['ubuntutest']
@@ -79,6 +81,7 @@
self._publisher = SamplePublisher(self._archive)
def tearDown(self):
+ super(TestFTPArchive, self).tearDown()
shutil.rmtree(self._config.distroroot)
def _verifyFile(self, filename, directory, output_filter=None):
@@ -116,6 +119,19 @@
self._publisher)
return fa
+ def test_NoLucilleConfig(self):
+ # Distroseries without a lucille configuration get ignored
+ # and trigger a warning, they don't break the publisher
+ logger = BufferLogger()
+ publisher = Publisher(
+ logger, self._config, self._dp, self._archive)
+ self.factory.makeDistroSeries(self._distribution, name="somename")
+ fa = FTPArchiveHandler(logger, self._config, self._dp,
+ self._distribution, publisher)
+ fa.createEmptyPocketRequests(fullpublish=True)
+ self.assertEquals("WARNING: Distroseries somename in ubuntutest doesn't "
+ "have a lucille configuration.\n", logger.buffer.getvalue())
+
def test_getSourcesForOverrides(self):
# getSourcesForOverrides returns a list of tuples containing:
# (sourcename, suite, component, section)
=== modified file 'lib/lp/archiveuploader/tests/__init__.py'
--- lib/lp/archiveuploader/tests/__init__.py 2010-05-04 15:38:08 +0000
+++ lib/lp/archiveuploader/tests/__init__.py 2010-08-18 11:41:24 +0000
@@ -1,6 +1,10 @@
# Copyright 2009 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
+"""Tests for the archive uploader."""
+
+from __future__ import with_statement
+
__metaclass__ = type
__all__ = ['datadir', 'getPolicy', 'insertFakeChangesFile',
@@ -25,6 +29,7 @@
raise ValueError("Path is not relative: %s" % path)
return os.path.join(here, 'data', path)
+
def insertFakeChangesFile(fileID, path=None):
"""Insert a fake changes file into the librarian.
@@ -34,11 +39,11 @@
"""
if path is None:
path = datadir("ed-0.2-21/ed_0.2-21_source.changes")
- changes_file_obj = open(path, 'r')
- test_changes_file = changes_file_obj.read()
- changes_file_obj.close()
+ with open(path, 'r') as changes_file_obj:
+ test_changes_file = changes_file_obj.read()
fillLibrarianFile(fileID, content=test_changes_file)
+
def insertFakeChangesFileForAllPackageUploads():
"""Ensure all the PackageUpload records point to a valid changes file."""
for id in set(pu.changesfile.id for pu in PackageUploadSet()):
@@ -53,6 +58,7 @@
self.distroseries = distroseries
self.buildid = buildid
+
def getPolicy(name='anything', distro='ubuntu', distroseries=None,
buildid=None):
"""Build and return an Upload Policy for the given context."""
=== modified file 'lib/lp/archiveuploader/tests/test_buildduploads.py'
--- lib/lp/archiveuploader/tests/test_buildduploads.py 2010-07-18 00:26:33 +0000
+++ lib/lp/archiveuploader/tests/test_buildduploads.py 2010-08-18 11:41:24 +0000
@@ -7,7 +7,6 @@
from lp.archiveuploader.tests.test_securityuploads import (
TestStagedBinaryUploadBase)
-from lp.archiveuploader.uploadprocessor import UploadProcessor
from lp.registry.interfaces.pocket import PackagePublishingPocket
from canonical.database.constants import UTC_NOW
from canonical.launchpad.interfaces import PackagePublishingStatus
@@ -84,8 +83,8 @@
"""Setup an UploadProcessor instance for a given buildd context."""
self.options.context = self.policy
self.options.buildid = str(build_candidate.id)
- self.uploadprocessor = UploadProcessor(
- self.options, self.layer.txn, self.log)
+ self.uploadprocessor = self.getUploadProcessor(
+ self.layer.txn)
def testDelayedBinaryUpload(self):
"""Check if Soyuz copes with delayed binary uploads.
=== modified file 'lib/lp/archiveuploader/tests/test_ppauploadprocessor.py'
--- lib/lp/archiveuploader/tests/test_ppauploadprocessor.py 2010-08-02 02:13:52 +0000
+++ lib/lp/archiveuploader/tests/test_ppauploadprocessor.py 2010-08-18 11:41:24 +0000
@@ -18,7 +18,6 @@
from zope.security.proxy import removeSecurityProxy
from lp.app.errors import NotFoundError
-from lp.archiveuploader.uploadprocessor import UploadProcessor
from lp.archiveuploader.tests.test_uploadprocessor import (
TestUploadProcessorBase)
from canonical.config import config
@@ -74,8 +73,7 @@
# Set up the uploadprocessor with appropriate options and logger
self.options.context = 'insecure'
- self.uploadprocessor = UploadProcessor(
- self.options, self.layer.txn, self.log)
+ self.uploadprocessor = self.getUploadProcessor(self.layer.txn)
def assertEmail(self, contents=None, recipients=None,
ppa_header='name16'):
@@ -1224,8 +1222,7 @@
# Re-initialize uploadprocessor since it depends on the new
# transaction reset by switchDbUser.
- self.uploadprocessor = UploadProcessor(
- self.options, self.layer.txn, self.log)
+ self.uploadprocessor = self.getUploadProcessor(self.layer.txn)
def testPPASizeQuotaSourceRejection(self):
"""Verify the size quota check for PPA uploads.
=== modified file 'lib/lp/archiveuploader/tests/test_recipeuploads.py'
--- lib/lp/archiveuploader/tests/test_recipeuploads.py 2010-07-22 15:24:02 +0000
+++ lib/lp/archiveuploader/tests/test_recipeuploads.py 2010-08-18 11:41:24 +0000
@@ -12,7 +12,6 @@
from lp.archiveuploader.tests.test_uploadprocessor import (
TestUploadProcessorBase)
-from lp.archiveuploader.uploadprocessor import UploadProcessor
from lp.buildmaster.interfaces.buildbase import BuildStatus
from lp.code.interfaces.sourcepackagerecipebuild import (
ISourcePackageRecipeBuildSource)
@@ -42,8 +41,8 @@
self.options.context = 'recipe'
self.options.buildid = self.build.id
- self.uploadprocessor = UploadProcessor(
- self.options, self.layer.txn, self.log)
+ self.uploadprocessor = self.getUploadProcessor(
+ self.layer.txn)
def testSetsBuildAndState(self):
# Ensure that the upload processor correctly links the SPR to
=== modified file 'lib/lp/archiveuploader/tests/test_securityuploads.py'
--- lib/lp/archiveuploader/tests/test_securityuploads.py 2010-07-18 00:26:33 +0000
+++ lib/lp/archiveuploader/tests/test_securityuploads.py 2010-08-18 11:41:24 +0000
@@ -11,7 +11,6 @@
from lp.archiveuploader.tests.test_uploadprocessor import (
TestUploadProcessorBase)
-from lp.archiveuploader.uploadprocessor import UploadProcessor
from lp.registry.interfaces.pocket import PackagePublishingPocket
from lp.soyuz.model.binarypackagebuild import BinaryPackageBuild
from lp.soyuz.model.processor import ProcessorFamily
@@ -70,8 +69,7 @@
self.options.context = self.policy
self.options.nomails = self.no_mails
# Set up the uploadprocessor with appropriate options and logger
- self.uploadprocessor = UploadProcessor(
- self.options, self.layer.txn, self.log)
+ self.uploadprocessor = self.getUploadProcessor(self.layer.txn)
self.builds_before_upload = BinaryPackageBuild.select().count()
self.source_queue = None
self._uploadSource()
@@ -232,8 +230,7 @@
"""
build_candidate = self._createBuild('i386')
self.options.buildid = str(build_candidate.id)
- self.uploadprocessor = UploadProcessor(
- self.options, self.layer.txn, self.log)
+ self.uploadprocessor = self.getUploadProcessor(self.layer.txn)
build_used = self._uploadBinary('i386')
@@ -254,8 +251,7 @@
"""
build_candidate = self._createBuild('hppa')
self.options.buildid = str(build_candidate.id)
- self.uploadprocessor = UploadProcessor(
- self.options, self.layer.txn, self.log)
+ self.uploadprocessor = self.getUploadProcessor(self.layer.txn)
self.assertRaises(AssertionError, self._uploadBinary, 'i386')
=== modified file 'lib/lp/archiveuploader/tests/test_uploadprocessor.py'
--- lib/lp/archiveuploader/tests/test_uploadprocessor.py 2010-08-02 02:13:52 +0000
+++ lib/lp/archiveuploader/tests/test_uploadprocessor.py 2010-08-18 11:41:24 +0000
@@ -23,8 +23,10 @@
from zope.security.proxy import removeSecurityProxy
from lp.app.errors import NotFoundError
-from lp.archiveuploader.uploadpolicy import AbstractUploadPolicy
+from lp.archiveuploader.uploadpolicy import (AbstractUploadPolicy,
+ findPolicyByOptions)
from lp.archiveuploader.uploadprocessor import UploadProcessor
+from lp.buildmaster.interfaces.buildbase import BuildStatus
from canonical.config import config
from canonical.database.constants import UTC_NOW
from lp.soyuz.model.archivepermission import ArchivePermission
@@ -37,6 +39,7 @@
from lp.registry.model.sourcepackagename import SourcePackageName
from lp.soyuz.model.sourcepackagerelease import (
SourcePackageRelease)
+from lp.soyuz.scripts.initialise_distroseries import InitialiseDistroSeries
from canonical.launchpad.ftests import import_public_test_keys
from lp.registry.interfaces.distribution import IDistributionSet
from lp.registry.interfaces.series import SeriesStatus
@@ -59,7 +62,7 @@
ISourcePackageNameSet)
from lp.services.mail import stub
from canonical.launchpad.testing.fakepackager import FakePackager
-from lp.testing import TestCaseWithFactory
+from lp.testing import TestCase, TestCaseWithFactory
from lp.testing.mail_helpers import pop_notifications
from canonical.launchpad.webapp.errorlog import ErrorReportingUtility
from canonical.testing import LaunchpadZopelessLayer
@@ -113,7 +116,8 @@
super(TestUploadProcessorBase, self).setUp()
self.queue_folder = tempfile.mkdtemp()
- os.makedirs(os.path.join(self.queue_folder, "incoming"))
+ self.incoming_folder = os.path.join(self.queue_folder, "incoming")
+ os.makedirs(self.incoming_folder)
self.test_files_dir = os.path.join(config.root,
"lib/lp/archiveuploader/tests/data/suite")
@@ -139,6 +143,30 @@
shutil.rmtree(self.queue_folder)
super(TestUploadProcessorBase, self).tearDown()
+ def getUploadProcessor(self, txn):
+ def getPolicy(distro):
+ self.options.distro = distro.name
+ return findPolicyByOptions(self.options)
+ return UploadProcessor(
+ self.options.base_fsroot, self.options.dryrun,
+ self.options.nomails,
+ self.options.keep, getPolicy, txn, self.log)
+
+ def publishPackage(self, packagename, version, source=True,
+ archive=None):
+ """Publish a single package that is currently NEW in the queue."""
+ queue_items = self.breezy.getQueueItems(
+ status=PackageUploadStatus.NEW, name=packagename,
+ version=version, exact_match=True, archive=archive)
+ self.assertEqual(queue_items.count(), 1)
+ queue_item = queue_items[0]
+ queue_item.setAccepted()
+ if source:
+ pubrec = queue_item.sources[0].publish(self.log)
+ else:
+ pubrec = queue_item.builds[0].publish(self.log)
+ return pubrec
+
def assertLogContains(self, line):
"""Assert if a given line is present in the log messages."""
self.assertTrue(line in self.log.lines,
@@ -184,15 +212,13 @@
name, 'Breezy Badger',
'The Breezy Badger', 'Black and White', 'Someone',
'5.10', bat, bat.owner)
- breezy_i386 = self.breezy.newArch(
- 'i386', bat['i386'].processorfamily, True, self.breezy.owner)
- self.breezy.nominatedarchindep = breezy_i386
-
- fake_chroot = self.addMockFile('fake_chroot.tar.gz')
- breezy_i386.addOrUpdateChroot(fake_chroot)
self.breezy.changeslist = 'breezy-changes@xxxxxxxxxx'
- self.breezy.initialiseFromParent()
+ ids = InitialiseDistroSeries(self.breezy)
+ ids.initialise()
+
+ fake_chroot = self.addMockFile('fake_chroot.tar.gz')
+ self.breezy['i386'].addOrUpdateChroot(fake_chroot)
if permitted_formats is None:
permitted_formats = [SourcePackageFormat.FORMAT_1_0]
@@ -208,25 +234,29 @@
filename, len(content), StringIO(content),
'application/x-gtar')
- def queueUpload(self, upload_name, relative_path="", test_files_dir=None):
+ def queueUpload(self, upload_name, relative_path="", test_files_dir=None,
+ queue_entry=None):
"""Queue one of our test uploads.
- upload_name is the name of the test upload directory. It is also
+ upload_name is the name of the test upload directory. If there
+ is no explicit queue entry name specified, it is also
the name of the queue entry directory we create.
relative_path is the path to create inside the upload, eg
ubuntu/~malcc/default. If not specified, defaults to "".
Return the path to the upload queue entry directory created.
"""
+ if queue_entry is None:
+ queue_entry = upload_name
target_path = os.path.join(
- self.queue_folder, "incoming", upload_name, relative_path)
+ self.incoming_folder, queue_entry, relative_path)
if test_files_dir is None:
test_files_dir = self.test_files_dir
upload_dir = os.path.join(test_files_dir, upload_name)
if relative_path:
os.makedirs(os.path.dirname(target_path))
shutil.copytree(upload_dir, target_path)
- return os.path.join(self.queue_folder, "incoming", upload_name)
+ return os.path.join(self.incoming_folder, queue_entry)
def processUpload(self, processor, upload_dir):
"""Process an upload queue entry directory.
@@ -248,8 +278,7 @@
self.layer.txn.commit()
if policy is not None:
self.options.context = policy
- return UploadProcessor(
- self.options, self.layer.txn, self.log)
+ return self.getUploadProcessor(self.layer.txn)
def assertEmail(self, contents=None, recipients=None):
"""Check last email content and recipients.
@@ -341,24 +370,9 @@
"Expected acceptance email not rejection. Actually Got:\n%s"
% raw_msg)
- def _publishPackage(self, packagename, version, source=True,
- archive=None):
- """Publish a single package that is currently NEW in the queue."""
- queue_items = self.breezy.getQueueItems(
- status=PackageUploadStatus.NEW, name=packagename,
- version=version, exact_match=True, archive=archive)
- self.assertEqual(queue_items.count(), 1)
- queue_item = queue_items[0]
- queue_item.setAccepted()
- if source:
- pubrec = queue_item.sources[0].publish(self.log)
- else:
- pubrec = queue_item.builds[0].publish(self.log)
- return pubrec
-
def testInstantiate(self):
"""UploadProcessor should instantiate"""
- up = UploadProcessor(self.options, None, self.log)
+ up = self.getUploadProcessor(None)
def testLocateDirectories(self):
"""Return a sorted list of subdirs in a directory.
@@ -372,7 +386,7 @@
os.mkdir("%s/dir1" % testdir)
os.mkdir("%s/dir2" % testdir)
- up = UploadProcessor(self.options, None, self.log)
+ up = self.getUploadProcessor(None)
located_dirs = up.locateDirectories(testdir)
self.assertEqual(located_dirs, ['dir1', 'dir2', 'dir3'])
finally:
@@ -390,7 +404,7 @@
open("%s/2_source.changes" % testdir, "w").close()
open("%s/3.not_changes" % testdir, "w").close()
- up = UploadProcessor(self.options, None, self.log)
+ up = self.getUploadProcessor(None)
located_files = up.locateChangesFiles(testdir)
self.assertEqual(
located_files, ["2_source.changes", "1.changes"])
@@ -418,7 +432,7 @@
# Move it
self.options.base_fsroot = testdir
- up = UploadProcessor(self.options, None, self.log)
+ up = self.getUploadProcessor(None)
up.moveUpload(upload, target_name)
# Check it moved
@@ -439,7 +453,7 @@
# Remove it
self.options.base_fsroot = testdir
- up = UploadProcessor(self.options, None, self.log)
+ up = self.getUploadProcessor(None)
up.moveProcessedUpload(upload, "accepted")
# Check it was removed, not moved
@@ -462,7 +476,7 @@
# Move it
self.options.base_fsroot = testdir
- up = UploadProcessor(self.options, None, self.log)
+ up = self.getUploadProcessor(None)
up.moveProcessedUpload(upload, "rejected")
# Check it moved
@@ -485,7 +499,7 @@
# Remove it
self.options.base_fsroot = testdir
- up = UploadProcessor(self.options, None, self.log)
+ up = self.getUploadProcessor(None)
up.removeUpload(upload)
# Check it was removed, not moved
@@ -498,7 +512,7 @@
def testOrderFilenames(self):
"""orderFilenames sorts _source.changes ahead of other files."""
- up = UploadProcessor(self.options, None, self.log)
+ up = self.getUploadProcessor(None)
self.assertEqual(["d_source.changes", "a", "b", "c"],
up.orderFilenames(["b", "a", "d_source.changes", "c"]))
@@ -522,8 +536,7 @@
# Register our broken upload policy
AbstractUploadPolicy._registerPolicy(BrokenUploadPolicy)
self.options.context = 'broken'
- uploadprocessor = UploadProcessor(
- self.options, self.layer.txn, self.log)
+ uploadprocessor = self.getUploadProcessor(self.layer.txn)
# Upload a package to Breezy.
upload_dir = self.queueUpload("baz_1.0-1")
@@ -634,7 +647,7 @@
# Upload 'bar-1.0-1' source and binary to ubuntu/breezy.
upload_dir = self.queueUpload("bar_1.0-1")
self.processUpload(uploadprocessor, upload_dir)
- bar_source_pub = self._publishPackage('bar', '1.0-1')
+ bar_source_pub = self.publishPackage('bar', '1.0-1')
[bar_original_build] = bar_source_pub.createMissingBuilds()
# Move the source from the accepted queue.
@@ -653,7 +666,7 @@
self.processUpload(uploadprocessor, upload_dir)
self.assertEqual(
uploadprocessor.last_processed_upload.is_rejected, False)
- bar_bin_pubs = self._publishPackage('bar', '1.0-1', source=False)
+ bar_bin_pubs = self.publishPackage('bar', '1.0-1', source=False)
# Mangle its publishing component to "restricted" so we can check
# the copy archive ancestry override later.
restricted = getUtility(IComponentSet)["restricted"]
@@ -746,14 +759,14 @@
# Upload 'bar-1.0-1' source and binary to ubuntu/breezy.
upload_dir = self.queueUpload("bar_1.0-1")
self.processUpload(uploadprocessor, upload_dir)
- bar_source_pub = self._publishPackage('bar', '1.0-1')
+ bar_source_pub = self.publishPackage('bar', '1.0-1')
[bar_original_build] = bar_source_pub.createMissingBuilds()
self.options.context = 'buildd'
self.options.buildid = bar_original_build.id
upload_dir = self.queueUpload("bar_1.0-1_binary")
self.processUpload(uploadprocessor, upload_dir)
- [bar_binary_pub] = self._publishPackage("bar", "1.0-1", source=False)
+ [bar_binary_pub] = self.publishPackage("bar", "1.0-1", source=False)
# Prepare ubuntu/breezy-autotest to build sources in i386.
breezy_autotest = self.ubuntu['breezy-autotest']
@@ -803,7 +816,7 @@
# Upload 'bar-1.0-1' source and binary to ubuntu/breezy.
upload_dir = self.queueUpload("bar_1.0-1")
self.processUpload(uploadprocessor, upload_dir)
- bar_source_old = self._publishPackage('bar', '1.0-1')
+ bar_source_old = self.publishPackage('bar', '1.0-1')
# Upload 'bar-1.0-1' source and binary to ubuntu/breezy.
upload_dir = self.queueUpload("bar_1.0-2")
@@ -816,7 +829,7 @@
self.options.buildid = bar_original_build.id
upload_dir = self.queueUpload("bar_1.0-2_binary")
self.processUpload(uploadprocessor, upload_dir)
- [bar_binary_pub] = self._publishPackage("bar", "1.0-2", source=False)
+ [bar_binary_pub] = self.publishPackage("bar", "1.0-2", source=False)
# Create a COPY archive for building in non-virtual builds.
uploader = getUtility(IPersonSet).getByName('name16')
@@ -971,7 +984,7 @@
partner_archive = getUtility(IArchiveSet).getByDistroPurpose(
self.ubuntu, ArchivePurpose.PARTNER)
self.assertTrue(partner_archive)
- self._publishPackage("foocomm", "1.0-1", archive=partner_archive)
+ self.publishPackage("foocomm", "1.0-1", archive=partner_archive)
# Check the publishing record's archive and component.
foocomm_spph = SourcePackagePublishingHistory.selectOneBy(
@@ -1015,7 +1028,7 @@
self.assertEqual(foocomm_bpr.component.name, 'partner')
# Publish the upload so we can check the publishing record.
- self._publishPackage("foocomm", "1.0-1", source=False)
+ self.publishPackage("foocomm", "1.0-1", source=False)
# Check the publishing record's archive and component.
foocomm_bpph = BinaryPackagePublishingHistory.selectOneBy(
@@ -1054,14 +1067,14 @@
# Accept and publish the upload.
partner_archive = getUtility(IArchiveSet).getByDistroPurpose(
self.ubuntu, ArchivePurpose.PARTNER)
- self._publishPackage("foocomm", "1.0-1", archive=partner_archive)
+ self.publishPackage("foocomm", "1.0-1", archive=partner_archive)
# Now do the same thing with a binary package.
upload_dir = self.queueUpload("foocomm_1.0-1_binary")
self.processUpload(uploadprocessor, upload_dir)
# Accept and publish the upload.
- self._publishPackage("foocomm", "1.0-1", source=False,
+ self.publishPackage("foocomm", "1.0-1", source=False,
archive=partner_archive)
# Upload the next source version of the package.
@@ -1105,8 +1118,7 @@
self.breezy.status = SeriesStatus.CURRENT
self.layer.txn.commit()
self.options.context = 'insecure'
- uploadprocessor = UploadProcessor(
- self.options, self.layer.txn, self.log)
+ uploadprocessor = self.getUploadProcessor(self.layer.txn)
# Upload a package for Breezy.
upload_dir = self.queueUpload("foocomm_1.0-1_proposed")
@@ -1124,8 +1136,7 @@
self.breezy.status = SeriesStatus.CURRENT
self.layer.txn.commit()
self.options.context = 'insecure'
- uploadprocessor = UploadProcessor(
- self.options, self.layer.txn, self.log)
+ uploadprocessor = self.getUploadProcessor(self.layer.txn)
# Upload a package for Breezy.
upload_dir = self.queueUpload("foocomm_1.0-1")
@@ -1140,8 +1151,7 @@
pocket and ensure it fails."""
# Set up the uploadprocessor with appropriate options and logger.
self.options.context = 'insecure'
- uploadprocessor = UploadProcessor(
- self.options, self.layer.txn, self.log)
+ uploadprocessor = self.getUploadProcessor(self.layer.txn)
# Upload a package for Breezy.
upload_dir = self.queueUpload("foocomm_1.0-1_updates")
@@ -1302,8 +1312,7 @@
used.
That exception will then initiate the creation of an OOPS report.
"""
- processor = UploadProcessor(
- self.options, self.layer.txn, self.log)
+ processor = self.getUploadProcessor(self.layer.txn)
upload_dir = self.queueUpload("foocomm_1.0-1_proposed")
bogus_changesfile_data = '''
@@ -1346,8 +1355,7 @@
self.setupBreezy()
self.layer.txn.commit()
self.options.context = 'absolutely-anything'
- uploadprocessor = UploadProcessor(
- self.options, self.layer.txn, self.log)
+ uploadprocessor = self.getUploadProcessor(self.layer.txn)
# Upload the source first to enable the binary later:
upload_dir = self.queueUpload("bar_1.0-1_lzma")
@@ -1357,7 +1365,7 @@
self.assertTrue(
"rejected" not in raw_msg,
"Failed to upload bar source:\n%s" % raw_msg)
- self._publishPackage("bar", "1.0-1")
+ self.publishPackage("bar", "1.0-1")
# Clear out emails generated during upload.
ignore = pop_notifications()
@@ -1456,15 +1464,14 @@
permission=ArchivePermissionType.UPLOAD, person=uploader,
component=restricted)
- uploadprocessor = UploadProcessor(
- self.options, self.layer.txn, self.log)
+ uploadprocessor = self.getUploadProcessor(self.layer.txn)
# Upload the first version and accept it to make it known in
# Ubuntu. The uploader has rights to upload NEW packages to
# components that he does not have direct rights to.
upload_dir = self.queueUpload("bar_1.0-1")
self.processUpload(uploadprocessor, upload_dir)
- bar_source_pub = self._publishPackage('bar', '1.0-1')
+ bar_source_pub = self.publishPackage('bar', '1.0-1')
# Clear out emails generated during upload.
ignore = pop_notifications()
@@ -1509,15 +1516,14 @@
permission=ArchivePermissionType.UPLOAD, person=uploader,
component=restricted)
- uploadprocessor = UploadProcessor(
- self.options, self.layer.txn, self.log)
+ uploadprocessor = self.getUploadProcessor(self.layer.txn)
# Upload the first version and accept it to make it known in
# Ubuntu. The uploader has rights to upload NEW packages to
# components that he does not have direct rights to.
upload_dir = self.queueUpload("bar_1.0-1")
self.processUpload(uploadprocessor, upload_dir)
- bar_source_pub = self._publishPackage('bar', '1.0-1')
+ bar_source_pub = self.publishPackage('bar', '1.0-1')
# Clear out emails generated during upload.
ignore = pop_notifications()
@@ -1590,8 +1596,7 @@
# with pointer to the Soyuz questions in Launchpad and the
# reason why the message was sent to the current recipients.
self.setupBreezy()
- uploadprocessor = UploadProcessor(
- self.options, self.layer.txn, self.log)
+ uploadprocessor = self.getUploadProcessor(self.layer.txn)
upload_dir = self.queueUpload("bar_1.0-1", "boing")
self.processUpload(uploadprocessor, upload_dir)
@@ -1636,8 +1641,7 @@
self.setupBreezy()
self.layer.txn.commit()
self.options.context = 'absolutely-anything'
- uploadprocessor = UploadProcessor(
- self.options, self.layer.txn, self.log)
+ uploadprocessor = self.getUploadProcessor(self.layer.txn)
# Upload the source.
upload_dir = self.queueUpload("bar_1.0-1_3.0-quilt")
@@ -1655,8 +1659,7 @@
permitted_formats=[SourcePackageFormat.FORMAT_3_0_QUILT])
self.layer.txn.commit()
self.options.context = 'absolutely-anything'
- uploadprocessor = UploadProcessor(
- self.options, self.layer.txn, self.log)
+ uploadprocessor = self.getUploadProcessor(self.layer.txn)
# Upload the source.
upload_dir = self.queueUpload("bar_1.0-1_3.0-quilt")
@@ -1666,7 +1669,7 @@
self.assertTrue(
"rejected" not in raw_msg,
"Failed to upload bar source:\n%s" % raw_msg)
- spph = self._publishPackage("bar", "1.0-1")
+ spph = self.publishPackage("bar", "1.0-1")
self.assertEquals(
sorted((sprf.libraryfile.filename, sprf.filetype)
@@ -1689,8 +1692,7 @@
permitted_formats=[SourcePackageFormat.FORMAT_3_0_QUILT])
self.layer.txn.commit()
self.options.context = 'absolutely-anything'
- uploadprocessor = UploadProcessor(
- self.options, self.layer.txn, self.log)
+ uploadprocessor = self.getUploadProcessor(self.layer.txn)
# Upload the first source.
upload_dir = self.queueUpload("bar_1.0-1_3.0-quilt")
@@ -1700,7 +1702,7 @@
self.assertTrue(
"rejected" not in raw_msg,
"Failed to upload bar source:\n%s" % raw_msg)
- spph = self._publishPackage("bar", "1.0-1")
+ spph = self.publishPackage("bar", "1.0-1")
# Upload another source sharing the same (component) orig.
upload_dir = self.queueUpload("bar_1.0-2_3.0-quilt_without_orig")
@@ -1728,8 +1730,7 @@
permitted_formats=[SourcePackageFormat.FORMAT_3_0_NATIVE])
self.layer.txn.commit()
self.options.context = 'absolutely-anything'
- uploadprocessor = UploadProcessor(
- self.options, self.layer.txn, self.log)
+ uploadprocessor = self.getUploadProcessor(self.layer.txn)
# Upload the source.
upload_dir = self.queueUpload("bar_1.0_3.0-native")
@@ -1739,7 +1740,7 @@
self.assertTrue(
"rejected" not in raw_msg,
"Failed to upload bar source:\n%s" % raw_msg)
- spph = self._publishPackage("bar", "1.0")
+ spph = self.publishPackage("bar", "1.0")
self.assertEquals(
sorted((sprf.libraryfile.filename, sprf.filetype)
@@ -1754,8 +1755,7 @@
self.setupBreezy()
self.layer.txn.commit()
self.options.context = 'absolutely-anything'
- uploadprocessor = UploadProcessor(
- self.options, self.layer.txn, self.log)
+ uploadprocessor = self.getUploadProcessor(self.layer.txn)
# Upload the source.
upload_dir = self.queueUpload("bar_1.0-1_1.0-bzip2")
@@ -1772,8 +1772,7 @@
self.setupBreezy()
breezy = self.ubuntu['breezy']
breezy.status = SeriesStatus.CURRENT
- uploadprocessor = UploadProcessor(
- self.options, self.layer.txn, self.log)
+ uploadprocessor = self.getUploadProcessor(self.layer.txn)
upload_dir = self.queueUpload("bar_1.0-1")
self.processUpload(uploadprocessor, upload_dir)
=== modified file 'lib/lp/archiveuploader/uploadprocessor.py'
--- lib/lp/archiveuploader/uploadprocessor.py 2010-08-04 00:30:56 +0000
+++ lib/lp/archiveuploader/uploadprocessor.py 2010-08-18 11:41:24 +0000
@@ -60,7 +60,7 @@
from lp.archiveuploader.nascentupload import (
NascentUpload, FatalUploadError, EarlyReturnUploadError)
from lp.archiveuploader.uploadpolicy import (
- findPolicyByOptions, UploadPolicyError)
+ UploadPolicyError)
from lp.soyuz.interfaces.archive import IArchiveSet, NoSuchPPA
from lp.registry.interfaces.distribution import IDistributionSet
from lp.registry.interfaces.person import IPersonSet
@@ -108,16 +108,33 @@
class UploadProcessor:
"""Responsible for processing uploads. See module docstring."""
- def __init__(self, options, ztm, log):
- self.options = options
+ def __init__(self, base_fsroot, dry_run, no_mails, keep, policy_for_distro,
+ ztm, log):
+ """Create a new upload processor.
+
+ :param base_fsroot: Root path for queue to use
+ :param dry_run: Run but don't commit changes to database
+ :param no_mails: Don't send out any emails
+ :param builds: Interpret leaf names as build ids
+ :param keep: Leave the files in place, don't move them away
+ :param policy_for_distro: callback to obtain Policy object for a
+ distribution
+ :param ztm: Database transaction to use
+ :param log: Logger to use for reporting
+ """
+ self.base_fsroot = base_fsroot
+ self.dry_run = dry_run
+ self.keep = keep
+ self.last_processed_upload = None
+ self.log = log
+ self.no_mails = no_mails
+ self._getPolicyForDistro = policy_for_distro
self.ztm = ztm
- self.log = log
- self.last_processed_upload = None
- def processUploadQueue(self):
+ def processUploadQueue(self, leaf_name=None):
"""Search for uploads, and process them.
- Uploads are searched for in the 'incoming' directory inside the
+ Uploads are searched for in the 'incoming' directory inside the
base_fsroot.
This method also creates the 'incoming', 'accepted', 'rejected', and
@@ -127,19 +144,22 @@
self.log.debug("Beginning processing")
for subdir in ["incoming", "accepted", "rejected", "failed"]:
- full_subdir = os.path.join(self.options.base_fsroot, subdir)
+ full_subdir = os.path.join(self.base_fsroot, subdir)
if not os.path.exists(full_subdir):
self.log.debug("Creating directory %s" % full_subdir)
os.mkdir(full_subdir)
- fsroot = os.path.join(self.options.base_fsroot, "incoming")
+ fsroot = os.path.join(self.base_fsroot, "incoming")
uploads_to_process = self.locateDirectories(fsroot)
self.log.debug("Checked in %s, found %s"
% (fsroot, uploads_to_process))
for upload in uploads_to_process:
self.log.debug("Considering upload %s" % upload)
+ if leaf_name is not None and upload != leaf_name:
+ self.log.debug("Skipping %s -- does not match %s" % (
+ upload, leaf_name))
+ continue
self.processUpload(fsroot, upload)
-
finally:
self.log.debug("Rolling back any remaining transactions.")
self.ztm.abort()
@@ -152,16 +172,7 @@
is 'failed', otherwise it is the worst of the results from the
individual changes files, in order 'failed', 'rejected', 'accepted'.
- If the leafname option is set but its value is not the same as the
- name of the upload directory, skip it entirely.
-
"""
- if (self.options.leafname is not None and
- upload != self.options.leafname):
- self.log.debug("Skipping %s -- does not match %s" % (
- upload, self.options.leafname))
- return
-
upload_path = os.path.join(fsroot, upload)
changes_files = self.locateChangesFiles(upload_path)
@@ -242,7 +253,7 @@
# Skip lockfile deletion, see similar code in lp.poppy.hooks.
fsroot_lock.release(skip_delete=True)
- sorted_dir_names = sorted(
+ sorted_dir_names = sorted(
dir_name
for dir_name in dir_names
if os.path.isdir(os.path.join(fsroot, dir_name)))
@@ -321,8 +332,7 @@
"https://help.launchpad.net/Packaging/PPA#Uploading "
"and update your configuration.")))
self.log.debug("Finding fresh policy")
- self.options.distro = distribution.name
- policy = findPolicyByOptions(self.options)
+ policy = self._getPolicyForDistro(distribution)
policy.archive = archive
# DistroSeries overriding respect the following precedence:
@@ -396,7 +406,7 @@
# when transaction is committed) this will cause any emails sent
# sent by do_reject to be lost.
notify = True
- if self.options.dryrun or self.options.nomails:
+ if self.dry_run or self.no_mails:
notify = False
if upload.is_rejected:
result = UploadStatusEnum.REJECTED
@@ -415,7 +425,7 @@
for msg in upload.rejections:
self.log.warn("\t%s" % msg)
- if self.options.dryrun:
+ if self.dry_run:
self.log.info("Dry run, aborting transaction.")
self.ztm.abort()
else:
@@ -434,7 +444,7 @@
This includes moving the given upload directory and moving the
matching .distro file, if it exists.
"""
- if self.options.keep or self.options.dryrun:
+ if self.keep or self.dry_run:
self.log.debug("Keeping contents untouched")
return
@@ -462,21 +472,21 @@
This includes moving the given upload directory and moving the
matching .distro file, if it exists.
"""
- if self.options.keep or self.options.dryrun:
+ if self.keep or self.dry_run:
self.log.debug("Keeping contents untouched")
return
pathname = os.path.basename(upload)
target_path = os.path.join(
- self.options.base_fsroot, subdir_name, pathname)
+ self.base_fsroot, subdir_name, pathname)
self.log.debug("Moving upload directory %s to %s" %
(upload, target_path))
shutil.move(upload, target_path)
distro_filename = upload + ".distro"
if os.path.isfile(distro_filename):
- target_path = os.path.join(self.options.base_fsroot, subdir_name,
+ target_path = os.path.join(self.base_fsroot, subdir_name,
os.path.basename(distro_filename))
self.log.debug("Moving distro file %s to %s" % (distro_filename,
target_path))
=== modified file 'lib/lp/archiveuploader/utils.py'
--- lib/lp/archiveuploader/utils.py 2010-08-01 06:59:04 +0000
+++ lib/lp/archiveuploader/utils.py 2010-08-18 11:41:24 +0000
@@ -266,6 +266,10 @@
# Force the name to be UTF-8
name = force_to_utf8(name)
+ # If the maintainer's name contains a full stop then the whole field will
+ # not work directly as an email address due to a misfeature in the syntax
+ # specified in RFC822; see Debian policy 5.6.2 (Maintainer field syntax)
+ # for details.
if name.find(',') != -1 or name.find('.') != -1:
rfc822_maint = "%s (%s)" % (email, name)
rfc2047_maint = "%s (%s)" % (email, rfc2047_name)
=== modified file 'lib/lp/blueprints/interfaces/specification.py'
--- lib/lp/blueprints/interfaces/specification.py 2010-07-28 16:56:05 +0000
+++ lib/lp/blueprints/interfaces/specification.py 2010-08-18 11:41:24 +0000
@@ -34,7 +34,7 @@
from zope.schema import Datetime, Int, Choice, Text, TextLine, Bool
from canonical.launchpad import _
-from canonical.launchpad.fields import (
+from lp.services.fields import (
ContentNameField, PublicPersonChoice, Summary, Title)
from canonical.launchpad.validators import LaunchpadValidationError
from lp.registry.interfaces.role import IHasOwner
=== modified file 'lib/lp/blueprints/interfaces/specificationbranch.py'
--- lib/lp/blueprints/interfaces/specificationbranch.py 2009-07-17 00:26:05 +0000
+++ lib/lp/blueprints/interfaces/specificationbranch.py 2010-08-18 11:41:24 +0000
@@ -20,7 +20,7 @@
export_operation_as, export_write_operation)
from canonical.launchpad import _
-from canonical.launchpad.fields import Summary
+from lp.services.fields import Summary
from canonical.launchpad.interfaces.launchpad import IHasDateCreated
from lp.blueprints.interfaces.specification import ISpecification
from lp.code.interfaces.branch import IBranch
=== modified file 'lib/lp/blueprints/interfaces/specificationfeedback.py'
--- lib/lp/blueprints/interfaces/specificationfeedback.py 2009-09-22 14:02:23 +0000
+++ lib/lp/blueprints/interfaces/specificationfeedback.py 2010-08-18 11:41:24 +0000
@@ -17,7 +17,7 @@
from zope.schema import Int, Text
from canonical.launchpad import _
-from canonical.launchpad.fields import PublicPersonChoice
+from lp.services.fields import PublicPersonChoice
class ISpecificationFeedback(Interface):
"""The queue entry for a specification on a person, including a message
=== modified file 'lib/lp/blueprints/interfaces/specificationsubscription.py'
--- lib/lp/blueprints/interfaces/specificationsubscription.py 2009-10-11 03:19:50 +0000
+++ lib/lp/blueprints/interfaces/specificationsubscription.py 2010-08-18 11:41:24 +0000
@@ -14,7 +14,7 @@
from zope.interface import Interface
from zope.schema import Int, Bool
from canonical.launchpad import _
-from canonical.launchpad.fields import PublicPersonChoice
+from lp.services.fields import PublicPersonChoice
class ISpecificationSubscription(Interface):
"""A subscription for a person to a specification."""
=== modified file 'lib/lp/blueprints/interfaces/sprint.py'
--- lib/lp/blueprints/interfaces/sprint.py 2009-10-28 21:51:18 +0000
+++ lib/lp/blueprints/interfaces/sprint.py 2010-08-18 11:41:24 +0000
@@ -22,7 +22,7 @@
from zope.schema import Datetime, Int, Choice, Text, TextLine
from canonical.launchpad import _
-from canonical.launchpad.fields import (
+from lp.services.fields import (
ContentNameField, IconImageUpload, LogoImageUpload, MugshotImageUpload,
PublicPersonChoice)
from canonical.launchpad.validators.name import name_validator
=== modified file 'lib/lp/blueprints/interfaces/sprintattendance.py'
--- lib/lp/blueprints/interfaces/sprintattendance.py 2009-11-06 02:53:22 +0000
+++ lib/lp/blueprints/interfaces/sprintattendance.py 2010-08-18 11:41:24 +0000
@@ -14,7 +14,7 @@
from zope.interface import Interface
from zope.schema import Bool, Choice, Datetime
from canonical.launchpad import _
-from canonical.launchpad.fields import PublicPersonChoice
+from lp.services.fields import PublicPersonChoice
class ISprintAttendance(Interface):
=== modified file 'lib/lp/blueprints/interfaces/sprintspecification.py'
--- lib/lp/blueprints/interfaces/sprintspecification.py 2009-06-25 00:00:26 +0000
+++ lib/lp/blueprints/interfaces/sprintspecification.py 2010-08-18 11:41:24 +0000
@@ -17,7 +17,7 @@
from lazr.enum import DBEnumeratedType, DBItem
from canonical.launchpad import _
-from canonical.launchpad.fields import PublicPersonChoice
+from lp.services.fields import PublicPersonChoice
class ISprintSpecification(Interface):
=== modified file 'lib/lp/bugs/browser/bug.py'
--- lib/lp/bugs/browser/bug.py 2010-08-07 14:54:40 +0000
+++ lib/lp/bugs/browser/bug.py 2010-08-18 11:41:24 +0000
@@ -50,7 +50,6 @@
from canonical.launchpad import _
from canonical.launchpad.browser.librarian import ProxiedLibraryFileAlias
-from canonical.launchpad.fields import DuplicateBug
from canonical.launchpad.webapp.interfaces import ILaunchBag
from lp.app.errors import NotFoundError
from lp.bugs.interfaces.bug import IBug, IBugSet
@@ -62,6 +61,7 @@
from lp.bugs.interfaces.bugattachment import IBugAttachmentSet
from lp.bugs.interfaces.bugnomination import IBugNominationSet
from lp.bugs.mail.bugnotificationbuilder import format_rfc2822_date
+from lp.services.fields import DuplicateBug
from canonical.launchpad.mailnotification import (
MailWrapper)
=== modified file 'lib/lp/bugs/browser/bugalsoaffects.py'
--- lib/lp/bugs/browser/bugalsoaffects.py 2010-07-26 12:49:23 +0000
+++ lib/lp/bugs/browser/bugalsoaffects.py 2010-08-18 11:41:24 +0000
@@ -24,7 +24,7 @@
from canonical.cachedproperty import cachedproperty
from canonical.launchpad import _
from canonical.launchpad.browser.multistep import MultiStepView, StepView
-from canonical.launchpad.fields import StrippedTextLine
+from lp.services.fields import StrippedTextLine
from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
from canonical.launchpad.interfaces.validation import (
valid_upstreamtask, validate_new_distrotask)
=== modified file 'lib/lp/bugs/browser/bugtask.py'
--- lib/lp/bugs/browser/bugtask.py 2010-08-03 08:49:19 +0000
+++ lib/lp/bugs/browser/bugtask.py 2010-08-18 11:41:24 +0000
@@ -88,7 +88,7 @@
from canonical.database.sqlbase import cursor
from canonical.launchpad import _
from canonical.cachedproperty import cachedproperty
-from canonical.launchpad.fields import PersonChoice
+from lp.services.fields import PersonChoice
from canonical.launchpad.mailnotification import get_unified_diff
from canonical.launchpad.validators import LaunchpadValidationError
from canonical.launchpad.webapp import (
=== modified file 'lib/lp/bugs/browser/bugwatch.py'
--- lib/lp/bugs/browser/bugwatch.py 2010-05-17 15:54:18 +0000
+++ lib/lp/bugs/browser/bugwatch.py 2010-08-18 11:41:24 +0000
@@ -22,7 +22,7 @@
from lp.bugs.browser.bugtask import get_comments_for_bugtask
from lp.bugs.browser.bugcomment import (
should_display_remote_comments)
-from canonical.launchpad.fields import URIField
+from lp.services.fields import URIField
from canonical.launchpad.webapp.interfaces import ILaunchBag
from lp.bugs.interfaces.bugwatch import (
BUG_WATCH_ACTIVITY_SUCCESS_STATUSES, IBugWatch, IBugWatchSet,
=== modified file 'lib/lp/bugs/doc/bugwidget.txt'
--- lib/lp/bugs/doc/bugwidget.txt 2009-06-12 16:36:02 +0000
+++ lib/lp/bugs/doc/bugwidget.txt 2010-08-18 11:41:24 +0000
@@ -3,7 +3,7 @@
The BugWidget converts string bug ids to the corresponding bug object.
>>> from canonical.launchpad.webapp.servers import LaunchpadTestRequest
- >>> from canonical.launchpad.fields import BugField
+ >>> from lp.services.fields import BugField
>>> from canonical.widgets.bug import BugWidget
>>> bug_field = BugField(__name__='bug', title=u'Bug')
>>> request = LaunchpadTestRequest(form={'field.bug': '1'})
=== modified file 'lib/lp/bugs/interfaces/bug.py'
--- lib/lp/bugs/interfaces/bug.py 2010-08-07 14:54:40 +0000
+++ lib/lp/bugs/interfaces/bug.py 2010-08-18 11:41:24 +0000
@@ -29,7 +29,7 @@
from zope.schema.vocabulary import SimpleVocabulary
from canonical.launchpad import _
-from canonical.launchpad.fields import (
+from lp.services.fields import (
BugField, ContentNameField, DuplicateBug, PublicPersonChoice, Tag, Title)
from lazr.lifecycle.snapshot import doNotSnapshot
from lp.bugs.interfaces.bugattachment import IBugAttachment
=== modified file 'lib/lp/bugs/interfaces/bugattachment.py'
--- lib/lp/bugs/interfaces/bugattachment.py 2010-07-27 14:12:47 +0000
+++ lib/lp/bugs/interfaces/bugattachment.py 2010-08-18 11:41:24 +0000
@@ -22,7 +22,7 @@
from canonical.launchpad.interfaces.message import IMessage
from canonical.launchpad.interfaces.launchpad import IHasBug
-from canonical.launchpad.fields import Title
+from lp.services.fields import Title
from canonical.launchpad import _
from lazr.restful.fields import Reference
=== modified file 'lib/lp/bugs/interfaces/bugbranch.py'
--- lib/lp/bugs/interfaces/bugbranch.py 2009-08-12 00:22:33 +0000
+++ lib/lp/bugs/interfaces/bugbranch.py 2010-08-18 11:41:24 +0000
@@ -19,7 +19,7 @@
from lazr.restful.fields import ReferenceChoice
from canonical.launchpad import _
-from canonical.launchpad.fields import BugField, Summary
+from lp.services.fields import BugField, Summary
from canonical.launchpad.interfaces.launchpad import IHasBug, IHasDateCreated
from lp.bugs.interfaces.bugtask import IBugTask
from lp.code.interfaces.branchtarget import IHasBranchTarget
=== modified file 'lib/lp/bugs/interfaces/buglink.py'
--- lib/lp/bugs/interfaces/buglink.py 2009-06-25 00:40:31 +0000
+++ lib/lp/bugs/interfaces/buglink.py 2010-08-18 11:41:24 +0000
@@ -21,7 +21,7 @@
from zope.security.interfaces import Unauthorized
from canonical.launchpad import _
-from canonical.launchpad.fields import BugField
+from lp.services.fields import BugField
from lp.bugs.interfaces.bug import IBug
from canonical.launchpad.interfaces.launchpad import IHasBug
=== modified file 'lib/lp/bugs/interfaces/bugmessage.py'
--- lib/lp/bugs/interfaces/bugmessage.py 2010-06-23 03:25:52 +0000
+++ lib/lp/bugs/interfaces/bugmessage.py 2010-08-18 11:41:24 +0000
@@ -16,7 +16,7 @@
from zope.interface import Attribute, Interface
from zope.schema import Bool, Bytes, Int, Object, Text, TextLine
-from canonical.launchpad.fields import Title
+from lp.services.fields import Title
from lp.bugs.interfaces.bug import IBug
from lp.bugs.interfaces.bugwatch import IBugWatch
from canonical.launchpad.interfaces.launchpad import IHasBug
=== modified file 'lib/lp/bugs/interfaces/bugnomination.py'
--- lib/lp/bugs/interfaces/bugnomination.py 2009-10-26 18:40:04 +0000
+++ lib/lp/bugs/interfaces/bugnomination.py 2010-08-18 11:41:24 +0000
@@ -26,7 +26,7 @@
from lazr.restful.fields import Reference, ReferenceChoice
from canonical.launchpad import _
-from canonical.launchpad.fields import PublicPersonChoice
+from lp.services.fields import PublicPersonChoice
from canonical.launchpad.interfaces.launchpad import IHasBug, IHasDateCreated
from lp.bugs.interfaces.bug import IBug
from lp.bugs.interfaces.bugtarget import IBugTarget
=== modified file 'lib/lp/bugs/interfaces/bugnotification.py'
--- lib/lp/bugs/interfaces/bugnotification.py 2009-07-17 00:26:05 +0000
+++ lib/lp/bugs/interfaces/bugnotification.py 2010-08-18 11:41:24 +0000
@@ -16,7 +16,7 @@
from zope.schema import Bool, Datetime, TextLine
from canonical.launchpad import _
-from canonical.launchpad.fields import BugField
+from lp.services.fields import BugField
from lp.registry.interfaces.role import IHasOwner
=== modified file 'lib/lp/bugs/interfaces/bugsubscription.py'
--- lib/lp/bugs/interfaces/bugsubscription.py 2010-07-27 19:17:43 +0000
+++ lib/lp/bugs/interfaces/bugsubscription.py 2010-08-18 11:41:24 +0000
@@ -14,7 +14,7 @@
from zope.interface import Interface, Attribute
from zope.schema import Int, Datetime
from canonical.launchpad import _
-from canonical.launchpad.fields import PersonChoice
+from lp.services.fields import PersonChoice
from lp.bugs.interfaces.bug import IBug
from lazr.restful.declarations import (
=== modified file 'lib/lp/bugs/interfaces/bugsupervisor.py'
--- lib/lp/bugs/interfaces/bugsupervisor.py 2010-07-27 19:17:43 +0000
+++ lib/lp/bugs/interfaces/bugsupervisor.py 2010-08-18 11:41:24 +0000
@@ -12,7 +12,7 @@
]
from canonical.launchpad import _
-from canonical.launchpad.fields import PersonChoice
+from lp.services.fields import PersonChoice
from zope.interface import Interface
=== modified file 'lib/lp/bugs/interfaces/bugtarget.py'
--- lib/lp/bugs/interfaces/bugtarget.py 2010-06-25 18:48:13 +0000
+++ lib/lp/bugs/interfaces/bugtarget.py 2010-08-18 11:41:24 +0000
@@ -24,7 +24,7 @@
from zope.schema import Bool, Choice, Datetime, List, Object, Text, TextLine
from canonical.launchpad import _
-from canonical.launchpad.fields import Tag
+from lp.services.fields import Tag
from lp.bugs.interfaces.bugtask import (
BugBranchSearch, BugTagsSearchCombinator, IBugTask, IBugTaskSearch)
from lazr.enum import DBEnumeratedType
=== modified file 'lib/lp/bugs/interfaces/bugtask.py'
--- lib/lp/bugs/interfaces/bugtask.py 2010-07-28 13:26:55 +0000
+++ lib/lp/bugs/interfaces/bugtask.py 2010-08-18 11:41:24 +0000
@@ -56,7 +56,7 @@
DBEnumeratedType, DBItem, EnumeratedType, Item, use_template)
from canonical.launchpad import _
-from canonical.launchpad.fields import (
+from lp.services.fields import (
BugField, PersonChoice, ProductNameField, SearchTag,
StrippedTextLine, Summary)
from lp.bugs.interfaces.bugwatch import (
=== modified file 'lib/lp/bugs/interfaces/bugtracker.py'
--- lib/lp/bugs/interfaces/bugtracker.py 2010-06-21 04:08:54 +0000
+++ lib/lp/bugs/interfaces/bugtracker.py 2010-08-18 11:41:24 +0000
@@ -25,7 +25,7 @@
from lazr.enum import DBEnumeratedType, DBItem
from canonical.launchpad import _
-from canonical.launchpad.fields import (
+from lp.services.fields import (
ContentNameField, StrippedTextLine, URIField)
from canonical.launchpad.validators import LaunchpadValidationError
from canonical.launchpad.validators.name import name_validator
=== modified file 'lib/lp/bugs/interfaces/bugwatch.py'
--- lib/lp/bugs/interfaces/bugwatch.py 2010-05-19 14:03:18 +0000
+++ lib/lp/bugs/interfaces/bugwatch.py 2010-08-18 11:41:24 +0000
@@ -23,7 +23,7 @@
from lazr.enum import DBEnumeratedType, DBItem
from canonical.launchpad import _
-from canonical.launchpad.fields import StrippedTextLine
+from lp.services.fields import StrippedTextLine
from canonical.launchpad.interfaces.launchpad import IHasBug
from lp.bugs.interfaces.bugtracker import IBugTracker
=== modified file 'lib/lp/bugs/interfaces/securitycontact.py'
--- lib/lp/bugs/interfaces/securitycontact.py 2010-06-14 20:14:03 +0000
+++ lib/lp/bugs/interfaces/securitycontact.py 2010-08-18 11:41:24 +0000
@@ -16,7 +16,7 @@
from lazr.restful.declarations import exported
from canonical.launchpad import _
-from canonical.launchpad.fields import PublicPersonChoice
+from lp.services.fields import PublicPersonChoice
class IHasSecurityContact(Interface):
=== added file 'lib/lp/bugs/mail/tests/test_bug_task_modification.py'
--- lib/lp/bugs/mail/tests/test_bug_task_modification.py 1970-01-01 00:00:00 +0000
+++ lib/lp/bugs/mail/tests/test_bug_task_modification.py 2010-08-18 11:41:24 +0000
@@ -0,0 +1,60 @@
+# Copyright 2010 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Test for emails sent after bug task modification."""
+
+from unittest import TestLoader
+
+import transaction
+
+from zope.component import getUtility
+from zope.interface import providedBy
+from zope.event import notify
+
+from canonical.testing import DatabaseFunctionalLayer
+from canonical.launchpad.database import BugNotification
+from canonical.launchpad.webapp.interfaces import ILaunchBag
+
+
+from lp.bugs.interfaces.bugtask import BugTaskStatus
+from lp.bugs.scripts.bugnotification import construct_email_notifications
+from lp.testing import TestCaseWithFactory
+
+from lazr.lifecycle.event import ObjectModifiedEvent
+from lazr.lifecycle.snapshot import Snapshot
+
+
+class TestModificationNotification(TestCaseWithFactory):
+ """Test email notifications when a bug task is modified."""
+
+ layer = DatabaseFunctionalLayer
+
+ def setUp(self):
+ # Run the tests as a logged-in user.
+ super(TestModificationNotification, self).setUp(
+ user='test@xxxxxxxxxxxxx')
+ self.user = getUtility(ILaunchBag).user
+ self.product = self.factory.makeProduct(owner=self.user)
+ self.bug = self.factory.makeBug(product=self.product)
+ self.bug_task = self.bug.getBugTask(self.product)
+ self.bug_task_before_modification = Snapshot(self.bug_task,
+ providing=providedBy(self.bug_task))
+
+ def test_for_bug_modifier_header(self):
+ """Test X-Launchpad-Bug-Modifier appears when a bug is modified."""
+ self.bug_task.transitionToStatus(BugTaskStatus.CONFIRMED, self.user)
+ notify(ObjectModifiedEvent(
+ self.bug_task, self.bug_task_before_modification,
+ ['status'], user=self.user))
+ transaction.commit()
+ latest_notification = BugNotification.selectFirst(orderBy='-id')
+ notifications, messages = construct_email_notifications(
+ [latest_notification])
+ self.assertEqual(len(notifications), 1,
+ 'email notification not created')
+ headers = [msg['X-Launchpad-Bug-Modifier'] for msg in messages]
+ self.assertEqual(len(headers), len(messages))
+
+
+def test_suite():
+ return TestLoader().loadTestsFromName(__name__)
=== modified file 'lib/lp/bugs/model/bug.py'
--- lib/lp/bugs/model/bug.py 2010-08-07 14:54:40 +0000
+++ lib/lp/bugs/model/bug.py 2010-08-18 11:41:24 +0000
@@ -49,7 +49,7 @@
from canonical.launchpad.database.librarian import LibraryFileAlias
from canonical.launchpad.database.message import (
Message, MessageChunk, MessageSet)
-from canonical.launchpad.fields import DuplicateBug
+from lp.services.fields import DuplicateBug
from canonical.launchpad.helpers import shortlist
from lp.hardwaredb.interfaces.hwdb import IHWSubmissionBugSet
from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
=== modified file 'lib/lp/bugs/scripts/bugnotification.py'
--- lib/lp/bugs/scripts/bugnotification.py 2010-06-23 09:36:02 +0000
+++ lib/lp/bugs/scripts/bugnotification.py 2010-08-18 11:41:24 +0000
@@ -111,7 +111,7 @@
(email_person, recipient)
for email_person, recipient in recipients.items()
if recipient.person.inTeam(comment_syncing_team))
- bug_notification_builder = BugNotificationBuilder(bug)
+ bug_notification_builder = BugNotificationBuilder(bug, person)
sorted_recipients = sorted(
recipients.items(), key=lambda t: t[0].preferredemail.email)
for email_person, recipient in sorted_recipients:
=== modified file 'lib/lp/bugs/tests/test_bugheat.py'
--- lib/lp/bugs/tests/test_bugheat.py 2010-07-18 00:55:24 +0000
+++ lib/lp/bugs/tests/test_bugheat.py 2010-08-18 11:41:24 +0000
@@ -11,6 +11,7 @@
from canonical.testing import LaunchpadZopelessLayer
+from lp.bugs.interfaces.bugtask import BugTaskStatus
from lp.testing import TestCaseWithFactory
from lp.testing.factory import LaunchpadObjectFactory
@@ -110,9 +111,13 @@
def setUp(self):
TestCaseWithFactory.setUp(self)
- self.target = self.factory.makeDistributionSourcePackage()
+ self.target = self.factory.makeDistributionSourcePackage(with_db=True)
self.bugtask1 = self.factory.makeBugTask(target=self.target)
self.bugtask2 = self.factory.makeBugTask(target=self.target)
+ self.bugtask3 = self.factory.makeBugTask(target=self.target)
+ # A closed bug is not include in DSP bug heat calculations.
+ self.bugtask3.transitionToStatus(
+ BugTaskStatus.FIXRELEASED, self.target.distribution.owner)
# Bug heat gets calculated by complicated rules in a db
# stored procedure. We will override them here to avoid
# testing inconsitencies if those values are calculated
@@ -121,8 +126,10 @@
# automatically by bug.setHeat().
bug1 = self.bugtask1.bug
bug2 = self.bugtask2.bug
+ bug3 = self.bugtask3.bug
bug1.setHeat(7)
bug2.setHeat(19)
+ bug3.setHeat(11)
Store.of(bug1).flush()
self.max_heat = max(bug1.heat, bug2.heat)
self.total_heat = sum([bug1.heat, bug2.heat])
=== modified file 'lib/lp/buildmaster/interfaces/builder.py'
--- lib/lp/buildmaster/interfaces/builder.py 2010-08-06 10:16:14 +0000
+++ lib/lp/buildmaster/interfaces/builder.py 2010-08-18 11:41:24 +0000
@@ -23,7 +23,7 @@
from zope.schema import Bool, Choice, Field, Text, TextLine
from canonical.launchpad import _
-from canonical.launchpad.fields import Title, Description
+from lp.services.fields import Title, Description
from lp.registry.interfaces.role import IHasOwner
from canonical.launchpad.validators.name import name_validator
from canonical.launchpad.validators.url import builder_url_validator
=== modified file 'lib/lp/code/browser/branch.py'
--- lib/lp/code/browser/branch.py 2010-08-04 04:07:21 +0000
+++ lib/lp/code/browser/branch.py 2010-08-18 11:41:24 +0000
@@ -319,7 +319,10 @@
'+upgrade', 'Upgrade this branch', icon='edit', enabled=enabled)
def create_recipe(self):
- enabled = config.build_from_branch.enabled
+ if not self.context.private and config.build_from_branch.enabled:
+ enabled = True
+ else:
+ enabled = False
text = 'Create packaging recipe'
return Link('+new-recipe', text, enabled=enabled, icon='add')
@@ -564,8 +567,8 @@
def show_merge_links(self):
"""Return whether or not merge proposal links should be shown.
- Merge proposal links should not be shown if there is only one branch in
- a non-final state.
+ Merge proposal links should not be shown if there is only one branch
+ in a non-final state.
"""
if not self.context.target.supports_merge_proposals:
return False
=== modified file 'lib/lp/code/browser/branchmergeproposal.py'
--- lib/lp/code/browser/branchmergeproposal.py 2010-07-30 06:08:54 +0000
+++ lib/lp/code/browser/branchmergeproposal.py 2010-08-18 11:41:24 +0000
@@ -53,7 +53,7 @@
from canonical.config import config
from canonical.launchpad import _
-from canonical.launchpad.fields import Summary, Whiteboard
+from lp.services.fields import Summary, Whiteboard
from canonical.launchpad.interfaces.message import IMessageSet
from canonical.launchpad.webapp import (
canonical_url, ContextMenu, custom_widget, Link, enabled_with_permission,
=== modified file 'lib/lp/code/browser/codeimport.py'
--- lib/lp/code/browser/codeimport.py 2010-08-04 04:07:21 +0000
+++ lib/lp/code/browser/codeimport.py 2010-08-18 11:41:24 +0000
@@ -27,7 +27,7 @@
from canonical.cachedproperty import cachedproperty
from canonical.launchpad import _
-from canonical.launchpad.fields import URIField
+from lp.services.fields import URIField
from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
from lp.app.errors import NotFoundError
from lp.code.enums import (
=== modified file 'lib/lp/code/browser/codereviewvote.py'
--- lib/lp/code/browser/codereviewvote.py 2009-12-14 09:22:06 +0000
+++ lib/lp/code/browser/codereviewvote.py 2010-08-18 11:41:24 +0000
@@ -9,7 +9,7 @@
from zope.interface import Interface
from canonical.launchpad import _
-from canonical.launchpad.fields import PublicPersonChoice
+from lp.services.fields import PublicPersonChoice
from canonical.launchpad.webapp import (
action, canonical_url, LaunchpadFormView)
from lp.code.errors import ReviewNotPending, UserHasExistingReview
=== modified file 'lib/lp/code/browser/configure.zcml'
--- lib/lp/code/browser/configure.zcml 2010-08-09 16:17:24 +0000
+++ lib/lp/code/browser/configure.zcml 2010-08-18 11:41:24 +0000
@@ -1129,13 +1129,13 @@
class="lp.code.browser.sourcepackagerecipebuild.SourcePackageRecipeBuildCancelView"
name="+cancel"
template="../../app/templates/generic-edit.pt"
- permission="launchpad.Edit"/>
+ permission="launchpad.Admin"/>
<browser:page
for="lp.code.interfaces.sourcepackagerecipebuild.ISourcePackageRecipeBuild"
class="lp.code.browser.sourcepackagerecipebuild.SourcePackageRecipeBuildRescoreView"
name="+rescore"
template="../../app/templates/generic-edit.pt"
- permission="launchpad.Edit"/>
+ permission="launchpad.Admin"/>
<browser:menus
classes="
SourcePackageRecipeNavigationMenu
=== modified file 'lib/lp/code/browser/sourcepackagerecipebuild.py'
--- lib/lp/code/browser/sourcepackagerecipebuild.py 2010-07-28 14:36:32 +0000
+++ lib/lp/code/browser/sourcepackagerecipebuild.py 2010-08-18 11:41:24 +0000
@@ -47,7 +47,7 @@
links = ('cancel', 'rescore')
- @enabled_with_permission('launchpad.Edit')
+ @enabled_with_permission('launchpad.Admin')
def cancel(self):
if self.context.status in UNEDITABLE_BUILD_STATES:
enabled = False
@@ -55,7 +55,7 @@
enabled = True
return Link('+cancel', 'Cancel build', icon='remove', enabled=enabled)
- @enabled_with_permission('launchpad.Edit')
+ @enabled_with_permission('launchpad.Admin')
def rescore(self):
if self.context.status in UNEDITABLE_BUILD_STATES:
enabled = False
=== modified file 'lib/lp/code/browser/tests/test_sourcepackagerecipe.py'
--- lib/lp/code/browser/tests/test_sourcepackagerecipe.py 2010-08-09 17:14:10 +0000
+++ lib/lp/code/browser/tests/test_sourcepackagerecipe.py 2010-08-18 11:41:24 +0000
@@ -136,6 +136,18 @@
self.assertTextMatchesExpressionIgnoreWhitespace(
pattern, main_text)
+ def test_create_new_recipe_private_branch(self):
+ # Recipes can't be created on private branches.
+ with person_logged_in(self.chef):
+ branch = self.factory.makeBranch(private=True, owner=self.chef)
+ branch_url = canonical_url(branch)
+
+ browser = self.getUserBrowser(branch_url, user=self.chef)
+ self.assertRaises(
+ LinkNotFoundError,
+ browser.getLink,
+ 'Create packaging recipe')
+
def test_create_new_recipe_users_teams_as_owner_options(self):
# Teams that the user is in are options for the recipe owner.
self.factory.makeTeam(
=== modified file 'lib/lp/code/configure.zcml'
--- lib/lp/code/configure.zcml 2010-08-09 20:37:40 +0000
+++ lib/lp/code/configure.zcml 2010-08-18 11:41:24 +0000
@@ -606,6 +606,11 @@
for="lp.registry.interfaces.sourcepackage.ISourcePackage"
provides="lp.code.interfaces.branchtarget.IBranchTarget"
factory="lp.code.model.branchtarget.PackageBranchTarget"/>
+ <adapter
+ for="lp.registry.interfaces.distributionsourcepackage.IDistributionSourcePackage"
+ provides="lp.code.interfaces.branchtarget.IBranchTarget"
+ factory="lp.code.model.branchtarget.distribution_sourcepackage_to_branch_target"/>
+
<class class="lp.code.model.branchtarget.PersonBranchTarget">
<allow interface="lp.code.interfaces.branchtarget.IBranchTarget"/>
</class>
@@ -613,6 +618,7 @@
for="lp.registry.interfaces.person.IPerson"
provides="lp.code.interfaces.branchtarget.IBranchTarget"
factory="lp.code.model.branchtarget.PersonBranchTarget"/>
+
<class class="lp.code.model.branchtarget.ProductBranchTarget">
<allow interface="lp.code.interfaces.branchtarget.IBranchTarget"/>
</class>
@@ -621,6 +627,10 @@
provides="lp.code.interfaces.branchtarget.IBranchTarget"
factory="lp.code.model.branchtarget.ProductBranchTarget"/>
<adapter
+ for="lp.registry.interfaces.productseries.IProductSeries"
+ provides="lp.code.interfaces.branchtarget.IBranchTarget"
+ factory="lp.code.model.branchtarget.product_series_to_branch_target"/>
+ <adapter
for="lp.code.interfaces.branchtarget.IBranchTarget"
provides="canonical.launchpad.webapp.interfaces.ICanonicalUrlData"
factory="lp.code.model.branchtarget.get_canonical_url_data_for_target"/>
=== modified file 'lib/lp/code/interfaces/branch.py'
--- lib/lp/code/interfaces/branch.py 2010-08-03 08:49:19 +0000
+++ lib/lp/code/interfaces/branch.py 2010-08-18 11:41:24 +0000
@@ -42,7 +42,7 @@
from canonical.config import config
from canonical.launchpad import _
-from canonical.launchpad.fields import (
+from lp.services.fields import (
PersonChoice, PublicPersonChoice, URIField, Whiteboard)
from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
from canonical.launchpad.validators import LaunchpadValidationError
=== modified file 'lib/lp/code/interfaces/branchlookup.py'
--- lib/lp/code/interfaces/branchlookup.py 2009-11-19 15:46:10 +0000
+++ lib/lp/code/interfaces/branchlookup.py 2010-08-18 11:41:24 +0000
@@ -34,10 +34,8 @@
def traverse(path):
"""Traverse to the linked object referred to by 'path'.
- :raises NoSuchBranch: If we can't find a branch that matches the
- branch component of the path.
- :raises NoSuchPerson: If we can't find a person who matches the person
- component of the path.
+ :raises InvalidProductName: If the first segment of the path is not a
+ valid name.
:raises NoSuchProduct: If we can't find a product that matches the
product component of the path.
:raises NoSuchProductSeries: If the series component doesn't match an
=== modified file 'lib/lp/code/interfaces/branchmergeproposal.py'
--- lib/lp/code/interfaces/branchmergeproposal.py 2010-05-07 04:53:47 +0000
+++ lib/lp/code/interfaces/branchmergeproposal.py 2010-08-18 11:41:24 +0000
@@ -36,7 +36,7 @@
Bytes, Bool, Choice, Datetime, Int, Object, Text, TextLine)
from canonical.launchpad import _
-from canonical.launchpad.fields import PublicPersonChoice, Summary, Whiteboard
+from lp.services.fields import PublicPersonChoice, Summary, Whiteboard
from canonical.launchpad.interfaces.launchpad import IPrivacy
from lp.bugs.interfaces.bug import IBug
from lp.code.enums import BranchMergeProposalStatus, CodeReviewVote
=== modified file 'lib/lp/code/interfaces/branchmergequeue.py'
--- lib/lp/code/interfaces/branchmergequeue.py 2009-06-25 04:06:00 +0000
+++ lib/lp/code/interfaces/branchmergequeue.py 2010-08-18 11:41:24 +0000
@@ -17,7 +17,7 @@
from zope.schema import TextLine, Datetime
from canonical.launchpad import _
-from canonical.launchpad.fields import PublicPersonChoice, Summary
+from lp.services.fields import PublicPersonChoice, Summary
from canonical.launchpad.validators.name import name_validator
=== modified file 'lib/lp/code/interfaces/branchsubscription.py'
--- lib/lp/code/interfaces/branchsubscription.py 2010-07-27 19:17:43 +0000
+++ lib/lp/code/interfaces/branchsubscription.py 2010-08-18 11:41:24 +0000
@@ -19,7 +19,7 @@
BranchSubscriptionDiffSize, BranchSubscriptionNotificationLevel,
CodeReviewNotificationLevel)
from lp.code.interfaces.branch import IBranch
-from canonical.launchpad.fields import PersonChoice
+from lp.services.fields import PersonChoice
from lazr.restful.declarations import (
REQUEST_USER, call_with, export_as_webservice_entry,
export_read_operation, exported)
=== modified file 'lib/lp/code/interfaces/branchtarget.py'
--- lib/lp/code/interfaces/branchtarget.py 2010-04-23 04:11:12 +0000
+++ lib/lp/code/interfaces/branchtarget.py 2010-08-18 11:41:24 +0000
@@ -60,13 +60,15 @@
target = Attribute("The branch target, as an `IBranchTarget`.")
-class IBranchTarget(IPrimaryContext):
+class IBranchTarget(Interface):
"""A target of branches.
A product contains branches, a source package on a distroseries contains
branches, and a person contains 'junk' branches.
"""
+ context = Attribute('The primary context.')
+
name = Attribute("The name of the target.")
components = Attribute(
=== modified file 'lib/lp/code/interfaces/branchvisibilitypolicy.py'
--- lib/lp/code/interfaces/branchvisibilitypolicy.py 2010-07-27 19:17:43 +0000
+++ lib/lp/code/interfaces/branchvisibilitypolicy.py 2010-08-18 11:41:24 +0000
@@ -17,7 +17,7 @@
from zope.schema import Choice
from canonical.launchpad import _
-from canonical.launchpad.fields import PersonChoice
+from lp.services.fields import PersonChoice
from lp.code.enums import TeamBranchVisibilityRule
=== modified file 'lib/lp/code/interfaces/codehosting.py'
--- lib/lp/code/interfaces/codehosting.py 2010-04-21 04:17:23 +0000
+++ lib/lp/code/interfaces/codehosting.py 2010-08-18 11:41:24 +0000
@@ -7,6 +7,7 @@
__metaclass__ = type
__all__ = [
+ 'BRANCH_ALIAS_PREFIX',
'BRANCH_TRANSPORT',
'CONTROL_TRANSPORT',
'ICodehostingAPI',
@@ -45,6 +46,9 @@
# Indicates that a path points to a control directory.
CONTROL_TRANSPORT = 'CONTROL_TRANSPORT'
+# The path prefix for getting at branches via their short name.
+BRANCH_ALIAS_PREFIX = '+branch'
+
class ICodehostingApplication(ILaunchpadApplication):
"""Branch Puller application root."""
=== modified file 'lib/lp/code/interfaces/codeimport.py'
--- lib/lp/code/interfaces/codeimport.py 2010-04-28 02:49:58 +0000
+++ lib/lp/code/interfaces/codeimport.py 2010-08-18 11:41:24 +0000
@@ -19,7 +19,7 @@
from CVS.protocol import CVSRoot, CvsRootError
from canonical.launchpad import _
-from canonical.launchpad.fields import PublicPersonChoice, URIField
+from lp.services.fields import PublicPersonChoice, URIField
from canonical.launchpad.validators import LaunchpadValidationError
from lp.code.enums import CodeImportReviewStatus, RevisionControlSystems
from lp.code.interfaces.branch import IBranch
=== modified file 'lib/lp/code/interfaces/codeimportevent.py'
--- lib/lp/code/interfaces/codeimportevent.py 2009-06-25 04:06:00 +0000
+++ lib/lp/code/interfaces/codeimportevent.py 2010-08-18 11:41:24 +0000
@@ -16,7 +16,7 @@
from zope.schema import Datetime, Choice, Int
from canonical.launchpad import _
-from canonical.launchpad.fields import PublicPersonChoice
+from lp.services.fields import PublicPersonChoice
from lp.code.enums import CodeImportEventType
=== modified file 'lib/lp/code/interfaces/codereviewvote.py'
--- lib/lp/code/interfaces/codereviewvote.py 2009-12-14 06:41:18 +0000
+++ lib/lp/code/interfaces/codereviewvote.py 2010-08-18 11:41:24 +0000
@@ -12,7 +12,7 @@
from zope.schema import Bool, Datetime, Int, TextLine
from canonical.launchpad import _
-from canonical.launchpad.fields import PublicPersonChoice
+from lp.services.fields import PublicPersonChoice
from lp.code.interfaces.branchmergeproposal import (
IBranchMergeProposal)
from lp.code.interfaces.codereviewcomment import (
=== modified file 'lib/lp/code/interfaces/linkedbranch.py'
--- lib/lp/code/interfaces/linkedbranch.py 2010-08-03 03:37:46 +0000
+++ lib/lp/code/interfaces/linkedbranch.py 2010-08-18 11:41:24 +0000
@@ -12,7 +12,7 @@
__metaclass__ = type
__all__ = [
- 'get_linked_branch',
+ 'get_linked_to_branch',
'ICanHasLinkedBranch',
]
@@ -41,14 +41,14 @@
"""
-def get_linked_branch(provided):
- """Get the linked branch for 'provided', whatever that is.
+def get_linked_to_branch(provided):
+ """Get the `ICanHasLinkedBranch` for 'provided', whatever that is.
:raise CannotHaveLinkedBranch: If 'provided' can never have a linked
branch.
:raise NoLinkedBranch: If 'provided' could have a linked branch, but
doesn't.
- :return: The linked branch, an `IBranch`.
+ :return: The `ICanHasLinkedBranch` object.
"""
has_linked_branch = ICanHasLinkedBranch(provided, None)
if has_linked_branch is None:
@@ -57,7 +57,6 @@
# pocket.
provided = provided[0]
raise CannotHaveLinkedBranch(provided)
- branch = has_linked_branch.branch
- if branch is None:
+ if has_linked_branch.branch is None:
raise NoLinkedBranch(provided)
- return branch
+ return has_linked_branch
=== modified file 'lib/lp/code/interfaces/revision.py'
--- lib/lp/code/interfaces/revision.py 2010-07-20 14:36:53 +0000
+++ lib/lp/code/interfaces/revision.py 2010-08-18 11:41:24 +0000
@@ -14,7 +14,7 @@
from zope.schema import Bool, Datetime, Int, Text, TextLine
from canonical.launchpad import _
-from canonical.launchpad.fields import PublicPersonChoice
+from lp.services.fields import PublicPersonChoice
class IRevision(Interface):
=== modified file 'lib/lp/code/interfaces/sourcepackagerecipe.py'
--- lib/lp/code/interfaces/sourcepackagerecipe.py 2010-07-27 19:17:43 +0000
+++ lib/lp/code/interfaces/sourcepackagerecipe.py 2010-08-18 11:41:24 +0000
@@ -27,7 +27,7 @@
from zope.schema import Bool, Choice, Datetime, Int, Object, Text, TextLine
from canonical.launchpad import _
-from canonical.launchpad.fields import (
+from lp.services.fields import (
PersonChoice, PublicPersonChoice
)
from canonical.launchpad.validators.name import name_validator
=== modified file 'lib/lp/code/mail/codehandler.py'
--- lib/lp/code/mail/codehandler.py 2010-08-02 02:51:42 +0000
+++ lib/lp/code/mail/codehandler.py 2010-08-18 11:41:24 +0000
@@ -545,8 +545,12 @@
# access to any needed but not supplied revisions.
md.target_branch = target_url
md.install_revisions(bzr_branch.repository)
- bzr_branch.pull(bzr_branch, stop_revision=md.revision_id,
- overwrite=True)
+ bzr_branch.lock_write()
+ try:
+ bzr_branch.pull(bzr_branch, stop_revision=md.revision_id,
+ overwrite=True)
+ finally:
+ bzr_branch.unlock()
def findMergeDirectiveAndComment(self, message):
"""Extract the comment and Merge Directive from a SignedMessage."""
=== modified file 'lib/lp/code/mail/tests/test_codehandler.py'
--- lib/lp/code/mail/tests/test_codehandler.py 2010-07-27 05:30:04 +0000
+++ lib/lp/code/mail/tests/test_codehandler.py 2010-08-18 11:41:24 +0000
@@ -3,6 +3,8 @@
"""Testing the CodeHandler."""
+from __future__ import with_statement
+
__metaclass__ = type
from difflib import unified_diff
@@ -48,6 +50,7 @@
from lp.codehosting.vfs import get_lp_server
from lp.registry.interfaces.person import IPersonSet
from lp.services.job.runner import JobRunner
+from lp.services.osutils import override_environ
from lp.testing import login, login_person, TestCase, TestCaseWithFactory
from lp.testing.mail_helpers import pop_notifications
@@ -888,12 +891,16 @@
db_target_branch, target_tree = self.create_branch_and_tree(
tree_location='.', format=format)
target_tree.branch.set_public_branch(db_target_branch.bzr_identity)
- target_tree.commit('rev1')
- # Make sure that the created branch has been mirrored.
- removeSecurityProxy(db_target_branch).branchChanged(
- '', 'rev1', None, None, None)
- source_tree = target_tree.bzrdir.sprout('source').open_workingtree()
- source_tree.commit('rev2')
+ # XXX: AaronBentley 2010-08-06 bug=614404: a bzr username is
+ # required to generate the revision-id.
+ with override_environ(BZR_EMAIL='me@xxxxxxxxxxx'):
+ target_tree.commit('rev1')
+ # Make sure that the created branch has been mirrored.
+ removeSecurityProxy(db_target_branch).branchChanged(
+ '', 'rev1', None, None, None)
+ sprout_bzrdir = target_tree.bzrdir.sprout('source')
+ source_tree = sprout_bzrdir.open_workingtree()
+ source_tree.commit('rev2')
message = self.factory.makeBundleMergeDirectiveEmail(
source_tree.branch, db_target_branch)
return db_target_branch, source_tree.branch, message
@@ -994,7 +1001,10 @@
branch, source, message = self._createTargetSourceAndBundle(
format="1.9")
target_tree = WorkingTree.open('.')
- target_tree.commit('rev2b')
+ # XXX: AaronBentley 2010-08-06 bug=614404: a bzr username is
+ # required to generate the revision-id.
+ with override_environ(BZR_EMAIL='me@xxxxxxxxxxx'):
+ target_tree.commit('rev2b')
bmp = self._processMergeDirective(message)
lp_branch = self._openBazaarBranchAsClient(bmp.source_branch)
self.assertEqual(source.last_revision(), lp_branch.last_revision())
@@ -1005,20 +1015,24 @@
db_target_branch, target_tree = self.create_branch_and_tree(
'target', format=target_format)
target_tree.branch.set_public_branch(db_target_branch.bzr_identity)
- revid = target_tree.commit('rev1')
- removeSecurityProxy(db_target_branch).branchChanged(
- '', revid, None, None, None)
+ # XXX: AaronBentley 2010-08-06 bug=614404: a bzr username is
+ # required to generate the revision-id.
+ with override_environ(BZR_EMAIL='me@xxxxxxxxxxx'):
+ revid = target_tree.commit('rev1')
+ removeSecurityProxy(db_target_branch).branchChanged(
+ '', revid, None, None, None)
- db_source_branch, source_tree = self.create_branch_and_tree(
- 'lpsource', db_target_branch.product, format=source_format)
- # The branch is not scheduled to be mirrorred.
- self.assertIs(db_source_branch.next_mirror_time, None)
- source_tree.pull(target_tree.branch)
- source_tree.commit('rev2', rev_id='rev2')
- # bundle_tree is effectively behaving like a local copy of
- # db_source_branch, and is used to create the merge directive.
- bundle_tree = source_tree.bzrdir.sprout('source').open_workingtree()
- bundle_tree.commit('rev3', rev_id='rev3')
+ db_source_branch, source_tree = self.create_branch_and_tree(
+ 'lpsource', db_target_branch.product, format=source_format)
+ # The branch is not scheduled to be mirrorred.
+ self.assertIs(db_source_branch.next_mirror_time, None)
+ source_tree.pull(target_tree.branch)
+ source_tree.commit('rev2', rev_id='rev2')
+ # bundle_tree is effectively behaving like a local copy of
+ # db_source_branch, and is used to create the merge directive.
+ sprout_bzrdir = source_tree.bzrdir.sprout('source')
+ bundle_tree = sprout_bzrdir.open_workingtree()
+ bundle_tree.commit('rev3', rev_id='rev3')
bundle_tree.branch.set_public_branch(db_source_branch.bzr_identity)
message = self.factory.makeBundleMergeDirectiveEmail(
bundle_tree.branch, db_target_branch,
=== modified file 'lib/lp/code/model/branch.py'
--- lib/lp/code/model/branch.py 2010-08-03 08:49:19 +0000
+++ lib/lp/code/model/branch.py 2010-08-18 11:41:24 +0000
@@ -650,6 +650,7 @@
# actually a very interesting thing to tell the user about.
if self.code_import is not None:
DeleteCodeImport(self.code_import)()
+ Store.of(self).flush()
def associatedProductSeries(self):
"""See `IBranch`."""
@@ -1131,7 +1132,6 @@
def __call__(self):
self.affected_object.prerequisite_branch = None
- Store.of(self.affected_object).flush()
class ClearSeriesBranch(DeletionOperation):
@@ -1145,7 +1145,6 @@
def __call__(self):
if self.affected_object.branch == self.branch:
self.affected_object.branch = None
- Store.of(self.affected_object).flush()
class ClearSeriesTranslationsBranch(DeletionOperation):
@@ -1158,9 +1157,8 @@
self.branch = branch
def __call__(self):
- if self.affected_object.branch == self.branch:
- self.affected_object.branch = None
- self.affected_object.syncUpdate()
+ if self.affected_object.translations_branch == self.branch:
+ self.affected_object.translations_branch = None
class ClearOfficialPackageBranch(DeletionOperation):
@@ -1322,6 +1320,6 @@
assert scheme in accepted_schemes, "Unknown scheme: %s" % scheme
host = URI(config.codehosting.supermirror_root).host
path = '/' + urlutils.escape(unique_name)
- if suffix is not None:
+ if suffix:
path = os.path.join(path, suffix)
return str(URI(scheme=scheme, host=host, path=path))
=== modified file 'lib/lp/code/model/branchlookup.py'
--- lib/lp/code/model/branchlookup.py 2010-08-03 03:43:33 +0000
+++ lib/lp/code/model/branchlookup.py 2010-08-18 11:41:24 +0000
@@ -26,7 +26,7 @@
from lp.code.interfaces.branchlookup import (
IBranchLookup, ILinkedBranchTraversable, ILinkedBranchTraverser)
from lp.code.interfaces.branchnamespace import IBranchNamespaceSet
-from lp.code.interfaces.linkedbranch import get_linked_branch
+from lp.code.interfaces.linkedbranch import get_linked_to_branch
from lp.registry.interfaces.distribution import IDistribution
from lp.registry.interfaces.distroseries import (
IDistroSeries, IDistroSeriesSet, NoSuchDistroSeries)
@@ -352,25 +352,27 @@
def getByLPPath(self, path):
"""See `IBranchLookup`."""
- branch = suffix = None
- if not path.startswith('~'):
- # If the first element doesn't start with a tilde, then maybe
- # 'path' is a shorthand notation for a branch.
- result = getUtility(ILinkedBranchTraverser).traverse(path)
- branch = self._getLinkedBranch(result)
- else:
+ if path.startswith('~'):
namespace_set = getUtility(IBranchNamespaceSet)
segments = iter(path.lstrip('~').split('/'))
branch = namespace_set.traverse(segments)
suffix = '/'.join(segments)
if not check_permission('launchpad.View', branch):
raise NoSuchBranch(path)
- if suffix == '':
- suffix = None
+ else:
+ # If the first element doesn't start with a tilde, then maybe
+ # 'path' is a shorthand notation for a branch.
+ object_with_branch_link = getUtility(
+ ILinkedBranchTraverser).traverse(path)
+ branch, bzr_path = self._getLinkedBranchAndPath(
+ object_with_branch_link)
+ suffix = path[len(bzr_path) + 1:]
+ if suffix == '':
+ suffix = None
return branch, suffix
- def _getLinkedBranch(self, provided):
- """Get the linked branch for 'provided'.
+ def _getLinkedBranchAndPath(self, provided):
+ """Get the linked branch for 'provided', and the bzr_path.
:raise CannotHaveLinkedBranch: If 'provided' can never have a linked
branch.
@@ -378,7 +380,7 @@
doesn't.
:return: The linked branch, an `IBranch`.
"""
- branch = get_linked_branch(provided)
- if not check_permission('launchpad.View', branch):
+ linked = get_linked_to_branch(provided)
+ if not check_permission('launchpad.View', linked.branch):
raise NoLinkedBranch(provided)
- return branch
+ return linked.branch, linked.bzr_path
=== modified file 'lib/lp/code/model/branchtarget.py'
--- lib/lp/code/model/branchtarget.py 2010-04-14 17:44:00 +0000
+++ lib/lp/code/model/branchtarget.py 2010-08-18 11:41:24 +0000
@@ -340,3 +340,17 @@
def get_canonical_url_data_for_target(branch_target):
"""Return the `ICanonicalUrlData` for an `IBranchTarget`."""
return ICanonicalUrlData(branch_target.context)
+
+
+def product_series_to_branch_target(product_series):
+ """The Product itself is the branch target given a ProductSeries."""
+ return ProductBranchTarget(product_series.product)
+
+def distribution_sourcepackage_to_branch_target(distro_sourcepackage):
+ """The development version of the distro sourcepackage is the target."""
+ dev_version = distro_sourcepackage.development_version
+ # It is possible for distributions to not have any series, and if that is
+ # the case, the dev_version is None.
+ if dev_version is None:
+ return None
+ return PackageBranchTarget(dev_version)
=== modified file 'lib/lp/code/model/diff.py'
--- lib/lp/code/model/diff.py 2010-08-02 02:13:52 +0000
+++ lib/lp/code/model/diff.py 2010-08-18 11:41:24 +0000
@@ -140,7 +140,7 @@
source_revision)
merger = Merge3Merger(
merge_target, merge_target, merge_base, merge_source,
- do_merge=False)
+ this_branch=target_branch, do_merge=False)
def dummy_warning(self, *args, **kwargs):
pass
real_warning = trace.warning
=== modified file 'lib/lp/code/model/directbranchcommit.py'
--- lib/lp/code/model/directbranchcommit.py 2010-05-15 17:43:59 +0000
+++ lib/lp/code/model/directbranchcommit.py 2010-08-18 11:41:24 +0000
@@ -3,6 +3,8 @@
"""Commit files straight to bzr branch."""
+from __future__ import with_statement
+
__metaclass__ = type
__all__ = [
'ConcurrentUpdateError',
@@ -19,6 +21,8 @@
from canonical.launchpad.interfaces import IMasterObject
from lp.codehosting.bzrutils import get_stacked_on_url
+from lp.services.osutils import override_environ
+from lp.services.mail.sendmail import format_address_for_person
class ConcurrentUpdateError(Exception):
"""Bailout exception for concurrent updates.
@@ -188,8 +192,12 @@
if rev_id == NULL_REVISION:
if list(self.transform_preview.iter_changes()) == []:
return
- new_rev_id = self.transform_preview.commit(
- self.bzrbranch, commit_message)
+ committer_id = format_address_for_person(self.committer)
+ # XXX: AaronBentley 2010-08-06 bug=614404: a bzr username is
+ # required to generate the revision-id.
+ with override_environ(BZR_EMAIL=committer_id):
+ new_rev_id = self.transform_preview.commit(
+ self.bzrbranch, commit_message, committer=committer_id)
IMasterObject(self.db_branch).branchChanged(
get_stacked_on_url(self.bzrbranch), new_rev_id,
self.db_branch.control_format, self.db_branch.branch_format,
=== modified file 'lib/lp/code/model/tests/test_branch.py'
--- lib/lp/code/model/tests/test_branch.py 2010-08-02 02:36:32 +0000
+++ lib/lp/code/model/tests/test_branch.py 2010-08-18 11:41:24 +0000
@@ -84,6 +84,7 @@
from lp.testing.factory import LaunchpadObjectFactory
from lp.translations.model.translationtemplatesbuildjob import (
ITranslationTemplatesBuildJobSource)
+from lp.services.osutils import override_environ
class TestCodeImport(TestCase):
@@ -931,15 +932,18 @@
layer = LaunchpadZopelessLayer
def setUp(self):
- TestCaseWithFactory.setUp(self, 'test@xxxxxxxxxxxxx')
- self.product = ProductSet().getByName('firefox')
- self.user = getUtility(IPersonSet).getByEmail('test@xxxxxxxxxxxxx')
+ TestCaseWithFactory.setUp(self)
+ self.user = self.factory.makePerson()
+ self.product = self.factory.makeProduct(owner=self.user)
self.branch = self.factory.makeProductBranch(
name='to-delete', owner=self.user, product=self.product)
# The owner of the branch is subscribed to the branch when it is
# created. The tests here assume no initial connections, so
# unsubscribe the branch owner here.
self.branch.unsubscribe(self.branch.owner, self.branch.owner)
+ # Make sure that the tests all flush the database changes.
+ self.addCleanup(Store.of(self.branch).flush)
+ login_person(self.user)
def test_deletable(self):
"""A newly created branch can be deleted without any problems."""
@@ -1077,10 +1081,14 @@
# remove the Job and BranchJob.
branch = self.factory.makeAnyBranch()
getUtility(ITranslationTemplatesBuildJobSource).create(branch)
-
branch.destroySelf(break_references=True)
- Store.of(branch).flush()
+ def test_linked_translations_branch_cleared(self):
+ # The translations_branch of a series that is linked to the branch
+ # should be cleared.
+ dev_focus = self.branch.product.development_focus
+ dev_focus.translations_branch = self.branch
+ self.branch.destroySelf(break_references=True)
def test_unrelated_TranslationTemplatesBuildJob_intact(self):
# No innocent BuildQueue entries are harmed in deleting a
@@ -1120,21 +1128,15 @@
def test_destroySelf_with_SourcePackageRecipe(self):
"""If branch is a base_branch in a recipe, it is deleted."""
recipe = self.factory.makeSourcePackageRecipe()
- store = Store.of(recipe)
recipe.base_branch.destroySelf(break_references=True)
- # show no DB constraints have been violated
- store.flush()
def test_destroySelf_with_SourcePackageRecipe_as_non_base(self):
"""If branch is referred to by a recipe, it is deleted."""
branch1 = self.factory.makeAnyBranch()
branch2 = self.factory.makeAnyBranch()
- recipe = self.factory.makeSourcePackageRecipe(
+ self.factory.makeSourcePackageRecipe(
branches=[branch1, branch2])
- store = Store.of(recipe)
branch2.destroySelf(break_references=True)
- # show no DB constraints have been violated
- store.flush()
class TestBranchDeletionConsequences(TestCase):
@@ -2572,7 +2574,10 @@
# safe_open returns the underlying bzr branch of a database branch in
# the simple, unstacked, case.
db_branch, tree = self.create_branch_and_tree()
- revid = tree.commit('')
+ # XXX: AaronBentley 2010-08-06 bug=614404: a bzr username is
+ # required to generate the revision-id.
+ with override_environ(BZR_EMAIL='me@xxxxxxxxxxx'):
+ revid = tree.commit('')
bzr_branch = db_branch.getBzrBranch()
self.assertEqual(revid, bzr_branch.last_revision())
=== modified file 'lib/lp/code/model/tests/test_branchjob.py'
--- lib/lp/code/model/tests/test_branchjob.py 2010-07-29 08:06:07 +0000
+++ lib/lp/code/model/tests/test_branchjob.py 2010-08-18 11:41:24 +0000
@@ -3,6 +3,8 @@
"""Tests for BranchJobs."""
+from __future__ import with_statement
+
__metaclass__ = type
import datetime
@@ -41,6 +43,7 @@
from lp.testing.mail_helpers import pop_notifications
from lp.services.job.interfaces.job import JobStatus
from lp.services.job.model.job import Job
+from lp.services.osutils import override_environ
from lp.code.bzr import BranchFormat, RepositoryFormat
from lp.code.enums import (
BranchMergeProposalStatus, BranchSubscriptionDiffSize,
@@ -104,7 +107,10 @@
"""Ensure that run calculates revision ids."""
self.useBzrBranches(direct_database=True)
branch, tree = self.create_branch_and_tree()
- tree.commit('First commit', rev_id='rev1')
+ # XXX: AaronBentley 2010-08-06 bug=614404: a bzr username is
+ # required to generate the revision-id.
+ with override_environ(BZR_EMAIL='me@xxxxxxxxxxx'):
+ tree.commit('First commit', rev_id='rev1')
job = BranchDiffJob.create(branch, '0', '1')
static_diff = job.run()
self.assertEqual('null:', static_diff.from_revision_id)
@@ -122,9 +128,12 @@
tree_file = os.path.join(tree_location, 'file')
open(tree_file, 'wb').write('foo\n')
tree.add('file')
- tree.commit('First commit')
- open(tree_file, 'wb').write('bar\n')
- tree.commit('Next commit')
+ # XXX: AaronBentley 2010-08-06 bug=614404: a bzr username is
+ # required to generate the revision-id.
+ with override_environ(BZR_EMAIL='me@xxxxxxxxxxx'):
+ tree.commit('First commit')
+ open(tree_file, 'wb').write('bar\n')
+ tree.commit('Next commit')
job = BranchDiffJob.create(branch, '1', '2')
static_diff = job.run()
transaction.commit()
@@ -138,7 +147,10 @@
"""Ensure running an equivalent job emits the same diff."""
self.useBzrBranches(direct_database=True)
branch, tree = self.create_branch_and_tree()
- tree.commit('First commit')
+ # XXX: AaronBentley 2010-08-06 bug=614404: a bzr username is
+ # required to generate the revision-id.
+ with override_environ(BZR_EMAIL='me@xxxxxxxxxxx'):
+ tree.commit('First commit')
job1 = BranchDiffJob.create(branch, '0', '1')
static_diff1 = job1.run()
job2 = BranchDiffJob.create(branch, '0', '1')
@@ -157,7 +169,10 @@
tree_transport = tree.bzrdir.root_transport
tree_transport.put_bytes("hello.txt", "Hello World\n")
tree.add('hello.txt')
- tree.commit('rev1', timestamp=1e9, timezone=0)
+ # XXX: AaronBentley 2010-08-06 bug=614404: a bzr username is
+ # required to generate the revision-id.
+ with override_environ(BZR_EMAIL='me@xxxxxxxxxxx'):
+ tree.commit('rev1', timestamp=1e9, timezone=0)
job = BranchDiffJob.create(branch, '0', '1')
diff = job.run()
transaction.commit()
@@ -202,20 +217,23 @@
self.useBzrBranches(direct_database=True)
db_branch, bzr_tree = self.create_branch_and_tree()
- bzr_tree.commit('First commit', rev_id='rev1')
- bzr_tree.commit('Second commit', rev_id='rev2')
- bzr_tree.commit('Third commit', rev_id='rev3')
- LaunchpadZopelessLayer.commit()
-
- job = BranchScanJob.create(db_branch)
- LaunchpadZopelessLayer.switchDbUser(config.branchscanner.dbuser)
- job.run()
- LaunchpadZopelessLayer.switchDbUser(config.launchpad.dbuser)
-
- self.assertEqual(db_branch.revision_count, 3)
-
- bzr_tree.commit('Fourth commit', rev_id='rev4')
- bzr_tree.commit('Fifth commit', rev_id='rev5')
+ # XXX: AaronBentley 2010-08-06 bug=614404: a bzr username is
+ # required to generate the revision-id.
+ with override_environ(BZR_EMAIL='me@xxxxxxxxxxx'):
+ bzr_tree.commit('First commit', rev_id='rev1')
+ bzr_tree.commit('Second commit', rev_id='rev2')
+ bzr_tree.commit('Third commit', rev_id='rev3')
+ LaunchpadZopelessLayer.commit()
+
+ job = BranchScanJob.create(db_branch)
+ LaunchpadZopelessLayer.switchDbUser(config.branchscanner.dbuser)
+ job.run()
+ LaunchpadZopelessLayer.switchDbUser(config.launchpad.dbuser)
+
+ self.assertEqual(db_branch.revision_count, 3)
+
+ bzr_tree.commit('Fourth commit', rev_id='rev4')
+ bzr_tree.commit('Fifth commit', rev_id='rev5')
job = BranchScanJob.create(db_branch)
LaunchpadZopelessLayer.switchDbUser(config.branchscanner.dbuser)
@@ -381,7 +399,10 @@
branch, tree = self.create_branch_and_tree()
tree.bzrdir.root_transport.put_bytes('foo', 'bar\n')
tree.add('foo')
- tree.commit('First commit')
+ # XXX: AaronBentley 2010-08-06 bug=614404: a bzr username is
+ # required to generate the revision-id.
+ with override_environ(BZR_EMAIL='me@xxxxxxxxxxx'):
+ tree.commit('First commit')
job = RevisionMailJob.create(
branch, 1, 'from@xxxxxxxxxxx', 'hello', True, 'subject')
mailer = job.getMailer()
@@ -477,9 +498,12 @@
branch, tree = self.create_branch_and_tree()
tree.lock_write()
try:
- tree.commit('rev1', rev_id='rev1')
- tree.commit('rev2', rev_id='rev2')
- tree.commit('rev3', rev_id='rev3')
+ # XXX: AaronBentley 2010-08-06 bug=614404: a bzr username is
+ # required to generate the revision-id.
+ with override_environ(BZR_EMAIL='me@xxxxxxxxxxx'):
+ tree.commit('rev1', rev_id='rev1')
+ tree.commit('rev2', rev_id='rev2')
+ tree.commit('rev3', rev_id='rev3')
transaction.commit()
self.layer.switchDbUser('branchscanner')
self.updateDBRevisions(
@@ -504,7 +528,10 @@
branch, tree = self.create3CommitsBranch()
tree.pull(tree.branch, overwrite=True, stop_revision='rev2')
tree.add_parent_tree_id('rev3')
- tree.commit('rev3a', rev_id='rev3a')
+ # XXX: AaronBentley 2010-08-06 bug=614404: a bzr username is
+ # required to generate the revision-id.
+ with override_environ(BZR_EMAIL='me@xxxxxxxxxxx'):
+ tree.commit('rev3a', rev_id='rev3a')
self.updateDBRevisions(branch, tree.branch, ['rev3', 'rev3a'])
job = RevisionsAddedJob.create(branch, 'rev1', 'rev3', '')
job.bzr_branch.lock_read()
@@ -542,9 +569,12 @@
tree.branch.nick = 'nicholas'
tree.lock_write()
self.addCleanup(tree.unlock)
- tree.commit(
- 'rev1', rev_id='rev1', timestamp=1000, timezone=0,
- committer='J. Random Hacker <jrandom@xxxxxxxxxxx>')
+ # XXX: AaronBentley 2010-08-06 bug=614404: a bzr username is
+ # required to generate the revision-id.
+ with override_environ(BZR_EMAIL='me@xxxxxxxxxxx'):
+ tree.commit(
+ 'rev1', rev_id='rev1', timestamp=1000, timezone=0,
+ committer='J. Random Hacker <jrandom@xxxxxxxxxxx>')
return branch, tree
def makeRevisionsAddedWithMergeCommit(self, authors=None,
@@ -558,20 +588,23 @@
self.useBzrBranches(direct_database=True)
branch, tree = self.create_branch_and_tree()
tree.branch.nick = 'nicholas'
- tree.commit('rev1')
- tree2 = tree.bzrdir.sprout('tree2').open_workingtree()
- tree2.commit('rev2a', rev_id='rev2a-id', committer='foo@')
- tree2.commit('rev3', rev_id='rev3-id',
- authors=['bar@', 'baz@xxxxxxxxxx'])
- tree.merge_from_branch(tree2.branch)
- tree3 = tree.bzrdir.sprout('tree3').open_workingtree()
- tree3.commit('rev2b', rev_id='rev2b-id', committer='qux@')
- tree.merge_from_branch(tree3.branch, force=True)
- if include_ghost:
- tree.add_parent_tree_id('rev2c-id')
- tree.commit('rev2d', rev_id='rev2d-id', timestamp=1000, timezone=0,
- committer='J. Random Hacker <jrandom@xxxxxxxxxxx>',
- authors=authors)
+ # XXX: AaronBentley 2010-08-06 bug=614404: a bzr username is
+ # required to generate the revision-id.
+ with override_environ(BZR_EMAIL='me@xxxxxxxxxxx'):
+ tree.commit('rev1')
+ tree2 = tree.bzrdir.sprout('tree2').open_workingtree()
+ tree2.commit('rev2a', rev_id='rev2a-id', committer='foo@')
+ tree2.commit('rev3', rev_id='rev3-id',
+ authors=['bar@', 'baz@xxxxxxxxxx'])
+ tree.merge_from_branch(tree2.branch)
+ tree3 = tree.bzrdir.sprout('tree3').open_workingtree()
+ tree3.commit('rev2b', rev_id='rev2b-id', committer='qux@')
+ tree.merge_from_branch(tree3.branch, force=True)
+ if include_ghost:
+ tree.add_parent_tree_id('rev2c-id')
+ tree.commit('rev2d', rev_id='rev2d-id', timestamp=1000, timezone=0,
+ committer='J. Random Hacker <jrandom@xxxxxxxxxxx>',
+ authors=authors)
return RevisionsAddedJob.create(branch, 'rev2d-id', 'rev2d-id', '')
def test_getMergedRevisionIDs(self):
@@ -817,17 +850,20 @@
first_revision = 'rev-1'
tree.bzrdir.root_transport.put_bytes('hello.txt', 'Hello World\n')
tree.add('hello.txt')
- tree.commit(
- rev_id=first_revision, message="Log message",
- committer="Joe Bloggs <joe@xxxxxxxxxxx>", timestamp=1000000000.0,
- timezone=0)
- tree.bzrdir.root_transport.put_bytes(
- 'hello.txt', 'Hello World\n\nFoo Bar\n')
- second_revision = 'rev-2'
- tree.commit(
- rev_id=second_revision, message="Extended contents",
- committer="Joe Bloggs <joe@xxxxxxxxxxx>", timestamp=1000100000.0,
- timezone=0)
+ # XXX: AaronBentley 2010-08-06 bug=614404: a bzr username is
+ # required to generate the revision-id.
+ with override_environ(BZR_EMAIL='me@xxxxxxxxxxx'):
+ tree.commit(
+ rev_id=first_revision, message="Log message",
+ committer="Joe Bloggs <joe@xxxxxxxxxxx>",
+ timestamp=1000000000.0, timezone=0)
+ tree.bzrdir.root_transport.put_bytes(
+ 'hello.txt', 'Hello World\n\nFoo Bar\n')
+ second_revision = 'rev-2'
+ tree.commit(
+ rev_id=second_revision, message="Extended contents",
+ committer="Joe Bloggs <joe@xxxxxxxxxxx>",
+ timestamp=1000100000.0, timezone=0)
transaction.commit()
self.layer.switchDbUser('branchscanner')
self.updateDBRevisions(db_branch, tree.branch)
@@ -874,9 +910,13 @@
self.useBzrBranches(direct_database=True)
db_branch, tree = self.create_branch_and_tree()
rev_id = 'rev-1'
- tree.commit(
- rev_id=rev_id, message=u"Non ASCII: \xe9",
- committer=u"Non ASCII: \xed", timestamp=1000000000.0, timezone=0)
+ # XXX: AaronBentley 2010-08-06 bug=614404: a bzr username is
+ # required to generate the revision-id.
+ with override_environ(BZR_EMAIL='me@xxxxxxxxxxx'):
+ tree.commit(
+ rev_id=rev_id, message=u"Non ASCII: \xe9",
+ committer=u"Non ASCII: \xed", timestamp=1000000000.0,
+ timezone=0)
transaction.commit()
self.layer.switchDbUser('branchscanner')
self.updateDBRevisions(db_branch, tree.branch)
@@ -986,7 +1026,10 @@
[self.tree.abspath(file_pair[0]) for file_pair in files])
if commit_message is None:
commit_message = self.factory.getUniqueString('commit')
- revision_id = self.tree.commit(commit_message)
+ # XXX: AaronBentley 2010-08-06 bug=614404: a bzr username is
+ # required to generate the revision-id.
+ with override_environ(BZR_EMAIL='me@xxxxxxxxxxx'):
+ revision_id = self.tree.commit(commit_message)
self.branch.last_scanned_id = revision_id
self.branch.last_mirrored_id = revision_id
return revision_id
=== modified file 'lib/lp/code/model/tests/test_branchlookup.py'
--- lib/lp/code/model/tests/test_branchlookup.py 2010-08-03 03:43:33 +0000
+++ lib/lp/code/model/tests/test_branchlookup.py 2010-08-18 11:41:24 +0000
@@ -636,7 +636,7 @@
series.branch = branch
result = self.branch_lookup.getByLPPath(
'%s/%s/other/bits' % (series.product.name, series.name))
- self.assertEqual((branch, None), result)
+ self.assertEqual((branch, u'other/bits'), result)
def test_too_long_sourcepackage(self):
# If the provided path points to an existing source package with a
@@ -654,7 +654,7 @@
ubuntu_branches.teamowner)
result = self.branch_lookup.getByLPPath(
'%s/other/bits' % package.path)
- self.assertEqual((branch, None), result)
+ self.assertEqual((branch, u'other/bits'), result)
def test_suite():
=== modified file 'lib/lp/code/model/tests/test_branchmergeproposaljobs.py'
--- lib/lp/code/model/tests/test_branchmergeproposaljobs.py 2010-06-11 01:51:15 +0000
+++ lib/lp/code/model/tests/test_branchmergeproposaljobs.py 2010-08-18 11:41:24 +0000
@@ -3,6 +3,8 @@
"""Tests for branch merge proposal jobs."""
+from __future__ import with_statement
+
__metaclass__ = type
from datetime import datetime, timedelta
@@ -39,6 +41,7 @@
from lp.code.subscribers.branchmergeproposal import merge_proposal_modified
from lp.services.job.runner import JobRunner
from lp.services.job.model.job import Job
+from lp.services.osutils import override_environ
from lp.testing import TestCaseWithFactory
from lp.testing.mail_helpers import pop_notifications
@@ -104,7 +107,10 @@
def createProposalWithEmptyBranches(self):
target_branch, tree = self.create_branch_and_tree()
- tree.commit('test')
+ # XXX: AaronBentley 2010-08-06 bug=614404: a bzr username is
+ # required to generate the revision-id.
+ with override_environ(BZR_EMAIL='me@xxxxxxxxxxx'):
+ tree.commit('test')
source_branch = self.factory.makeProductBranch(
product=target_branch.product)
self.createBzrBranch(source_branch, tree.branch)
@@ -137,7 +143,10 @@
bmp = self.factory.makeBranchMergeProposal(
target_branch=self.factory.makePackageBranch())
tree = self.create_branch_and_tree(db_branch=bmp.target_branch)[1]
- tree.commit('Initial commit')
+ # XXX: AaronBentley 2010-08-06 bug=614404: a bzr username is
+ # required to generate the revision-id.
+ with override_environ(BZR_EMAIL='me@xxxxxxxxxxx'):
+ tree.commit('Initial commit')
self.createBzrBranch(bmp.source_branch, tree.branch)
self.factory.makeRevisionsForBranch(bmp.source_branch, count=1)
job = MergeProposalCreatedJob.create(bmp)
=== modified file 'lib/lp/code/model/tests/test_branchtarget.py'
--- lib/lp/code/model/tests/test_branchtarget.py 2010-07-28 04:18:53 +0000
+++ lib/lp/code/model/tests/test_branchtarget.py 2010-08-18 11:41:24 +0000
@@ -26,8 +26,8 @@
class BaseBranchTargetTests:
- def test_provides_IPrimaryContext(self):
- self.assertProvides(self.target, IPrimaryContext)
+ def test_provides_IBranchTarget(self):
+ self.assertProvides(self.target, IBranchTarget)
def test_context(self):
# IBranchTarget.context is the original object.
@@ -93,6 +93,21 @@
target = IBranchTarget(self.original)
self.assertIsInstance(target, PackageBranchTarget)
+ def test_distrosourcepackage_adapter(self):
+ # Adapting a distrosourcepackage will make a branch target with the
+ # current series of the distro as the distroseries.
+ distro = self.original.distribution
+ distro_sourcepackage = distro.getSourcePackage(
+ self.original.sourcepackagename)
+ target = IBranchTarget(distro_sourcepackage)
+ self.assertIsInstance(target, PackageBranchTarget)
+ self.assertEqual(
+ [distro, distro.currentseries],
+ target.components[:2])
+ self.assertEqual(
+ self.original.sourcepackagename,
+ target.components[2].sourcepackagename)
+
def test_components(self):
target = IBranchTarget(self.original)
self.assertEqual(
@@ -320,6 +335,14 @@
target = IBranchTarget(self.original)
self.assertIsInstance(target, ProductBranchTarget)
+ def test_productseries_adapter(self):
+ # Adapting a product series will make a product branch target.
+ product = self.factory.makeProduct()
+ series = self.factory.makeProductSeries(product)
+ target = IBranchTarget(series)
+ self.assertIsInstance(target, ProductBranchTarget)
+ self.assertEqual([product], target.components)
+
def test_components(self):
target = IBranchTarget(self.original)
self.assertEqual([self.original], list(target.components))
=== modified file 'lib/lp/code/model/tests/test_diff.py'
--- lib/lp/code/model/tests/test_diff.py 2010-08-02 02:13:52 +0000
+++ lib/lp/code/model/tests/test_diff.py 2010-08-18 11:41:24 +0000
@@ -3,6 +3,8 @@
"""Tests for Diff, etc."""
+from __future__ import with_statement
+
__metaclass__ = type
@@ -26,6 +28,7 @@
from lp.code.interfaces.diff import (
IDiff, IPreviewDiff, IStaticDiff, IStaticDiffSource)
from lp.testing import login, login_person, TestCaseWithFactory
+from lp.services.osutils import override_environ
class RecordLister(logging.Handler):
@@ -285,7 +288,10 @@
"""Ensure that acquire returns the existing StaticDiff."""
self.useBzrBranches(direct_database=True)
branch, tree = self.create_branch_and_tree()
- tree.commit('First commit', rev_id='rev1')
+ # XXX: AaronBentley 2010-08-06 bug=614404: a bzr username is
+ # required to generate the revision-id.
+ with override_environ(BZR_EMAIL='me@xxxxxxxxxxx'):
+ tree.commit('First commit', rev_id='rev1')
diff1 = StaticDiff.acquire('null:', 'rev1', tree.branch.repository)
diff2 = StaticDiff.acquire('null:', 'rev1', tree.branch.repository)
self.assertIs(diff1, diff2)
@@ -294,7 +300,10 @@
"""The existing object is used even if the repository is different."""
self.useBzrBranches(direct_database=True)
branch1, tree1 = self.create_branch_and_tree('tree1')
- tree1.commit('First commit', rev_id='rev1')
+ # XXX: AaronBentley 2010-08-06 bug=614404: a bzr username is
+ # required to generate the revision-id.
+ with override_environ(BZR_EMAIL='me@xxxxxxxxxxx'):
+ tree1.commit('First commit', rev_id='rev1')
branch2, tree2 = self.create_branch_and_tree('tree2')
tree2.pull(tree1.branch)
diff1 = StaticDiff.acquire('null:', 'rev1', tree1.branch.repository)
@@ -305,8 +314,11 @@
"""A new object is created if there is no existant matching object."""
self.useBzrBranches(direct_database=True)
branch, tree = self.create_branch_and_tree()
- tree.commit('First commit', rev_id='rev1')
- tree.commit('Next commit', rev_id='rev2')
+ # XXX: AaronBentley 2010-08-06 bug=614404: a bzr username is
+ # required to generate the revision-id.
+ with override_environ(BZR_EMAIL='me@xxxxxxxxxxx'):
+ tree.commit('First commit', rev_id='rev1')
+ tree.commit('Next commit', rev_id='rev2')
diff1 = StaticDiff.acquire('null:', 'rev1', tree.branch.repository)
diff2 = StaticDiff.acquire('rev1', 'rev2', tree.branch.repository)
self.assertIsNot(diff1, diff2)
=== modified file 'lib/lp/code/model/tests/test_linkedbranch.py'
--- lib/lp/code/model/tests/test_linkedbranch.py 2010-07-20 17:50:45 +0000
+++ lib/lp/code/model/tests/test_linkedbranch.py 2010-08-18 11:41:24 +0000
@@ -14,7 +14,7 @@
from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
from canonical.testing.layers import DatabaseFunctionalLayer
from lp.code.interfaces.linkedbranch import (
- CannotHaveLinkedBranch, get_linked_branch, ICanHasLinkedBranch)
+ CannotHaveLinkedBranch, get_linked_to_branch, ICanHasLinkedBranch)
from lp.registry.interfaces.distroseries import NoSuchDistroSeries
from lp.registry.interfaces.pocket import PackagePublishingPocket
from lp.testing import run_with_login, TestCaseWithFactory
@@ -75,6 +75,13 @@
ICanHasLinkedBranch(product).setBranch(branch)
self.assertEqual(branch, product.development_focus.branch)
+ def test_get_linked_to_branch(self):
+ branch = self.factory.makeProductBranch()
+ product = removeSecurityProxy(branch.product)
+ ICanHasLinkedBranch(product).setBranch(branch)
+ got_linkable = get_linked_to_branch(product)
+ self.assertEqual(got_linkable, ICanHasLinkedBranch(product))
+
def test_bzr_path(self):
# The bzr_path of a product linked branch is the product name.
product = self.factory.makeProduct()
@@ -203,7 +210,7 @@
# ProjectGroups cannot have linked branches.
project = self.factory.makeProject()
self.assertRaises(
- CannotHaveLinkedBranch, get_linked_branch, project)
+ CannotHaveLinkedBranch, get_linked_to_branch, project)
class TestLinkedBranchSorting(TestCaseWithFactory):
=== modified file 'lib/lp/code/scripts/tests/test_scan_branches.py'
--- lib/lp/code/scripts/tests/test_scan_branches.py 2010-06-07 09:11:06 +0000
+++ lib/lp/code/scripts/tests/test_scan_branches.py 2010-08-18 11:41:24 +0000
@@ -6,6 +6,8 @@
"""Test the scan_branches script."""
+from __future__ import with_statement
+
from storm.locals import Store
import transaction
@@ -17,6 +19,7 @@
CodeReviewNotificationLevel)
from lp.code.model.branchjob import BranchJob, BranchJobType, BranchScanJob
from lp.services.job.model.job import Job, JobStatus
+from lp.services.osutils import override_environ
class TestScanBranches(TestCaseWithFactory):
@@ -27,9 +30,12 @@
def make_branch_with_commits_and_scan_job(self, db_branch):
"""Create a branch from a db_branch, make commits and a scan job."""
target, target_tree = self.create_branch_and_tree(db_branch=db_branch)
- target_tree.commit('First commit', rev_id='rev1')
- target_tree.commit('Second commit', rev_id='rev2')
- target_tree.commit('Third commit', rev_id='rev3')
+ # XXX: AaronBentley 2010-08-06 bug=614404: a bzr username is
+ # required to generate the revision-id.
+ with override_environ(BZR_EMAIL='me@xxxxxxxxxxx'):
+ target_tree.commit('First commit', rev_id='rev1')
+ target_tree.commit('Second commit', rev_id='rev2')
+ target_tree.commit('Third commit', rev_id='rev3')
BranchScanJob.create(db_branch)
transaction.commit()
=== modified file 'lib/lp/code/scripts/tests/test_sendbranchmail.py'
--- lib/lp/code/scripts/tests/test_sendbranchmail.py 2010-06-07 09:11:06 +0000
+++ lib/lp/code/scripts/tests/test_sendbranchmail.py 2010-08-18 11:41:24 +0000
@@ -5,6 +5,8 @@
"""Test the sendbranchmail script"""
+from __future__ import with_statement
+
import unittest
import transaction
@@ -16,6 +18,7 @@
from lp.code.model.branchjob import (
RevisionMailJob, RevisionsAddedJob)
from lp.testing import TestCaseWithFactory
+from lp.services.osutils import override_environ
class TestSendbranchmail(TestCaseWithFactory):
@@ -33,7 +36,10 @@
transport = tree.bzrdir.root_transport
transport.put_bytes('foo', 'bar')
tree.add('foo')
- tree.commit('Added foo.', rev_id='rev1')
+ # XXX: AaronBentley 2010-08-06 bug=614404: a bzr username is
+ # required to generate the revision-id.
+ with override_environ(BZR_EMAIL='me@xxxxxxxxxxx'):
+ tree.commit('Added foo.', rev_id='rev1')
return branch, tree
def test_sendbranchmail(self):
@@ -73,7 +79,10 @@
self.useBzrBranches()
branch, tree = self.createBranch()
tree.bzrdir.root_transport.put_bytes('foo', 'baz')
- tree.commit('Added foo.', rev_id='rev2')
+ # XXX: AaronBentley 2010-08-06 bug=614404: a bzr username is
+ # required to generate the revision-id.
+ with override_environ(BZR_EMAIL='me@xxxxxxxxxxx'):
+ tree.commit('Added foo.', rev_id='rev2')
RevisionsAddedJob.create(
branch, 'rev1', 'rev2', 'from@xxxxxxxxxxx')
transaction.commit()
=== modified file 'lib/lp/code/stories/branches/xx-product-branches.txt'
--- lib/lp/code/stories/branches/xx-product-branches.txt 2010-06-28 14:00:17 +0000
+++ lib/lp/code/stories/branches/xx-product-branches.txt 2010-08-18 11:41:24 +0000
@@ -66,6 +66,9 @@
There are no branches for NetApplet in Launchpad.
...
There are download files available for NetApplet.
+ >>> browser.getLink('download files')
+ <Link...url='http://launchpad.dev/netapplet/+download'>
+
Development focus branches
=== modified file 'lib/lp/code/stories/branches/xx-upload-directions.txt'
--- lib/lp/code/stories/branches/xx-upload-directions.txt 2009-09-18 13:59:17 +0000
+++ lib/lp/code/stories/branches/xx-upload-directions.txt 2010-08-18 11:41:24 +0000
@@ -81,7 +81,7 @@
First, unregister the existing SSH key for Sample Person.
>>> name12_browser.open('http://launchpad.dev/')
- >>> name12_browser.getLink('Sample Person').click()
+ >>> name12_browser.getLink('name12').click()
>>> name12_browser.getLink(url='editsshkeys').click()
>>> name12_browser.getControl('Remove').click()
=== modified file 'lib/lp/code/templates/product-branch-summary.pt'
--- lib/lp/code/templates/product-branch-summary.pt 2010-06-25 17:40:07 +0000
+++ lib/lp/code/templates/product-branch-summary.pt 2010-08-18 11:41:24 +0000
@@ -60,9 +60,10 @@
</div>
</tal:has-branches>
-
<p tal:condition="view/latest_release_with_download_files">
- <img src="/@@/download"/> There are <a href="+download">download files</a>
+ <img src="/@@/download"/> There are
+ <a tal:define="rooturl modules/canonical.launchpad.webapp.vhosts/allvhosts/configs/mainsite/rooturl"
+ tal:attributes="href string:${rooturl}${context/name}/+download">download files</a>
available for <tal:project-name replace="context/displayname"/>.
</p>
=== modified file 'lib/lp/code/xmlrpc/codehosting.py'
--- lib/lp/code/xmlrpc/codehosting.py 2010-08-03 03:56:32 +0000
+++ lib/lp/code/xmlrpc/codehosting.py 2010-08-18 11:41:24 +0000
@@ -11,6 +11,7 @@
import datetime
+import transaction
import pytz
@@ -33,14 +34,18 @@
from lp.code.bzr import BranchFormat, ControlFormat, RepositoryFormat
from lp.code.enums import BranchType
from lp.code.errors import (
- BranchCreationException, InvalidNamespace, UnknownBranchTypeError)
-from lp.code.interfaces.branchlookup import IBranchLookup
+ BranchCreationException, InvalidNamespace, NoLinkedBranch,
+ UnknownBranchTypeError)
+from lp.code.interfaces import branchpuller
+from lp.code.interfaces.branchlookup import (
+ IBranchLookup, ILinkedBranchTraverser)
from lp.code.interfaces.branchnamespace import (
lookup_branch_namespace, split_unique_name)
-from lp.code.interfaces import branchpuller
+from lp.code.interfaces.branchtarget import IBranchTarget
from lp.code.interfaces.codehosting import (
- BRANCH_TRANSPORT, CONTROL_TRANSPORT, ICodehostingAPI, LAUNCHPAD_ANONYMOUS,
- LAUNCHPAD_SERVICES)
+ BRANCH_ALIAS_PREFIX, BRANCH_TRANSPORT, CONTROL_TRANSPORT, ICodehostingAPI,
+ LAUNCHPAD_ANONYMOUS, LAUNCHPAD_SERVICES)
+from lp.code.interfaces.linkedbranch import ICanHasLinkedBranch
from lp.registry.interfaces.person import IPersonSet, NoSuchPerson
from lp.registry.interfaces.product import NoSuchProduct
from lp.services.scripts.interfaces.scriptactivity import IScriptActivitySet
@@ -90,7 +95,6 @@
endInteraction()
-
class CodehostingAPI(LaunchpadXMLRPCView):
"""See `ICodehostingAPI`."""
@@ -141,6 +145,35 @@
date_completed=date_completed, hostname=hostname)
return True
+ def _getBranchNamespaceExtras(self, path, requester):
+ """Get the branch namespace, branch name and callback for the path.
+
+ If the path defines a full branch path including the owner and branch
+ name, then the namespace that is returned is the namespace for the
+ owner and the branch target specified.
+
+ If the path uses an lp short name, then we only allow the requester to
+ create a branch if they have permission to link the newly created
+ branch to the short name target. If there is an existing branch
+ already linked, then BranchExists is raised. The branch name that is
+ used is determined by the namespace as the first unused name starting
+ with 'trunk'.
+ """
+ if path.startswith(BRANCH_ALIAS_PREFIX + '/'):
+ path = path[len(BRANCH_ALIAS_PREFIX) + 1:]
+ if not path.startswith('~'):
+ context = getUtility(ILinkedBranchTraverser).traverse(path)
+ target = IBranchTarget(context)
+ namespace = target.getNamespace(requester)
+ branch_name = namespace.findUnusedName('trunk')
+ def link_func(new_branch):
+ link = ICanHasLinkedBranch(context)
+ link.setBranch(new_branch, requester)
+ return namespace, branch_name, link_func, path
+ namespace_name, branch_name = split_unique_name(path)
+ namespace = lookup_branch_namespace(namespace_name)
+ return namespace, branch_name, None, path
+
def createBranch(self, login_id, branch_path):
"""See `ICodehostingAPI`."""
def create_branch(requester):
@@ -148,12 +181,11 @@
return faults.InvalidPath(branch_path)
escaped_path = unescape(branch_path.strip('/'))
try:
- namespace_name, branch_name = split_unique_name(escaped_path)
+ namespace, branch_name, link_func, path = (
+ self._getBranchNamespaceExtras(escaped_path, requester))
except ValueError:
return faults.PermissionDenied(
"Cannot create branch at '%s'" % branch_path)
- try:
- namespace = lookup_branch_namespace(namespace_name)
except InvalidNamespace:
return faults.PermissionDenied(
"Cannot create branch at '%s'" % branch_path)
@@ -175,8 +207,17 @@
return faults.PermissionDenied(msg)
except BranchCreationException, e:
return faults.PermissionDenied(str(e))
- else:
- return branch.id
+
+ if link_func:
+ try:
+ link_func(branch)
+ except Unauthorized:
+ # We don't want to keep the branch we created.
+ transaction.abort()
+ return faults.PermissionDenied(
+ "Cannot create linked branch at '%s'." % path)
+
+ return branch.id
return run_with_login(login_id, create_branch)
def _canWriteToBranch(self, requester, branch):
@@ -266,7 +307,26 @@
for first, second in iter_split(stripped_path, '/'):
first = unescape(first)
# Is it a branch?
- branch = getUtility(IBranchLookup).getByUniqueName(first)
+ if first.startswith(BRANCH_ALIAS_PREFIX + '/'):
+ try:
+ # translatePath('/+branch/.bzr') *must* return not
+ # found, otherwise bzr will look for it and we don't
+ # have a global bzr dir.
+ lp_path = first[len(BRANCH_ALIAS_PREFIX + '/'):]
+ if lp_path == '.bzr' or lp_path.startswith('.bzr/'):
+ raise faults.PathTranslationError(path)
+ branch, trailing = getUtility(
+ IBranchLookup).getByLPPath(lp_path)
+ except (NameLookupFailed, InvalidNamespace, NoLinkedBranch):
+ # The reason we're doing it is that getByLPPath thinks
+ # that 'foo/.bzr' is a request for the '.bzr' series
+ # of a product.
+ continue
+ if trailing is None:
+ trailing = ''
+ second = '/'.join([trailing, second]).strip('/')
+ else:
+ branch = getUtility(IBranchLookup).getByUniqueName(first)
if branch is not None:
branch = self._serializeBranch(requester, branch, second)
if branch is None:
@@ -279,4 +339,3 @@
return product
raise faults.PathTranslationError(path)
return run_with_login(requester_id, translate_path)
-
=== modified file 'lib/lp/code/xmlrpc/tests/test_codehosting.py'
--- lib/lp/code/xmlrpc/tests/test_codehosting.py 2010-08-05 01:00:49 +0000
+++ lib/lp/code/xmlrpc/tests/test_codehosting.py 2010-08-18 11:41:24 +0000
@@ -6,6 +6,7 @@
__metaclass__ = type
import datetime
+import os
import pytz
import unittest
@@ -20,7 +21,8 @@
from canonical.launchpad.ftests import ANONYMOUS, login, logout
from lp.services.scripts.interfaces.scriptactivity import (
IScriptActivitySet)
-from lp.code.interfaces.codehosting import BRANCH_TRANSPORT, CONTROL_TRANSPORT
+from lp.code.interfaces.codehosting import (
+ BRANCH_ALIAS_PREFIX, BRANCH_TRANSPORT, CONTROL_TRANSPORT)
from canonical.launchpad.interfaces.launchpad import ILaunchBag
from lp.testing import TestCaseWithFactory
from lp.testing.factory import LaunchpadObjectFactory
@@ -34,6 +36,7 @@
from lp.code.interfaces.branch import BRANCH_NAME_VALIDATION_ERROR_MESSAGE
from lp.code.interfaces.branchlookup import IBranchLookup
from lp.code.interfaces.branchtarget import IBranchTarget
+from lp.code.interfaces.linkedbranch import ICanHasLinkedBranch
from lp.code.model.tests.test_branchpuller import AcquireBranchToPullTests
from lp.code.xmlrpc.codehosting import (
CodehostingAPI, LAUNCHPAD_ANONYMOUS, LAUNCHPAD_SERVICES, run_with_login)
@@ -410,6 +413,120 @@
message = "No such source package: 'ningnangnong'."
self.assertEqual(faults.NotFound(message), fault)
+ def test_createBranch_using_branch_alias(self):
+ # Branches can be created using the branch alias and the full unique
+ # name of the branch.
+ owner = self.factory.makePerson()
+ product = self.factory.makeProduct()
+ branch_name = self.factory.getUniqueString('branch-name')
+ unique_name = u'~%s/%s/%s' % (owner.name, product.name, branch_name)
+ path = u'/%s/%s' % (BRANCH_ALIAS_PREFIX, unique_name)
+ branch_id = self.codehosting_api.createBranch(owner.id, escape(path))
+ login(ANONYMOUS)
+ branch = self.branch_lookup.get(branch_id)
+ self.assertEqual(unique_name, branch.unique_name)
+
+ def test_createBranch_using_branch_alias_then_lookup(self):
+ # A branch newly created using createBranch is immediately traversable
+ # using translatePath.
+ owner = self.factory.makePerson()
+ product = self.factory.makeProduct()
+ branch_name = self.factory.getUniqueString('branch-name')
+ unique_name = u'~%s/%s/%s' % (owner.name, product.name, branch_name)
+ path = escape(u'/%s/%s' % (BRANCH_ALIAS_PREFIX, unique_name))
+ branch_id = self.codehosting_api.createBranch(owner.id, path)
+ login(ANONYMOUS)
+ translation = self.codehosting_api.translatePath(owner.id, path)
+ self.assertEqual(
+ (BRANCH_TRANSPORT, {'id': branch_id, 'writable': True}, ''),
+ translation)
+
+ def test_createBranch_using_branch_alias_product(self):
+ # If the person creating the branch has permission to link the new
+ # branch to the alias, then they are able to create a branch and link
+ # it.
+ owner = self.factory.makePerson()
+ product = self.factory.makeProduct(owner=owner)
+ path = u'/%s/%s' % (BRANCH_ALIAS_PREFIX, product.name)
+ branch_id = self.codehosting_api.createBranch(owner.id, escape(path))
+ login(ANONYMOUS)
+ branch = self.branch_lookup.get(branch_id)
+ self.assertEqual(owner, branch.owner)
+ self.assertEqual('trunk', branch.name)
+ self.assertEqual(product, branch.product)
+ self.assertEqual(ICanHasLinkedBranch(product).branch, branch)
+
+ def test_createBranch_using_branch_alias_product_then_lookup(self):
+ # A branch newly created using createBranch using a product alias is
+ # immediately traversable using translatePath.
+ product = self.factory.makeProduct()
+ owner = product.owner
+ path = escape(u'/%s/%s' % (BRANCH_ALIAS_PREFIX, product.name))
+ branch_id = self.codehosting_api.createBranch(owner.id, path)
+ login(ANONYMOUS)
+ translation = self.codehosting_api.translatePath(owner.id, path)
+ self.assertEqual(
+ (BRANCH_TRANSPORT, {'id': branch_id, 'writable': True}, ''),
+ translation)
+
+ def test_createBranch_using_branch_alias_product_not_auth(self):
+ # If the person creating the branch does not have permission to link
+ # the new branch to the alias, then can't create the branch.
+ owner = self.factory.makePerson(name='eric')
+ product = self.factory.makeProduct('wibble')
+ path = u'/%s/%s' % (BRANCH_ALIAS_PREFIX, product.name)
+ fault = self.codehosting_api.createBranch(owner.id, escape(path))
+ message = "Cannot create linked branch at 'wibble'."
+ self.assertEqual(faults.PermissionDenied(message), fault)
+ # Make sure that the branch doesn't exist.
+ login(ANONYMOUS)
+ branch = self.branch_lookup.getByUniqueName('~eric/wibble/trunk')
+ self.assertIs(None, branch)
+
+ def test_createBranch_using_branch_alias_product_not_exist(self):
+ # If the product doesn't exist, we don't (yet) create one.
+ owner = self.factory.makePerson()
+ path = u'/%s/foible' % (BRANCH_ALIAS_PREFIX,)
+ fault = self.codehosting_api.createBranch(owner.id, escape(path))
+ message = "Project 'foible' does not exist."
+ self.assertEqual(faults.NotFound(message), fault)
+
+ def test_createBranch_using_branch_alias_productseries(self):
+ # If the person creating the branch has permission to link the new
+ # branch to the alias, then they are able to create a branch and link
+ # it.
+ owner = self.factory.makePerson()
+ product = self.factory.makeProduct(owner=owner)
+ series = self.factory.makeProductSeries(product=product)
+ path = u'/%s/%s/%s' % (BRANCH_ALIAS_PREFIX, product.name, series.name)
+ branch_id = self.codehosting_api.createBranch(owner.id, escape(path))
+ login(ANONYMOUS)
+ branch = self.branch_lookup.get(branch_id)
+ self.assertEqual(owner, branch.owner)
+ self.assertEqual('trunk', branch.name)
+ self.assertEqual(product, branch.product)
+ self.assertEqual(ICanHasLinkedBranch(series).branch, branch)
+
+ def test_createBranch_using_branch_alias_productseries_not_auth(self):
+ # If the person creating the branch does not have permission to link
+ # the new branch to the alias, then can't create the branch.
+ owner = self.factory.makePerson()
+ product = self.factory.makeProduct(name='wibble')
+ self.factory.makeProductSeries(product=product, name='nip')
+ path = u'/%s/wibble/nip' % (BRANCH_ALIAS_PREFIX,)
+ fault = self.codehosting_api.createBranch(owner.id, escape(path))
+ message = "Cannot create linked branch at 'wibble/nip'."
+ self.assertEqual(faults.PermissionDenied(message), fault)
+
+ def test_createBranch_using_branch_alias_productseries_not_exist(self):
+ # If the product series doesn't exist, we don't (yet) create it.
+ owner = self.factory.makePerson()
+ self.factory.makeProduct(name='wibble')
+ path = u'/%s/wibble/nip' % (BRANCH_ALIAS_PREFIX,)
+ fault = self.codehosting_api.createBranch(owner.id, escape(path))
+ message = "No such product series: 'nip'."
+ self.assertEqual(faults.NotFound(message), fault)
+
def test_requestMirror(self):
# requestMirror should set the next_mirror_time field to be the
# current time.
@@ -742,6 +859,109 @@
(BRANCH_TRANSPORT, {'id': branch.id, 'writable': False}, ''),
translation)
+ def test_translatePath_branch_alias_short_name(self):
+ # translatePath translates the short name of a branch if it's prefixed
+ # by +branch.
+ requester = self.factory.makePerson()
+ branch = self.factory.makeProductBranch()
+ removeSecurityProxy(branch.product.development_focus).branch = branch
+ short_name = ICanHasLinkedBranch(branch.product).bzr_path
+ path_in_branch = '.bzr/branch-format'
+ path = escape(u'/%s' % os.path.join(
+ BRANCH_ALIAS_PREFIX, short_name, path_in_branch))
+ translation = self.codehosting_api.translatePath(requester.id, path)
+ login(ANONYMOUS)
+ self.assertEqual(
+ (BRANCH_TRANSPORT, {'id': branch.id, 'writable': False},
+ path_in_branch), translation)
+
+ def test_translatePath_branch_alias_unique_name(self):
+ # translatePath translates +branch paths that are followed by the
+ # unique name as if they didn't have the prefix at all.
+ requester = self.factory.makePerson()
+ branch = self.factory.makeBranch()
+ path_in_branch = '.bzr/branch-format'
+ path = escape(u'/%s' % os.path.join(
+ BRANCH_ALIAS_PREFIX, branch.unique_name, path_in_branch))
+ translation = self.codehosting_api.translatePath(requester.id, path)
+ login(ANONYMOUS)
+ self.assertEqual(
+ (BRANCH_TRANSPORT, {'id': branch.id, 'writable': False},
+ path_in_branch), translation)
+
+ def test_translatePath_branch_alias_no_such_branch(self):
+ # translatePath returns a not found when there's no such branch, given
+ # a unique name after +branch.
+ requester = self.factory.makePerson()
+ product = self.factory.makeProduct()
+ path = '/%s/~%s/%s/doesntexist' % (
+ BRANCH_ALIAS_PREFIX, requester.name, product.name)
+ self.assertNotFound(requester, path)
+
+ def test_translatePath_branch_alias_no_such_person(self):
+ # translatePath returns a not found when there's no such person, given
+ # a unique name after +branch.
+ requester = self.factory.makePerson()
+ path = '/%s/~doesntexist/dontcare/noreally' % (BRANCH_ALIAS_PREFIX,)
+ self.assertNotFound(requester, path)
+
+ def test_translatePath_branch_alias_no_such_product(self):
+ # translatePath returns a not found when there's no such product,
+ # given a unique name after +branch.
+ requester = self.factory.makePerson()
+ path = '/%s/~%s/doesntexist/branchname' % (
+ BRANCH_ALIAS_PREFIX, requester.name)
+ self.assertNotFound(requester, path)
+
+ def test_translatePath_branch_alias_no_such_distro(self):
+ # translatePath returns a not found when there's no such distro, given
+ # a unique name after +branch.
+ requester = self.factory.makePerson()
+ path = '/%s/~%s/doesntexist/lucid/openssh/branchname' % (
+ BRANCH_ALIAS_PREFIX, requester.name)
+ self.assertNotFound(requester, path)
+
+ def test_translatePath_branch_alias_no_such_distroseries(self):
+ # translatePath returns a not found when there's no such distroseries,
+ # given a unique name after +branch.
+ requester = self.factory.makePerson()
+ distro = self.factory.makeDistribution()
+ path = '/%s/~%s/%s/doesntexist/openssh/branchname' % (
+ BRANCH_ALIAS_PREFIX, requester.name, distro.name)
+ self.assertNotFound(requester, path)
+
+ def test_translatePath_branch_alias_no_such_sourcepackagename(self):
+ # translatePath returns a not found when there's no such
+ # sourcepackagename, given a unique name after +branch.
+ requester = self.factory.makePerson()
+ distroseries = self.factory.makeDistroSeries()
+ distro = distroseries.distribution
+ path = '/%s/~%s/%s/%s/doesntexist/branchname' % (
+ BRANCH_ALIAS_PREFIX, requester.name, distro.name,
+ distroseries.name)
+ self.assertNotFound(requester, path)
+
+ def test_translatePath_branch_alias_product_with_no_branch(self):
+ # translatePath returns a not found when we look up a product that has
+ # no linked branch.
+ requester = self.factory.makePerson()
+ product = self.factory.makeProduct()
+ path = '/%s/%s' % (BRANCH_ALIAS_PREFIX, product.name)
+ self.assertNotFound(requester, path)
+
+ def test_translatePath_branch_alias_bzrdir_content(self):
+ # translatePath('/+branch/.bzr/.*') *must* return not found, otherwise
+ # bzr will look for it and we don't have a global bzr dir.
+ requester = self.factory.makePerson()
+ self.assertNotFound(
+ requester, '/%s/.bzr/branch-format' % BRANCH_ALIAS_PREFIX)
+
+ def test_translatePath_branch_alias_bzrdir(self):
+ # translatePath('/+branch/.bzr') *must* return not found, otherwise
+ # bzr will look for it and we don't have a global bzr dir.
+ requester = self.factory.makePerson()
+ self.assertNotFound(requester, '/%s/.bzr' % BRANCH_ALIAS_PREFIX)
+
def assertTranslationIsControlDirectory(self, translation,
default_stacked_on,
trailing_path):
=== modified file 'lib/lp/codehosting/codeimport/tests/test_worker.py'
--- lib/lp/codehosting/codeimport/tests/test_worker.py 2010-08-02 23:01:15 +0000
+++ lib/lp/codehosting/codeimport/tests/test_worker.py 2010-08-18 11:41:24 +0000
@@ -952,7 +952,7 @@
t = get_transport(self.get_url('.'))
t.mkdir('reference')
a_bzrdir = BzrDir.create(self.get_url('reference'))
- BranchReferenceFormat().initialize(a_bzrdir, branch)
+ BranchReferenceFormat().initialize(a_bzrdir, target_branch=branch)
return a_bzrdir.root_transport.base
def test_reject_branch_reference(self):
=== modified file 'lib/lp/codehosting/codeimport/uifactory.py'
--- lib/lp/codehosting/codeimport/uifactory.py 2009-08-24 16:27:33 +0000
+++ lib/lp/codehosting/codeimport/uifactory.py 2010-08-18 11:41:24 +0000
@@ -75,6 +75,24 @@
# There's no point showing a progress bar in a flat log.
return ''
+ def _render_line(self):
+ bar_string = self._render_bar()
+ if self._last_task:
+ task_part, counter_part = self._format_task(self._last_task)
+ else:
+ task_part = counter_part = ''
+ if self._last_task and not self._last_task.show_transport_activity:
+ trans = ''
+ else:
+ trans = self._last_transport_msg
+ # the bar separates the transport activity from the message, so even
+ # if there's no bar or spinner, we must show something if both those
+ # fields are present
+ if (task_part and trans) and not bar_string:
+ bar_string = ' | '
+ s = trans + bar_string + task_part + counter_part
+ return s
+
def _format_transport_msg(self, scheme, dir_char, rate):
# We just report the amount of data transferred.
return '%s bytes transferred' % self._bytes_since_update
=== modified file 'lib/lp/codehosting/inmemory.py'
--- lib/lp/codehosting/inmemory.py 2010-04-26 00:23:45 +0000
+++ lib/lp/codehosting/inmemory.py 2010-08-18 11:41:24 +0000
@@ -30,11 +30,13 @@
from lp.code.interfaces.branch import IBranch
from lp.code.interfaces.branchtarget import IBranchTarget
from lp.code.interfaces.codehosting import (
- BRANCH_TRANSPORT, CONTROL_TRANSPORT, LAUNCHPAD_ANONYMOUS,
- LAUNCHPAD_SERVICES)
+ BRANCH_ALIAS_PREFIX, BRANCH_TRANSPORT, CONTROL_TRANSPORT,
+ LAUNCHPAD_ANONYMOUS, LAUNCHPAD_SERVICES)
+from lp.code.interfaces.linkedbranch import ICanHasLinkedBranch
from lp.code.xmlrpc.codehosting import datetime_from_tuple
from lp.registry.interfaces.pocket import PackagePublishingPocket
from lp.services.utils import iter_split
+from lp.services.xmlrpc import LaunchpadFault
from lp.testing.factory import ObjectFactory
@@ -120,6 +122,13 @@
return self._find(name=name)
+class BranchSet(ObjectSet):
+ """Extra find methods for branches."""
+
+ def getByUniqueName(self, unique_name):
+ return self._find(unique_name=unique_name)
+
+
class FakeSourcePackage:
"""Fake ISourcePackage."""
@@ -265,9 +274,17 @@
class FakeProduct(FakeDatabaseObject):
"""Fake product."""
- def __init__(self, name):
+ def __init__(self, name, owner):
self.name = name
- self.development_focus = FakeProductSeries()
+ self.owner = owner
+ self.bzr_path = name
+ self.development_focus = FakeProductSeries(self, 'trunk')
+ self.series = {
+ 'trunk': self.development_focus,
+ }
+
+ def getSeries(self, name):
+ return self.series.get(name, None)
@adapter(FakeProduct)
@@ -277,11 +294,36 @@
return ProductBranchTarget(fake_product)
+@adapter(FakeProduct)
+@implementer(ICanHasLinkedBranch)
+def fake_product_to_can_has_linked_branch(fake_product):
+ """Adapt a `FakeProduct` to `ICanHasLinkedBranch`."""
+ return fake_product.development_focus
+
+
class FakeProductSeries(FakeDatabaseObject):
"""Fake product series."""
branch = None
+ def __init__(self, product, name):
+ self.product = product
+ self.name = name
+
+ @property
+ def bzr_path(self):
+ if self.product.development_focus is self:
+ return self.product.name
+ else:
+ return "%s/%s" % (self.product.name, self.name)
+
+
+@adapter(FakeProductSeries)
+@implementer(ICanHasLinkedBranch)
+def fake_productseries_to_can_has_linked_branch(fake_productseries):
+ """Adapt a `FakeProductSeries` to `ICanHasLinkedBranch`."""
+ return fake_productseries
+
class FakeScriptActivity(FakeDatabaseObject):
"""Fake script activity."""
@@ -394,6 +436,8 @@
self._distroseries_set._add(distroseries)
return distroseries
+ makeDistroSeries = makeDistroRelease
+
def makeSourcePackageName(self):
sourcepackagename = FakeSourcePackageName(self.getUniqueString())
self._sourcepackagename_set._add(sourcepackagename)
@@ -413,16 +457,29 @@
self._person_set._add(team)
return team
- def makePerson(self):
- person = FakePerson(name=self.getUniqueString())
+ def makePerson(self, name=None):
+ if name is None:
+ name = self.getUniqueString()
+ person = FakePerson(name=name)
self._person_set._add(person)
return person
- def makeProduct(self):
- product = FakeProduct(self.getUniqueString())
+ def makeProduct(self, name=None, owner=None):
+ if name is None:
+ name = self.getUniqueString()
+ if owner is None:
+ owner = self.makePerson()
+ product = FakeProduct(name, owner)
self._product_set._add(product)
return product
+ def makeProductSeries(self, product, name=None):
+ if name is None:
+ name = self.getUniqueString()
+ series = FakeProductSeries(product, name)
+ product.series[name] = series
+ return series
+
def enableDefaultStackingForProduct(self, product, branch=None):
"""Give 'product' a default stacked-on branch.
@@ -509,28 +566,60 @@
FakeScriptActivity(name, hostname, date_started, date_completed))
return True
- def createBranch(self, requester_id, branch_path):
- if not branch_path.startswith('/'):
- return faults.InvalidPath(branch_path)
- escaped_path = unescape(branch_path.strip('/'))
+ def _parseUniqueName(self, branch_path):
+ """Return a dict of the parsed information and the branch name."""
try:
- namespace_path, branch_name = escaped_path.rsplit('/', 1)
+ namespace_path, branch_name = branch_path.rsplit('/', 1)
except ValueError:
- return faults.PermissionDenied(
- "Cannot create branch at '%s'" % branch_path)
+ raise faults.PermissionDenied(
+ "Cannot create branch at '/%s'" % branch_path)
data = BranchNamespaceSet().parse(namespace_path)
+ return data, branch_name
+
+ def _createBranch(self, registrant, branch_path):
+ """The guts of the create branch method.
+
+ Raises exceptions on error conditions.
+ """
+ to_link = None
+ if branch_path.startswith(BRANCH_ALIAS_PREFIX + '/'):
+ branch_path = branch_path[len(BRANCH_ALIAS_PREFIX) + 1:]
+ if branch_path.startswith('~'):
+ data, branch_name = self._parseUniqueName(branch_path)
+ else:
+ tokens = branch_path.split('/')
+ data = {
+ 'person': registrant.name,
+ 'product': tokens[0],
+ }
+ branch_name = 'trunk'
+ # check the series
+ product = self._product_set.getByName(data['product'])
+ if product is not None:
+ if len(tokens) > 1:
+ series = product.getSeries(tokens[1])
+ if series is None:
+ raise faults.NotFound(
+ "No such product series: '%s'." % tokens[1])
+ else:
+ to_link = ICanHasLinkedBranch(series)
+ else:
+ to_link = ICanHasLinkedBranch(product)
+ # don't forget the link.
+ else:
+ data, branch_name = self._parseUniqueName(branch_path)
+
owner = self._person_set.getByName(data['person'])
if owner is None:
- return faults.NotFound(
+ raise faults.NotFound(
"User/team '%s' does not exist." % (data['person'],))
- registrant = self._person_set.get(requester_id)
# The real code consults the branch creation policy of the product. We
# don't need to do so here, since the tests above this layer never
# encounter that behaviour. If they *do* change to rely on the branch
# creation policy, the observed behaviour will be failure to raise
# exceptions.
if not registrant.inTeam(owner):
- return faults.PermissionDenied(
+ raise faults.PermissionDenied(
('%s cannot create branches owned by %s'
% (registrant.displayname, owner.displayname)))
product = sourcepackage = None
@@ -539,35 +628,52 @@
elif data['product'] is not None:
product = self._product_set.getByName(data['product'])
if product is None:
- return faults.NotFound(
+ raise faults.NotFound(
"Project '%s' does not exist." % (data['product'],))
elif data['distribution'] is not None:
distro = self._distribution_set.getByName(data['distribution'])
if distro is None:
- return faults.NotFound(
+ raise faults.NotFound(
"No such distribution: '%s'." % (data['distribution'],))
distroseries = self._distroseries_set.getByName(
data['distroseries'])
if distroseries is None:
- return faults.NotFound(
+ raise faults.NotFound(
"No such distribution series: '%s'."
% (data['distroseries'],))
sourcepackagename = self._sourcepackagename_set.getByName(
data['sourcepackagename'])
if sourcepackagename is None:
- return faults.NotFound(
+ raise faults.NotFound(
"No such source package: '%s'."
% (data['sourcepackagename'],))
sourcepackage = self._factory.makeSourcePackage(
distroseries, sourcepackagename)
else:
- return faults.PermissionDenied(
+ raise faults.PermissionDenied(
"Cannot create branch at '%s'" % branch_path)
+ branch = self._factory.makeBranch(
+ owner=owner, name=branch_name, product=product,
+ sourcepackage=sourcepackage, registrant=registrant,
+ branch_type=BranchType.HOSTED)
+ if to_link is not None:
+ if registrant.inTeam(to_link.product.owner):
+ to_link.branch = branch
+ else:
+ self._branch_set._delete(branch)
+ raise faults.PermissionDenied(
+ "Cannot create linked branch at '%s'." % branch_path)
+ return branch.id
+
+ def createBranch(self, requester_id, branch_path):
+ if not branch_path.startswith('/'):
+ return faults.InvalidPath(branch_path)
+ escaped_path = unescape(branch_path.strip('/'))
+ registrant = self._person_set.get(requester_id)
try:
- return self._factory.makeBranch(
- owner=owner, name=branch_name, product=product,
- sourcepackage=sourcepackage, registrant=registrant,
- branch_type=BranchType.HOSTED).id
+ return self._createBranch(registrant, escaped_path)
+ except LaunchpadFault, e:
+ return e
except LaunchpadValidationError, e:
msg = e.args[0]
if isinstance(msg, unicode):
@@ -704,7 +810,15 @@
for first, second in iter_split(stripped_path, '/'):
first = unescape(first).encode('utf-8')
# Is it a branch?
- branch = self._branch_set._find(unique_name=first)
+ if first.startswith('+branch/'):
+ component_name = first[len('+branch/'):]
+ product = self._product_set.getByName(component_name)
+ if product:
+ branch = product.development_focus.branch
+ else:
+ branch = self._branch_set._find(unique_name=component_name)
+ else:
+ branch = self._branch_set._find(unique_name=first)
if branch is not None:
branch = self._serializeBranch(requester_id, branch, second)
if isinstance(branch, Fault):
@@ -728,7 +842,7 @@
"""
def __init__(self):
- self._branch_set = ObjectSet()
+ self._branch_set = BranchSet()
self._script_activity_set = ObjectSet()
self._person_set = ObjectSet()
self._product_set = ObjectSet()
@@ -745,8 +859,10 @@
self._sourcepackagename_set, self._factory,
self._script_activity_set)
sm = getSiteManager()
+ sm.registerAdapter(fake_product_to_can_has_linked_branch)
sm.registerAdapter(fake_product_to_branch_target)
sm.registerAdapter(fake_source_package_to_branch_target)
+ sm.registerAdapter(fake_productseries_to_can_has_linked_branch)
def getCodehostingEndpoint(self):
"""See `LaunchpadDatabaseFrontend`.
=== modified file 'lib/lp/codehosting/puller/worker.py'
--- lib/lp/codehosting/puller/worker.py 2010-04-21 01:56:51 +0000
+++ lib/lp/codehosting/puller/worker.py 2010-08-18 11:41:24 +0000
@@ -503,7 +503,7 @@
def get_boolean(self, prompt):
"""If we're asked to break a lock like a stale lock of ours, say yes.
"""
- assert prompt.startswith('Break lock'), (
+ assert prompt.startswith('Break '), (
"Didn't expect prompt %r" % (prompt,))
branch_id = self.puller_worker_protocol.branch_id
if get_lock_id_for_branch_id(branch_id) in prompt:
=== modified file 'lib/lp/codehosting/scanner/tests/test_buglinks.py'
--- lib/lp/codehosting/scanner/tests/test_buglinks.py 2010-08-02 02:13:52 +0000
+++ lib/lp/codehosting/scanner/tests/test_buglinks.py 2010-08-18 11:41:24 +0000
@@ -3,6 +3,8 @@
"""Tests for creating BugBranch items based on Bazaar revisions."""
+from __future__ import with_statement
+
__metaclass__ = type
import unittest
@@ -22,6 +24,7 @@
from lp.codehosting.scanner.tests.test_bzrsync import BzrSyncTestCase
from lp.registry.interfaces.pocket import PackagePublishingPocket
from lp.testing import TestCase, TestCaseWithFactory
+from lp.services.osutils import override_environ
class RevisionPropertyParsing(TestCase):
@@ -189,28 +192,31 @@
"""Don't add BugBranches based on non-mainline revisions."""
# Make the base revision.
author = self.factory.getUniqueString()
- self.bzr_tree.commit(
- u'common parent', committer=author, rev_id='r1',
- allow_pointless=True)
-
- # Branch from the base revision.
- new_tree = self.make_branch_and_tree('bzr_branch_merged')
- new_tree.pull(self.bzr_branch)
-
- # Commit to both branches
- self.bzr_tree.commit(
- u'commit one', committer=author, rev_id='r2',
- allow_pointless=True)
- new_tree.commit(
- u'commit two', committer=author, rev_id='r1.1.1',
- allow_pointless=True,
- revprops={'bugs': '%s fixed' % self.getBugURL(self.bug1)})
-
- # Merge and commit.
- self.bzr_tree.merge_from_branch(new_tree.branch)
- self.bzr_tree.commit(
- u'merge', committer=author, rev_id='r3',
- allow_pointless=True)
+ # XXX: AaronBentley 2010-08-06 bug=614404: a bzr username is
+ # required to generate the revision-id.
+ with override_environ(BZR_EMAIL='me@xxxxxxxxxxx'):
+ self.bzr_tree.commit(
+ u'common parent', committer=author, rev_id='r1',
+ allow_pointless=True)
+
+ # Branch from the base revision.
+ new_tree = self.make_branch_and_tree('bzr_branch_merged')
+ new_tree.pull(self.bzr_branch)
+
+ # Commit to both branches
+ self.bzr_tree.commit(
+ u'commit one', committer=author, rev_id='r2',
+ allow_pointless=True)
+ new_tree.commit(
+ u'commit two', committer=author, rev_id='r1.1.1',
+ allow_pointless=True,
+ revprops={'bugs': '%s fixed' % self.getBugURL(self.bug1)})
+
+ # Merge and commit.
+ self.bzr_tree.merge_from_branch(new_tree.branch)
+ self.bzr_tree.commit(
+ u'merge', committer=author, rev_id='r3',
+ allow_pointless=True)
self.syncBazaarBranchToDatabase(self.bzr_branch, self.db_branch)
self.assertEqual(
@@ -251,8 +257,12 @@
bug = self.factory.makeBug()
self.layer.txn.commit()
LaunchpadZopelessLayer.switchDbUser(config.branchscanner.dbuser)
- revision_id = tree.commit('fix revision',
- revprops={'bugs': 'https://launchpad.net/bugs/%d fixed' % bug.id})
+ # XXX: AaronBentley 2010-08-06 bug=614404: a bzr username is
+ # required to generate the revision-id.
+ with override_environ(BZR_EMAIL='me@xxxxxxxxxxx'):
+ revision_id = tree.commit('fix revision',
+ revprops={
+ 'bugs': 'https://launchpad.net/bugs/%d fixed' % bug.id})
bzr_revision = tree.branch.repository.get_revision(revision_id)
revno = 1
revision_set = getUtility(IRevisionSet)
=== modified file 'lib/lp/codehosting/scanner/tests/test_bzrsync.py'
--- lib/lp/codehosting/scanner/tests/test_bzrsync.py 2010-07-14 14:48:46 +0000
+++ lib/lp/codehosting/scanner/tests/test_bzrsync.py 2010-08-18 11:41:24 +0000
@@ -5,6 +5,8 @@
# pylint: disable-msg=W0141
+from __future__ import with_statement
+
import datetime
import os
import random
@@ -32,6 +34,7 @@
from lp.codehosting.scanner.bzrsync import BzrSync
from lp.testing import TestCaseWithFactory
from canonical.testing import LaunchpadZopelessLayer
+from lp.services.osutils import override_environ
def run_as_db_user(username):
@@ -160,10 +163,13 @@
committer = self.factory.getUniqueString()
if extra_parents is not None:
self.bzr_tree.add_pending_merge(*extra_parents)
- return self.bzr_tree.commit(
- message, committer=committer, rev_id=rev_id,
- timestamp=timestamp, timezone=timezone, allow_pointless=True,
- revprops=revprops)
+ # XXX: AaronBentley 2010-08-06 bug=614404: a bzr username is
+ # required to generate the revision-id.
+ with override_environ(BZR_EMAIL='me@xxxxxxxxxxx'):
+ return self.bzr_tree.commit(
+ message, committer=committer, rev_id=rev_id,
+ timestamp=timestamp, timezone=timezone, allow_pointless=True,
+ revprops=revprops)
def uncommitRevision(self):
branch = self.bzr_tree.branch
@@ -207,21 +213,24 @@
db_branch = self.makeDatabaseBranch()
db_branch, trunk_tree = self.create_branch_and_tree(
db_branch=db_branch)
- trunk_tree.commit(u'base revision', rev_id=base_rev_id)
-
- # Branch from the base revision.
- new_db_branch = self.makeDatabaseBranch(product=db_branch.product)
- new_db_branch, branch_tree = self.create_branch_and_tree(
- db_branch=new_db_branch)
- branch_tree.pull(trunk_tree.branch)
-
- # Commit to both branches.
- trunk_tree.commit(u'trunk revision', rev_id=trunk_rev_id)
- branch_tree.commit(u'branch revision', rev_id=branch_rev_id)
-
- # Merge branch into trunk.
- trunk_tree.merge_from_branch(branch_tree.branch)
- trunk_tree.commit(u'merge revision', rev_id=merge_rev_id)
+ # XXX: AaronBentley 2010-08-06 bug=614404: a bzr username is
+ # required to generate the revision-id.
+ with override_environ(BZR_EMAIL='me@xxxxxxxxxxx'):
+ trunk_tree.commit(u'base revision', rev_id=base_rev_id)
+
+ # Branch from the base revision.
+ new_db_branch = self.makeDatabaseBranch(product=db_branch.product)
+ new_db_branch, branch_tree = self.create_branch_and_tree(
+ db_branch=new_db_branch)
+ branch_tree.pull(trunk_tree.branch)
+
+ # Commit to both branches.
+ trunk_tree.commit(u'trunk revision', rev_id=trunk_rev_id)
+ branch_tree.commit(u'branch revision', rev_id=branch_rev_id)
+
+ # Merge branch into trunk.
+ trunk_tree.merge_from_branch(branch_tree.branch)
+ trunk_tree.commit(u'merge revision', rev_id=merge_rev_id)
LaunchpadZopelessLayer.txn.commit()
LaunchpadZopelessLayer.switchDbUser(config.branchscanner.dbuser)
=== modified file 'lib/lp/codehosting/scanner/tests/test_mergedetection.py'
--- lib/lp/codehosting/scanner/tests/test_mergedetection.py 2010-04-12 17:02:16 +0000
+++ lib/lp/codehosting/scanner/tests/test_mergedetection.py 2010-08-18 11:41:24 +0000
@@ -3,6 +3,8 @@
"""Tests for the scanner's merge detection."""
+from __future__ import with_statement
+
__metaclass__ = type
import logging
@@ -27,6 +29,7 @@
BranchMergeProposalJob, BranchMergeProposalJobFactory,
BranchMergeProposalJobType)
from lp.code.interfaces.branchlookup import IBranchLookup
+from lp.services.osutils import override_environ
from lp.testing import TestCase, TestCaseWithFactory
from lp.testing.mail_helpers import pop_notifications
@@ -129,7 +132,10 @@
proposal, db_trunk, db_branch, branch_tree = (
self._createBranchesAndProposal())
- branch_tree.commit(u'another revision', rev_id='another-rev')
+ # XXX: AaronBentley 2010-08-06 bug=614404: a bzr username is
+ # required to generate the revision-id.
+ with override_environ(BZR_EMAIL='me@xxxxxxxxxxx'):
+ branch_tree.commit(u'another revision', rev_id='another-rev')
current_proposal_status = proposal.queue_status
self.assertNotEqual(
current_proposal_status,
@@ -147,7 +153,10 @@
proposal, db_trunk, db_branch, branch_tree = (
self._createBranchesAndProposal())
- branch_tree.commit(u'another revision', rev_id='another-rev')
+ # XXX: AaronBentley 2010-08-06 bug=614404: a bzr username is
+ # required to generate the revision-id.
+ with override_environ(BZR_EMAIL='me@xxxxxxxxxxx'):
+ branch_tree.commit(u'another revision', rev_id='another-rev')
current_proposal_status = proposal.queue_status
self.assertNotEqual(
current_proposal_status,
=== modified file 'lib/lp/codehosting/tests/test_acceptance.py'
--- lib/lp/codehosting/tests/test_acceptance.py 2010-04-19 06:35:23 +0000
+++ lib/lp/codehosting/tests/test_acceptance.py 2010-08-18 11:41:24 +0000
@@ -242,10 +242,7 @@
creator_id, '/~%s/%s/%s' % (user, product, branch))
branch_url = 'file://' + os.path.abspath(
os.path.join(branch_root, branch_id_to_path(branch_id)))
- self.runInChdir(
- self.local_branch_path,
- self.run_bzr, ['push', '--create-prefix', branch_url],
- retcode=None)
+ self.push(self.local_branch_path, branch_url, ['--create-prefix'])
return branch_url
@@ -487,6 +484,27 @@
'~landscape-developers/landscape/some-branch')
self.assertNotBranch(remote_url)
+ def test_push_to_new_full_branch_alias(self):
+ # We can also push branches to URLs like /+branch/~foo/bar/baz.
+ unique_name = '~testuser/firefox/new-branch'
+ remote_url = self.getTransportURL('+branch/%s' % unique_name)
+ self.push(self.local_branch_path, remote_url)
+ self.assertBranchesMatch(self.local_branch_path, remote_url)
+ self.assertBranchesMatch(
+ self.local_branch_path, self.getTransportURL(unique_name))
+
+ def test_push_to_new_short_branch_alias(self):
+ # We can also push branches to URLs like /+branch/firefox
+ # Hack 'firefox' so we have permission to do this.
+ LaunchpadZopelessTestSetup().txn.begin()
+ firefox = Product.selectOneBy(name='firefox')
+ testuser = Person.selectOneBy(name='testuser')
+ firefox.development_focus.owner = testuser
+ LaunchpadZopelessTestSetup().txn.commit()
+ remote_url = self.getTransportURL('+branch/firefox')
+ self.push(self.local_branch_path, remote_url)
+ self.assertBranchesMatch(self.local_branch_path, remote_url)
+
def test_can_push_to_existing_hosted_branch(self):
# If a hosted branch exists in the database, but not on the
# filesystem, and is writable by the user, then the user is able to
=== modified file 'lib/lp/codehosting/tests/test_branchdistro.py'
--- lib/lp/codehosting/tests/test_branchdistro.py 2010-04-23 05:49:08 +0000
+++ lib/lp/codehosting/tests/test_branchdistro.py 2010-08-18 11:41:24 +0000
@@ -4,6 +4,8 @@
"""Tests for making new source package branches just after a distro release.
"""
+from __future__ import with_statement
+
__metaclass__ = type
import os
@@ -32,6 +34,7 @@
from lp.codehosting.vfs import branch_id_to_path
from lp.registry.interfaces.pocket import PackagePublishingPocket
from lp.testing import TestCaseWithFactory
+from lp.services.osutils import override_environ
# We say "RELEASE" often enough to not want to say "PackagePublishingPocket."
@@ -66,7 +69,10 @@
old_branch = FakeBranch(1)
self.get_transport(old_branch.unique_name).create_prefix()
tree = self.make_branch_and_tree(old_branch.unique_name)
- tree.commit(message='.')
+ # XXX: AaronBentley 2010-08-06 bug=614404: a bzr username is
+ # required to generate the revision-id.
+ with override_environ(BZR_EMAIL='me@xxxxxxxxxxx'):
+ tree.commit(message='.')
new_branch = FakeBranch(2)
@@ -119,7 +125,10 @@
_, tree = self.create_branch_and_tree(
tree_location=self.factory.getUniqueString(), db_branch=db_branch)
- tree.commit('')
+ # XXX: AaronBentley 2010-08-06 bug=614404: a bzr username is
+ # required to generate the revision-id.
+ with override_environ(BZR_EMAIL='me@xxxxxxxxxxx'):
+ tree.commit('')
return db_branch
@@ -484,8 +493,11 @@
brancher.makeOneNewBranch(db_branch)
url = 'lp-internal:///' + db_branch.unique_name
old_bzr_branch = Branch.open(url)
- old_bzr_branch.create_checkout(
- self.factory.getUniqueString()).commit('')
+ # XXX: AaronBentley 2010-08-06 bug=614404: a bzr username is
+ # required to generate the revision-id.
+ with override_environ(BZR_EMAIL='me@xxxxxxxxxxx'):
+ old_bzr_branch.create_checkout(
+ self.factory.getUniqueString()).commit('')
ok = brancher.checkOneBranch(db_branch)
self.assertLogMessages([
'^WARNING Repository at lp-internal:///.*/.*/.*/.* has 1 '
=== modified file 'lib/lp/codehosting/tests/test_bzrutils.py'
--- lib/lp/codehosting/tests/test_bzrutils.py 2010-04-23 01:47:30 +0000
+++ lib/lp/codehosting/tests/test_bzrutils.py 2010-08-18 11:41:24 +0000
@@ -14,7 +14,7 @@
from bzrlib.branch import Branch, BranchReferenceFormat
from bzrlib.bzrdir import BzrDir, format_registry
from bzrlib.remote import RemoteBranch
-from bzrlib.smart import server
+from bzrlib.tests import test_server
from bzrlib.tests import (
multiply_tests, TestCase, TestCaseWithTransport, TestLoader,
TestNotApplicable)
@@ -191,7 +191,7 @@
# of the branch, repo and bzrdir, even if the branch is a
# RemoteBranch.
vfs_branch = self.make_branch('.')
- smart_server = server.SmartTCPServer_for_testing()
+ smart_server = test_server.SmartTCPServer_for_testing()
smart_server.start_server(self.get_vfs_only_server())
self.addCleanup(smart_server.stop_server)
remote_branch = Branch.open(smart_server.get_url())
=== modified file 'lib/lp/codehosting/tests/test_jobs.py'
--- lib/lp/codehosting/tests/test_jobs.py 2010-05-27 02:04:21 +0000
+++ lib/lp/codehosting/tests/test_jobs.py 2010-08-18 11:41:24 +0000
@@ -3,6 +3,7 @@
"""Tests for Job-running facilities."""
+from __future__ import with_statement
from unittest import TestLoader
@@ -15,6 +16,7 @@
from lp.code.model.branchjob import RevisionMailJob
from lp.code.model.diff import StaticDiff
from lp.services.job.runner import JobRunner
+from lp.services.osutils import override_environ
from lp.testing import TestCaseWithFactory
@@ -34,7 +36,10 @@
tree_transport = tree.bzrdir.root_transport
tree_transport.put_bytes("hello.txt", "Hello World\n")
tree.add('hello.txt')
- to_revision_id = tree.commit('rev1', timestamp=1e9, timezone=0)
+ # XXX: AaronBentley 2010-08-06 bug=614404: a bzr username is
+ # required to generate the revision-id.
+ with override_environ(BZR_EMAIL='me@xxxxxxxxxxx'):
+ to_revision_id = tree.commit('rev1', timestamp=1e9, timezone=0)
job = RevisionMailJob.create(
branch, 1, 'from@xxxxxxxxxxx', 'body', True, 'subject')
LaunchpadZopelessLayer.txn.commit()
=== modified file 'lib/lp/codehosting/vfs/branchfsclient.py'
--- lib/lp/codehosting/vfs/branchfsclient.py 2010-04-20 04:39:31 +0000
+++ lib/lp/codehosting/vfs/branchfsclient.py 2010-08-18 11:41:24 +0000
@@ -95,6 +95,8 @@
for object_path, value in self._cache.iteritems():
transport_type, data, inserted_time = value
split_object_path = object_path.strip('/').split('/')
+ # Do a segment-by-segment comparison. Python sucks, lists should
+ # also have startswith.
if split_path[:len(split_object_path)] == split_object_path:
if (self.expiry_time is not None
and self._now() > inserted_time + self.expiry_time):
=== modified file 'lib/lp/codehosting/vfs/tests/test_transport.py'
--- lib/lp/codehosting/vfs/tests/test_transport.py 2010-04-19 07:05:57 +0000
+++ lib/lp/codehosting/vfs/tests/test_transport.py 2010-08-18 11:41:24 +0000
@@ -43,6 +43,9 @@
BlockingProxy(branchfs), LocalTransport(local_path_to_url('.')))
self._chroot_servers = []
+ def get_bogus_url(self):
+ return self._scheme + 'bogus'
+
def _transportFactory(self, url):
"""See `LaunchpadInternalServer._transportFactory`.
=== modified file 'lib/lp/registry/browser/__init__.py'
--- lib/lp/registry/browser/__init__.py 2010-06-12 03:37:58 +0000
+++ lib/lp/registry/browser/__init__.py 2010-08-18 11:41:24 +0000
@@ -115,10 +115,11 @@
var create_milestone_link = Y.get(
'.menu-link-create_milestone');
create_milestone_link.addClass('js-action');
+ var milestone_table = Y.lp.registry.milestonetable;
var config = {
milestone_form_uri: milestone_form_uri,
series_uri: series_uri,
- next_step: Y.lp.registry.milestonetable.get_milestone_row,
+ next_step: milestone_table.get_milestone_row,
activate_node: create_milestone_link
};
Y.lp.registry.milestoneoverlay.attach_widget(config);
@@ -216,7 +217,10 @@
"""Delete a milestone and unlink related objects."""
self._unsubscribe_structure(milestone)
for bugtask in self._getBugtasks(milestone):
- bugtask.milestone = None
+ if bugtask.conjoined_master is not None:
+ Store.of(bugtask).remove(bugtask.conjoined_master)
+ else:
+ bugtask.milestone = None
for spec in self._getSpecifications(milestone):
spec.milestone = None
self._deleteRelease(milestone.product_release)
=== modified file 'lib/lp/registry/browser/announcement.py'
--- lib/lp/registry/browser/announcement.py 2010-04-19 14:47:49 +0000
+++ lib/lp/registry/browser/announcement.py 2010-08-18 11:41:24 +0000
@@ -28,7 +28,7 @@
from canonical.launchpad import _
from canonical.launchpad.browser.feeds import (
AnnouncementsFeedLink, FeedsMixin, RootAnnouncementsFeedLink)
-from canonical.launchpad.fields import AnnouncementDate, Summary, Title
+from lp.services.fields import AnnouncementDate, Summary, Title
from canonical.launchpad.interfaces.validation import valid_webref
from canonical.launchpad.webapp.authorization import check_permission
from canonical.launchpad.webapp.batching import BatchNavigator
=== modified file 'lib/lp/registry/browser/peoplemerge.py'
--- lib/lp/registry/browser/peoplemerge.py 2010-07-15 08:38:19 +0000
+++ lib/lp/registry/browser/peoplemerge.py 2010-08-18 11:41:24 +0000
@@ -321,6 +321,9 @@
@action('Delete', name='delete', condition=canDelete)
def merge_action(self, action, data):
base = super(DeleteTeamView, self)
+ # Delete is implemented as a merge process, but email addresses should
+ # be deleted because ~registry can never claim them.
+ self.context.setContactAddress(None)
base.deactivate_members_and_merge_action.success(data)
=== modified file 'lib/lp/registry/browser/person.py'
--- lib/lp/registry/browser/person.py 2010-08-06 10:48:49 +0000
+++ lib/lp/registry/browser/person.py 2010-08-18 11:41:24 +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).
# pylint: disable-msg=E0211,E0213,C0322
@@ -156,7 +156,7 @@
from lp.registry.interfaces.codeofconduct import ISignedCodeOfConductSet
from lp.registry.interfaces.gpg import IGPGKeySet
from lp.registry.interfaces.irc import IIrcIDSet
-from lp.registry.interfaces.jabber import IJabberIDSet
+from lp.registry.interfaces.jabber import IJabberID, IJabberIDSet
from lp.registry.interfaces.mailinglist import (
CannotUnsubscribe, IMailingListSet)
from lp.registry.interfaces.mailinglistsubscription import (
@@ -207,7 +207,7 @@
TopLevelMenuMixin)
from lp.answers.browser.questiontarget import SearchQuestionsView
-from canonical.launchpad.fields import LocationField
+from lp.services.fields import LocationField
from canonical.launchpad.mailnotification import send_direct_contact_email
from canonical.launchpad.validators.email import valid_email
@@ -1400,6 +1400,11 @@
label = "Deactivate your Launchpad account"
custom_widget('comment', TextAreaWidget, height=5, width=60)
+ def validate(self, data):
+ """See `LaunchpadFormView`."""
+ if self.context.account_status != AccountStatus.ACTIVE:
+ self.addError('This account is already deactivated.')
+
@action(_("Deactivate My Account"), name="deactivate")
def deactivate_action(self, action, data):
self.context.deactivateAccount(data['comment'])
@@ -3073,19 +3078,40 @@
def label(self):
return 'Team participation for ' + self.context.displayname
- def _asParticipation(self, membership, team_path=None):
- """Return a dict of participation information for the membership."""
+ def _asParticipation(self, membership=None, team=None, team_path=None):
+ """Return a dict of participation information for the membership.
+
+ Method requires membership or team, not both.
+ """
+ if ((membership is None and team is None) or
+ (membership is not None and team is not None)):
+ raise AssertionError(
+ "The membership or team argument must be provided, not both.")
+
if team_path is None:
via = None
else:
via = COMMASPACE.join(team.displayname for team in team_path)
- team = membership.team
- if membership.person == team.teamowner:
- role = 'Owner'
- elif membership.status == TeamMembershipStatus.ADMIN:
- role = 'Admin'
- else:
+
+ if membership is None:
+ #we have membership via an indirect team, and can use
+ #sane defaults
+
+ #the user can only be a member of this team
role = 'Member'
+ #the user never joined, and can't have a join date
+ datejoined = None
+ else:
+ #the member is a direct member, so we can use membership data
+ team = membership.team
+ datejoined = membership.datejoined
+ if membership.person == team.teamowner:
+ role = 'Owner'
+ elif membership.status == TeamMembershipStatus.ADMIN:
+ role = 'Admin'
+ else:
+ role = 'Member'
+
if team.mailing_list is not None and team.mailing_list.is_usable:
subscription = team.mailing_list.getSubscription(self.context)
if subscription is None:
@@ -3093,15 +3119,16 @@
else:
subscribed = 'Subscribed'
else:
- subscribed = None
+ subscribed = '—'
+
return dict(
- displayname=team.displayname, team=team, membership=membership,
+ displayname=team.displayname, team=team, datejoined=datejoined,
role=role, via=via, subscribed=subscribed)
@cachedproperty
def active_participations(self):
"""Return the participation information for active memberships."""
- participations = [self._asParticipation(membership)
+ participations = [self._asParticipation(membership=membership)
for membership in self.context.myactivememberships
if check_permission('launchpad.View', membership.team)]
membership_set = getUtility(ITeamMembershipSet)
@@ -3111,10 +3138,8 @@
# The key points of the path for presentation are:
# [-?] indirect memberships, [-2] direct membership, [-1] team.
team_path = self.context.findPathToTeam(team)
- membership = membership_set.getByPersonAndTeam(
- team_path[-2], team)
participations.append(
- self._asParticipation(membership, team_path=team_path[:-1]))
+ self._asParticipation(team_path=team_path[:-1], team=team))
return sorted(participations, key=itemgetter('displayname'))
@cachedproperty
@@ -3518,7 +3543,18 @@
class PersonEditJabberIDsView(LaunchpadFormView):
- schema = Interface
+
+ schema = IJabberID
+ field_names = ['jabberid']
+
+ def setUpFields(self):
+ super(PersonEditJabberIDsView, self).setUpFields()
+ if self.context.jabberids.count() > 0:
+ # Make the jabberid entry optional on the edit page if one or more
+ # ids already exist, which allows the removal of ids without
+ # filling out the new jabberid field.
+ jabber_field = self.form_fields['jabberid']
+ jabber_field.field.required = False
@property
def page_title(self):
@@ -3527,42 +3563,42 @@
label = page_title
@property
- def cancel_url(self):
+ def next_url(self):
return canonical_url(self.context)
+ cancel_url = next_url
+
+ def validate(self, data):
+ """Ensure the edited data does not already exist."""
+ jabberid = data.get('jabberid')
+ if jabberid is not None:
+ jabberset = getUtility(IJabberIDSet)
+ existingjabber = jabberset.getByJabberID(jabberid)
+ if existingjabber is not None:
+ if existingjabber.person != self.context:
+ self.setFieldError(
+ 'jabberid',
+ structured(
+ 'The Jabber ID %s is already registered by '
+ '<a href="%s">%s</a>.',
+ jabberid, canonical_url(existingjabber.person),
+ existingjabber.person.displayname))
+ else:
+ self.setFieldError(
+ 'jabberid',
+ 'The Jabber ID %s already belongs to you.' % jabberid)
+
@action(_("Save Changes"), name="save")
def save(self, action, data):
"""Process the Jabber ID form."""
- # XXX: EdwinGrubbs 2009-09-01 bug=422784
- # This view should use schema and form validation.
form = self.request.form
for jabber in self.context.jabberids:
if form.get('remove_%s' % jabber.jabberid):
jabber.destroySelf()
- else:
- jabberid = form.get('jabberid_%s' % jabber.jabberid)
- if not jabberid:
- self.request.response.addErrorNotification(
- "You cannot save an empty Jabber ID.")
- return
- jabber.jabberid = jabberid
-
- jabberid = form.get('newjabberid')
- if jabberid:
+ jabberid = data.get('jabberid')
+ if jabberid is not None:
jabberset = getUtility(IJabberIDSet)
- existingjabber = jabberset.getByJabberID(jabberid)
- if existingjabber is None:
- jabberset.new(self.context, jabberid)
- elif existingjabber.person != self.context:
- self.request.response.addErrorNotification(
- structured(
- 'The Jabber ID %s is already registered by '
- '<a href="%s">%s</a>.',
- jabberid, canonical_url(existingjabber.person),
- existingjabber.person.displayname))
- else:
- self.request.response.addErrorNotification(
- 'The Jabber ID %s already belongs to you.' % jabberid)
+ jabberset.new(self.context, jabberid)
class PersonEditSSHKeysView(LaunchpadView):
@@ -5132,7 +5168,7 @@
@cachedproperty
def related_projects_count(self):
"""The number of project owned or driven by this person."""
- return len(self._related_projects())
+ return self._related_projects().count()
@cachedproperty
def has_more_related_projects(self):
@@ -5599,24 +5635,32 @@
:type person_or_team: `IPerson`.
"""
if self._primary_reason is self.TO_USER:
- reason = 'the "Contact this user" link on your profile page'
+ reason = (
+ 'using the "Contact this user" link on your profile page\n'
+ '(%s)' % canonical_url(person_or_team))
header = 'ContactViaWeb user'
elif self._primary_reason is self.TO_OWNER:
reason = (
- 'the "Contact this team" owner link on the '
- '%s team page' % person_or_team.displayname)
+ 'using the "Contact this team\'s owner" link on the '
+ '%s team page\n(%s)' % (
+ person_or_team.displayname,
+ canonical_url(person_or_team)))
header = 'ContactViaWeb owner (%s team)' % person_or_team.name
elif self._primary_reason is self.TO_TEAM:
reason = (
- 'the "Contact this team" link on the '
- '%s team page' % person_or_team.displayname)
+ 'using the "Contact this team" link on the '
+ '%s team page\n(%s)' % (
+ person_or_team.displayname,
+ canonical_url(person_or_team)))
header = 'ContactViaWeb member (%s team)' % person_or_team.name
else:
# self._primary_reason is self.TO_MEMBERS.
reason = (
- 'the "Contact this team" link on the %s\n'
- 'team page to each member directly' %
- person_or_team.displayname)
+ 'to each member of the %s team using the '
+ '"Contact this team" link on the %s team page\n(%s)' % (
+ person_or_team.displayname,
+ person_or_team.displayname,
+ canonical_url(person_or_team)))
header = 'ContactViaWeb member (%s team)' % person_or_team.name
return (reason, header)
=== modified file 'lib/lp/registry/browser/product.py'
--- lib/lp/registry/browser/product.py 2010-08-04 04:07:21 +0000
+++ lib/lp/registry/browser/product.py 2010-08-18 11:41:24 +0000
@@ -68,7 +68,7 @@
from lazr.delegates import delegates
from lazr.restful.interface import copy_field
from canonical.launchpad import _
-from canonical.launchpad.fields import PillarAliases, PublicPersonChoice
+from lp.services.fields import PillarAliases, PublicPersonChoice
from lp.app.errors import NotFoundError
from lp.app.interfaces.headings import IEditableContextTitle
from lp.blueprints.browser.specificationtarget import (
@@ -83,6 +83,7 @@
from lp.registry.interfaces.pillar import IPillarNameSet
from lp.registry.interfaces.product import IProductReviewSearch, License
from lp.registry.interfaces.series import SeriesStatus
+from lp.registry.interfaces.sourcepackagename import ISourcePackageNameSet
from lp.registry.interfaces.product import (
IProduct, IProductSet, LicenseStatus)
from lp.registry.interfaces.productrelease import (
@@ -120,7 +121,7 @@
from canonical.launchpad.webapp.breadcrumb import Breadcrumb
from canonical.launchpad.webapp.launchpadform import (
action, custom_widget, LaunchpadEditFormView, LaunchpadFormView,
- ReturnToReferrerMixin)
+ ReturnToReferrerMixin, safe_action)
from canonical.launchpad.webapp.menu import NavigationMenu
from canonical.launchpad.webapp.tales import MenuAPI
from canonical.widgets.popup import PersonPickerWidget
@@ -1833,6 +1834,17 @@
return canonical_url(self.product)
+def create_source_package_fields():
+ return form.Fields(
+ Choice(__name__='source_package_name',
+ vocabulary='SourcePackageName',
+ required=False),
+ Choice(__name__='distroseries',
+ vocabulary='DistroSeries',
+ required=False),
+ )
+
+
class ProjectAddStepOne(StepView):
"""product/+new view class for creating a new project."""
@@ -1849,6 +1861,29 @@
step_description = 'Project basics'
search_results_count = 0
+ def setUpFields(self):
+ """See `LaunchpadFormView`."""
+ super(ProjectAddStepOne, self).setUpFields()
+ self.form_fields = (
+ self.form_fields +
+ create_source_package_fields())
+
+ def setUpWidgets(self):
+ """See `LaunchpadFormView`."""
+ super(ProjectAddStepOne, self).setUpWidgets()
+ self.widgets['source_package_name'].visible = False
+ self.widgets['distroseries'].visible = False
+
+ @property
+ def _return_url(self):
+ """This view is using the hidden _return_url field.
+
+ It is not using the `ReturnToReferrerMixin`, since none
+ of its other code is used, because multistep views can't
+ have next_url set until the form submission succeeds.
+ """
+ return self.request.form.get('_return_url')
+
@property
def _next_step(self):
"""Define the next step.
@@ -1862,9 +1897,10 @@
def main_action(self, data):
"""See `MultiStepView`."""
self.next_step = self._next_step
- self.request.form['displayname'] = data['displayname']
- self.request.form['name'] = data['name'].lower()
- self.request.form['summary'] = data['summary']
+
+ # Make this a safe_action, so that the sourcepackage page can skip
+ # the first step with a link (GET request) providing form values.
+ continue_action = safe_action(StepView.continue_action)
class ProjectAddStepTwo(StepView, ProductLicenseMixin, ReturnToReferrerMixin):
@@ -1873,7 +1909,6 @@
_field_names = ['displayname', 'name', 'title', 'summary',
'description', 'licenses', 'license_info',
]
- main_action_label = u'Complete Registration'
schema = IProduct
step_name = 'projectaddstep2'
template = ViewPageTemplateFile('../templates/product-new.pt')
@@ -1887,6 +1922,25 @@
custom_widget('license_info', GhostWidget)
@property
+ def main_action_label(self):
+ if self.source_package_name is None:
+ return u'Complete Registration'
+ else:
+ return u'Complete registration and link to %s package' % (
+ self.source_package_name.name,
+ )
+
+ @property
+ def _return_url(self):
+ """This view is using the hidden _return_url field.
+
+ It is not using the `ReturnToReferrerMixin`, since none
+ of its other code is used, because multistep views can't
+ have next_url set until the form submission succeeds.
+ """
+ return self.request.form.get('_return_url')
+
+ @property
def step_description(self):
"""See `MultiStepView`."""
if self.search_results_count > 0:
@@ -1897,7 +1951,8 @@
"""See `LaunchpadFormView`."""
super(ProjectAddStepTwo, self).setUpFields()
self.form_fields = (self.form_fields +
- self._createDisclaimMaintainerField())
+ self._createDisclaimMaintainerField() +
+ create_source_package_fields())
def _createDisclaimMaintainerField(self):
"""Return a Bool field for disclaiming maintainer.
@@ -1930,12 +1985,39 @@
"this will be the project's URL.")
self.widgets['displayname'].visible = False
+ self.widgets['source_package_name'].visible = False
+ self.widgets['distroseries'].visible = False
+
+ # Set the source_package_release attribute on the licenses
+ # widget, so that the source package's copyright info can be
+ # displayed.
+ ubuntu = getUtility(ILaunchpadCelebrities).ubuntu
+ if self.source_package_name is not None:
+ release_list = ubuntu.getCurrentSourceReleases(
+ [self.source_package_name])
+ if len(release_list) != 0:
+ self.widgets['licenses'].source_package_release = (
+ release_list.items()[0][1])
+
+ @property
+ def source_package_name(self):
+ # setUpWidgets() doesn't have access to the data dictionary,
+ # so the source package name needs to be converted from a string
+ # into an object here.
+ package_name_string = self.request.form.get(
+ 'field.source_package_name')
+ if package_name_string is None:
+ return None
+ else:
+ return getUtility(ISourcePackageNameSet).queryByName(
+ package_name_string)
+
@cachedproperty
def _search_string(self):
"""Return the ORed terms to match."""
- search_text = SPACE.join((self.request.form['name'],
- self.request.form['displayname'],
- self.request.form['summary']))
+ search_text = SPACE.join((self.request.form['field.name'],
+ self.request.form['field.displayname'],
+ self.request.form['field.summary']))
# OR all the terms together.
return OR.join(search_text.split())
@@ -1972,7 +2054,8 @@
def label(self):
"""See `LaunchpadFormView`."""
return 'Register %s (%s) in Launchpad' % (
- self.request.form['displayname'], self.request.form['name'])
+ self.request.form['field.displayname'],
+ self.request.form['field.name'])
def create_product(self, data):
"""Create the product from the user data."""
@@ -1996,12 +2079,28 @@
license_info=data['license_info'],
project=project)
+ def link_source_package(self, product, data):
+ if (data.get('distroseries') is not None
+ and self.source_package_name is not None):
+ source_package = data['distroseries'].getSourcePackage(
+ self.source_package_name)
+ source_package.setPackaging(
+ product.development_focus, self.user)
+ self.request.response.addInfoNotification(
+ 'Linked %s project to %s source package.' % (
+ product.displayname, self.source_package_name.name))
+
def main_action(self, data):
"""See `MultiStepView`."""
self.product = self.create_product(data)
self.notifyCommercialMailingList()
notify(ObjectCreatedEvent(self.product))
- self.next_url = canonical_url(self.product)
+ self.link_source_package(self.product, data)
+
+ if self._return_url is None:
+ self.next_url = canonical_url(self.product)
+ else:
+ self.next_url = self._return_url
class ProductAddView(MultiStepView):
@@ -2027,7 +2126,7 @@
driver = copy_field(IProduct['driver'])
- transfer_to_registry = Bool(
+ transfer_to_registry = Bool(
title=_("I do not want to maintain this project"),
required=False,
description=_(
=== modified file 'lib/lp/registry/browser/productseries.py'
--- lib/lp/registry/browser/productseries.py 2010-08-03 12:41:29 +0000
+++ lib/lp/registry/browser/productseries.py 2010-08-18 11:41:24 +0000
@@ -44,7 +44,7 @@
from canonical.cachedproperty import cachedproperty
from canonical.launchpad import _
-from canonical.launchpad.fields import URIField
+from lp.services.fields import URIField
from lp.blueprints.browser.specificationtarget import (
HasSpecificationsMenuMixin)
from lp.blueprints.interfaces.specification import (
=== modified file 'lib/lp/registry/browser/project.py'
--- lib/lp/registry/browser/project.py 2010-08-02 02:13:52 +0000
+++ lib/lp/registry/browser/project.py 2010-08-18 11:41:24 +0000
@@ -63,7 +63,7 @@
QuestionTargetFacetMixin, QuestionCollectionAnswersMenu)
from lp.registry.browser.objectreassignment import (
ObjectReassignmentView)
-from canonical.launchpad.fields import PillarAliases, PublicPersonChoice
+from lp.services.fields import PillarAliases, PublicPersonChoice
from canonical.launchpad.webapp import (
ApplicationMenu, ContextMenu, LaunchpadEditFormView, LaunchpadFormView,
LaunchpadView, Link, Navigation, StandardLaunchpadFacets, action,
=== modified file 'lib/lp/registry/browser/sourcepackage.py'
--- lib/lp/registry/browser/sourcepackage.py 2010-07-02 14:34:58 +0000
+++ lib/lp/registry/browser/sourcepackage.py 2010-08-18 11:41:24 +0000
@@ -19,6 +19,8 @@
from apt_pkg import ParseSrcDepends
from cgi import escape
+import string
+import urllib
from z3c.ptcompat import ViewPageTemplateFile
from zope.app.form.browser import DropdownWidget
from zope.app.form.interfaces import IInputWidget
@@ -35,12 +37,14 @@
from canonical.launchpad import helpers
from canonical.launchpad.browser.multistep import MultiStepView, StepView
+
from lp.bugs.browser.bugtask import BugTargetTraversalMixin
from canonical.launchpad.browser.packagerelationship import (
relationship_builder)
from lp.answers.browser.questiontarget import (
QuestionTargetFacetMixin, QuestionTargetAnswersMenu)
from lp.services.worlddata.interfaces.country import ICountry
+from lp.registry.browser.product import ProjectAddStepOne
from lp.registry.interfaces.packaging import IPackaging, IPackagingUtil
from lp.registry.interfaces.pocket import PackagePublishingPocket
from lp.registry.interfaces.product import IProductSet
@@ -62,6 +66,35 @@
from canonical.lazr.utils import smartquote
+def get_register_upstream_url(source_package):
+ displayname = string.capwords(source_package.name.replace('-', ' '))
+ distroseries_string = "%s/%s" % (
+ source_package.distroseries.distribution.name,
+ source_package.distroseries.name)
+ params = {
+ '_return_url': canonical_url(source_package),
+ 'field.source_package_name': source_package.sourcepackagename.name,
+ 'field.distroseries': distroseries_string,
+ 'field.name': source_package.name,
+ 'field.displayname': displayname,
+ 'field.title': displayname,
+ 'field.__visited_steps__': ProjectAddStepOne.step_name,
+ 'field.actions.continue': 'Continue',
+ }
+ if len(source_package.releases) == 0:
+ params['field.summary'] = ''
+ else:
+ # This is based on the SourcePackageName.summary attribute, but
+ # it eliminates the binary.name and duplicate summary lines.
+ summary_set = set()
+ for binary in source_package.releases[0].sample_binary_packages:
+ summary_set.add(binary.summary)
+ params['field.summary'] = '\n'.join(sorted(summary_set))
+ query_string = urllib.urlencode(
+ sorted(params.items()), doseq=True)
+ return '/projects/+new?%s' % query_string
+
+
class SourcePackageNavigation(GetitemNavigation, BugTargetTraversalMixin):
usedfor = ISourcePackage
@@ -190,6 +223,10 @@
self.next_step = SourcePackageChangeUpstreamStepTwo
self.request.form['product'] = data['product']
+ @property
+ def register_upstream_url(self):
+ return get_register_upstream_url(self.context)
+
class SourcePackageChangeUpstreamStepTwo(ReturnToReferrerMixin, StepView):
"""A view to set the `IProductSeries` of a sourcepackage."""
@@ -345,7 +382,7 @@
def processForm(self):
# look for an update to any of the things we track
form = self.request.form
- if form.has_key('packaging'):
+ if 'packaging' in form:
if self.productseries_widget.hasValidInput():
new_ps = self.productseries_widget.getInputValue()
# we need to create or update the packaging
@@ -445,6 +482,7 @@
initial_focus_widget = None
max_suggestions = 9
other_upstream = object()
+ register_upstream = object()
def setUpFields(self):
"""See `LaunchpadFormView`."""
@@ -467,9 +505,12 @@
vocab_terms.append(SimpleTerm(product, product.name, description))
# Add an option to represent the user's decision to choose a
# different project. Note that project names cannot be uppercase.
- description = 'Choose another upstream project'
- vocab_terms.append(
- SimpleTerm(self.other_upstream, 'OTHER_UPSTREAM', description))
+ vocab_terms.append(
+ SimpleTerm(self.other_upstream, 'OTHER_UPSTREAM',
+ 'Choose another upstream project'))
+ vocab_terms.append(
+ SimpleTerm(self.register_upstream, 'REGISTER_UPSTREAM',
+ 'Register the upstream project'))
upstream_vocabulary = SimpleVocabulary(vocab_terms)
self.form_fields = Fields(
@@ -487,6 +528,11 @@
self.next_url = canonical_url(
self.context, view_name="+edit-packaging")
return
+ elif upstream is self.register_upstream:
+ # The user wants to create a new project.
+ url = get_register_upstream_url(self.context)
+ self.request.response.redirect(url)
+ return
self.context.setPackaging(upstream.development_focus, self.user)
self.request.response.addInfoNotification(
'The project %s was linked to this source package.' %
=== modified file 'lib/lp/registry/browser/team.py'
--- lib/lp/registry/browser/team.py 2010-08-03 08:49:19 +0000
+++ lib/lp/registry/browser/team.py 2010-08-18 11:41:24 +0000
@@ -40,7 +40,7 @@
from canonical.launchpad import _
from lp.app.errors import UnexpectedFormData
from lp.registry.browser.branding import BrandingChangeView
-from canonical.launchpad.fields import PublicPersonChoice
+from lp.services.fields import PublicPersonChoice
from canonical.launchpad.validators import LaunchpadValidationError
from canonical.cachedproperty import cachedproperty
from canonical.launchpad.webapp import (
=== modified file 'lib/lp/registry/browser/tests/distroseries-views.txt'
--- lib/lp/registry/browser/tests/distroseries-views.txt 2010-08-03 22:03:56 +0000
+++ lib/lp/registry/browser/tests/distroseries-views.txt 2010-08-18 11:41:24 +0000
@@ -375,7 +375,7 @@
>>> view = create_initialized_view(ubuntu, '+addseries', form=form)
>>> for error in view.errors:
... print error[2]
- 'Hardy-6.06-LTS': Bad upstream version format
+ 'Hardy-6.06-LTS': Could not parse version...
The distroseries version is unique to a distribution. Version '2009.06'
cannot be reused by another Ubuntu series.
=== modified file 'lib/lp/registry/browser/tests/peoplemerge-views.txt'
--- lib/lp/registry/browser/tests/peoplemerge-views.txt 2010-07-12 16:29:33 +0000
+++ lib/lp/registry/browser/tests/peoplemerge-views.txt 2010-08-18 11:41:24 +0000
@@ -251,9 +251,8 @@
>>> print find_tag_by_id(content, 'field.actions.delete')
None
-The registry experts can delete a team with a new email address (from
-an import), which will be invisible, since only preferred email addresses are
-shown for teams.
+The registry experts can delete a team with an email address. The email
+address is deleted instead of being transferred to the registry experts team.
>>> from canonical.launchpad.interfaces.emailaddress import (
... IEmailAddressSet, EmailAddressStatus)
@@ -275,6 +274,9 @@
>>> for notification in view.request.response.notifications:
... print notification.message
Team deleted.
+ >>> emails = getUtility(IEmailAddressSet).getByPerson(registry_experts)
+ >>> emails.count()
+ 0
Private teams can be deleted by admins.
=== modified file 'lib/lp/registry/browser/tests/project-add-views.txt'
--- lib/lp/registry/browser/tests/project-add-views.txt 2010-05-25 04:41:12 +0000
+++ lib/lp/registry/browser/tests/project-add-views.txt 2010-08-18 11:41:24 +0000
@@ -15,25 +15,24 @@
are forwarded in the form data to the second step. The title is also
forwarded, but is only required by the Zope machinery, not the view.
- >>> form = {'field.actions.continue': 'Continue'}
+ >>> from lp.registry.browser.product import ProjectAddStepOne
+ >>> form = {
+ ... 'field.actions.continue': 'Continue',
+ ... 'field.__visited_steps__': ProjectAddStepOne.step_name,
+ ... 'field.displayname': '',
+ ... 'field.name': '',
+ ... 'field.summary': '',
+ ... }
>>> view = create_initialized_view(product_set, name='+new', form=form)
- Traceback (most recent call last):
- ...
- KeyError: 'displayname'
+ >>> for error in view.view.errors:
+ ... print error
+ ('displayname', 'Name', RequiredMissing())
+ ('name', 'URL', RequiredMissing())
+ ('summary', u'Summary', RequiredMissing())
>>> form['field.displayname'] = 'Snowdog'
- >>> view = create_initialized_view(product_set, name='+new', form=form)
- Traceback (most recent call last):
- ...
- KeyError: 'name'
-
>>> form['field.name'] = 'snowdog'
- >>> view = create_initialized_view(product_set, name='+new', form=form)
- Traceback (most recent call last):
- ...
- KeyError: 'summary'
-
>>> form['field.summary'] = 'By-tor and the Snowdog'
>>> view = create_initialized_view(product_set, name='+new', form=form)
@@ -44,7 +43,6 @@
# steps individually.
>>> from canonical.launchpad.webapp.servers import LaunchpadTestRequest
- >>> from lp.registry.browser.product import ProjectAddStepOne
>>> form['field.__visited_steps__'] = ProjectAddStepOne.step_name
>>> request = LaunchpadTestRequest(form=form, method='POST')
@@ -63,10 +61,12 @@
>>> from lp.registry.browser.product import ProjectAddStepTwo
>>> form = {
- ... 'displayname': 'Snowdog',
- ... 'name': 'snowdog',
- ... 'title': 'The Snowdog',
- ... 'summary': 'By-tor and the Snowdog',
+ ... 'field.actions.continue': 'Continue',
+ ... 'field.__visited_steps__': ProjectAddStepTwo.step_name,
+ ... 'field.displayname': 'Snowdog',
+ ... 'field.name': 'snowdog',
+ ... 'field.title': 'The Snowdog',
+ ... 'field.summary': 'By-tor and the Snowdog',
... }
>>> request = LaunchpadTestRequest(form=form, method='POST')
@@ -90,7 +90,7 @@
existing projects for possible matches. By tweaking the project summary, we
can see that there are search results available.
- >>> form['summary'] = 'My Snowdog ate your Firefox'
+ >>> form['field.summary'] = 'My Snowdog ate your Firefox'
>>> request = LaunchpadTestRequest(form=form, method='POST')
>>> view = ProjectAddStepTwo(product_set, request)
@@ -229,9 +229,9 @@
questions.
<BLANKLINE>
Sometimes new projects are licensed as 'Other/Open Source' because the
- licensing decisions have not yet been made. If that is your situation we u=
- rge
- you to update the licensing in Launchpad as soon as you make that choice.
+ licensing decisions have not yet been made. If that is your situation
+ we urge you to update the licensing in Launchpad as soon as you make
+ that choice.
<BLANKLINE>
If the license for your project needs to be corrected you can do so by
following the 'Change Details' link on your project's overview page.
=== modified file 'lib/lp/registry/browser/tests/sourcepackage-views.txt'
--- lib/lp/registry/browser/tests/sourcepackage-views.txt 2010-05-13 18:55:10 +0000
+++ lib/lp/registry/browser/tests/sourcepackage-views.txt 2010-08-18 11:41:24 +0000
@@ -119,6 +119,7 @@
empty.
>>> form = {
+ ... 'field.__visited_steps__': 'sourcepackage_change_upstream_step1',
... 'field.product': '',
... 'field.actions.continue': 'Continue',
... }
@@ -133,12 +134,18 @@
but there is no notification message that the upstream link was updated.
>>> form = {
- ... 'field.productseries': 'bonkers/crazy',
- ... 'field.actions.change': 'Change',
+ ... 'field.__visited_steps__': 'sourcepackage_change_upstream_step2',
+ ... 'field.product': 'bonkers',
+ ... 'field.productseries': 'crazy',
+ ... 'field.actions.continue': 'Continue',
... }
>>> view = create_initialized_view(
... package, name='+edit-packaging', form=form,
... principal=product.owner)
+ >>> print view.view
+ <...SourcePackageChangeUpstreamStepTwo object...>
+ >>> print view.view.next_url
+ http://launchpad.dev/youbuntu/busy/+source/bonkers
>>> view.view.errors
[]
@@ -199,6 +206,7 @@
Registered upstream project:
Lernid
Choose another upstream project
+ Register the upstream project
The form does not steal focus because it is not the primary purpose of the
page.
@@ -229,6 +237,7 @@
Lernid...
Lernid Dev...
Choose another upstream project
+ Register the upstream project
Choosing the "Choose another upstream project" option redirects the user
to the +edit-packaging page where the user can search for a project.
@@ -259,7 +268,8 @@
... name='stinkyseries', product=product)
>>> distroseries = factory.makeDistroRelease(name='wonky',
... distribution=distribution)
- >>> sourcepackagename = factory.makeSourcePackageName(name='stinkypackage')
+ >>> sourcepackagename = factory.makeSourcePackageName(
+ ... name='stinkypackage')
>>> package = factory.makeSourcePackage(
... sourcepackagename=sourcepackagename, distroseries=distroseries)
@@ -360,3 +370,4 @@
match for this source package. Can you help us find one?
Registered upstream project:
Choose another upstream project
+ Register the upstream project
=== added file 'lib/lp/registry/browser/tests/test_mailinglists.py'
--- lib/lp/registry/browser/tests/test_mailinglists.py 1970-01-01 00:00:00 +0000
+++ lib/lp/registry/browser/tests/test_mailinglists.py 2010-08-18 11:41:24 +0000
@@ -0,0 +1,52 @@
+
+# Copyright 2010 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+from __future__ import with_statement
+
+"""Test harness for mailinglist views unit tests."""
+
+__metaclass__ = type
+
+
+from canonical.testing.layers import DatabaseFunctionalLayer
+from canonical.launchpad.ftests import login_person
+from canonical.launchpad.testing.pages import find_tag_by_id
+from lp.testing import TestCaseWithFactory, person_logged_in
+from lp.testing.views import create_view
+
+
+class MailingListSubscriptionControlsTestCase(TestCaseWithFactory):
+ """Verify the team index subscribe/unsubscribe to mailing list content."""
+
+ layer = DatabaseFunctionalLayer
+
+ def setUp(self):
+ super(MailingListSubscriptionControlsTestCase, self).setUp()
+ self.a_team = self.factory.makeTeam(name='a')
+ self.b_team = self.factory.makeTeam(name='b', owner=self.a_team)
+ self.b_team_list = self.factory.makeMailingList(team=self.b_team,
+ owner=self.b_team.teamowner)
+ self.user = self.factory.makePerson()
+ with person_logged_in(self.a_team.teamowner):
+ self.a_team.addMember(self.user, self.a_team.teamowner)
+
+ def test_subscribe_control_renders(self):
+ login_person(self.user)
+ view = create_view(self.b_team, name='+index',
+ principal=self.user, server_url='http://launchpad.dev',
+ path_info='/~%s' % self.b_team.name)
+ content = view.render()
+ link_tag = find_tag_by_id(content, "link-list-subscribe")
+ self.assertNotEqual(None, link_tag)
+
+ def test_subscribe_control_doesnt_render_for_anon(self):
+ other_person = self.factory.makePerson()
+ login_person(other_person)
+ view = create_view(self.b_team, name='+index',
+ principal=other_person, server_url='http://launchpad.dev',
+ path_info='/~%s' % self.b_team.name)
+ content = view.render()
+ self.assertNotEqual('', content)
+ link_tag = find_tag_by_id(content, "link-list-subscribe")
+ self.assertEqual(None, link_tag)
=== modified file 'lib/lp/registry/browser/tests/test_milestone.py'
--- lib/lp/registry/browser/tests/test_milestone.py 2010-08-06 20:15:31 +0000
+++ lib/lp/registry/browser/tests/test_milestone.py 2010-08-18 11:41:24 +0000
@@ -7,11 +7,63 @@
import unittest
-from lp.testing import ANONYMOUS, login_person, login
+from zope.component import getUtility
+
+from canonical.testing.layers import DatabaseFunctionalLayer
+from lp.bugs.interfaces.bugtask import IBugTaskSet
+from lp.testing import ANONYMOUS, login_person, login, TestCaseWithFactory
from lp.testing.views import create_initialized_view
from lp.testing.memcache import MemcacheTestCase
+class TestMilestoneViews(TestCaseWithFactory):
+
+ layer = DatabaseFunctionalLayer
+
+ def setUp(self):
+ TestCaseWithFactory.setUp(self)
+ self.product = self.factory.makeProduct()
+ self.series = self.factory.makeSeries(product=self.product)
+ owner = self.product.owner
+ login_person(owner)
+
+ def test_add_milestone(self):
+ form = {
+ 'field.name': '1.1',
+ 'field.actions.register': 'Register Milestone',
+ }
+ view = create_initialized_view(
+ self.series, '+addmilestone', form=form)
+ self.assertEqual([], view.errors)
+
+ def test_add_milestone_with_good_date(self):
+ form = {
+ 'field.name': '1.1',
+ 'field.dateexpected': '2010-10-10',
+ 'field.actions.register': 'Register Milestone',
+ }
+ view = create_initialized_view(
+ self.series, '+addmilestone', form=form)
+ # It's important to make sure no errors occured, but
+ # but also confirm that the milestone was created.
+ self.assertEqual([], view.errors)
+ self.assertEqual('1.1', self.product.milestones[0].name)
+
+ def test_add_milestone_with_bad_date(self):
+ form = {
+ 'field.name': '1.1',
+ 'field.dateexpected': '1010-10-10',
+ 'field.actions.register': 'Register Milestone',
+ }
+ view = create_initialized_view(
+ self.series, '+addmilestone', form=form)
+ error_msg = view.errors[0].errors[0]
+ expected_msg = (
+ "Date could not be formatted. Provide a date formatted "
+ "like YYYY-MM-DD format. The year must be after 1900.")
+ self.assertEqual(expected_msg, error_msg)
+
+
class TestMilestoneMemcache(MemcacheTestCase):
def setUp(self):
@@ -78,5 +130,28 @@
self.assertEqual(360, view.expire_cache_minutes)
+class TestMilestoneDeleteView(TestCaseWithFactory):
+ """Test the delete rules applied by the Milestone Delete view."""
+
+ layer = DatabaseFunctionalLayer
+
+ def test_delete_conjoined_bugtask(self):
+ product = self.factory.makeProduct()
+ bug = self.factory.makeBug(product=product)
+ master_bugtask = getUtility(IBugTaskSet).createTask(
+ bug, productseries=product.development_focus, owner=product.owner)
+ milestone = self.factory.makeMilestone(
+ productseries=product.development_focus)
+ login_person(product.owner)
+ master_bugtask.transitionToMilestone(milestone, product.owner)
+ form = {
+ 'field.actions.delete': 'Delete Milestone',
+ }
+ view = create_initialized_view(milestone, '+delete', form=form)
+ self.assertEqual([], view.errors)
+ self.assertEqual(0, len(product.all_milestones))
+ self.assertEqual(0, product.development_focus.all_bugtasks.count())
+
+
def test_suite():
return unittest.TestLoader().loadTestsFromName(__name__)
=== modified file 'lib/lp/registry/browser/tests/test_person_view.py'
--- lib/lp/registry/browser/tests/test_person_view.py 2010-08-02 02:13:52 +0000
+++ lib/lp/registry/browser/tests/test_person_view.py 2010-08-18 11:41:24 +0000
@@ -1,15 +1,14 @@
-# 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).
__metaclass__ = type
-import unittest
-
import transaction
from zope.component import getUtility
from canonical.config import config
from canonical.launchpad.ftests import ANONYMOUS, login
+from canonical.launchpad.interfaces.account import AccountStatus
from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
from lp.app.errors import NotFoundError
from canonical.launchpad.webapp.servers import LaunchpadTestRequest
@@ -274,7 +273,7 @@
login_person(team.teamowner)
team.addMember(self.user, team.teamowner)
[participation] = self.view.active_participations
- self.assertEqual(None, participation['subscribed'])
+ self.assertEqual('—', participation['subscribed'])
def test__asParticpation_unsubscribed_to_mailing_list(self):
# The default team role is 'Member'.
@@ -548,5 +547,33 @@
self.build.id) in html)
-def test_suite():
- return unittest.TestLoader().loadTestsFromName(__name__)
+class TestPersonDeactivateAccountView(TestCaseWithFactory):
+ """Tests for the PersonDeactivateAccountView."""
+
+ layer = DatabaseFunctionalLayer
+ form = {
+ 'field.comment': 'Gotta go.',
+ 'field.actions.deactivate': 'Deactivate My Account',
+ }
+
+ def test_deactivate_user_active(self):
+ user = self.factory.makePerson()
+ login_person(user)
+ view = create_initialized_view(
+ user, '+deactivate-account', form=self.form)
+ self.assertEqual([], view.errors)
+ notifications = view.request.response.notifications
+ self.assertEqual(1, len(notifications))
+ self.assertEqual(
+ 'Your account has been deactivated.', notifications[0].message)
+ self.assertEqual(AccountStatus.DEACTIVATED, user.account_status)
+
+ def test_deactivate_user_already_deactivated(self):
+ deactivated_user = self.factory.makePerson()
+ login_person(deactivated_user)
+ deactivated_user.deactivateAccount('going.')
+ view = create_initialized_view(
+ deactivated_user, '+deactivate-account', form=self.form)
+ self.assertEqual(1, len(view.errors))
+ self.assertEqual(
+ 'This account is already deactivated.', view.errors[0])
=== added file 'lib/lp/registry/browser/tests/test_sourcepackage_views.py'
--- lib/lp/registry/browser/tests/test_sourcepackage_views.py 1970-01-01 00:00:00 +0000
+++ lib/lp/registry/browser/tests/test_sourcepackage_views.py 2010-08-18 11:41:24 +0000
@@ -0,0 +1,146 @@
+# Copyright 2010 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Tests for SourcePackage view code."""
+
+__metaclass__ = type
+
+import cgi
+import urllib
+
+from zope.component import getUtility
+from zope.interface import implements
+
+from canonical.testing import DatabaseFunctionalLayer
+
+
+from lp.registry.browser.sourcepackage import get_register_upstream_url
+from lp.registry.interfaces.distribution import IDistribution
+from lp.registry.interfaces.distroseries import (
+ IDistroSeries, IDistroSeriesSet)
+from lp.registry.interfaces.sourcepackage import ISourcePackage
+from lp.soyuz.tests.test_publishing import SoyuzTestPublisher
+from lp.testing import TestCaseWithFactory
+
+
+class TestSourcePackageViewHelpers(TestCaseWithFactory):
+ """Tests for SourcePackage view helper functions."""
+
+ layer = DatabaseFunctionalLayer
+
+ def test_get_register_upstream_url_displayname(self):
+ distroseries = self.factory.makeDistroRelease(
+ distribution=self.factory.makeDistribution(name='zoobuntu'),
+ name='walrus')
+ source_package = self.factory.makeSourcePackage(
+ distroseries=distroseries,
+ sourcepackagename='python-super-package')
+ url = get_register_upstream_url(source_package)
+ expected_base = '/projects/+new'
+ expected_params = [
+ ('_return_url',
+ 'http://launchpad.dev/zoobuntu/walrus/'
+ '+source/python-super-package'),
+ ('field.__visited_steps__', 'projectaddstep1'),
+ ('field.actions.continue', 'Continue'),
+ # The sourcepackagename 'python-super-package' is split on
+ # the hyphens, and each word is capitalized.
+ ('field.displayname', 'Python Super Package'),
+ ('field.distroseries', 'zoobuntu/walrus'),
+ ('field.name', 'python-super-package'),
+ # The summary is missing, since the source package doesn't
+ # have a binary package release, and parse_qsl() excludes
+ # empty params.
+ ('field.source_package_name', 'python-super-package'),
+ ('field.title', 'Python Super Package'),
+ ]
+ base, query = urllib.splitquery(url)
+ params = cgi.parse_qsl(query)
+ self.assertEqual((expected_base, expected_params),
+ (base, params))
+
+ def test_get_register_upstream_url_summary(self):
+ test_publisher = SoyuzTestPublisher()
+ test_data = test_publisher.makeSourcePackageWithBinaryPackageRelease()
+ source_package_name = (
+ test_data['source_package'].sourcepackagename.name)
+ distroseries_id = test_data['distroseries'].id
+ test_publisher.updateDistroSeriesPackageCache(
+ test_data['distroseries'])
+
+ # updateDistroSeriesPackageCache reconnects the db, so the
+ # objects need to be reloaded.
+ distroseries = getUtility(IDistroSeriesSet).get(distroseries_id)
+ source_package = distroseries.getSourcePackage(source_package_name)
+ url = get_register_upstream_url(source_package)
+ expected_base = '/projects/+new'
+ expected_params = [
+ ('_return_url',
+ 'http://launchpad.dev/youbuntu/busy/+source/bonkers'),
+ ('field.__visited_steps__', 'projectaddstep1'),
+ ('field.actions.continue', 'Continue'),
+ ('field.displayname', 'Bonkers'),
+ ('field.distroseries', 'youbuntu/busy'),
+ ('field.name', 'bonkers'),
+ ('field.source_package_name', 'bonkers'),
+ ('field.summary', 'summary for flubber-bin\n'
+ + 'summary for flubber-lib'),
+ ('field.title', 'Bonkers'),
+ ]
+ base, query = urllib.splitquery(url)
+ params = cgi.parse_qsl(query)
+ self.assertEqual((expected_base, expected_params),
+ (base, params))
+
+ def test_get_register_upstream_url_summary_duplicates(self):
+
+ class Faker:
+ # Fakes attributes easily.
+ def __init__(self, **kw):
+ self.__dict__.update(kw)
+
+ class FakeSourcePackage(Faker):
+ # Interface necessary for canonical_url() call in
+ # get_register_upstream_url().
+ implements(ISourcePackage)
+
+ class FakeDistroSeries(Faker):
+ implements(IDistroSeries)
+
+ class FakeDistribution(Faker):
+ implements(IDistribution)
+
+ releases = Faker(sample_binary_packages=[
+ Faker(summary='summary for foo'),
+ Faker(summary='summary for bar'),
+ Faker(summary='summary for baz'),
+ Faker(summary='summary for baz'),
+ ])
+ source_package = FakeSourcePackage(
+ name='foo',
+ sourcepackagename=Faker(name='foo'),
+ distroseries=FakeDistroSeries(
+ name='walrus',
+ distribution=FakeDistribution(name='zoobuntu')),
+ releases=[releases])
+
+ url = get_register_upstream_url(source_package)
+ expected_base = '/projects/+new'
+ expected_params = [
+ ('_return_url',
+ 'http://launchpad.dev/zoobuntu/walrus/+source/foo'),
+ ('field.__visited_steps__', 'projectaddstep1'),
+ ('field.actions.continue', 'Continue'),
+ ('field.displayname', 'Foo'),
+ ('field.distroseries', 'zoobuntu/walrus'),
+ ('field.name', 'foo'),
+ ('field.source_package_name', 'foo'),
+ ('field.summary', 'summary for bar\n'
+ + 'summary for baz\n'
+ + 'summary for foo'),
+ ('field.title', 'Foo'),
+ ]
+ base, query = urllib.splitquery(url)
+ params = cgi.parse_qsl(query)
+ self.assertEqual((expected_base, expected_params),
+ (base, params))
=== modified file 'lib/lp/registry/browser/tests/user-to-user-views.txt'
--- lib/lp/registry/browser/tests/user-to-user-views.txt 2010-07-14 16:27:33 +0000
+++ lib/lp/registry/browser/tests/user-to-user-views.txt 2010-08-18 11:41:24 +0000
@@ -304,9 +304,10 @@
Bodies:
Can one of you help me?
--
- This message was sent from Launchpad by the user
+ This message was sent from Launchpad by
Bart (http://launchpad.dev/~bart)
- using the "Contact this team" owner link on the GuadaMen team page.
+ using the "Contact this team's owner" link on the GuadaMen team page
+ (http://launchpad.dev/~guadamen).
For more information see
https://help.launchpad.net/YourAccount/ContactingPeople
# of Messages: 1
@@ -344,10 +345,10 @@
Bodies:
Can one of you help me?
--
- This message was sent from Launchpad by the user
+ This message was sent from Launchpad by
Foo Bar (http://launchpad.dev/~name16)
- using the "Contact this team" link on the GuadaMen
- team page to each member directly.
+ to each member of the GuadaMen team using the "Contact this team" link on
+ the GuadaMen team page (http://launchpad.dev/~guadamen).
For more information see
https://help.launchpad.net/YourAccount/ContactingPeople
# of Messages: 10
@@ -397,10 +398,11 @@
Bodies:
Can one of you help me with "%s" usage!
--
- This message was sent from Launchpad by the user
+ This message was sent from Launchpad by
Foo Bar (http://launchpad.dev/~name16)
+ to each member of the GuadaMen team
using the "Contact this team" link on the GuadaMen
- team page to each member directly.
+ team page (http://launchpad.dev/~guadamen).
For more information see
https://help.launchpad.net/YourAccount/ContactingPeople
# of Messages: 10
@@ -459,9 +461,10 @@
<BLANKLINE>
Can one of you help me?
--
- This message was sent from Launchpad by the user
+ This message was sent from Launchpad by
Foo Bar (http://launchpad.dev/~name16)
- using the "Contact this team" link on the GuadaMen team page.
+ using the "Contact this team" link on the GuadaMen team page
+ (http://launchpad.dev/~guadamen).
For more information see
https://help.launchpad.net/YourAccount/ContactingPeople
@@ -541,9 +544,10 @@
<BLANKLINE>
Can you help me?
--
- This message was sent from Launchpad by the user
+ This message was sent from Launchpad by
Cris (http://launchpad.dev/~cris)
- using the "Contact this user" link on your profile page.
+ using the "Contact this user" link on your profile page
+ (http://launchpad.dev/~dave).
For more information see
https://help.launchpad.net/YourAccount/ContactingPeople
@@ -573,10 +577,11 @@
^Can you help me? Can you help me? Can you help me? Can you help me? Can$
^you help me? Can you help me? Can you help me? Can you help me?$
^-- $
- ^This message was sent from Launchpad by the user$
+ ^This message was sent from Launchpad by$
^Sample Person (http://launchpad.dev/~name12)$
- ^using the "Contact this team" link on the Landscape Developers$
- ^team page to each member directly.$
+ ^to each member of the Landscape Developers team using the "Contact this$
+ ^team" link on the Landscape Developers team page$
+ ^(http://launchpad.dev/~landscape-developers).$
^For more information see$
^https://help.launchpad.net/YourAccount/ContactingPeople$
=== modified file 'lib/lp/registry/doc/distroseries.txt'
--- lib/lp/registry/doc/distroseries.txt 2010-08-10 02:56:06 +0000
+++ lib/lp/registry/doc/distroseries.txt 2010-08-18 11:41:24 +0000
@@ -309,21 +309,21 @@
publishing records etc. Essentially this is a "Do not push this button
again" type set of assertions.
+ >>> from lp.soyuz.scripts.initialise_distroseries import (
+ ... InitialiseDistroSeries)
>>> login("foo.bar@xxxxxxxxxxxxx")
>>> humpy = ubuntu.newSeries('humpy', 'Humpy Hippo',
... 'The Humpy Hippo', 'Fat', 'Yo Momma',
... '99.2',hoary, hoary.owner)
- >>> humpy_i386 = humpy.newArch('i386', hoary['i386'].processorfamily,
- ... True, humpy.owner)
- >>> humpy.nominatedarchindep = humpy_i386
- >>> humpy.initialiseFromParent()
+ >>> ids = InitialiseDistroSeries(humpy)
+ >>> ids.initialise()
>>> len(hoary.getPublishedReleases('pmount'))
1
>>> len(humpy.getPublishedReleases('pmount'))
1
>>> len(hoary['i386'].getReleasedPackages('pmount'))
1
- >>> len(humpy_i386.getReleasedPackages('pmount'))
+ >>> len(humpy['i386'].getReleasedPackages('pmount'))
1
Check if the attributes of an DRSPR instance for the just initialised
@@ -349,11 +349,8 @@
>>> bumpy = ubuntu.newSeries('bumpy', 'Bumpy',
... 'The Bumpy', 'Fat', 'Boom',
... '99.3', warty, warty.owner)
-
- >>> bumpy_i386 = bumpy.newArch('i386', warty['i386'].processorfamily,
- ... True, bumpy.owner)
- >>> bumpy.nominatedarchindep = bumpy_i386
- >>> bumpy.initialiseFromParent()
+ >>> ids = InitialiseDistroSeries(bumpy)
+ >>> ids.initialise()
Build a new ISourcePackage based in the new distroseries:
@@ -364,11 +361,12 @@
'binaries' should be inherited from parent release.
>>> bumpy_firefox_sp.currentrelease.binaries.count()
- 2
+ 3
>>> for bin in bumpy_firefox_sp.currentrelease.binaries:
... print bin.id, bin.title, bin.build.distro_arch_series.title
27 mozilla-firefox-data-0.9 The Warty Warthog Release for i386 (x86)
+ 26 mozilla-firefox-0.9 The Warty Warthog Release for hppa (hppa)
12 mozilla-firefox-0.9 The Warty Warthog Release for i386 (x86)
=== modified file 'lib/lp/registry/doc/person-account.txt'
--- lib/lp/registry/doc/person-account.txt 2010-07-14 21:19:42 +0000
+++ lib/lp/registry/doc/person-account.txt 2010-08-18 11:41:24 +0000
@@ -185,7 +185,7 @@
...no owned or driven pillars...
- >>> len(foobar.getOwnedOrDrivenPillars())
+ >>> foobar.getOwnedOrDrivenPillars().count()
0
...and, finally, to not be considered a valid person in Launchpad.
=== modified file 'lib/lp/registry/doc/pillar-aliases-field.txt'
--- lib/lp/registry/doc/pillar-aliases-field.txt 2010-02-16 20:36:48 +0000
+++ lib/lp/registry/doc/pillar-aliases-field.txt 2010-08-18 11:41:24 +0000
@@ -1,6 +1,6 @@
= Field for assigning aliases to pillars =
- >>> from canonical.launchpad.fields import PillarAliases
+ >>> from lp.services.fields import PillarAliases
>>> from lp.registry.interfaces.product import IProductSet
>>> firefox = getUtility(IProductSet)['firefox']
>>> field = PillarAliases(__name__='aliases')
=== modified file 'lib/lp/registry/doc/private-team-visibility.txt'
--- lib/lp/registry/doc/private-team-visibility.txt 2010-07-27 22:13:36 +0000
+++ lib/lp/registry/doc/private-team-visibility.txt 2010-08-18 11:41:24 +0000
@@ -10,9 +10,11 @@
administrators .
>>> from lp.registry.interfaces.person import PersonVisibility
+ >>> from lp.testing import login_celebrity
+
>>> priv_owner = factory.makePerson(name="priv-owner")
>>> priv_member = factory.makePerson(name="priv-member")
- >>> login('commercial-member@xxxxxxxxxxxxx')
+ >>> commercial_admin = login_celebrity('commercial_admin')
>>> priv_team = factory.makeTeam(owner=priv_owner, name="priv-team",
... visibility=PersonVisibility.PRIVATE)
>>> login_person(priv_owner)
@@ -31,6 +33,17 @@
>>> login_person(priv_member)
>>> members = priv_team.activemembers
+A commercial admin can view private teams and private team memberships.
+
+ >>> from canonical.launchpad.webapp.authorization import check_permission
+
+ >>> commercial_admin = login_celebrity('commercial_admin')
+ >>> check_permission('launchpad.View', priv_team)
+ True
+ >>> team_membership = priv_member.myactivememberships[0]
+ >>> check_permission('launchpad.View', team_membership)
+ True
+
A person who is not in the team cannot see the membership and cannot
see other details of the team, such as the name.
@@ -38,12 +51,14 @@
>>> members = priv_team.activemembers
Traceback (most recent call last):
...
- Unauthorized: (<Person at ... priv-team (Priv Team)>, 'activemembers', 'launchpad.View')
+ Unauthorized: (<Person at ... priv-team (Priv Team)>,
+ 'activemembers', 'launchpad.View')
>>> print priv_team.name
Traceback (most recent call last):
...
- Unauthorized: (<Person at ... priv-team (Priv Team)>, 'name', 'launchpad.View')
+ Unauthorized: (<Person at ... priv-team (Priv Team)>,
+ 'name', 'launchpad.View')
Public teams can join private teams. When adding one team to another
the team is invited to join and that invitation must be accepted by
@@ -63,7 +78,8 @@
>>> print priv_team.name
Traceback (most recent call last):
...
- Unauthorized: (<Person at ... priv-team (Priv Team)>, 'name', 'launchpad.View')
+ Unauthorized: (<Person at ... priv-team (Priv Team)>,
+ 'name', 'launchpad.View')
>>> login_person(priv_owner)
>>> ignored = priv_team.addMember(pub_team, reviewer=priv_owner)
@@ -89,4 +105,5 @@
>>> print priv_team.name
Traceback (most recent call last):
...
- Unauthorized: (<Person at ... priv-team (Priv Team)>, 'name', 'launchpad.View')
+ Unauthorized: (<Person at ... priv-team (Priv Team)>,
+ 'name', 'launchpad.View')
=== modified file 'lib/lp/registry/doc/teammembership.txt'
--- lib/lp/registry/doc/teammembership.txt 2010-07-14 15:28:42 +0000
+++ lib/lp/registry/doc/teammembership.txt 2010-08-18 11:41:24 +0000
@@ -1007,7 +1007,8 @@
>>> from canonical.launchpad.interfaces import IMasterObject
>>> IMasterObject(bad_user.account).status = AccountStatus.SUSPENDED
>>> IMasterObject(bad_user.preferredemail).status = EmailAddressStatus.OLD
- >>> removeSecurityProxy(bad_user)._preferredemail_cached = None
+ >>> from canonical.cachedproperty import clear_property
+ >>> clear_property(removeSecurityProxy(bad_user), '_preferredemail_cached')
>>> transaction.commit()
>>> [m.displayname for m in t3.allmembers]
=== modified file 'lib/lp/registry/interfaces/commercialsubscription.py'
--- lib/lp/registry/interfaces/commercialsubscription.py 2009-06-25 04:06:00 +0000
+++ lib/lp/registry/interfaces/commercialsubscription.py 2010-08-18 11:41:24 +0000
@@ -19,7 +19,7 @@
export_as_webservice_entry, exported)
from canonical.launchpad import _
-from canonical.launchpad.fields import PublicPersonChoice
+from lp.services.fields import PublicPersonChoice
class ICommercialSubscription(Interface):
=== modified file 'lib/lp/registry/interfaces/distribution.py'
--- lib/lp/registry/interfaces/distribution.py 2010-08-06 11:52:05 +0000
+++ lib/lp/registry/interfaces/distribution.py 2010-08-18 11:41:24 +0000
@@ -23,6 +23,7 @@
from zope.schema import Bool, Choice, Datetime, List, Object, Text, TextLine
from zope.interface import Attribute, Interface
+from lazr.lifecycle.snapshot import doNotSnapshot
from lazr.restful.fields import CollectionField, Reference
from lazr.restful.declarations import (
collection_default_content, export_as_webservice_collection,
@@ -33,7 +34,7 @@
from lazr.restful.interface import copy_field
from canonical.launchpad import _
-from canonical.launchpad.fields import (
+from lp.services.fields import (
Description, PublicPersonChoice, Summary, Title)
from lp.registry.interfaces.structuralsubscription import (
IStructuralSubscriptionTarget)
@@ -61,7 +62,7 @@
from lp.translations.interfaces.translationgroup import (
ITranslationPolicy)
from canonical.launchpad.validators.name import name_validator
-from canonical.launchpad.fields import (
+from lp.services.fields import (
IconImageUpload, LogoImageUpload, MugshotImageUpload, PillarNameField)
@@ -200,25 +201,27 @@
lucilleconfig = TextLine(
title=_("Lucille Config"),
description=_("The Lucille Config."), required=False)
- archive_mirrors = exported(CollectionField(
- description=_("All enabled and official ARCHIVE mirrors of this "
- "Distribution."),
- readonly=True, value_type=Object(schema=IDistributionMirror)))
- cdimage_mirrors = exported(CollectionField(
- description=_("All enabled and official RELEASE mirrors of this "
- "Distribution."),
- readonly=True, value_type=Object(schema=IDistributionMirror)))
+ archive_mirrors = exported(doNotSnapshot(
+ CollectionField(
+ description=_("All enabled and official ARCHIVE mirrors "
+ "of this Distribution."),
+ readonly=True, value_type=Object(schema=IDistributionMirror))))
+ cdimage_mirrors = exported(doNotSnapshot(
+ CollectionField(
+ description=_("All enabled and official RELEASE mirrors "
+ "of this Distribution."),
+ readonly=True, value_type=Object(schema=IDistributionMirror))))
disabled_mirrors = Attribute(
"All disabled and official mirrors of this Distribution.")
unofficial_mirrors = Attribute(
"All unofficial mirrors of this Distribution.")
pending_review_mirrors = Attribute(
"All mirrors of this Distribution that haven't been reviewed yet.")
- series = exported(
+ series = exported(doNotSnapshot(
CollectionField(
title=_("DistroSeries inside this Distribution"),
# Really IDistroSeries, see _schema_circular_imports.py.
- value_type=Reference(schema=Interface)))
+ value_type=Reference(schema=Interface))))
architectures = List(
title=_("DistroArchSeries inside this Distribution"))
uploaders = Attribute(_(
@@ -262,11 +265,11 @@
# Really IArchive, see _schema_circular_imports.py.
schema=Interface))
- all_distro_archives = exported(
+ all_distro_archives = exported(doNotSnapshot(
CollectionField(
title=_("A sequence of the distribution's non-PPA Archives."),
readonly=True, required=False,
- value_type=Reference(schema=Interface)),
+ value_type=Reference(schema=Interface))),
# Really IArchive, see _schema_circular_imports.py.
exported_as='archives')
=== modified file 'lib/lp/registry/interfaces/distributionmirror.py'
--- lib/lp/registry/interfaces/distributionmirror.py 2010-04-19 14:45:50 +0000
+++ lib/lp/registry/interfaces/distributionmirror.py 2010-08-18 11:41:24 +0000
@@ -41,7 +41,7 @@
from lazr.restful.interface import copy_field
from canonical.launchpad import _
-from canonical.launchpad.fields import (
+from lp.services.fields import (
ContentNameField, PublicPersonChoice, URIField, Whiteboard)
from canonical.launchpad.validators.name import name_validator
from canonical.launchpad.validators import LaunchpadValidationError
=== modified file 'lib/lp/registry/interfaces/distroseries.py'
--- lib/lp/registry/interfaces/distroseries.py 2010-08-05 00:25:36 +0000
+++ lib/lp/registry/interfaces/distroseries.py 2010-08-18 11:41:24 +0000
@@ -28,7 +28,7 @@
from lazr.restful.fields import Reference, ReferenceChoice
from canonical.launchpad import _
-from canonical.launchpad.fields import (
+from lp.services.fields import (
ContentNameField, Description, PublicPersonChoice, Title,
UniqueField)
from canonical.launchpad.interfaces.launchpad import IHasAppointedDriver
@@ -127,7 +127,7 @@
Version(version)
except VersionError, error:
raise LaunchpadValidationError(
- "'%s': %s" % (version, error[0]))
+ "'%s': %s" % (version, error))
class IDistroSeriesEditRestricted(Interface):
@@ -721,35 +721,6 @@
supports_virtualized=False):
"""Create a new port or DistroArchSeries for this DistroSeries."""
- def initialiseFromParent():
- """Copy in all of the parent distroseries's configuration. This
- includes all configuration for distroseries and distroarchseries
- publishing and all publishing records for sources and binaries.
-
- Preconditions:
- The distroseries must have been set up with its distroarchseriess
- as needed. It should have its nominated arch-indep set up along
- with all other basic requirements for the structure of the
- distroseries. This distroseries and all its distroarchseriess
- must have empty publishing sets. Section and component selections
- must be empty.
-
- Outcome:
- The publishing structure will be copied from the parent. All
- PUBLISHED and PENDING packages in the parent will be created in
- this distroseries and its distroarchseriess. The lucille config
- will be copied in, all component and section selections will be
- duplicated as will any permission-related structures.
-
- Note:
- This method will assert all of its preconditions where possible.
- After this is run, you still need to construct chroots for building,
- you need to add anything missing wrt. ports etc. This method is
- only meant to give you a basic copy of a parent series in order
- to assist you in preparing a new series of a distribution or
- in the initialisation of a derivative.
- """
-
def copyTranslationsFromParent(ztm):
"""Copy any translation done in parent that we lack.
=== modified file 'lib/lp/registry/interfaces/entitlement.py'
--- lib/lp/registry/interfaces/entitlement.py 2009-06-25 04:06:00 +0000
+++ lib/lp/registry/interfaces/entitlement.py 2010-08-18 11:41:24 +0000
@@ -22,7 +22,7 @@
from lazr.enum import DBEnumeratedType, DBItem
from canonical.launchpad import _
-from canonical.launchpad.fields import Whiteboard
+from lp.services.fields import Whiteboard
class EntitlementQuotaExceededError(Exception):
=== modified file 'lib/lp/registry/interfaces/jabber.py'
--- lib/lp/registry/interfaces/jabber.py 2009-07-17 00:26:05 +0000
+++ lib/lp/registry/interfaces/jabber.py 2010-08-18 11:41:24 +0000
@@ -33,7 +33,7 @@
Reference(
title=_("Owner"), required=True, schema=Interface, readonly=True))
jabberid = exported(
- TextLine(title=_("Jabber user ID"), required=True))
+ TextLine(title=_("New Jabber user ID"), required=True))
def destroySelf():
"""Delete this JabberID from the database."""
@@ -50,4 +50,3 @@
def getByPerson(person):
"""Return all JabberIDs for the given person."""
-
=== modified file 'lib/lp/registry/interfaces/mailinglist.py'
--- lib/lp/registry/interfaces/mailinglist.py 2009-12-24 06:44:57 +0000
+++ lib/lp/registry/interfaces/mailinglist.py 2010-08-18 11:41:24 +0000
@@ -30,7 +30,7 @@
from lazr.enum import DBEnumeratedType, DBItem
from canonical.launchpad import _
-from canonical.launchpad.fields import PublicPersonChoice
+from lp.services.fields import PublicPersonChoice
from canonical.launchpad.interfaces.emailaddress import IEmailAddress
from canonical.launchpad.interfaces.librarian import ILibraryFileAlias
from canonical.launchpad.interfaces.message import IMessage
=== modified file 'lib/lp/registry/interfaces/mentoringoffer.py'
--- lib/lp/registry/interfaces/mentoringoffer.py 2009-07-17 00:26:05 +0000
+++ lib/lp/registry/interfaces/mentoringoffer.py 2010-08-18 11:41:24 +0000
@@ -20,7 +20,7 @@
from zope.schema import Datetime, Bool
from canonical.launchpad import _
-from canonical.launchpad.fields import PublicPersonChoice
+from lp.services.fields import PublicPersonChoice
from lp.registry.interfaces.role import IHasOwner
class IMentoringOffer(IHasOwner):
"""An offer of mentoring help."""
=== modified file 'lib/lp/registry/interfaces/milestone.py'
--- lib/lp/registry/interfaces/milestone.py 2010-04-24 11:19:53 +0000
+++ lib/lp/registry/interfaces/milestone.py 2010-08-18 11:41:24 +0000
@@ -23,8 +23,9 @@
from lp.registry.interfaces.productrelease import IProductRelease
from lp.bugs.interfaces.bugtarget import IHasBugs, IHasOfficialBugTags
from lp.bugs.interfaces.bugtask import IBugTask
+from lp.services.fields import FormattableDate
from canonical.launchpad import _
-from canonical.launchpad.fields import (
+from lp.services.fields import (
ContentNameField, NoneableDescription, NoneableTextLine)
from canonical.launchpad.validators.name import name_validator
from canonical.launchpad.components.apihelpers import (
@@ -107,7 +108,7 @@
vocabulary="FilteredDistroSeries",
required=False) # for now
dateexpected = exported(
- Date(title=_("Date Targeted"), required=False,
+ FormattableDate(title=_("Date Targeted"), required=False,
description=_("Example: 2005-11-24")),
exported_as='date_targeted')
active = exported(
=== modified file 'lib/lp/registry/interfaces/person.py'
--- lib/lp/registry/interfaces/person.py 2010-08-03 08:49:19 +0000
+++ lib/lp/registry/interfaces/person.py 2010-08-18 11:41:24 +0000
@@ -64,7 +64,7 @@
from canonical.database.sqlbase import block_implicit_flushes
from canonical.launchpad import _
-from canonical.launchpad.fields import (
+from lp.services.fields import (
BlacklistableContentNameField, IconImageUpload, LogoImageUpload,
MugshotImageUpload, PasswordField, PersonChoice, PublicPersonChoice,
StrippedTextLine, is_public_person)
@@ -1231,7 +1231,7 @@
all_member_count = Attribute(
"The total number of real people who are members of this team, "
"including subteams.")
- allmembers = exported(
+ all_members_prepopulated = exported(
doNotSnapshot(
CollectionField(
title=_("All participants of this team."),
@@ -1243,6 +1243,8 @@
"IPerson.inTeam()."),
value_type=Reference(schema=Interface))),
exported_as='participants')
+ allmembers = doNotSnapshot(
+ Attribute("List of all members, without checking karma etc."))
approvedmembers = doNotSnapshot(
Attribute("List of members with APPROVED status"))
deactivated_member_count = Attribute("Number of deactivated members")
@@ -1322,14 +1324,17 @@
center_lat, and center_lng
"""
- def getMembersWithPreferredEmails(include_teams=False):
+ def getMembersWithPreferredEmails():
"""Returns a result set of persons with precached addresses.
Persons or teams without preferred email addresses are not included.
"""
- def getMembersWithPreferredEmailsCount(include_teams=False):
- """Returns the count of persons/teams with preferred emails."""
+ def getMembersWithPreferredEmailsCount():
+ """Returns the count of persons/teams with preferred emails.
+
+ See also getMembersWithPreferredEmails.
+ """
def getDirectAdministrators():
"""Return this team's administrators.
@@ -2123,9 +2128,16 @@
# Fix value_type.schema of IPersonViewRestricted attributes.
-for name in ['allmembers', 'activemembers', 'adminmembers', 'proposedmembers',
- 'invited_members', 'deactivatedmembers', 'expiredmembers',
- 'unmapped_participants']:
+for name in [
+ 'all_members_prepopulated',
+ 'activemembers',
+ 'adminmembers',
+ 'proposedmembers',
+ 'invited_members',
+ 'deactivatedmembers',
+ 'expiredmembers',
+ 'unmapped_participants',
+ ]:
IPersonViewRestricted[name].value_type.schema = IPerson
IPersonPublic['sub_teams'].value_type.schema = ITeam
=== modified file 'lib/lp/registry/interfaces/poll.py'
--- lib/lp/registry/interfaces/poll.py 2009-08-24 14:18:10 +0000
+++ lib/lp/registry/interfaces/poll.py 2010-08-18 11:41:24 +0000
@@ -32,7 +32,7 @@
from canonical.launchpad import _
from canonical.launchpad.validators.name import name_validator
from lp.registry.interfaces.person import ITeam
-from canonical.launchpad.fields import ContentNameField
+from lp.services.fields import ContentNameField
class PollNameField(ContentNameField):
=== modified file 'lib/lp/registry/interfaces/product.py'
--- lib/lp/registry/interfaces/product.py 2010-08-06 14:49:35 +0000
+++ lib/lp/registry/interfaces/product.py 2010-08-18 11:41:24 +0000
@@ -34,7 +34,7 @@
from lazr.enum import DBEnumeratedType, DBItem
from canonical.launchpad import _
-from canonical.launchpad.fields import (
+from lp.services.fields import (
Description, IconImageUpload, LogoImageUpload, MugshotImageUpload,
PersonChoice, ProductBugTracker, ProductNameField,
PublicPersonChoice, Summary, Title, URIField)
@@ -74,6 +74,7 @@
ITranslationPolicy)
from canonical.launchpad.validators import LaunchpadValidationError
from canonical.launchpad.validators.name import name_validator
+from lazr.lifecycle.snapshot import doNotSnapshot
from lazr.restful.fields import CollectionField, Reference, ReferenceChoice
from lazr.restful.declarations import (
REQUEST_USER, call_with, collection_default_content,
@@ -585,7 +586,8 @@
"this product"))
series = exported(
- CollectionField(value_type=Object(schema=IProductSeries)))
+ doNotSnapshot(
+ CollectionField(value_type=Object(schema=IProductSeries))))
development_focus = exported(
ReferenceChoice(
@@ -602,10 +604,12 @@
"product; otherwise, simply returns the product name."))
releases = exported(
- CollectionField(
- title=_("An iterator over the ProductReleases for this product."),
- readonly=True,
- value_type=Reference(schema=IProductRelease)))
+ doNotSnapshot(
+ CollectionField(
+ title=_("An iterator over the ProductReleases for "
+ "this product."),
+ readonly=True,
+ value_type=Reference(schema=IProductRelease))))
translation_focus = exported(
ReferenceChoice(
=== modified file 'lib/lp/registry/interfaces/productrelease.py'
--- lib/lp/registry/interfaces/productrelease.py 2010-07-28 20:44:25 +0000
+++ lib/lp/registry/interfaces/productrelease.py 2010-08-18 11:41:24 +0000
@@ -27,7 +27,7 @@
from canonical.config import config
from canonical.launchpad import _
from canonical.launchpad.validators.version import sane_version
-from canonical.launchpad.fields import (
+from lp.services.fields import (
ContentNameField, PersonChoice)
from canonical.launchpad.validators import LaunchpadValidationError
=== modified file 'lib/lp/registry/interfaces/productseries.py'
--- lib/lp/registry/interfaces/productseries.py 2010-08-03 08:49:19 +0000
+++ lib/lp/registry/interfaces/productseries.py 2010-08-18 11:41:24 +0000
@@ -18,7 +18,7 @@
from zope.schema import Bool, Choice, Datetime, Int, TextLine
from zope.interface import Interface, Attribute
-from canonical.launchpad.fields import (
+from lp.services.fields import (
ContentNameField, PersonChoice, Title)
from lp.registry.interfaces.structuralsubscription import (
IStructuralSubscriptionTarget)
=== modified file 'lib/lp/registry/interfaces/projectgroup.py'
--- lib/lp/registry/interfaces/projectgroup.py 2010-05-24 13:54:12 +0000
+++ lib/lp/registry/interfaces/projectgroup.py 2010-08-18 11:41:24 +0000
@@ -18,7 +18,7 @@
from zope.schema import Bool, Datetime, Int, Object, Text, TextLine
from canonical.launchpad import _
-from canonical.launchpad.fields import (
+from lp.services.fields import (
PublicPersonChoice, Summary, Title, URIField)
from lp.app.interfaces.headings import IRootContext
from lp.code.interfaces.branchvisibilitypolicy import (
@@ -43,7 +43,7 @@
from lp.registry.interfaces.structuralsubscription import (
IStructuralSubscriptionTarget)
from canonical.launchpad.validators.name import name_validator
-from canonical.launchpad.fields import (
+from lp.services.fields import (
IconImageUpload, LogoImageUpload, MugshotImageUpload, PillarNameField)
from lazr.restful.fields import CollectionField, Reference, ReferenceChoice
=== modified file 'lib/lp/registry/interfaces/series.py'
--- lib/lp/registry/interfaces/series.py 2010-05-17 09:35:18 +0000
+++ lib/lp/registry/interfaces/series.py 2010-08-18 11:41:24 +0000
@@ -19,7 +19,7 @@
from lazr.restful.declarations import exported
from canonical.launchpad import _
-from canonical.launchpad.fields import (
+from lp.services.fields import (
PublicPersonChoice, Summary)
from canonical.launchpad.interfaces.launchpad import IHasDrivers
from lp.registry.interfaces.person import IPerson
=== modified file 'lib/lp/registry/interfaces/structuralsubscription.py'
--- lib/lp/registry/interfaces/structuralsubscription.py 2010-07-28 20:44:25 +0000
+++ lib/lp/registry/interfaces/structuralsubscription.py 2010-08-18 11:41:24 +0000
@@ -22,7 +22,7 @@
from lazr.enum import DBEnumeratedType, DBItem
from canonical.launchpad import _
-from canonical.launchpad.fields import (
+from lp.services.fields import (
PersonChoice, PublicPersonChoice)
from lp.registry.interfaces.person import IPerson
=== modified file 'lib/lp/registry/interfaces/wikiname.py'
--- lib/lp/registry/interfaces/wikiname.py 2009-07-17 00:26:05 +0000
+++ lib/lp/registry/interfaces/wikiname.py 2010-08-18 11:41:24 +0000
@@ -16,7 +16,7 @@
export_as_webservice_entry, exported)
from canonical.launchpad import _
-from canonical.launchpad.fields import URIField
+from lp.services.fields import URIField
from lp.registry.interfaces.role import IHasOwner
=== modified file 'lib/lp/registry/model/distribution.py'
--- lib/lp/registry/model/distribution.py 2010-08-11 02:17:14 +0000
+++ lib/lp/registry/model/distribution.py 2010-08-18 11:41:24 +0000
@@ -19,7 +19,7 @@
from zope.interface import alsoProvides, implements
from lp.archivepublisher.debversion import Version
-from canonical.cachedproperty import cachedproperty
+from canonical.cachedproperty import cachedproperty, clear_property
from canonical.database.constants import UTC_NOW
from canonical.database.datetimecol import UtcDateTimeCol
@@ -1617,8 +1617,9 @@
# This driver is a release manager.
series.driver = owner
- if safe_hasattr(self, '_cached_series'):
- del self._cached_series
+ # May wish to add this to the series rather than clearing the cache --
+ # RBC 20100816.
+ clear_property(self, '_cached_series')
return series
@property
=== modified file 'lib/lp/registry/model/distributionsourcepackage.py'
--- lib/lp/registry/model/distributionsourcepackage.py 2010-07-07 16:53:36 +0000
+++ lib/lp/registry/model/distributionsourcepackage.py 2010-08-18 11:41:24 +0000
@@ -33,6 +33,7 @@
from canonical.lazr.utils import smartquote
from lp.answers.interfaces.questiontarget import IQuestionTarget
from lp.bugs.interfaces.bugtarget import IHasBugHeat
+from lp.bugs.interfaces.bugtask import UNRESOLVED_BUGTASK_STATUSES
from lp.bugs.model.bug import Bug, BugSet, get_bug_tags_open_count
from lp.bugs.model.bugtarget import BugTargetBase, HasBugHeatMixin
from lp.bugs.model.bugtask import BugTask
@@ -56,6 +57,7 @@
from lp.translations.model.customlanguagecode import (
CustomLanguageCode, HasCustomLanguageCodesMixin)
+
def is_upstream_link_allowed(spph):
"""Metapackages shouldn't have upstream links.
@@ -67,6 +69,7 @@
class DistributionSourcePackageProperty:
+
def __init__(self, attrname):
self.attrname = attrname
@@ -90,8 +93,8 @@
SourcePackagePublishingHistory.sourcepackagereleaseID ==
SourcePackageRelease.id,
SourcePackageRelease.sourcepackagenameID ==
- obj.sourcepackagename.id
- ).order_by(Desc(SourcePackagePublishingHistory.id)).first()
+ obj.sourcepackagename.id).order_by(
+ Desc(SourcePackagePublishingHistory.id)).first()
obj._new(obj.distribution, obj.sourcepackagename,
is_upstream_link_allowed(spph))
setattr(obj._self_in_database, self.attrname, value)
@@ -160,6 +163,8 @@
@property
def summary(self):
"""See `IDistributionSourcePackage`."""
+ if self.development_version is None:
+ return None
return self.development_version.summary
@property
@@ -185,7 +190,8 @@
(Max(Bug.heat), Sum(Bug.heat), Count(Bug.id)),
BugTask.bug == Bug.id,
BugTask.distributionID == self.distribution.id,
- BugTask.sourcepackagenameID == self.sourcepackagename.id).one()
+ BugTask.sourcepackagenameID == self.sourcepackagename.id,
+ BugTask.status.is_in(UNRESOLVED_BUGTASK_STATUSES)).one()
# Aggregate functions return NULL if zero rows match.
row = list(row)
@@ -329,8 +335,7 @@
# Next, the joins for the ordering by soyuz karma of the
# SPR creator.
KarmaTotalCache.person == SourcePackageRelease.creatorID,
- *extra_args
- )
+ *extra_args)
# Note: If and when we later have a field on IArchive to order by,
# such as IArchive.rank, we will then be able to return distinct
@@ -352,8 +357,7 @@
condition = And(
Packaging.sourcepackagename == self.sourcepackagename,
Packaging.distroseriesID == DistroSeries.id,
- DistroSeries.distribution == self.distribution
- )
+ DistroSeries.distribution == self.distribution)
result = store.find(Packaging, condition)
result.order_by("debversion_sort_key(version) DESC")
if result.count() == 0:
@@ -518,8 +522,7 @@
DistributionSourcePackageInDatabase.sourcepackagename ==
sourcepackagename,
DistributionSourcePackageInDatabase.distribution ==
- distribution
- ).one()
+ distribution).one()
@classmethod
def _new(cls, distribution, sourcepackagename,
=== modified file 'lib/lp/registry/model/distroseries.py'
--- lib/lp/registry/model/distroseries.py 2010-08-10 21:54:41 +0000
+++ lib/lp/registry/model/distroseries.py 2010-08-18 11:41:24 +0000
@@ -31,7 +31,7 @@
from canonical.database.datetimecol import UtcDateTimeCol
from canonical.database.enumcol import EnumCol
from canonical.database.sqlbase import (
- cursor, flush_database_caches, flush_database_updates, quote_like,
+ flush_database_caches, flush_database_updates, quote_like,
quote, SQLBase, sqlvalues)
from canonical.launchpad.components.decoratedresultset import (
DecoratedResultSet)
@@ -40,7 +40,6 @@
POFileTranslator)
from lp.translations.model.pofile import POFile
from canonical.launchpad.interfaces.lpstorm import IStore
-from lp.soyuz.adapters.packagelocation import PackageLocation
from lp.soyuz.model.binarypackagename import BinaryPackageName
from lp.soyuz.model.binarypackagerelease import (
BinaryPackageRelease)
@@ -66,7 +65,6 @@
from lp.translations.model.languagepack import LanguagePack
from lp.registry.model.milestone import (
HasMilestonesMixin, Milestone)
-from lp.soyuz.model.packagecloner import clone_packages
from lp.registry.model.packaging import Packaging
from lp.registry.model.person import Person
from canonical.launchpad.database.librarian import LibraryFileAlias
@@ -90,7 +88,7 @@
HasTranslationImportsMixin)
from canonical.launchpad.helpers import shortlist
from lp.soyuz.interfaces.archive import (
- ALLOW_RELEASE_BUILDS, ArchivePurpose, IArchiveSet, MAIN_ARCHIVE_PURPOSES)
+ ALLOW_RELEASE_BUILDS, ArchivePurpose)
from lp.soyuz.interfaces.binarypackagebuild import IBinaryPackageBuildSet
from lp.soyuz.interfaces.buildrecords import IHasBuildRecords
from lp.soyuz.interfaces.binarypackagename import (
@@ -1594,175 +1592,6 @@
"""See BugTargetBase."""
return 'BugTask.distroseries = %s' % sqlvalues(self)
- def initialiseFromParent(self):
- """See `IDistroSeries`."""
- archives = self.distribution.all_distro_archive_ids
- assert self.parent_series is not None, "Parent series must be present"
- assert SourcePackagePublishingHistory.select("""
- Distroseries = %s AND
- Archive IN %s""" % sqlvalues(self.id, archives)).count() == 0, (
- "Source Publishing must be empty")
- for arch in self.architectures:
- assert BinaryPackagePublishingHistory.select("""
- DistroArchSeries = %s AND
- Archive IN %s""" % sqlvalues(arch, archives)).count() == 0, (
- "Binary Publishing must be empty")
- try:
- parent_arch = self.parent_series[arch.architecturetag]
- assert parent_arch.processorfamily == arch.processorfamily, (
- "The arch tags must match the processor families.")
- except KeyError:
- raise AssertionError("Parent series lacks %s" % (
- arch.architecturetag))
- assert self.nominatedarchindep is not None, (
- "Must have a nominated archindep architecture.")
- assert self.components.count() == 0, (
- "Component selections must be empty.")
- assert self.sections.count() == 0, (
- "Section selections must be empty.")
-
- # MAINTAINER: dsilvers: 20051031
- # Here we go underneath the SQLObject caching layers in order to
- # generate what will potentially be tens of thousands of rows
- # in various tables. Thus we flush pending updates from the SQLObject
- # layer, perform our work directly in the transaction and then throw
- # the rest of the SQLObject cache away to make sure it hasn't cached
- # anything that is no longer true.
-
- # Prepare for everything by flushing updates to the database.
- flush_database_updates()
- cur = cursor()
-
- # Perform the copies
- self._copy_component_section_and_format_selections(cur)
-
- # Prepare the list of distroarchseries for which binary packages
- # shall be copied.
- distroarchseries_list = []
- for arch in self.architectures:
- parent_arch = self.parent_series[arch.architecturetag]
- distroarchseries_list.append((parent_arch, arch))
- # Now copy source and binary packages.
- self._copy_publishing_records(distroarchseries_list)
- self._copy_lucille_config(cur)
- self._copy_packaging_links(cur)
-
- # Finally, flush the caches because we've altered stuff behind the
- # back of sqlobject.
- flush_database_caches()
-
- def _copy_lucille_config(self, cur):
- """Copy all lucille related configuration from our parent series."""
- cur.execute('''
- UPDATE DistroSeries SET lucilleconfig=(
- SELECT pdr.lucilleconfig FROM DistroSeries AS pdr
- WHERE pdr.id = %s)
- WHERE id = %s
- ''' % sqlvalues(self.parent_series.id, self.id))
-
- def _copy_publishing_records(self, distroarchseries_list):
- """Copy the publishing records from the parent arch series
- to the given arch series in ourselves.
-
- We copy all PENDING and PUBLISHED records as PENDING into our own
- publishing records.
-
- We copy only the RELEASE pocket in the PRIMARY and PARTNER
- archives.
- """
- archive_set = getUtility(IArchiveSet)
-
- for archive in self.parent_series.distribution.all_distro_archives:
- # We only want to copy PRIMARY and PARTNER archives.
- if archive.purpose not in MAIN_ARCHIVE_PURPOSES:
- continue
-
- # XXX cprov 20080612: Implicitly creating a PARTNER archive for
- # the destination distroseries is bad. Why are we copying
- # partner to a series in another distribution anyway ?
- # See bug #239807 for further information.
- target_archive = archive_set.getByDistroPurpose(
- self.distribution, archive.purpose)
- if target_archive is None:
- target_archive = archive_set.new(
- distribution=self.distribution, purpose=archive.purpose,
- owner=self.distribution.owner)
-
- origin = PackageLocation(
- archive, self.parent_series.distribution, self.parent_series,
- PackagePublishingPocket.RELEASE)
- destination = PackageLocation(
- target_archive, self.distribution, self,
- PackagePublishingPocket.RELEASE)
- clone_packages(origin, destination, distroarchseries_list)
-
- def _copy_component_section_and_format_selections(self, cur):
- """Copy the section, component and format selections from the parent
- distro series into this one.
- """
- # Copy the component selections
- cur.execute('''
- INSERT INTO ComponentSelection (distroseries, component)
- SELECT %s AS distroseries, cs.component AS component
- FROM ComponentSelection AS cs WHERE cs.distroseries = %s
- ''' % sqlvalues(self.id, self.parent_series.id))
- # Copy the section selections
- cur.execute('''
- INSERT INTO SectionSelection (distroseries, section)
- SELECT %s as distroseries, ss.section AS section
- FROM SectionSelection AS ss WHERE ss.distroseries = %s
- ''' % sqlvalues(self.id, self.parent_series.id))
- # Copy the source format selections
- cur.execute('''
- INSERT INTO SourcePackageFormatSelection (distroseries, format)
- SELECT %s as distroseries, spfs.format AS format
- FROM SourcePackageFormatSelection AS spfs
- WHERE spfs.distroseries = %s
- ''' % sqlvalues(self.id, self.parent_series.id))
-
- def _copy_packaging_links(self, cur):
- """Copy the packaging links from the parent series to this one."""
- cur.execute("""
- INSERT INTO
- Packaging(
- distroseries, sourcepackagename, productseries,
- packaging, owner)
- SELECT
- ChildSeries.id,
- Packaging.sourcepackagename,
- Packaging.productseries,
- Packaging.packaging,
- Packaging.owner
- FROM
- Packaging
- -- Joining the parent distroseries permits the query to build
- -- the data set for the series being updated, yet results are
- -- in fact the data from the original series.
- JOIN Distroseries ChildSeries
- ON Packaging.distroseries = ChildSeries.parent_series
- WHERE
- -- Select only the packaging links that are in the parent
- -- that are not in the child.
- ChildSeries.id = %s
- AND Packaging.sourcepackagename in (
- SELECT sourcepackagename
- FROM Packaging
- WHERE distroseries in (
- SELECT id
- FROM Distroseries
- WHERE id = ChildSeries.parent_series
- )
- EXCEPT
- SELECT sourcepackagename
- FROM Packaging
- WHERE distroseries in (
- SELECT id
- FROM Distroseries
- WHERE id = ChildSeries.id
- )
- )
- """ % self.id)
-
def copyTranslationsFromParent(self, transaction, logger=None):
"""See `IDistroSeries`."""
if logger is None:
=== modified file 'lib/lp/registry/model/person.py'
--- lib/lp/registry/model/person.py 2010-07-27 22:13:36 +0000
+++ lib/lp/registry/model/person.py 2010-08-18 11:41:24 +0000
@@ -48,7 +48,9 @@
StringCol)
from sqlobject.sqlbuilder import AND, OR, SQLConstant
from storm.store import EmptyResultSet, Store
-from storm.expr import And, In, Join, Lower, Not, Or, SQL
+from storm.expr import (
+ Alias, And, Exists, In, Join, LeftJoin, Lower, Min, Not, Or, Select, SQL,
+ )
from storm.info import ClassAlias
from canonical.config import config
@@ -59,10 +61,13 @@
from canonical.database.sqlbase import (
cursor, quote, quote_like, sqlvalues, SQLBase)
-from canonical.cachedproperty import cachedproperty
-
-from canonical.lazr.utils import get_current_browser_request, safe_hasattr
-
+from canonical.cachedproperty import (cachedproperty, cache_property,
+ clear_property)
+
+from canonical.lazr.utils import get_current_browser_request
+
+from canonical.launchpad.components.decoratedresultset import (
+ DecoratedResultSet)
from canonical.launchpad.database.account import Account, AccountPassword
from canonical.launchpad.interfaces.account import AccountSuspendedError
from lp.bugs.model.bugtarget import HasBugsBase
@@ -390,13 +395,12 @@
Order them by name if necessary.
"""
- self._languages_cache = sorted(
- languages, key=attrgetter('englishname'))
+ cache_property(self, '_languages_cache', sorted(
+ languages, key=attrgetter('englishname')))
def deleteLanguagesCache(self):
"""Delete this person's cached languages, if it exists."""
- if safe_hasattr(self, '_languages_cache'):
- del self._languages_cache
+ clear_property(self, '_languages_cache')
def addLanguage(self, language):
"""See `IPerson`."""
@@ -888,14 +892,16 @@
SELECT name, 3 as kind, displayname
FROM product
WHERE
- driver = %(person)s
- OR owner = %(person)s
+ active = True AND
+ (driver = %(person)s
+ OR owner = %(person)s)
UNION
SELECT name, 2 as kind, displayname
FROM project
WHERE
- driver = %(person)s
- OR owner = %(person)s
+ active = True AND
+ (driver = %(person)s
+ OR owner = %(person)s)
UNION
SELECT name, 1 as kind, displayname
FROM distribution
@@ -907,7 +913,12 @@
""" % sqlvalues(person=self))
results = IStore(self).using(origin).find(find_spec)
results = results.order_by('kind', 'displayname')
- return [pillar_name for pillar_name, kind, displayname in results]
+
+ def get_pillar_name(result):
+ pillar_name, kind, displayname = result
+ return pillar_name
+
+ return DecoratedResultSet(results, get_pillar_name)
def getOwnedProjects(self, match_name=None):
"""See `IPerson`."""
@@ -1012,9 +1023,10 @@
result = result.order_by(KarmaCategory.title)
return [karma_cache for (karma_cache, category) in result]
- @property
+ @cachedproperty('_karma_cached')
def karma(self):
"""See `IPerson`."""
+ # May also be loaded from _all_members
cache = KarmaTotalCache.selectOneBy(person=self)
if cache is None:
# Newly created accounts may not be in the cache yet, meaning the
@@ -1032,7 +1044,7 @@
return self.is_valid_person
- @property
+ @cachedproperty('_is_valid_person_cached')
def is_valid_person(self):
"""See `IPerson`."""
if self.is_team:
@@ -1431,14 +1443,134 @@
@property
def allmembers(self):
"""See `IPerson`."""
- query = """
- Person.id = TeamParticipation.person AND
- TeamParticipation.team = %s AND
- TeamParticipation.person != %s
- """ % sqlvalues(self.id, self.id)
- return Person.select(query, clauseTables=['TeamParticipation'])
-
- def _getMembersWithPreferredEmails(self, include_teams=False):
+ return self._all_members()
+
+ @property
+ def all_members_prepopulated(self):
+ """See `IPerson`."""
+ return self._all_members(need_karma=True, need_ubuntu_coc=True,
+ need_location=True, need_archive=True, need_preferred_email=True,
+ need_validity=True)
+
+ def _all_members(self, need_karma=False, need_ubuntu_coc=False,
+ need_location=False, need_archive=False, need_preferred_email=False,
+ need_validity=False):
+ """Lookup all members of the team with optional precaching.
+
+ :param need_karma: The karma attribute will be cached.
+ :param need_ubuntu_coc: The is_ubuntu_coc_signer attribute will be
+ cached.
+ :param need_location: The location attribute will be cached.
+ :param need_archive: The archive attribute will be cached.
+ :param need_preferred_email: The preferred email attribute will be
+ cached.
+ :param need_validity: The is_valid attribute will be cached.
+ """
+ # TODO: consolidate this with getMembersWithPreferredEmails.
+ # The difference between the two is that
+ # getMembersWithPreferredEmails includes self, which is arguably
+ # wrong, but perhaps deliberate.
+ store = Store.of(self)
+ origin = [
+ Person,
+ Join(TeamParticipation, TeamParticipation.person == Person.id),
+ ]
+ conditions = And(
+ # Members of this team,
+ TeamParticipation.team == self.id,
+ # But not the team itself.
+ TeamParticipation.person != self.id)
+ columns = [Person]
+ if need_karma:
+ # New people have no karmatotalcache rows.
+ origin.append(
+ LeftJoin(KarmaTotalCache,
+ KarmaTotalCache.person == Person.id))
+ columns.append(KarmaTotalCache)
+ if need_ubuntu_coc:
+ columns.append(Alias(Exists(Select(SignedCodeOfConduct,
+ AND(Person._is_ubuntu_coc_signer_condition(),
+ SignedCodeOfConduct.ownerID == Person.id))),
+ name='is_ubuntu_coc_signer'))
+ if need_location:
+ # New people have no location rows
+ origin.append(
+ LeftJoin(PersonLocation, PersonLocation.person == Person.id))
+ columns.append(PersonLocation)
+ if need_archive:
+ # Not everyone has PPA's
+ # It would be nice to cleanly expose the soyuz rules for this to
+ # avoid duplicating the relationships.
+ origin.append(
+ LeftJoin(Archive, Archive.owner == Person.id))
+ columns.append(Archive)
+ conditions = And(conditions,
+ Or(Archive.id == None, And(
+ Archive.id == Select(Min(Archive.id),
+ Archive.owner == Person.id, Archive),
+ Archive.purpose == ArchivePurpose.PPA)))
+ if need_preferred_email:
+ # Teams don't have email.
+ origin.append(
+ LeftJoin(EmailAddress, EmailAddress.person == Person.id))
+ columns.append(EmailAddress)
+ conditions = And(conditions,
+ Or(EmailAddress.status == None,
+ EmailAddress.status == EmailAddressStatus.PREFERRED))
+ if need_validity:
+ # May find invalid persons
+ origin.append(
+ LeftJoin(ValidPersonCache, ValidPersonCache.id == Person.id))
+ columns.append(ValidPersonCache)
+ if len(columns) == 1:
+ columns = columns[0]
+ # Return a simple ResultSet
+ return store.using(*origin).find(columns, conditions)
+ # Adapt the result into a cached Person.
+ columns = tuple(columns)
+ raw_result = store.using(*origin).find(columns, conditions)
+
+ def prepopulate_person(row):
+ result = row[0]
+ index = 1
+ #-- karma caching
+ if need_karma:
+ karma = row[index]
+ index += 1
+ if karma is None:
+ karma_total = 0
+ else:
+ karma_total = karma.karma_total
+ cache_property(result, '_karma_cached', karma_total)
+ #-- ubuntu code of conduct signer status caching.
+ if need_ubuntu_coc:
+ signed = row[index]
+ index += 1
+ cache_property(result, '_is_ubuntu_coc_signer_cached', signed)
+ #-- location caching
+ if need_location:
+ location = row[index]
+ index += 1
+ cache_property(result, '_location', location)
+ #-- archive caching
+ if need_archive:
+ archive = row[index]
+ index += 1
+ cache_property(result, '_archive_cached', archive)
+ #-- preferred email caching
+ if need_preferred_email:
+ email = row[index]
+ index += 1
+ cache_property(result, '_preferredemail_cached', email)
+ if need_validity:
+ valid = row[index] is not None
+ index += 1
+ cache_property(result, '_is_valid_person_cached', valid)
+ return result
+ return DecoratedResultSet(raw_result,
+ result_decorator=prepopulate_person)
+
+ def _getMembersWithPreferredEmails(self):
"""Helper method for public getMembersWithPreferredEmails.
We can't return the preferred email address directly to the
@@ -1456,20 +1588,18 @@
EmailAddress.status == EmailAddressStatus.PREFERRED)
return store.using(*origin).find((Person, EmailAddress), conditions)
- def getMembersWithPreferredEmails(self, include_teams=False):
+ def getMembersWithPreferredEmails(self):
"""See `IPerson`."""
- result = self._getMembersWithPreferredEmails(
- include_teams=include_teams)
+ result = self._getMembersWithPreferredEmails()
person_list = []
for person, email in result:
- person._preferredemail_cached = email
+ cache_property(person, '_preferredemail_cached', email)
person_list.append(person)
return person_list
- def getMembersWithPreferredEmailsCount(self, include_teams=False):
+ def getMembersWithPreferredEmailsCount(self):
"""See `IPerson`."""
- result = self._getMembersWithPreferredEmails(
- include_teams=include_teams)
+ result = self._getMembersWithPreferredEmails()
return result.count()
@property
@@ -1737,7 +1867,7 @@
self.account_status = AccountStatus.DEACTIVATED
self.account_status_comment = comment
IMasterObject(self.preferredemail).status = EmailAddressStatus.NEW
- self._preferredemail_cached = None
+ clear_property(self, '_preferredemail_cached')
base_new_name = self.name + '-deactivatedaccount'
self.name = self._ensureNewName(base_new_name)
@@ -1940,27 +2070,23 @@
@property
def teams_indirectly_participated_in(self):
"""See `IPerson`."""
- return Person.select("""
- -- we are looking for teams, so we want "people" that are on the
- -- teamparticipation.team side of teamparticipation
- Person.id = TeamParticipation.team AND
- -- where this person participates in the team
- TeamParticipation.person = %s AND
- -- but not the teamparticipation for "this person in himself"
- -- which exists for every person
- TeamParticipation.team != %s AND
- -- nor do we want teams in which the person is a direct
- -- participant, so we exclude the teams in which there is
- -- a teammembership for this person
- TeamParticipation.team NOT IN
- (SELECT TeamMembership.team FROM TeamMembership WHERE
- TeamMembership.person = %s AND
- TeamMembership.status IN (%s, %s))
- """ % sqlvalues(self.id, self.id, self.id,
- TeamMembershipStatus.APPROVED,
- TeamMembershipStatus.ADMIN),
- clauseTables=['TeamParticipation'],
- orderBy=Person.sortingColumns)
+ Team = ClassAlias(Person, "Team")
+ store = Store.of(self)
+ origin = [
+ Team,
+ Join(TeamParticipation, Team.id == TeamParticipation.teamID),
+ LeftJoin(TeamMembership,
+ And(TeamMembership.person == self.id,
+ TeamMembership.teamID == TeamParticipation.teamID,
+ TeamMembership.status.is_in([
+ TeamMembershipStatus.APPROVED,
+ TeamMembershipStatus.ADMIN])))]
+ find_objects = (Team)
+ return store.using(*origin).find(find_objects,
+ And(
+ TeamParticipation.person == self.id,
+ TeamParticipation.person != TeamParticipation.teamID,
+ TeamMembership.id == None))
@property
def teams_with_icons(self):
@@ -2059,6 +2185,10 @@
all_addresses = IMasterStore(self).find(
EmailAddress, EmailAddress.personID == self.id)
for address in all_addresses:
+ # Delete all email addresses that are not the preferred email
+ # address, or the team's email address. If this method was called
+ # with None, and there is no mailing list, then this condidition
+ # is (None, None), causing all email addresses to be deleted.
if address not in (email, mailing_list_email):
address.destroySelf()
@@ -2070,7 +2200,7 @@
if email_address is not None:
email_address.status = EmailAddressStatus.VALIDATED
email_address.syncUpdate()
- self._preferredemail_cached = None
+ clear_property(self, '_preferredemail_cached')
def setPreferredEmail(self, email):
"""See `IPerson`."""
@@ -2107,7 +2237,7 @@
IMasterObject(email).syncUpdate()
# Now we update our cache of the preferredemail.
- self._preferredemail_cached = email
+ cache_property(self, '_preferredemail_cached', email)
@cachedproperty('_preferredemail_cached')
def preferredemail(self):
@@ -2274,17 +2404,23 @@
distribution.main_archive, self)
return permissions.count() > 0
- @cachedproperty
+ @cachedproperty('_is_ubuntu_coc_signer_cached')
def is_ubuntu_coc_signer(self):
"""See `IPerson`."""
+ # Also assigned to by self._all_members.
+ store = Store.of(self)
+ query = AND(SignedCodeOfConduct.ownerID == self.id,
+ Person._is_ubuntu_coc_signer_condition())
+ # TODO: Using exists would be faster than count().
+ return bool(store.find(SignedCodeOfConduct, query).count())
+
+ @staticmethod
+ def _is_ubuntu_coc_signer_condition():
+ """Generate a Storm Expr for determing the coc signing status."""
sigset = getUtility(ISignedCodeOfConductSet)
lastdate = sigset.getLastAcceptedDate()
-
- query = AND(SignedCodeOfConduct.q.active==True,
- SignedCodeOfConduct.q.ownerID==self.id,
- SignedCodeOfConduct.q.datecreated>=lastdate)
-
- return bool(SignedCodeOfConduct.select(query).count())
+ return AND(SignedCodeOfConduct.active == True,
+ SignedCodeOfConduct.datecreated >= lastdate)
@property
def activesignatures(self):
@@ -2298,7 +2434,7 @@
sCoC_util = getUtility(ISignedCodeOfConductSet)
return sCoC_util.searchByUser(self.id, active=False)
- @property
+ @cachedproperty('_archive_cached')
def archive(self):
"""See `IPerson`."""
return getUtility(IArchiveSet).getPPAOwnedByPerson(self)
@@ -2622,7 +2758,8 @@
# Populate the previously empty 'preferredemail' cached
# property, so the Person record is up-to-date.
if master_email.status == EmailAddressStatus.PREFERRED:
- account_person._preferredemail_cached = master_email
+ cache_property(account_person, '_preferredemail_cached',
+ master_email)
return account_person
# There is no associated `Person` to the email `Account`.
# This is probably because the account was created externally
=== modified file 'lib/lp/registry/model/product.py'
--- lib/lp/registry/model/product.py 2010-08-06 14:49:35 +0000
+++ lib/lp/registry/model/product.py 2010-08-18 11:41:24 +0000
@@ -509,9 +509,8 @@
def __storm_invalidated__(self):
"""Clear cached non-storm attributes when the transaction ends."""
+ super(Product, self).__storm_invalidated__()
self._cached_licenses = None
- if safe_hasattr(self, '_commercial_subscription_cached'):
- del self._commercial_subscription_cached
def _getLicenses(self):
"""Get the licenses as a tuple."""
=== modified file 'lib/lp/registry/model/sourcepackage.py'
--- lib/lp/registry/model/sourcepackage.py 2010-08-02 21:38:00 +0000
+++ lib/lp/registry/model/sourcepackage.py 2010-08-18 11:41:24 +0000
@@ -39,7 +39,6 @@
from lp.registry.model.packaging import Packaging
from lp.translations.model.potemplate import (
HasTranslationTemplatesMixin,
- POTemplate,
TranslationTemplatesCollection)
from canonical.launchpad.interfaces.lpstorm import IStore
from lp.soyuz.model.publishing import (
@@ -52,7 +51,6 @@
SourcePackageRelease)
from lp.translations.model.translationimportqueue import (
HasTranslationImportsMixin)
-from canonical.launchpad.helpers import shortlist
from lp.soyuz.interfaces.buildrecords import IHasBuildRecords
from lp.registry.interfaces.packaging import PackagingType
from lp.registry.interfaces.distribution import NoPartnerArchive
=== modified file 'lib/lp/registry/model/teammembership.py'
--- lib/lp/registry/model/teammembership.py 2010-07-16 14:58:17 +0000
+++ lib/lp/registry/model/teammembership.py 2010-08-18 11:41:24 +0000
@@ -11,7 +11,6 @@
]
from datetime import datetime, timedelta
-import itertools
import pytz
from storm.locals import Store
@@ -167,8 +166,10 @@
simple_sendmail(from_addr, address, subject, msg)
def canChangeStatusSilently(self, user):
- """Ensure that the user is in the Launchpad Administrators group before
- silently making changes to their membership status."""
+ """Ensure that the user is in the Launchpad Administrators group.
+
+ Then the user can silently make changes to their membership status.
+ """
return user.inTeam(getUtility(ILaunchpadCelebrities).admin)
def canChangeExpirationDate(self, person):
@@ -288,8 +289,8 @@
if silent and not self.canChangeStatusSilently(user):
raise UserCannotChangeMembershipSilently(
- "Only Launchpad administrators may change membership statuses "
- "silently.")
+ "Only Launchpad administrators may change membership "
+ "statuses silently.")
approved = TeamMembershipStatus.APPROVED
admin = TeamMembershipStatus.ADMIN
@@ -536,7 +537,7 @@
conditions = [
TeamMembership.dateexpires <= when,
TeamMembership.status.is_in(
- [TeamMembershipStatus.ADMIN, TeamMembershipStatus.APPROVED])
+ [TeamMembershipStatus.ADMIN, TeamMembershipStatus.APPROVED]),
]
if exclude_autorenewals:
# Avoid circular import.
@@ -718,20 +719,48 @@
return Store.of(person).find(Person, "id IN (%s)" % query)
-def _fillTeamParticipation(member, team):
+def _fillTeamParticipation(member, accepting_team):
"""Add relevant entries in TeamParticipation for given member and team.
Add a tuple "member, team" in TeamParticipation for the given team and all
of its superteams. More information on how to use the TeamParticipation
table can be found in the TeamParticipationUsage spec.
"""
- members = [member]
if member.isTeam():
- # The given member is, in fact, a team, and in this case we must
- # add all of its members to the given team and to its superteams.
- members.extend(member.allmembers)
+ # The submembers will be all the members of the team that is
+ # being added as a member. The superteams will be all the teams
+ # that the accepting_team belongs to, so all the members will
+ # also be joining the superteams indirectly. It is important to
+ # remember that teams are members of themselves, so the member
+ # team will also be one of the submembers, and the
+ # accepting_team will also be one of the superteams.
+ query = """
+ INSERT INTO TeamParticipation (person, team)
+ SELECT submember.person, superteam.team
+ FROM TeamParticipation submember
+ JOIN TeamParticipation superteam ON TRUE
+ WHERE submember.team = %(member)d
+ AND superteam.person = %(accepting_team)d
+ AND NOT EXISTS (
+ SELECT 1
+ FROM TeamParticipation
+ WHERE person = submember.person
+ AND team = superteam.team
+ )
+ """ % dict(member=member.id, accepting_team=accepting_team.id)
+ else:
+ query = """
+ INSERT INTO TeamParticipation (person, team)
+ SELECT %(member)d, superteam.team
+ FROM TeamParticipation superteam
+ WHERE superteam.person = %(accepting_team)d
+ AND NOT EXISTS (
+ SELECT 1
+ FROM TeamParticipation
+ WHERE person = %(member)d
+ AND team = superteam.team
+ )
+ """ % dict(member=member.id, accepting_team=accepting_team.id)
- for m in members:
- for t in itertools.chain(team.super_teams, [team]):
- if not m.hasParticipationEntryFor(t):
- TeamParticipation(person=m, team=t)
+ store = Store.of(member)
+ store.execute(query)
=== modified file 'lib/lp/registry/stories/packaging/xx-sourcepackage-packaging.txt'
--- lib/lp/registry/stories/packaging/xx-sourcepackage-packaging.txt 2010-05-18 17:05:29 +0000
+++ lib/lp/registry/stories/packaging/xx-sourcepackage-packaging.txt 2010-08-18 11:41:24 +0000
@@ -1,4 +1,15 @@
-= Packaging =
+Packaging
+=========
+
+Create test data.
+
+ >>> from lp.soyuz.tests.test_publishing import SoyuzTestPublisher
+ >>> test_publisher = SoyuzTestPublisher()
+ >>> login('admin@xxxxxxxxxxxxx')
+ >>> test_data = test_publisher.makeSourcePackageWithBinaryPackageRelease()
+ >>> test_publisher.updateDistroSeriesPackageCache(
+ ... test_data['distroseries'])
+ >>> logout()
No Privileges Person visit the distroseries upstream links page for Hoary
and sees that pmount is not linked.
@@ -20,6 +31,7 @@
match for this source package. Can you help us find one?
Registered upstream project:
Choose another upstream project
+ Register the upstream project
No Privileges Person knows that the pmount package comes from the thunderbird
project. He sets the upstream packaging link and sees that it is set.
@@ -58,3 +70,93 @@
... user_browser.contents, 'packages_list'))
The Hoary Hedgehog Release (active development) ...
0.1-2 release (main) ... weeks ago
+
+Register a project from a source package
+----------------------------------------
+
+If an upstream project doesn't already exist in Launchpad, it can
+be registered with data from the source package prefilling the first
+step of the multistep form.
+
+ >>> user_browser.open(
+ ... 'http://launchpad.dev/youbuntu/busy/+source/bonkers')
+ >>> user_browser.getControl(
+ ... 'Register the upstream project').selected = True
+ >>> user_browser.getControl("Link to Upstream Project").click()
+ >>> print user_browser.url.replace('&', '\n&')
+ http://launchpad.dev/projects/+new?_return_url=http...%2Bsource%2Fbonkers
+ &field.__visited_steps__=projectaddstep1
+ &field.actions.continue=Continue
+ &field.displayname=Bonkers
+ &field.distroseries=youbuntu%2Fbusy
+ &field.name=bonkers
+ &field.source_package_name=bonkers
+ &field.summary=summary+for+flubber-bin%0Asummary+for+flubber-lib
+ &field.title=Bonkers
+ >>> print user_browser.getControl(name='field.name').value
+ bonkers
+ >>> print user_browser.getControl(name='field.displayname').value
+ Bonkers
+ >>> print user_browser.getControl(name='field.title').value
+ Bonkers
+ >>> print user_browser.getControl(name='field.summary').value
+ summary for flubber-bin
+ summary for flubber-lib
+ >>> print extract_text(
+ ... find_tag_by_id(user_browser.contents, 'step-title'))
+ Step 2 (of 2): Check for duplicate projects
+
+If the user selects "Choose another upstream project" and then finds out
+that the project doesn't exist, there is a also a link on the
++edit-packaging page to register the project.
+
+ >>> user_browser.open(
+ ... 'http://launchpad.dev/youbuntu/busy/+source/bonkers/')
+ >>> user_browser.getControl(
+ ... 'Choose another upstream project').selected = True
+ >>> user_browser.getControl("Link to Upstream Project").click()
+ >>> print user_browser.url
+ http://launchpad.dev/youbuntu/busy/+source/bonkers/+edit-packaging
+
+ >>> user_browser.getLink("Register the upstream project").click()
+ >>> print user_browser.url.replace('&', '\n&')
+ http://launchpad.dev/projects/+new?_return_url=http...%2Bsource%2Fbonkers
+ &field.__visited_steps__=projectaddstep1
+ &field.actions.continue=Continue
+ &field.displayname=Bonkers
+ &field.distroseries=youbuntu%2Fbusy
+ &field.name=bonkers
+ &field.source_package_name=bonkers
+ &field.summary=summary+for+flubber-bin%0Asummary+for+flubber-lib
+ &field.title=Bonkers
+ >>> print user_browser.getControl(name='field.name').value
+ bonkers
+ >>> print user_browser.getControl(name='field.displayname').value
+ Bonkers
+ >>> print user_browser.getControl(name='field.title').value
+ Bonkers
+ >>> print user_browser.getControl(name='field.summary').value
+ summary for flubber-bin
+ summary for flubber-lib
+ >>> print extract_text(
+ ... find_tag_by_id(user_browser.contents, 'step-title'))
+ Step 2 (of 2): Check for duplicate projects
+
+If there are no problems with the prefilled data, then the license
+just needs to be selected. The user will then be redirected back
+to the source package page and an informational message will be displayed.
+
+ >>> user_browser.getControl(name='field.licenses').value = ['BSD']
+ >>> user_browser.getControl(
+ ... "Complete registration and link to bonkers package").click()
+ >>> print user_browser.url
+ http://launchpad.dev/youbuntu/busy/+source/bonkers
+ >>> for tag in find_tags_by_class(
+ ... user_browser.contents, 'informational message'):
+ ... print extract_text(tag)
+ Linked Bonkers project to bonkers source package.
+ >>> print extract_text(
+ ... find_tag_by_id(user_browser.contents, 'upstreams'))
+ Bonkers ⇒ trunk
+ Change upstream link
+ Remove upstream link...
=== modified file 'lib/lp/registry/stories/person/xx-person-edit-jabber-ids.txt'
--- lib/lp/registry/stories/person/xx-person-edit-jabber-ids.txt 2009-11-07 04:30:07 +0000
+++ lib/lp/registry/stories/person/xx-person-edit-jabber-ids.txt 2010-08-18 11:41:24 +0000
@@ -1,8 +1,12 @@
-= Jabber IDs =
+==========
+Jabber IDs
+==========
+
In their Launchpad profile, users can register their Jabber IDs.
-== Adding and editing an ID ==
+Adding and editing an ID
+------------------------
To register a Jabber ID with his account, the user visits his
profile page and uses the 'Update Jabber IDs' link.
@@ -15,50 +19,46 @@
The user enters the Jabber ID in the text field and clicks on the
'Save Changes' button.
- >>> user_browser.getControl(name='newjabberid').value = 'jeff@xxxxxxxxxx'
+ >>> user_browser.getControl(name='field.jabberid').value = (
+ ... 'jeff@xxxxxxxxxx')
>>> user_browser.getControl('Save Changes').click()
In this case, the user tried registering a jabber ID that was already
registered by someone else. Since only one person can use a Jabber ID,
an error is displayed and the user can enter another one:
- >>> for error in find_tags_by_class(user_browser.contents, 'error'):
- ... print extract_text(error)
+ >>> def show_errors(browser):
+ ... for error in find_tags_by_class(browser.contents, 'error'):
+ ... print extract_text(error)
+ >>> show_errors(user_browser)
+ There is 1 error.
+ New Jabber user ID:
The Jabber ID jeff@xxxxxxxxxx is already registered by Jeff Waugh.
However, if the user enters a Jabber ID which isn't already registered,
it will be associated with his account.
- >>> user_browser.getControl(name='newjabberid').value = 'nopriv@xxxxxxxxxx'
- >>> user_browser.getControl('Save Changes').click()
-
-The new ID can be edited by changing its value, and another ID can
-also be entered.
-
- >>> user_browser.getControl(name='jabberid_nopriv@xxxxxxxxxx').value = (
+ >>> user_browser.getControl(name='field.jabberid').value = (
... 'no-priv@xxxxxxxxxx')
- >>> user_browser.getControl(name='newjabberid').value = (
- ... 'no-priv@xxxxxxxxx')
>>> user_browser.getControl('Save Changes').click()
-
- >>> import re
- >>> main_content = find_main_content(user_browser.contents)
- >>> for input in main_content.findAll(
- ... 'input', {'name': re.compile('jabberid_')}):
- ... print input['value']
- no-priv@xxxxxxxxx
+ >>> show_errors(user_browser)
+
+ >>> def show_jabberids(browser):
+ ... tags = find_tag_by_id(browser.contents, 'jabber-ids')
+ ... for dd in tags.findAll('dd'):
+ ... print extract_text(dd)
+
+ >>> show_jabberids(user_browser)
no-priv@xxxxxxxxxx
-== Removing an ID ==
+Removing an ID
+--------------
-To remove an existing Jabber ID, the user simply check the 'Remove'
+To remove an existing Jabber ID, the user simply checks the 'Remove'
checkbox besides the ID:
-
+ >>> user_browser.getLink('Update Jabber IDs').click()
>>> user_browser.getControl('Remove', index=0).click()
>>> user_browser.getControl('Save Changes').click()
- >>> main_content = find_main_content(user_browser.contents)
- >>> for input in main_content.findAll(
- ... 'input', {'name': re.compile('jabberid_')}):
- ... print input['value']
- no-priv@xxxxxxxxxx
+ >>> show_jabberids(user_browser)
+ No Jabber IDs registered.
=== modified file 'lib/lp/registry/stories/person/xx-person-projects.txt'
--- lib/lp/registry/stories/person/xx-person-projects.txt 2010-07-14 16:27:33 +0000
+++ lib/lp/registry/stories/person/xx-person-projects.txt 2010-08-18 11:41:24 +0000
@@ -9,8 +9,8 @@
... anon_browser.contents, 'portlet-related-projects')
>>> for tr in related_projects.findAll('tr'):
... print extract_text(tr)
- Ubuntu Linux
- Ubuntu Test
+ Ubuntu
+ ubuntutest
Tomcat
The +related-software page displays a table with links to open bugs,
=== modified file 'lib/lp/registry/stories/person/xx-user-to-user.txt'
--- lib/lp/registry/stories/person/xx-user-to-user.txt 2010-07-14 21:19:42 +0000
+++ lib/lp/registry/stories/person/xx-user-to-user.txt 2010-08-18 11:41:24 +0000
@@ -38,24 +38,7 @@
From: No Privileges Person <no-priv@xxxxxxxxxxxxx>
To: Guilherme Salgado <guilherme.salgado@xxxxxxxxxxxxx>
Subject: Hi Salgado
- Message-ID: ...
- X-Launchpad-Message-Rationale: ContactViaWeb user
- Date: ...
- Reply-To: No Privileges Person <no-priv@xxxxxxxxxxxxx>
- Sender: bounces@xxxxxxxxxxxxx
- Errors-To: bounces@xxxxxxxxxxxxx
- Return-Path: bounces@xxxxxxxxxxxxx
- X-Generated-By: Launchpad (canonical.com); Revision="...";
- Instance="launchpad-lazr.conf"
- X-Launchpad-Hash: ...
- <BLANKLINE>
- Just saying hello
- --
- This message was sent from Launchpad by the user
- No Privileges Person (http://launchpad.dev/~no-priv)
- using the "Contact this user" link on your profile page.
- For more information see
- https://help.launchpad.net/YourAccount/ContactingPeople
+ ...
No Priv starts to send another message to Salgado, but then changes her mind.
@@ -97,69 +80,4 @@
From: Sample Person <testing@xxxxxxxxxxxxx>
To: Guilherme Salgado <guilherme.salgado@xxxxxxxxxxxxx>
Subject: Hi Salgado
- Message-ID: ...
- X-Launchpad-Message-Rationale: ContactViaWeb user
- Date: ...
- Reply-To: Sample Person <testing@xxxxxxxxxxxxx>
- Sender: bounces@xxxxxxxxxxxxx
- Errors-To: bounces@xxxxxxxxxxxxx
- Return-Path: bounces@xxxxxxxxxxxxx
- X-Generated-By: Launchpad (canonical.com); Revision="...";
- Instance="launchpad-lazr.conf"
- X-Launchpad-Hash: ...
- <BLANKLINE>
- Hello again
- --
- This message was sent from Launchpad by the user
- Sample Person (http://launchpad.dev/~name12)
- using the "Contact this user" link on your profile page.
- For more information see
- https://help.launchpad.net/YourAccount/ContactingPeople
-
-
-Bypassing the quota
-===================
-
-Salgado is the Launchpad Community Help Rotation expert for the day and he
-needs to contact several people. As a convenience, he opens multiple browser
-pages for each person he wants to contact. Then he fills out the text and
-begins to send the messages.
-
-However, Salgado has a quota limit of 3 so while he can open four +contactuser
-pages, when he tries to submit the fourth one, the submission fails.
-
- >>> browser_1 = setupBrowser(auth='Basic salgado@xxxxxxxxxx:zeca')
- >>> browser_2 = setupBrowser(auth='Basic salgado@xxxxxxxxxx:zeca')
- >>> browser_3 = setupBrowser(auth='Basic salgado@xxxxxxxxxx:zeca')
- >>> browser_4 = setupBrowser(auth='Basic salgado@xxxxxxxxxx:zeca')
-
- >>> browser_1.open('http://launchpad.dev/~no-priv/+contactuser')
- >>> browser_2.open('http://launchpad.dev/~carlos/+contactuser')
- >>> browser_3.open('http://launchpad.dev/~daf/+contactuser')
- >>> browser_4.open('http://launchpad.dev/~name12/+contactuser')
-
- >>> browser_1.getControl('Subject').value = 'One'
- >>> browser_1.getControl('Message').value = 'One'
-
- >>> browser_2.getControl('Subject').value = 'Two'
- >>> browser_2.getControl('Message').value = 'Two'
-
- >>> browser_3.getControl('Subject').value = 'Three'
- >>> browser_3.getControl('Message').value = 'Three'
-
- >>> browser_4.getControl('Subject').value = 'Four'
- >>> browser_4.getControl('Message').value = 'Four'
-
- >>> browser_1.getControl('Send').click()
- >>> print_errors(browser_1.contents)
-
- >>> browser_2.getControl('Send').click()
- >>> print_errors(browser_2.contents)
-
- >>> browser_3.getControl('Send').click()
- >>> print_errors(browser_3.contents)
-
- >>> browser_4.getControl('Send').click()
- >>> print_errors(browser_4.contents)
- Your message was not sent because you have exceeded your daily quota of 3
- messages to contact users. Try again in ... hours.
+ ...
=== modified file 'lib/lp/registry/templates/person-editjabberids.pt'
--- lib/lp/registry/templates/person-editjabberids.pt 2009-09-01 19:30:03 +0000
+++ lib/lp/registry/templates/person-editjabberids.pt 2010-08-18 11:41:24 +0000
@@ -13,17 +13,15 @@
<table>
- <div tal:condition="context/jabberids">
+ <tal:existing_jabber condition="context/jabberids">
<tr>
<td><label>Existing Jabber IDs:</label></td>
</tr>
<tr tal:repeat="jabber context/jabberids">
- <td>
- <input tal:attributes="name string:jabberid_${jabber/jabberid};
- value jabber/jabberid"
- type="text" style="margin-bottom: 0.5em;"/>
+ <td tal:content="jabber/jabberid"
+ class="jabberid">
</td>
<td>
@@ -34,23 +32,16 @@
</label>
</td>
</tr>
-
- </div>
-
- <tr tal:condition="context/jabberids">
- <td><label>New Jabber ID:</label></td>
- </tr>
-
- <tr>
- <td>
- <input name="newjabberid" type="text" />
- </td>
- </tr>
-
- <tr>
- <td class="formHelp">Example: yourname@xxxxxxxxxx</td>
- </tr>
- </table>
+ </tal:existing_jabber>
+
+ <tal:widget define="widget nocall:view/widgets/jabberid">
+ <metal:block use-macro="context/@@launchpad_form/widget_row" />
+ </tal:widget>
+
+ <tr>
+ <td class="formHelp">Example: yourname@xxxxxxxxxx</td>
+ </tr>
+ </table>
</div>
</div>
=== modified file 'lib/lp/registry/templates/person-participation.pt'
--- lib/lp/registry/templates/person-participation.pt 2010-06-24 18:27:11 +0000
+++ lib/lp/registry/templates/person-participation.pt 2010-08-18 11:41:24 +0000
@@ -39,7 +39,7 @@
</td>
<td>
<tal:date condition="not: participation/via"
- tal:replace="participation/membership/datejoined/fmt:date">
+ tal:replace="participation/datejoined/fmt:date">
2005-06-17
</tal:date>
<tal:no-date condition="participation/via">
@@ -60,13 +60,9 @@
</td>
<td
tal:condition="not: context/teamowner">
- <tal:subscribed condition="participation/team/mailing_list"
- replace="participation/subscribed">
+ <tal:subscribed replace="structure participation/subscribed">
yes
</tal:subscribed>
- <tal:no-list condition="not: participation/team/mailing_list">
- —
- </tal:no-list>
</td>
</tr>
</tbody>
=== modified file 'lib/lp/registry/templates/person-portlet-contact-details.pt'
--- lib/lp/registry/templates/person-portlet-contact-details.pt 2010-08-03 20:58:54 +0000
+++ lib/lp/registry/templates/person-portlet-contact-details.pt 2010-08-18 11:41:24 +0000
@@ -83,7 +83,8 @@
<a tal:repeat="team context/@@+restricted-membership/teams_with_icons"
tal:attributes="href team/fmt:url"
><img tal:attributes="src team/icon/getURL;
- title string:Icon of ${team/name}"
+ title string:Member of ${team/displayname};
+ alt string:Icon of ${team/displayname}"
/></a>
</div>
=== modified file 'lib/lp/registry/templates/person-portlet-related-projects.pt'
--- lib/lp/registry/templates/person-portlet-related-projects.pt 2009-08-25 02:01:55 +0000
+++ lib/lp/registry/templates/person-portlet-related-projects.pt 2010-08-18 11:41:24 +0000
@@ -25,9 +25,7 @@
<tbody>
<tr tal:repeat="pillarname pillarnames">
<td>
- <a tal:attributes="href string:/${pillarname/pillar/name};
- class pillarname/pillar/image:sprite_css"
- tal:content="pillarname/pillar/title">Pillar title</a>
+ <a tal:replace="structure pillarname/pillar/fmt:link" />
</td>
</tr>
</tbody>
=== modified file 'lib/lp/registry/templates/product-new.pt'
--- lib/lp/registry/templates/product-new.pt 2010-05-12 19:06:17 +0000
+++ lib/lp/registry/templates/product-new.pt 2010-08-18 11:41:24 +0000
@@ -299,8 +299,8 @@
<img src="/@@/info" />
There are similar projects already registered in Launchpad.
Is project
- <strong><tal:displayname tal:replace="view/request/displayname" />
- (<tal:name tal:replace="view/request/name" />)</strong>
+ <strong><tal:displayname tal:replace="view/request/field.displayname" />
+ (<tal:name tal:replace="view/request/field.name" />)</strong>
one of these?
</div>
@@ -326,8 +326,8 @@
tal:condition="view/search_results_count"
>Registration details</h3>
Select the licenses for project
- <strong><tal:displayname tal:replace="view/request/displayname" />
- (<tal:name tal:replace="view/request/name" />)</strong>
+ <strong><tal:displayname tal:replace="view/request/field.displayname" />
+ (<tal:name tal:replace="view/request/field.name" />)</strong>
and complete the registration. You may also update the project's
title and summary.
</div>
=== modified file 'lib/lp/registry/templates/sourcepackage-edit-packaging.pt'
--- lib/lp/registry/templates/sourcepackage-edit-packaging.pt 2010-02-16 17:37:36 +0000
+++ lib/lp/registry/templates/sourcepackage-edit-packaging.pt 2010-08-18 11:41:24 +0000
@@ -27,6 +27,26 @@
If you need a new series created, contact the owner of
<a tal:content="structure view/product/fmt:link"/>.
</div>
+
+ <div metal:fill-slot="buttons">
+ <input tal:repeat="action view/actions"
+ tal:replace="structure action/render"
+ />
+ or
+ <tal:comment condition="nothing">
+ This template is for a multistep view, and only the first
+ step provides the register_upstream_url.
+ </tal:comment>
+ <a id="register-upstream-link"
+ tal:condition="view/register_upstream_url | nothing"
+ tal:attributes="href view/register_upstream_url">
+ Register the upstream project
+ </a>
+ <tal:has-cancel-link condition="view/cancel_url">
+ or
+ <a tal:attributes="href view/cancel_url">Cancel</a>
+ </tal:has-cancel-link>
+ </div>
</div>
</div>
=== modified file 'lib/lp/registry/templates/team-portlet-mailinglist.pt'
--- lib/lp/registry/templates/team-portlet-mailinglist.pt 2010-06-04 15:37:24 +0000
+++ lib/lp/registry/templates/team-portlet-mailinglist.pt 2010-08-18 11:41:24 +0000
@@ -19,16 +19,16 @@
<strong>Policy:</strong>
You must be a team member to subscribe to the team mailing list.
<br/>
- <tal:member condition="view/user_is_active_member">
+ <tal:member condition="view/userIsParticipant">
<tal:can-subscribe-to-list
condition="view/user_can_subscribe_to_list">
- <a class="sprite add"
+ <a id="link-list-subscribe" class="sprite add"
href="/people/+me/+editemails">Subscribe to mailing list</a>
<br />
</tal:can-subscribe-to-list>
<tal:subscribed-to-list
condition="view/user_is_subscribed_to_list">
- <form id="form.list.unsubscribe" name="unsubscribe"
+ <form id="form-list-unsubscribe" name="unsubscribe"
action="" method="post">
<input type="submit" name="unsubscribe" value="Unsubscribe" />
</form>
=== modified file 'lib/lp/registry/tests/test_distribution.py'
--- lib/lp/registry/tests/test_distribution.py 2010-08-03 15:15:16 +0000
+++ lib/lp/registry/tests/test_distribution.py 2010-08-18 11:41:24 +0000
@@ -7,10 +7,13 @@
from zope.security.proxy import removeSecurityProxy
+from lazr.lifecycle.snapshot import Snapshot
from lp.registry.tests.test_distroseries import (
TestDistroSeriesCurrentSourceReleases)
from lp.registry.interfaces.distroseries import NoSuchDistroSeries
from lp.registry.interfaces.series import SeriesStatus
+from lp.registry.interfaces.distribution import IDistribution
+
from lp.soyuz.interfaces.distributionsourcepackagerelease import (
IDistributionSourcePackageRelease)
from lp.testing import TestCaseWithFactory
@@ -140,3 +143,31 @@
series = self.factory.makeDistroSeries(distribution=distro,
name="dappere", version="42.6")
self.assertEquals(series, distro.getSeries("42.6"))
+
+
+class DistroSnapshotTestCase(TestCaseWithFactory):
+ """A TestCase for distribution snapshots."""
+
+ layer = LaunchpadFunctionalLayer
+
+ def setUp(self):
+ super(DistroSnapshotTestCase, self).setUp()
+ self.distribution = self.factory.makeDistribution(name="boobuntu")
+
+ def test_snapshot(self):
+ """Snapshots of products should not include marked attribues.
+
+ Wrap an export with 'doNotSnapshot' to force the snapshot to not
+ include that attribute.
+ """
+ snapshot = Snapshot(self.distribution, providing=IDistribution)
+ omitted = [
+ 'archive_mirrors',
+ 'cdimage_mirrors',
+ 'series',
+ 'all_distro_archives',
+ ]
+ for attribute in omitted:
+ self.assertFalse(
+ hasattr(snapshot, attribute),
+ "Snapshot should not include %s." % attribute)
=== modified file 'lib/lp/registry/tests/test_distributionsourcepackage.py'
--- lib/lp/registry/tests/test_distributionsourcepackage.py 2009-07-19 23:17:34 +0000
+++ lib/lp/registry/tests/test_distributionsourcepackage.py 2010-08-18 11:41:24 +0000
@@ -9,8 +9,9 @@
import unittest
from zope.component import getUtility
+from zope.security.proxy import removeSecurityProxy
-from canonical.testing import LaunchpadZopelessLayer
+from canonical.testing import DatabaseFunctionalLayer, LaunchpadZopelessLayer
from lp.registry.interfaces.distribution import IDistributionSet
from lp.registry.model.karma import KarmaTotalCache
@@ -19,6 +20,23 @@
from lp.testing import TestCaseWithFactory
+class TestDistributionSourcePackage(TestCaseWithFactory):
+
+ layer = DatabaseFunctionalLayer
+
+ def test_dsp_with_no_series_summary(self):
+ distribution_set = getUtility(IDistributionSet)
+
+ distribution = distribution_set.new(name='wart',
+ displayname='wart', title='wart', description='lots of warts',
+ summary='lots of warts', domainname='wart.dumb',
+ members=self.factory.makeTeam(), owner=self.factory.makePerson())
+ naked_distribution = removeSecurityProxy(distribution)
+ self.factory.makeSourcePackage(distroseries=distribution)
+ dsp = naked_distribution.getSourcePackage(name='pmount')
+ self.assertEqual(None, dsp.summary)
+
+
class TestDistributionSourcePackageFindRelatedArchives(TestCaseWithFactory):
layer = LaunchpadZopelessLayer
=== modified file 'lib/lp/registry/tests/test_person.py'
--- lib/lp/registry/tests/test_person.py 2010-08-05 13:23:52 +0000
+++ lib/lp/registry/tests/test_person.py 2010-08-18 11:41:24 +0000
@@ -1,12 +1,16 @@
# Copyright 2009-2010 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
+from __future__ import with_statement
+
__metaclass__ = type
from datetime import datetime
import pytz
import time
+from testtools.matchers import LessThan
+
import transaction
from zope.component import getUtility
@@ -26,6 +30,7 @@
IPersonSet, ImmutableVisibilityError, NameAlreadyTaken,
PersonCreationRationale, PersonVisibility)
from canonical.launchpad.database import Bug
+from canonical.launchpad.testing.pages import LaunchpadWebServiceCaller
from lp.registry.model.structuralsubscription import (
StructuralSubscription)
from lp.registry.model.karma import KarmaCategory
@@ -34,16 +39,55 @@
from lp.bugs.interfaces.bugtask import IllegalRelatedBugTasksParams
from lp.answers.model.answercontact import AnswerContact
from lp.blueprints.model.specification import Specification
-from lp.testing import login_person, logout, TestCase, TestCaseWithFactory
+from lp.testing import (
+ login_person, logout, person_logged_in, TestCase, TestCaseWithFactory,
+ )
+from lp.testing.matchers import HasQueryCount
from lp.testing.views import create_initialized_view
+from lp.testing import celebrity_logged_in
+from lp.testing._webservice import QueryCollector
from lp.registry.interfaces.person import PrivatePersonLinkageError
from canonical.testing.layers import DatabaseFunctionalLayer, reconnect_stores
+class TestPersonTeams(TestCaseWithFactory):
+
+ layer = DatabaseFunctionalLayer
+
+ def test_teams_indirectly_participated_in(self):
+ self.user = self.factory.makePerson()
+ a_team = self.factory.makeTeam(name='a')
+ b_team = self.factory.makeTeam(name='b', owner=a_team)
+ c_team = self.factory.makeTeam(name='c', owner=b_team)
+ login_person(a_team.teamowner)
+ a_team.addMember(self.user, a_team.teamowner)
+ indirect_teams = self.user.teams_indirectly_participated_in
+ expected_teams = [b_team, c_team]
+ test_teams = sorted(indirect_teams,
+ key=lambda team: team.displayname)
+ self.assertEqual(expected_teams, test_teams)
+
+
class TestPerson(TestCaseWithFactory):
layer = DatabaseFunctionalLayer
+ def test_getOwnedOrDrivenPillars(self):
+ user = self.factory.makePerson()
+ active_project = self.factory.makeProject(owner=user)
+ inactive_project = self.factory.makeProject(owner=user)
+ with celebrity_logged_in('admin'):
+ inactive_project.active = False
+ expected_pillars = [active_project.name]
+ received_pillars = [pillar.name for pillar in
+ user.getOwnedOrDrivenPillars()]
+ self.assertEqual(expected_pillars, received_pillars)
+
+
+class TestPersonStates(TestCaseWithFactory):
+
+ layer = DatabaseFunctionalLayer
+
def setUp(self):
TestCaseWithFactory.setUp(self, 'foo.bar@xxxxxxxxxxxxx')
person_set = getUtility(IPersonSet)
@@ -173,7 +217,8 @@
def test_person_snapshot(self):
omitted = (
- 'activemembers', 'adminmembers', 'allmembers', 'approvedmembers',
+ 'activemembers', 'adminmembers', 'allmembers',
+ 'all_members_prepopulated', 'approvedmembers',
'deactivatedmembers', 'expiredmembers', 'inactivemembers',
'invited_members', 'member_memberships', 'pendingmembers',
'proposedmembers', 'unmapped_participants',
@@ -249,8 +294,8 @@
transaction.commit()
logout()
- def _get_testible_account(self, person, date_created, openid_identifier):
- # Return a naked account with predicable attributes.
+ def _get_testable_account(self, person, date_created, openid_identifier):
+ # Return a naked account with predictable attributes.
account = removeSecurityProxy(person.account)
account.date_created = date_created
account.openid_identifier = openid_identifier
@@ -264,7 +309,7 @@
2010, 04, 01, 0, 0, 0, 0, tzinfo=pytz.timezone('UTC'))
# Free an OpenID identifier using merge.
first_duplicate = self.factory.makePerson()
- first_account = self._get_testible_account(
+ first_account = self._get_testable_account(
first_duplicate, test_date, test_identifier)
first_person = self.factory.makePerson()
self._do_premerge(first_duplicate, first_person)
@@ -276,7 +321,7 @@
# Create an account that reuses the freed OpenID_identifier.
test_date = test_date.replace(2010, 05)
second_duplicate = self.factory.makePerson()
- second_account = self._get_testible_account(
+ second_account = self._get_testable_account(
second_duplicate, test_date, test_identifier)
second_person = self.factory.makePerson()
self._do_premerge(second_duplicate, second_person)
@@ -535,3 +580,32 @@
names = [entry['project'].name for entry in results]
self.assertEqual(
['cc', 'bb', 'aa', 'dd', 'ee'], names)
+
+
+class TestAPIPartipication(TestCaseWithFactory):
+
+ layer = DatabaseFunctionalLayer
+
+ def test_participation_query_limit(self):
+ # A team with 3 members should only query once for all their
+ # attributes.
+ team = self.factory.makeTeam()
+ with person_logged_in(team.teamowner):
+ team.addMember(self.factory.makePerson(), team.teamowner)
+ team.addMember(self.factory.makePerson(), team.teamowner)
+ team.addMember(self.factory.makePerson(), team.teamowner)
+ webservice = LaunchpadWebServiceCaller()
+ collector = QueryCollector()
+ collector.register()
+ self.addCleanup(collector.unregister)
+ url = "/~%s/participants" % team.name
+ logout()
+ response = webservice.get(url,
+ headers={'User-Agent': 'AnonNeedsThis'})
+ self.assertEqual(response.status, 200,
+ "Got %d for url %r with response %r" % (
+ response.status, url, response.body))
+ # XXX: This number should really be 10, but see
+ # https://bugs.launchpad.net/storm/+bug/619017 which is adding 3
+ # queries to the test.
+ self.assertThat(collector, HasQueryCount(LessThan(13)))
=== modified file 'lib/lp/registry/tests/test_product.py'
--- lib/lp/registry/tests/test_product.py 2010-08-02 20:24:24 +0000
+++ lib/lp/registry/tests/test_product.py 2010-08-18 11:41:24 +0000
@@ -18,14 +18,16 @@
from canonical.launchpad.ftests import syncUpdate
+from lazr.lifecycle.snapshot import Snapshot
from lp.registry.interfaces.person import IPersonSet
-from lp.registry.interfaces.product import License
+from lp.registry.interfaces.product import IProduct, License
from lp.registry.model.product import Product
from lp.registry.model.productlicense import ProductLicense
from lp.registry.model.commercialsubscription import (
CommercialSubscription)
from lp.testing import TestCaseWithFactory
+
class TestProduct(TestCaseWithFactory):
"""Tests product object."""
@@ -178,6 +180,32 @@
'new')
+class ProductSnapshotTestCase(TestCaseWithFactory):
+ """A TestCase for product snapshots."""
+
+ layer = LaunchpadFunctionalLayer
+
+ def setUp(self):
+ super(ProductSnapshotTestCase, self).setUp()
+ self.product = self.factory.makeProduct(name="shamwow")
+
+ def test_snapshot(self):
+ """Snapshots of products should not include marked attribues.
+
+ Wrap an export with 'doNotSnapshot' to force the snapshot to not
+ include that attribute.
+ """
+ snapshot = Snapshot(self.product, providing=IProduct)
+ omitted = [
+ 'series',
+ 'releases',
+ ]
+ for attribute in omitted:
+ self.assertFalse(
+ hasattr(snapshot, attribute),
+ "Snapshot should not include %s." % attribute)
+
+
class BugSupervisorTestCase(TestCaseWithFactory):
"""A TestCase for bug supervisor management."""
=== modified file 'lib/lp/scripts/utilities/importfascist.py'
--- lib/lp/scripts/utilities/importfascist.py 2010-08-02 02:13:52 +0000
+++ lib/lp/scripts/utilities/importfascist.py 2010-08-18 11:41:24 +0000
@@ -38,6 +38,7 @@
canonical.librarian.client
canonical.librarian.db
doctest
+ lp.shipit
""")
@@ -175,9 +176,9 @@
% self.import_into)
-# The names of the arguments form part of the interface of __import__(...), and
-# must not be changed, as code may choose to invoke __import__ using keyword
-# arguments - e.g. the encodings module in Python 2.6.
+# The names of the arguments form part of the interface of __import__(...),
+# and must not be changed, as code may choose to invoke __import__ using
+# keyword arguments - e.g. the encodings module in Python 2.6.
# pylint: disable-msg=W0102,W0602
def import_fascist(name, globals={}, locals={}, fromlist=[], level=-1):
global naughty_imports
=== modified file 'lib/lp/services/features/__init__.py'
--- lib/lp/services/features/__init__.py 2010-07-21 11:05:14 +0000
+++ lib/lp/services/features/__init__.py 2010-08-18 11:41:24 +0000
@@ -5,6 +5,25 @@
These can be turned on and off by admins, and can affect particular
defined scopes such as "beta users" or "production servers."
-
-See <https://dev.launchpad.net/LEP/FeatureFlags>
-"""
+"""
+
+import threading
+
+
+__all__ = [
+ 'getFeatureFlag',
+ 'per_thread',
+ ]
+
+
+per_thread = threading.local()
+"""Holds the default per-thread feature controller in its .features attribute.
+
+Framework code is responsible for setting this in the appropriate context, eg
+when starting a web request.
+"""
+
+
+def getFeatureFlag(flag):
+ """Get the value of a flag for this thread's scopes."""
+ return per_thread.features.getFlag(flag)
=== added file 'lib/lp/services/features/configure.zcml'
--- lib/lp/services/features/configure.zcml 1970-01-01 00:00:00 +0000
+++ lib/lp/services/features/configure.zcml 2010-08-18 11:41:24 +0000
@@ -0,0 +1,19 @@
+<!-- Copyright 2010 Canonical Ltd. This software is licensed under the
+ GNU Affero General Public License version 3 (see the file LICENSE).
+-->
+
+<configure
+ xmlns="http://namespaces.zope.org/zope"
+ xmlns:browser="http://namespaces.zope.org/browser">
+
+ <subscriber
+ for="canonical.launchpad.webapp.interfaces.IStartRequestEvent"
+ handler="lp.services.features.webapp.start_request"
+ />
+
+ <subscriber
+ for="zope.app.publication.interfaces.IEndRequestEvent"
+ handler="lp.services.features.webapp.end_request"
+ />
+
+</configure>
=== added directory 'lib/lp/services/features/doc'
=== added file 'lib/lp/services/features/doc/features.txt'
--- lib/lp/services/features/doc/features.txt 1970-01-01 00:00:00 +0000
+++ lib/lp/services/features/doc/features.txt 2010-08-18 11:41:24 +0000
@@ -0,0 +1,127 @@
+****************************
+Feature Flag Developer Guide
+****************************
+
+Introduction
+************
+
+The point of feature flags is to let us turn some features of Launchpad on
+and off without changing the code or restarting the application, and to
+expose different features to different subsets of users.
+
+See <https://dev.launchpad.net/LEP/FeatureFlags> for more discussion and
+rationale.
+
+The typical use for feature flags is within web page requests but they can
+also be used in asynchronous jobs or apis or other parts of Launchpad.
+
+Internal model for feature flags
+********************************
+
+A feature flag maps from a *name* to a *value*. The specific value used
+for a particular request is determined by a set of zero or more *scopes*
+that apply to that request, by finding the *rule* with the highest
+*priority*.
+
+Flags are defined by a *name* that typically looks like a Python
+identifier, for example ``notification.global.text``. A definition is
+given for a particular *scope*, which also looks like a dotted identifier,
+for example ``user.beta`` or ``server.edge``. This is just a naming
+convention, and they do not need to correspond to Python modules.
+
+The value is stored in the database as just a Unicode string, and it might
+be interpreted as a boolean, number, human-readable string or whatever.
+
+The default for flags is to be None if they're not set in the database, so
+that should be a sensible baseline default state.
+
+Performance model
+*****************
+
+Flags are supposed to be cheap enough that you can introduce them without
+causing a performance concern.
+
+If the page does not check any flags, no extra work will be done. The
+first time a page checks a flag, all the rules will be read from the
+database and held in memory for the duration of the request.
+
+Scopes may be expensive in some cases, such as checking group membership.
+Whether a scope is active or not is looked up the first time it's needed
+within a particular request.
+
+The standard page footer identifies the flags and scopes that were
+actually used by the page.
+
+Naming conventions
+******************
+
+We have naming conventions for feature flags and scopes, so that people can
+understand the likely impact of a particular flag and so they can find all
+the flags likely to affect a feature.
+
+So for any flag we want to say:
+
+* What application area does this affect? (malone, survey, questions,
+ code, etc)
+
+* What specific feature does it change?
+
+* What affect does it have on this feature? The most common is "enabled"
+ but for some other we want to specify a specific value as well such as
+ "date" or "size".
+
+These are concatenated with dots so the overall feature name looks a bit
+like a Python module name.
+
+A similar approach is used for scopes.
+
+Checking flags in page templates
+********************************
+
+You can conditionally show some text like this::
+
+ <tal:survey condition="features/user_survey.enabled">
+ •
+ <a href="http://survey.example.com/">Take our survey!</a>
+ </tal:survey>
+
+You can use the built-in TAL feature of prepending ``not:`` to the
+condition, and for flags that have a value you could use them in
+``tal:replace`` or ``tal:attributes``.
+
+If you just want to simply insert some text taken from a feature, say
+something like::
+
+ Message of the day: ${motd.text}
+
+Templates can also check whether the request is in a particular scope, but
+before using this consider whether the code will always be bound to that
+scope or whether it would be more correct to define a new feature::
+
+ <p tal:condition="feature_scopes/server.staging">
+ Staging server: all data will be discarded daily!</p>
+
+Checking flags in code
+**********************
+
+The Zope traversal code establishes a `FeatureController` for the duration
+of a request. The object can be obtained through either
+`request.features` or `lp.services.features.per_thread.features`. This
+provides various useful methods including `getFlag` to look up one feature
+(memoized), and `isInScope` to check one scope (also memoized).
+
+As a convenience, `lp.services.features.getFeatureFlag` looks up a single
+flag in the thread default controller.
+
+Debugging feature usage
+***********************
+
+The flags active during a page request, and the scopes that were looked
+up are visible in the comment at the bottom of every standard Launchpad
+page.
+
+Defining scopes
+***************
+
+
+.. vim: ft=rst
=== modified file 'lib/lp/services/features/flags.py'
--- lib/lp/services/features/flags.py 2010-07-22 12:45:51 +0000
+++ lib/lp/services/features/flags.py 2010-08-18 11:41:24 +0000
@@ -1,17 +1,47 @@
# Copyright 2010 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
-__all__ = ['FeatureController']
+__all__ = [
+ 'FeatureController',
+ 'NullFeatureController',
+ ]
+
__metaclass__ = type
+from storm.locals import Desc
+
from lp.services.features.model import (
+ FeatureFlag,
getFeatureStore,
- FeatureFlag,
)
+class Memoize(object):
+
+ def __init__(self, calc):
+ self._known = {}
+ self._calc = calc
+
+ def lookup(self, key):
+ if key in self._known:
+ return self._known[key]
+ v = self._calc(key)
+ self._known[key] = v
+ return v
+
+
+class ScopeDict(object):
+ """Allow scopes to be looked up by getitem"""
+
+ def __init__(self, features):
+ self.features = features
+
+ def __getitem__(self, scope_name):
+ return self.features.isInScope(scope_name)
+
+
class FeatureController(object):
"""A FeatureController tells application code what features are active.
@@ -34,52 +64,101 @@
be one per web app request.
Intended performance: when this object is first constructed, it will read
- the whole current feature flags from the database. This will take a few
- ms. The controller is then supposed to be held in a thread-local for the
- duration of the request.
+ the whole feature flag table from the database. It is expected to be
+ reasonably small. The scopes may be expensive to compute (eg checking
+ team membership) so they are checked at most once when they are first
+ needed.
+
+ The controller is then supposed to be held in a thread-local and reused
+ for the duration of the request.
See <https://dev.launchpad.net/LEP/FeatureFlags>
"""
- def __init__(self, scopes):
+ def __init__(self, scope_check_callback):
"""Construct a new view of the features for a set of scopes.
- """
- self._store = getFeatureStore()
- self._scopes = self._preenScopes(scopes)
- self._cached_flags = self._queryAllFlags()
-
- def getScopes(self):
- return frozenset(self._scopes)
-
- def getFlag(self, flag_name):
- return self._cached_flags.get(flag_name)
+
+ :param scope_check_callback: Given a scope name, says whether
+ it's active or not.
+ """
+ self._known_scopes = Memoize(scope_check_callback)
+ self._known_flags = Memoize(self._checkFlag)
+ # rules are read from the database the first time they're needed
+ self._rules = None
+ self.scopes = ScopeDict(self)
+
+ def getFlag(self, flag):
+ """Get the value of a specific flag.
+
+ :param flag: A name to lookup. e.g. 'recipes.enabled'
+ :return: The value of the flag determined by the highest priority rule
+ that matched.
+ """
+ return self._known_flags.lookup(flag)
+
+ def _checkFlag(self, flag):
+ self._needRules()
+ if flag in self._rules:
+ for scope, value in self._rules[flag]:
+ if self._known_scopes.lookup(scope):
+ return value
+
+ def isInScope(self, scope):
+ return self._known_scopes.lookup(scope)
+
+ def __getitem__(self, flag_name):
+ """FeatureController can be indexed.
+
+ This is to support easy zope traversal through eg
+ "request/features/a.b.c". We don't support other collection
+ protocols.
+
+ Note that calling this the first time for any key may cause
+ arbitrarily large amounts of work to be done to determine if the
+ controller is in any scopes relevant to this flag.
+ """
+ return self.getFlag(flag_name)
def getAllFlags(self):
- """Get the feature flags active for the current scopes.
+ """Return a dict of all active flags.
- :returns: dict from flag_name (unicode) to value (unicode).
+ This may be expensive because of evaluating many scopes, so it
+ shouldn't normally be used by code that only wants to know about one
+ or a few flags.
"""
- return dict(self._cached_flags)
+ self._needRules()
+ return dict((f, self.getFlag(f)) for f in self._rules)
- def _queryAllFlags(self):
+ def _loadRules(self):
+ store = getFeatureStore()
d = {}
- rs = (self._store
- .find(FeatureFlag,
- FeatureFlag.scope.is_in(self._scopes))
- .order_by(FeatureFlag.priority)
- .values(FeatureFlag.flag, FeatureFlag.value))
- for flag, value in rs:
- d[str(flag)] = value
+ rs = (store
+ .find(FeatureFlag)
+ .order_by(Desc(FeatureFlag.priority))
+ .values(FeatureFlag.flag, FeatureFlag.scope,
+ FeatureFlag.value))
+ for flag, scope, value in rs:
+ d.setdefault(str(flag), []).append((str(scope), value))
return d
- def _preenScopes(self, scopes):
- # for convenience turn strings to unicode
- us = []
- for s in scopes:
- if isinstance(s, unicode):
- us.append(s)
- elif isinstance(s, str):
- us.append(unicode(s))
- else:
- raise TypeError("invalid scope: %r" % s)
- return us
+ def _needRules(self):
+ if self._rules is None:
+ self._rules = self._loadRules()
+
+ def usedFlags(self):
+ """Return dict of flags used in this controller so far."""
+ return dict(self._known_flags._known)
+
+ def usedScopes(self):
+ """Return {scope: active} for scopes that have been used so far."""
+ return dict(self._known_scopes._known)
+
+
+class NullFeatureController(FeatureController):
+ """For use in testing: everything is turned off"""
+
+ def __init__(self):
+ FeatureController.__init__(self, lambda scope: None)
+
+ def _loadRules(self):
+ return []
=== modified file 'lib/lp/services/features/tests/test_flags.py'
--- lib/lp/services/features/tests/test_flags.py 2010-07-22 12:45:51 +0000
+++ lib/lp/services/features/tests/test_flags.py 2010-08-18 11:41:24 +0000
@@ -12,7 +12,11 @@
from canonical.testing import layers
from lp.testing import TestCase
-from lp.services.features import model
+from lp.services.features import (
+ getFeatureFlag,
+ model,
+ per_thread,
+ )
from lp.services.features.flags import (
FeatureController,
)
@@ -20,11 +24,10 @@
notification_name = 'notification.global.text'
notification_value = u'\N{SNOWMAN} stormy Launchpad weather ahead'
-example_scope = 'beta_user'
testdata = [
- (example_scope, notification_name, notification_value, 100),
+ ('beta_user', notification_name, notification_value, 100),
('default', 'ui.icing', u'3.0', 100),
('beta_user', 'ui.icing', u'4.0', 300),
]
@@ -40,6 +43,14 @@
from storm.tracer import debug
debug(True)
+ def makeControllerInScopes(self, scopes):
+ """Make a controller that will report it's in the given scopes."""
+ call_log = []
+ def scope_cb(scope):
+ call_log.append(scope)
+ return scope in scopes
+ return FeatureController(scope_cb), call_log
+
def populateStore(self):
store = model.getFeatureStore()
for (scope, flag, value, priority) in testdata:
@@ -49,37 +60,51 @@
value=value,
priority=priority))
- def test_defaultFlags(self):
- # the sample db has no flags set
- control = FeatureController([])
- self.assertEqual({},
- control.getAllFlags())
-
def test_getFlag(self):
self.populateStore()
- control = FeatureController(['default'])
+ control, call_log = self.makeControllerInScopes(['default'])
self.assertEqual(u'3.0',
control.getFlag('ui.icing'))
+ self.assertEqual(['beta_user', 'default'], call_log)
+
+ def test_getItem(self):
+ # for use in page templates, the flags can be treated as a dict
+ self.populateStore()
+ control, call_log = self.makeControllerInScopes(['default'])
+ self.assertEqual(u'3.0',
+ control['ui.icing'])
+ self.assertEqual(['beta_user', 'default'], call_log)
+ # after looking this up the value is known and the scopes are
+ # positively and negatively cached
+ self.assertEqual({'ui.icing': u'3.0'}, control.usedFlags())
+ self.assertEqual(dict(beta_user=False, default=True),
+ control.usedScopes())
def test_getAllFlags(self):
# can fetch all the active flags, and it gives back only the
- # highest-priority settings
+ # highest-priority settings. this may be expensive and shouldn't
+ # normally be used.
self.populateStore()
- control = FeatureController(['default', 'beta_user'])
+ control, call_log = self.makeControllerInScopes(
+ ['beta_user', 'default'])
self.assertEqual(
{'ui.icing': '4.0',
notification_name: notification_value},
control.getAllFlags())
+ # evaluates all necessary flags; in this test data beta_user shadows
+ # default settings
+ self.assertEqual(['beta_user'], call_log)
def test_overrideFlag(self):
# if there are multiple settings for a flag, and they match multiple
# scopes, the priorities determine which is matched
self.populateStore()
- default_control = FeatureController(['default'])
+ default_control, call_log = self.makeControllerInScopes(['default'])
self.assertEqual(
u'3.0',
default_control.getFlag('ui.icing'))
- beta_control = FeatureController(['default', 'beta_user'])
+ beta_control, call_log = self.makeControllerInScopes(
+ ['beta_user', 'default'])
self.assertEqual(
u'4.0',
beta_control.getFlag('ui.icing'))
@@ -87,9 +112,53 @@
def test_undefinedFlag(self):
# if the flag is not defined, we get None
self.populateStore()
- control = FeatureController(['default', 'beta_user'])
+ control, call_log = self.makeControllerInScopes(
+ ['beta_user', 'default'])
self.assertIs(None,
control.getFlag('unknown_flag'))
- no_scope_flags = FeatureController([])
+ no_scope_flags, call_log = self.makeControllerInScopes([])
self.assertIs(None,
no_scope_flags.getFlag('ui.icing'))
+
+ def test_threadGetFlag(self):
+ self.populateStore()
+ # the start-of-request handler will do something like this:
+ per_thread.features, call_log = self.makeControllerInScopes(
+ ['default', 'beta_user'])
+ try:
+ # then application code can simply ask without needing a context
+ # object
+ self.assertEqual(u'4.0', getFeatureFlag('ui.icing'))
+ finally:
+ per_thread.features = None
+
+ def testLazyScopeLookup(self):
+ # feature scopes may be a bit expensive to look up, so we do it only
+ # when it will make a difference to the result.
+ self.populateStore()
+ f, call_log = self.makeControllerInScopes(['beta_user'])
+ self.assertEqual(u'4.0', f.getFlag('ui.icing'))
+ # to calculate this it should only have had to check we're in the
+ # beta_users scope; nothing else makes a difference
+ self.assertEqual(dict(beta_user=True), f._known_scopes._known)
+
+ def testUnknownFeature(self):
+ # looking up an unknown feature gives you None
+ self.populateStore()
+ f, call_log = self.makeControllerInScopes([])
+ self.assertEqual(None, f.getFlag('unknown'))
+ # no scopes need to be checked because it's just not in the database
+ # and there's no point checking
+ self.assertEqual({}, f._known_scopes._known)
+ self.assertEquals([], call_log)
+ # however, this we have now negative-cached the flag
+ self.assertEqual(dict(unknown=None), f.usedFlags())
+ self.assertEqual(dict(), f.usedScopes())
+
+ def testScopeDict(self):
+ # can get scopes as a dict, for use by "feature_scopes/server.demo"
+ f, call_log = self.makeControllerInScopes(['beta_user'])
+ self.assertEqual(True, f.scopes['beta_user'])
+ self.assertEqual(False, f.scopes['alpha_user'])
+ self.assertEqual(True, f.scopes['beta_user'])
+ self.assertEqual(['beta_user', 'alpha_user'], call_log)
=== added file 'lib/lp/services/features/webapp.py'
--- lib/lp/services/features/webapp.py 1970-01-01 00:00:00 +0000
+++ lib/lp/services/features/webapp.py 2010-08-18 11:41:24 +0000
@@ -0,0 +1,41 @@
+# Copyright 2010 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Connect Feature flags into webapp requests."""
+
+__all__ = []
+
+__metaclass__ = type
+
+import canonical.config
+
+from lp.services.features import (
+ per_thread,
+ )
+from lp.services.features.flags import (
+ FeatureController,
+ )
+
+
+class ScopesFromRequest(object):
+ """Identify feature scopes based on request state."""
+
+ def __init__(self, request):
+ self._request = request
+
+ def lookup(self, scope_name):
+ parts = scope_name.split('.')
+ if len(parts) == 2:
+ if parts[0] == 'server':
+ return canonical.config.config['launchpad']['is_' + parts[1]]
+
+
+def start_request(event):
+ """Register FeatureController."""
+ event.request.features = per_thread.features = FeatureController(
+ ScopesFromRequest(event.request).lookup)
+
+
+def end_request(event):
+ """Done with this FeatureController."""
+ event.request.features = per_thread.features = None
=== renamed directory 'lib/canonical/launchpad/fields' => 'lib/lp/services/fields'
=== modified file 'lib/lp/services/fields/__init__.py'
--- lib/canonical/launchpad/fields/__init__.py 2010-08-07 14:54:40 +0000
+++ lib/lp/services/fields/__init__.py 2010-08-18 11:41:24 +0000
@@ -1,16 +1,18 @@
-# Copyright 2009 Canonical Ltd. This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
+# copyright 2009-2010 canonical ltd. this software is licensed under the
+# gnu affero general public license version 3 (see the file license).
# pylint: disable-msg=E0211,E0213,W0401
__metaclass__ = type
__all__ = [
'AnnouncementDate',
+ 'FormattableDate',
'BaseImageUpload',
'BlacklistableContentNameField',
'BugField',
'ContentNameField',
'Description',
+ 'Datetime',
'DuplicateBug',
'FieldNotBoundError',
'IAnnouncementDate',
@@ -62,17 +64,16 @@
from zope.component import getUtility
from zope.schema import (
- Bool, Bytes, Choice, Datetime, Field, Float, Int, Password, Text,
+ Bool, Bytes, Choice, Date, Datetime, Field, Float, Int, Password, Text,
TextLine, Tuple)
from zope.schema.interfaces import (
- ConstraintNotSatisfied, IBytes, IDatetime, IField, IObject,
+ ConstraintNotSatisfied, IBytes, IDate, IDatetime, IField, IObject,
IPassword, IText, ITextLine, Interface)
from zope.interface import implements
from zope.security.interfaces import ForbiddenAttribute
from canonical.launchpad import _
from lp.registry.interfaces.pillar import IPillarNameSet
-from canonical.launchpad.webapp.interfaces import ILaunchBag
from lazr.uri import URI, InvalidURIError
from canonical.launchpad.validators import LaunchpadValidationError
from canonical.launchpad.validators.name import valid_name, name_validator
@@ -256,6 +257,28 @@
implements(IWhiteboard)
+class FormattableDate(Date):
+ """A datetime field that checks for compatibility with Python's strformat.
+
+ From the user's perspective this is a date entry field; it converts to and
+ from datetime because that's what the db is expecting.
+ """
+ implements(IDate)
+
+ def _validate(self, value):
+ error_msg = ("Date could not be formatted. Provide a date formatted "
+ "like YYYY-MM-DD format. The year must be after 1900.")
+
+ super(FormattableDate, self)._validate(value)
+ # The only thing of interest here is whether or the input can be
+ # formatted properly, not whether it makes sense otherwise.
+ # As a minimal sanity check, just raise an error if it fails.
+ try:
+ value.strftime('%Y')
+ except ValueError:
+ raise LaunchpadValidationError(error_msg)
+
+
class AnnouncementDate(Datetime):
implements(IDatetime)
@@ -328,6 +351,7 @@
class SearchTag(Tag):
+
def constraint(self, value):
"""Make sure the value is a valid search tag.
@@ -532,6 +556,7 @@
ob.official_malone = False
setattr(ob, self.__name__, value)
+
class URIField(TextLine):
implements(IURIField)
=== renamed file 'lib/canonical/launchpad/zcml/fields.zcml' => 'lib/lp/services/fields/configure.zcml'
--- lib/canonical/launchpad/zcml/fields.zcml 2009-07-13 18:15:02 +0000
+++ lib/lp/services/fields/configure.zcml 2010-08-18 11:41:24 +0000
@@ -12,7 +12,7 @@
<view
type="zope.publisher.interfaces.browser.IBrowserRequest"
- for="canonical.launchpad.fields.ITitle"
+ for="lp.services.fields.ITitle"
provides="zope.app.form.interfaces.IInputWidget"
factory="canonical.launchpad.browser.TitleWidget"
permission="zope.Public"
@@ -20,7 +20,7 @@
<view
type="zope.publisher.interfaces.browser.IBrowserRequest"
- for="canonical.launchpad.fields.ISummary"
+ for="lp.services.fields.ISummary"
provides="zope.app.form.interfaces.IInputWidget"
factory="canonical.launchpad.browser.SummaryWidget"
permission="zope.Public"
@@ -28,7 +28,7 @@
<view
type="zope.publisher.interfaces.browser.IBrowserRequest"
- for="canonical.launchpad.fields.IDescription"
+ for="lp.services.fields.IDescription"
provides="zope.app.form.interfaces.IInputWidget"
factory="canonical.launchpad.browser.DescriptionWidget"
permission="zope.Public"
@@ -36,7 +36,7 @@
<view
type="zope.publisher.interfaces.browser.IBrowserRequest"
- for="canonical.launchpad.fields.INoneableDescription"
+ for="lp.services.fields.INoneableDescription"
provides="zope.app.form.interfaces.IInputWidget"
factory="canonical.launchpad.browser.NoneableDescriptionWidget"
permission="zope.Public"
@@ -44,7 +44,7 @@
<view
type="zope.publisher.interfaces.browser.IBrowserRequest"
- for="canonical.launchpad.fields.IWhiteboard"
+ for="lp.services.fields.IWhiteboard"
provides="zope.app.form.interfaces.IInputWidget"
factory="canonical.launchpad.browser.WhiteboardWidget"
permission="zope.Public"
@@ -52,7 +52,7 @@
<view
type="zope.publisher.interfaces.browser.IBrowserRequest"
- for="canonical.launchpad.fields.IBugField"
+ for="lp.services.fields.IBugField"
provides="zope.app.form.interfaces.IInputWidget"
factory="canonical.widgets.bug.BugWidget"
permission="zope.Public"
@@ -60,7 +60,7 @@
<view
type="zope.publisher.interfaces.browser.IBrowserRequest"
- for="canonical.launchpad.fields.IStrippedTextLine"
+ for="lp.services.fields.IStrippedTextLine"
provides="zope.app.form.interfaces.IInputWidget"
factory="canonical.widgets.textwidgets.StrippedTextWidget"
permission="zope.Public"
@@ -68,7 +68,7 @@
<view
type="zope.publisher.interfaces.browser.IBrowserRequest"
- for="canonical.launchpad.fields.INoneableTextLine"
+ for="lp.services.fields.INoneableTextLine"
provides="zope.app.form.interfaces.IInputWidget"
factory="canonical.widgets.textwidgets.NoneableTextWidget"
permission="zope.Public"
@@ -76,7 +76,7 @@
<view
type="zope.publisher.interfaces.browser.IBrowserRequest"
- for="canonical.launchpad.fields.IURIField"
+ for="lp.services.fields.IURIField"
provides="zope.app.form.interfaces.IInputWidget"
factory="canonical.widgets.textwidgets.URIWidget"
permission="zope.Public"
@@ -84,10 +84,17 @@
<view
type="zope.publisher.interfaces.browser.IBrowserRequest"
- for="canonical.launchpad.fields.ContentNameField"
+ for="lp.services.fields.ContentNameField"
provides="zope.app.form.interfaces.IInputWidget"
factory="canonical.widgets.textwidgets.LowerCaseTextWidget"
permission="zope.Public"
/>
+ <view
+ type="zope.publisher.interfaces.browser.IBrowserRequest"
+ for="lp.services.fields.FormattableDate"
+ provides="zope.app.form.interfaces.IInputWidget"
+ factory="canonical.widgets.DateWidget"
+ permission="zope.Public"
+ />
</configure>
=== added directory 'lib/lp/services/fields/doc'
=== renamed file 'lib/canonical/launchpad/doc/uri-field.txt' => 'lib/lp/services/fields/doc/uri-field.txt'
--- lib/canonical/launchpad/doc/uri-field.txt 2009-02-18 09:05:32 +0000
+++ lib/lp/services/fields/doc/uri-field.txt 2010-08-18 11:41:24 +0000
@@ -14,7 +14,7 @@
To demonstrate, we'll create a sample interface:
>>> from zope.interface import Interface, implements
- >>> from canonical.launchpad.fields import URIField
+ >>> from lp.services.fields import URIField
>>> class IURIFieldTest(Interface):
... field = URIField()
... sftp_only = URIField(allowed_schemes=['sftp'])
=== modified file 'lib/lp/services/fields/tests/__init__.py'
--- lib/canonical/launchpad/fields/tests/__init__.py 2009-11-23 04:55:02 +0000
+++ lib/lp/services/fields/tests/__init__.py 2010-08-18 11:41:24 +0000
@@ -1,1 +1,1 @@
-"""Tests for canonical.launchpad.fields."""
+"""Tests for lp.services.fields."""
=== modified file 'lib/lp/services/fields/tests/test_fields.py'
--- lib/canonical/launchpad/fields/tests/test_fields.py 2009-11-23 04:55:02 +0000
+++ lib/lp/services/fields/tests/test_fields.py 2010-08-18 11:41:24 +0000
@@ -5,24 +5,43 @@
__metaclass__ = type
+import datetime
import unittest
-
-from canonical.launchpad.fields import StrippableText
-
+import time
+
+from canonical.launchpad.validators import LaunchpadValidationError
+from lp.services.fields import (StrippableText, FormattableDate)
from lp.testing import TestCase
+def make_target():
+ """Make a trivial object to be a target of the field setting."""
+ class Simple:
+ """A simple class to test setting fields on."""
+ return Simple()
+
+
+class TestFormattableDate(TestCase):
+
+ def test_validation_fails_on_bad_data(self):
+ field = FormattableDate()
+ date_value = datetime.date(
+ *(time.strptime('1000-01-01', '%Y-%m-%d'))[:3])
+ self.assertRaises(
+ LaunchpadValidationError, field.validate, date_value)
+
+ def test_validation_passes_good_data(self):
+ field = FormattableDate()
+ date_value = datetime.date(
+ *(time.strptime('2010-01-01', '%Y-%m-%d'))[:3])
+ self.assertIs(None, field.validate(date_value))
+
+
class TestStrippableText(TestCase):
- def makeTarget(self):
- """Make a trivial object to be a target of the field setting."""
- class Simple:
- """A simple class to test setting fields on."""
- return Simple()
-
def test_strips_text(self):
# The set method should strip the string before setting the field.
- target = self.makeTarget()
+ target = make_target()
field = StrippableText(__name__='test', strip_text=True)
self.assertTrue(field.strip_text)
field.set(target, ' testing ')
@@ -31,7 +50,7 @@
def test_default_constructor(self):
# If strip_text is not set, or set to false, then the text is not
# stripped when set.
- target = self.makeTarget()
+ target = make_target()
field = StrippableText(__name__='test')
self.assertFalse(field.strip_text)
field.set(target, ' testing ')
@@ -39,7 +58,7 @@
def test_setting_with_none(self):
# The set method is given None, the attribute is set to None
- target = self.makeTarget()
+ target = make_target()
field = StrippableText(__name__='test', strip_text=True)
field.set(target, None)
self.assertIs(None, target.test)
=== renamed file 'lib/canonical/launchpad/tests/test_tag_fields.py' => 'lib/lp/services/fields/tests/test_tag_fields.py'
--- lib/canonical/launchpad/tests/test_tag_fields.py 2009-06-25 05:30:52 +0000
+++ lib/lp/services/fields/tests/test_tag_fields.py 2010-08-18 11:41:24 +0000
@@ -7,7 +7,7 @@
from zope.schema.interfaces import ConstraintNotSatisfied
-from canonical.launchpad.fields import SearchTag, Tag
+from lp.services.fields import SearchTag, Tag
from canonical.testing import LaunchpadFunctionalLayer
from lp.testing import TestCase
=== modified file 'lib/lp/services/osutils.py'
--- lib/lp/services/osutils.py 2010-03-18 19:18:34 +0000
+++ lib/lp/services/osutils.py 2010-08-18 11:41:24 +0000
@@ -5,12 +5,14 @@
__metaclass__ = type
__all__ = [
+ 'override_environ',
'remove_tree',
'kill_by_pidfile',
'remove_if_exists',
'two_stage_kill',
]
+from contextlib import contextmanager
import os.path
import shutil
@@ -26,3 +28,33 @@
"""Remove the tree at 'path' from disk."""
if os.path.exists(path):
shutil.rmtree(path)
+
+
+def set_environ(new_values):
+ """Set the environment variables as specified by new_values.
+
+ :return: a dict of the old values
+ """
+ old_values = {}
+ for name, value in new_values.iteritems():
+ old_values[name] = os.environ.get(name)
+ if value is None:
+ if old_values[name] is not None:
+ del os.environ[name]
+ else:
+ os.environ[name] = value
+ return old_values
+
+
+@contextmanager
+def override_environ(**kwargs):
+ """Override environment variables with the kwarg values.
+
+ If a value is None, the environment variable is deleted. Variables are
+ restored to their previous state when exiting the context.
+ """
+ old_values = set_environ(kwargs)
+ try:
+ yield
+ finally:
+ set_environ(old_values)
=== modified file 'lib/lp/services/worlddata/interfaces/country.py'
--- lib/lp/services/worlddata/interfaces/country.py 2010-02-22 15:49:14 +0000
+++ lib/lp/services/worlddata/interfaces/country.py 2010-08-18 11:41:24 +0000
@@ -16,7 +16,7 @@
from zope.interface import Interface, Attribute
from zope.schema import Int, TextLine
-from canonical.launchpad.fields import Title, Description
+from lp.services.fields import Title, Description
from canonical.launchpad.validators.name import valid_name
from canonical.launchpad import _
=== added file 'lib/lp/shipit.py'
--- lib/lp/shipit.py 1970-01-01 00:00:00 +0000
+++ lib/lp/shipit.py 2010-08-18 11:41:24 +0000
@@ -0,0 +1,94 @@
+from canonical.launchpad import _
+from canonical.launchpad import versioninfo
+# From browser/configure.zcml.
+from canonical.launchpad.browser import MaintenanceMessage
+# From browser/configure.zcml.
+from canonical.launchpad.browser.launchpad import LaunchpadImageFolder
+from canonical.launchpad.database.account import Account
+from canonical.launchpad.datetimeutils import make_mondays_between
+from canonical.launchpad.ftests import ANONYMOUS
+from canonical.launchpad.ftests import login
+from canonical.launchpad.helpers import intOrZero
+from canonical.launchpad.helpers import shortlist
+# From browser/configure.zcml.
+from canonical.launchpad.interfaces import ILaunchpadRoot
+from canonical.launchpad.interfaces import IMasterObject
+from canonical.launchpad.interfaces import ISlaveStore
+from canonical.launchpad.interfaces import IStore
+from canonical.launchpad.interfaces.account import AccountStatus
+from canonical.launchpad.interfaces.account import IAccount
+from canonical.launchpad.interfaces.account import IAccountSet
+from canonical.launchpad.interfaces.emailaddress import EmailAddressStatus
+from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
+from canonical.launchpad.interfaces.librarian import ILibraryFileAliasSet
+from canonical.launchpad.interfaces.openidconsumer import IOpenIDConsumerStore
+from canonical.launchpad.layers import setFirstLayer
+from canonical.launchpad.security import AuthorizationBase
+from canonical.launchpad.testing.browser import setUp
+from canonical.launchpad.testing.browser import tearDown
+from canonical.launchpad.testing.pages import PageTestSuite
+from canonical.launchpad.testing.pages import extract_text
+from canonical.launchpad.testing.pages import find_tags_by_class
+from canonical.launchpad.testing.pages import setUpGlobs
+from canonical.launchpad.testing.systemdocs import LayeredDocFileSuite
+from canonical.launchpad.testing.systemdocs import setUp as sd_setUp
+from canonical.launchpad.testing.systemdocs import tearDown as sd_tearDown
+from canonical.launchpad.validators import LaunchpadValidationError
+from canonical.launchpad.versioninfo import revno
+from canonical.launchpad.webapp import Navigation
+from canonical.launchpad.webapp import canonical_url
+from canonical.launchpad.webapp import redirection
+from canonical.launchpad.webapp import stepto
+from canonical.launchpad.webapp import urlappend
+from canonical.launchpad.webapp.batching import BatchNavigator
+from canonical.launchpad.webapp.dbpolicy import MasterDatabasePolicy
+from canonical.launchpad.webapp.error import SystemErrorView
+from canonical.launchpad.webapp.interaction import Participation
+from canonical.launchpad.webapp.interfaces import ILaunchBag
+from canonical.launchpad.webapp.interfaces import ILaunchpadApplication
+from canonical.launchpad.webapp.interfaces import IPlacelessLoginSource
+from canonical.launchpad.webapp.interfaces import IStoreSelector
+from canonical.launchpad.webapp.interfaces import UnexpectedFormData
+from canonical.launchpad.webapp.launchpadform import LaunchpadEditFormView
+from canonical.launchpad.webapp.launchpadform import LaunchpadFormView
+from canonical.launchpad.webapp.launchpadform import action
+from canonical.launchpad.webapp.launchpadform import custom_widget
+from canonical.launchpad.webapp.login import allowUnauthenticatedSession
+from canonical.launchpad.webapp.login import logInPrincipal
+from canonical.launchpad.webapp.menu import structured
+from canonical.launchpad.webapp.publication import LaunchpadBrowserPublication
+from canonical.launchpad.webapp.publisher import LaunchpadView
+from canonical.launchpad.webapp.servers import AccountPrincipalMixin
+from canonical.launchpad.webapp.servers import LaunchpadBrowserRequest
+from canonical.launchpad.webapp.servers import LaunchpadTestRequest
+from canonical.launchpad.webapp.servers import (
+ VirtualHostRequestPublicationFactory)
+from canonical.launchpad.webapp.testing import verifyObject
+from canonical.launchpad.webapp.tests.test_login import FakeOpenIDConsumer
+from canonical.launchpad.webapp.tests.test_login import FakeOpenIDResponse
+from canonical.launchpad.webapp.tests.test_login import (
+ IAccountSet_getByOpenIDIdentifier_monkey_patched)
+from canonical.launchpad.webapp.tests.test_login import (
+ SRegResponse_fromSuccessResponse_stubbed)
+from canonical.launchpad.webapp.tests.test_login import (
+ fill_login_form_and_submit)
+from canonical.launchpad.webapp.vhosts import allvhosts
+from lp.registry.interfaces.person import IPerson
+from lp.registry.interfaces.person import IPersonSet
+from lp.registry.interfaces.person import PersonCreationRationale
+from lp.registry.model.karma import Karma
+from lp.registry.model.person import Person
+from lp.services.mail import stub
+from lp.services.mail.sendmail import simple_sendmail
+from lp.services.scripts.base import LaunchpadCronScript
+from lp.services.scripts.base import LaunchpadScript
+from lp.services.scripts.base import LaunchpadScriptFailure
+from lp.services.worlddata.interfaces.country import ICountrySet
+from lp.services.worlddata.model.country import Country
+from lp.testing import TestCase
+from lp.testing import TestCaseWithFactory
+from lp.testing import login_person
+from lp.testing import logout
+from lp.testing import run_script
+from lp.testing.factory import LaunchpadObjectFactory
+from lp.testing.publication import get_request_and_publication
=== modified file 'lib/lp/soyuz/browser/archivesubscription.py'
--- lib/lp/soyuz/browser/archivesubscription.py 2010-07-27 19:17:43 +0000
+++ lib/lp/soyuz/browser/archivesubscription.py 2010-08-18 11:41:24 +0000
@@ -27,7 +27,7 @@
from canonical.cachedproperty import cachedproperty
from canonical.launchpad import _
-from canonical.launchpad.fields import PersonChoice
+from lp.services.fields import PersonChoice
from lp.soyuz.browser.sourceslist import (
SourcesListEntries, SourcesListEntriesView)
from lp.soyuz.interfaces.archive import IArchiveSet
=== modified file 'lib/lp/soyuz/configure.zcml'
--- lib/lp/soyuz/configure.zcml 2010-08-06 03:27:00 +0000
+++ lib/lp/soyuz/configure.zcml 2010-08-18 11:41:24 +0000
@@ -188,7 +188,8 @@
displayversion
is_delayed_copy
isPPA
- components"/>
+ components
+ isAutoSyncUpload"/>
<require
permission="launchpad.Edit"
attributes="
=== modified file 'lib/lp/soyuz/doc/distroseriesqueue-dist-upgrader.txt'
--- lib/lp/soyuz/doc/distroseriesqueue-dist-upgrader.txt 2010-08-05 03:10:05 +0000
+++ lib/lp/soyuz/doc/distroseriesqueue-dist-upgrader.txt 2010-08-18 11:41:24 +0000
@@ -249,7 +249,7 @@
>>> pub_records = upload.queue_root.realiseUpload(mock_logger)
DEBUG: Publishing custom dist-upgrader_20070219.1234_all.tar.gz to ubuntutest/breezy-autotest
- ERROR: Queue item ignored: bad version found in '.../dist-upgrader_20070219.1234_all.tar.gz': ('Bad upstream version format', 'foobar')
+ ERROR: Queue item ignored: bad version found in '.../dist-upgrader_20070219.1234_all.tar.gz': Could not parse version: Bad upstream version format foobar
Check if the queue item remained in ACCEPTED and not cruft was
inserted in the archive:
=== removed file 'lib/lp/soyuz/doc/initialise-from-parent.txt'
--- lib/lp/soyuz/doc/initialise-from-parent.txt 2010-05-14 06:14:12 +0000
+++ lib/lp/soyuz/doc/initialise-from-parent.txt 1970-01-01 00:00:00 +0000
@@ -1,194 +0,0 @@
-Check the behaviour of the initialise_from_parent script. It basically
-calls IDistroSeries.initialiseFromParent method with experimental extra
-checks and tasks.
-
-We need to create an initialisable DistroSeries as a child of Ubuntu
-Hoary (we do it inside the ubuntutest distribution to avoid conflicts
-with other tests)
-
- >>> from canonical.launchpad.interfaces import IDistributionSet
- >>> from canonical.launchpad.ftests import login
-
- >>> login("foo.bar@xxxxxxxxxxxxx")
- >>> distribution_set = getUtility(IDistributionSet)
- >>> ubuntutest = distribution_set['ubuntutest']
- >>> ubuntu = distribution_set['ubuntu']
- >>> hoary = ubuntu['hoary']
-
- # XXX cprov 2006-05-29 bug=49133:
- # New distroseries should be provided by IDistribution.
- # This maybe affected by derivation design and is documented in bug.
-
- >>> foobuntu = ubuntutest.newSeries('foobuntu', 'FooBuntu',
- ... 'The Foobuntu', 'yeck', 'doom',
- ... '888', hoary, hoary.owner)
-
-
-The script will check that there are no NEEDSBUILD builds in the parent
-distroseries' release pocket, so we need to tweak the status of the NEEDSBUILD
-builds that exist in Ubuntu Hoary in the sampledata:
-
- >>> from lp.buildmaster.interfaces.buildbase import BuildStatus
- >>> from lp.registry.interfaces.pocket import PackagePublishingPocket
-
- >>> pending_builds = hoary.getBuildRecords(BuildStatus.NEEDSBUILD,
- ... pocket=PackagePublishingPocket.RELEASE)
- >>> for build in pending_builds:
- ... build.status = BuildStatus.FAILEDTOBUILD
-
- >>> import transaction
- >>> transaction.commit()
-
-
- >>> import subprocess
- >>> import os
- >>> import sys
- >>> from canonical.config import config
-
-
-Check if it fails for an already released distroseries:
-
- >>> script = os.path.join(config.root, "scripts", "ftpmaster-tools",
- ... "initialise-from-parent.py")
- >>> process = subprocess.Popen([sys.executable, script, "-vv",
- ... "breezy-autotest"],
- ... stdout=subprocess.PIPE,
- ... stderr=subprocess.PIPE,)
- >>> stdout, stderr = process.communicate()
- >>> process.returncode
- 1
- >>> print stderr
- DEBUG Acquiring lock
- DEBUG Initialising connection.
- DEBUG Check empty mutable queues in parentseries
- DEBUG Check for no pending builds in parentseries
- DEBUG Copying distroarchseries from parent and setting nominatedarchindep.
- Traceback (most recent call last):
- ...
- AssertionError: Can not copy distroarchseries from parent, there are already distroarchseries(s) initialised for this series.
- <BLANKLINE>
-
-
-Let's initialise the just created distroseries:
-
- >>> process = subprocess.Popen([sys.executable, script, "-vv",
- ... "-d", "ubuntutest", "foobuntu"],
- ... stdout=subprocess.PIPE,
- ... stderr=subprocess.PIPE,)
- >>> stdout, stderr = process.communicate()
- >>> process.returncode
- 0
- >>> print stderr
- DEBUG Acquiring lock
- DEBUG Initialising connection.
- DEBUG Check empty mutable queues in parentseries
- DEBUG Check for no pending builds in parentseries
- DEBUG Copying distroarchseries from parent and setting nominatedarchindep.
- DEBUG initialising from parent, copying publishing records.
- DEBUG Committing transaction.
- DEBUG Releasing lock
- <BLANKLINE>
-
-
-Checking the published sources and binaries of ubuntutest/foobuntu
-against its parent, ubuntu/hoary:
-
- >>> hoary_pmount_pubs = hoary.getPublishedReleases('pmount')
- >>> foobuntu_pmount_pubs = foobuntu.getPublishedReleases('pmount')
- >>> len(foobuntu_pmount_pubs) == len(hoary_pmount_pubs)
- True
-
- >>> hoary_i386_pmount_pubs = hoary['i386'].getReleasedPackages('pmount')
- >>> foobuntu_i386_pmount_pubs = (
- ... foobuntu['i386'].getReleasedPackages('pmount'))
- >>> len(foobuntu_i386_pmount_pubs) == len(hoary_i386_pmount_pubs)
- True
-
-Check how the publication records behave in a just-initialise distroseries.
-First we get a binarypackagerelease published in foobuntu:
-
- >>> pmount_binrel = (
- ... foobuntu['i386'].getReleasedPackages(
- ... 'pmount')[0].binarypackagerelease)
- >>> pmount_binrel.title
- u'pmount-0.1-1'
-
-Follow BPR.build and discover it was built in the parent series:
-
- >>> pmount_binrel.build.id
- 7
- >>> pmount_binrel.build.title
- u'i386 build of pmount 0.1-1 in ubuntu hoary RELEASE'
-
-Now we obtain the sourcepackagerelease from the build:
-
- >>> pmount_srcrel = pmount_binrel.build.source_package_release
- >>> pmount_srcrel.title
- u'pmount - 0.1-1'
-
-and check it the ISPR.getBuildByArch() would find out the same build
-record for foobuntu and it's parent series (hoary):
-
- >>> foobuntu_pmount = pmount_srcrel.getBuildByArch(
- ... foobuntu['i386'], foobuntu.main_archive)
- >>> hoary_pmount = pmount_srcrel.getBuildByArch(
- ... hoary['i386'], hoary.main_archive)
-
- >>> foobuntu_pmount.id == hoary_pmount.id
- True
-
-It means that queuebuilder doesn't need to create a new build record
-in for pmount_0.1-1 in foobuntu.
-
-In the other hand there is a newer source for pmount published in
-hoary and consequently in foobuntu:
-
-Note: This is a very unlikely situation, since ubuntu/hoary was marked
-as RELEASED before build pmount_0.1-2 in the sampledata. So when we
-try initialise a distroseries in another distribution based on hoary,
-since they have independent archives (pool), pmount_0.1-1 binary
-becomes a NBS (not build from source) since the pmount_0.1-1 source
-was superseded in hoary and won't be inherited by the initialised
-distroseries.
-
- >>> pmount_source = hoary.getSourcePackage('pmount').currentrelease
- >>> print pmount_source.title
- "pmount" 0.1-2 source package in The Hoary Hedgehog Release
-
- >>> pmount_source = foobuntu.getSourcePackage('pmount').currentrelease
- >>> print pmount_source.title
- "pmount" 0.1-2 source package in The Foobuntu
-
-
-Since pmount_0.1-2 source is published we can safely look up for the
-respective build record:
-
- >>> pmount_source.sourcepackagerelease.getBuildByArch(
- ... foobuntu['i386'], ubuntu.main_archive) is None
- True
-
-It's not present, Let's create it to check if getBuildByArch responds
-appropriately (we won't care about the source architecturehintlist in
-this test, see more details in buildd-queuebuilder)
-
- >>> from lp.registry.interfaces.pocket import PackagePublishingPocket
- >>> created_build = pmount_source.sourcepackagerelease.createBuild(
- ... foobuntu['i386'], PackagePublishingPocket.RELEASE,
- ... ubuntu.main_archive)
-
- >>> retrieved_build = pmount_source.sourcepackagerelease.getBuildByArch(
- ... foobuntu['i386'], ubuntu.main_archive)
-
- >>> retrieved_build.id == created_build.id
- True
-
- >>> pmount_source.sourcepackagerelease.getBuildByArch(
- ... foobuntu['hppa'], ubuntu.main_archive) is None
- True
-
-initialiseFromParent also copies the permitted source formats from the
-parent series.
-
- >>> from lp.soyuz.interfaces.sourcepackageformat import SourcePackageFormat
- >>> foobuntu.isSourcePackageFormatPermitted(SourcePackageFormat.FORMAT_1_0)
- True
=== modified file 'lib/lp/soyuz/doc/sampledata-setup.txt'
--- lib/lp/soyuz/doc/sampledata-setup.txt 2010-03-18 21:30:51 +0000
+++ lib/lp/soyuz/doc/sampledata-setup.txt 2010-08-18 11:41:24 +0000
@@ -1,5 +1,7 @@
= Sample data setup =
+# XXX: StevenK 2010-07-13 bug=617164: Calling utility scripts is bad
+
In order to run Soyuz locally on a development system, the sample data
must be cleaned up and customized a bit. This is done by a the script
utilities/soyuz-sampledata-setup.py.
=== modified file 'lib/lp/soyuz/doc/soyuz-set-of-uploads.txt'
--- lib/lp/soyuz/doc/soyuz-set-of-uploads.txt 2010-07-12 13:06:41 +0000
+++ lib/lp/soyuz/doc/soyuz-set-of-uploads.txt 2010-08-18 11:41:24 +0000
@@ -70,6 +70,10 @@
for the ubuntutest distribution.
>>> from lp.registry.model.distribution import Distribution
+ >>> from lp.registry.interfaces.pocket import PackagePublishingPocket
+ >>> from lp.soyuz.interfaces.queue import PackageUploadStatus
+ >>> from lp.soyuz.scripts.initialise_distroseries import (
+ ... InitialiseDistroSeries)
>>> from canonical.launchpad.database import LibraryFileAlias
>>> ubuntu = Distribution.byName('ubuntu')
>>> breezy_autotest = ubuntu['breezy-autotest']
@@ -78,13 +82,11 @@
... 'breezy', 'Breezy Badger', 'The Breezy Badger',
... 'Black and White', 'Someone', '5.10', breezy_autotest,
... breezy_autotest.owner)
- >>> breezy_i386 = breezy.newArch(
- ... 'i386', breezy_autotest['i386'].processorfamily, True, breezy.owner)
- >>> breezy.nominatedarchindep = breezy_i386
+ >>> ids = InitialiseDistroSeries(breezy)
+ >>> ids.initialise()
>>> breezy.changeslist = 'breezy-changes@xxxxxxxxxx'
- >>> breezy.initialiseFromParent()
>>> fake_chroot = LibraryFileAlias.get(1)
- >>> unused = breezy_i386.addOrUpdateChroot(fake_chroot)
+ >>> unused = breezy['i386'].addOrUpdateChroot(fake_chroot)
Add disk content for file inherited from ubuntu/breezy-autotest:
=== modified file 'lib/lp/soyuz/interfaces/archive.py'
--- lib/lp/soyuz/interfaces/archive.py 2010-08-05 02:29:25 +0000
+++ lib/lp/soyuz/interfaces/archive.py 2010-08-18 11:41:24 +0000
@@ -52,7 +52,7 @@
from lazr.enum import DBEnumeratedType, DBItem
from canonical.launchpad import _
-from canonical.launchpad.fields import (
+from lp.services.fields import (
PersonChoice, PublicPersonChoice, StrippedTextLine)
from canonical.launchpad.interfaces.launchpad import IPrivacy
from lp.app.errors import NameLookupFailed
=== modified file 'lib/lp/soyuz/interfaces/archivepermission.py'
--- lib/lp/soyuz/interfaces/archivepermission.py 2010-03-08 17:06:41 +0000
+++ lib/lp/soyuz/interfaces/archivepermission.py 2010-08-18 11:41:24 +0000
@@ -20,7 +20,7 @@
from lazr.enum import DBEnumeratedType, DBItem
from canonical.launchpad import _
-from canonical.launchpad.fields import PublicPersonChoice
+from lp.services.fields import PublicPersonChoice
from lp.soyuz.interfaces.archive import IArchive
from lp.soyuz.interfaces.component import IComponent
from lp.soyuz.interfaces.packageset import IPackageset
=== modified file 'lib/lp/soyuz/interfaces/archivesubscriber.py'
--- lib/lp/soyuz/interfaces/archivesubscriber.py 2010-07-27 19:17:43 +0000
+++ lib/lp/soyuz/interfaces/archivesubscriber.py 2010-08-18 11:41:24 +0000
@@ -20,7 +20,7 @@
from lazr.enum import DBEnumeratedType, DBItem
from canonical.launchpad import _
-from canonical.launchpad.fields import PersonChoice
+from lp.services.fields import PersonChoice
from lp.soyuz.interfaces.archive import IArchive
from lp.registry.interfaces.person import IPerson
from lazr.restful.declarations import export_as_webservice_entry, exported
=== modified file 'lib/lp/soyuz/interfaces/binarypackagename.py'
--- lib/lp/soyuz/interfaces/binarypackagename.py 2010-01-12 19:02:09 +0000
+++ lib/lp/soyuz/interfaces/binarypackagename.py 2010-08-18 11:41:24 +0000
@@ -14,7 +14,7 @@
]
from zope.schema import Int, TextLine
-from zope.interface import Interface, Attribute
+from zope.interface import Interface
from canonical.launchpad import _
from canonical.launchpad.validators.name import name_validator
@@ -26,11 +26,6 @@
name = TextLine(title=_('Valid Binary package name'),
required=True, constraint=name_validator)
- binarypackages = Attribute('binarypackages')
-
- def nameSelector(sourcepackage=None, selected=None):
- """Return browser-ready HTML to select a Binary Package Name"""
-
def __unicode__():
"""Return the name"""
=== renamed directory 'lib/canonical/launchpad/javascript/soyuz' => 'lib/lp/soyuz/javascript'
=== modified file 'lib/lp/soyuz/javascript/archivesubscribers_index.js'
--- lib/canonical/launchpad/javascript/soyuz/archivesubscribers_index.js 2009-11-24 09:30:01 +0000
+++ lib/lp/soyuz/javascript/archivesubscribers_index.js 2010-08-18 11:41:24 +0000
@@ -3,19 +3,20 @@
*
* Enhancements for adding ppa subscribers.
*
- * @module ArchiveSubscribersIndex
+ * @module soyuz
+ * @submodule archivesubscribers_index
* @requires event, node, oop
*/
-YUI.add('soyuz.archivesubscribers_index', function(Y) {
+YUI.add('lp.soyuz.archivesubscribers_index', function(Y) {
-var soyuz = Y.namespace('soyuz');
+var namespace = Y.namespace('lp.soyuz.archivesubscribers_index');
/*
* Setup the style and click handler for the add subscriber link.
*
* @method setup_archivesubscribers_index
*/
-Y.soyuz.setup_archivesubscribers_index = function() {
+namespace.setup_archivesubscribers_index = function() {
// If there are no errors then we hide the add-subscriber row and
// potentially the whole table if there are no subscribers.
if (Y.Lang.isNull(Y.one('p.error.message'))) {
=== modified file 'lib/lp/soyuz/javascript/base.js'
--- lib/canonical/launchpad/javascript/soyuz/base.js 2009-11-23 19:29:02 +0000
+++ lib/lp/soyuz/javascript/base.js 2010-08-18 11:41:24 +0000
@@ -4,24 +4,21 @@
* Auxiliary functions used in Soyuz pages
*
* @module soyuz
- * @submodule soyuz-base
+ * @submodule base
* @namespace soyuz
* @requires yahoo, node
*/
-YUI.add('soyuz-base', function(Y) {
+YUI.add('lp.soyuz.base', function(Y) {
-/*
- * Define the 'Y.soyuz' namespace
- */
-var soyuz = Y.namespace('soyuz');
+var namespace = Y.namespace('lp.soyuz.base');
/*
* Return a node containing a standard failure message to be used
* in XHR-based page updates.
*/
-soyuz.makeFailureNode = function (text, handler) {
+namespace.makeFailureNode = function (text, handler) {
var failure_message = Y.Node.create('<p>');
failure_message.addClass('update-failure-message');
@@ -44,7 +41,7 @@
* Return a node containing a standard in-progress message to be used
* in XHR-based page updates.
*/
-soyuz.makeInProgressNode = function (text) {
+namespace.makeInProgressNode = function (text) {
var in_progress_message = Y.Node.create('<p>');
var message = Y.Node.create('<span>');
=== modified file 'lib/lp/soyuz/javascript/lp_dynamic_dom_updater.js'
--- lib/canonical/launchpad/javascript/soyuz/lp_dynamic_dom_updater.js 2009-11-24 16:11:43 +0000
+++ lib/lp/soyuz/javascript/lp_dynamic_dom_updater.js 2010-08-18 11:41:24 +0000
@@ -5,16 +5,16 @@
* can be plugged in to a DOM subtree, so that the subtree can update itself
* regularly using the Launchpad API.
*
- * @module DynamicDomUpdater
+ * @module soyuz
+ * @submodule dynamic_dom_updater
* @requires yahoo, node, plugin, LP
*/
-YUI.add('soyuz.dynamic_dom_updater', function(Y) {
+YUI.add('lp.soyuz.dynamic_dom_updater', function(Y) {
-/* Ensure that the Y.lp namespace exists. */
-var lp = Y.namespace('lp');
+var namespace = Y.namespace('lp.soyuz.dynamic_dom_updater');
/**
- * The DomUpdater class provides the ability to plugin functionality
+ * The DomUpdater class provides the ability to plugin functionality
* to a DOM subtree so that it can update itself when given data in an
* expected format.
*
@@ -74,12 +74,12 @@
});
/*
- * Ensure that the DomUpdater is available within the Y.lp namespace.
+ * Ensure that the DomUpdater is available within the namespace.
*/
- Y.lp.DomUpdater = DomUpdater;
+ namespace.DomUpdater = DomUpdater;
/**
- * The DynamicDomUpdater class provides the ability to plug functionality
+ * The DynamicDomUpdater class provides the ability to plug functionality
* into a DOM subtree so that it can update itself using an LP api method.
*
* For example:
@@ -94,7 +94,7 @@
* Once configured, the 'table' dom subtree will now update itself
* by calling the user defined domUpdateFunction (with a default interval
* of 6000ms) with the result of the LPs api call.
- *
+ *
* @class DynamicDomUpdater
* @extends DomUpdater
* @constructor
@@ -326,7 +326,7 @@
}
if (actual_interval_updated) {
- Y.log("Actual poll interval updated to " +
+ Y.log("Actual poll interval updated to " +
this._actual_interval + "ms.");
}
@@ -350,9 +350,8 @@
});
/*
- * Ensure that the DynamicDomUpdater is available within the Y.lp
- * namespace.
+ * Ensure that the DynamicDomUpdater is available within the namespace.
*/
- Y.lp.DynamicDomUpdater = DynamicDomUpdater;
+ namespace.DynamicDomUpdater = DynamicDomUpdater;
}, "0.1", {"requires":["node", "plugin"]});
=== modified file 'lib/lp/soyuz/javascript/tests/archivesubscribers_index.js'
--- lib/canonical/launchpad/javascript/soyuz/tests/archivesubscribers_index.js 2010-07-26 13:42:32 +0000
+++ lib/lp/soyuz/javascript/tests/archivesubscribers_index.js 2010-08-18 11:41:24 +0000
@@ -2,11 +2,11 @@
GNU Affero General Public License version 3 (see the file LICENSE). */
YUI({
- base: '../../../icing/yui/',
+ base: '../../../../canonical/launchpad/icing/yui/',
filter: 'raw',
combine: false
}).use(
- 'test', 'console', 'soyuz.archivesubscribers_index', function(Y) {
+ 'test', 'console', 'lp.soyuz.archivesubscribers_index', function(Y) {
var Assert = Y.Assert; // For easy access to isTrue(), etc.
@@ -68,7 +68,7 @@
test_add_row_hidden_after_setup: function() {
// The add subscriber row is hidden during setup.
- Y.soyuz.setup_archivesubscribers_index();
+ Y.lp.soyuz.archivesubscribers_index.setup_archivesubscribers_index();
Assert.areEqual(
'none', this.add_subscriber_row.getStyle('display'),
'The add subscriber row should be hidden during setup.');
@@ -76,7 +76,7 @@
test_subscribers_section_displayed_after_setup: function() {
// The subscribers div normally remains displayed after setup.
- Y.soyuz.setup_archivesubscribers_index();
+ Y.lp.soyuz.archivesubscribers_index.setup_archivesubscribers_index();
Assert.areEqual(
'block', this.subscribers_div.getStyle('display'),
'The subscribers div should remain displayed after setup.');
@@ -87,7 +87,7 @@
// Add a paragraph with the no-subscribers id.
this.error_div.set('innerHTML', '<p id="no-subscribers">blah</p>');
- Y.soyuz.setup_archivesubscribers_index();
+ Y.lp.soyuz.archivesubscribers_index.setup_archivesubscribers_index();
Assert.areEqual(
'none', this.subscribers_div.getStyle('display'),
'The subscribers div should be hidden when there are ' +
@@ -100,7 +100,7 @@
// Add an error paragraph.
this.error_div.set('innerHTML', '<p class="error message">Blah</p>');
- Y.soyuz.setup_archivesubscribers_index();
+ Y.lp.soyuz.archivesubscribers_index.setup_archivesubscribers_index();
Assert.areEqual(
'table-row', this.add_subscriber_row.getStyle('display'),
'The add subscriber row should not be hidden if there are ' +
@@ -110,7 +110,7 @@
test_add_access_link_added_after_setup: function() {
// The 'Add access' link is created during setup.
- Y.soyuz.setup_archivesubscribers_index();
+ Y.lp.soyuz.archivesubscribers_index.setup_archivesubscribers_index();
Assert.areEqual(
'<a class="js-action sprite add" href="#">Add access</a>',
this.add_subscriber_placeholder.get('innerHTML'),
@@ -119,7 +119,7 @@
test_click_add_access_displays_add_row: function() {
// The add subscriber row is displayed after clicking 'Add access'.
- Y.soyuz.setup_archivesubscribers_index();
+ Y.lp.soyuz.archivesubscribers_index.setup_archivesubscribers_index();
var link_node = this.add_subscriber_placeholder.one('a');
Assert.areEqual(
'Add access', link_node.get('innerHTML'));
@@ -133,6 +133,11 @@
}
}));
+Y.Test.Runner.on('complete', function(data) {
+ status_node = Y.Node.create(
+ '<p id="complete">Test status: complete</p>');
+ Y.get('body').appendChild(status_node);
+});
Y.Test.Runner.add(suite);
var yconsole = new Y.Console({
=== modified file 'lib/lp/soyuz/javascript/tests/lp_dynamic_dom_updater.js'
--- lib/canonical/launchpad/javascript/soyuz/tests/lp_dynamic_dom_updater.js 2010-07-26 13:42:32 +0000
+++ lib/lp/soyuz/javascript/tests/lp_dynamic_dom_updater.js 2010-08-18 11:41:24 +0000
@@ -2,10 +2,10 @@
GNU Affero General Public License version 3 (see the file LICENSE). */
YUI({
- base: '../../../icing/yui/',
+ base: '../../../../canonical/launchpad/icing/yui/',
filter: 'raw',
combine: false
- }).use('test', 'console', 'soyuz.dynamic_dom_updater', function(Y) {
+ }).use('test', 'console', 'lp.soyuz.dynamic_dom_updater', function(Y) {
var Assert = Y.Assert; // For easy access to isTrue(), etc.
@@ -31,14 +31,15 @@
this.eg_div.updater,
"Sanity check: initially there is no updater attribute.");
- this.eg_div.plug(Y.lp.DomUpdater, this.config);
+ this.eg_div.plug(
+ Y.lp.soyuz.dynamic_dom_updater.DomUpdater, this.config);
Assert.isNotUndefined(
this.eg_div.updater,
"After plugging, the object has an 'updater' attribute.");
Assert.isInstanceOf(
- Y.lp.DomUpdater,
+ Y.lp.soyuz.dynamic_dom_updater.DomUpdater,
this.eg_div.updater,
"DomUpdater was not plugged correctly.");
},
@@ -52,7 +53,8 @@
"Sanity check that the innerHTML of our example div has not" +
"been modified.");
- this.eg_div.plug(Y.lp.DomUpdater, this.config);
+ this.eg_div.plug(
+ Y.lp.soyuz.dynamic_dom_updater.DomUpdater, this.config);
this.eg_div.updater.update({msg: "Boo. I've changed."});
Assert.areEqual(
"Boo. I've changed.",
@@ -97,14 +99,15 @@
this.eg_div.updater,
"Sanity check: initially there is no updater attribute.");
- this.eg_div.plug(Y.lp.DynamicDomUpdater, this.config);
+ this.eg_div.plug(
+ Y.lp.soyuz.dynamic_dom_updater.DynamicDomUpdater, this.config);
Assert.isNotUndefined(
this.eg_div.updater,
"After plugging, the object has an 'updater' attribute.");
Assert.isInstanceOf(
- Y.lp.DynamicDomUpdater,
+ Y.lp.soyuz.dynamic_dom_updater.DynamicDomUpdater,
this.eg_div.updater,
"DynamicDomUpdater was not plugged correctly.");
},
@@ -113,7 +116,8 @@
// Requests to the LP API should only be re-issued if a successful
// response was received previously.
- this.eg_div.plug(Y.lp.DynamicDomUpdater, this.config);
+ this.eg_div.plug(
+ Y.lp.soyuz.dynamic_dom_updater.DynamicDomUpdater, this.config);
// Verify that our expectation of only one named_get was met:
this.wait(function() {
@@ -132,7 +136,8 @@
callCount: 2
});
- this.eg_div.plug(Y.lp.DynamicDomUpdater, this.config);
+ this.eg_div.plug(
+ Y.lp.soyuz.dynamic_dom_updater.DynamicDomUpdater, this.config);
// Wait 5ms just to ensure that the DynamicDomUpdater finishes
// its initialization.
@@ -151,7 +156,8 @@
// The actual polling interval is doubled if the elapsed time
// for the previous request was greater than the
// long_processing_time attribute.
- this.eg_div.plug(Y.lp.DynamicDomUpdater, this.config);
+ this.eg_div.plug(
+ Y.lp.soyuz.dynamic_dom_updater.DynamicDomUpdater, this.config);
// Wait 5ms just to ensure that the DynamicDomUpdater finishes
// its initialization.
@@ -175,7 +181,8 @@
// The actual polling interval remains unchanged if the elapsed time
// for the previous request was within the short/long processing
// times.
- this.eg_div.plug(Y.lp.DynamicDomUpdater, this.config);
+ this.eg_div.plug(
+ Y.lp.soyuz.dynamic_dom_updater.DynamicDomUpdater, this.config);
// Wait 5ms just to ensure that the DynamicDomUpdater finishes
// its initialization.
@@ -200,7 +207,8 @@
// The actual polling interval is halved if the elapsed time
// for the previous request was less than the short_processing_time
- this.eg_div.plug(Y.lp.DynamicDomUpdater, this.config);
+ this.eg_div.plug(
+ Y.lp.soyuz.dynamic_dom_updater.DynamicDomUpdater, this.config);
// Wait 5ms just to ensure that the DynamicDomUpdater finishes
// its initialization.
@@ -220,7 +228,8 @@
Assert.areEqual(
8,
this.eg_div.updater._actual_interval,
- "Poll interval is halved if request is faster than expected.");
+ "Poll interval is halved if request is faster than " +
+ "expected.");
}, 5);
},
@@ -229,7 +238,8 @@
// Even if the processing time is quick, the actual interval
// is never smaller than the interval set in the configuration.
- this.eg_div.plug(Y.lp.DynamicDomUpdater, this.config);
+ this.eg_div.plug(
+ Y.lp.soyuz.dynamic_dom_updater.DynamicDomUpdater, this.config);
// Wait 5ms just to ensure that the DynamicDomUpdater finishes
// its initialization.
@@ -259,6 +269,11 @@
}));
+ Y.Test.Runner.on('complete', function(data) {
+ status_node = Y.Node.create(
+ '<p id="complete">Test status: complete</p>');
+ Y.get('body').appendChild(status_node);
+ });
Y.Test.Runner.add(suite);
var yconsole = new Y.Console({
=== renamed file 'lib/canonical/launchpad/javascript/soyuz/tests/archivesubscribers_index.html' => 'lib/lp/soyuz/javascript/tests/test_archivesubscribers_index.html'
--- lib/canonical/launchpad/javascript/soyuz/tests/archivesubscribers_index.html 2010-07-26 13:42:32 +0000
+++ lib/lp/soyuz/javascript/tests/test_archivesubscribers_index.html 2010-08-18 11:41:24 +0000
@@ -4,11 +4,11 @@
<title>Launchpad ArchiveSubscriberIndex</title>
<!-- YUI 3.0 Setup -->
- <script type="text/javascript" src="../../../icing/yui/yui/yui.js"></script>
- <link rel="stylesheet" href="../../../icing/yui/cssreset/reset.css"/>
- <link rel="stylesheet" href="../../../icing/yui/cssfonts/fonts.css"/>
- <link rel="stylesheet" href="../../../icing/yui/cssbase/base.css"/>
- <link rel="stylesheet" href="../../test.css" />
+ <script type="text/javascript" src="../../../../canonical/launchpad/icing/yui/yui/yui.js"></script>
+ <link rel="stylesheet" href="../../../../canonical/launchpad/icing/yui/cssreset/reset.css"/>
+ <link rel="stylesheet" href="../../../../canonical/launchpad/icing/yui/cssfonts/fonts.css"/>
+ <link rel="stylesheet" href="../../../../canonical/launchpad/icing/yui/cssbase/base.css"/>
+ <link rel="stylesheet" href="../../../../canonical/launchpad/test.css" />
<!-- The module under test -->
<script type="text/javascript" src="../archivesubscribers_index.js"></script>
=== renamed file 'lib/canonical/launchpad/javascript/soyuz/tests/lp_dynamic_dom_updater.html' => 'lib/lp/soyuz/javascript/tests/test_lp_dynamic_dom_updater.html'
--- lib/canonical/launchpad/javascript/soyuz/tests/lp_dynamic_dom_updater.html 2010-07-26 13:42:32 +0000
+++ lib/lp/soyuz/javascript/tests/test_lp_dynamic_dom_updater.html 2010-08-18 11:41:24 +0000
@@ -4,11 +4,11 @@
<title>Launchpad DynamicDomUpdater</title>
<!-- YUI 3.0 Setup -->
- <script type="text/javascript" src="../../../icing/yui/yui/yui.js"></script>
- <link rel="stylesheet" href="../../../icing/yui/cssreset/reset.css"/>
- <link rel="stylesheet" href="../../../icing/yui/cssfonts/fonts.css"/>
- <link rel="stylesheet" href="../../../icing/yui/cssbase/base.css"/>
- <link rel="stylesheet" href="../../test.css" />
+ <script type="text/javascript" src="../../../../canonical/launchpad/icing/yui/yui/yui.js"></script>
+ <link rel="stylesheet" href="../../../../canonical/launchpad/icing/yui/cssreset/reset.css"/>
+ <link rel="stylesheet" href="../../../../canonical/launchpad/icing/yui/cssfonts/fonts.css"/>
+ <link rel="stylesheet" href="../../../../canonical/launchpad/icing/yui/cssbase/base.css"/>
+ <link rel="stylesheet" href="../../../../canonical/launchpad/javascript/test.css" />
<!-- The module under test -->
<script type="text/javascript" src="../lp_dynamic_dom_updater.js"></script>
=== modified file 'lib/lp/soyuz/javascript/update_archive_build_statuses.js'
--- lib/canonical/launchpad/javascript/soyuz/update_archive_build_statuses.js 2009-11-24 09:30:01 +0000
+++ lib/lp/soyuz/javascript/update_archive_build_statuses.js 2010-08-18 11:41:24 +0000
@@ -11,7 +11,7 @@
* The second is the Archive/PPA source package table, the configuration of
Follow ups