launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #03223
[Merge] lp:~danilo/launchpad/db-add-subscription-link into lp:launchpad/db-devel
Gary Poster has proposed merging lp:~danilo/launchpad/db-add-subscription-link into lp:launchpad/db-devel.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~danilo/launchpad/db-add-subscription-link/+merge/56622
This branch merges in some conflicting devel branches into db-devel, and resolves the conflicts.
--
https://code.launchpad.net/~danilo/launchpad/db-add-subscription-link/+merge/56622
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~danilo/launchpad/db-add-subscription-link into lp:launchpad/db-devel.
=== modified file 'lib/lp/bugs/browser/structuralsubscription.py'
--- lib/lp/bugs/browser/structuralsubscription.py 2011-04-06 00:14:46 +0000
+++ lib/lp/bugs/browser/structuralsubscription.py 2011-04-06 17:31:46 +0000
@@ -33,6 +33,7 @@
from zope.traversing.browser import absoluteURL
from canonical.launchpad.webapp.authorization import check_permission
+from canonical.launchpad.webapp.interfaces import NoCanonicalUrl
from canonical.launchpad.webapp.menu import (
enabled_with_permission,
Link,
@@ -390,10 +391,13 @@
expose_user_administered_teams_to_js(request, user, context)
expose_enum_to_js(request, BugTaskImportance, 'importances')
expose_enum_to_js(request, BugTaskStatus, 'statuses')
- if subscriptions is None:
+ if subscriptions is None or len(list(subscriptions)) == 0:
subscriptions = []
+ target = context
+ else:
+ target = None
expose_user_subscriptions_to_js(
- user, subscriptions, request)
+ user, subscriptions, request, target)
def expose_enum_to_js(request, enum, name):
@@ -421,11 +425,12 @@
info.append({
'link': absoluteURL(team, api_request),
'title': team.title,
+ 'url': canonical_url(team),
})
IJSONRequestCache(request).objects['administratedTeams'] = info
-def expose_user_subscriptions_to_js(user, subscriptions, request):
+def expose_user_subscriptions_to_js(user, subscriptions, request, target=None):
"""Make the user's subscriptions available to JavaScript."""
info = {}
api_request = IWebServiceClientRequest(request)
@@ -433,6 +438,19 @@
administered_teams = []
else:
administered_teams = user.getAdministratedTeams()
+
+ if target is not None:
+ try:
+ # No subscriptions, which means we are on a target
+ # subscriptions page. Let's at least provide target details.
+ target_info = {}
+ target_info['title'] = target.title
+ target_info['url'] = canonical_url(target, rootsite='mainsite')
+ IJSONRequestCache(request).objects['target_info'] = target_info
+ except NoCanonicalUrl:
+ # We export nothing if the target implements no canonical URL.
+ pass
+
for subscription in subscriptions:
target = subscription.target
record = info.get(target)
=== modified file 'lib/lp/bugs/browser/tests/test_expose.py'
--- lib/lp/bugs/browser/tests/test_expose.py 2011-04-05 18:29:45 +0000
+++ lib/lp/bugs/browser/tests/test_expose.py 2011-04-06 17:31:46 +0000
@@ -132,7 +132,7 @@
self.assertThat(len(team_info), Equals(expected_number_teams))
# The items info consist of a dictionary with link and title keys.
for i in range(expected_number_teams):
- self.assertThat(team_info[i], KeysEqual('link', 'title'))
+ self.assertThat(team_info[i], KeysEqual('link', 'title', 'url'))
# The link is the title of the team.
self.assertThat(
team_info[0]['title'], Equals(u'Bug Supervisor Sub Team'))
@@ -165,7 +165,7 @@
self.assertThat(len(team_info), Equals(expected_number_teams))
# The items info consist of a dictionary with link and title keys.
for i in range(expected_number_teams):
- self.assertThat(team_info[i], KeysEqual('link', 'title'))
+ self.assertThat(team_info[i], KeysEqual('link', 'title', 'url'))
# The link is the title of the team.
self.assertThat(
team_info[0]['title'], Equals(u'Bug Supervisor Sub Team'))
@@ -193,7 +193,7 @@
self.assertThat(len(team_info), Equals(expected_number_teams))
# The items info consist of a dictionary with link and title keys.
for i in range(expected_number_teams):
- self.assertThat(team_info[i], KeysEqual('link', 'title'))
+ self.assertThat(team_info[i], KeysEqual('link', 'title', 'url'))
# The link is the title of the team.
self.assertThat(
team_info[0]['title'], Equals(u'Bug Supervisor Sub Team'))
=== modified file 'lib/lp/bugs/templates/bugtarget-subscription-list.pt'
--- lib/lp/bugs/templates/bugtarget-subscription-list.pt 2011-03-29 22:34:04 +0000
+++ lib/lp/bugs/templates/bugtarget-subscription-list.pt 2011-04-06 17:31:46 +0000
@@ -17,9 +17,13 @@
request/features/malone.advanced-structural-subscriptions.enabled">
LPS.use('lp.registry.structural_subscription', function(Y) {
var module = Y.lp.registry.structural_subscription;
+ var config = {
+ content_box: "#structural-subscription-content-box",
+ add_filter_description: true};
Y.on('domready', function() {
- module.setup_bug_subscriptions(
- {content_box: "#structural-subscription-content-box"})
+ module.setup_bug_subscriptions(config);
+ module.setup_subscription_link(
+ config, '#create-new-subscription');
});
});
</script>
@@ -31,6 +35,12 @@
<div id="maincontent">
<div id="nonportlets" class="readable">
+ <div tal:condition="
+ features/malone.advanced-structural-subscriptions.enabled">
+ <a class="sprite add" id="create-new-subscription"
+ href="#">Add a subscription</a>
+ </div>
+
<div id="subscription-listing"></div>
<div id="structural-subscription-content-box"></div>
=== modified file 'lib/lp/code/interfaces/codehosting.py'
--- lib/lp/code/interfaces/codehosting.py 2011-02-24 15:30:54 +0000
+++ lib/lp/code/interfaces/codehosting.py 2011-04-06 17:31:46 +0000
@@ -8,6 +8,7 @@
__metaclass__ = type
__all__ = [
'BRANCH_ALIAS_PREFIX',
+ 'BRANCH_ID_ALIAS_PREFIX',
'BRANCH_TRANSPORT',
'compose_public_url',
'CONTROL_TRANSPORT',
@@ -55,6 +56,9 @@
# The path prefix for getting at branches via their short name.
BRANCH_ALIAS_PREFIX = '+branch'
+# The path prefix for getting at branches via their id.
+BRANCH_ID_ALIAS_PREFIX = '+branch-id'
+
# The scheme types that are supported for codehosting.
SUPPORTED_SCHEMES = 'bzr+ssh', 'http'
=== modified file 'lib/lp/code/model/branch.py'
--- lib/lp/code/model/branch.py 2011-03-29 00:11:57 +0000
+++ lib/lp/code/model/branch.py 2011-04-06 17:31:46 +0000
@@ -118,7 +118,10 @@
from lp.code.interfaces.branchnamespace import IBranchNamespacePolicy
from lp.code.interfaces.branchpuller import IBranchPuller
from lp.code.interfaces.branchtarget import IBranchTarget
-from lp.code.interfaces.codehosting import compose_public_url
+from lp.code.interfaces.codehosting import (
+ BRANCH_ID_ALIAS_PREFIX,
+ compose_public_url,
+ )
from lp.code.interfaces.seriessourcepackagebranch import (
IFindOfficialBranchLinks,
)
@@ -997,6 +1000,17 @@
self.last_mirror_attempt = UTC_NOW
self.next_mirror_time = None
+ def _findStackedBranch(self, stacked_on_location):
+ location = stacked_on_location.strip('/')
+ if location.startswith(BRANCH_ID_ALIAS_PREFIX + '/'):
+ try:
+ branch_id = int(location.split('/', 1)[1])
+ except (ValueError, IndexError):
+ return None
+ return getUtility(IBranchLookup).get(branch_id)
+ else:
+ return getUtility(IBranchLookup).getByUniqueName(location)
+
def branchChanged(self, stacked_on_location, last_revision_id,
control_format, branch_format, repository_format):
"""See `IBranch`."""
@@ -1004,8 +1018,7 @@
if stacked_on_location == '' or stacked_on_location is None:
stacked_on_branch = None
else:
- stacked_on_branch = getUtility(IBranchLookup).getByUniqueName(
- stacked_on_location.strip('/'))
+ stacked_on_branch = self._findStackedBranch(stacked_on_location)
if stacked_on_branch is None:
self.mirror_status_message = (
'Invalid stacked on location: ' + stacked_on_location)
=== modified file 'lib/lp/code/model/branchlookup.py'
--- lib/lp/code/model/branchlookup.py 2011-02-24 15:30:54 +0000
+++ lib/lp/code/model/branchlookup.py 2011-04-06 17:31:46 +0000
@@ -46,6 +46,7 @@
ILinkedBranchTraverser,
)
from lp.code.interfaces.branchnamespace import IBranchNamespaceSet
+from lp.code.interfaces.codehosting import BRANCH_ID_ALIAS_PREFIX
from lp.code.interfaces.linkedbranch import get_linked_to_branch
from lp.code.model.branch import Branch
from lp.registry.errors import (
@@ -288,14 +289,29 @@
return None
return self._getBranchInNamespace(namespace_data, branch_name)
- def getIdAndTrailingPath(self, path, from_slave=False):
- """See `IBranchLookup`. """
- if from_slave:
- store = ISlaveStore(Branch)
+ def _getIdAndTrailingPathByIdAlias(self, store, path):
+ """Query by the integer id."""
+ parts = path.split('/', 2)
+ try:
+ branch_id = int(parts[1])
+ except (ValueError, IndexError):
+ return None, None
+ result = store.find(
+ (Branch.id),
+ Branch.id == branch_id,
+ Branch.private == False).one()
+ if result is None:
+ return None, None
else:
- store = IMasterStore(Branch)
+ try:
+ return branch_id, '/' + parts[2]
+ except IndexError:
+ return branch_id, ''
+
+ def _getIdAndTrailingPathByUniqueName(self, store, path):
+ """Query based on the unique name."""
prefixes = []
- for first, second in iter_split(path[1:], '/'):
+ for first, second in iter_split(path, '/'):
prefixes.append(first)
result = store.find(
(Branch.id, Branch.unique_name),
@@ -304,9 +320,21 @@
return None, None
else:
branch_id, unique_name = result
- trailing = path[len(unique_name) + 1:]
+ trailing = path[len(unique_name):]
return branch_id, trailing
+ def getIdAndTrailingPath(self, path, from_slave=False):
+ """See `IBranchLookup`. """
+ if from_slave:
+ store = ISlaveStore(Branch)
+ else:
+ store = IMasterStore(Branch)
+ path = path.lstrip('/')
+ if path.startswith(BRANCH_ID_ALIAS_PREFIX):
+ return self._getIdAndTrailingPathByIdAlias(store, path)
+ else:
+ return self._getIdAndTrailingPathByUniqueName(store, path)
+
def _getBranchInNamespace(self, namespace_data, branch_name):
if namespace_data['product'] == '+junk':
return self._getPersonalBranch(
=== modified file 'lib/lp/code/model/tests/test_branch.py'
--- lib/lp/code/model/tests/test_branch.py 2011-03-29 00:11:57 +0000
+++ lib/lp/code/model/tests/test_branch.py 2011-04-06 17:31:46 +0000
@@ -79,6 +79,7 @@
)
from lp.code.interfaces.branchnamespace import IBranchNamespaceSet
from lp.code.interfaces.branchrevision import IBranchRevision
+from lp.code.interfaces.codehosting import BRANCH_ID_ALIAS_PREFIX
from lp.code.interfaces.linkedbranch import ICanHasLinkedBranch
from lp.code.interfaces.seriessourcepackagebranch import (
IFindOfficialBranchLinks,
@@ -180,6 +181,17 @@
stacked_on.unique_name, '', *self.arbitrary_formats)
self.assertEqual(stacked_on, branch.stacked_on)
+ def test_branchChanged_sets_stacked_on_branch_id_alias(self):
+ # branchChanged sets the stacked_on attribute based on the id of the
+ # branch if it is valid.
+ branch = self.factory.makeAnyBranch()
+ stacked_on = self.factory.makeAnyBranch()
+ login_person(branch.owner)
+ stacked_on_location = '/%s/%s' % (
+ BRANCH_ID_ALIAS_PREFIX, stacked_on.id)
+ branch.branchChanged(stacked_on_location, '', *self.arbitrary_formats)
+ self.assertEqual(stacked_on, branch.stacked_on)
+
def test_branchChanged_unsets_stacked_on(self):
# branchChanged clears the stacked_on attribute on the branch if '' is
# passed in as the stacked_on location.
=== modified file 'lib/lp/code/model/tests/test_branchlookup.py'
--- lib/lp/code/model/tests/test_branchlookup.py 2010-10-24 21:00:11 +0000
+++ lib/lp/code/model/tests/test_branchlookup.py 2011-04-06 17:31:46 +0000
@@ -25,6 +25,7 @@
ILinkedBranchTraverser,
)
from lp.code.interfaces.branchnamespace import get_branch_namespace
+from lp.code.interfaces.codehosting import BRANCH_ID_ALIAS_PREFIX
from lp.code.interfaces.linkedbranch import ICanHasLinkedBranch
from lp.registry.errors import (
NoSuchDistroSeries,
@@ -38,6 +39,7 @@
)
from lp.registry.interfaces.productseries import NoSuchProductSeries
from lp.testing import (
+ person_logged_in,
run_with_login,
TestCaseWithFactory,
)
@@ -118,6 +120,48 @@
'/' + branch.unique_name + '/' + path)
self.assertEqual((branch.id, '/' + path), result)
+ def test_branch_id_alias(self):
+ # The prefix by itself returns no branch, and no path.
+ path = BRANCH_ID_ALIAS_PREFIX
+ result = self.branch_set.getIdAndTrailingPath('/' + path)
+ self.assertEqual((None, None), result)
+
+ def test_branch_id_alias_not_int(self):
+ # The prefix followed by a non-integer returns no branch and no path.
+ path = BRANCH_ID_ALIAS_PREFIX + '/foo'
+ result = self.branch_set.getIdAndTrailingPath('/' + path)
+ self.assertEqual((None, None), result)
+
+ def test_branch_id_alias_private(self):
+ # Private branches are not found at all (this is for anonymous access)
+ owner = self.factory.makePerson()
+ branch = self.factory.makeAnyBranch(owner=owner, private=True)
+ with person_logged_in(owner):
+ path = '/%s/%s' % (BRANCH_ID_ALIAS_PREFIX, branch.id)
+ result = self.branch_set.getIdAndTrailingPath(path)
+ self.assertEqual((None, None), result)
+
+ def test_branch_id_alias_public(self):
+ # Public branches can be accessed.
+ branch = self.factory.makeAnyBranch()
+ path = '/%s/%s' % (BRANCH_ID_ALIAS_PREFIX, branch.id)
+ result = self.branch_set.getIdAndTrailingPath(path)
+ self.assertEqual((branch.id, ''), result)
+
+ def test_branch_id_alias_public_slash(self):
+ # A trailing slash is returned as the extra path.
+ branch = self.factory.makeAnyBranch()
+ path = '/%s/%s/' % (BRANCH_ID_ALIAS_PREFIX, branch.id)
+ result = self.branch_set.getIdAndTrailingPath(path)
+ self.assertEqual((branch.id, '/'), result)
+
+ def test_branch_id_alias_public_with_path(self):
+ # All the path after the number is returned as the trailing path.
+ branch = self.factory.makeAnyBranch()
+ path = '/%s/%s/foo' % (BRANCH_ID_ALIAS_PREFIX, branch.id)
+ result = self.branch_set.getIdAndTrailingPath(path)
+ self.assertEqual((branch.id, '/foo'), result)
+
class TestGetByPath(TestCaseWithFactory):
"""Test `IBranchLookup.getByLPPath`."""
=== modified file 'lib/lp/code/xmlrpc/codehosting.py'
--- lib/lp/code/xmlrpc/codehosting.py 2011-02-24 15:30:54 +0000
+++ lib/lp/code/xmlrpc/codehosting.py 2011-04-06 17:31:46 +0000
@@ -59,6 +59,7 @@
from lp.code.interfaces.branchtarget import IBranchTarget
from lp.code.interfaces.codehosting import (
BRANCH_ALIAS_PREFIX,
+ BRANCH_ID_ALIAS_PREFIX,
BRANCH_TRANSPORT,
CONTROL_TRANSPORT,
ICodehostingAPI,
@@ -287,7 +288,8 @@
return run_with_login(login_id, branch_changed)
- def _serializeBranch(self, requester, branch, trailing_path):
+ def _serializeBranch(self, requester, branch, trailing_path,
+ force_readonly=False):
if requester == LAUNCHPAD_SERVICES:
branch = removeSecurityProxy(branch)
try:
@@ -296,10 +298,13 @@
raise faults.PermissionDenied()
if branch.branch_type == BranchType.REMOTE:
return None
+ if force_readonly:
+ writable = False
+ else:
+ writable = self._canWriteToBranch(requester, branch)
return (
BRANCH_TRANSPORT,
- {'id': branch_id,
- 'writable': self._canWriteToBranch(requester, branch)},
+ {'id': branch_id, 'writable': writable},
trailing_path)
def _serializeControlDirectory(self, requester, product_path,
@@ -323,12 +328,34 @@
{'default_stack_on': escape('/' + unique_name)},
trailing_path)
+ def _translateBranchIdAlias(self, requester, path):
+ # If the path isn't a branch id alias, nothing more to do.
+ stripped_path = unescape(path.strip('/'))
+ if not stripped_path.startswith(BRANCH_ID_ALIAS_PREFIX + '/'):
+ return None
+ try:
+ parts = stripped_path.split('/', 2)
+ branch_id = int(parts[1])
+ except (ValueError, IndexError):
+ raise faults.PathTranslationError(path)
+ branch = getUtility(IBranchLookup).get(branch_id)
+ if branch is None:
+ raise faults.PathTranslationError(path)
+ try:
+ trailing = parts[2]
+ except IndexError:
+ trailing = ''
+ return self._serializeBranch(requester, branch, trailing, True)
+
def translatePath(self, requester_id, path):
"""See `ICodehostingAPI`."""
@return_fault
def translate_path(requester):
if not path.startswith('/'):
return faults.InvalidPath(path)
+ branch = self._translateBranchIdAlias(requester, path)
+ if branch is not None:
+ return branch
stripped_path = path.strip('/')
for first, second in iter_split(stripped_path, '/'):
first = unescape(first)
=== modified file 'lib/lp/code/xmlrpc/tests/test_codehosting.py'
--- lib/lp/code/xmlrpc/tests/test_codehosting.py 2010-10-13 03:56:46 +0000
+++ lib/lp/code/xmlrpc/tests/test_codehosting.py 2011-04-06 17:31:46 +0000
@@ -41,6 +41,7 @@
from lp.code.interfaces.branchtarget import IBranchTarget
from lp.code.interfaces.codehosting import (
BRANCH_ALIAS_PREFIX,
+ BRANCH_ID_ALIAS_PREFIX,
BRANCH_TRANSPORT,
CONTROL_TRANSPORT,
)
@@ -991,6 +992,63 @@
requester = self.factory.makePerson()
self.assertNotFound(requester, '/%s/.bzr' % BRANCH_ALIAS_PREFIX)
+ def test_translatePath_branch_id_alias_bzrdir_content(self):
+ # translatePath('/+branch-id/.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_ID_ALIAS_PREFIX)
+
+ def test_translatePath_branch_id_alias_bzrdir(self):
+ # translatePath('/+branch-id/.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_ID_ALIAS_PREFIX)
+
+ def test_translatePath_branch_id_alias_trailing(self):
+ # Make sure the trailing path is returned.
+ requester = self.factory.makePerson()
+ branch = removeSecurityProxy(self.factory.makeAnyBranch())
+ path = escape(u'/%s/%s/foo/bar' % (BRANCH_ID_ALIAS_PREFIX, branch.id))
+ translation = self.codehosting_api.translatePath(requester.id, path)
+ self.assertEqual(
+ (BRANCH_TRANSPORT, {'id': branch.id, 'writable': False}, 'foo/bar'),
+ translation)
+
+ def test_translatePath_branch_id_alias_owned(self):
+ # Even if the the requester is the owner, the branch is read only.
+ requester = self.factory.makePerson()
+ branch = removeSecurityProxy(
+ self.factory.makeAnyBranch(
+ branch_type=BranchType.HOSTED, owner=requester))
+ path = escape(u'/%s/%s' % (BRANCH_ID_ALIAS_PREFIX, branch.id))
+ translation = self.codehosting_api.translatePath(requester.id, path)
+ self.assertEqual(
+ (BRANCH_TRANSPORT, {'id': branch.id, 'writable': False}, ''),
+ translation)
+
+ def test_translatePath_branch_id_alias_private_branch(self):
+ # Private branches are accessible but read-only even if you are the
+ # owner.
+ requester = self.factory.makePerson()
+ branch = removeSecurityProxy(
+ self.factory.makeAnyBranch(
+ branch_type=BranchType.HOSTED, private=True, owner=requester))
+ path = escape(u'/%s/%s' % (BRANCH_ID_ALIAS_PREFIX, branch.id))
+ translation = self.codehosting_api.translatePath(requester.id, path)
+ self.assertEqual(
+ (BRANCH_TRANSPORT, {'id': branch.id, 'writable': False}, ''),
+ translation)
+
+ def test_translatePath_branch_id_alias_private_branch_no_access(self):
+ # Private branches you don't have access to raise permission denied.
+ requester = self.factory.makePerson()
+ branch = removeSecurityProxy(
+ self.factory.makeAnyBranch(
+ branch_type=BranchType.HOSTED, private=True))
+ path = escape(u'/%s/%s' % (BRANCH_ID_ALIAS_PREFIX, branch.id))
+ self.assertPermissionDenied(requester, path)
+
def assertTranslationIsControlDirectory(self, translation,
default_stacked_on,
trailing_path):
=== modified file 'lib/lp/codehosting/inmemory.py'
--- lib/lp/codehosting/inmemory.py 2011-02-24 15:30:54 +0000
+++ lib/lp/codehosting/inmemory.py 2011-04-06 17:31:46 +0000
@@ -37,6 +37,7 @@
from lp.code.interfaces.branchtarget import IBranchTarget
from lp.code.interfaces.codehosting import (
BRANCH_ALIAS_PREFIX,
+ BRANCH_ID_ALIAS_PREFIX,
BRANCH_TRANSPORT,
CONTROL_TRANSPORT,
LAUNCHPAD_ANONYMOUS,
@@ -806,21 +807,46 @@
{'default_stack_on': escape('/' + default_branch.unique_name)},
trailing_path)
- def _serializeBranch(self, requester_id, branch, trailing_path):
+ def _serializeBranch(self, requester_id, branch, trailing_path,
+ force_readonly=False):
if not self._canRead(requester_id, branch):
return faults.PermissionDenied()
elif branch.branch_type == BranchType.REMOTE:
return None
+ if force_readonly:
+ writable = False
else:
- return (
- BRANCH_TRANSPORT,
- {'id': branch.id,
- 'writable': self._canWrite(requester_id, branch),
- }, trailing_path)
+ writable = self._canWrite(requester_id, branch)
+ return (
+ BRANCH_TRANSPORT,
+ {'id': branch.id, 'writable': writable},
+ trailing_path)
+
+ def _translateBranchIdAlias(self, requester, path):
+ # If the path isn't a branch id alias, nothing more to do.
+ stripped_path = unescape(path.strip('/'))
+ if not stripped_path.startswith(BRANCH_ID_ALIAS_PREFIX + '/'):
+ return None
+ try:
+ parts = stripped_path.split('/', 2)
+ branch_id = int(parts[1])
+ except (ValueError, IndexError):
+ return faults.PathTranslationError(path)
+ branch = self._branch_set.get(branch_id)
+ if branch is None:
+ return faults.PathTranslationError(path)
+ try:
+ trailing = parts[2]
+ except IndexError:
+ trailing = ''
+ return self._serializeBranch(requester, branch, trailing, True)
def translatePath(self, requester_id, path):
if not path.startswith('/'):
return faults.InvalidPath(path)
+ branch = self._translateBranchIdAlias(requester_id, path)
+ if branch is not None:
+ return branch
stripped_path = path.strip('/')
for first, second in iter_split(stripped_path, '/'):
first = unescape(first).encode('utf-8')
=== modified file 'lib/lp/codehosting/rewrite.py'
--- lib/lp/codehosting/rewrite.py 2010-08-20 20:31:18 +0000
+++ lib/lp/codehosting/rewrite.py 2011-04-06 17:31:46 +0000
@@ -11,6 +11,7 @@
from canonical.config import config
from lp.code.interfaces.branchlookup import IBranchLookup
+from lp.code.interfaces.codehosting import BRANCH_ID_ALIAS_PREFIX
from lp.codehosting.vfs import branch_id_to_path
from lp.services.utils import iter_split
@@ -99,7 +100,10 @@
branch_id, trailing, cached = self._getBranchIdAndTrailingPath(
resource_location)
if branch_id is None:
- r = self._codebrowse_url(resource_location)
+ if resource_location.startswith('/' + BRANCH_ID_ALIAS_PREFIX):
+ r = 'NULL'
+ else:
+ r = self._codebrowse_url(resource_location)
else:
if trailing.startswith('/.bzr'):
r = urlutils.join(
=== modified file 'lib/lp/codehosting/tests/test_rewrite.py'
--- lib/lp/codehosting/tests/test_rewrite.py 2010-12-23 01:02:00 +0000
+++ lib/lp/codehosting/tests/test_rewrite.py 2011-04-06 17:31:46 +0000
@@ -16,6 +16,7 @@
from canonical.config import config
from canonical.testing.layers import DatabaseFunctionalLayer
+from lp.code.interfaces.codehosting import BRANCH_ID_ALIAS_PREFIX
from lp.codehosting.rewrite import BranchRewriter
from lp.codehosting.vfs import branch_id_to_path
from lp.services.log.logger import BufferLogger
@@ -91,6 +92,56 @@
'http://localhost:8080/%s/.bzr' % unique_name],
output)
+ def test_rewriteLine_id_alias_found_dot_bzr(self):
+ # Requests for /+branch-id/$id/.bzr/... are redirected to where the
+ # branches are served from by ID if they are public.
+ rewriter = self.makeRewriter()
+ branches = [
+ self.factory.makeProductBranch(private=False),
+ self.factory.makePersonalBranch(private=False),
+ self.factory.makePackageBranch(private=False)]
+ transaction.commit()
+ output = [
+ rewriter.rewriteLine("/%s/%s/.bzr/README" % (
+ BRANCH_ID_ALIAS_PREFIX, branch.id))
+ for branch in branches]
+ expected = [
+ 'file:///var/tmp/bazaar.launchpad.dev/mirrors/%s/.bzr/README'
+ % branch_id_to_path(branch.id)
+ for branch in branches]
+ self.assertEqual(expected, output)
+
+ def test_rewriteLine_id_alias_private(self):
+ # All requests for /+branch-id/$id/... for private branches return
+ # 'NULL'. This is translated by apache to a 404.
+ rewriter = self.makeRewriter()
+ branch = self.factory.makeAnyBranch(private=True)
+ path = '/%s/%s' % (
+ BRANCH_ID_ALIAS_PREFIX, removeSecurityProxy(branch).id)
+ transaction.commit()
+ output = [
+ rewriter.rewriteLine("%s/changes" % path),
+ rewriter.rewriteLine("%s/.bzr" % path)
+ ]
+ self.assertEqual(['NULL', 'NULL'], output)
+
+ def test_rewriteLine_id_alias_logs_cache_hit(self):
+ # The second request for a branch using the alias hits the cache.
+ rewriter = self.makeRewriter()
+ branch = self.factory.makeAnyBranch()
+ transaction.commit()
+ path = "/%s/%s/.bzr/README" % (BRANCH_ID_ALIAS_PREFIX, branch.id)
+ rewriter.rewriteLine(path)
+ rewriter.rewriteLine(path)
+ logging_output_lines = self.getLoggerOutput(
+ rewriter).strip().split('\n')
+ self.assertEqual(2, len(logging_output_lines))
+ self.assertIsNot(
+ None,
+ re.match("INFO .* -> .* (.*s, cache: HIT)",
+ logging_output_lines[-1]),
+ "No hit found in %r" % logging_output_lines[-1])
+
def test_rewriteLine_static(self):
# Requests to /static are rewritten to codebrowse urls.
rewriter = self.makeRewriter()
=== modified file 'lib/lp/registry/javascript/structural-subscription.js'
--- lib/lp/registry/javascript/structural-subscription.js 2011-04-05 17:19:14 +0000
+++ lib/lp/registry/javascript/structural-subscription.js 2011-04-06 17:31:46 +0000
@@ -164,20 +164,27 @@
* @method create_structural_subscription filter
* @param {Object} who Link to the user or team to be subscribed.
* @param {Object} form_data The data returned from the form submission.
+ * @param {Object} success_callback Function to execute when filter is added.
*/
-function add_bug_filter(who, form_data) {
+function add_bug_filter(who, form_data, success_callback) {
var config = {
on: {success: function (bug_filter) {
// If we fail to PATCH the new bug filter, DELETE it.
- var on = {failure: function () {
- // We use the namespace binding so tests can override
- // these functions.
- namespace._delete_filter(bug_filter);
- // Call the failure handler to report the original
- // error to the user.
- overlay_error_handler.getFailureHandler()
- .apply(this, arguments);
- }};
+ var on = {
+ failure: function () {
+ // We use the namespace binding so tests can override
+ // these functions.
+ namespace._delete_filter(bug_filter);
+ // Call the failure handler to report the original
+ // error to the user.
+ overlay_error_handler.getFailureHandler()
+ .apply(this, arguments);
+ },
+ success: function (bug_filter) {
+ success_callback(form_data, bug_filter);
+ subscription_success(bug_filter);
+ }
+ };
patch_bug_filter(bug_filter.getAttrs(), form_data, on);
},
failure: overlay_error_handler.getFailureHandler()
@@ -195,28 +202,31 @@
namespace._add_bug_filter = add_bug_filter;
/**
- * Given the form data from a user, save the subscription.
+ * Create a handler to save the subscription given the form data from a user.
*
* @private
- * @method save_subscription
- * @param {Object} form_data The data generated by the form submission.
+ * @method make_add_subscription_handler
+ * @param {Object} success_callback Function to execute on successful addition.
*/
-
-function save_subscription(form_data) {
- var who;
- var has_errors = check_for_errors_in_overlay(add_subscription_overlay);
- if (has_errors) {
- return false;
- }
- if (form_data.recipient[0] === 'user') {
- who = LP.links.me;
- } else {
- // There can be only one.
- who = form_data.team[0];
- }
- add_bug_filter(who, form_data);
+function make_add_subscription_handler(success_callback) {
+ var save_subscription = function(form_data) {
+ var who;
+ var has_errors = check_for_errors_in_overlay(
+ add_subscription_overlay);
+ if (has_errors) {
+ return false;
+ }
+ if (form_data.recipient[0] === 'user') {
+ who = LP.links.me;
+ } else {
+ // There can be only one.
+ who = form_data.team[0];
+ }
+ return add_bug_filter(who, form_data, success_callback);
+ };
+ return save_subscription;
}
-namespace.save_subscription = save_subscription;
+namespace._make_add_subscription_handler = make_add_subscription_handler;
function check_for_errors_in_overlay(overlay) {
var has_errors = false;
@@ -245,6 +255,18 @@
}
/**
+ * Fill the filter name and description.
+ */
+function fill_filter_description(filter_node, filter_info, filter) {
+ filter_node.one('.filter-description')
+ .empty()
+ .appendChild(create_filter_description(filter));
+ filter_node.one('.filter-name')
+ .empty()
+ .appendChild(render_filter_title(filter_info, filter));
+};
+
+/**
* Handle the activation of the edit subscription link.
*/
function edit_subscription_handler(context, form_data) {
@@ -256,12 +278,8 @@
}
var on = {success: function (new_data) {
var filter = new_data.getAttrs();
- filter_node.one('.filter-description')
- .empty()
- .appendChild(create_filter_description(filter));
- filter_node.one('.filter-name')
- .empty()
- .appendChild(render_filter_title(context.filter_info, filter));
+ fill_filter_description(
+ filter_node, context.filter_info, filter);
add_subscription_overlay.hide();
}};
patch_bug_filter(context.filter_info.filter, form_data, on);
@@ -279,7 +297,7 @@
* Populate the overlay element with the contents of the add/edit form.
*/
function create_overlay(content_box_id, overlay_id, submit_button,
- submit_callback) {
+ submit_callback, success_callback) {
// Create the overlay.
add_subscription_overlay = new Y.lazr.FormOverlay({
headerContent:
@@ -540,14 +558,18 @@
});
});
anim.on("end", function() {
- node.setStyles({
- height: 0,
- visibility: 'hidden',
- display: null
- // Don't set display: none because then the node won't be taken
- // into account and the rendering will sometimes jiggle
- // horizontally when the node is opened.
- });
+ if (user_cfg && user_cfg.remove_on_end === true) {
+ node.remove();
+ } else {
+ node.setStyles({
+ height: 0,
+ visibility: 'hidden',
+ display: null
+ // Don't set display: none because then the node won't be taken
+ // into account and the rendering will sometimes jiggle
+ // horizontally when the node is opened.
+ });
+ }
});
anim.run();
}
@@ -883,13 +905,8 @@
var recipient_label = content_node.one('input[name="recipient"] + span'),
teams = LP.cache.administratedTeams;
if (filter_info !== undefined && filter_info.subscriber_is_team) {
- var i;
- for (i=0; i<teams.length; i++) {
- if (teams[i].link === filter_info.subscriber_link){
- recipient_label.set('text', teams[i].title);
- break;
- }
- }
+ var team = get_team(filter_info.subscriber_link);
+ recipient_label.set('text', team.title);
} else {
recipient_label.set('text', 'Yourself');
}
@@ -1032,7 +1049,7 @@
/**
* Construct a handler for an unsubscribe link.
*/
-function make_delete_handler(filter, node, subscriber_id) {
+function make_delete_handler(filter, filter_id, node, subscriber_id) {
var error_handler = new Y.lp.client.ErrorHandler();
error_handler.showError = function(error_msg) {
var unsubscribe_node = node.one('a.delete-subscription');
@@ -1042,19 +1059,24 @@
var y_config = {
method: "POST",
headers: {'X-HTTP-Method-Override': 'DELETE'},
- on: {success: function(transactionid, response, args){
+ on: {
+ success: function(transactionid, response, args){
+ var filter_node = Y.one(
+ '#subscription-filter-'+filter_id.toString());
+ filter_node.setStyle("margin-top", "0");
var subscriber = Y.one(
'#subscription-'+subscriber_id.toString());
- var to_collapse = subscriber;
var filters = subscriber.all('.subscription-filter');
- if (!filters.isEmpty()) {
- to_collapse = node;
+
+ collapse_node(filter_node, { remove_on_end: true });
+ if (filters.size() <= 1) {
+ collapse_node(subscriber, { remove_on_end: true });
+ LP.cache.subscription_info[subscriber_id].filters = [];
}
- collapse_node(to_collapse);
- },
- failure: error_handler.getFailureHandler()
- }
- };
+ },
+ failure: error_handler.getFailureHandler()
+ }
+ };
Y.io(filter.self_link, y_config);
};
}
@@ -1093,6 +1115,31 @@
}
/**
+ * Attach activation (click) handlers to links for a particular filter.
+ */
+function wire_up_edit_links_for_filter(
+ config, subscription, subscription_id, filter_info, filter_id,
+ filter_node) {
+ var node = filter_node || Y.one(
+ '#subscription-filter-'+filter_id.toString());
+ if (filter_info.can_mute) {
+ var mute_link = node.one('a.mute-subscription');
+ mute_link.on('click', make_mute_handler(filter_info, node));
+ }
+ if (!filter_info.subscriber_is_team ||
+ filter_info.user_is_team_admin) {
+ var edit_link = node.one('a.edit-subscription');
+ var edit_handler = make_edit_handler(
+ subscription, filter_info, filter_id, config);
+ edit_link.on('click', edit_handler);
+ var delete_link = node.one('a.delete-subscription');
+ var delete_handler = make_delete_handler(
+ filter_info.filter, filter_id, node, subscription_id);
+ delete_link.on('click', delete_handler);
+ }
+}
+
+/**
* Attach activation (click) handlers to all of the edit links on the page.
*/
function wire_up_edit_links(config) {
@@ -1105,22 +1152,8 @@
var sub = subscription_info[i];
for (j=0; j<sub.filters.length; j++) {
var filter_info = sub.filters[j];
- var node = Y.one('#subscription-filter-'+filter_id.toString());
- if (filter_info.can_mute) {
- var mute_link = node.one('a.mute-subscription');
- mute_link.on('click', make_mute_handler(filter_info, node));
- }
- if (!filter_info.subscriber_is_team ||
- filter_info.user_is_team_admin) {
- var edit_link = node.one('a.edit-subscription');
- var edit_handler = make_edit_handler(
- sub, filter_info, filter_id, config);
- edit_link.on('click', edit_handler);
- var delete_link = node.one('a.delete-subscription');
- var delete_handler = make_delete_handler(
- filter_info.filter, node, i);
- delete_link.on('click', delete_handler);
- }
+ wire_up_edit_links_for_filter(
+ config, sub, i, filter_info, filter_id);
filter_id += 1;
}
}
@@ -1147,6 +1180,97 @@
}
/**
+ * Create filter node to include in the subscription's filter listing.
+ */
+function create_filter_node(filter_id, filter_info, filter) {
+ var filter_node = Y.Node.create(
+ '<div style="margin: 1em 0em 0em 1em"'+
+ ' class="subscription-filter"></div>')
+ .set('id', 'subscription-filter-'+filter_id.toString());
+ filter_node.appendChild(Y.Node.create(
+ '<div style="margin-top: 1em"></div>'));
+ filter_node.appendChild(Y.Node.create(
+ '<strong class="filter-name"></strong>'));
+
+ if (filter_info.can_mute) {
+ filter_node.appendChild(Y.Node.create(
+ '<em class="mute-label" style="padding-left: 1em;">You '+
+ 'do not receive emails from this subscription.</em>'));
+ }
+
+ var can_edit = (!filter_info.subscriber_is_team ||
+ filter_info.user_is_team_admin);
+
+ // Whitespace is stripped from the left and right of the string
+ // when you make a node, so we have to build the string with the
+ // intermediate whitespace and then create the node at the end.
+ var control_template = '';
+
+ if (filter_info.can_mute) {
+ control_template += (
+ '<a href="#" class="sprite js-action mute-subscription"></a>');
+ if (can_edit) {
+ control_template += ' or ';
+ }
+ }
+ if (can_edit) {
+ // User can edit the subscription.
+ control_template += (
+ '<a href="#" class="sprite modify edit js-action '+
+ ' edit-subscription">Edit this subscription</a> or '+
+ '<a href="#" class="sprite modify remove js-action '+
+ ' delete-subscription">Unsubscribe</a>');
+ }
+
+ filter_node.appendChild(
+ Y.Node.create(
+ '<span style="float: right"></span>'))
+ .appendChild(Y.Node.create(control_template));
+ filter_node.appendChild(
+ Y.Node.create('<div style="padding-left: 1em" '+
+ 'class="filter-description"></div>'));
+
+ if (filter_info.can_mute) {
+ handle_mute(filter_node, filter_info.is_muted);
+ }
+
+ fill_filter_description(filter_node, filter_info, filter);
+
+ return filter_node;
+}
+
+/**
+ * Create a node with subscription description.
+ */
+function create_subscription_node(serial_id, subscription_data, filter_id) {
+ var node = Y.Node.create(
+ '<div style="margin-top: 2em; padding: 0 1em 1em 1em; '+
+ ' border: 1px solid #ddd;"></div>')
+ .set('id', 'subscription-'+serial_id.toString())
+ node.appendChild(Y.Node.create(
+ ' <span style="float: left; margin-top: -0.6em; '+
+ ' padding: 0 1ex; background-color: #fff;"></a>'))
+ .appendChild('<span>Subscriptions to </span>')
+ .appendChild(Y.Node.create('<a></a>')
+ .set('href', subscription_data.target_url)
+ .set('text', subscription_data.target_title));
+
+ for (j=0; j<subscription_data.filters.length; j++) {
+ var filter_info = subscription_data.filters[j];
+ var filter = filter_info.filter;
+ // We put the filters in the cache so that the patch mechanism
+ // can automatically find them and update them on a successful
+ // edit. This makes it possible to open up a filter after an edit
+ // and see the information you expect to see.
+ LP.cache['structural-subscription-filter-'+filter_id.toString()] =
+ filter;
+ node.appendChild(create_filter_node(filter_id, filter_info, filter));
+ filter_id += 1;
+ };
+ return node;
+}
+
+/**
* Populate the subscription list DOM element with subscription descriptions.
*/
function fill_in_bug_subscriptions(config) {
@@ -1156,89 +1280,14 @@
var subscription_info = LP.cache.subscription_info;
var top_node = Y.Node.create(
'<div class="yui-g"><div id="structural-subscriptions"></div></div>');
+ var i;
var filter_id = 0;
- var i;
- var j;
for (i=0; i<subscription_info.length; i++) {
- var sub = subscription_info[i];
- var sub_node = top_node.appendChild(Y.Node.create(
- '<div style="margin-top: 2em; padding: 0 1em 1em 1em; '+
- ' border: 1px solid #ddd;"></div>')
- .set('id', 'subscription-'+i.toString()));
- sub_node.appendChild(Y.Node.create(
- ' <span style="float: left; margin-top: -0.6em; '+
- ' padding: 0 1ex; background-color: #fff;"></a>'))
- .appendChild('<span>Subscriptions to </span>')
- .appendChild(Y.Node.create('<a></a>')
- .set('href', sub.target_url)
- .set('text', sub.target_title));
-
- for (j=0; j<sub.filters.length; j++) {
- var filter = sub.filters[j].filter;
- // We put the filters in the cache so that the patch mechanism
- // can automatically find them and update them on a successful
- // edit. This makes it possible to open up a filter after an edit
- // and see the information you expect to see.
- LP.cache['structural-subscription-filter-'+filter_id.toString()] =
- filter;
- var filter_node = sub_node.appendChild(Y.Node.create(
- '<div style="margin: 1em 0em 0em 1em"'+
- ' class="subscription-filter"></div>')
- .set('id', 'subscription-filter-'+filter_id.toString()))
- .appendChild(Y.Node.create(
- '<div style="margin-top: 1em"></div>'));
- filter_node.appendChild(Y.Node.create(
- '<strong class="filter-name"></strong>'))
- .appendChild(render_filter_title(sub.filters[j], filter));
- if (sub.filters[j].can_mute) {
- filter_node.appendChild(Y.Node.create(
- '<em class="mute-label" style="padding-left: 1em;">You '+
- 'do not receive emails from this subscription.</em>'));
- }
- var can_edit = (!sub.filters[j].subscriber_is_team ||
- sub.filters[j].user_is_team_admin);
- // Whitespace is stripped from the left and right of the string
- // when you make a node, so we have to build the string with the
- // intermediate whitespace and then create the node at the end.
- var control_template = '';
- if (sub.filters[j].can_mute) {
- control_template += (
- '<a href="#" class="sprite js-action '+
- 'mute-subscription"></a>');
- if (can_edit) {
- control_template += ' or ';
- }
- }
- if (can_edit) {
- // User can edit the subscription.
- control_template += (
- '<a href="#" class="sprite modify edit js-action '+
- ' edit-subscription">Edit this subscription</a> or '+
- '<a href="#" class="sprite modify remove js-action '+
- ' delete-subscription">Unsubscribe</a>');
- }
- filter_node.appendChild(Y.Node.create(
- '<span style="float: right"></span>')
- ).appendChild(Y.Node.create(control_template));
- filter_node.appendChild(Y.Node.create(
- '<div style="padding-left: 1em" '+
- 'class="filter-description"></div>'))
- .appendChild(create_filter_description(filter));
- if (sub.filters[j].can_mute) {
- handle_mute(filter_node, sub.filters[j].is_muted);
- }
-
- filter_id += 1;
- }
-
- // We can remove this once we enforce at least one filter per
- // subscription.
- if (subscription_info[i].filters.length === 0) {
- sub_node.appendChild(
- '<div style="clear: both; padding: 1em 0 0 1em"></div>')
- .appendChild('<strong>All messages</strong>');
- }
+ top_node.appendChild(
+ create_subscription_node(i, subscription_info[i], filter_id));
+ filter_id += subscription_info[i].filters.length;
}
+
listing.appendChild(top_node);
wire_up_edit_links(config);
@@ -1272,7 +1321,6 @@
*/
function create_filter_description(filter) {
var description = Y.Node.create('<div></div>');
-
var filter_items = [];
// Format status conditions.
if (filter.statuses.length !== 0) {
@@ -1372,20 +1420,154 @@
}
/**
+ * Get team information.
+ */
+function get_team(url) {
+ var teams = LP.cache.administratedTeams;
+ var i;
+ for (i=0; i<teams.length; i++) {
+ if (teams[i].link === url) {
+ return teams[i];
+ }
+ }
+}
+
+/**
+ * Get team information from the submitted form data.
+ */
+function get_team_info(form_data) {
+ var is_team = (form_data.recipient[0] == "team");
+ var link, team, title, url;
+ if (is_team) {
+ link = form_data.team[0];
+ team = get_team(link);
+ if (team !== undefined) {
+ title = team.title;
+ url = team.url;
+ } else {
+ is_team = false;
+ link = LP.links.me;
+ }
+ }
+ return {
+ is_team: is_team,
+ link: link,
+ title: title,
+ url: url}
+}
+
+/**
+ * Get target details from either target_info or first subscription_info.
+ */
+function get_target_info() {
+ if (LP.cache.target_info !== undefined) {
+ return LP.cache.target_info
+ } else {
+ var info = LP.cache.subscription_info[0];
+ return {
+ title: info.target_title,
+ url: info.target_url}
+ }
+}
+
+/**
+ * Constructs filter info on-the-go.
+ */
+function construct_filter_info(filter, form_data, target_info) {
+ var team_info = get_team_info(form_data);
+
+ var filter_info = {
+ filter : filter,
+ subscriber_is_team: team_info.is_team,
+ user_is_team_admin: team_info.is_team,
+ can_mute: team_info.is_team,
+ subscriber_url: team_info.url,
+ subscriber_link: team_info.link,
+ subscriber_title: team_info.title,
+ target_title: target_info.title,
+ target_url: target_info.url
+ };
+ return filter_info;
+}
+
+/**
+ * Return a function that adds the newly created filter to the
+ * subscription's filter list after it has been saved.
+ */
+
+function make_add_subscription_success_handler(config) {
+ return function(form_data, filter) {
+ if (config.add_filter_description === true) {
+ // This way to figure out the ID works only
+ // if the page shows exactly one "target" (eg. Firefox).
+ var subscriber_id = 0;
+ var filter_id;
+ var subscriptions_list = Y.one(
+ '#subscription-' + subscriber_id.toString());
+
+ var filter_data = filter.getAttrs();
+ var target_info = get_target_info();
+ var filter_info = construct_filter_info(
+ filter_data, form_data, target_info);
+
+ /* Node to flash with green on success. */
+ var anim_node;
+
+ var subscription_info;
+ if (subscriptions_list === null) {
+ // No subscriptions are listed at all.
+ filter_id = 0;
+ subscription_info = {
+ filters: [filter_info],
+ target_url: target_info.url,
+ target_title: target_info.title
+ };
+ LP.cache.subscription_info = [subscription_info]
+
+ subscriptions_list = create_subscription_node(
+ 0, subscription_info, 0);
+ var subscriptions = Y.one("#structural-subscriptions");
+ subscriptions.appendChild(subscriptions_list);
+ anim_node = subscriptions_list.one("#subscription-filter-0");
+ } else {
+ // There's at least one filter in the page.
+ subscription_info = LP.cache.subscription_info[0];
+ filter_id = subscription_info.filters.length;
+ subscription_info.filters.push(filter);
+
+ var description_node = create_filter_node(
+ filter_id, filter_info, filter_data);
+ subscriptions_list.append(description_node);
+ anim_node = description_node;
+ }
+
+ wire_up_edit_links_for_filter(
+ config, subscription_info, 0,
+ filter_info, filter_id, anim_node);
+ Y.lazr.anim.green_flash({node: anim_node}).run();
+ }
+ }
+}
+
+/**
* Show the overlay for creating a new subscription.
*/
function show_add_overlay(config) {
- Y.one(config.content_box).empty();
+ var content_node = Y.one(config.content_box);
+ content_node.empty();
var overlay_id = setup_overlay(config.content_box);
- clear_overlay(Y.one(config.content_box), false);
+ clear_overlay(content_node, false);
var submit_button = Y.Node.create(
'<button type="submit" name="field.actions.create" ' +
'value="Create subscription" class="lazr-pos lazr-btn" '+
'>OK</button>');
+ var success_callback = make_add_subscription_success_handler(config);
+
+ var save_subscription = make_add_subscription_handler(success_callback);
create_overlay(config.content_box, overlay_id, submit_button,
- save_subscription);
+ save_subscription, success_callback);
// We need to initialize the help links. They may have already been
// initialized except for the ones we added, so setupHelpTrigger
// is idempotent. Notice that this is old MochiKit code.
@@ -1406,7 +1588,7 @@
// Modify the menu-link-subscribe-to-bug-mail link to be visible.
var link = Y.one(link_id);
if (!Y.Lang.isValue(link)) {
- Y.fail('Link to set as the pop-up link not found.');
+ Y.Assert.fail('Link to set as the pop-up link not found.');
}
link.removeClass('invisible-link');
link.addClass('visible-link');
@@ -1416,6 +1598,7 @@
});
link.addClass('js-action');
} // setup_subscription_links
+namespace.setup_subscription_link = setup_subscription_link;
/**
* External entry point for configuring the structural subscription.
@@ -1435,6 +1618,6 @@
}; // setup
}, '0.1', {requires: [
- 'dom', 'node', 'lazr.anim', 'lazr.formoverlay', 'lazr.overlay',
- 'lazr.effects', 'lp.app.errors', 'lp.client', 'gallery-accordion'
- ]});
+ 'dom', 'node', 'test', 'lazr.anim', 'lazr.formoverlay', 'lazr.overlay',
+ 'lazr.effects', 'lp.app.errors', 'lp.client', 'gallery-accordion'
+]});
=== modified file 'lib/lp/registry/javascript/tests/test_structural_subscription.js'
--- lib/lp/registry/javascript/tests/test_structural_subscription.js 2011-04-05 18:29:45 +0000
+++ lib/lp/registry/javascript/tests/test_structural_subscription.js 2011-04-06 17:31:46 +0000
@@ -264,6 +264,10 @@
recipient: ['user']
};
context = {};
+
+ // Get the save subscription handler with empty success handler.
+ this.save_subscription = module._make_add_subscription_handler(
+ function() {});
},
tearDown: function() {
@@ -278,7 +282,7 @@
module.setup(this.configuration);
module._show_add_overlay(this.configuration);
this.form_data.recipient = ['user'];
- module.save_subscription(this.form_data);
+ this.save_subscription(this.form_data);
Assert.areEqual(
LP.links.me,
context.config.parameters.subscriber);
@@ -291,7 +295,7 @@
module._show_add_overlay(this.configuration);
this.form_data.recipient = ['team'];
this.form_data.team = ['https://launchpad.dev/api/~super-team'];
- module.save_subscription(this.form_data);
+ this.save_subscription(this.form_data);
Assert.areEqual(
this.form_data.team[0],
context.config.parameters.subscriber);