launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #19377
[Merge] lp:~cjwatson/launchpad/snap-listings into lp:launchpad
Colin Watson has proposed merging lp:~cjwatson/launchpad/snap-listings into lp:launchpad.
Commit message:
Add various snap package listing views.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
Related bugs:
Bug #1476405 in Launchpad itself: "Add support for building snaps"
https://bugs.launchpad.net/launchpad/+bug/1476405
For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/snap-listings/+merge/271307
Add snap package listing views for Branch, GitRepository, GitRef, Person, and Product.
BranchCollection and GitCollection gain methods to get associated ResultSets of Snaps; this makes it practical to only return Snaps for sources that the user can see, and can be extended reasonably easily in future if Snaps gain their own privacy flag. IHasSnaps and friends return in a modified form, mainly because since various domain objects now have the notion of associated snap packages it's the most lightweight way to provide mixins to implement the methods they need to find those packages, but I've kept it as minimal as possible and most of the lookup logic lives in lib/lp/snappy/ where it belongs, and IHasSnaps no longer provides any webservice exports. There's no batch navigation at present, partly because I based some of this on recipes which don't have that either, and partly because none of the listings seem likely to grow very large; we can of course add that later if need be.
Sorry this is so long; I couldn't see a sensible way to split it up without making the motivation for the current code layout distinctly non-obvious!
--
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/snap-listings into lp:launchpad.
=== modified file 'lib/lp/code/browser/branch.py'
--- lib/lp/code/browser/branch.py 2015-07-08 16:05:11 +0000
+++ lib/lp/code/browser/branch.py 2015-09-16 13:47:51 +0000
@@ -155,6 +155,10 @@
from lp.services.webapp.breadcrumb import NameBreadcrumb
from lp.services.webapp.escaping import structured
from lp.services.webapp.interfaces import ICanonicalUrlData
+from lp.snappy.browser.hassnaps import (
+ HasSnapsMenuMixin,
+ HasSnapsViewMixin,
+ )
from lp.translations.interfaces.translationtemplatesbuild import (
ITranslationTemplatesBuildSource,
)
@@ -267,7 +271,7 @@
return Link('+reviewer', text, icon='edit')
-class BranchContextMenu(ContextMenu, HasRecipesMenuMixin):
+class BranchContextMenu(ContextMenu, HasRecipesMenuMixin, HasSnapsMenuMixin):
"""Context menu for branches."""
usedfor = IBranch
@@ -276,7 +280,7 @@
'add_subscriber', 'browse_revisions', 'create_recipe', 'link_bug',
'link_blueprint', 'register_merge', 'source', 'subscription',
'edit_status', 'edit_import', 'upgrade_branch', 'view_recipes',
- 'visibility']
+ 'view_snaps', 'visibility']
@enabled_with_permission('launchpad.Edit')
def edit_status(self):
@@ -397,7 +401,7 @@
class BranchView(InformationTypePortletMixin, FeedsMixin, BranchMirrorMixin,
- LaunchpadView):
+ LaunchpadView, HasSnapsViewMixin):
feed_types = (
BranchFeedLink,
=== modified file 'lib/lp/code/browser/configure.zcml'
--- lib/lp/code/browser/configure.zcml 2015-09-11 15:11:34 +0000
+++ lib/lp/code/browser/configure.zcml 2015-09-16 13:47:51 +0000
@@ -348,6 +348,9 @@
name="++branch-recipes"
template="../templates/branch-recipes.pt" />
<browser:page
+ name="++branch-snaps"
+ template="../templates/branch-snaps.pt" />
+ <browser:page
name="++branch-import-details"
template="../templates/branch-import-details.pt" />
<browser:page
@@ -794,6 +797,9 @@
<browser:page
name="++repository-management"
template="../templates/gitrepository-management.pt"/>
+ <browser:page
+ name="++repository-snaps"
+ template="../templates/gitrepository-snaps.pt"/>
</browser:pages>
<browser:page
for="lp.code.interfaces.gitrepository.IGitRepository"
@@ -878,6 +884,9 @@
<browser:page
name="++ref-pending-merges"
template="../templates/gitref-pending-merges.pt"/>
+ <browser:page
+ name="++ref-snaps"
+ template="../templates/gitref-snaps.pt"/>
</browser:pages>
<browser:page
for="lp.code.interfaces.gitref.IGitRef"
=== modified file 'lib/lp/code/browser/gitref.py'
--- lib/lp/code/browser/gitref.py 2015-05-26 12:18:12 +0000
+++ lib/lp/code/browser/gitref.py 2015-09-16 13:47:51 +0000
@@ -54,6 +54,7 @@
stepthrough,
)
from lp.services.webapp.authorization import check_permission
+from lp.snappy.browser.hassnaps import HasSnapsViewMixin
# XXX cjwatson 2015-05-26: We can get rid of this after a short while, since
@@ -88,7 +89,7 @@
return Link('+register-merge', text, icon='add', enabled=enabled)
-class GitRefView(LaunchpadView):
+class GitRefView(LaunchpadView, HasSnapsViewMixin):
@property
def label(self):
=== modified file 'lib/lp/code/browser/gitrepository.py'
--- lib/lp/code/browser/gitrepository.py 2015-09-10 11:20:58 +0000
+++ lib/lp/code/browser/gitrepository.py 2015-09-16 13:47:51 +0000
@@ -91,6 +91,7 @@
from lp.services.webapp.escaping import structured
from lp.services.webapp.interfaces import ICanonicalUrlData
from lp.services.webhooks.browser import WebhookTargetNavigationMixin
+from lp.snappy.browser.hassnaps import HasSnapsViewMixin
@implementer(ICanonicalUrlData)
@@ -256,7 +257,8 @@
return "listing sortable"
-class GitRepositoryView(InformationTypePortletMixin, LaunchpadView):
+class GitRepositoryView(InformationTypePortletMixin, LaunchpadView,
+ HasSnapsViewMixin):
@property
def page_title(self):
=== modified file 'lib/lp/code/interfaces/branch.py'
--- lib/lp/code/interfaces/branch.py 2015-08-06 12:03:36 +0000
+++ lib/lp/code/interfaces/branch.py 2015-09-16 13:47:51 +0000
@@ -105,6 +105,7 @@
structured,
)
from lp.services.webapp.interfaces import ITableBatchNavigator
+from lp.snappy.interfaces.hassnaps import IHasSnaps
DEFAULT_BRANCH_STATUS_IN_LISTING = (
@@ -275,7 +276,7 @@
class IBranchView(IHasOwner, IHasBranchTarget, IHasMergeProposals,
- IHasRecipes):
+ IHasRecipes, IHasSnaps):
"""IBranch attributes that require launchpad.View permission."""
id = Int(title=_('ID'), readonly=True, required=True)
=== modified file 'lib/lp/code/interfaces/branchcollection.py'
--- lib/lp/code/interfaces/branchcollection.py 2015-04-21 14:33:09 +0000
+++ lib/lp/code/interfaces/branchcollection.py 2015-09-16 13:47:51 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2013 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""A collection of branches.
@@ -108,6 +108,27 @@
are returned.
"""
+ def getSnaps(owner=None, eager_load=False):
+ """Return a result set of snap packages for the branches in this
+ collection.
+
+ :param owner: If specified, only return snap packages with this
+ owner.
+ :param eager_load: If True, preload all the related information for
+ snap packages.
+ """
+
+ def getSnapsForPerson(person, eager_load=False):
+ """Snap packages for `person`.
+
+ Return snap packages for branches owned by `person`, or where
+ `person` is the owner of the snap package.
+
+ :param person: An `IPerson`.
+ :param eager_load: If True, preload all the related information for
+ snap packages.
+ """
+
def getExtendedRevisionDetails(user, revisions):
"""Return information about the specified revisions on a branch.
=== modified file 'lib/lp/code/interfaces/gitcollection.py'
--- lib/lp/code/interfaces/gitcollection.py 2015-04-22 16:42:57 +0000
+++ lib/lp/code/interfaces/gitcollection.py 2015-09-16 13:47:51 +0000
@@ -105,6 +105,31 @@
states are returned.
"""
+ def getSnaps(paths=None, owner=None, eager_load=False):
+ """Return a result set of snap packages for the repositories in this
+ collection.
+
+ :param paths: If specified, only return snap packages based on
+ branches whose paths are in this list.
+ :param owner: If specified, only return snap packages with this
+ owner.
+ :param eager_load: If True, preload all the related information for
+ snap packages.
+ """
+
+ def getSnapsForPerson(person, paths=None, eager_load=False):
+ """Snap packages for `person`.
+
+ Return snap packages for repositories owned by `person`, or where
+ `person` is the owner of the snap package.
+
+ :param person: An `IPerson`.
+ :param paths: If specified, only return snap packages based on
+ branches whose paths are in this list.
+ :param eager_load: If True, preload all the related information for
+ snap packages.
+ """
+
def getTeamsWithRepositories(person):
"""Return the teams that person is a member of that have
repositories."""
=== modified file 'lib/lp/code/interfaces/gitref.py'
--- lib/lp/code/interfaces/gitref.py 2015-07-21 16:30:15 +0000
+++ lib/lp/code/interfaces/gitref.py 2015-09-16 13:47:51 +0000
@@ -49,9 +49,10 @@
from lp.code.interfaces.hasbranches import IHasMergeProposals
from lp.registry.interfaces.person import IPerson
from lp.services.webapp.interfaces import ITableBatchNavigator
-
-
-class IGitRef(IHasMergeProposals, IPrivacy, IInformationType):
+from lp.snappy.interfaces.hassnaps import IHasSnaps
+
+
+class IGitRef(IHasMergeProposals, IPrivacy, IInformationType, IHasSnaps):
"""A reference in a Git repository."""
# XXX cjwatson 2015-01-19 bug=760849: "beta" is a lie to get WADL
=== modified file 'lib/lp/code/interfaces/gitrepository.py'
--- lib/lp/code/interfaces/gitrepository.py 2015-08-06 12:03:36 +0000
+++ lib/lp/code/interfaces/gitrepository.py 2015-09-16 13:47:51 +0000
@@ -78,6 +78,7 @@
PublicPersonChoice,
)
from lp.services.webhooks.interfaces import IWebhookTarget
+from lp.snappy.interfaces.hassnaps import IHasSnaps
GIT_REPOSITORY_NAME_VALIDATION_ERROR_MESSAGE = _(
@@ -117,7 +118,7 @@
return True
-class IGitRepositoryView(Interface):
+class IGitRepositoryView(IHasSnaps):
"""IGitRepository attributes that require launchpad.View permission."""
id = Int(title=_("ID"), readonly=True, required=True)
=== modified file 'lib/lp/code/model/branch.py'
--- lib/lp/code/model/branch.py 2015-08-06 12:03:36 +0000
+++ lib/lp/code/model/branch.py 2015-09-16 13:47:51 +0000
@@ -10,7 +10,6 @@
from datetime import datetime
import operator
-from urllib import quote_plus
from bzrlib import urlutils
from bzrlib.revision import NULL_REVISION
@@ -181,10 +180,11 @@
from lp.services.webapp import urlappend
from lp.services.webapp.authorization import check_permission
from lp.snappy.interfaces.snap import ISnapSet
+from lp.snappy.model.hassnaps import HasSnapsMixin
@implementer(IBranch, IPrivacy, IInformationType)
-class Branch(SQLBase, BzrIdentityMixin):
+class Branch(SQLBase, BzrIdentityMixin, HasSnapsMixin):
"""A sequence of ordered revisions in Bazaar."""
_table = 'Branch'
@@ -820,7 +820,7 @@
deletion_operations.extend(
DeletionCallable.forSourcePackageRecipe(recipe)
for recipe in self.recipes)
- if not getUtility(ISnapSet).findByBranch(self).is_empty():
+ if not self.getSnaps().is_empty():
alteration_operations.append(DeletionCallable(
None, _('Some snap packages build from this branch.'),
getUtility(ISnapSet).detachFromBranch, self))
=== modified file 'lib/lp/code/model/branchcollection.py'
--- lib/lp/code/model/branchcollection.py 2015-07-08 16:05:11 +0000
+++ lib/lp/code/model/branchcollection.py 2015-09-16 13:47:51 +0000
@@ -77,6 +77,7 @@
from lp.services.database.sqlbase import quote
from lp.services.propertycache import get_property_cache
from lp.services.searchbuilder import any
+from lp.snappy.interfaces.snap import ISnapSet
@implementer(IBranchCollection)
@@ -454,6 +455,34 @@
proposals.order_by(Desc(CodeReviewComment.vote))
return proposals
+ def getSnaps(self, owner=None, eager_load=False):
+ """See `IBranchCollection`."""
+ # Circular import.
+ from lp.snappy.model.snap import Snap
+
+ expressions = [Snap.branch_id.is_in(self._getBranchSelect())]
+ if owner is not None:
+ expressions.append(Snap.owner == owner)
+ resultset = self.store.find(Snap, *expressions)
+ if not eager_load:
+ return resultset
+ else:
+ loader = partial(
+ getUtility(ISnapSet).preloadDataForSnaps, user=self._user)
+ return DecoratedResultSet(resultset, pre_iter_hook=loader)
+
+ def getSnapsForPerson(self, person, eager_load=False):
+ """See `IBranchCollection`."""
+ owned = self.ownedBy(person).getSnaps()
+ packaged = self.getSnaps(owner=person)
+ resultset = owned.union(packaged)
+ if not eager_load:
+ return resultset
+ else:
+ loader = partial(
+ getUtility(ISnapSet).preloadDataForSnaps, user=self._user)
+ return DecoratedResultSet(resultset, pre_iter_hook=loader)
+
def getExtendedRevisionDetails(self, user, revisions):
"""See `IBranchCollection`."""
=== modified file 'lib/lp/code/model/gitcollection.py'
--- lib/lp/code/model/gitcollection.py 2015-07-08 16:05:11 +0000
+++ lib/lp/code/model/gitcollection.py 2015-09-16 13:47:51 +0000
@@ -59,6 +59,7 @@
from lp.services.database.decoratedresultset import DecoratedResultSet
from lp.services.database.interfaces import IStore
from lp.services.propertycache import get_property_cache
+from lp.snappy.interfaces.snap import ISnapSet
@implementer(IGitCollection)
@@ -375,6 +376,37 @@
proposals.order_by(Desc(CodeReviewComment.vote))
return proposals
+ def getSnaps(self, paths=None, owner=None, eager_load=False):
+ """See `IGitCollection`."""
+ # Circular import.
+ from lp.snappy.model.snap import Snap
+
+ expressions = [
+ Snap.git_repository_id.is_in(self._getRepositorySelect())]
+ if owner is not None:
+ expressions.append(Snap.owner == owner)
+ if paths is not None:
+ expressions.append(Snap.git_path.is_in(paths))
+ resultset = self.store.find(Snap, *expressions)
+ if not eager_load:
+ return resultset
+ else:
+ loader = partial(
+ getUtility(ISnapSet).preloadDataForSnaps, user=self._user)
+ return DecoratedResultSet(resultset, pre_iter_hook=loader)
+
+ def getSnapsForPerson(self, person, paths=None, eager_load=False):
+ """See `IGitCollection`."""
+ owned = self.ownedBy(person).getSnaps(paths=paths)
+ packaged = self.getSnaps(paths=paths, owner=person)
+ resultset = owned.union(packaged)
+ if not eager_load:
+ return resultset
+ else:
+ loader = partial(
+ getUtility(ISnapSet).preloadDataForSnaps, user=self._user)
+ return DecoratedResultSet(resultset, pre_iter_hook=loader)
+
def getTeamsWithRepositories(self, person):
"""See `IGitCollection`."""
# This method doesn't entirely fit with the intent of the
=== modified file 'lib/lp/code/model/gitref.py'
--- lib/lp/code/model/gitref.py 2015-07-21 16:30:15 +0000
+++ lib/lp/code/model/gitref.py 2015-09-16 13:47:51 +0000
@@ -49,9 +49,10 @@
from lp.services.database.enumcol import EnumCol
from lp.services.database.interfaces import IStore
from lp.services.database.stormbase import StormBase
-
-
-class GitRefMixin:
+from lp.snappy.model.hassnaps import HasSnapsMixin
+
+
+class GitRefMixin(HasSnapsMixin):
"""Methods and properties common to GitRef and GitRefFrozen.
These can be derived solely from the repository and path, and so do not
=== modified file 'lib/lp/code/model/gitrepository.py'
--- lib/lp/code/model/gitrepository.py 2015-08-10 06:39:16 +0000
+++ lib/lp/code/model/gitrepository.py 2015-09-16 13:47:51 +0000
@@ -150,6 +150,7 @@
from lp.services.webhooks.interfaces import IWebhookSet
from lp.services.webhooks.model import WebhookTargetMixin
from lp.snappy.interfaces.snap import ISnapSet
+from lp.snappy.model.hassnaps import HasSnapsMixin
object_type_map = {
@@ -172,7 +173,8 @@
@implementer(IGitRepository, IHasOwner, IPrivacy, IInformationType)
-class GitRepository(StormBase, WebhookTargetMixin, GitIdentityMixin):
+class GitRepository(StormBase, WebhookTargetMixin, GitIdentityMixin,
+ HasSnapsMixin):
"""See `IGitRepository`."""
__storm_table__ = 'GitRepository'
@@ -1008,7 +1010,7 @@
prerequisite_git_repository=self):
alteration_operations.append(
ClearPrerequisiteRepository(merge_proposal))
- if not getUtility(ISnapSet).findByGitRepository(self).is_empty():
+ if not self.getSnaps().is_empty():
alteration_operations.append(DeletionCallable(
None, msg("Some snap packages build from this repository."),
getUtility(ISnapSet).detachFromGitRepository, self))
=== modified file 'lib/lp/code/model/tests/test_hasrecipes.py'
--- lib/lp/code/model/tests/test_hasrecipes.py 2014-06-10 16:13:03 +0000
+++ lib/lp/code/model/tests/test_hasrecipes.py 2015-09-16 13:47:51 +0000
@@ -1,4 +1,4 @@
-# Copyright 2010-2014 Canonical Ltd. This software is licensed under the
+# Copyright 2010-2015 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Tests for classes that implement IHasRecipes."""
@@ -41,7 +41,7 @@
def test_person_implements_hasrecipes(self):
# Person should implement IHasRecipes.
- person = self.factory.makeBranch()
+ person = self.factory.makePerson()
self.assertProvides(person, IHasRecipes)
def test_person_recipes(self):
=== modified file 'lib/lp/code/templates/branch-index.pt'
--- lib/lp/code/templates/branch-index.pt 2014-12-06 02:16:30 +0000
+++ lib/lp/code/templates/branch-index.pt 2015-09-16 13:47:51 +0000
@@ -93,8 +93,9 @@
<tal:branch-pending-merges
replace="structure context/@@++branch-pending-merges" />
<tal:branch-recipes
- replace="structure context/@@++branch-recipes"
- />
+ replace="structure context/@@++branch-recipes" />
+ <tal:branch-snaps
+ replace="structure context/@@++branch-snaps" />
<tal:related-bugs-specs
replace="structure context/@@++branch-related-bugs-specs" />
</div>
=== added file 'lib/lp/code/templates/branch-snaps.pt'
--- lib/lp/code/templates/branch-snaps.pt 1970-01-01 00:00:00 +0000
+++ lib/lp/code/templates/branch-snaps.pt 2015-09-16 13:47:51 +0000
@@ -0,0 +1,24 @@
+<div
+ xmlns:tal="http://xml.zope.org/namespaces/tal"
+ xmlns:metal="http://xml.zope.org/namespaces/metal"
+ xmlns:i18n="http://xml.zope.org/namespaces/i18n"
+ tal:define="context_menu view/context/menu:context"
+ id="related-snaps">
+
+ <h3>Related snap packages</h3>
+
+ <div id="snap-links" class="actions">
+ <div id="snap-summary" tal:condition="view/show_snap_information">
+ <tal:no-snaps condition="not: view/snap_count">
+ No snap packages
+ </tal:no-snaps>
+ <tal:snaps condition="view/snap_count">
+ <a href="+snaps" tal:content="structure view/snap_count_text">
+ 1 snap package
+ </a>
+ </tal:snaps>
+ using this branch.
+ </div>
+ </div>
+
+</div>
=== modified file 'lib/lp/code/templates/gitref-index.pt'
--- lib/lp/code/templates/gitref-index.pt 2015-04-29 15:06:39 +0000
+++ lib/lp/code/templates/gitref-index.pt 2015-09-16 13:47:51 +0000
@@ -20,6 +20,8 @@
<div id="ref-relations" class="portlet">
<tal:ref-pending-merges
replace="structure context/@@++ref-pending-merges" />
+ <tal:ref-snaps
+ replace="structure context/@@++ref-snaps" />
</div>
</div>
=== added file 'lib/lp/code/templates/gitref-snaps.pt'
--- lib/lp/code/templates/gitref-snaps.pt 1970-01-01 00:00:00 +0000
+++ lib/lp/code/templates/gitref-snaps.pt 2015-09-16 13:47:51 +0000
@@ -0,0 +1,25 @@
+<div
+ xmlns:tal="http://xml.zope.org/namespaces/tal"
+ xmlns:metal="http://xml.zope.org/namespaces/metal"
+ xmlns:i18n="http://xml.zope.org/namespaces/i18n"
+ tal:condition="view/show_snap_information"
+ tal:define="context_menu view/context/menu:context"
+ id="related-snaps">
+
+ <h3>Related snap packages</h3>
+
+ <div id="snap-links" class="actions">
+ <div id="snap-summary">
+ <tal:no-snaps condition="not: view/snap_count">
+ No snap packages
+ </tal:no-snaps>
+ <tal:snaps condition="view/snap_count">
+ <a href="+snaps" tal:content="structure view/snap_count_text">
+ 1 snap package
+ </a>
+ </tal:snaps>
+ using this branch.
+ </div>
+ </div>
+
+</div>
=== modified file 'lib/lp/code/templates/gitrepository-index.pt'
--- lib/lp/code/templates/gitrepository-index.pt 2015-06-12 12:12:01 +0000
+++ lib/lp/code/templates/gitrepository-index.pt 2015-09-16 13:47:51 +0000
@@ -41,6 +41,13 @@
</div>
<div class="yui-g">
+ <div id="repository-relations" class="portlet">
+ <tal:repository-snaps
+ replace="structure context/@@++repository-snaps" />
+ </div>
+ </div>
+
+ <div class="yui-g">
<div id="repository-branches" class="portlet"
tal:define="branches view/branches">
<h2>Branches</h2>
=== added file 'lib/lp/code/templates/gitrepository-snaps.pt'
--- lib/lp/code/templates/gitrepository-snaps.pt 1970-01-01 00:00:00 +0000
+++ lib/lp/code/templates/gitrepository-snaps.pt 2015-09-16 13:47:51 +0000
@@ -0,0 +1,25 @@
+<div
+ xmlns:tal="http://xml.zope.org/namespaces/tal"
+ xmlns:metal="http://xml.zope.org/namespaces/metal"
+ xmlns:i18n="http://xml.zope.org/namespaces/i18n"
+ tal:condition="view/show_snap_information"
+ tal:define="context_menu context/menu:context"
+ id="related-snaps">
+
+ <h3>Related snap packages</h3>
+
+ <div id="snap-links" class="actions">
+ <div id="snap-summary">
+ <tal:no-snaps condition="not: view/snap_count">
+ No snap packages
+ </tal:no-snaps>
+ <tal:snaps condition="view/snap_count">
+ <a href="+snaps" tal:content="structure view/snap_count_text">
+ 1 snap package
+ </a>
+ </tal:snaps>
+ using this repository.
+ </div>
+ </div>
+
+</div>
=== modified file 'lib/lp/registry/browser/person.py'
--- lib/lp/registry/browser/person.py 2015-09-11 12:20:23 +0000
+++ lib/lp/registry/browser/person.py 2015-09-16 13:47:51 +0000
@@ -274,6 +274,10 @@
from lp.services.webapp.publisher import LaunchpadView
from lp.services.worlddata.interfaces.country import ICountry
from lp.services.worlddata.interfaces.language import ILanguageSet
+from lp.snappy.browser.hassnaps import (
+ HasSnapsMenuMixin,
+ HasSnapsViewMixin,
+ )
from lp.snappy.interfaces.snap import ISnapSet
from lp.soyuz.browser.archivesubscription import (
traverse_archive_subscription_for_subscriber,
@@ -776,7 +780,7 @@
class PersonOverviewMenu(ApplicationMenu, PersonMenuMixin,
- HasRecipesMenuMixin):
+ HasRecipesMenuMixin, HasSnapsMenuMixin):
usedfor = IPerson
facet = 'overview'
@@ -807,6 +811,7 @@
'oauth_tokens',
'related_software_summary',
'view_recipes',
+ 'view_snaps',
'subscriptions',
'structural_subscriptions',
]
@@ -1648,7 +1653,8 @@
raise AssertionError('Unknown group to contact.')
-class PersonView(LaunchpadView, FeedsMixin, ContactViaWebLinksMixin):
+class PersonView(LaunchpadView, FeedsMixin, ContactViaWebLinksMixin,
+ HasSnapsViewMixin):
"""A View class used in almost all Person's pages."""
@property
=== modified file 'lib/lp/registry/browser/product.py'
--- lib/lp/registry/browser/product.py 2015-07-15 04:26:30 +0000
+++ lib/lp/registry/browser/product.py 2015-09-16 13:47:51 +0000
@@ -144,6 +144,7 @@
from lp.code.browser.branchref import BranchRef
from lp.code.browser.codeimport import validate_import_url
from lp.code.browser.sourcepackagerecipelisting import HasRecipesMenuMixin
+from lp.code.browser.vcslisting import TargetDefaultVCSNavigationMixin
from lp.code.enums import (
BranchType,
RevisionControlSystems,
@@ -160,8 +161,6 @@
ICodeImportSet,
)
from lp.code.interfaces.gitrepository import IGitRepositorySet
-from lp.code.browser.vcslisting import TargetDefaultVCSNavigationMixin
-from lp.code.interfaces.gitrepository import IGitRepositorySet
from lp.registry.browser import (
add_subscribe_link,
BaseRdfView,
@@ -225,6 +224,7 @@
from lp.services.webapp.vhosts import allvhosts
from lp.services.worlddata.helpers import browser_languages
from lp.services.worlddata.interfaces.country import ICountry
+from lp.snappy.browser.hassnaps import HasSnapsMenuMixin
from lp.translations.browser.customlanguagecode import (
HasCustomLanguageCodesTraversalMixin,
)
@@ -520,7 +520,7 @@
class ProductOverviewMenu(ApplicationMenu, ProductEditLinksMixin,
- HasRecipesMenuMixin):
+ HasRecipesMenuMixin, HasSnapsMenuMixin):
usedfor = IProduct
facet = 'overview'
@@ -546,6 +546,7 @@
'rdf',
'branding',
'view_recipes',
+ 'view_snaps',
]
def top_contributors(self):
=== modified file 'lib/lp/registry/browser/team.py'
--- lib/lp/registry/browser/team.py 2015-07-08 16:05:11 +0000
+++ lib/lp/registry/browser/team.py 2015-09-16 13:47:51 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2013 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
__metaclass__ = type
@@ -177,6 +177,7 @@
ILaunchBag,
IMultiFacetedBreadcrumb,
)
+from lp.snappy.browser.hassnaps import HasSnapsMenuMixin
@implementer(IObjectPrivacy)
@@ -1614,7 +1615,8 @@
return Link(target, text, icon='team', enabled=enabled)
-class TeamOverviewMenu(ApplicationMenu, TeamMenuMixin, HasRecipesMenuMixin):
+class TeamOverviewMenu(ApplicationMenu, TeamMenuMixin, HasRecipesMenuMixin,
+ HasSnapsMenuMixin):
usedfor = ITeam
facet = 'overview'
@@ -1643,6 +1645,7 @@
'ppa',
'related_software_summary',
'view_recipes',
+ 'view_snaps',
'subscriptions',
'structural_subscriptions',
'upcomingwork',
=== modified file 'lib/lp/registry/interfaces/person.py'
--- lib/lp/registry/interfaces/person.py 2015-07-21 09:04:01 +0000
+++ lib/lp/registry/interfaces/person.py 2015-09-16 13:47:51 +0000
@@ -167,6 +167,7 @@
patch_reference_property,
)
from lp.services.worlddata.interfaces.language import ILanguage
+from lp.snappy.interfaces.hassnaps import IHasSnaps
from lp.translations.interfaces.hastranslationimports import (
IHasTranslationImports,
)
@@ -695,7 +696,8 @@
IHasMergeProposals, IHasMugshot,
IHasLocation, IHasRequestedReviews, IObjectWithLocation,
IHasBugs, IHasRecipes, IHasTranslationImports,
- IPersonSettings, IQuestionsPerson, IHasGitRepositories):
+ IPersonSettings, IQuestionsPerson, IHasGitRepositories,
+ IHasSnaps):
"""IPerson attributes that require launchpad.View permission."""
account = Object(schema=IAccount)
accountID = Int(title=_('Account ID'), required=True, readonly=True)
=== modified file 'lib/lp/registry/interfaces/product.py'
--- lib/lp/registry/interfaces/product.py 2015-07-07 22:33:29 +0000
+++ lib/lp/registry/interfaces/product.py 2015-09-16 13:47:51 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2013 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Interfaces including and related to IProduct."""
@@ -147,6 +147,7 @@
patch_collection_property,
patch_reference_property,
)
+from lp.snappy.interfaces.hassnaps import IHasSnaps
from lp.translations.interfaces.hastranslationimports import (
IHasTranslationImports,
)
@@ -482,7 +483,8 @@
IHasMugshot, IHasSprints, IHasTranslationImports,
ITranslationPolicy, IKarmaContext, IMakesAnnouncements,
IOfficialBugTagTargetPublic, IHasOOPSReferences,
- IHasRecipes, IHasCodeImports, IServiceUsage, IHasGitRepositories):
+ IHasRecipes, IHasCodeImports, IServiceUsage, IHasGitRepositories,
+ IHasSnaps):
"""Public IProduct properties."""
registrant = exported(
=== modified file 'lib/lp/registry/model/person.py'
--- lib/lp/registry/model/person.py 2015-07-21 09:04:01 +0000
+++ lib/lp/registry/model/person.py 2015-09-16 13:47:51 +0000
@@ -305,6 +305,7 @@
from lp.services.verification.model.logintoken import LoginToken
from lp.services.webapp.interfaces import ILaunchBag
from lp.services.worlddata.model.language import Language
+from lp.snappy.model.hassnaps import HasSnapsMixin
from lp.soyuz.enums import (
ArchivePurpose,
ArchiveStatus,
@@ -476,7 +477,7 @@
class Person(
SQLBase, HasBugsBase, HasSpecificationsMixin, HasTranslationImportsMixin,
HasBranchesMixin, HasMergeProposalsMixin, HasRequestedReviewsMixin,
- QuestionsPersonMixin):
+ QuestionsPersonMixin, HasSnapsMixin):
"""A Person."""
def __init__(self, *args, **kwargs):
=== modified file 'lib/lp/registry/model/product.py'
--- lib/lp/registry/model/product.py 2015-07-09 20:06:17 +0000
+++ lib/lp/registry/model/product.py 2015-09-16 13:47:51 +0000
@@ -44,8 +44,6 @@
SQL,
)
from storm.locals import (
- Int,
- List,
Store,
Unicode,
)
@@ -208,6 +206,7 @@
)
from lp.services.statistics.interfaces.statistic import ILaunchpadStatisticSet
from lp.services.webapp.interfaces import ILaunchBag
+from lp.snappy.model.hassnaps import HasSnapsMixin
from lp.translations.enums import TranslationPermission
from lp.translations.interfaces.customlanguagecode import (
IHasCustomLanguageCodes,
@@ -367,7 +366,7 @@
OfficialBugTagTargetMixin, HasBranchesMixin,
HasCustomLanguageCodesMixin, HasMergeProposalsMixin,
HasCodeImportsMixin, InformationTypeMixin,
- TranslationPolicyMixin):
+ TranslationPolicyMixin, HasSnapsMixin):
"""A Product."""
_table = 'Product'
=== modified file 'lib/lp/registry/personmerge.py'
--- lib/lp/registry/personmerge.py 2015-08-03 14:22:23 +0000
+++ lib/lp/registry/personmerge.py 2015-09-16 13:47:51 +0000
@@ -619,9 +619,9 @@
def _mergeSnap(cur, from_person, to_person):
# This shouldn't use removeSecurityProxy.
- snaps = getUtility(ISnapSet).findByPerson(from_person)
+ snaps = getUtility(ISnapSet).findByOwner(from_person)
existing_names = [
- s.name for s in getUtility(ISnapSet).findByPerson(to_person)]
+ s.name for s in getUtility(ISnapSet).findByOwner(to_person)]
for snap in snaps:
new_name = snap.name
count = 1
=== modified file 'lib/lp/registry/templates/product-index.pt'
--- lib/lp/registry/templates/product-index.pt 2015-06-21 22:06:53 +0000
+++ lib/lp/registry/templates/product-index.pt 2015-09-16 13:47:51 +0000
@@ -203,6 +203,10 @@
tal:condition="link/enabled">
<a tal:replace="structure link/fmt:link" />
</li>
+ <li tal:define="link context/menu:overview/view_snaps"
+ tal:condition="link/enabled">
+ <a tal:replace="structure link/fmt:link" />
+ </li>
</ul>
</div>
</div>
=== modified file 'lib/lp/registry/tests/test_personmerge.py'
--- lib/lp/registry/tests/test_personmerge.py 2015-08-03 14:22:23 +0000
+++ lib/lp/registry/tests/test_personmerge.py 2015-09-16 13:47:51 +0000
@@ -574,7 +574,7 @@
self._do_premerge(duplicate, mergee)
login_person(mergee)
duplicate, mergee = self._do_merge(duplicate, mergee)
- self.assertEqual(1, getUtility(ISnapSet).findByPerson(mergee).count())
+ self.assertEqual(1, getUtility(ISnapSet).findByOwner(mergee).count())
def test_merge_with_duplicated_snaps(self):
# If both the from and to people have snap packages with the same
@@ -592,7 +592,7 @@
login_person(mergee)
duplicate, mergee = self._do_merge(duplicate, mergee)
snaps = sorted(
- getUtility(ISnapSet).findByPerson(mergee), key=attrgetter("name"))
+ getUtility(ISnapSet).findByOwner(mergee), key=attrgetter("name"))
self.assertEqual(2, len(snaps))
self.assertIsNone(snaps[0].branch)
self.assertEqual(ref.repository, snaps[0].git_repository)
=== modified file 'lib/lp/snappy/browser/configure.zcml'
--- lib/lp/snappy/browser/configure.zcml 2015-09-04 16:20:26 +0000
+++ lib/lp/snappy/browser/configure.zcml 2015-09-16 13:47:51 +0000
@@ -91,5 +91,36 @@
for="lp.snappy.interfaces.snapbuild.ISnapBuild"
factory="lp.services.webapp.breadcrumb.TitleBreadcrumb"
permission="zope.Public" />
+
+ <browser:page
+ for="lp.code.interfaces.branch.IBranch"
+ class="lp.snappy.browser.snaplisting.BranchSnapListingView"
+ permission="zope.Public"
+ name="+snaps"
+ template="../templates/snap-listing.pt" />
+ <browser:page
+ for="lp.code.interfaces.gitrepository.IGitRepository"
+ class="lp.snappy.browser.snaplisting.GitSnapListingView"
+ permission="zope.Public"
+ name="+snaps"
+ template="../templates/snap-listing.pt" />
+ <browser:page
+ for="lp.code.interfaces.gitref.IGitRef"
+ class="lp.snappy.browser.snaplisting.GitSnapListingView"
+ permission="zope.Public"
+ name="+snaps"
+ template="../templates/snap-listing.pt" />
+ <browser:page
+ for="lp.registry.interfaces.person.IPerson"
+ class="lp.snappy.browser.snaplisting.PersonSnapListingView"
+ permission="zope.Public"
+ name="+snaps"
+ template="../templates/snap-listing.pt" />
+ <browser:page
+ for="lp.registry.interfaces.product.IProduct"
+ class="lp.snappy.browser.snaplisting.ProjectSnapListingView"
+ permission="zope.Public"
+ name="+snaps"
+ template="../templates/snap-listing.pt" />
</facet>
</configure>
=== added file 'lib/lp/snappy/browser/hassnaps.py'
--- lib/lp/snappy/browser/hassnaps.py 1970-01-01 00:00:00 +0000
+++ lib/lp/snappy/browser/hassnaps.py 2015-09-16 13:47:51 +0000
@@ -0,0 +1,46 @@
+# Copyright 2015 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Mixins for browser classes for objects that implement IHasSnaps."""
+
+__metaclass__ = type
+__all__ = [
+ 'HasSnapsMenuMixin',
+ 'HasSnapsViewMixin',
+ ]
+
+from lp.services.features import getFeatureFlag
+from lp.services.propertycache import cachedproperty
+from lp.services.webapp import Link
+from lp.snappy.interfaces.snap import SNAP_FEATURE_FLAG
+
+
+class HasSnapsMenuMixin:
+ """A mixin for context menus for objects that implement IHasSnaps."""
+
+ def view_snaps(self):
+ text = 'View snap packages'
+ enabled = not self.context.getSnaps().is_empty()
+ return Link('+snaps', text, icon='info', enabled=enabled)
+
+
+class HasSnapsViewMixin:
+ """A view mixin for objects that implement IHasSnaps."""
+
+ @cachedproperty
+ def snap_count(self):
+ return self.context.getSnaps().count()
+
+ @property
+ def show_snap_information(self):
+ return getFeatureFlag(SNAP_FEATURE_FLAG) or self.snap_count
+
+ @property
+ def snap_count_text(self):
+ count = self.snap_count
+ if count == 0:
+ return 'No snap packages'
+ elif count == 1:
+ return '1 snap package'
+ else:
+ return '%s snap packages' % count
=== modified file 'lib/lp/snappy/browser/snap.py'
--- lib/lp/snappy/browser/snap.py 2015-09-09 14:17:46 +0000
+++ lib/lp/snappy/browser/snap.py 2015-09-16 13:47:51 +0000
@@ -113,15 +113,6 @@
self.context, field, format_link(self.context.owner),
header='Change owner', step_title='Select a new owner')
- @property
- def source(self):
- if self.context.branch is not None:
- return self.context.branch
- elif self.context.git_ref is not None:
- return self.context.git_ref
- else:
- return None
-
def builds_for_snap(snap):
"""A list of interesting builds.
=== added file 'lib/lp/snappy/browser/snaplisting.py'
--- lib/lp/snappy/browser/snaplisting.py 1970-01-01 00:00:00 +0000
+++ lib/lp/snappy/browser/snaplisting.py 2015-09-16 13:47:51 +0000
@@ -0,0 +1,74 @@
+# Copyright 2015 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Base class view for snap listings."""
+
+__metaclass__ = type
+
+__all__ = [
+ 'BranchSnapListingView',
+ 'GitSnapListingView',
+ 'PersonSnapListingView',
+ ]
+
+from lp.code.browser.decorations import DecoratedBranch
+from lp.services.feeds.browser import FeedsMixin
+from lp.services.webapp import (
+ canonical_url,
+ LaunchpadView,
+ )
+
+
+class SnapListingView(LaunchpadView, FeedsMixin):
+
+ feed_types = ()
+
+ source_enabled = True
+ owner_enabled = True
+
+ @property
+ def page_title(self):
+ return 'Snap packages'
+
+ @property
+ def label(self):
+ return 'Snap packages for %(displayname)s' % {
+ 'displayname': self.context.displayname}
+
+ def initialize(self):
+ super(SnapListingView, self).initialize()
+ self.snaps = self.context.getSnaps(eager_load=True)
+ if self.snaps.count() == 1:
+ snap = self.snaps.one()
+ self.request.response.redirect(canonical_url(snap))
+
+
+class BranchSnapListingView(SnapListingView):
+
+ source_enabled = False
+
+ def initialize(self):
+ super(BranchSnapListingView, self).initialize()
+ # Replace our context with a decorated branch, if it is not already
+ # decorated.
+ if not isinstance(self.context, DecoratedBranch):
+ self.context = DecoratedBranch(self.context)
+
+
+class GitSnapListingView(SnapListingView):
+
+ source_enabled = False
+
+ @property
+ def label(self):
+ return 'Snap packages for %(display_name)s' % {
+ 'display_name': self.context.display_name}
+
+
+class PersonSnapListingView(SnapListingView):
+
+ owner_enabled = False
+
+
+class ProjectSnapListingView(SnapListingView):
+ pass
=== added file 'lib/lp/snappy/browser/tests/test_snaplisting.py'
--- lib/lp/snappy/browser/tests/test_snaplisting.py 1970-01-01 00:00:00 +0000
+++ lib/lp/snappy/browser/tests/test_snaplisting.py 2015-09-16 13:47:51 +0000
@@ -0,0 +1,248 @@
+# Copyright 2015 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Test snap package listings."""
+
+__metaclass__ = type
+
+import soupmatchers
+from testtools.matchers import (
+ Equals,
+ Not,
+ )
+
+from lp.services.database.constants import (
+ ONE_DAY_AGO,
+ UTC_NOW,
+ )
+from lp.services.features.testing import FeatureFixture
+from lp.services.webapp import canonical_url
+from lp.snappy.interfaces.snap import SNAP_FEATURE_FLAG
+from lp.testing import (
+ ANONYMOUS,
+ BrowserTestCase,
+ login,
+ person_logged_in,
+ record_two_runs,
+ )
+from lp.testing.layers import DatabaseFunctionalLayer
+from lp.testing.matchers import HasQueryCount
+
+
+class TestSnapListing(BrowserTestCase):
+
+ layer = DatabaseFunctionalLayer
+
+ def makeSnap(self, **kwargs):
+ """Create a snap package, enabling the feature flag.
+
+ We do things this way rather than by calling self.useFixture because
+ opening a URL in a test browser loses the thread-local feature flag.
+ """
+ with FeatureFixture({SNAP_FEATURE_FLAG: u"on"}):
+ return self.factory.makeSnap(**kwargs)
+
+ def assertSnapsLink(self, context, link_text, link_has_context=False,
+ **kwargs):
+ if link_has_context:
+ expected_href = canonical_url(context, view_name="+snaps")
+ else:
+ expected_href = "+snaps"
+ matcher = soupmatchers.HTMLContains(
+ soupmatchers.Tag(
+ "View snap packages link", "a", text=link_text,
+ attrs={"href": expected_href}))
+ self.assertThat(self.getViewBrowser(context).contents, Not(matcher))
+ login(ANONYMOUS)
+ self.makeSnap(**kwargs)
+ self.assertThat(self.getViewBrowser(context).contents, matcher)
+
+ def test_branch_links_to_snaps(self):
+ branch = self.factory.makeAnyBranch()
+ self.assertSnapsLink(branch, "1 snap package", branch=branch)
+
+ def test_git_repository_links_to_snaps(self):
+ repository = self.factory.makeGitRepository()
+ [ref] = self.factory.makeGitRefs(repository=repository)
+ self.assertSnapsLink(repository, "1 snap package", git_ref=ref)
+
+ def test_git_ref_links_to_snaps(self):
+ [ref] = self.factory.makeGitRefs()
+ self.assertSnapsLink(ref, "1 snap package", git_ref=ref)
+
+ def test_person_links_to_snaps(self):
+ person = self.factory.makePerson()
+ self.assertSnapsLink(
+ person, "View snap packages", link_has_context=True,
+ registrant=person, owner=person)
+
+ def test_project_links_to_snaps(self):
+ project = self.factory.makeProduct()
+ [ref] = self.factory.makeGitRefs(target=project)
+ self.assertSnapsLink(
+ project, "View snap packages", link_has_context=True, git_ref=ref)
+
+ def test_branch_snap_listing(self):
+ # We can see snap packages for a Bazaar branch. We need to create
+ # two, since if there's only one then +snaps will redirect to that
+ # package.
+ branch = self.factory.makeAnyBranch()
+ for _ in range(2):
+ self.makeSnap(branch=branch)
+ text = self.getMainText(branch, "+snaps")
+ self.assertTextMatchesExpressionIgnoreWhitespace("""
+ Snap packages for lp:.*
+ Name Owner Registered
+ snap-name.* Team Name.* .*
+ snap-name.* Team Name.* .*""", text)
+
+ def test_git_repository_snap_listing(self):
+ # We can see snap packages for a Git repository. We need to create
+ # two, since if there's only one then +snaps will redirect to that
+ # package.
+ repository = self.factory.makeGitRepository()
+ ref1, ref2 = self.factory.makeGitRefs(
+ repository=repository,
+ paths=[u"refs/heads/branch-1", u"refs/heads/branch-2"])
+ for ref in ref1, ref2:
+ self.makeSnap(git_ref=ref)
+ text = self.getMainText(repository, "+snaps")
+ self.assertTextMatchesExpressionIgnoreWhitespace("""
+ Snap packages for lp:~.*
+ Name Owner Registered
+ snap-name.* Team Name.* .*
+ snap-name.* Team Name.* .*""", text)
+
+ def test_git_ref_snap_listing(self):
+ # We can see snap packages for a Git reference. We need to create
+ # two, since if there's only one then +snaps will redirect to that
+ # package.
+ [ref] = self.factory.makeGitRefs()
+ for _ in range(2):
+ self.makeSnap(git_ref=ref)
+ text = self.getMainText(ref, "+snaps")
+ self.assertTextMatchesExpressionIgnoreWhitespace("""
+ Snap packages for ~.*:.*
+ Name Owner Registered
+ snap-name.* Team Name.* .*
+ snap-name.* Team Name.* .*""", text)
+
+ def test_person_snap_listing(self):
+ # We can see snap packages for a person. We need to create two,
+ # since if there's only one then +snaps will redirect to that
+ # package.
+ owner = self.factory.makePerson(displayname="Snap Owner")
+ self.makeSnap(
+ registrant=owner, owner=owner, branch=self.factory.makeAnyBranch(),
+ date_created=ONE_DAY_AGO)
+ [ref] = self.factory.makeGitRefs()
+ self.makeSnap(
+ registrant=owner, owner=owner, git_ref=ref, date_created=UTC_NOW)
+ text = self.getMainText(owner, "+snaps")
+ self.assertTextMatchesExpressionIgnoreWhitespace("""
+ Snap packages for Snap Owner
+ Name Source Registered
+ snap-name.* ~.*:.* .*
+ snap-name.* lp:.* .*""", text)
+
+ def test_project_snap_listing(self):
+ # We can see snap packages for a project. We need to create two,
+ # since if there's only one then +snaps will redirect to that
+ # package.
+ project = self.factory.makeProduct(displayname="Snappable")
+ self.makeSnap(
+ branch=self.factory.makeProductBranch(product=project),
+ date_created=ONE_DAY_AGO)
+ [ref] = self.factory.makeGitRefs(target=project)
+ self.makeSnap(git_ref=ref, date_created=UTC_NOW)
+ text = self.getMainText(project, "+snaps")
+ self.assertTextMatchesExpressionIgnoreWhitespace("""
+ Snap packages for Snappable
+ Name Owner Source Registered
+ snap-name.* Team Name.* ~.*:.* .*
+ snap-name.* Team Name.* lp:.* .*""", text)
+
+ def assertSnapsQueryCount(self, context, item_creator):
+ recorder1, recorder2 = record_two_runs(
+ lambda: self.getMainText(context, "+snaps"), item_creator, 5)
+ self.assertThat(recorder2, HasQueryCount(Equals(recorder1.count)))
+
+ def test_branch_query_count(self):
+ # The number of queries required to render the list of all snap
+ # packages for a Bazaar branch is constant in the number of owners
+ # and snap packages.
+ person = self.factory.makePerson()
+ branch = self.factory.makeAnyBranch(owner=person)
+
+ def create_snap():
+ with person_logged_in(person):
+ self.makeSnap(branch=branch)
+
+ self.assertSnapsQueryCount(branch, create_snap)
+
+ def test_git_repository_query_count(self):
+ # The number of queries required to render the list of all snap
+ # packages for a Git repository is constant in the number of owners
+ # and snap packages.
+ person = self.factory.makePerson()
+ repository = self.factory.makeGitRepository(owner=person)
+
+ def create_snap():
+ with person_logged_in(person):
+ [ref] = self.factory.makeGitRefs(repository=repository)
+ self.makeSnap(git_ref=ref)
+
+ self.assertSnapsQueryCount(repository, create_snap)
+
+ def test_git_ref_query_count(self):
+ # The number of queries required to render the list of all snap
+ # packages for a Git reference is constant in the number of owners
+ # and snap packages.
+ person = self.factory.makePerson()
+ [ref] = self.factory.makeGitRefs(owner=person)
+
+ def create_snap():
+ with person_logged_in(person):
+ self.makeSnap(git_ref=ref)
+
+ self.assertSnapsQueryCount(ref, create_snap)
+
+ def test_person_query_count(self):
+ # The number of queries required to render the list of all snap
+ # packages for a person is constant in the number of projects,
+ # sources, and snap packages.
+ person = self.factory.makePerson()
+ i = 0
+
+ def create_snap():
+ with person_logged_in(person):
+ project = self.factory.makeProduct()
+ if (i % 2) == 0:
+ branch = self.factory.makeProductBranch(
+ owner=person, product=project)
+ self.makeSnap(branch=branch)
+ else:
+ [ref] = self.factory.makeGitRefs(
+ owner=person, target=project)
+ self.makeSnap(git_ref=ref)
+
+ self.assertSnapsQueryCount(person, create_snap)
+
+ def test_project_query_count(self):
+ # The number of queries required to render the list of all snap
+ # packages for a person is constant in the number of owners,
+ # sources, and snap packages.
+ person = self.factory.makePerson()
+ project = self.factory.makeProduct(owner=person)
+ i = 0
+
+ def create_snap():
+ with person_logged_in(person):
+ if (i % 2) == 0:
+ branch = self.factory.makeProductBranch(product=project)
+ self.makeSnap(branch=branch)
+ else:
+ [ref] = self.factory.makeGitRefs(target=project)
+ self.makeSnap(git_ref=ref)
+
+ self.assertSnapsQueryCount(project, create_snap)
=== added file 'lib/lp/snappy/interfaces/hassnaps.py'
--- lib/lp/snappy/interfaces/hassnaps.py 1970-01-01 00:00:00 +0000
+++ lib/lp/snappy/interfaces/hassnaps.py 2015-09-16 13:47:51 +0000
@@ -0,0 +1,20 @@
+# Copyright 2015 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Interface definitions for IHasSnaps."""
+
+__metaclass__ = type
+__all__ = [
+ 'IHasSnaps',
+ ]
+
+from zope.interface import Interface
+
+
+class IHasSnaps(Interface):
+ """An object that has snap packages."""
+
+ # For internal convenience and intentionally not exported.
+ # XXX cjwatson 2015-09-16: Export something suitable on ISnapSet.
+ def getSnaps(eager_load=False, order_by_date=True):
+ """Return all snap packages associated with the object."""
=== modified file 'lib/lp/snappy/interfaces/snap.py'
--- lib/lp/snappy/interfaces/snap.py 2015-09-09 14:17:46 +0000
+++ lib/lp/snappy/interfaces/snap.py 2015-09-16 13:47:51 +0000
@@ -6,6 +6,7 @@
__metaclass__ = type
__all__ = [
+ 'BadSnapSearchContext',
'CannotDeleteSnap',
'DuplicateSnapName',
'ISnap',
@@ -45,7 +46,10 @@
Reference,
ReferenceChoice,
)
-from zope.interface import Interface
+from zope.interface import (
+ Attribute,
+ Interface,
+ )
from zope.schema import (
Bool,
Choice,
@@ -163,6 +167,10 @@
"""This snap package cannot be deleted."""
+class BadSnapSearchContext(Exception):
+ """The context is not valid for a snap package search."""
+
+
class ISnapView(Interface):
"""`ISnap` attributes that require launchpad.View permission."""
@@ -176,6 +184,9 @@
vocabulary="ValidPersonOrTeam",
description=_("The person who registered this snap package.")))
+ source = Attribute(
+ "The source branch for this snap package (VCS-agnostic).")
+
@call_with(requester=REQUEST_USER)
@operation_parameters(
archive=Reference(schema=IArchive),
@@ -351,15 +362,52 @@
def getByName(owner, name):
"""Return the appropriate `ISnap` for the given objects."""
- def findByPerson(owner):
+ def findByOwner(owner):
"""Return all snap packages with the given `owner`."""
+ def findByPerson(person, visible_by_user=None):
+ """Return all snap packages relevant to `person`.
+
+ This returns snap packages for Bazaar or Git branches owned by
+ `person`, or where `person` is the owner of the snap package.
+
+ :param person: An `IPerson`.
+ :param visible_by_user: If not None, only return packages visible by
+ this user.
+ """
+
+ def findByProject(project, visible_by_user=None):
+ """Return all snap packages for the given project.
+
+ :param project: An `IProduct`.
+ :param visible_by_user: If not None, only return packages visible by
+ this user.
+ """
+
def findByBranch(branch):
"""Return all snap packages for the given Bazaar branch."""
def findByGitRepository(repository):
"""Return all snap packages for the given Git repository."""
+ def findByGitRef(ref):
+ """Return all snap packages for the given Git reference."""
+
+ def findByContext(context, visible_by_user=None, order_by_date=True):
+ """Return all snap packages for the given context.
+
+ :param context: An `IPerson`, `IProduct, `IBranch`,
+ `IGitRepository`, or `IGitRef`.
+ :param visible_by_user: If not None, only return packages visible by
+ this user.
+ :param order_by_date: If True, order packages by descending
+ modification date.
+ :raises BadSnapSearchContext: if the context is not understood.
+ """
+
+ def preloadDataForSnaps(snaps, user):
+ """Load the data related to a list of snap packages."""
+
def detachFromBranch(branch):
"""Detach all snap packages from the given Bazaar branch.
=== added file 'lib/lp/snappy/model/hassnaps.py'
--- lib/lp/snappy/model/hassnaps.py 1970-01-01 00:00:00 +0000
+++ lib/lp/snappy/model/hassnaps.py 2015-09-16 13:47:51 +0000
@@ -0,0 +1,32 @@
+# Copyright 2015 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Mixin classes for objects that implement IHasSnaps."""
+
+__metaclass__ = type
+__all__ = [
+ 'HasSnapsMixin',
+ ]
+
+from functools import partial
+
+from zope.component import getUtility
+
+from lp.services.database.decoratedresultset import DecoratedResultSet
+from lp.services.webapp.interfaces import ILaunchBag
+from lp.snappy.interfaces.snap import ISnapSet
+
+
+class HasSnapsMixin:
+ """A mixin implementation for `IHasSnaps`."""
+
+ def getSnaps(self, eager_load=False, order_by_date=True):
+ user = getUtility(ILaunchBag).user
+ snaps = getUtility(ISnapSet).findByContext(
+ self, visible_by_user=user, order_by_date=order_by_date)
+ if not eager_load:
+ return snaps
+ else:
+ loader = partial(
+ getUtility(ISnapSet).preloadDataForSnaps, user=user)
+ return DecoratedResultSet(snaps, pre_iter_hook=loader)
=== modified file 'lib/lp/snappy/model/snap.py'
--- lib/lp/snappy/model/snap.py 2015-09-10 17:15:45 +0000
+++ lib/lp/snappy/model/snap.py 2015-09-16 13:47:51 +0000
@@ -26,7 +26,32 @@
from lp.buildmaster.enums import BuildStatus
from lp.buildmaster.interfaces.processor import IProcessorSet
from lp.buildmaster.model.processor import Processor
+from lp.code.interfaces.branch import IBranch
+from lp.code.interfaces.branchcollection import (
+ IAllBranches,
+ IBranchCollection,
+ )
+from lp.code.interfaces.gitcollection import (
+ IAllGitRepositories,
+ IGitCollection,
+ )
+from lp.code.interfaces.gitref import IGitRef
+from lp.code.interfaces.gitrepository import IGitRepository
+from lp.code.model.branch import Branch
+from lp.code.model.branchcollection import GenericBranchCollection
+from lp.code.model.gitcollection import GenericGitCollection
+from lp.code.model.gitref import GitRef
+from lp.code.model.gitrepository import GitRepository
+from lp.registry.interfaces.person import (
+ IPerson,
+ IPersonSet,
+ )
+from lp.registry.interfaces.product import IProduct
from lp.registry.interfaces.role import IHasOwner
+from lp.services.database.bulk import (
+ load,
+ load_related,
+ )
from lp.services.database.constants import (
DEFAULT,
UTC_NOW,
@@ -42,6 +67,7 @@
from lp.services.features import getFeatureFlag
from lp.services.webapp.interfaces import ILaunchBag
from lp.snappy.interfaces.snap import (
+ BadSnapSearchContext,
CannotDeleteSnap,
DuplicateSnapName,
ISnap,
@@ -146,6 +172,15 @@
self.git_repository = None
self.git_path = None
+ @property
+ def source(self):
+ if self.branch is not None:
+ return self.branch
+ elif self.git_ref is not None:
+ return self.git_ref
+ else:
+ return None
+
def _getProcessors(self):
return list(Store.of(self).find(
Processor,
@@ -348,10 +383,29 @@
raise NoSuchSnap(name)
return snap
- def findByPerson(self, owner):
+ def findByOwner(self, owner):
"""See `ISnapSet`."""
return IStore(Snap).find(Snap, Snap.owner == owner)
+ def findByPerson(self, person, visible_by_user=None):
+ """See `ISnapSet`."""
+ def _getSnaps(collection):
+ collection = collection.visibleByUser(visible_by_user)
+ return collection.getSnapsForPerson(person)
+
+ bzr_collection = removeSecurityProxy(getUtility(IAllBranches))
+ git_collection = removeSecurityProxy(getUtility(IAllGitRepositories))
+ return _getSnaps(bzr_collection).union(_getSnaps(git_collection))
+
+ def findByProject(self, project, visible_by_user=None):
+ """See `ISnapSet`."""
+ def _getSnaps(collection):
+ return collection.visibleByUser(visible_by_user).getSnaps()
+
+ bzr_collection = removeSecurityProxy(IBranchCollection(project))
+ git_collection = removeSecurityProxy(IGitCollection(project))
+ return _getSnaps(bzr_collection).union(_getSnaps(git_collection))
+
def findByBranch(self, branch):
"""See `ISnapSet`."""
return IStore(Snap).find(Snap, Snap.branch == branch)
@@ -360,6 +414,76 @@
"""See `ISnapSet`."""
return IStore(Snap).find(Snap, Snap.git_repository == repository)
+ def findByGitRef(self, ref):
+ """See `ISnapSet`."""
+ return IStore(Snap).find(
+ Snap,
+ Snap.git_repository == ref.repository, Snap.git_path == ref.path)
+
+ def findByContext(self, context, visible_by_user=None, order_by_date=True):
+ if IPerson.providedBy(context):
+ snaps = self.findByPerson(context, visible_by_user=visible_by_user)
+ elif IProduct.providedBy(context):
+ snaps = self.findByProject(
+ context, visible_by_user=visible_by_user)
+ # XXX cjwatson 2015-09-15: At the moment we can assume that if you
+ # can see the source context then you can see the snap packages
+ # based on it. This will cease to be true if snap packages gain
+ # privacy of their own.
+ elif IBranch.providedBy(context):
+ snaps = self.findByBranch(context)
+ elif IGitRepository.providedBy(context):
+ snaps = self.findByGitRepository(context)
+ elif IGitRef.providedBy(context):
+ snaps = self.findByGitRef(context)
+ else:
+ raise BadSnapSearchContext(context)
+ if order_by_date:
+ snaps.order_by(Desc(Snap.date_last_modified))
+ return snaps
+
+ def preloadDataForSnaps(self, snaps, user=None):
+ """See `ISnapSet`."""
+ snaps = [removeSecurityProxy(snap) for snap in snaps]
+
+ branch_ids = set()
+ git_ref_keys = set()
+ person_ids = set()
+ for snap in snaps:
+ if snap.branch_id is not None:
+ branch_ids.add(snap.branch_id)
+ if snap.git_repository_id is not None:
+ git_ref_keys.add((snap.git_repository_id, snap.git_path))
+ person_ids.add(snap.registrant_id)
+ person_ids.add(snap.owner_id)
+ git_repository_ids = set(
+ repository_id for repository_id, _ in git_ref_keys)
+
+ branches = load_related(Branch, snaps, ["branch_id"])
+ repositories = load_related(
+ GitRepository, snaps, ["git_repository_id"])
+ load(GitRef, git_ref_keys)
+ # The stacked-on branches are used to check branch visibility.
+ GenericBranchCollection.preloadVisibleStackedOnBranches(branches, user)
+ GenericGitCollection.preloadVisibleRepositories(repositories, user)
+
+ # Add branch/repository owners to the list of pre-loaded persons.
+ # We need the target repository owner as well; unlike branches,
+ # repository unique names aren't trigger-maintained.
+ person_ids.update(
+ branch.ownerID for branch in branches if branch.id in branch_ids)
+ person_ids.update(
+ repository.owner_id for repository in repositories
+ if repository.id in git_repository_ids)
+
+ list(getUtility(IPersonSet).getPrecachedPersonsFromIDs(
+ person_ids, need_validity=True))
+
+ if branches:
+ GenericBranchCollection.preloadDataForBranches(branches)
+ if repositories:
+ GenericGitCollection.preloadDataForRepositories(repositories)
+
def detachFromBranch(self, branch):
"""See `ISnapSet`."""
self.findByBranch(branch).set(
=== modified file 'lib/lp/snappy/model/snapbuild.py'
--- lib/lp/snappy/model/snapbuild.py 2015-08-03 13:20:45 +0000
+++ lib/lp/snappy/model/snapbuild.py 2015-09-16 13:47:51 +0000
@@ -33,6 +33,8 @@
from lp.buildmaster.model.buildfarmjob import SpecificBuildFarmJobSourceMixin
from lp.buildmaster.model.packagebuild import PackageBuildMixin
from lp.registry.interfaces.pocket import PackagePublishingPocket
+from lp.registry.model.distribution import Distribution
+from lp.registry.model.distroseries import DistroSeries
from lp.registry.model.person import Person
from lp.services.config import config
from lp.services.database.bulk import load_related
@@ -50,6 +52,7 @@
LibraryFileContent,
)
from lp.snappy.interfaces.snap import (
+ ISnapSet,
SNAP_FEATURE_FLAG,
SnapFeatureDisabled,
)
@@ -61,6 +64,7 @@
from lp.snappy.mail.snapbuild import SnapBuildMailer
from lp.soyuz.interfaces.component import IComponentSet
from lp.soyuz.model.archive import Archive
+from lp.soyuz.model.distroarchseries import DistroArchSeries
@implementer(ISnapFile)
@@ -357,7 +361,13 @@
load_related(LibraryFileAlias, builds, ["log_id"])
archives = load_related(Archive, builds, ["archive_id"])
load_related(Person, archives, ["ownerID"])
- load_related(Snap, builds, ["snap_id"])
+ distroarchseries = load_related(
+ DistroArchSeries, builds, ['distro_arch_series_id'])
+ distroseries = load_related(
+ DistroSeries, distroarchseries, ['distroseriesID'])
+ load_related(Distribution, distroseries, ['distributionID'])
+ snaps = load_related(Snap, builds, ["snap_id"])
+ getUtility(ISnapSet).preloadDataForSnaps(snaps)
def getByBuildFarmJobs(self, build_farm_jobs):
"""See `ISpecificBuildFarmJobSource`."""
=== modified file 'lib/lp/snappy/templates/snap-index.pt'
--- lib/lp/snappy/templates/snap-index.pt 2015-09-07 15:29:00 +0000
+++ lib/lp/snappy/templates/snap-index.pt 2015-09-16 13:47:51 +0000
@@ -40,7 +40,8 @@
<a tal:replace="structure view/menu:overview/edit/fmt:icon"/>
</dd>
</dl>
- <dl id="source" tal:define="source view/source" tal:condition="source">
+ <dl id="source"
+ tal:define="source context/source" tal:condition="source">
<dt>Source:</dt>
<dd>
<a tal:replace="structure source/fmt:link"/>
=== added file 'lib/lp/snappy/templates/snap-listing.pt'
--- lib/lp/snappy/templates/snap-listing.pt 1970-01-01 00:00:00 +0000
+++ lib/lp/snappy/templates/snap-listing.pt 2015-09-16 13:47:51 +0000
@@ -0,0 +1,40 @@
+<html
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:tal="http://xml.zope.org/namespaces/tal"
+ xmlns:metal="http://xml.zope.org/namespaces/metal"
+ xmlns:i18n="http://xml.zope.org/namespaces/i18n"
+ metal:use-macro="view/macro:page/main_only"
+ i18n:domain="launchpad">
+
+<body>
+
+ <div metal:fill-slot="main">
+
+ <table id="snaptable" class="listing sortable">
+ <thead>
+ <tr>
+ <th colspan="2">Name</th>
+ <th tal:condition="view/owner_enabled">Owner</th>
+ <th tal:condition="view/source_enabled">Source</th>
+ <th>Registered</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tal:snaps repeat="snap view/snaps">
+ <tr>
+ <td colspan="2">
+ <a tal:attributes="href snap/fmt:url" tal:content="snap/name" />
+ </td>
+ <td tal:condition="view/owner_enabled"
+ tal:content="structure snap/owner/fmt:link" />
+ <td tal:condition="view/source_enabled"
+ tal:content="structure snap/source/fmt:link" />
+ <td tal:content="snap/date_created/fmt:datetime" />
+ </tr>
+ </tal:snaps>
+ </tbody>
+ </table>
+
+ </div>
+</body>
+</html>
=== added file 'lib/lp/snappy/tests/test_hassnaps.py'
--- lib/lp/snappy/tests/test_hassnaps.py 1970-01-01 00:00:00 +0000
+++ lib/lp/snappy/tests/test_hassnaps.py 2015-09-16 13:47:51 +0000
@@ -0,0 +1,90 @@
+# Copyright 2015 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Tests for classes that implement IHasSnaps."""
+
+__metaclass__ = type
+
+from lp.services.features.testing import FeatureFixture
+from lp.snappy.interfaces.hassnaps import IHasSnaps
+from lp.snappy.interfaces.snap import SNAP_FEATURE_FLAG
+from lp.testing import TestCaseWithFactory
+from lp.testing.layers import DatabaseFunctionalLayer
+
+
+class TestIHasSnaps(TestCaseWithFactory):
+ """Test that the correct objects implement the interface."""
+
+ layer = DatabaseFunctionalLayer
+
+ def setUp(self):
+ super(TestIHasSnaps, self).setUp()
+ self.useFixture(FeatureFixture({SNAP_FEATURE_FLAG: u"on"}))
+
+ def test_branch_implements_hassnaps(self):
+ branch = self.factory.makeBranch()
+ self.assertProvides(branch, IHasSnaps)
+
+ def test_branch_getSnaps(self):
+ # IBranch.getSnaps returns all the Snaps based on that branch.
+ branch = self.factory.makeBranch()
+ self.factory.makeSnap(branch=branch)
+ self.factory.makeSnap(branch=branch)
+ self.factory.makeSnap()
+ self.assertEqual(2, branch.getSnaps().count())
+
+ def test_git_repository_implements_hassnaps(self):
+ repository = self.factory.makeGitRepository()
+ self.assertProvides(repository, IHasSnaps)
+
+ def test_git_repository_getSnaps(self):
+ # IGitRepository.getSnaps returns all the Snaps based on that
+ # repository.
+ repository = self.factory.makeGitRepository()
+ [ref] = self.factory.makeGitRefs(repository=repository)
+ self.factory.makeSnap(git_ref=ref)
+ self.factory.makeSnap(git_ref=ref)
+ self.factory.makeSnap()
+ self.assertEqual(2, repository.getSnaps().count())
+
+ def test_git_ref_implements_hassnaps(self):
+ [ref] = self.factory.makeGitRefs()
+ self.assertProvides(ref, IHasSnaps)
+
+ def test_git_ref_getSnaps(self):
+ # IGitRef.getSnaps returns all the Snaps based on that ref.
+ [ref] = self.factory.makeGitRefs()
+ self.factory.makeSnap(git_ref=ref)
+ self.factory.makeSnap(git_ref=ref)
+ self.factory.makeSnap()
+ self.assertEqual(2, ref.getSnaps().count())
+
+ def test_person_implements_hassnaps(self):
+ person = self.factory.makePerson()
+ self.assertProvides(person, IHasSnaps)
+
+ def test_person_getSnaps(self):
+ # IPerson.getSnaps returns all the Snaps owned by that person or
+ # based on branches or repositories owned by that person.
+ person = self.factory.makePerson()
+ self.factory.makeSnap(registrant=person, owner=person)
+ self.factory.makeSnap(branch=self.factory.makeAnyBranch(owner=person))
+ [ref] = self.factory.makeGitRefs(owner=person)
+ self.factory.makeSnap(git_ref=ref)
+ self.factory.makeSnap()
+ self.assertEqual(3, person.getSnaps().count())
+
+ def test_project_implements_hassnaps(self):
+ project = self.factory.makeProduct()
+ self.assertProvides(project, IHasSnaps)
+
+ def test_project_getSnaps(self):
+ # IProduct.getSnaps returns all the Snaps based on that project's
+ # branches or repositories.
+ project = self.factory.makeProduct()
+ self.factory.makeSnap(
+ branch=self.factory.makeProductBranch(product=project))
+ [ref] = self.factory.makeGitRefs(target=project)
+ self.factory.makeSnap(git_ref=ref)
+ self.factory.makeSnap()
+ self.assertEqual(2, project.getSnaps().count())
=== modified file 'lib/lp/snappy/tests/test_snap.py'
--- lib/lp/snappy/tests/test_snap.py 2015-09-09 14:17:46 +0000
+++ lib/lp/snappy/tests/test_snap.py 2015-09-16 13:47:51 +0000
@@ -31,6 +31,7 @@
from lp.services.features.testing import FeatureFixture
from lp.services.webapp.interfaces import OAuthPermission
from lp.snappy.interfaces.snap import (
+ BadSnapSearchContext,
CannotDeleteSnap,
ISnap,
ISnapSet,
@@ -434,18 +435,51 @@
getUtility(ISnapSet).exists(self.factory.makePerson(), snap.name))
self.assertFalse(getUtility(ISnapSet).exists(snap.owner, u"different"))
+ def test_findByOwner(self):
+ # ISnapSet.findByOwner returns all Snaps with the given owner.
+ owners = [self.factory.makePerson() for i in range(2)]
+ snaps = []
+ for owner in owners:
+ for i in range(2):
+ snaps.append(self.factory.makeSnap(
+ registrant=owner, owner=owner))
+ snap_set = getUtility(ISnapSet)
+ self.assertContentEqual(snaps[:2], snap_set.findByOwner(owners[0]))
+ self.assertContentEqual(snaps[2:], snap_set.findByOwner(owners[1]))
+
def test_findByPerson(self):
- # ISnapSet.findByPerson returns all Snaps with the given owner.
+ # ISnapSet.findByPerson returns all Snaps with the given owner or
+ # based on branches or repositories with the given owner.
owners = [self.factory.makePerson() for i in range(2)]
snaps = []
for owner in owners:
- for i in range(2):
- snaps.append(self.factory.makeSnap(
- registrant=owner, owner=owner))
- self.assertContentEqual(
- snaps[:2], getUtility(ISnapSet).findByPerson(owners[0]))
- self.assertContentEqual(
- snaps[2:], getUtility(ISnapSet).findByPerson(owners[1]))
+ snaps.append(self.factory.makeSnap(registrant=owner, owner=owner))
+ snaps.append(self.factory.makeSnap(
+ branch=self.factory.makeAnyBranch(owner=owner)))
+ [ref] = self.factory.makeGitRefs(owner=owner)
+ snaps.append(self.factory.makeSnap(git_ref=ref))
+ snap_set = getUtility(ISnapSet)
+ self.assertContentEqual(snaps[:3], snap_set.findByPerson(owners[0]))
+ self.assertContentEqual(snaps[3:], snap_set.findByPerson(owners[1]))
+
+ def test_findByProject(self):
+ # ISnapSet.findByProject returns all Snaps based on branches or
+ # repositories for the given project.
+ projects = [self.factory.makeProduct() for i in range(2)]
+ snaps = []
+ for project in projects:
+ snaps.append(self.factory.makeSnap(
+ branch=self.factory.makeProductBranch(product=project)))
+ [ref] = self.factory.makeGitRefs(target=project)
+ snaps.append(self.factory.makeSnap(git_ref=ref))
+ snaps.append(self.factory.makeSnap(
+ branch=self.factory.makePersonalBranch()))
+ [ref] = self.factory.makeGitRefs(target=None)
+ snaps.append(self.factory.makeSnap(git_ref=ref))
+ snap_set = getUtility(ISnapSet)
+ self.assertContentEqual(snaps[:2], snap_set.findByProject(projects[0]))
+ self.assertContentEqual(
+ snaps[2:4], snap_set.findByProject(projects[1]))
def test_findByBranch(self):
# ISnapSet.findByBranch returns all Snaps with the given Bazaar branch.
@@ -454,10 +488,9 @@
for branch in branches:
for i in range(2):
snaps.append(self.factory.makeSnap(branch=branch))
- self.assertContentEqual(
- snaps[:2], getUtility(ISnapSet).findByBranch(branches[0]))
- self.assertContentEqual(
- snaps[2:], getUtility(ISnapSet).findByBranch(branches[1]))
+ snap_set = getUtility(ISnapSet)
+ self.assertContentEqual(snaps[:2], snap_set.findByBranch(branches[0]))
+ self.assertContentEqual(snaps[2:], snap_set.findByBranch(branches[1]))
def test_findByGitRepository(self):
# ISnapSet.findByGitRepository returns all Snaps with the given Git
@@ -468,12 +501,55 @@
for i in range(2):
[ref] = self.factory.makeGitRefs(repository=repository)
snaps.append(self.factory.makeSnap(git_ref=ref))
- self.assertContentEqual(
- snaps[:2],
- getUtility(ISnapSet).findByGitRepository(repositories[0]))
- self.assertContentEqual(
- snaps[2:],
- getUtility(ISnapSet).findByGitRepository(repositories[1]))
+ snap_set = getUtility(ISnapSet)
+ self.assertContentEqual(
+ snaps[:2], snap_set.findByGitRepository(repositories[0]))
+ self.assertContentEqual(
+ snaps[2:], snap_set.findByGitRepository(repositories[1]))
+
+ def test_findByGitRef(self):
+ # ISnapSet.findByGitRef returns all Snaps with the given Git
+ # reference.
+ repositories = [self.factory.makeGitRepository() for i in range(2)]
+ refs = []
+ snaps = []
+ for repository in repositories:
+ refs.extend(self.factory.makeGitRefs(
+ paths=[u"refs/heads/master", u"refs/heads/other"]))
+ snaps.append(self.factory.makeSnap(git_ref=refs[-2]))
+ snaps.append(self.factory.makeSnap(git_ref=refs[-1]))
+ snap_set = getUtility(ISnapSet)
+ for ref, snap in zip(refs, snaps):
+ self.assertContentEqual([snap], snap_set.findByGitRef(ref))
+
+ def test_findByContext(self):
+ # ISnapSet.findByContext returns all Snaps with the given context.
+ person = self.factory.makePerson()
+ project = self.factory.makeProduct()
+ branch = self.factory.makeProductBranch(owner=person, product=project)
+ other_branch = self.factory.makeProductBranch()
+ repository = self.factory.makeGitRepository(target=project)
+ refs = self.factory.makeGitRefs(
+ repository=repository,
+ paths=[u"refs/heads/master", u"refs/heads/other"])
+ snaps = []
+ snaps.append(self.factory.makeSnap(branch=branch))
+ snaps.append(self.factory.makeSnap(branch=other_branch))
+ snaps.append(
+ self.factory.makeSnap(
+ registrant=person, owner=person, git_ref=refs[0]))
+ snaps.append(self.factory.makeSnap(git_ref=refs[1]))
+ snap_set = getUtility(ISnapSet)
+ self.assertContentEqual(
+ [snaps[0], snaps[2]], snap_set.findByContext(person))
+ self.assertContentEqual(
+ [snaps[0], snaps[2], snaps[3]], snap_set.findByContext(project))
+ self.assertContentEqual([snaps[0]], snap_set.findByContext(branch))
+ self.assertContentEqual(snaps[2:], snap_set.findByContext(repository))
+ self.assertContentEqual([snaps[2]], snap_set.findByContext(refs[0]))
+ self.assertRaises(
+ BadSnapSearchContext, snap_set.findByContext,
+ self.factory.makeDistribution())
def test_detachFromBranch(self):
# ISnapSet.detachFromBranch clears the given Bazaar branch from all
=== modified file 'lib/lp/soyuz/templates/person-portlet-ppas.pt'
--- lib/lp/soyuz/templates/person-portlet-ppas.pt 2012-11-01 03:41:36 +0000
+++ lib/lp/soyuz/templates/person-portlet-ppas.pt 2015-09-16 13:47:51 +0000
@@ -31,10 +31,13 @@
</ul>
</div>
- <div tal:define="link context/menu:overview/view_recipes"
- tal:condition="link/enabled">
- <a tal:replace="structure link/fmt:link" />
- </div>
+ <ul class="horizontal" style="margin-top: 0;"
+ tal:define="recipes_link context/menu:overview/view_recipes;
+ snaps_link context/menu:overview/view_snaps"
+ tal:condition="python: recipes_link.enabled or snaps_link.enabled">
+ <li><a tal:replace="structure recipes_link/fmt:link" /></li>
+ <li><a tal:replace="structure snaps_link/fmt:link" /></li>
+ </ul>
</tal:root>
Follow ups