launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #18726
[Merge] lp:~cjwatson/launchpad/git-repository-ui-edit-target into lp:launchpad
Colin Watson has proposed merging lp:~cjwatson/launchpad/git-repository-ui-edit-target into lp:launchpad with lp:~cjwatson/launchpad/git-repository-ui-edit-owner as a prerequisite.
Commit message:
Teach GitRepository:+edit how to change target, target_default, and owner_default.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/git-repository-ui-edit-target/+merge/261232
Teach GitRepository:+edit how to change target, target_default, and owner_default. This involves some JavaScript to disable the *_default fields when the target is set to a personal repository, where they don't make sense.
--
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/git-repository-ui-edit-target into lp:launchpad.
=== modified file 'lib/lp/code/browser/configure.zcml'
--- lib/lp/code/browser/configure.zcml 2015-06-05 09:08:50 +0000
+++ lib/lp/code/browser/configure.zcml 2015-06-05 14:00:28 +0000
@@ -799,7 +799,7 @@
class="lp.code.browser.gitrepository.GitRepositoryEditView"
permission="launchpad.Edit"
name="+edit"
- template="../../app/templates/generic-edit.pt"/>
+ template="../templates/gitrepository-edit.pt"/>
<browser:page
for="lp.code.interfaces.gitrepository.IGitRepository"
class="lp.code.browser.gitrepository.GitRepositoryDeletionView"
=== modified file 'lib/lp/code/browser/gitrepository.py'
--- lib/lp/code/browser/gitrepository.py 2015-06-05 14:00:28 +0000
+++ lib/lp/code/browser/gitrepository.py 2015-06-05 14:00:28 +0000
@@ -47,13 +47,18 @@
from lp.app.errors import NotFoundError
from lp.app.vocabularies import InformationTypeVocabulary
from lp.app.widgets.itemswidgets import LaunchpadRadioWidgetWithDescription
+from lp.code.browser.widgets.gitrepositorytarget import (
+ GitRepositoryTargetWidget,
+ )
from lp.code.errors import (
GitRepositoryCreationForbidden,
GitRepositoryExists,
+ GitTargetError,
)
from lp.code.interfaces.gitnamespace import get_git_namespace
from lp.code.interfaces.gitref import IGitRefBatchNavigator
from lp.code.interfaces.gitrepository import IGitRepository
+from lp.registry.interfaces.person import IPerson
from lp.registry.vocabularies import UserTeamsParticipationPlusSelfVocabulary
from lp.services.config import config
from lp.services.database.constants import UTC_NOW
@@ -277,7 +282,12 @@
vocabulary=InformationTypeVocabulary(types=info_types))
name = copy_field(IGitRepository["name"], readonly=False)
owner = copy_field(IGitRepository["owner"], readonly=False)
+ owner_default = copy_field(
+ IGitRepository["owner_default"], readonly=False)
reviewer = copy_field(IGitRepository["reviewer"], required=True)
+ target = copy_field(IGitRepository["target"], readonly=False)
+ target_default = copy_field(
+ IGitRepository["target_default"], readonly=False)
return GitRepositoryEditSchema
@@ -322,6 +332,25 @@
information_type = data.pop("information_type")
self.context.transitionToInformationType(
information_type, self.user)
+ if "target" in data:
+ target = data.pop("target")
+ if target is None:
+ target = self.context.owner
+ if target != self.context.target:
+ try:
+ self.context.setTarget(target, self.user)
+ except GitTargetError as e:
+ self.setFieldError("target", e.message)
+ return
+ changed = True
+ if IPerson.providedBy(target):
+ self.request.response.addNotification(
+ "This repository is now a personal repository for %s "
+ "(%s)" % (target.displayname, target.name))
+ else:
+ self.request.response.addNotification(
+ "The repository target has been changed to %s (%s)" %
+ (target.displayname, target.name))
if "reviewer" in data:
reviewer = data.pop("reviewer")
if reviewer != self.context.code_reviewer:
@@ -331,6 +360,16 @@
else:
self.context.reviewer = reviewer
changed = True
+ if "target_default" in data:
+ target_default = data.pop("target_default")
+ if (self.context.namespace.has_defaults and
+ target_default != self.context.target_default):
+ self.context.setTargetDefault(target_default)
+ if "owner_default" in data:
+ owner_default = data.pop("owner_default")
+ if (self.context.namespace.has_defaults and
+ owner_default != self.context.owner_default):
+ self.context.setOwnerDefault(owner_default)
if self.updateContextFromData(data, notify_modified=False):
changed = True
@@ -383,9 +422,13 @@
field_names = [
"owner",
"name",
+ "target",
"information_type",
+ "target_default",
+ "owner_default",
]
+ custom_widget("target", GitRepositoryTargetWidget)
custom_widget("information_type", LaunchpadRadioWidgetWithDescription)
def setUpFields(self):
@@ -442,14 +485,15 @@
self.setFieldError(field_name, message)
def validate(self, data):
- if "name" in data and "owner" in data:
+ if "name" in data and "owner" in data and "target" in data:
name = data["name"]
owner = data["owner"]
- if name != self.context.name or owner != self.context.owner:
- if self.context.owner == self.context.target:
- target = owner
- else:
- target = self.context.target
+ target = data["target"]
+ if target is None:
+ target = owner
+ if (name != self.context.name or
+ owner != self.context.owner or
+ target != self.context.target):
namespace = get_git_namespace(target, owner)
try:
namespace.validateMove(self.context, self.user, name=name)
=== modified file 'lib/lp/code/browser/tests/test_gitrepository.py'
--- lib/lp/code/browser/tests/test_gitrepository.py 2015-06-05 14:00:28 +0000
+++ lib/lp/code/browser/tests/test_gitrepository.py 2015-06-05 14:00:28 +0000
@@ -38,7 +38,10 @@
TestCaseWithFactory,
)
from lp.testing.layers import DatabaseFunctionalLayer
-from lp.testing.matchers import HasQueryCount
+from lp.testing.matchers import (
+ Contains,
+ HasQueryCount,
+ )
from lp.testing.pages import (
get_feedback_messages,
setupBrowser,
@@ -255,6 +258,171 @@
layer = DatabaseFunctionalLayer
+ def test_repository_target_widget_renders_personal(self):
+ # The repository target widget renders correctly for a personal
+ # repository.
+ person = self.factory.makePerson()
+ repository = self.factory.makeGitRepository(
+ owner=person, target=person)
+ login_person(person)
+ view = create_initialized_view(repository, name="+edit")
+ self.assertEqual("personal", view.widgets["target"].default_option)
+
+ def test_repository_target_widget_renders_product(self):
+ # The repository target widget renders correctly for a product
+ # repository.
+ person = self.factory.makePerson()
+ project = self.factory.makeProduct()
+ repository = self.factory.makeGitRepository(
+ owner=person, target=project)
+ login_person(person)
+ view = create_initialized_view(repository, name="+edit")
+ self.assertEqual("project", view.widgets["target"].default_option)
+ self.assertEqual(
+ project.name, view.widgets["target"].project_widget.selected_value)
+
+ def test_repository_target_widget_renders_package(self):
+ # The repository target widget renders correctly for a package
+ # repository.
+ person = self.factory.makePerson()
+ dsp = self.factory.makeDistributionSourcePackage()
+ repository = self.factory.makeGitRepository(owner=person, target=dsp)
+ login_person(person)
+ view = create_initialized_view(repository, name="+edit")
+ self.assertEqual("package", view.widgets["target"].default_option)
+ self.assertEqual(
+ dsp.distribution,
+ view.widgets["target"].distribution_widget._getFormValue())
+ self.assertEqual(
+ dsp.sourcepackagename.name,
+ view.widgets["target"].package_widget.selected_value)
+
+ def test_repository_target_widget_saves_personal(self):
+ # The repository target widget can retarget to a personal
+ # repository.
+ person = self.factory.makePerson()
+ repository = self.factory.makeGitRepository(owner=person)
+ login_person(person)
+ form = {
+ "field.target": "personal",
+ "field.actions.change": "Change Git Repository",
+ }
+ view = create_initialized_view(repository, name="+edit", form=form)
+ self.assertEqual(person, repository.target)
+ self.assertEqual(1, len(view.request.response.notifications))
+ self.assertEqual(
+ "This repository is now a personal repository for %s (%s)"
+ % (person.displayname, person.name),
+ view.request.response.notifications[0].message)
+
+ def test_repository_target_widget_saves_personal_different_owner(self):
+ # The repository target widget can retarget to a personal repository
+ # for a different owner.
+ person = self.factory.makePerson()
+ repository = self.factory.makeGitRepository(
+ owner=person, target=person)
+ new_owner = self.factory.makeTeam(name="newowner", members=[person])
+ login_person(person)
+ form = {
+ "field.target": "personal",
+ "field.owner": "newowner",
+ "field.actions.change": "Change Git Repository",
+ }
+ view = create_initialized_view(repository, name="+edit", form=form)
+ self.assertEqual(new_owner, repository.target)
+ self.assertEqual(1, len(view.request.response.notifications))
+ self.assertEqual(
+ "The repository owner has been changed to Newowner (newowner)",
+ view.request.response.notifications[0].message)
+
+ def test_repository_target_widget_saves_personal_clears_defaults(self):
+ # When retargeting to a personal repository, the target and
+ # owner-target default flags are cleared.
+ person = self.factory.makePerson()
+ project = self.factory.makeProduct(owner=person)
+ repository = self.factory.makeGitRepository(
+ owner=person, target=project)
+ login_person(person)
+ repository.setTargetDefault(True)
+ repository.setOwnerDefault(True)
+ form = {
+ "field.target": "personal",
+ "field.actions.change": "Change Git Repository",
+ }
+ view = create_initialized_view(repository, name="+edit", form=form)
+ self.assertEqual([], view.errors)
+ self.assertEqual(person, repository.target)
+ self.assertEqual(1, len(view.request.response.notifications))
+ self.assertEqual(
+ "This repository is now a personal repository for %s (%s)"
+ % (person.displayname, person.name),
+ view.request.response.notifications[0].message)
+
+ def test_repository_target_widget_saves_project(self):
+ # The repository target widget can retarget to a project repository.
+ person = self.factory.makePerson()
+ repository = self.factory.makeGitRepository(
+ owner=person, target=person)
+ project = self.factory.makeProduct()
+ login_person(person)
+ form = {
+ "field.target": "project",
+ "field.target.project": project.name,
+ "field.actions.change": "Change Git Repository",
+ }
+ view = create_initialized_view(repository, name="+edit", form=form)
+ self.assertEqual(project, repository.target)
+ self.assertEqual(
+ "The repository target has been changed to %s (%s)"
+ % (project.displayname, project.name),
+ view.request.response.notifications[0].message)
+
+ def test_repository_target_widget_saves_package(self):
+ # The repository target widget can retarget to a package repository.
+ person = self.factory.makePerson()
+ repository = self.factory.makeGitRepository(
+ owner=person, target=person)
+ dsp = self.factory.makeDistributionSourcePackage()
+ self.factory.makeSourcePackagePublishingHistory(
+ distroseries=dsp.distribution.currentseries,
+ sourcepackagename=dsp.sourcepackagename,
+ archive=dsp.distribution.main_archive)
+ login_person(person)
+ form = {
+ "field.target": "package",
+ "field.target.distribution": dsp.distribution.name,
+ "field.target.package": dsp.sourcepackagename.name,
+ "field.actions.change": "Change Git Repository",
+ }
+ view = create_initialized_view(repository, name="+edit", form=form)
+ self.assertEqual(dsp, repository.target)
+ self.assertEqual(
+ "The repository target has been changed to %s (%s)"
+ % (dsp.displayname, dsp.name),
+ view.request.response.notifications[0].message)
+
+ def test_forbidden_target_is_error(self):
+ # An error is displayed if a repository is saved with a target that
+ # is not allowed by the sharing policy.
+ owner = self.factory.makePerson()
+ initial_target = self.factory.makeProduct()
+ self.factory.makeProduct(
+ name="commercial", owner=owner,
+ branch_sharing_policy=BranchSharingPolicy.PROPRIETARY)
+ repository = self.factory.makeGitRepository(
+ owner=owner, target=initial_target,
+ information_type=InformationType.PUBLIC)
+ browser = self.getUserBrowser(
+ canonical_url(repository) + "/+edit", user=owner)
+ browser.getControl(name="field.target.project").value = "commercial"
+ browser.getControl("Change Git Repository").click()
+ self.assertThat(
+ browser.contents,
+ Contains(
+ "Public repositories are not allowed for target Commercial."))
+ with person_logged_in(owner):
+ self.assertEqual(initial_target, repository.target)
+
def test_rename(self):
# The name of a repository can be changed via the UI by an
# authorised user.
=== added file 'lib/lp/code/browser/widgets/gitrepositorytarget.py'
--- lib/lp/code/browser/widgets/gitrepositorytarget.py 1970-01-01 00:00:00 +0000
+++ lib/lp/code/browser/widgets/gitrepositorytarget.py 2015-06-05 14:00:28 +0000
@@ -0,0 +1,194 @@
+# Copyright 2015 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+__metaclass__ = type
+
+__all__ = [
+ 'GitRepositoryTargetWidget',
+ ]
+
+from z3c.ptcompat import ViewPageTemplateFile
+from zope.component import getUtility
+from zope.formlib.interfaces import (
+ ConversionError,
+ IInputWidget,
+ InputErrors,
+ MissingInputError,
+ WidgetInputError,
+ )
+from zope.formlib.utility import setUpWidget
+from zope.formlib.widget import (
+ BrowserWidget,
+ CustomWidgetFactory,
+ InputWidget,
+ renderElement,
+ )
+from zope.interface import implements
+from zope.schema import Choice
+
+from lp.app.errors import (
+ NotFoundError,
+ UnexpectedFormData,
+ )
+from lp.app.interfaces.launchpad import ILaunchpadCelebrities
+from lp.app.validators import LaunchpadValidationError
+from lp.app.widgets.itemswidgets import LaunchpadDropdownWidget
+from lp.registry.interfaces.distributionsourcepackage import (
+ IDistributionSourcePackage,
+ )
+from lp.registry.interfaces.person import IPerson
+from lp.registry.interfaces.product import IProduct
+from lp.services.webapp.interfaces import (
+ IAlwaysSubmittedWidget,
+ IMultiLineWidgetLayout,
+ )
+
+
+class GitRepositoryTargetWidget(BrowserWidget, InputWidget):
+ """Widget for selecting a Git repository target."""
+
+ implements(IAlwaysSubmittedWidget, IMultiLineWidgetLayout, IInputWidget)
+
+ template = ViewPageTemplateFile("templates/gitrepository-target.pt")
+ default_option = "project"
+ _widgets_set_up = False
+
+ def setUpSubWidgets(self):
+ if self._widgets_set_up:
+ return
+ fields = [
+ Choice(
+ __name__="project", title=u"Project",
+ required=True, vocabulary="Product"),
+ Choice(
+ __name__="distribution", title=u"Distribution",
+ required=True, vocabulary="Distribution",
+ default=getUtility(ILaunchpadCelebrities).ubuntu),
+ Choice(
+ __name__="package", title=u"Package",
+ required=False, vocabulary="BinaryAndSourcePackageName"),
+ ]
+ self.distribution_widget = CustomWidgetFactory(LaunchpadDropdownWidget)
+ for field in fields:
+ setUpWidget(
+ self, field.__name__, field, IInputWidget, prefix=self.name)
+ self._widgets_set_up = True
+
+ def setUpOptions(self):
+ """Set up options to be rendered."""
+ self.options = {}
+ for option in ["personal", "package", "project"]:
+ attributes = dict(
+ type="radio", name=self.name, value=option,
+ id="%s.option.%s" % (self.name, option))
+ if self.request.form_ng.getOne(
+ self.name, self.default_option) == option:
+ attributes["checked"] = "checked"
+ self.options[option] = renderElement("input", **attributes)
+ self.package_widget.onKeyPress = (
+ "selectWidget('%s.option.package', event)" % self.name)
+ self.project_widget.onKeyPress = (
+ "selectWidget('%s.option.project', event)" % self.name)
+
+ def hasInput(self):
+ return self.name in self.request.form
+
+ def hasValidInput(self):
+ """See `zope.formlib.interfaces.IInputWidget`."""
+ try:
+ self.getInputValue()
+ return True
+ except (InputErrors, UnexpectedFormData):
+ return False
+
+ def getInputValue(self):
+ """See `zope.formlib.interfaces.IInputWidget`."""
+ self.setUpSubWidgets()
+ form_value = self.request.form_ng.getOne(self.name)
+ if form_value == "project":
+ try:
+ return self.project_widget.getInputValue()
+ except MissingInputError:
+ raise WidgetInputError(
+ self.name, self.label,
+ LaunchpadValidationError("Please enter a project name"))
+ except ConversionError:
+ entered_name = self.request.form_ng.getOne(
+ "%s.project" % self.name)
+ raise WidgetInputError(
+ self.name, self.label,
+ LaunchpadValidationError(
+ "There is no project named '%s' registered in "
+ "Launchpad" % entered_name))
+ elif form_value == "package":
+ try:
+ distribution = self.distribution_widget.getInputValue()
+ except ConversionError:
+ entered_name = self.request.form_ng.getOne(
+ "%s.distribution" % self.name)
+ raise WidgetInputError(
+ self.name, self.label,
+ LaunchpadValidationError(
+ "There is no distribution named '%s' registered in "
+ "Launchpad" % entered_name))
+ try:
+ if self.package_widget.hasInput():
+ package_name = self.package_widget.getInputValue()
+ else:
+ package_name = None
+ if package_name is None:
+ raise WidgetInputError(
+ self.name, self.label,
+ LaunchpadValidationError(
+ "Please enter a package name"))
+ if IDistributionSourcePackage.providedBy(package_name):
+ dsp = package_name
+ else:
+ source_name = distribution.guessPublishedSourcePackageName(
+ package_name.name)
+ dsp = distribution.getSourcePackage(source_name)
+ except (ConversionError, NotFoundError):
+ entered_name = self.request.form_ng.getOne(
+ "%s.package" % self.name)
+ raise WidgetInputError(
+ self.name, self.label,
+ LaunchpadValidationError(
+ "There is no package named '%s' published in %s." %
+ (entered_name, distribution.displayname)))
+ return dsp
+ elif form_value == "personal":
+ return None
+ else:
+ raise UnexpectedFormData("No valid option was selected.")
+
+ def setRenderedValue(self, value):
+ """See `IWidget`."""
+ self.setUpSubWidgets()
+ if value is None or IPerson.providedBy(value):
+ self.default_option = "personal"
+ return
+ elif IProduct.providedBy(value):
+ self.default_option = "project"
+ self.project_widget.setRenderedValue(value)
+ return
+ elif IDistributionSourcePackage.providedBy(value):
+ self.default_option = "package"
+ self.distribution_widget.setRenderedValue(value.distribution)
+ self.package_widget.setRenderedValue(value.sourcepackagename)
+ else:
+ raise AssertionError("Not a valid value: %r" % value)
+
+ def error(self):
+ """See `zope.formlib.interfaces.IBrowserWidget`."""
+ try:
+ if self.hasInput():
+ self.getInputValue()
+ except InputErrors as error:
+ self._error = error
+ return super(GitRepositoryTargetWidget, self).error()
+
+ def __call__(self):
+ """See `zope.formlib.interfaces.IBrowserWidget`."""
+ self.setUpSubWidgets()
+ self.setUpOptions()
+ return self.template()
=== added file 'lib/lp/code/browser/widgets/templates/gitrepository-target.pt'
--- lib/lp/code/browser/widgets/templates/gitrepository-target.pt 1970-01-01 00:00:00 +0000
+++ lib/lp/code/browser/widgets/templates/gitrepository-target.pt 2015-06-05 14:00:28 +0000
@@ -0,0 +1,50 @@
+<table>
+ <tr>
+ <td colspan="2">
+ <label>
+ <input
+ type="radio" value="personal"
+ tal:replace="structure view/options/personal" />
+ Personal
+ </label>
+ </td>
+ </tr>
+
+ <tr>
+ <td>
+ <label>
+ <input
+ type="radio" value="package"
+ tal:replace="structure view/options/package" />
+ Distribution
+ </label>
+ </td>
+ <td>
+ <tal:distribution tal:replace="structure view/distribution_widget" />
+ </td>
+ </tr>
+ <tr>
+ <td align="right">
+ <label tal:attributes="for string:${view/name}.option.package">
+ Package
+ </label>
+ </td>
+ <td>
+ <tal:package tal:replace="structure view/package_widget" />
+ </td>
+ </tr>
+
+ <tr>
+ <td>
+ <label>
+ <input
+ type="radio" value="project"
+ tal:replace="structure view/options/project" />
+ Project
+ </label>
+ </td>
+ <td>
+ <tal:product tal:replace="structure view/project_widget" />
+ </td>
+ </tr>
+</table>
=== added file 'lib/lp/code/browser/widgets/tests/test_gitrepositorytargetwidget.py'
--- lib/lp/code/browser/widgets/tests/test_gitrepositorytargetwidget.py 1970-01-01 00:00:00 +0000
+++ lib/lp/code/browser/widgets/tests/test_gitrepositorytargetwidget.py 2015-06-05 14:00:28 +0000
@@ -0,0 +1,370 @@
+# Copyright 2015 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+__metaclass__ = type
+
+import re
+
+from BeautifulSoup import BeautifulSoup
+from lazr.restful.fields import Reference
+from zope.formlib.interfaces import (
+ IBrowserWidget,
+ IInputWidget,
+ WidgetInputError,
+ )
+from zope.interface import (
+ implements,
+ Interface,
+ )
+
+from lp.app.validators import LaunchpadValidationError
+from lp.code.browser.widgets.gitrepositorytarget import (
+ GitRepositoryTargetWidget,
+ )
+from lp.registry.vocabularies import (
+ DistributionVocabulary,
+ ProductVocabulary,
+ )
+from lp.services.webapp.escaping import html_escape
+from lp.services.webapp.servers import LaunchpadTestRequest
+from lp.soyuz.model.binaryandsourcepackagename import (
+ BinaryAndSourcePackageNameVocabulary,
+ )
+from lp.testing import (
+ TestCaseWithFactory,
+ verifyObject,
+ )
+from lp.testing.layers import DatabaseFunctionalLayer
+
+
+class IThing(Interface):
+ owner = Reference(schema=Interface)
+ target = Reference(schema=Interface)
+
+
+class Thing:
+ implements(IThing)
+ owner = None
+ target = None
+
+
+class TestGitRepositoryTargetWidget(TestCaseWithFactory):
+ """Test the GitRepositoryTargetWidget class."""
+
+ layer = DatabaseFunctionalLayer
+
+ @property
+ def form(self):
+ return {
+ "field.target": "project",
+ "field.target.distribution": "fnord",
+ "field.target.package": "snarf",
+ "field.target.project": "pting",
+ }
+
+ def setUp(self):
+ super(TestGitRepositoryTargetWidget, self).setUp()
+ self.distribution, self.package = self.factory.makeDSPCache(
+ distro_name="fnord", package_name="snarf")
+ self.project = self.factory.makeProduct("pting")
+ field = Reference(
+ __name__="target", schema=Interface, title=u"target")
+ self.context = Thing()
+ field = field.bind(self.context)
+ request = LaunchpadTestRequest()
+ self.widget = GitRepositoryTargetWidget(field, request)
+
+ def test_implements(self):
+ self.assertTrue(verifyObject(IBrowserWidget, self.widget))
+ self.assertTrue(verifyObject(IInputWidget, self.widget))
+
+ def test_template(self):
+ # The render template is setup.
+ self.assertTrue(
+ self.widget.template.filename.endswith("gitrepository-target.pt"),
+ "Template was not setup.")
+
+ def test_default_option(self):
+ # This project field is the default option.
+ self.assertEqual("project", self.widget.default_option)
+
+ def test_hasInput_false(self):
+ # hasInput is false when the widget's name is not in the form data.
+ self.widget.request = LaunchpadTestRequest(form={})
+ self.assertEqual("field.target", self.widget.name)
+ self.assertFalse(self.widget.hasInput())
+
+ def test_hasInput_true(self):
+ # hasInput is true is the widget's name in the form data.
+ self.widget.request = LaunchpadTestRequest(form=self.form)
+ self.assertEqual("field.target", self.widget.name)
+ self.assertTrue(self.widget.hasInput())
+
+ def test_setUpSubWidgets_first_call(self):
+ # The subwidgets are setup and a flag is set.
+ self.widget.setUpSubWidgets()
+ self.assertTrue(self.widget._widgets_set_up)
+ self.assertIsInstance(
+ self.widget.distribution_widget.context.vocabulary,
+ DistributionVocabulary)
+ self.assertIsInstance(
+ self.widget.package_widget.context.vocabulary,
+ BinaryAndSourcePackageNameVocabulary)
+ self.assertIsInstance(
+ self.widget.project_widget.context.vocabulary,
+ ProductVocabulary)
+
+ def test_setUpSubWidgets_second_call(self):
+ # The setUpSubWidgets method exits early if a flag is set to
+ # indicate that the widgets were setup.
+ self.widget._widgets_set_up = True
+ self.widget.setUpSubWidgets()
+ self.assertIsNone(getattr(self.widget, "distribution_widget", None))
+ self.assertIsNone(getattr(self.widget, "package_widget", None))
+ self.assertIsNone(getattr(self.widget, "project_widget", None))
+
+ def test_setUpOptions_default_project_checked(self):
+ # The radio button options are composed of the setup widgets with
+ # the project widget set as the default.
+ self.widget.setUpSubWidgets()
+ self.widget.setUpOptions()
+ self.assertEqual(
+ "selectWidget('field.target.option.package', event)",
+ self.widget.package_widget.onKeyPress)
+ self.assertEqual(
+ "selectWidget('field.target.option.project', event)",
+ self.widget.project_widget.onKeyPress)
+ self.assertEqual(
+ '<input class="radioType" '
+ 'id="field.target.option.personal" name="field.target" '
+ 'type="radio" value="personal" />',
+ self.widget.options["personal"])
+ self.assertEqual(
+ '<input class="radioType" '
+ 'id="field.target.option.package" name="field.target" '
+ 'type="radio" value="package" />',
+ self.widget.options["package"])
+ self.assertEqual(
+ '<input class="radioType" checked="checked" '
+ 'id="field.target.option.project" name="field.target" '
+ 'type="radio" value="project" />',
+ self.widget.options["project"])
+
+ def test_setUpOptions_personal_checked(self):
+ # The personal radio button is selected when the form is submitted
+ # when the target field's value is 'personal'.
+ form = {
+ "field.target": "personal",
+ }
+ self.widget.request = LaunchpadTestRequest(form=form)
+ self.widget.setUpSubWidgets()
+ self.widget.setUpOptions()
+ self.assertEqual(
+ '<input class="radioType" checked="checked" '
+ 'id="field.target.option.personal" name="field.target" '
+ 'type="radio" value="personal" />',
+ self.widget.options["personal"])
+ self.assertEqual(
+ '<input class="radioType" '
+ 'id="field.target.option.package" name="field.target" '
+ 'type="radio" value="package" />',
+ self.widget.options["package"])
+ self.assertEqual(
+ '<input class="radioType" '
+ 'id="field.target.option.project" name="field.target" '
+ 'type="radio" value="project" />',
+ self.widget.options["project"])
+
+ def test_setUpOptions_package_checked(self):
+ # The package radio button is selected when the form is submitted
+ # when the target field's value is 'package'.
+ form = {
+ "field.target": "package",
+ }
+ self.widget.request = LaunchpadTestRequest(form=form)
+ self.widget.setUpSubWidgets()
+ self.widget.setUpOptions()
+ self.assertEqual(
+ '<input class="radioType" '
+ 'id="field.target.option.personal" name="field.target" '
+ 'type="radio" value="personal" />',
+ self.widget.options["personal"])
+ self.assertEqual(
+ '<input class="radioType" checked="checked" '
+ 'id="field.target.option.package" name="field.target" '
+ 'type="radio" value="package" />',
+ self.widget.options["package"])
+ self.assertEqual(
+ '<input class="radioType" '
+ 'id="field.target.option.project" name="field.target" '
+ 'type="radio" value="project" />',
+ self.widget.options["project"])
+
+ def test_setUpOptions_project_checked(self):
+ # The project radio button is selected when the form is submitted
+ # when the target field's value is 'project'.
+ form = {
+ "field.target": "project",
+ }
+ self.widget.request = LaunchpadTestRequest(form=form)
+ self.widget.setUpSubWidgets()
+ self.widget.setUpOptions()
+ self.assertEqual(
+ '<input class="radioType" '
+ 'id="field.target.option.personal" name="field.target" '
+ 'type="radio" value="personal" />',
+ self.widget.options["personal"])
+ self.assertEqual(
+ '<input class="radioType" '
+ 'id="field.target.option.package" name="field.target" '
+ 'type="radio" value="package" />',
+ self.widget.options["package"])
+ self.assertEqual(
+ '<input class="radioType" checked="checked" '
+ 'id="field.target.option.project" name="field.target" '
+ 'type="radio" value="project" />',
+ self.widget.options["project"])
+
+ def test_hasValidInput_true(self):
+ # The field input is valid when all submitted parts are valid.
+ self.widget.request = LaunchpadTestRequest(form=self.form)
+ self.assertTrue(self.widget.hasValidInput())
+
+ def test_hasValidInput_false(self):
+ # The field input is invalid if any of the submitted parts are invalid.
+ form = self.form
+ form["field.target.project"] = "non-existent"
+ self.widget.request = LaunchpadTestRequest(form=form)
+ self.assertFalse(self.widget.hasValidInput())
+
+ def test_getInputValue_personal(self):
+ # The field value is None when the personal radio button is
+ # selected.
+ form = self.form
+ form["field.target"] = "personal"
+ self.context.owner = self.factory.makePerson()
+ self.widget.request = LaunchpadTestRequest(form=form)
+ self.assertIsNone(self.widget.getInputValue())
+
+ def test_getInputValue_package_spn(self):
+ # The field value is the package when the package radio button
+ # is selected and the package sub field has official spn.
+ form = self.form
+ form["field.target"] = "package"
+ self.widget.request = LaunchpadTestRequest(form=form)
+ self.assertEqual(self.package, self.widget.getInputValue())
+
+ def test_getInputValue_package_invalid(self):
+ # An error is raised when the package is not published in the distro.
+ form = self.form
+ form["field.target"] = "package"
+ form["field.target.package"] = 'non-existent'
+ self.widget.request = LaunchpadTestRequest(form=form)
+ message = (
+ "There is no package named 'non-existent' published in Fnord.")
+ e = self.assertRaises(WidgetInputError, self.widget.getInputValue)
+ self.assertEqual(LaunchpadValidationError(message), e.errors)
+ self.assertEqual(html_escape(message), self.widget.error())
+
+ def test_getInputValue_distribution(self):
+ # An error is raised when the package radio button is selected and
+ # the package sub field is empty.
+ form = self.form
+ form["field.target"] = "package"
+ form["field.target.package"] = ''
+ self.widget.request = LaunchpadTestRequest(form=form)
+ message = "Please enter a package name"
+ e = self.assertRaises(WidgetInputError, self.widget.getInputValue)
+ self.assertEqual(LaunchpadValidationError(message), e.errors)
+ self.assertEqual(message, self.widget.error())
+
+ def test_getInputValue_distribution_invalid(self):
+ # An error is raised when the distribution is invalid.
+ form = self.form
+ form["field.target"] = "package"
+ form["field.target.package"] = ''
+ form["field.target.distribution"] = 'non-existent'
+ self.widget.request = LaunchpadTestRequest(form=form)
+ message = (
+ "There is no distribution named 'non-existent' registered in "
+ "Launchpad")
+ e = self.assertRaises(WidgetInputError, self.widget.getInputValue)
+ self.assertEqual(LaunchpadValidationError(message), e.errors)
+ self.assertEqual(html_escape(message), self.widget.error())
+
+ def test_getInputValue_project(self):
+ # The field value is the project when the project radio button is
+ # selected and the project sub field is valid.
+ form = self.form
+ form["field.target"] = "project"
+ self.widget.request = LaunchpadTestRequest(form=form)
+ self.assertEqual(self.project, self.widget.getInputValue())
+
+ def test_getInputValue_project_missing(self):
+ # An error is raised when the project field is missing.
+ form = self.form
+ form["field.target"] = "project"
+ del form["field.target.project"]
+ self.widget.request = LaunchpadTestRequest(form=form)
+ message = "Please enter a project name"
+ e = self.assertRaises(WidgetInputError, self.widget.getInputValue)
+ self.assertEqual(LaunchpadValidationError(message), e.errors)
+ self.assertEqual(message, self.widget.error())
+
+ def test_getInputValue_project_invalid(self):
+ # An error is raised when the project is not valid.
+ form = self.form
+ form["field.target"] = "project"
+ form["field.target.project"] = "non-existent"
+ self.widget.request = LaunchpadTestRequest(form=form)
+ message = (
+ "There is no project named 'non-existent' registered in "
+ "Launchpad")
+ e = self.assertRaises(WidgetInputError, self.widget.getInputValue)
+ self.assertEqual(LaunchpadValidationError(message), e.errors)
+ self.assertEqual(html_escape(message), self.widget.error())
+
+ def test_setRenderedValue_personal(self):
+ # Passing a person will set the widget's render state to 'personal'.
+ self.widget.setUpSubWidgets()
+ self.widget.setRenderedValue(self.factory.makePerson())
+ self.assertEqual("personal", self.widget.default_option)
+
+ def test_setRenderedValue_package(self):
+ # Passing a package will set the widget's render state to 'package'.
+ self.widget.setUpSubWidgets()
+ self.widget.setRenderedValue(self.package)
+ self.assertEqual("package", self.widget.default_option)
+ self.assertEqual(
+ self.distribution,
+ self.widget.distribution_widget._getCurrentValue())
+ self.assertEqual(
+ self.package.sourcepackagename,
+ self.widget.package_widget._getCurrentValue())
+
+ def test_setRenderedValue_project(self):
+ # Passing a project will set the widget's render state to 'project'.
+ self.widget.setUpSubWidgets()
+ self.widget.setRenderedValue(self.project)
+ self.assertEqual("project", self.widget.default_option)
+ self.assertEqual(
+ self.project, self.widget.project_widget._getCurrentValue())
+
+ def test_call(self):
+ # The __call__ method setups the widgets and the options.
+ markup = self.widget()
+ self.assertIsNotNone(self.widget.project_widget)
+ self.assertIn("personal", self.widget.options)
+ self.assertIn("package", self.widget.options)
+ expected_ids = [
+ "field.target.distribution",
+ "field.target.option.personal",
+ "field.target.option.package",
+ "field.target.option.project",
+ "field.target.package",
+ "field.target.project",
+ ]
+ soup = BeautifulSoup(markup)
+ fields = soup.findAll(["input", "select"], {"id": re.compile(".*")})
+ ids = [field["id"] for field in fields]
+ self.assertContentEqual(expected_ids, ids)
=== added file 'lib/lp/code/javascript/gitrepository.edit.js'
--- lib/lp/code/javascript/gitrepository.edit.js 1970-01-01 00:00:00 +0000
+++ lib/lp/code/javascript/gitrepository.edit.js 2015-06-05 14:00:28 +0000
@@ -0,0 +1,34 @@
+/* Copyright 2015 Canonical Ltd. This software is licensed under the
+ * GNU Affero General Public License version 3 (see the file LICENSE).
+ *
+ * Control enabling/disabling form elements on the GitRepository:+edit page.
+ *
+ * @module Y.lp.code.gitrepository.edit
+ * @requires node, DOM
+ */
+YUI.add('lp.code.gitrepository.edit', function(Y) {
+ Y.log('loading lp.code.gitrepository.edit');
+ var module = Y.namespace('lp.code.gitrepository.edit');
+
+ module.set_enabled = function(field_id, is_enabled) {
+ var field = Y.DOM.byId(field_id);
+ field.disabled = !is_enabled;
+ };
+
+ module.onclick_target = function(e) {
+ var value = false;
+ Y.all('input[name="field.target"]').each(function(node) {
+ if (node.get('checked'))
+ value = node.get('value');
+ });
+ module.set_enabled('field.target_default', value !== 'personal');
+ module.set_enabled('field.owner_default', value !== 'personal');
+ };
+
+ module.setup = function() {
+ Y.all('input[name="field.target"]').on('click', module.onclick_target);
+
+ // Set the initial state.
+ module.onclick_target();
+ };
+}, '0.1', {'requires': ['node', 'DOM']});
=== added file 'lib/lp/code/javascript/tests/test_gitrepository.edit.html'
--- lib/lp/code/javascript/tests/test_gitrepository.edit.html 1970-01-01 00:00:00 +0000
+++ lib/lp/code/javascript/tests/test_gitrepository.edit.html 2015-06-05 14:00:28 +0000
@@ -0,0 +1,167 @@
+<!DOCTYPE html>
+<!--
+Copyright 2015 Canonical Ltd. This software is licensed under the
+GNU Affero General Public License version 3 (see the file LICENSE).
+-->
+
+<html>
+ <head>
+ <title>code.gitrepository.edit Tests</title>
+
+ <!-- YUI and test setup -->
+ <script type="text/javascript"
+ src="../../../../../build/js/yui/yui/yui.js">
+ </script>
+ <link rel="stylesheet"
+ href="../../../../../build/js/yui/console/assets/console-core.css" />
+ <link rel="stylesheet"
+ href="../../../../../build/js/yui/test-console/assets/skins/sam/test-console.css" />
+ <link rel="stylesheet"
+ href="../../../../../build/js/yui/test/assets/skins/sam/test.css" />
+
+ <script type="text/javascript"
+ src="../../../../../build/js/lp/app/testing/testrunner.js"></script>
+
+ <link rel="stylesheet" href="../../../app/javascript/testing/test.css" />
+
+ <!-- Dependencies -->
+ <script type="text/javascript"
+ src="../../../../../build/js/lp/app/lp.js"></script>
+
+ <!-- The module under test. -->
+ <script type="text/javascript" src="../gitrepository.edit.js"></script>
+
+ <!-- The test suite -->
+ <script type="text/javascript" src="test_gitrepository.edit.js"></script>
+
+ <script type="text/javascript">
+ YUI().use('lp.code.gitrepository.edit', function(Y) {
+ Y.on('domready', Y.lp.code.gitrepository.edit.setup);
+ });
+ </script>
+
+ </head>
+ <body class="yui3-skin-sam">
+ <ul id="suites">
+ <li>lp.code.gitrepository.edit.test</li>
+ </ul>
+ <div id="gitrepository.edit">
+ <form action="." name="launchpadform" method="post"
+ enctype="multipart/form-data"
+ accept-charset="UTF-8">
+
+ <table class="form">
+ <tr>
+ <td colspan="2" style="text-align: left">
+ <div>
+ <label for="field.target">Target:</label>
+ <div>
+ <table>
+ <tr>
+ <td colspan="2">
+ <label>
+ <input class="radioType"
+ id="field.target.option.personal"
+ name="field.target"
+ type="radio"
+ value="personal" />
+ Personal
+ </label>
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <label>
+ <input class="radioType"
+ id="field.target.option.package"
+ name="field.target"
+ type="radio" value="package" />
+ Distribution
+ </label>
+ </td>
+ <td>
+ <select id="field.target.distribution"
+ name="field.target.distribution" size="1">
+ <option value="debian">Debian GNU/Linux</option>
+ <option value="ubuntu">Ubuntu</option>
+ </select>
+ </td>
+ </tr>
+ <tr>
+ <td align="right">
+ <label for="field.target.option.package">
+ Package
+ </label>
+ </td>
+ <td>
+ <input type="text" value="" id="field.target.package"
+ name="field.target.package" size="20" />
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <label>
+ <input class="radioType"
+ id="field.target.option.project"
+ name="field.target"
+ type="radio"
+ value="project" />
+ Project
+ </label>
+ </td>
+ <td>
+ <input type="text" value="" id="field.target.project"
+ name="field.target.project" size="20" />
+ </td>
+ </tr>
+ </table>
+ </div>
+ <p class="formHelp">The target of the repository.</p>
+ </div>
+ </td>
+ </tr>
+
+ <tr>
+ <td colspan="2">
+ <div>
+ <input class="hiddenType" id="field.target_default.used"
+ name="field.target_default.used" type="hidden"
+ value="" />
+ <input class="checkboxType" checked="checked"
+ id="field.target_default" name="field.target_default"
+ type="checkbox" value="on" />
+ <label for="field.target_default">Target default</label>
+ <p class="formHelp">
+ Whether this repository is the default for its target.
+ </p>
+ </div>
+ </td>
+ </tr>
+
+ <tr>
+ <td colspan="2">
+ <div>
+ <input class="hiddenType" id="field.owner_default.used"
+ name="field.owner_default.used" type="hidden"
+ value="" />
+ <input class="checkboxType" checked="checked"
+ id="field.owner_default" name="field.owner_default"
+ type="checkbox" value="on" />
+ <label for="field.owner_default">Owner default</label>
+ <p class="formHelp">
+ Whether this repository is the default for its owner.
+ </p>
+ </div>
+ </td>
+ </tr>
+ </table>
+
+ <input type="submit" id="field.actions.change"
+ name="field.actions.change" value="Change Git Repository"
+ class="button" />
+ or
+ <a href="https://code.launchpad.dev/~me/p/+git/r">Cancel</a>
+ </form>
+ </div>
+ </body>
+</html>
=== added file 'lib/lp/code/javascript/tests/test_gitrepository.edit.js'
--- lib/lp/code/javascript/tests/test_gitrepository.edit.js 1970-01-01 00:00:00 +0000
+++ lib/lp/code/javascript/tests/test_gitrepository.edit.js 2015-06-05 14:00:28 +0000
@@ -0,0 +1,84 @@
+/* Copyright 2015 Canonical Ltd. This software is licensed under the
+ * GNU Affero General Public License version 3 (see the file LICENSE).
+ *
+ * Test driver for gitrepository.edit.js.
+ */
+YUI.add('lp.code.gitrepository.edit.test', function(Y) {
+ var tests = Y.namespace('lp.code.gitrepository.edit.test');
+ var module = Y.lp.code.gitrepository.edit;
+ tests.suite = new Y.Test.Suite('code.gitrepository.edit Tests');
+
+ tests.suite.add(new Y.Test.Case({
+ name: 'code.gitrepository.edit_tests',
+
+ setUp: function() {
+ this.tbody = Y.one('#gitrepository.edit');
+
+ // Get the individual target type radio buttons.
+ this.target_personal = Y.DOM.byId('field.target.option.personal');
+ this.target_package = Y.DOM.byId('field.target.option.package');
+ this.target_project = Y.DOM.byId('field.target.option.project');
+
+ // Get the input widgets.
+ this.target_default = Y.DOM.byId('field.target_default');
+ this.owner_default = Y.DOM.byId('field.owner_default');
+ },
+
+ tearDown: function() {
+ delete this.tbody;
+ },
+
+ test_handlers_connected: function() {
+ // Manually invoke the setup function to ensure the handlers are
+ // set.
+ module.setup();
+
+ var check_handler = function(field, expected) {
+ var custom_events = Y.Event.getListeners(field, 'click');
+ var click_event = custom_events[0];
+ var subscribers = click_event.subscribers;
+ Y.each(subscribers, function(sub) {
+ Y.Assert.isTrue(sub.contains(expected),
+ 'handler not set up');
+ });
+ };
+
+ check_handler(this.target_personal, module.onclick_target);
+ check_handler(this.target_package, module.onclick_target);
+ check_handler(this.target_project, module.onclick_target);
+ },
+
+ test_select_target_personal: function() {
+ this.target_personal.checked = true;
+ module.onclick_target();
+ // The target_default and owner_default checkboxes are disabled.
+ Y.Assert.isTrue(this.target_default.disabled,
+ 'target_default not disabled');
+ Y.Assert.isTrue(this.owner_default.disabled,
+ 'owner_default not disabled');
+ },
+
+ test_select_target_package: function() {
+ this.target_package.checked = true;
+ module.onclick_target();
+ // The target_default and owner_default checkboxes are enabled.
+ Y.Assert.isFalse(this.target_default.disabled,
+ 'target_default not disabled');
+ Y.Assert.isFalse(this.owner_default.disabled,
+ 'owner_default not disabled');
+ },
+
+ test_select_target_project: function() {
+ this.target_project.checked = true;
+ module.onclick_target();
+ // The target_default and owner_default checkboxes are enabled.
+ Y.Assert.isFalse(this.target_default.disabled,
+ 'target_default not disabled');
+ Y.Assert.isFalse(this.owner_default.disabled,
+ 'owner_default not disabled');
+ },
+ }));
+}, '0.1', {
+ requires: ['lp.testing.runner', 'test', 'test-console', 'Event',
+ 'lp.code.gitrepository.edit']
+});
=== added file 'lib/lp/code/templates/gitrepository-edit.pt'
--- lib/lp/code/templates/gitrepository-edit.pt 1970-01-01 00:00:00 +0000
+++ lib/lp/code/templates/gitrepository-edit.pt 2015-06-05 14:00:28 +0000
@@ -0,0 +1,23 @@
+<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">
+ <div metal:use-macro="context/@@launchpad_form/form" />
+
+ <script type="text/javascript">
+ LPJS.use('lp.code.gitrepository.edit', function(Y) {
+ Y.on('domready', function(e) {
+ Y.lp.code.gitrepository.edit.setup();
+ }, window);
+ });
+ </script>
+</div>
+
+</body>
+</html>
Follow ups