launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #24489
[Merge] ~pappacena/launchpad:oci-project-api into launchpad:master
Thiago F. Pappacena has proposed merging ~pappacena/launchpad:oci-project-api into launchpad:master.
Commit message:
API for managing OCI Project
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~pappacena/launchpad/+git/launchpad/+merge/380909
This is still a work in progress, but I would like to open for discussions.
One thing that we should need to think about is: should we expose this API immediately, or shall we keep these endpoints hidden in production? Should we use a feature flag? Allow only for certain users?
--
Your team Launchpad code reviewers is requested to review the proposed merge of ~pappacena/launchpad:oci-project-api into launchpad:master.
diff --git a/lib/lp/_schema_circular_imports.py b/lib/lp/_schema_circular_imports.py
index 70c5b72..46247fc 100644
--- a/lib/lp/_schema_circular_imports.py
+++ b/lib/lp/_schema_circular_imports.py
@@ -120,6 +120,8 @@ from lp.registry.interfaces.milestone import (
IHasMilestones,
IMilestone,
)
+from lp.registry.interfaces.ociproject import IOCIProject
+from lp.registry.interfaces.ociprojectseries import IOCIProjectSeries
from lp.registry.interfaces.person import (
IPerson,
IPersonEditRestricted,
@@ -1093,6 +1095,9 @@ patch_operations_explicit_version(
# IWikiName
patch_entry_explicit_version(IWikiName, 'beta')
+# IOCIProject
+patch_collection_property(IOCIProject, 'series', IOCIProjectSeries)
+
# IOCIRecipe
patch_collection_property(IOCIRecipe, 'builds', IOCIRecipeBuild)
patch_collection_property(IOCIRecipe, 'completed_builds', IOCIRecipeBuild)
diff --git a/lib/lp/oci/configure.zcml b/lib/lp/oci/configure.zcml
index da8d695..3382341 100644
--- a/lib/lp/oci/configure.zcml
+++ b/lib/lp/oci/configure.zcml
@@ -5,10 +5,13 @@
xmlns="http://namespaces.zope.org/zope"
xmlns:i18n="http://namespaces.zope.org/i18n"
xmlns:lp="http://namespaces.canonical.com/lp"
+ xmlns:webservice="http://namespaces.canonical.com/webservice"
i18n_domain="launchpad">
<include package=".browser" />
+ <webservice:register module="lp.oci.interfaces.webservice" />
+
<!-- OCIRecipe -->
<class
class="lp.oci.model.ocirecipe.OCIRecipe">
diff --git a/lib/lp/oci/interfaces/ocirecipe.py b/lib/lp/oci/interfaces/ocirecipe.py
index d5bef5e..b4f52eb 100644
--- a/lib/lp/oci/interfaces/ocirecipe.py
+++ b/lib/lp/oci/interfaces/ocirecipe.py
@@ -20,7 +20,11 @@ __all__ = [
'OCIRecipeNotOwner',
]
-from lazr.restful.declarations import error_status
+from lazr.restful.declarations import (
+ error_status,
+ export_as_webservice_entry,
+ exported,
+ )
from lazr.restful.fields import (
CollectionField,
Reference,
@@ -93,40 +97,40 @@ class IOCIRecipeView(Interface):
"""`IOCIRecipe` attributes that require launchpad.View permission."""
id = Int(title=_("ID"), required=True, readonly=True)
- date_created = Datetime(
- title=_("Date created"), required=True, readonly=True)
- date_last_modified = Datetime(
- title=_("Date last modified"), required=True, readonly=True)
+ date_created = exported(Datetime(
+ title=_("Date created"), required=True, readonly=True))
+ date_last_modified = exported(Datetime(
+ title=_("Date last modified"), required=True, readonly=True))
- registrant = PublicPersonChoice(
+ registrant = exported(PublicPersonChoice(
title=_("Registrant"),
description=_("The user who registered this recipe."),
- vocabulary='ValidPersonOrTeam', required=True, readonly=True)
+ vocabulary='ValidPersonOrTeam', required=True, readonly=True))
- builds = CollectionField(
+ builds = exported(CollectionField(
title=_("Completed builds of this OCI recipe."),
description=_(
"Completed builds of this OCI recipe, sorted in descending "
"order of finishing."),
# Really IOCIRecipeBuild, patched in _schema_circular_imports.
value_type=Reference(schema=Interface),
- required=True, readonly=True)
+ required=True, readonly=True))
- completed_builds = CollectionField(
+ completed_builds = exported(CollectionField(
title=_("Completed builds of this OCI recipe."),
description=_(
"Completed builds of this OCI recipe, sorted in descending "
"order of finishing."),
# Really IOCIRecipeBuild, patched in _schema_circular_imports.
- value_type=Reference(schema=Interface), readonly=True)
+ value_type=Reference(schema=Interface), readonly=True))
- pending_builds = CollectionField(
+ pending_builds = exported(CollectionField(
title=_("Pending builds of this OCI recipe."),
description=_(
"Pending builds of this OCI recipe, sorted in descending "
"order of creation."),
# Really IOCIRecipeBuild, patched in _schema_circular_imports.
- value_type=Reference(schema=Interface), readonly=True)
+ value_type=Reference(schema=Interface), readonly=True))
def requestBuild(requester, architecture):
"""Request that the OCI recipe is built.
@@ -150,74 +154,74 @@ class IOCIRecipeEditableAttributes(IHasOwner):
These attributes need launchpad.View to see, and launchpad.Edit to change.
"""
- name = TextLine(
+ name = exported(TextLine(
title=_("Name"),
description=_("The name of this recipe."),
constraint=name_validator,
required=True,
- readonly=False)
+ readonly=False))
- owner = PersonChoice(
+ owner = exported(PersonChoice(
title=_("Owner"),
required=True,
vocabulary="AllUserTeamsParticipationPlusSelf",
description=_("The owner of this OCI recipe."),
- readonly=False)
+ readonly=False))
- oci_project = Reference(
+ oci_project = exported(Reference(
IOCIProject,
title=_("OCI project"),
description=_("The OCI project that this recipe is for."),
required=True,
- readonly=True)
+ readonly=True))
- official = Bool(
+ official = exported(Bool(
title=_("OCI project official"),
required=True,
default=False,
description=_("True if this recipe is official for its OCI project."),
- readonly=False)
+ readonly=False))
- git_ref = Reference(
+ git_ref = exported(Reference(
IGitRef, title=_("Git branch"), required=True, readonly=False,
description=_(
"The Git branch containing a Dockerfile at the location "
- "defined by the build_file attribute."))
+ "defined by the build_file attribute.")))
- git_repository = ReferenceChoice(
+ git_repository = exported(ReferenceChoice(
title=_("Git repository"),
schema=IGitRepository, vocabulary="GitRepository",
required=True, readonly=False,
description=_(
"A Git repository with a branch containing a Dockerfile "
- "at the location defined by the build_file attribute."))
+ "at the location defined by the build_file attribute.")))
- git_path = TextLine(
+ git_path = exported(TextLine(
title=_("Git branch path"), required=True, readonly=False,
description=_(
"The path of the Git branch containing a Dockerfile "
- "at the location defined by the build_file attribute."))
+ "at the location defined by the build_file attribute.")))
- description = Text(
+ description = exported(Text(
title=_("Description"),
description=_("A short description of this recipe."),
required=False,
- readonly=False)
+ readonly=False))
- build_file = TextLine(
+ build_file = exported(TextLine(
title=_("Build file path"),
description=_("The relative path to the file within this recipe's "
"branch that defines how to build the recipe."),
constraint=path_does_not_escape,
required=True,
- readonly=False)
+ readonly=False))
- build_daily = Bool(
+ build_daily = exported(Bool(
title=_("Build daily"),
required=True,
default=False,
description=_("If True, this recipe should be built daily."),
- readonly=False)
+ readonly=False))
class IOCIRecipeAdminAttributes(Interface):
@@ -235,6 +239,8 @@ class IOCIRecipe(IOCIRecipeView, IOCIRecipeEdit, IOCIRecipeEditableAttributes,
IOCIRecipeAdminAttributes):
"""A recipe for building Open Container Initiative images."""
+ export_as_webservice_entry(publish_web_link=True, as_of="devel")
+
class IOCIRecipeSet(Interface):
"""A utility to create and access OCI Recipes."""
diff --git a/lib/lp/oci/interfaces/webservice.py b/lib/lp/oci/interfaces/webservice.py
new file mode 100644
index 0000000..0a6933b
--- /dev/null
+++ b/lib/lp/oci/interfaces/webservice.py
@@ -0,0 +1,14 @@
+# Copyright 2020 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""All the interfaces that are exposed through the webservice."""
+
+__all__ = [
+ 'IOCIProject',
+ 'IOCIProjectSeries',
+ 'IOCIRecipe',
+ ]
+
+from lp.oci.interfaces.ocirecipe import IOCIRecipe
+from lp.registry.interfaces.ociproject import IOCIProject
+from lp.registry.interfaces.ociprojectseries import IOCIProjectSeries
diff --git a/lib/lp/oci/tests/test_ociproject.py b/lib/lp/oci/tests/test_ociproject.py
new file mode 100644
index 0000000..824c1d2
--- /dev/null
+++ b/lib/lp/oci/tests/test_ociproject.py
@@ -0,0 +1,98 @@
+# Copyright 2020 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Tests OCI project."""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+import json
+
+from testtools.matchers import (
+ ContainsDict,
+ Equals,
+ )
+from zope.security.proxy import removeSecurityProxy
+
+from lp.services.webapp.interfaces import OAuthPermission
+from lp.services.webhooks.testing import StartsWith
+from lp.testing import (
+ admin_logged_in,
+ api_url,
+ person_logged_in,
+ TestCaseWithFactory,
+ )
+from lp.testing.layers import DatabaseFunctionalLayer
+from lp.testing.pages import webservice_for_person
+
+
+class TestOCIProjectWebservice(TestCaseWithFactory):
+ layer = DatabaseFunctionalLayer
+
+ def setUp(self):
+ super(TestOCIProjectWebservice, self).setUp()
+ self.person = self.factory.makePerson(displayname="Test Person")
+ self.webservice = webservice_for_person(
+ self.person, permission=OAuthPermission.WRITE_PUBLIC,
+ default_api_version="devel")
+
+ def load_from_api(self, url):
+ response = self.webservice.get(url)
+ self.assertEqual(200, response.status, response.body)
+ return response.jsonBody()
+
+ def test_api_get_oci_project(self):
+ with person_logged_in(self.person):
+ project = removeSecurityProxy(self.factory.makeOCIProject(
+ registrant=self.person))
+ self.factory.makeOCIProjectSeries(
+ oci_project=project, registrant=self.person)
+ url = api_url(project)
+
+ ws_project = self.load_from_api(url)
+
+ self.assertThat(ws_project, ContainsDict(dict(
+ date_created=Equals(project.date_created.isoformat()),
+ date_last_modified=Equals(project.date_last_modified.isoformat()),
+ display_name=Equals(project.display_name),
+ registrant_link=StartsWith("http"),
+ series_collection_link=StartsWith("http"))
+ ))
+
+ def test_api_save_oci_project(self):
+ with person_logged_in(self.person):
+ # Only the owner of the distribution (which is the pillar of the
+ # OCIProject) is allowed to update it's attributed.
+ distro = self.factory.makeDistribution(owner=self.person)
+ project = removeSecurityProxy(self.factory.makeOCIProject(
+ registrant=self.person, pillar=distro))
+ url = api_url(project)
+
+ new_description = 'Some other description'
+ resp = self.webservice.patch(
+ url, 'application/json',
+ json.dumps({'description': new_description}))
+ self.assertEqual(209, resp.status, resp.body)
+
+ ws_project = self.load_from_api(url)
+ self.assertEqual(new_description, ws_project['description'])
+
+ def test_api_save_oci_project_prevents_updates_from_others(self):
+ with admin_logged_in():
+ other_person = self.factory.makePerson()
+ with person_logged_in(other_person):
+ # Only the owner of the distribution (which is the pillar of the
+ # OCIProject) is allowed to update it's attributed.
+ distro = self.factory.makeDistribution(owner=other_person)
+ project = removeSecurityProxy(self.factory.makeOCIProject(
+ registrant=other_person, pillar=distro,
+ description="old description"))
+ url = api_url(project)
+
+ new_description = 'Some other description'
+ resp = self.webservice.patch(
+ url, 'application/json',
+ json.dumps({'description': new_description}))
+ self.assertEqual(401, resp.status, resp.body)
+
+ ws_project = self.load_from_api(url)
+ self.assertEqual("old description", ws_project['description'])
diff --git a/lib/lp/oci/tests/test_ocirecipe.py b/lib/lp/oci/tests/test_ocirecipe.py
index e930ccc..50ccf53 100644
--- a/lib/lp/oci/tests/test_ocirecipe.py
+++ b/lib/lp/oci/tests/test_ocirecipe.py
@@ -5,12 +5,16 @@
from __future__ import absolute_import, print_function, unicode_literals
+import json
+
from fixtures import FakeLogger
from storm.exceptions import LostObjectError
from testtools.matchers import (
+ ContainsDict,
Equals,
MatchesDict,
MatchesStructure,
+ StartsWith,
)
import transaction
from zope.component import getUtility
@@ -35,16 +39,19 @@ from lp.services.database.constants import (
)
from lp.services.database.sqlbase import flush_database_caches
from lp.services.features.testing import FeatureFixture
+from lp.services.webapp.interfaces import OAuthPermission
from lp.services.webapp.publisher import canonical_url
from lp.services.webapp.snapshot import notify_modified
from lp.services.webhooks.testing import LogsScheduledWebhooks
from lp.testing import (
admin_logged_in,
+ api_url,
person_logged_in,
TestCaseWithFactory,
)
from lp.testing.dbuser import dbuser
from lp.testing.layers import DatabaseFunctionalLayer
+from lp.testing.pages import webservice_for_person
class TestOCIRecipe(TestCaseWithFactory):
@@ -286,3 +293,92 @@ class TestOCIRecipeSet(TestCaseWithFactory):
owner=owner,
oci_project=oci_project,
name="missing")
+
+
+class TestOCIRecipeWebservice(TestCaseWithFactory):
+ layer = DatabaseFunctionalLayer
+
+ def setUp(self):
+ super(TestOCIRecipeWebservice, self).setUp()
+ self.person = self.factory.makePerson(displayname="Test Person")
+ self.webservice = webservice_for_person(
+ self.person, permission=OAuthPermission.WRITE_PUBLIC,
+ default_api_version="devel")
+
+ def load_from_api(self, url):
+ response = self.webservice.get(url)
+ self.assertEqual(200, response.status, response.body)
+ return response.jsonBody()
+
+ def test_api_get_oci_recipe(self):
+ with person_logged_in(self.person):
+ project = removeSecurityProxy(self.factory.makeOCIProject(
+ registrant=self.person))
+ recipe = removeSecurityProxy(self.factory.makeOCIRecipe(
+ oci_project=project))
+ url = api_url(recipe)
+
+ ws_recipe = self.load_from_api(url)
+
+ self.assertThat(ws_recipe, ContainsDict(dict(
+ date_created=Equals(recipe.date_created.isoformat()),
+ date_last_modified=Equals(recipe.date_last_modified.isoformat()),
+ registrant_link=StartsWith("http"),
+ resource_type_link=StartsWith("http"),
+ pending_builds_collection_link=StartsWith("http"),
+ webhooks_collection_link=StartsWith("http"),
+ name=Equals(recipe.name),
+ owner_link=StartsWith("http"),
+ oci_project_link=StartsWith("http"),
+ official=Equals(recipe.official),
+ git_ref_link=StartsWith("http"),
+ git_repository_link=StartsWith("http"),
+ git_path=Equals(recipe.git_path),
+ description=Equals(recipe.description),
+ build_file=Equals(recipe.build_file),
+ build_daily=Equals(recipe.build_daily)
+ )))
+
+ def test_api_patch_oci_recipe(self):
+ with person_logged_in(self.person):
+ distro = self.factory.makeDistribution(owner=self.person)
+ project = removeSecurityProxy(self.factory.makeOCIProject(
+ pillar=distro, registrant=self.person))
+ # Only the owner should be able to edit.
+ recipe = removeSecurityProxy(self.factory.makeOCIRecipe(
+ oci_project=project, owner=self.person,
+ registrant=self.person))
+ url = api_url(recipe)
+
+ new_description = 'Some other description'
+ resp = self.webservice.patch(
+ url, 'application/json',
+ json.dumps({'description': new_description}))
+
+ self.assertEqual(209, resp.status, resp.body)
+
+ ws_project = self.load_from_api(url)
+ self.assertEqual(new_description, ws_project['description'])
+
+ def test_api_patch_fails_with_different_user(self):
+ with admin_logged_in():
+ other_person = self.factory.makePerson()
+ with person_logged_in(other_person):
+ distro = self.factory.makeDistribution(owner=other_person)
+ project = removeSecurityProxy(self.factory.makeOCIProject(
+ pillar=distro, registrant=other_person))
+ # Only the owner should be able to edit.
+ recipe = removeSecurityProxy(self.factory.makeOCIRecipe(
+ oci_project=project, owner=other_person,
+ registrant=other_person,
+ description="old description"))
+ url = api_url(recipe)
+
+ new_description = 'Some other description'
+ resp = self.webservice.patch(
+ url, 'application/json',
+ json.dumps({'description': new_description}))
+ self.assertEqual(401, resp.status, resp.body)
+
+ ws_project = self.load_from_api(url)
+ self.assertEqual("old description", ws_project['description'])
diff --git a/lib/lp/registry/browser/configure.zcml b/lib/lp/registry/browser/configure.zcml
index 72864b9..f465bc0 100644
--- a/lib/lp/registry/browser/configure.zcml
+++ b/lib/lp/registry/browser/configure.zcml
@@ -1,4 +1,4 @@
-<!-- Copyright 2009-2019 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).
-->
@@ -643,6 +643,11 @@
for="lp.registry.interfaces.ociproject.IOCIProject"
/>
<browser:url
+ for="lp.registry.interfaces.ociprojectseries.IOCIProjectSeries"
+ path_expression="string:+oci/${oci_project}/series/${name}"
+ attribute_to_parent="oci_project"
+ />
+ <browser:url
for="lp.registry.interfaces.commercialsubscription.ICommercialSubscription"
path_expression="string:+commercialsubscription/${id}"
attribute_to_parent="product"
diff --git a/lib/lp/registry/interfaces/ociproject.py b/lib/lp/registry/interfaces/ociproject.py
index e0b3c00..84561e3 100644
--- a/lib/lp/registry/interfaces/ociproject.py
+++ b/lib/lp/registry/interfaces/ociproject.py
@@ -1,4 +1,4 @@
-# Copyright 2019 Canonical Ltd. This software is licensed under the
+# Copyright 2019-2020 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""OCI Project interfaces."""
@@ -11,6 +11,10 @@ __all__ = [
'IOCIProjectSet',
]
+from lazr.restful.declarations import (
+ export_as_webservice_entry,
+ exported,
+ )
from lazr.restful.fields import (
CollectionField,
Reference,
@@ -42,22 +46,23 @@ class IOCIProjectView(IHasGitRepositories, Interface):
"""IOCIProject attributes that require launchpad.View permission."""
id = Int(title=_("ID"), required=True, readonly=True)
- date_created = Datetime(
- title=_("Date created"), required=True, readonly=True)
- date_last_modified = Datetime(
- title=_("Date last modified"), required=True, readonly=True)
+ date_created = exported(Datetime(
+ title=_("Date created"), required=True, readonly=True))
+ date_last_modified = exported(Datetime(
+ title=_("Date last modified"), required=True, readonly=True))
- registrant = PublicPersonChoice(
+ registrant = exported(PublicPersonChoice(
title=_("Registrant"),
description=_("The person that registered this project."),
- vocabulary='ValidPersonOrTeam', required=True, readonly=True)
+ vocabulary='ValidPersonOrTeam', required=True, readonly=True))
- series = CollectionField(
+ series = exported(CollectionField(
title=_("Series inside this OCI project."),
# Really IOCIProjectSeries
- value_type=Reference(schema=Interface))
+ value_type=Reference(schema=Interface)))
- display_name = Attribute(_("Display name for this OCI project."))
+ display_name = exported(TextLine(
+ title=_("Display name for this OCI project.")))
class IOCIProjectEditableAttributes(IBugTarget):
@@ -66,23 +71,24 @@ class IOCIProjectEditableAttributes(IBugTarget):
These attributes need launchpad.View to see, and launchpad.Edit to change.
"""
- distribution = ReferenceChoice(
+ distribution = exported(ReferenceChoice(
title=_("The distribution that this OCI project is associated with."),
schema=IDistribution, vocabulary="Distribution",
- required=True, readonly=False)
- name = TextLine(
+ required=True, readonly=False))
+ name = exported(TextLine(
title=_("Name"), required=True, readonly=False,
constraint=name_validator,
- description=_("The name of this OCI project."))
+ description=_("The name of this OCI project.")))
ociprojectname = Reference(
IOCIProjectName,
title=_("The name of this OCI project, as an `IOCIProjectName`."),
required=True,
readonly=True)
- description = Text(title=_("The description for this OCI project."))
- pillar = Reference(
+ description = exported(Text(
+ title=_("The description for this OCI project.")))
+ pillar = exported(Reference(
IDistribution,
- title=_("The pillar containing this target."), readonly=True)
+ title=_("The pillar containing this target."), readonly=True))
class IOCIProjectEdit(Interface):
@@ -97,6 +103,8 @@ class IOCIProject(IOCIProjectView, IOCIProjectEdit,
IOCIProjectEditableAttributes):
"""A project containing Open Container Initiative recipes."""
+ export_as_webservice_entry(publish_web_link=True, as_of="devel")
+
class IOCIProjectSet(Interface):
"""A utility to create and access OCI Projects."""
diff --git a/lib/lp/registry/interfaces/ociprojectseries.py b/lib/lp/registry/interfaces/ociprojectseries.py
index 3019c22..57e3571 100644
--- a/lib/lp/registry/interfaces/ociprojectseries.py
+++ b/lib/lp/registry/interfaces/ociprojectseries.py
@@ -1,4 +1,4 @@
-# Copyright 2019 Canonical Ltd. This software is licensed under the
+# Copyright 2019-2020 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Interfaces to allow bug filing on multiple versions of an OCI Project."""
@@ -12,6 +12,10 @@ __all__ = [
'IOCIProjectSeriesView',
]
+from lazr.restful.declarations import (
+ export_as_webservice_entry,
+ exported,
+ )
from lazr.restful.fields import Reference
from zope.interface import Interface
from zope.schema import (
@@ -34,20 +38,20 @@ class IOCIProjectSeriesView(Interface):
id = Int(title=_("ID"), required=True, readonly=True)
- oci_project = Reference(
+ oci_project = exported(Reference(
IOCIProject,
title=_("The OCI project that this series belongs to."),
- required=True, readonly=True)
+ required=True, readonly=True))
- date_created = Datetime(
+ date_created = exported(Datetime(
title=_("Date created"), required=True, readonly=True,
description=_(
- "The date on which this series was created in Launchpad."))
+ "The date on which this series was created in Launchpad.")))
- registrant = PublicPersonChoice(
+ registrant = exported(PublicPersonChoice(
title=_("Registrant"),
description=_("The person that registered this series."),
- vocabulary='ValidPersonOrTeam', required=True, readonly=True)
+ vocabulary='ValidPersonOrTeam', required=True, readonly=True))
class IOCIProjectSeriesEditableAttributes(Interface):
@@ -56,18 +60,18 @@ class IOCIProjectSeriesEditableAttributes(Interface):
These attributes need launchpad.View to see, and launchpad.Edit to change.
"""
- name = TextLine(
+ name = exported(TextLine(
title=_("Name"), constraint=name_validator,
required=True, readonly=False,
- description=_("The name of this series."))
+ description=_("The name of this series.")))
- summary = Text(
+ summary = exported(Text(
title=_("Summary"), required=True, readonly=False,
- description=_("A brief summary of this series."))
+ description=_("A brief summary of this series.")))
- status = Choice(
+ status = exported(Choice(
title=_("Status"), required=True,
- vocabulary=SeriesStatus)
+ vocabulary=SeriesStatus))
class IOCIProjectSeriesEdit(Interface):
@@ -80,3 +84,5 @@ class IOCIProjectSeries(IOCIProjectSeriesView, IOCIProjectSeriesEdit,
This is used to allow tracking bugs against multiple versions of images.
"""
+
+ export_as_webservice_entry(publish_web_link=True, as_of="devel")