launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #25702
[Merge] ~pappacena/launchpad:bug-oci-project-navigation into launchpad:master
Thiago F. Pappacena has proposed merging ~pappacena/launchpad:bug-oci-project-navigation into launchpad:master with ~pappacena/launchpad:bugtask-oci-project as a prerequisite.
Commit message:
Navigation on the UI for OCI project bugs
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~pappacena/launchpad/+git/launchpad/+merge/394181
--
Your team Launchpad code reviewers is requested to review the proposed merge of ~pappacena/launchpad:bug-oci-project-navigation into launchpad:master.
diff --git a/lib/lp/app/widgets/launchpadtarget.py b/lib/lp/app/widgets/launchpadtarget.py
index 89b2a95..b2cc50b 100644
--- a/lib/lp/app/widgets/launchpadtarget.py
+++ b/lib/lp/app/widgets/launchpadtarget.py
@@ -1,4 +1,4 @@
-# Copyright 2009-2016 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2020 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
__metaclass__ = type
@@ -33,6 +33,7 @@ from lp.registry.interfaces.distribution import IDistribution
from lp.registry.interfaces.distributionsourcepackage import (
IDistributionSourcePackage,
)
+from lp.registry.interfaces.ociproject import IOCIProject
from lp.registry.interfaces.product import IProduct
from lp.services.features import getFeatureFlag
from lp.services.webapp.interfaces import (
@@ -43,11 +44,17 @@ from lp.services.webapp.interfaces import (
@implementer(IAlwaysSubmittedWidget, IMultiLineWidgetLayout, IInputWidget)
class LaunchpadTargetWidget(BrowserWidget, InputWidget):
- """Widget for selecting a product, distribution or package target."""
+ """Widget for selecting a product, distribution, package target or OCI
+ project (the last is disabled by default).
+
+ To enable OCIProject as target, subclass LaunchpadTargetWidget and set
+ `show_ociproject` to True.
+ """
template = ViewPageTemplateFile('templates/launchpad-target.pt')
default_option = "package"
_widgets_set_up = False
+ show_ociproject = False
def getDistributionVocabulary(self):
return 'Distribution'
@@ -55,6 +62,9 @@ class LaunchpadTargetWidget(BrowserWidget, InputWidget):
def getProductVocabulary(self):
return 'Product'
+ def getOCIProjectVocabulary(self):
+ return 'OCIProject'
+
def setUpSubWidgets(self):
if self._widgets_set_up:
return
@@ -76,6 +86,10 @@ class LaunchpadTargetWidget(BrowserWidget, InputWidget):
__name__='package', title=u"Package",
required=False, vocabulary=package_vocab),
]
+ if self.show_ociproject:
+ fields.append(Choice(
+ __name__='ociproject', title=u"OCI project",
+ required=False, vocabulary=self.getOCIProjectVocabulary()))
self.distribution_widget = CustomWidgetFactory(
LaunchpadDropdownWidget)
for field in fields:
@@ -86,7 +100,10 @@ class LaunchpadTargetWidget(BrowserWidget, InputWidget):
def setUpOptions(self):
"""Set up options to be rendered."""
self.options = {}
- for option in ['package', 'product']:
+ option_names = ['package', 'product']
+ if self.show_ociproject:
+ option_names.append('ociproject')
+ for option in option_names:
attributes = dict(
type='radio', name=self.name, value=option,
id='%s.option.%s' % (self.name, option))
@@ -131,6 +148,24 @@ class LaunchpadTargetWidget(BrowserWidget, InputWidget):
"There is no project named '%s' registered in"
" Launchpad" % entered_name))
raise self._error
+ if form_value == 'ociproject':
+ try:
+ return self.ociproject_widget.getInputValue()
+ except MissingInputError:
+ self._error = WidgetInputError(
+ self.name, self.label,
+ LaunchpadValidationError(
+ 'Please enter an OCI project name'))
+ raise self._error
+ except ConversionError:
+ entered_name = self.request.form_ng.getOne(
+ "%s.ociproject" % self.name)
+ self._error = WidgetInputError(
+ self.name, self.label,
+ LaunchpadValidationError(
+ "There is no OCI project named '%s' registered in"
+ " Launchpad" % entered_name))
+ raise self._error
elif form_value == 'package':
try:
distribution = self.distribution_widget.getInputValue()
@@ -186,6 +221,9 @@ class LaunchpadTargetWidget(BrowserWidget, InputWidget):
self.default_option = 'package'
self.distribution_widget.setRenderedValue(value.distribution)
self.package_widget.setRenderedValue(value.sourcepackagename)
+ elif IOCIProject.providedBy(value):
+ self.default_option = 'ociproject'
+ self.ociproject_widget.setRenderedValue(value)
else:
raise AssertionError('Not a valid value: %r' % value)
diff --git a/lib/lp/app/widgets/templates/launchpad-target.pt b/lib/lp/app/widgets/templates/launchpad-target.pt
index 3efdb81..0d3b6a3 100644
--- a/lib/lp/app/widgets/templates/launchpad-target.pt
+++ b/lib/lp/app/widgets/templates/launchpad-target.pt
@@ -28,11 +28,24 @@
<label>
<input type="radio" value="product"
tal:replace="structure view/options/product" />
- Project
+ Project
</label>
</td>
<td>
<tal:product tal:replace="structure view/product_widget" />
</td>
</tr>
+
+ <tr>
+ <td>
+ <label>
+ <input type="radio" value="ociproject"
+ tal:replace="structure view/options/ociproject" />
+ OCI project
+ </label>
+ </td>
+ <td>
+ <tal:product tal:replace="structure view/ociproject_widget" />
+ </td>
+ </tr>
</table>
diff --git a/lib/lp/bugs/browser/buglisting.py b/lib/lp/bugs/browser/buglisting.py
index 6acbf8a..82c069a 100644
--- a/lib/lp/bugs/browser/buglisting.py
+++ b/lib/lp/bugs/browser/buglisting.py
@@ -113,6 +113,7 @@ from lp.registry.interfaces.distributionsourcepackage import (
IDistributionSourcePackage,
)
from lp.registry.interfaces.distroseries import IDistroSeries
+from lp.registry.interfaces.ociproject import IOCIProject
from lp.registry.interfaces.person import IPerson
from lp.registry.interfaces.product import IProduct
from lp.registry.interfaces.productseries import IProductSeries
@@ -1109,6 +1110,7 @@ class BugTaskSearchListingView(LaunchpadFormView, FeedsMixin, BugsInfoMixin):
distroseries_context = self._distroSeriesContext()
distrosourcepackage_context = self._distroSourcePackageContext()
sourcepackage_context = self._sourcePackageContext()
+ ociproject_context = self._ociprojectContext()
if (upstream_context or productseries_context or
distrosourcepackage_context or sourcepackage_context):
@@ -1121,6 +1123,9 @@ class BugTaskSearchListingView(LaunchpadFormView, FeedsMixin, BugsInfoMixin):
return [
"id", "summary", "productname", "importance", "status",
"heat"]
+ elif ociproject_context:
+ return [
+ "id", "summary", "ociproject", "importance", "status", "heat"]
else:
raise AssertionError(
"Unrecognized context; don't know which report "
@@ -1528,15 +1533,20 @@ class BugTaskSearchListingView(LaunchpadFormView, FeedsMixin, BugsInfoMixin):
@property
def structural_subscriber_label(self):
- if IDistribution.providedBy(self.context):
+ if IOCIProject.providedBy(self.context):
+ target = self.context.pillar
+ else:
+ target = self.context
+
+ if IDistribution.providedBy(target):
return 'Package or series subscriber'
- elif IDistroSeries.providedBy(self.context):
+ elif IDistroSeries.providedBy(target):
return 'Package subscriber'
- elif IProduct.providedBy(self.context):
+ elif IProduct.providedBy(target):
return 'Series subscriber'
- elif IProjectGroup.providedBy(self.context):
+ elif IProjectGroup.providedBy(target):
return 'Project or series subscriber'
- elif IPerson.providedBy(self.context):
+ elif IPerson.providedBy(target):
return 'Project, distribution, package, or series subscriber'
else:
return None
@@ -1548,7 +1558,12 @@ class BugTaskSearchListingView(LaunchpadFormView, FeedsMixin, BugsInfoMixin):
"""
# It doesn't make sense to show the target name when viewing product
# bugs.
- if IProduct.providedBy(self.context):
+ if IOCIProject.providedBy(self.context):
+ target = self.context.pillar
+ else:
+ target = self.context
+
+ if IProduct.providedBy(target):
return False
else:
return True
@@ -1645,6 +1660,13 @@ class BugTaskSearchListingView(LaunchpadFormView, FeedsMixin, BugsInfoMixin):
"""
return IDistributionSourcePackage(self.context, None)
+ def _ociprojectContext(self):
+ """Is this page being viewed in an OCI project context?
+
+ Return the IOCIProject if yes, otherwise return None.
+ """
+ return IOCIProject(self.context, None)
+
@property
def addquestion_url(self):
"""Return the URL for the +addquestion view for the context."""
diff --git a/lib/lp/bugs/browser/bugtarget.py b/lib/lp/bugs/browser/bugtarget.py
index e25187c..c59aa5f 100644
--- a/lib/lp/bugs/browser/bugtarget.py
+++ b/lib/lp/bugs/browser/bugtarget.py
@@ -1,4 +1,4 @@
-# Copyright 2010-2019 Canonical Ltd. This software is licensed under the
+# Copyright 2010-2020 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""IBugTarget-related browser views."""
@@ -127,6 +127,7 @@ from lp.registry.interfaces.distributionsourcepackage import (
IDistributionSourcePackage,
)
from lp.registry.interfaces.distroseries import IDistroSeries
+from lp.registry.interfaces.ociproject import IOCIProject
from lp.registry.interfaces.person import IPerson
from lp.registry.interfaces.product import IProduct
from lp.registry.interfaces.productseries import IProductSeries
@@ -813,7 +814,8 @@ class FileBugViewBase(LaunchpadFormView):
elif (IDistribution.providedBy(context) or
IProjectGroup.providedBy(context) or
IDistroSeries.providedBy(context) or
- ISourcePackage.providedBy(context)):
+ ISourcePackage.providedBy(context) or
+ IOCIProject.providedBy(context)):
pass
else:
raise TypeError("Unexpected bug target: %r" % context)
diff --git a/lib/lp/bugs/browser/bugtask.py b/lib/lp/bugs/browser/bugtask.py
index 219c9e6..18ccef5 100644
--- a/lib/lp/bugs/browser/bugtask.py
+++ b/lib/lp/bugs/browser/bugtask.py
@@ -154,6 +154,7 @@ from lp.registry.interfaces.distroseries import (
IDistroSeries,
IDistroSeriesSet,
)
+from lp.registry.interfaces.ociproject import IOCIProject
from lp.registry.interfaces.person import IPersonSet
from lp.registry.interfaces.product import IProduct
from lp.registry.interfaces.productseries import IProductSeries
@@ -1655,26 +1656,35 @@ def bugtask_sort_key(bugtask):
Designed to make sense when bugtargetdisplayname is shown.
"""
if IDistribution.providedBy(bugtask.target):
- key = (None, bugtask.target.displayname, None, None, None)
+ key = (None, bugtask.target.displayname, None, None, None, None)
elif IDistroSeries.providedBy(bugtask.target):
key = (
None, bugtask.target.distribution.displayname,
- bugtask.target.name, None, None)
+ bugtask.target.name, None, None, None)
elif IDistributionSourcePackage.providedBy(bugtask.target):
key = (
bugtask.target.sourcepackagename.name,
- bugtask.target.distribution.displayname, None, None, None)
+ bugtask.target.distribution.displayname, None, None, None, None)
elif ISourcePackage.providedBy(bugtask.target):
key = (
bugtask.target.sourcepackagename.name,
bugtask.target.distribution.displayname,
- Version(bugtask.target.distroseries.version), None, None)
+ Version(bugtask.target.distroseries.version), None, None, None)
elif IProduct.providedBy(bugtask.target):
- key = (None, None, None, bugtask.target.displayname, None)
+ key = (None, None, None, bugtask.target.displayname, None, None)
elif IProductSeries.providedBy(bugtask.target):
key = (
None, None, None, bugtask.target.product.displayname,
- bugtask.target.name)
+ bugtask.target.name, None)
+ elif IOCIProject.providedBy(bugtask.target):
+ ociproject = bugtask.target
+ pillar = ociproject.pillar
+ key = [None, None, None, None, None, ociproject.displayname]
+ if IDistribution.providedBy(pillar):
+ key[1] = pillar.displayname
+ elif IProduct.providedBy(pillar):
+ key[3] = pillar.displayname
+ key = tuple(key)
else:
raise AssertionError("No sort key for %r" % bugtask.target)
@@ -1993,6 +2003,8 @@ class BugTasksTableView(LaunchpadView):
# fake one.
if ISeriesBugTarget.providedBy(bugtask.target):
parent = bugtask.target.bugtarget_parent
+ elif IOCIProject.providedBy(bugtask.target):
+ latest_parent = parent = bugtask.target.pillar
else:
latest_parent = parent = bugtask.target
diff --git a/lib/lp/bugs/browser/tests/test_bugtarget_filebug.py b/lib/lp/bugs/browser/tests/test_bugtarget_filebug.py
index d3efbe2..5fdcdf1 100644
--- a/lib/lp/bugs/browser/tests/test_bugtarget_filebug.py
+++ b/lib/lp/bugs/browser/tests/test_bugtarget_filebug.py
@@ -1,4 +1,4 @@
-# Copyright 2010-2019 Canonical Ltd. This software is licensed under the
+# Copyright 2010-2020 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
from __future__ import absolute_import, print_function, unicode_literals
@@ -36,6 +36,8 @@ from lp.bugs.interfaces.bugtask import (
BugTaskStatus,
)
from lp.registry.enums import BugSharingPolicy
+from lp.registry.interfaces.ociproject import IOCIProject
+from lp.registry.interfaces.product import IProduct
from lp.registry.interfaces.projectgroup import IProjectGroup
from lp.services.beautifulsoup import BeautifulSoup
from lp.services.features.testing import FeatureFixture
@@ -375,20 +377,25 @@ class TestFileBugViewBase(FileBugViewMixin, TestCaseWithFactory):
}]
self.assertEqual(expected_guidelines, view.bug_reporting_guidelines)
- def filebug_via_view(self, information_type=None,
- bug_sharing_policy=None, extra_data_token=None):
+ def filebug_via_view(self, information_type=None, bug_sharing_policy=None,
+ extra_data_token=None, target=None):
+ if target is None:
+ target = self.factory.makeProduct(official_malone=True)
+ owner = (target.owner if not IOCIProject.providedBy(target)
+ else target.registrant)
form = self.get_form()
if information_type:
form['field.information_type'] = information_type
- product = self.factory.makeProduct(official_malone=True)
if bug_sharing_policy:
- self.factory.makeCommercialSubscription(product=product)
- with person_logged_in(product.owner):
- product.setBugSharingPolicy(bug_sharing_policy)
- with person_logged_in(product.owner):
+ if not IProduct.providedBy(target):
+ raise ValueError("Only Product supports this.")
+ self.factory.makeCommercialSubscription(product=target)
+ with person_logged_in(owner):
+ target.setBugSharingPolicy(bug_sharing_policy)
+ with person_logged_in(owner):
view = create_view(
- product, '+filebug', method='POST', form=form,
- principal=product.owner)
+ target, '+filebug', method='POST', form=form,
+ principal=owner)
if extra_data_token is not None:
view = view.publishTraverse(view.request, extra_data_token)
view.initialize()
@@ -396,6 +403,29 @@ class TestFileBugViewBase(FileBugViewMixin, TestCaseWithFactory):
bug_number = bug_url.split('/')[-1]
return (getUtility(IBugSet).getByNameOrID(bug_number), view)
+ def assertFilesBugForTarget(self, target):
+ bug, view = self.filebug_via_view(target=target)
+ self.assertEqual(
+ InformationType.PUBLIC, view.default_information_type)
+ self.assertEqual([target], [i.target for i in bug.bugtasks])
+ self.assertEqual(InformationType.PUBLIC, bug.information_type)
+
+ def test_filebug_distribution(self):
+ # We should be able to open bugs for distributions.
+ self.assertFilesBugForTarget(self.factory.makeDistribution())
+
+ def test_filebug_ociproject_of_distribution(self):
+ # We should be able to open bugs for oci project of a distribution.
+ distro = self.factory.makeDistribution()
+ self.assertFilesBugForTarget(
+ self.factory.makeOCIProject(pillar=distro))
+
+ def test_filebug_ociproject_of_project(self):
+ # We should be able to open bugs for oci project of a project.
+ product = self.factory.makeProduct()
+ self.assertFilesBugForTarget(
+ self.factory.makeOCIProject(pillar=product))
+
def test_filebug_default_information_type(self):
# If we don't specify the bug's information_type, it is PUBLIC.
bug, view = self.filebug_via_view()
diff --git a/lib/lp/bugs/browser/tests/test_bugtarget_tags.py b/lib/lp/bugs/browser/tests/test_bugtarget_tags.py
index 204861a..311ae8a 100644
--- a/lib/lp/bugs/browser/tests/test_bugtarget_tags.py
+++ b/lib/lp/bugs/browser/tests/test_bugtarget_tags.py
@@ -1,34 +1,67 @@
-# Copyright 2010-2011 Canonical Ltd. This software is licensed under the
+# Copyright 2010-2020 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
__metaclass__ = type
+from testscenarios.testcase import WithScenarios
+
from lp.testing import TestCaseWithFactory
from lp.testing.layers import DatabaseFunctionalLayer
from lp.testing.views import create_view
-class TestBugTargetTags(TestCaseWithFactory):
+class TestBugTargetTags(WithScenarios, TestCaseWithFactory):
layer = DatabaseFunctionalLayer
+ scenarios = [
+ ("product + group", {"factory_name": 'makeProductAndGroup'}),
+ ("product", {"factory_name": 'makeProduct'}),
+ ("distribution", {"factory_name": 'makeDistribution'}),
+ ("ociproject of product", {
+ "factory_name": 'makeOCIProjectFromProduct'}),
+ ("ociproject of distro", {"factory_name": 'makeOCIProjectFromDistro'}),
+ ]
+
+ def makeProductAndGroup(self):
+ project_group = self.factory.makeProject()
+ product = self.factory.makeProduct(projectgroup=project_group)
+ return project_group, product
+
+ def makeProduct(self):
+ prod = self.factory.makeProduct()
+ return prod, prod
+
+ def makeDistribution(self):
+ distro = self.factory.makeDistribution()
+ return distro, distro
+
+ def makeOCIProjectFromProduct(self):
+ target = self.factory.makeProduct()
+ ociproject = self.factory.makeOCIProject(pillar=target)
+ return ociproject, ociproject
+
+ def makeOCIProjectFromDistro(self):
+ target = self.factory.makeDistribution()
+ ociproject = self.factory.makeOCIProject(pillar=target)
+ return ociproject, ociproject
+
def setUp(self):
super(TestBugTargetTags, self).setUp()
- self.project_group = self.factory.makeProject()
- self.target_product = self.factory.makeProduct(
- projectgroup=self.project_group)
+ builder = getattr(self, self.factory_name)
+ self.view_context, self.bug_target = builder()
def test_no_tags(self):
- self.factory.makeBug(target=self.target_product)
+ self.factory.makeBug(target=self.bug_target)
view = create_view(
- self.project_group,
+ self.view_context,
name="+bugtarget-portlet-tags-content")
self.assertEqual([], [tag['tag'] for tag in view.tags_cloud_data])
def test_tags(self):
- self.factory.makeBug(target=self.target_product, tags=[u'foo'])
+ self.factory.makeBug(target=self.bug_target, tags=[u'foo'])
view = create_view(
- self.project_group,
+ self.view_context,
name="+bugtarget-portlet-tags-content")
self.assertEqual(
[u'foo'],
@@ -36,15 +69,15 @@ class TestBugTargetTags(TestCaseWithFactory):
def test_tags_order(self):
"""Test that the tags are ordered by most used first"""
- self.factory.makeBug(target=self.target_product, tags=[u'tag-last'])
+ self.factory.makeBug(target=self.bug_target, tags=[u'tag-last'])
for counter in range(0, 2):
self.factory.makeBug(
- target=self.target_product, tags=[u'tag-middle'])
+ target=self.bug_target, tags=[u'tag-middle'])
for counter in range(0, 3):
self.factory.makeBug(
- target=self.target_product, tags=[u'tag-first'])
+ target=self.bug_target, tags=[u'tag-first'])
view = create_view(
- self.project_group,
+ self.view_context,
name="+bugtarget-portlet-tags-content")
self.assertEqual(
[u'tag-first', u'tag-middle', u'tag-last'],
diff --git a/lib/lp/bugs/browser/tests/test_bugtask.py b/lib/lp/bugs/browser/tests/test_bugtask.py
index cb98e74..8e659d3 100644
--- a/lib/lp/bugs/browser/tests/test_bugtask.py
+++ b/lib/lp/bugs/browser/tests/test_bugtask.py
@@ -657,6 +657,13 @@ class TestBugTasksTableView(TestCaseWithFactory):
self.view.initialize()
self.assertIs(None, self.view.getTargetLinkTitle(bug_task.target))
+ def test_getTargetLinkTitle_ociproject(self):
+ # The target link title is always none for ociprojects.
+ target = self.factory.makeOCIProject()
+ bug_task = self.factory.makeBugTask(bug=self.bug, target=target)
+ self.view.initialize()
+ self.assertIs(None, self.view.getTargetLinkTitle(bug_task.target))
+
def test_getTargetLinkTitle_unpublished_distributionsourcepackage(self):
# The target link title states that the package is not published
# in the current release.
@@ -782,6 +789,7 @@ class TestBugTasksTableView(TestCaseWithFactory):
# Product tasks come first, sorted by product then series.
# Distro tasks follow, sorted by package, distribution, then
# series (by version in the case of distribution series).
+ # OCI projects comes after their pillars.
foo = self.factory.makeProduct(displayname='Foo')
self.factory.makeProductSeries(product=foo, name='2.0')
self.factory.makeProductSeries(product=foo, name='1.0')
@@ -801,9 +809,14 @@ class TestBugTasksTableView(TestCaseWithFactory):
foo_spn = self.factory.makeSourcePackageName('foo')
bar_spn = self.factory.makeSourcePackageName('bar')
+ foo_ociproject = self.factory.makeOCIProject(pillar=foo)
+ barix_ociproject = self.factory.makeOCIProject(pillar=barix)
+
expected_targets = [
bar, bar.getSeries('0.0'),
- foo, foo.getSeries('1.0'), foo.getSeries('2.0'),
+ foo, foo_ociproject,
+ foo.getSeries('1.0'), foo.getSeries('2.0'),
+ barix_ociproject,
barix.getSourcePackage(bar_spn),
barix.getSeries('beta').getSourcePackage(bar_spn),
barix.getSeries('aaa-release').getSourcePackage(bar_spn),
@@ -1564,6 +1577,37 @@ class TestDistributionBugs(TestCaseWithFactory, BugTaskViewTestMixin):
self._assert_shouldShowStructuralSubscriberWidget()
+class TestOCIProjectOfProductBugs(TestCaseWithFactory, BugTaskViewTestMixin):
+ """Test the bugs overview page for OCI project based on project."""
+
+ layer = DatabaseFunctionalLayer
+
+ def setUp(self):
+ super(TestOCIProjectOfProductBugs, self).setUp()
+ self.initTarget()
+
+ def initTarget(self):
+ product = self.factory.makeProduct()
+ self.target = self.factory.makeOCIProject(pillar=product)
+ self.structural_label_subscriber = 'Series subscriber'
+
+ def test_structural_subscriber_label(self):
+ self._assert_structural_subscriber_label(
+ self.structural_label_subscriber)
+
+ def test_shouldShowStructuralSubscriberWidget(self):
+ self._assert_shouldShowStructuralSubscriberWidget()
+
+
+class TestOCIProjectOfDistributionBugs(TestOCIProjectOfProductBugs):
+ """Test the bugs overview page for OCI project based on distribution."""
+
+ def initTarget(self):
+ distro = self.factory.makeDistribution()
+ self.target = self.factory.makeOCIProject(pillar=distro)
+ self.structural_label_subscriber = 'Package or series subscriber'
+
+
class TestDistroSeriesBugs(TestCaseWithFactory, BugTaskViewTestMixin):
"""Test the bugs overview page for distro series."""
diff --git a/lib/lp/bugs/browser/widgets/bugtask.py b/lib/lp/bugs/browser/widgets/bugtask.py
index 5169abc..f480b0b 100644
--- a/lib/lp/bugs/browser/widgets/bugtask.py
+++ b/lib/lp/bugs/browser/widgets/bugtask.py
@@ -1,4 +1,4 @@
-# Copyright 2009-2016 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2020 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Widgets related to IBugTask."""
@@ -468,6 +468,7 @@ class BugTaskBugWatchWidget(RadioWidget):
class BugTaskTargetWidget(LaunchpadTargetWidget):
+ show_ociproject = True
def getDistributionVocabulary(self):
distro = self.context.context.distribution
diff --git a/lib/lp/bugs/configure.zcml b/lib/lp/bugs/configure.zcml
index 3e4544b..8225706 100644
--- a/lib/lp/bugs/configure.zcml
+++ b/lib/lp/bugs/configure.zcml
@@ -1018,6 +1018,9 @@
factory=".model.structuralsubscription.DistroSeriesTargetHelper"
permission="zope.Public"/>
<adapter
+ factory=".model.structuralsubscription.OCIProjectTargetHelper"
+ permission="zope.Public"/>
+ <adapter
factory=".model.structuralsubscription.ProjectGroupTargetHelper"
permission="zope.Public"/>
<adapter
diff --git a/lib/lp/bugs/interfaces/bugtasksearch.py b/lib/lp/bugs/interfaces/bugtasksearch.py
index 736c5e1..37b3b5b 100644
--- a/lib/lp/bugs/interfaces/bugtasksearch.py
+++ b/lib/lp/bugs/interfaces/bugtasksearch.py
@@ -1,4 +1,4 @@
-# Copyright 2009-2016 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2020 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Interfaces for searching bug tasks. Mostly used with IBugTaskSet."""
@@ -172,7 +172,7 @@ class BugTaskSearchParams:
created_since=None, exclude_conjoined_tasks=False, cve=None,
upstream_target=None, milestone_dateexpected_before=None,
milestone_dateexpected_after=None, created_before=None,
- information_type=None, ignore_privacy=False):
+ information_type=None, ignore_privacy=False, ociproject=None):
self.bug = bug
self.searchtext = searchtext
@@ -223,6 +223,7 @@ class BugTaskSearchParams:
else:
self.information_type = None
self.ignore_privacy = ignore_privacy
+ self.ociproject = ociproject
def setProduct(self, product):
"""Set the upstream context on which to filter the search."""
@@ -281,6 +282,10 @@ class BugTaskSearchParams:
self.distribution = sourcepackage.distribution
self.sourcepackagename = sourcepackage.sourcepackagename
+ def setOCIProject(self, ociproject):
+ """Set the distribution context on which to filter the search."""
+ self.ociproject = ociproject
+
def setTarget(self, target):
"""Constrain the search to only return items in target.
@@ -303,6 +308,7 @@ class BugTaskSearchParams:
from lp.registry.interfaces.sourcepackage import ISourcePackage
from lp.registry.interfaces.distributionsourcepackage import \
IDistributionSourcePackage
+ from lp.registry.interfaces.ociproject import IOCIProject
if isinstance(target, (any, all)):
assert len(target.query_values), \
'cannot determine target with no targets'
@@ -325,6 +331,8 @@ class BugTaskSearchParams:
self.setSourcePackage(target)
elif IProjectGroup.providedBy(instance):
self.setProjectGroup(target)
+ elif IOCIProject.providedBy(instance):
+ self.setOCIProject(instance)
else:
raise AssertionError("unknown target type %r" % target)
@@ -362,7 +370,8 @@ class BugTaskSearchParams:
linked_merge_proposals=None, linked_blueprints=None,
structural_subscriber=None,
modified_since=None, created_since=None,
- created_before=None, information_type=None):
+ created_before=None, information_type=None,
+ ociproject=None):
"""Create and return a new instance using the parameter list."""
search_params = cls(user=user, orderby=order_by)
@@ -377,6 +386,7 @@ class BugTaskSearchParams:
search_params.owner = owner
search_params.affected_user = affected_user
search_params.distribution = distribution
+ search_params.ociproject = ociproject
if has_patch:
# Import this here to avoid circular imports
from lp.bugs.interfaces.bugattachment import (
diff --git a/lib/lp/bugs/model/bugtask.py b/lib/lp/bugs/model/bugtask.py
index 156b721..62feb1a 100644
--- a/lib/lp/bugs/model/bugtask.py
+++ b/lib/lp/bugs/model/bugtask.py
@@ -157,6 +157,7 @@ def bugtask_sort_key(bugtask):
distribution_name = ''
distroseries_name = ''
sourcepackage_name = ''
+ ociproject = ''
if bugtask.product:
product_name = bugtask.product.name
@@ -180,7 +181,7 @@ def bugtask_sort_key(bugtask):
return (
bugtask.bug.id, distribution_name, product_name, productseries_name,
- distroseries_name, sourcepackage_name)
+ distroseries_name, sourcepackage_name, ociproject)
def bug_target_from_key(product, productseries, distribution, distroseries,
diff --git a/lib/lp/bugs/model/bugtasksearch.py b/lib/lp/bugs/model/bugtasksearch.py
index 9ceb517..631c92e 100644
--- a/lib/lp/bugs/model/bugtasksearch.py
+++ b/lib/lp/bugs/model/bugtasksearch.py
@@ -321,6 +321,7 @@ def _build_query(params):
BugTaskFlat.importance: params.importance,
BugTaskFlat.product: params.product,
BugTaskFlat.distribution: params.distribution,
+ BugTaskFlat.ociproject: params.ociproject,
BugTaskFlat.distroseries: params.distroseries,
BugTaskFlat.productseries: params.productseries,
BugTaskFlat.assignee: params.assignee,
diff --git a/lib/lp/bugs/model/structuralsubscription.py b/lib/lp/bugs/model/structuralsubscription.py
index c2834e3..814d165 100644
--- a/lib/lp/bugs/model/structuralsubscription.py
+++ b/lib/lp/bugs/model/structuralsubscription.py
@@ -1,4 +1,4 @@
-# Copyright 2009-2013 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2020 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
__metaclass__ = type
@@ -71,6 +71,7 @@ from lp.registry.interfaces.distributionsourcepackage import (
)
from lp.registry.interfaces.distroseries import IDistroSeries
from lp.registry.interfaces.milestone import IMilestone
+from lp.registry.interfaces.ociproject import IOCIProject
from lp.registry.interfaces.person import (
validate_person,
validate_public_person,
@@ -202,6 +203,27 @@ class DistroSeriesTargetHelper:
@implementer(IStructuralSubscriptionTargetHelper)
+@adapter(IOCIProject)
+class OCIProjectTargetHelper:
+ """A helper for `OCIProject`s."""
+ target_type_display = 'OCI project'
+
+ def __init__(self, target):
+ self.target = target.pillar
+ self.target_parent = target.pillar
+ self.pillar = target.pillar
+ if IDistribution.providedBy(target.pillar):
+ self.target_arguments = {"distribution": target}
+ self.join = (StructuralSubscription.distribution == target.pillar)
+ elif IProduct.providedBy(target.pillar):
+ self.target_arguments = {"product": target}
+ self.join = (StructuralSubscription.product == target.pillar)
+ else:
+ raise AttributeError(
+ "Invalid pillar for OCIProject subscription: " % target.pillar)
+
+
+@implementer(IStructuralSubscriptionTargetHelper)
@adapter(IProjectGroup)
class ProjectGroupTargetHelper:
"""A helper for `IProjectGroup`s."""
diff --git a/lib/lp/bugs/stories/bugtask-management/xx-bugtask-edit-forms.txt b/lib/lp/bugs/stories/bugtask-management/xx-bugtask-edit-forms.txt
index 8ba4163..f4abae5 100644
--- a/lib/lp/bugs/stories/bugtask-management/xx-bugtask-edit-forms.txt
+++ b/lib/lp/bugs/stories/bugtask-management/xx-bugtask-edit-forms.txt
@@ -49,6 +49,7 @@ respective bug task.
Distribution
...
Project (Find…)
+ OCI project (Find...)
Status Importance Milestone
New... Low... (nothing selected)...
Assigned to... Mark Shuttleworth (mark)
diff --git a/lib/lp/registry/browser/ociproject.py b/lib/lp/registry/browser/ociproject.py
index 0d23a15..0fd322b 100644
--- a/lib/lp/registry/browser/ociproject.py
+++ b/lib/lp/registry/browser/ociproject.py
@@ -26,6 +26,8 @@ from lp.app.browser.launchpadform import (
)
from lp.app.browser.tales import CustomizableFormatter
from lp.app.errors import NotFoundError
+from lp.app.interfaces.headings import IHeadingBreadcrumb
+from lp.bugs.browser.bugtask import BugTargetTraversalMixin
from lp.code.browser.vcslisting import TargetDefaultVCSNavigationMixin
from lp.oci.interfaces.ocirecipe import IOCIRecipeSet
from lp.registry.interfaces.distribution import IDistribution
@@ -115,7 +117,8 @@ class OCIProjectFormatterAPI(CustomizableFormatter):
return {'displayname': displayname}
-class OCIProjectNavigation(TargetDefaultVCSNavigationMixin, Navigation):
+class OCIProjectNavigation(TargetDefaultVCSNavigationMixin,
+ BugTargetTraversalMixin, Navigation):
usedfor = IOCIProject
@@ -127,7 +130,7 @@ class OCIProjectNavigation(TargetDefaultVCSNavigationMixin, Navigation):
return series
-@implementer(IMultiFacetedBreadcrumb)
+@implementer(IMultiFacetedBreadcrumb, IHeadingBreadcrumb)
class OCIProjectBreadcrumb(Breadcrumb):
"""Builds a breadcrumb for an `IOCIProject`."""
@@ -142,8 +145,23 @@ class OCIProjectFacets(StandardLaunchpadFacets):
enable_only = [
'overview',
'branches',
+ 'bugs',
]
+ def makeLink(self, text, context, view_name, site):
+ site = 'mainsite' if self.mainsite_only else site
+ target = canonical_url(context, view_name=view_name, rootsite=site)
+ return Link(target, text, site=site)
+
+ def branches(self):
+ return self.makeLink('Code', self.context.pillar, '+branches', 'code')
+
+ def bugs(self):
+ """Override bugs link to show the OCIProject's bug page, instead of
+ the pillar's bug page.
+ """
+ return self.makeLink('Bugs', self.context, '+bugs', 'bugs')
+
class OCIProjectNavigationMenu(NavigationMenu):
"""Navigation menu for OCI projects."""
diff --git a/lib/lp/registry/browser/tests/test_ociproject.py b/lib/lp/registry/browser/tests/test_ociproject.py
index 9fb5822..4ec4630 100644
--- a/lib/lp/registry/browser/tests/test_ociproject.py
+++ b/lib/lp/registry/browser/tests/test_ociproject.py
@@ -12,6 +12,7 @@ __all__ = []
from datetime import datetime
import pytz
+import soupmatchers
from zope.security.proxy import removeSecurityProxy
from lp.oci.interfaces.ocirecipe import OCI_RECIPE_ALLOW_CREATE
@@ -83,6 +84,43 @@ class TestOCIProjectView(BrowserTestCase):
layer = DatabaseFunctionalLayer
+ def test_facet_top_links(self):
+ oci_project = self.factory.makeOCIProject()
+ browser = self.getViewBrowser(oci_project)
+ menu = soupmatchers.Tag(
+ "facetmenu", "ul", attrs={"class": "facetmenu"})
+
+ with admin_logged_in():
+ # Expected links with (title, link, "<li> element's css classes")
+ expected_links = [
+ ("Overview", None, "overview active"),
+ ("Code", canonical_url(
+ oci_project.pillar,
+ view_name="+branches",
+ rootsite="code"), None),
+ ("Bugs", canonical_url(
+ oci_project, view_name="+bugs", rootsite="bugs"), None),
+ ("Blueprints", None, "specifications disabled-tab"),
+ ("Translations", None, "translations disabled-tab"),
+ ("Answers", None, "answers disabled-tab"),
+ ]
+
+ tags = []
+ for text, link, css_classes in expected_links:
+ if link:
+ tags.append(soupmatchers.Tag(
+ text, "a", text=text, attrs={"href": link}))
+ else:
+ tags.append(soupmatchers.Tag(
+ text, "li",
+ text=text,
+ attrs={"class": css_classes}))
+
+ self.assertThat(
+ browser.contents,
+ soupmatchers.HTMLContains(*[
+ soupmatchers.Within(menu, tag) for tag in tags]))
+
def test_index_distribution_pillar(self):
distribution = self.factory.makeDistribution(displayname="My Distro")
oci_project = self.factory.makeOCIProject(
diff --git a/lib/lp/registry/interfaces/ociproject.py b/lib/lp/registry/interfaces/ociproject.py
index a189654..36c8cc5 100644
--- a/lib/lp/registry/interfaces/ociproject.py
+++ b/lib/lp/registry/interfaces/ociproject.py
@@ -44,12 +44,17 @@ from zope.schema import (
from zope.security.interfaces import Unauthorized
from lp import _
+from lp.app.interfaces.launchpad import IServiceUsage
from lp.app.validators.name import name_validator
from lp.app.validators.path import path_does_not_escape
from lp.bugs.interfaces.bugsupervisor import IHasBugSupervisor
from lp.bugs.interfaces.bugtarget import (
IBugTarget,
IHasExpirableBugs,
+ IHasOfficialBugTags,
+ )
+from lp.bugs.interfaces.structuralsubscription import (
+ IStructuralSubscriptionTarget,
)
from lp.code.interfaces.gitref import IGitRef
from lp.code.interfaces.hasgitrepositories import IHasGitRepositories
@@ -67,7 +72,9 @@ from lp.services.fields import (
OCI_PROJECT_ALLOW_CREATE = 'oci.project.create.enabled'
-class IOCIProjectView(IHasGitRepositories, Interface):
+class IOCIProjectView(IHasGitRepositories, IServiceUsage,
+ IHasOfficialBugTags, IStructuralSubscriptionTarget,
+ Interface):
"""IOCIProject attributes that require launchpad.View permission."""
id = Int(title=_("ID"), required=True, readonly=True)
@@ -96,6 +103,11 @@ class IOCIProjectView(IHasGitRepositories, Interface):
bug_supervisor = Attribute(_("The bug supervisor for this OCI Project."))
+ def getAllowedBugInformationTypes():
+ """Get which InformationTypes are allowed for bugs."""
+
+ title = Attribute(_("A title for this OCI project."))
+
def getSeriesByName(name):
"""Get an OCIProjectSeries for this OCIProject by series' name."""
diff --git a/lib/lp/registry/model/ociproject.py b/lib/lp/registry/model/ociproject.py
index 4dab589..fdc30ab 100644
--- a/lib/lp/registry/model/ociproject.py
+++ b/lib/lp/registry/model/ociproject.py
@@ -13,7 +13,6 @@ __all__ = [
import pytz
import six
-from lp.services.database.stormexpr import fti_search
from six import text_type
from storm.expr import (
Join,
@@ -31,7 +30,14 @@ from zope.component import getUtility
from zope.interface import implementer
from zope.security.proxy import removeSecurityProxy
+from lp.app.enums import (
+ FREE_INFORMATION_TYPES,
+ ServiceUsage,
+ )
from lp.bugs.model.bugtarget import BugTargetBase
+from lp.bugs.model.structuralsubscription import (
+ StructuralSubscriptionTargetMixin,
+ )
from lp.oci.interfaces.ocirecipe import IOCIRecipeSet
from lp.registry.interfaces.distribution import IDistribution
from lp.registry.interfaces.ociproject import (
@@ -55,6 +61,7 @@ from lp.services.database.interfaces import (
IStore,
)
from lp.services.database.stormbase import StormBase
+from lp.services.database.stormexpr import fti_search
def oci_project_modified(oci_project, event):
@@ -69,7 +76,7 @@ def oci_project_modified(oci_project, event):
@implementer(IOCIProject)
-class OCIProject(BugTargetBase, StormBase):
+class OCIProject(BugTargetBase, StructuralSubscriptionTargetMixin, StormBase):
"""See `IOCIProject` and `IOCIProjectSet`."""
__storm_table__ = "OCIProject"
@@ -99,6 +106,13 @@ class OCIProject(BugTargetBase, StormBase):
enable_bugfiling_duplicate_search = Bool(
name="enable_bugfiling_duplicate_search")
+ answers_usage = ServiceUsage.NOT_APPLICABLE
+ blueprints_usage = ServiceUsage.NOT_APPLICABLE
+ codehosting_usage = ServiceUsage.NOT_APPLICABLE
+ translations_usage = ServiceUsage.NOT_APPLICABLE
+ bug_tracking_usage = ServiceUsage.LAUNCHPAD
+ uses_launchpad = True
+
@property
def name(self):
return self.ociprojectname.name
@@ -132,9 +146,17 @@ class OCIProject(BugTargetBase, StormBase):
return "OCI project %s for %s" % (
self.ociprojectname.name, self.pillar.display_name)
- displayname = display_name
- bugtargetname = display_name
- bugtargetdisplayname = display_name
+ @property
+ def displayname(self):
+ return "%s (%s)" % (self.name, self.pillar.display_name)
+
+ bugtargetname = displayname
+ bugtargetdisplayname = displayname
+ title = displayname
+
+ def _customizeSearchParams(self, search_params):
+ """Customize `search_params` for this OCI project."""
+ search_params.setOCIProject(self)
@property
def driver(self):
@@ -146,6 +168,23 @@ class OCIProject(BugTargetBase, StormBase):
"""See `IOCIProject`."""
return self.pillar.bug_supervisor
+ def getAllowedBugInformationTypes(self):
+ """See `IDistribution.`"""
+ return FREE_INFORMATION_TYPES
+
+ def getBugSummaryContextWhereClause(self):
+ """See BugTargetBase."""
+ # Circular fail.
+ from lp.bugs.model.bugsummary import BugSummary
+ return BugSummary.ociproject_id == self.id
+
+ def _getOfficialTagClause(self):
+ return self.pillar._getOfficialTagClause()
+
+ @property
+ def official_bug_tags(self):
+ return self.pillar.official_bug_tags
+
def newRecipe(self, name, registrant, owner, git_ref,
build_file, description=None, build_daily=False,
require_virtualized=True, build_args=None):
Follow ups
-
[Merge] ~pappacena/launchpad:bug-oci-project-navigation into launchpad:master
From: Otto Co-Pilot, 2021-04-29
-
[Merge] ~pappacena/launchpad:bug-oci-project-navigation into launchpad:master
From: Thiago F. Pappacena, 2021-04-29
-
[Merge] ~pappacena/launchpad:bug-oci-project-navigation into launchpad:master
From: Otto Co-Pilot, 2021-04-29
-
Re: [Merge] ~pappacena/launchpad/+git/launchpad:bug-oci-project-navigation into ~launchpad/launchpad/+git/launchpad:master
From: Otto Co-Pilot, 2021-04-29
-
[Merge] ~pappacena/launchpad:bug-oci-project-navigation into launchpad:master
From: Thiago F. Pappacena, 2021-04-29
-
Re: [Merge] ~pappacena/launchpad:bug-oci-project-navigation into launchpad:master
From: Thiago F. Pappacena, 2021-04-26
-
Re: [Merge] ~pappacena/launchpad:bug-oci-project-navigation into launchpad:master
From: Colin Watson, 2021-04-26
-
Re: [Merge] ~pappacena/launchpad:bug-oci-project-navigation into launchpad:master
From: Colin Watson, 2021-04-26
-
Re: [Merge] ~pappacena/launchpad/+git/launchpad:bug-oci-project-navigation into ~launchpad/launchpad/+git/launchpad:master
From: Otto Co-Pilot, 2021-04-26
-
[Merge] ~pappacena/launchpad:bug-oci-project-navigation into launchpad:master
From: Thiago F. Pappacena, 2021-04-26
-
[Merge] ~pappacena/launchpad:bug-oci-project-navigation into launchpad:master
From: Thiago F. Pappacena, 2021-04-26
-
Re: [Merge] ~pappacena/launchpad:bug-oci-project-navigation into launchpad:master
From: Thiago F. Pappacena, 2021-04-26
-
Re: [Merge] ~pappacena/launchpad:bug-oci-project-navigation into launchpad:master
From: Colin Watson, 2021-04-23
-
Re: [Merge] ~pappacena/launchpad:bug-oci-project-navigation into launchpad:master
From: Thiago F. Pappacena, 2021-04-15
-
Re: [Merge] ~pappacena/launchpad:bug-oci-project-navigation into launchpad:master
From: Colin Watson, 2021-04-12
-
Re: [Merge] ~pappacena/launchpad:bug-oci-project-navigation into launchpad:master
From: Thiago F. Pappacena, 2020-11-20
-
Re: [Merge] ~pappacena/launchpad:bug-oci-project-navigation into launchpad:master
From: Thiago F. Pappacena, 2020-11-19