Merge lp:~cjwatson/launchpad/git-repository-ui-edit-target into lp:launchpad

Proposed by Colin Watson
Status: Merged
Merged at revision: 17574
Proposed branch: lp:~cjwatson/launchpad/git-repository-ui-edit-target
Merge into: lp:launchpad
Prerequisite: lp:~cjwatson/launchpad/git-repository-ui-edit-owner
Diff against target: 1467 lines (+1306/-8)
10 files modified
lib/lp/code/browser/configure.zcml (+1/-1)
lib/lp/code/browser/gitrepository.py (+113/-6)
lib/lp/code/browser/tests/test_gitrepository.py (+212/-1)
lib/lp/code/browser/widgets/gitrepositorytarget.py (+223/-0)
lib/lp/code/browser/widgets/templates/gitrepository-target.pt (+50/-0)
lib/lp/code/browser/widgets/tests/test_gitrepositorytargetwidget.py (+391/-0)
lib/lp/code/javascript/gitrepository.edit.js (+43/-0)
lib/lp/code/javascript/tests/test_gitrepository.edit.html (+150/-0)
lib/lp/code/javascript/tests/test_gitrepository.edit.js (+100/-0)
lib/lp/code/templates/gitrepository-edit.pt (+23/-0)
To merge this branch: bzr merge lp:~cjwatson/launchpad/git-repository-ui-edit-target
Reviewer Review Type Date Requested Status
William Grant code Approve
Review via email: mp+261232@code.launchpad.net

Commit message

Teach GitRepository:+edit how to change target, target_default, and owner_default.

Description of the change

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.

To post a comment you must log in.
Revision history for this message
William Grant (wgrant) wrote :

target_default and owner_default are a bit weird to show on a form like this. target_default can only be set by a few people, and there will be project-level UI for that. I don't know where the personal default flag should be set, but it makes a little more sense on this form as it can be used by anyone who can edit the repo.

There's also the issue that they only make sense for non-personal targets, though the JS makes that semi-clear by disabling them.

I'd consider replacing the ancient onKeyPress inline JS with proper stuff.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/code/browser/configure.zcml'
2--- lib/lp/code/browser/configure.zcml 2015-06-12 08:08:00 +0000
3+++ lib/lp/code/browser/configure.zcml 2015-06-18 20:19:28 +0000
4@@ -796,7 +796,7 @@
5 class="lp.code.browser.gitrepository.GitRepositoryEditView"
6 permission="launchpad.Edit"
7 name="+edit"
8- template="../../app/templates/generic-edit.pt"/>
9+ template="../templates/gitrepository-edit.pt"/>
10 <browser:page
11 for="lp.code.interfaces.gitrepository.IGitRepository"
12 class="lp.code.browser.gitrepository.GitRepositoryDeletionView"
13
14=== modified file 'lib/lp/code/browser/gitrepository.py'
15--- lib/lp/code/browser/gitrepository.py 2015-06-13 01:45:19 +0000
16+++ lib/lp/code/browser/gitrepository.py 2015-06-18 20:19:28 +0000
17@@ -27,11 +27,17 @@
18 )
19 from storm.expr import Desc
20 from zope.event import notify
21+from zope.formlib import form
22 from zope.interface import (
23 implements,
24 Interface,
25 providedBy,
26 )
27+from zope.schema import Choice
28+from zope.schema.vocabulary import (
29+ SimpleTerm,
30+ SimpleVocabulary,
31+ )
32
33 from lp import _
34 from lp.app.browser.informationtype import InformationTypePortletMixin
35@@ -45,13 +51,21 @@
36 from lp.app.vocabularies import InformationTypeVocabulary
37 from lp.app.widgets.itemswidgets import LaunchpadRadioWidgetWithDescription
38 from lp.code.browser.branch import CodeEditOwnerMixin
39+from lp.code.browser.widgets.gitrepositorytarget import (
40+ GitRepositoryTargetDisplayWidget,
41+ GitRepositoryTargetWidget,
42+ )
43 from lp.code.errors import (
44+ GitDefaultConflict,
45 GitRepositoryCreationForbidden,
46 GitRepositoryExists,
47+ GitTargetError,
48 )
49 from lp.code.interfaces.gitnamespace import get_git_namespace
50 from lp.code.interfaces.gitref import IGitRefBatchNavigator
51 from lp.code.interfaces.gitrepository import IGitRepository
52+from lp.registry.interfaces.person import IPerson
53+from lp.registry.vocabularies import UserTeamsParticipationPlusSelfVocabulary
54 from lp.services.config import config
55 from lp.services.database.constants import UTC_NOW
56 from lp.services.propertycache import cachedproperty
57@@ -275,7 +289,10 @@
58 vocabulary=InformationTypeVocabulary(types=info_types))
59 name = copy_field(IGitRepository["name"], readonly=False)
60 owner = copy_field(IGitRepository["owner"], readonly=False)
61+ owner_default = copy_field(
62+ IGitRepository["owner_default"], readonly=False)
63 reviewer = copy_field(IGitRepository["reviewer"], required=True)
64+ target = copy_field(IGitRepository["target"], readonly=False)
65
66 return GitRepositoryEditSchema
67
68@@ -320,6 +337,25 @@
69 information_type = data.pop("information_type")
70 self.context.transitionToInformationType(
71 information_type, self.user)
72+ if "target" in data:
73+ target = data.pop("target")
74+ if target is None:
75+ target = self.context.owner
76+ if target != self.context.target:
77+ try:
78+ self.context.setTarget(target, self.user)
79+ except GitTargetError as e:
80+ self.setFieldError("target", e.message)
81+ return
82+ changed = True
83+ if IPerson.providedBy(target):
84+ self.request.response.addNotification(
85+ "This repository is now a personal repository for %s "
86+ "(%s)" % (target.displayname, target.name))
87+ else:
88+ self.request.response.addNotification(
89+ "The repository target has been changed to %s (%s)" %
90+ (target.displayname, target.name))
91 if "reviewer" in data:
92 reviewer = data.pop("reviewer")
93 if reviewer != self.context.code_reviewer:
94@@ -329,6 +365,11 @@
95 else:
96 self.context.reviewer = reviewer
97 changed = True
98+ if "owner_default" in data:
99+ owner_default = data.pop("owner_default")
100+ if (self.context.namespace.has_defaults and
101+ owner_default != self.context.owner_default):
102+ self.context.setOwnerDefault(owner_default)
103
104 if self.updateContextFromData(data, notify_modified=False):
105 changed = True
106@@ -381,7 +422,9 @@
107 field_names = [
108 "owner",
109 "name",
110+ "target",
111 "information_type",
112+ "owner_default",
113 "default_branch",
114 ]
115
116@@ -391,6 +434,61 @@
117 "As an administrator you are able to assign this repository to any "
118 "person or team.")
119
120+ def setUpFields(self):
121+ super(GitRepositoryEditView, self).setUpFields()
122+ repository = self.context
123+ # If the user can administer repositories, then they should be able
124+ # to assign the ownership of the repository to any valid person or
125+ # team.
126+ if check_permission("launchpad.Admin", repository):
127+ owner_field = self.schema["owner"]
128+ any_owner_choice = Choice(
129+ __name__="owner", title=owner_field.title,
130+ description=_(
131+ "As an administrator you are able to assign this "
132+ "repository to any person or team."),
133+ required=True, vocabulary="ValidPersonOrTeam")
134+ any_owner_field = form.Fields(
135+ any_owner_choice, render_context=self.render_context)
136+ # Replace the normal owner field with a more permissive vocab.
137+ self.form_fields = self.form_fields.omit("owner")
138+ self.form_fields = any_owner_field + self.form_fields
139+ else:
140+ # For normal users, there are some cases (e.g. package
141+ # repositories) where the editor may not be in the team of the
142+ # repository owner. In these cases we need to extend the
143+ # vocabulary connected to the owner field.
144+ if not self.user.inTeam(self.context.owner):
145+ vocab = UserTeamsParticipationPlusSelfVocabulary()
146+ owner = self.context.owner
147+ terms = [SimpleTerm(
148+ owner, owner.name, owner.unique_displayname)]
149+ terms.extend([term for term in vocab])
150+ owner_field = self.schema["owner"]
151+ owner_choice = Choice(
152+ __name__="owner", title=owner_field.title,
153+ description=owner_field.description,
154+ required=True, vocabulary=SimpleVocabulary(terms))
155+ new_owner_field = form.Fields(
156+ owner_choice, render_context=self.render_context)
157+ # Replace the normal owner field with a more permissive vocab.
158+ self.form_fields = self.form_fields.omit("owner")
159+ self.form_fields = new_owner_field + self.form_fields
160+ # If this is the target default, then the target is read-only.
161+ target_field = self.form_fields.get("target")
162+ if self.context.target_default:
163+ target_field.for_display = True
164+ target_field.custom_widget = GitRepositoryTargetDisplayWidget
165+ else:
166+ target_field.custom_widget = GitRepositoryTargetWidget
167+
168+ def setUpWidgets(self, context=None):
169+ super(GitRepositoryEditView, self).setUpWidgets(context=context)
170+ if self.context.target_default:
171+ self.widgets["target"].hint = (
172+ "This is the default repository for this target, so it "
173+ "cannot be moved to another target.")
174+
175 def _setRepositoryExists(self, existing_repository, field_name="name"):
176 owner = existing_repository.owner
177 if owner == self.user:
178@@ -404,14 +502,15 @@
179 self.setFieldError(field_name, message)
180
181 def validate(self, data):
182- if "name" in data and "owner" in data:
183+ if "name" in data and "owner" in data and "target" in data:
184 name = data["name"]
185 owner = data["owner"]
186- if name != self.context.name or owner != self.context.owner:
187- if self.context.owner == self.context.target:
188- target = owner
189- else:
190- target = self.context.target
191+ target = data["target"]
192+ if target is None:
193+ target = owner
194+ if (name != self.context.name or
195+ owner != self.context.owner or
196+ target != self.context.target):
197 namespace = get_git_namespace(target, owner)
198 try:
199 namespace.validateMove(self.context, self.user, name=name)
200@@ -421,6 +520,14 @@
201 (owner.displayname, target.displayname))
202 except GitRepositoryExists as e:
203 self._setRepositoryExists(e.existing_repository)
204+ except GitDefaultConflict as e:
205+ self.addError(str(e))
206+ if (self.context.target_default and "target" in data and
207+ data["target"] != self.context.target):
208+ self.setFieldError(
209+ "target",
210+ "The default repository for a target cannot be moved to "
211+ "another target.")
212 if "default_branch" in data:
213 default_branch = data["default_branch"]
214 if (default_branch is not None and
215
216=== modified file 'lib/lp/code/browser/tests/test_gitrepository.py'
217--- lib/lp/code/browser/tests/test_gitrepository.py 2015-06-13 01:45:19 +0000
218+++ lib/lp/code/browser/tests/test_gitrepository.py 2015-06-18 20:19:28 +0000
219@@ -17,6 +17,7 @@
220 )
221 import transaction
222 from zope.component import getUtility
223+from zope.formlib.itemswidgets import ItemDisplayWidget
224 from zope.publisher.interfaces import NotFound
225 from zope.security.proxy import removeSecurityProxy
226
227@@ -42,7 +43,10 @@
228 from lp.testing.fakemethod import FakeMethod
229 from lp.testing.fixture import ZopeUtilityFixture
230 from lp.testing.layers import DatabaseFunctionalLayer
231-from lp.testing.matchers import HasQueryCount
232+from lp.testing.matchers import (
233+ Contains,
234+ HasQueryCount,
235+ )
236 from lp.testing.pages import (
237 get_feedback_messages,
238 setupBrowser,
239@@ -259,6 +263,213 @@
240
241 layer = DatabaseFunctionalLayer
242
243+ def test_repository_target_widget_read_only(self):
244+ # The repository target widget is read-only if the repository is the
245+ # default for its target.
246+ person = self.factory.makePerson()
247+ project = self.factory.makeProduct(owner=person)
248+ repository = self.factory.makeGitRepository(
249+ owner=person, target=project)
250+ login_person(person)
251+ repository.setTargetDefault(True)
252+ view = create_initialized_view(repository, name="+edit")
253+ self.assertEqual("project", view.widgets["target"].default_option)
254+ self.assertIsInstance(
255+ view.widgets["target"].project_widget, ItemDisplayWidget)
256+ self.assertEqual(
257+ project.title, view.widgets["target"].project_widget())
258+
259+ def test_repository_target_widget_renders_personal(self):
260+ # The repository target widget renders correctly for a personal
261+ # repository.
262+ person = self.factory.makePerson()
263+ repository = self.factory.makeGitRepository(
264+ owner=person, target=person)
265+ login_person(person)
266+ view = create_initialized_view(repository, name="+edit")
267+ self.assertEqual("personal", view.widgets["target"].default_option)
268+
269+ def test_repository_target_widget_renders_product(self):
270+ # The repository target widget renders correctly for a product
271+ # repository.
272+ person = self.factory.makePerson()
273+ project = self.factory.makeProduct()
274+ repository = self.factory.makeGitRepository(
275+ owner=person, target=project)
276+ login_person(person)
277+ view = create_initialized_view(repository, name="+edit")
278+ self.assertEqual("project", view.widgets["target"].default_option)
279+ self.assertEqual(
280+ project.name, view.widgets["target"].project_widget.selected_value)
281+
282+ def test_repository_target_widget_renders_package(self):
283+ # The repository target widget renders correctly for a package
284+ # repository.
285+ person = self.factory.makePerson()
286+ dsp = self.factory.makeDistributionSourcePackage()
287+ repository = self.factory.makeGitRepository(owner=person, target=dsp)
288+ login_person(person)
289+ view = create_initialized_view(repository, name="+edit")
290+ self.assertEqual("package", view.widgets["target"].default_option)
291+ self.assertEqual(
292+ dsp.distribution,
293+ view.widgets["target"].distribution_widget._getFormValue())
294+ self.assertEqual(
295+ dsp.sourcepackagename.name,
296+ view.widgets["target"].package_widget.selected_value)
297+
298+ def test_repository_target_widget_saves_personal(self):
299+ # The repository target widget can retarget to a personal
300+ # repository.
301+ person = self.factory.makePerson()
302+ repository = self.factory.makeGitRepository(owner=person)
303+ login_person(person)
304+ form = {
305+ "field.target": "personal",
306+ "field.actions.change": "Change Git Repository",
307+ }
308+ view = create_initialized_view(repository, name="+edit", form=form)
309+ self.assertEqual(person, repository.target)
310+ self.assertEqual(1, len(view.request.response.notifications))
311+ self.assertEqual(
312+ "This repository is now a personal repository for %s (%s)"
313+ % (person.displayname, person.name),
314+ view.request.response.notifications[0].message)
315+
316+ def test_repository_target_widget_saves_personal_different_owner(self):
317+ # The repository target widget can retarget to a personal repository
318+ # for a different owner.
319+ person = self.factory.makePerson()
320+ repository = self.factory.makeGitRepository(
321+ owner=person, target=person)
322+ new_owner = self.factory.makeTeam(name="newowner", members=[person])
323+ login_person(person)
324+ form = {
325+ "field.target": "personal",
326+ "field.owner": "newowner",
327+ "field.actions.change": "Change Git Repository",
328+ }
329+ view = create_initialized_view(repository, name="+edit", form=form)
330+ self.assertEqual(new_owner, repository.target)
331+ self.assertEqual(1, len(view.request.response.notifications))
332+ self.assertEqual(
333+ "The repository owner has been changed to Newowner (newowner)",
334+ view.request.response.notifications[0].message)
335+
336+ def test_repository_target_widget_saves_personal_clears_default(self):
337+ # When retargeting to a personal repository, the owner-target
338+ # default flag is cleared.
339+ person = self.factory.makePerson()
340+ project = self.factory.makeProduct(owner=person)
341+ repository = self.factory.makeGitRepository(
342+ owner=person, target=project)
343+ login_person(person)
344+ repository.setOwnerDefault(True)
345+ form = {
346+ "field.target": "personal",
347+ "field.actions.change": "Change Git Repository",
348+ }
349+ view = create_initialized_view(repository, name="+edit", form=form)
350+ self.assertEqual([], view.errors)
351+ self.assertEqual(person, repository.target)
352+ self.assertFalse(repository.owner_default)
353+ self.assertEqual(1, len(view.request.response.notifications))
354+ self.assertEqual(
355+ "This repository is now a personal repository for %s (%s)"
356+ % (person.displayname, person.name),
357+ view.request.response.notifications[0].message)
358+
359+ def test_repository_target_widget_saves_project(self):
360+ # The repository target widget can retarget to a project repository.
361+ person = self.factory.makePerson()
362+ repository = self.factory.makeGitRepository(
363+ owner=person, target=person)
364+ project = self.factory.makeProduct()
365+ login_person(person)
366+ form = {
367+ "field.target": "project",
368+ "field.target.project": project.name,
369+ "field.actions.change": "Change Git Repository",
370+ }
371+ view = create_initialized_view(repository, name="+edit", form=form)
372+ self.assertEqual(project, repository.target)
373+ self.assertEqual(
374+ "The repository target has been changed to %s (%s)"
375+ % (project.displayname, project.name),
376+ view.request.response.notifications[0].message)
377+
378+ def test_repository_target_widget_saves_package(self):
379+ # The repository target widget can retarget to a package repository.
380+ person = self.factory.makePerson()
381+ repository = self.factory.makeGitRepository(
382+ owner=person, target=person)
383+ dsp = self.factory.makeDistributionSourcePackage()
384+ self.factory.makeSourcePackagePublishingHistory(
385+ distroseries=dsp.distribution.currentseries,
386+ sourcepackagename=dsp.sourcepackagename,
387+ archive=dsp.distribution.main_archive)
388+ login_person(person)
389+ form = {
390+ "field.target": "package",
391+ "field.target.distribution": dsp.distribution.name,
392+ "field.target.package": dsp.sourcepackagename.name,
393+ "field.actions.change": "Change Git Repository",
394+ }
395+ view = create_initialized_view(repository, name="+edit", form=form)
396+ self.assertEqual(dsp, repository.target)
397+ self.assertEqual(
398+ "The repository target has been changed to %s (%s)"
399+ % (dsp.displayname, dsp.name),
400+ view.request.response.notifications[0].message)
401+
402+ def test_forbidden_target_is_error(self):
403+ # An error is displayed if a repository is saved with a target that
404+ # is not allowed by the sharing policy.
405+ owner = self.factory.makePerson()
406+ initial_target = self.factory.makeProduct()
407+ self.factory.makeProduct(
408+ name="commercial", owner=owner,
409+ branch_sharing_policy=BranchSharingPolicy.PROPRIETARY)
410+ repository = self.factory.makeGitRepository(
411+ owner=owner, target=initial_target,
412+ information_type=InformationType.PUBLIC)
413+ browser = self.getUserBrowser(
414+ canonical_url(repository) + "/+edit", user=owner)
415+ browser.getControl(name="field.target.project").value = "commercial"
416+ browser.getControl("Change Git Repository").click()
417+ self.assertThat(
418+ browser.contents,
419+ Contains(
420+ "Public repositories are not allowed for target Commercial."))
421+ with person_logged_in(owner):
422+ self.assertEqual(initial_target, repository.target)
423+
424+ def test_default_conflict_is_error(self):
425+ # An error is displayed if an owner-default repository is saved with
426+ # a new target that already has an owner-default repository.
427+ owner = self.factory.makePerson()
428+ initial_target = self.factory.makeProduct()
429+ new_target = self.factory.makeProduct(name="new", displayname="New")
430+ repository = self.factory.makeGitRepository(
431+ owner=owner, target=initial_target)
432+ existing_default = self.factory.makeGitRepository(
433+ owner=owner, target=new_target)
434+ login_person(owner)
435+ repository.setOwnerDefault(True)
436+ existing_default.setOwnerDefault(True)
437+ browser = self.getUserBrowser(
438+ canonical_url(repository) + "/+edit", user=owner)
439+ browser.getControl(name="field.target.project").value = "new"
440+ browser.getControl("Change Git Repository").click()
441+ with person_logged_in(owner):
442+ self.assertThat(
443+ browser.contents,
444+ Contains(
445+ "%s&#x27;s default repository for &#x27;New&#x27; is "
446+ "already set to %s." %
447+ (owner.displayname, existing_default.unique_name)))
448+ self.assertEqual(initial_target, repository.target)
449+
450 def test_rename(self):
451 # The name of a repository can be changed via the UI by an
452 # authorised user.
453
454=== added file 'lib/lp/code/browser/widgets/gitrepositorytarget.py'
455--- lib/lp/code/browser/widgets/gitrepositorytarget.py 1970-01-01 00:00:00 +0000
456+++ lib/lp/code/browser/widgets/gitrepositorytarget.py 2015-06-18 20:19:28 +0000
457@@ -0,0 +1,223 @@
458+# Copyright 2015 Canonical Ltd. This software is licensed under the
459+# GNU Affero General Public License version 3 (see the file LICENSE).
460+
461+__metaclass__ = type
462+
463+__all__ = [
464+ 'GitRepositoryTargetDisplayWidget',
465+ 'GitRepositoryTargetWidget',
466+ ]
467+
468+from z3c.ptcompat import ViewPageTemplateFile
469+from zope.component import getUtility
470+from zope.formlib.interfaces import (
471+ ConversionError,
472+ IDisplayWidget,
473+ IInputWidget,
474+ InputErrors,
475+ MissingInputError,
476+ WidgetInputError,
477+ )
478+from zope.formlib.utility import setUpWidget
479+from zope.formlib.widget import (
480+ BrowserWidget,
481+ CustomWidgetFactory,
482+ DisplayWidget,
483+ InputWidget,
484+ renderElement,
485+ )
486+from zope.interface import implements
487+from zope.schema import Choice
488+
489+from lp.app.errors import (
490+ NotFoundError,
491+ UnexpectedFormData,
492+ )
493+from lp.app.interfaces.launchpad import ILaunchpadCelebrities
494+from lp.app.validators import LaunchpadValidationError
495+from lp.app.widgets.itemswidgets import LaunchpadDropdownWidget
496+from lp.registry.interfaces.distributionsourcepackage import (
497+ IDistributionSourcePackage,
498+ )
499+from lp.registry.interfaces.person import IPerson
500+from lp.registry.interfaces.product import IProduct
501+from lp.services.webapp.interfaces import (
502+ IAlwaysSubmittedWidget,
503+ IMultiLineWidgetLayout,
504+ )
505+
506+
507+class GitRepositoryTargetWidgetBase(BrowserWidget):
508+
509+ implements(IMultiLineWidgetLayout)
510+
511+ template = ViewPageTemplateFile("templates/gitrepository-target.pt")
512+ default_option = "project"
513+ _widgets_set_up = False
514+
515+ def setUpSubWidgets(self):
516+ if self._widgets_set_up:
517+ return
518+ fields = [
519+ Choice(
520+ __name__="project", title=u"Project",
521+ required=True, vocabulary="Product"),
522+ Choice(
523+ __name__="distribution", title=u"Distribution",
524+ required=True, vocabulary="Distribution",
525+ default=getUtility(ILaunchpadCelebrities).ubuntu),
526+ Choice(
527+ __name__="package", title=u"Package",
528+ required=False, vocabulary="BinaryAndSourcePackageName"),
529+ ]
530+ if not self._read_only:
531+ self.distribution_widget = CustomWidgetFactory(
532+ LaunchpadDropdownWidget)
533+ for field in fields:
534+ setUpWidget(
535+ self, field.__name__, field, self._sub_widget_interface,
536+ prefix=self.name)
537+ self._widgets_set_up = True
538+
539+ def setUpOptions(self):
540+ """Set up options to be rendered."""
541+ self.options = {}
542+ for option in ["personal", "package", "project"]:
543+ attributes = dict(
544+ type="radio", name=self.name, value=option,
545+ id="%s.option.%s" % (self.name, option))
546+ if self.request.form_ng.getOne(
547+ self.name, self.default_option) == option:
548+ attributes["checked"] = "checked"
549+ if self._read_only:
550+ attributes["disabled"] = "disabled"
551+ self.options[option] = renderElement("input", **attributes)
552+
553+ @property
554+ def show_options(self):
555+ return {
556+ option: not self._read_only or self.default_option == option
557+ for option in ["personal", "package", "project"]}
558+
559+ def setRenderedValue(self, value):
560+ """See `IWidget`."""
561+ self.setUpSubWidgets()
562+ if value is None or IPerson.providedBy(value):
563+ self.default_option = "personal"
564+ return
565+ elif IProduct.providedBy(value):
566+ self.default_option = "project"
567+ self.project_widget.setRenderedValue(value)
568+ return
569+ elif IDistributionSourcePackage.providedBy(value):
570+ self.default_option = "package"
571+ self.distribution_widget.setRenderedValue(value.distribution)
572+ self.package_widget.setRenderedValue(value.sourcepackagename)
573+ else:
574+ raise AssertionError("Not a valid value: %r" % value)
575+
576+ def __call__(self):
577+ """See `zope.formlib.interfaces.IBrowserWidget`."""
578+ self.setUpSubWidgets()
579+ self.setUpOptions()
580+ return self.template()
581+
582+
583+class GitRepositoryTargetDisplayWidget(
584+ GitRepositoryTargetWidgetBase, DisplayWidget):
585+ """Widget for displaying a Git repository target."""
586+
587+ implements(IDisplayWidget)
588+
589+ _sub_widget_interface = IDisplayWidget
590+ _read_only = True
591+
592+
593+class GitRepositoryTargetWidget(GitRepositoryTargetWidgetBase, InputWidget):
594+ """Widget for selecting a Git repository target."""
595+
596+ implements(IAlwaysSubmittedWidget, IInputWidget)
597+
598+ _sub_widget_interface = IInputWidget
599+ _read_only = False
600+ _widgets_set_up = False
601+
602+ def hasInput(self):
603+ return self.name in self.request.form
604+
605+ def hasValidInput(self):
606+ """See `zope.formlib.interfaces.IInputWidget`."""
607+ try:
608+ self.getInputValue()
609+ return True
610+ except (InputErrors, UnexpectedFormData):
611+ return False
612+
613+ def getInputValue(self):
614+ """See `zope.formlib.interfaces.IInputWidget`."""
615+ self.setUpSubWidgets()
616+ form_value = self.request.form_ng.getOne(self.name)
617+ if form_value == "project":
618+ try:
619+ return self.project_widget.getInputValue()
620+ except MissingInputError:
621+ raise WidgetInputError(
622+ self.name, self.label,
623+ LaunchpadValidationError("Please enter a project name"))
624+ except ConversionError:
625+ entered_name = self.request.form_ng.getOne(
626+ "%s.project" % self.name)
627+ raise WidgetInputError(
628+ self.name, self.label,
629+ LaunchpadValidationError(
630+ "There is no project named '%s' registered in "
631+ "Launchpad" % entered_name))
632+ elif form_value == "package":
633+ try:
634+ distribution = self.distribution_widget.getInputValue()
635+ except ConversionError:
636+ entered_name = self.request.form_ng.getOne(
637+ "%s.distribution" % self.name)
638+ raise WidgetInputError(
639+ self.name, self.label,
640+ LaunchpadValidationError(
641+ "There is no distribution named '%s' registered in "
642+ "Launchpad" % entered_name))
643+ try:
644+ if self.package_widget.hasInput():
645+ package_name = self.package_widget.getInputValue()
646+ else:
647+ package_name = None
648+ if package_name is None:
649+ raise WidgetInputError(
650+ self.name, self.label,
651+ LaunchpadValidationError(
652+ "Please enter a package name"))
653+ if IDistributionSourcePackage.providedBy(package_name):
654+ dsp = package_name
655+ else:
656+ source_name = distribution.guessPublishedSourcePackageName(
657+ package_name.name)
658+ dsp = distribution.getSourcePackage(source_name)
659+ except (ConversionError, NotFoundError):
660+ entered_name = self.request.form_ng.getOne(
661+ "%s.package" % self.name)
662+ raise WidgetInputError(
663+ self.name, self.label,
664+ LaunchpadValidationError(
665+ "There is no package named '%s' published in %s." %
666+ (entered_name, distribution.displayname)))
667+ return dsp
668+ elif form_value == "personal":
669+ return None
670+ else:
671+ raise UnexpectedFormData("No valid option was selected.")
672+
673+ def error(self):
674+ """See `zope.formlib.interfaces.IBrowserWidget`."""
675+ try:
676+ if self.hasInput():
677+ self.getInputValue()
678+ except InputErrors as error:
679+ self._error = error
680+ return super(GitRepositoryTargetWidget, self).error()
681
682=== added file 'lib/lp/code/browser/widgets/templates/gitrepository-target.pt'
683--- lib/lp/code/browser/widgets/templates/gitrepository-target.pt 1970-01-01 00:00:00 +0000
684+++ lib/lp/code/browser/widgets/templates/gitrepository-target.pt 2015-06-18 20:19:28 +0000
685@@ -0,0 +1,50 @@
686+<table>
687+ <tr tal:condition="view/show_options/personal">
688+ <td colspan="2">
689+ <label>
690+ <input
691+ type="radio" value="personal"
692+ tal:replace="structure view/options/personal" />
693+ Personal
694+ </label>
695+ </td>
696+ </tr>
697+
698+ <tr tal:condition="view/show_options/package">
699+ <td>
700+ <label>
701+ <input
702+ type="radio" value="package"
703+ tal:replace="structure view/options/package" />
704+ Distribution
705+ </label>
706+ </td>
707+ <td>
708+ <tal:distribution tal:replace="structure view/distribution_widget" />
709+ </td>
710+ </tr>
711+ <tr tal:condition="view/show_options/package">
712+ <td align="right">
713+ <label tal:attributes="for string:${view/name}.option.package">
714+ Package
715+ </label>
716+ </td>
717+ <td>
718+ <tal:package tal:replace="structure view/package_widget" />
719+ </td>
720+ </tr>
721+
722+ <tr tal:condition="view/show_options/project">
723+ <td>
724+ <label>
725+ <input
726+ type="radio" value="project"
727+ tal:replace="structure view/options/project" />
728+ Project
729+ </label>
730+ </td>
731+ <td>
732+ <tal:product tal:replace="structure view/project_widget" />
733+ </td>
734+ </tr>
735+</table>
736
737=== added file 'lib/lp/code/browser/widgets/tests/test_gitrepositorytargetwidget.py'
738--- lib/lp/code/browser/widgets/tests/test_gitrepositorytargetwidget.py 1970-01-01 00:00:00 +0000
739+++ lib/lp/code/browser/widgets/tests/test_gitrepositorytargetwidget.py 2015-06-18 20:19:28 +0000
740@@ -0,0 +1,391 @@
741+# Copyright 2015 Canonical Ltd. This software is licensed under the
742+# GNU Affero General Public License version 3 (see the file LICENSE).
743+
744+__metaclass__ = type
745+
746+import re
747+
748+from BeautifulSoup import BeautifulSoup
749+from lazr.restful.fields import Reference
750+from zope.formlib.interfaces import (
751+ IBrowserWidget,
752+ IDisplayWidget,
753+ IInputWidget,
754+ WidgetInputError,
755+ )
756+from zope.interface import (
757+ implements,
758+ Interface,
759+ )
760+
761+from lp.app.validators import LaunchpadValidationError
762+from lp.code.browser.widgets.gitrepositorytarget import (
763+ GitRepositoryTargetDisplayWidget,
764+ GitRepositoryTargetWidget,
765+ )
766+from lp.registry.vocabularies import (
767+ DistributionVocabulary,
768+ ProductVocabulary,
769+ )
770+from lp.services.webapp.escaping import html_escape
771+from lp.services.webapp.servers import LaunchpadTestRequest
772+from lp.soyuz.model.binaryandsourcepackagename import (
773+ BinaryAndSourcePackageNameVocabulary,
774+ )
775+from lp.testing import (
776+ TestCaseWithFactory,
777+ verifyObject,
778+ )
779+from lp.testing.layers import DatabaseFunctionalLayer
780+
781+
782+class IThing(Interface):
783+ owner = Reference(schema=Interface)
784+ target = Reference(schema=Interface)
785+
786+
787+class Thing:
788+ implements(IThing)
789+ owner = None
790+ target = None
791+
792+
793+class TestGitRepositoryTargetWidgetBase:
794+
795+ layer = DatabaseFunctionalLayer
796+
797+ def setUp(self):
798+ super(TestGitRepositoryTargetWidgetBase, self).setUp()
799+ self.distribution, self.package = self.factory.makeDSPCache(
800+ distro_name="fnord", package_name="snarf")
801+ self.project = self.factory.makeProduct("pting")
802+ field = Reference(
803+ __name__="target", schema=Interface, title=u"target")
804+ self.context = Thing()
805+ field = field.bind(self.context)
806+ request = LaunchpadTestRequest()
807+ self.widget = self.widget_factory(field, request)
808+
809+ def test_implements(self):
810+ self.assertTrue(verifyObject(IBrowserWidget, self.widget))
811+ self.assertTrue(
812+ verifyObject(self.expected_widget_interface, self.widget))
813+
814+ def test_template(self):
815+ # The render template is setup.
816+ self.assertTrue(
817+ self.widget.template.filename.endswith("gitrepository-target.pt"),
818+ "Template was not setup.")
819+
820+ def test_default_option(self):
821+ # This project field is the default option.
822+ self.assertEqual("project", self.widget.default_option)
823+
824+ def test_setUpSubWidgets_first_call(self):
825+ # The subwidgets are setup and a flag is set.
826+ self.widget.setUpSubWidgets()
827+ self.assertTrue(self.widget._widgets_set_up)
828+ self.assertIsInstance(
829+ self.widget.distribution_widget.context.vocabulary,
830+ DistributionVocabulary)
831+ self.assertIsInstance(
832+ self.widget.package_widget.context.vocabulary,
833+ BinaryAndSourcePackageNameVocabulary)
834+ self.assertIsInstance(
835+ self.widget.project_widget.context.vocabulary,
836+ ProductVocabulary)
837+
838+ def test_setUpSubWidgets_second_call(self):
839+ # The setUpSubWidgets method exits early if a flag is set to
840+ # indicate that the widgets were setup.
841+ self.widget._widgets_set_up = True
842+ self.widget.setUpSubWidgets()
843+ self.assertIsNone(getattr(self.widget, "distribution_widget", None))
844+ self.assertIsNone(getattr(self.widget, "package_widget", None))
845+ self.assertIsNone(getattr(self.widget, "project_widget", None))
846+
847+ def test_setUpOptions_default_project_checked(self):
848+ # The radio button options are composed of the setup widgets with
849+ # the project widget set as the default.
850+ self.widget.setUpSubWidgets()
851+ self.widget.setUpOptions()
852+ self.assertEqual(
853+ '<input class="radioType" ' + self.expected_disabled_attr +
854+ 'id="field.target.option.personal" name="field.target" '
855+ 'type="radio" value="personal" />',
856+ self.widget.options["personal"])
857+ self.assertEqual(
858+ '<input class="radioType" ' + self.expected_disabled_attr +
859+ 'id="field.target.option.package" name="field.target" '
860+ 'type="radio" value="package" />',
861+ self.widget.options["package"])
862+ self.assertEqual(
863+ '<input class="radioType" checked="checked" ' +
864+ self.expected_disabled_attr +
865+ 'id="field.target.option.project" name="field.target" '
866+ 'type="radio" value="project" />',
867+ self.widget.options["project"])
868+
869+ def test_setUpOptions_personal_checked(self):
870+ # The personal radio button is selected when the form is submitted
871+ # when the target field's value is 'personal'.
872+ form = {
873+ "field.target": "personal",
874+ }
875+ self.widget.request = LaunchpadTestRequest(form=form)
876+ self.widget.setUpSubWidgets()
877+ self.widget.setUpOptions()
878+ self.assertEqual(
879+ '<input class="radioType" checked="checked" ' +
880+ self.expected_disabled_attr +
881+ 'id="field.target.option.personal" name="field.target" '
882+ 'type="radio" value="personal" />',
883+ self.widget.options["personal"])
884+ self.assertEqual(
885+ '<input class="radioType" ' + self.expected_disabled_attr +
886+ 'id="field.target.option.package" name="field.target" '
887+ 'type="radio" value="package" />',
888+ self.widget.options["package"])
889+ self.assertEqual(
890+ '<input class="radioType" ' + self.expected_disabled_attr +
891+ 'id="field.target.option.project" name="field.target" '
892+ 'type="radio" value="project" />',
893+ self.widget.options["project"])
894+
895+ def test_setUpOptions_package_checked(self):
896+ # The package radio button is selected when the form is submitted
897+ # when the target field's value is 'package'.
898+ form = {
899+ "field.target": "package",
900+ }
901+ self.widget.request = LaunchpadTestRequest(form=form)
902+ self.widget.setUpSubWidgets()
903+ self.widget.setUpOptions()
904+ self.assertEqual(
905+ '<input class="radioType" ' + self.expected_disabled_attr +
906+ 'id="field.target.option.personal" name="field.target" '
907+ 'type="radio" value="personal" />',
908+ self.widget.options["personal"])
909+ self.assertEqual(
910+ '<input class="radioType" checked="checked" ' +
911+ self.expected_disabled_attr +
912+ 'id="field.target.option.package" name="field.target" '
913+ 'type="radio" value="package" />',
914+ self.widget.options["package"])
915+ self.assertEqual(
916+ '<input class="radioType" ' + self.expected_disabled_attr +
917+ 'id="field.target.option.project" name="field.target" '
918+ 'type="radio" value="project" />',
919+ self.widget.options["project"])
920+
921+ def test_setUpOptions_project_checked(self):
922+ # The project radio button is selected when the form is submitted
923+ # when the target field's value is 'project'.
924+ form = {
925+ "field.target": "project",
926+ }
927+ self.widget.request = LaunchpadTestRequest(form=form)
928+ self.widget.setUpSubWidgets()
929+ self.widget.setUpOptions()
930+ self.assertEqual(
931+ '<input class="radioType" ' + self.expected_disabled_attr +
932+ 'id="field.target.option.personal" name="field.target" '
933+ 'type="radio" value="personal" />',
934+ self.widget.options["personal"])
935+ self.assertEqual(
936+ '<input class="radioType" ' + self.expected_disabled_attr +
937+ 'id="field.target.option.package" name="field.target" '
938+ 'type="radio" value="package" />',
939+ self.widget.options["package"])
940+ self.assertEqual(
941+ '<input class="radioType" checked="checked" ' +
942+ self.expected_disabled_attr +
943+ 'id="field.target.option.project" name="field.target" '
944+ 'type="radio" value="project" />',
945+ self.widget.options["project"])
946+
947+ def test_setRenderedValue_personal(self):
948+ # Passing a person will set the widget's render state to 'personal'.
949+ self.widget.setUpSubWidgets()
950+ self.widget.setRenderedValue(self.factory.makePerson())
951+ self.assertEqual("personal", self.widget.default_option)
952+
953+ def test_setRenderedValue_package(self):
954+ # Passing a package will set the widget's render state to 'package'.
955+ self.widget.setUpSubWidgets()
956+ self.widget.setRenderedValue(self.package)
957+ self.assertEqual("package", self.widget.default_option)
958+ self.assertEqual(
959+ self.distribution,
960+ self.widget.distribution_widget._getCurrentValue())
961+ self.assertEqual(
962+ self.package.sourcepackagename,
963+ self.widget.package_widget._getCurrentValue())
964+
965+ def test_setRenderedValue_project(self):
966+ # Passing a project will set the widget's render state to 'project'.
967+ self.widget.setUpSubWidgets()
968+ self.widget.setRenderedValue(self.project)
969+ self.assertEqual("project", self.widget.default_option)
970+ self.assertEqual(
971+ self.project, self.widget.project_widget._getCurrentValue())
972+
973+ def test_call(self):
974+ # The __call__ method sets up the widgets and the options.
975+ markup = self.widget()
976+ self.assertIsNotNone(self.widget.project_widget)
977+ self.assertIn("personal", self.widget.options)
978+ self.assertIn("package", self.widget.options)
979+ soup = BeautifulSoup(markup)
980+ fields = soup.findAll(["input", "select"], {"id": re.compile(".*")})
981+ ids = [field["id"] for field in fields]
982+ self.assertContentEqual(self.expected_ids, ids)
983+
984+
985+class TestGitRepositoryTargetDisplayWidget(
986+ TestGitRepositoryTargetWidgetBase, TestCaseWithFactory):
987+ """Test the GitRepositoryTargetDisplayWidget class."""
988+
989+ widget_factory = GitRepositoryTargetDisplayWidget
990+ expected_widget_interface = IDisplayWidget
991+ expected_disabled_attr = 'disabled="disabled" '
992+ expected_ids = [
993+ "field.target.option.project",
994+ ]
995+
996+
997+class TestGitRepositoryTargetWidget(
998+ TestGitRepositoryTargetWidgetBase, TestCaseWithFactory):
999+ """Test the GitRepositoryTargetWidget class."""
1000+
1001+ widget_factory = GitRepositoryTargetWidget
1002+ expected_widget_interface = IInputWidget
1003+ expected_disabled_attr = ''
1004+ expected_ids = [
1005+ "field.target.distribution",
1006+ "field.target.option.personal",
1007+ "field.target.option.package",
1008+ "field.target.option.project",
1009+ "field.target.package",
1010+ "field.target.project",
1011+ ]
1012+
1013+ @property
1014+ def form(self):
1015+ return {
1016+ "field.target": "project",
1017+ "field.target.distribution": "fnord",
1018+ "field.target.package": "snarf",
1019+ "field.target.project": "pting",
1020+ }
1021+
1022+ def test_hasInput_false(self):
1023+ # hasInput is false when the widget's name is not in the form data.
1024+ self.widget.request = LaunchpadTestRequest(form={})
1025+ self.assertEqual("field.target", self.widget.name)
1026+ self.assertFalse(self.widget.hasInput())
1027+
1028+ def test_hasInput_true(self):
1029+ # hasInput is true is the widget's name in the form data.
1030+ self.widget.request = LaunchpadTestRequest(form=self.form)
1031+ self.assertEqual("field.target", self.widget.name)
1032+ self.assertTrue(self.widget.hasInput())
1033+
1034+ def test_hasValidInput_true(self):
1035+ # The field input is valid when all submitted parts are valid.
1036+ self.widget.request = LaunchpadTestRequest(form=self.form)
1037+ self.assertTrue(self.widget.hasValidInput())
1038+
1039+ def test_hasValidInput_false(self):
1040+ # The field input is invalid if any of the submitted parts are invalid.
1041+ form = self.form
1042+ form["field.target.project"] = "non-existent"
1043+ self.widget.request = LaunchpadTestRequest(form=form)
1044+ self.assertFalse(self.widget.hasValidInput())
1045+
1046+ def test_getInputValue_personal(self):
1047+ # The field value is None when the personal radio button is
1048+ # selected.
1049+ form = self.form
1050+ form["field.target"] = "personal"
1051+ self.context.owner = self.factory.makePerson()
1052+ self.widget.request = LaunchpadTestRequest(form=form)
1053+ self.assertIsNone(self.widget.getInputValue())
1054+
1055+ def test_getInputValue_package_spn(self):
1056+ # The field value is the package when the package radio button
1057+ # is selected and the package sub field has official spn.
1058+ form = self.form
1059+ form["field.target"] = "package"
1060+ self.widget.request = LaunchpadTestRequest(form=form)
1061+ self.assertEqual(self.package, self.widget.getInputValue())
1062+
1063+ def test_getInputValue_package_invalid(self):
1064+ # An error is raised when the package is not published in the distro.
1065+ form = self.form
1066+ form["field.target"] = "package"
1067+ form["field.target.package"] = 'non-existent'
1068+ self.widget.request = LaunchpadTestRequest(form=form)
1069+ message = (
1070+ "There is no package named 'non-existent' published in Fnord.")
1071+ e = self.assertRaises(WidgetInputError, self.widget.getInputValue)
1072+ self.assertEqual(LaunchpadValidationError(message), e.errors)
1073+ self.assertEqual(html_escape(message), self.widget.error())
1074+
1075+ def test_getInputValue_distribution(self):
1076+ # An error is raised when the package radio button is selected and
1077+ # the package sub field is empty.
1078+ form = self.form
1079+ form["field.target"] = "package"
1080+ form["field.target.package"] = ''
1081+ self.widget.request = LaunchpadTestRequest(form=form)
1082+ message = "Please enter a package name"
1083+ e = self.assertRaises(WidgetInputError, self.widget.getInputValue)
1084+ self.assertEqual(LaunchpadValidationError(message), e.errors)
1085+ self.assertEqual(message, self.widget.error())
1086+
1087+ def test_getInputValue_distribution_invalid(self):
1088+ # An error is raised when the distribution is invalid.
1089+ form = self.form
1090+ form["field.target"] = "package"
1091+ form["field.target.package"] = ''
1092+ form["field.target.distribution"] = 'non-existent'
1093+ self.widget.request = LaunchpadTestRequest(form=form)
1094+ message = (
1095+ "There is no distribution named 'non-existent' registered in "
1096+ "Launchpad")
1097+ e = self.assertRaises(WidgetInputError, self.widget.getInputValue)
1098+ self.assertEqual(LaunchpadValidationError(message), e.errors)
1099+ self.assertEqual(html_escape(message), self.widget.error())
1100+
1101+ def test_getInputValue_project(self):
1102+ # The field value is the project when the project radio button is
1103+ # selected and the project sub field is valid.
1104+ form = self.form
1105+ form["field.target"] = "project"
1106+ self.widget.request = LaunchpadTestRequest(form=form)
1107+ self.assertEqual(self.project, self.widget.getInputValue())
1108+
1109+ def test_getInputValue_project_missing(self):
1110+ # An error is raised when the project field is missing.
1111+ form = self.form
1112+ form["field.target"] = "project"
1113+ del form["field.target.project"]
1114+ self.widget.request = LaunchpadTestRequest(form=form)
1115+ message = "Please enter a project name"
1116+ e = self.assertRaises(WidgetInputError, self.widget.getInputValue)
1117+ self.assertEqual(LaunchpadValidationError(message), e.errors)
1118+ self.assertEqual(message, self.widget.error())
1119+
1120+ def test_getInputValue_project_invalid(self):
1121+ # An error is raised when the project is not valid.
1122+ form = self.form
1123+ form["field.target"] = "project"
1124+ form["field.target.project"] = "non-existent"
1125+ self.widget.request = LaunchpadTestRequest(form=form)
1126+ message = (
1127+ "There is no project named 'non-existent' registered in "
1128+ "Launchpad")
1129+ e = self.assertRaises(WidgetInputError, self.widget.getInputValue)
1130+ self.assertEqual(LaunchpadValidationError(message), e.errors)
1131+ self.assertEqual(html_escape(message), self.widget.error())
1132
1133=== added file 'lib/lp/code/javascript/gitrepository.edit.js'
1134--- lib/lp/code/javascript/gitrepository.edit.js 1970-01-01 00:00:00 +0000
1135+++ lib/lp/code/javascript/gitrepository.edit.js 2015-06-18 20:19:28 +0000
1136@@ -0,0 +1,43 @@
1137+/* Copyright 2015 Canonical Ltd. This software is licensed under the
1138+ * GNU Affero General Public License version 3 (see the file LICENSE).
1139+ *
1140+ * Control enabling/disabling form elements on the GitRepository:+edit page.
1141+ *
1142+ * @module Y.lp.code.gitrepository.edit
1143+ * @requires node, DOM
1144+ */
1145+YUI.add('lp.code.gitrepository.edit', function(Y) {
1146+ Y.log('loading lp.code.gitrepository.edit');
1147+ var module = Y.namespace('lp.code.gitrepository.edit');
1148+
1149+ module.set_enabled = function(field_id, is_enabled) {
1150+ var field = Y.DOM.byId(field_id);
1151+ field.disabled = !is_enabled;
1152+ };
1153+
1154+ module.onclick_target = function(e) {
1155+ var value = false;
1156+ Y.all('input[name="field.target"]').each(function(node) {
1157+ if (node.get('checked'))
1158+ value = node.get('value');
1159+ });
1160+ module.set_enabled('field.owner_default', value !== 'personal');
1161+ };
1162+
1163+ module.setup = function() {
1164+ Y.all('input[name="field.target"]').on('click', module.onclick_target);
1165+ Y.all('input[name="field.target.package"]').on(
1166+ 'keypress', function(e) {
1167+ Y.DOM.byId('field.target.option.package', e).checked = true;
1168+ module.set_enabled('field.owner_default', true);
1169+ });
1170+ Y.all('input[name="field.target.project"]').on(
1171+ 'keypress', function(e) {
1172+ Y.DOM.byId('field.target.option.project', e).checked = true;
1173+ module.set_enabled('field.owner_default', true);
1174+ });
1175+
1176+ // Set the initial state.
1177+ module.onclick_target();
1178+ };
1179+}, '0.1', {'requires': ['node', 'DOM']});
1180
1181=== added file 'lib/lp/code/javascript/tests/test_gitrepository.edit.html'
1182--- lib/lp/code/javascript/tests/test_gitrepository.edit.html 1970-01-01 00:00:00 +0000
1183+++ lib/lp/code/javascript/tests/test_gitrepository.edit.html 2015-06-18 20:19:28 +0000
1184@@ -0,0 +1,150 @@
1185+<!DOCTYPE html>
1186+<!--
1187+Copyright 2015 Canonical Ltd. This software is licensed under the
1188+GNU Affero General Public License version 3 (see the file LICENSE).
1189+-->
1190+
1191+<html>
1192+ <head>
1193+ <title>code.gitrepository.edit Tests</title>
1194+
1195+ <!-- YUI and test setup -->
1196+ <script type="text/javascript"
1197+ src="../../../../../build/js/yui/yui/yui.js">
1198+ </script>
1199+ <link rel="stylesheet"
1200+ href="../../../../../build/js/yui/console/assets/console-core.css" />
1201+ <link rel="stylesheet"
1202+ href="../../../../../build/js/yui/test-console/assets/skins/sam/test-console.css" />
1203+ <link rel="stylesheet"
1204+ href="../../../../../build/js/yui/test/assets/skins/sam/test.css" />
1205+
1206+ <script type="text/javascript"
1207+ src="../../../../../build/js/lp/app/testing/testrunner.js"></script>
1208+
1209+ <link rel="stylesheet" href="../../../app/javascript/testing/test.css" />
1210+
1211+ <!-- Dependencies -->
1212+ <script type="text/javascript"
1213+ src="../../../../../build/js/lp/app/lp.js"></script>
1214+
1215+ <!-- The module under test. -->
1216+ <script type="text/javascript" src="../gitrepository.edit.js"></script>
1217+
1218+ <!-- The test suite -->
1219+ <script type="text/javascript" src="test_gitrepository.edit.js"></script>
1220+
1221+ <script type="text/javascript">
1222+ YUI().use('lp.code.gitrepository.edit', function(Y) {
1223+ Y.on('domready', Y.lp.code.gitrepository.edit.setup);
1224+ });
1225+ </script>
1226+
1227+ </head>
1228+ <body class="yui3-skin-sam">
1229+ <ul id="suites">
1230+ <li>lp.code.gitrepository.edit.test</li>
1231+ </ul>
1232+ <div id="gitrepository.edit">
1233+ <form action="." name="launchpadform" method="post"
1234+ enctype="multipart/form-data"
1235+ accept-charset="UTF-8">
1236+
1237+ <table class="form">
1238+ <tr>
1239+ <td colspan="2" style="text-align: left">
1240+ <div>
1241+ <label for="field.target">Target:</label>
1242+ <div>
1243+ <table>
1244+ <tr>
1245+ <td colspan="2">
1246+ <label>
1247+ <input class="radioType"
1248+ id="field.target.option.personal"
1249+ name="field.target"
1250+ type="radio"
1251+ value="personal" />
1252+ Personal
1253+ </label>
1254+ </td>
1255+ </tr>
1256+ <tr>
1257+ <td>
1258+ <label>
1259+ <input class="radioType"
1260+ id="field.target.option.package"
1261+ name="field.target"
1262+ type="radio" value="package" />
1263+ Distribution
1264+ </label>
1265+ </td>
1266+ <td>
1267+ <select id="field.target.distribution"
1268+ name="field.target.distribution" size="1">
1269+ <option value="debian">Debian GNU/Linux</option>
1270+ <option value="ubuntu">Ubuntu</option>
1271+ </select>
1272+ </td>
1273+ </tr>
1274+ <tr>
1275+ <td align="right">
1276+ <label for="field.target.option.package">
1277+ Package
1278+ </label>
1279+ </td>
1280+ <td>
1281+ <input type="text" value="" id="field.target.package"
1282+ name="field.target.package" size="20" />
1283+ </td>
1284+ </tr>
1285+ <tr>
1286+ <td>
1287+ <label>
1288+ <input class="radioType"
1289+ id="field.target.option.project"
1290+ name="field.target"
1291+ type="radio"
1292+ value="project" />
1293+ Project
1294+ </label>
1295+ </td>
1296+ <td>
1297+ <input type="text" value="" id="field.target.project"
1298+ name="field.target.project" size="20" />
1299+ </td>
1300+ </tr>
1301+ </table>
1302+ </div>
1303+ <p class="formHelp">The target of the repository.</p>
1304+ </div>
1305+ </td>
1306+ </tr>
1307+
1308+ <tr>
1309+ <td colspan="2">
1310+ <div>
1311+ <input class="hiddenType" id="field.owner_default.used"
1312+ name="field.owner_default.used" type="hidden"
1313+ value="" />
1314+ <input class="checkboxType" checked="checked"
1315+ id="field.owner_default" name="field.owner_default"
1316+ type="checkbox" value="on" />
1317+ <label for="field.owner_default">Owner default</label>
1318+ <p class="formHelp">
1319+ Whether this repository is the default for its owner.
1320+ </p>
1321+ </div>
1322+ </td>
1323+ </tr>
1324+ </table>
1325+
1326+ <input type="submit" id="field.actions.change"
1327+ name="field.actions.change" value="Change Git Repository"
1328+ class="button" />
1329+ or&nbsp;
1330+ <a href="https://code.launchpad.dev/~me/p/+git/r">Cancel</a>
1331+ </form>
1332+ </div>
1333+ </body>
1334+</html>
1335
1336=== added file 'lib/lp/code/javascript/tests/test_gitrepository.edit.js'
1337--- lib/lp/code/javascript/tests/test_gitrepository.edit.js 1970-01-01 00:00:00 +0000
1338+++ lib/lp/code/javascript/tests/test_gitrepository.edit.js 2015-06-18 20:19:28 +0000
1339@@ -0,0 +1,100 @@
1340+/* Copyright 2015 Canonical Ltd. This software is licensed under the
1341+ * GNU Affero General Public License version 3 (see the file LICENSE).
1342+ *
1343+ * Test driver for gitrepository.edit.js.
1344+ */
1345+YUI.add('lp.code.gitrepository.edit.test', function(Y) {
1346+ var tests = Y.namespace('lp.code.gitrepository.edit.test');
1347+ var module = Y.lp.code.gitrepository.edit;
1348+ tests.suite = new Y.Test.Suite('code.gitrepository.edit Tests');
1349+
1350+ tests.suite.add(new Y.Test.Case({
1351+ name: 'code.gitrepository.edit_tests',
1352+
1353+ setUp: function() {
1354+ this.tbody = Y.one('#gitrepository.edit');
1355+
1356+ // Get the individual target type radio buttons.
1357+ this.target_personal = Y.DOM.byId('field.target.option.personal');
1358+ this.target_package = Y.DOM.byId('field.target.option.package');
1359+ this.target_project = Y.DOM.byId('field.target.option.project');
1360+
1361+ // Get the input widgets.
1362+ this.input_package = Y.one('input[name="field.target.package"]');
1363+ this.input_project = Y.one('input[name="field.target.project"]');
1364+ this.owner_default = Y.DOM.byId('field.owner_default');
1365+ },
1366+
1367+ tearDown: function() {
1368+ delete this.tbody;
1369+ },
1370+
1371+ test_handlers_connected: function() {
1372+ // Manually invoke the setup function to ensure the handlers are
1373+ // set.
1374+ module.setup();
1375+
1376+ var check_handler = function(field, expected) {
1377+ var custom_events = Y.Event.getListeners(field, 'click');
1378+ var click_event = custom_events[0];
1379+ var subscribers = click_event.subscribers;
1380+ Y.each(subscribers, function(sub) {
1381+ Y.Assert.isTrue(sub.contains(expected),
1382+ 'handler not set up');
1383+ });
1384+ };
1385+
1386+ check_handler(this.target_personal, module.onclick_target);
1387+ check_handler(this.target_package, module.onclick_target);
1388+ check_handler(this.target_project, module.onclick_target);
1389+ },
1390+
1391+ test_select_target_personal: function() {
1392+ this.target_personal.checked = true;
1393+ module.onclick_target();
1394+ // The owner_default checkbox is disabled.
1395+ Y.Assert.isTrue(this.owner_default.disabled,
1396+ 'owner_default not disabled');
1397+ },
1398+
1399+ test_select_target_package: function() {
1400+ this.target_package.checked = true;
1401+ module.onclick_target();
1402+ // The owner_default checkbox is enabled.
1403+ Y.Assert.isFalse(this.owner_default.disabled,
1404+ 'owner_default not disabled');
1405+ },
1406+
1407+ test_select_target_project: function() {
1408+ this.target_project.checked = true;
1409+ module.onclick_target();
1410+ // The owner_default checkbox is enabled.
1411+ Y.Assert.isFalse(this.owner_default.disabled,
1412+ 'owner_default not disabled');
1413+ },
1414+
1415+ test_keypress_package: function() {
1416+ this.target_personal.checked = true;
1417+ this.owner_default.disabled = true;
1418+ this.input_package.simulate('keypress', { charCode: 97 });
1419+ Y.Assert.isTrue(this.target_package.checked);
1420+ // The owner_default checkbox is enabled.
1421+ Y.Assert.isFalse(this.owner_default.disabled,
1422+ 'owner_default not disabled');
1423+ },
1424+
1425+ test_keypress_project: function() {
1426+ this.target_personal.checked = true;
1427+ this.owner_default.disabled = true;
1428+ this.input_project.simulate('keypress', { charCode: 97 });
1429+ Y.Assert.isTrue(this.target_project.checked);
1430+ // The owner_default checkbox is enabled.
1431+ Y.Assert.isFalse(this.owner_default.disabled,
1432+ 'owner_default not disabled');
1433+ },
1434+ }));
1435+}, '0.1', {
1436+ requires: ['lp.testing.runner', 'test', 'test-console',
1437+ 'Event', 'node-event-simulate',
1438+ 'lp.code.gitrepository.edit']
1439+});
1440
1441=== added file 'lib/lp/code/templates/gitrepository-edit.pt'
1442--- lib/lp/code/templates/gitrepository-edit.pt 1970-01-01 00:00:00 +0000
1443+++ lib/lp/code/templates/gitrepository-edit.pt 2015-06-18 20:19:28 +0000
1444@@ -0,0 +1,23 @@
1445+<html
1446+ xmlns="http://www.w3.org/1999/xhtml"
1447+ xmlns:tal="http://xml.zope.org/namespaces/tal"
1448+ xmlns:metal="http://xml.zope.org/namespaces/metal"
1449+ xmlns:i18n="http://xml.zope.org/namespaces/i18n"
1450+ metal:use-macro="view/macro:page/main_only"
1451+ i18n:domain="launchpad">
1452+<body>
1453+
1454+<div metal:fill-slot="main">
1455+ <div metal:use-macro="context/@@launchpad_form/form" />
1456+
1457+ <script type="text/javascript">
1458+ LPJS.use('lp.code.gitrepository.edit', function(Y) {
1459+ Y.on('domready', function(e) {
1460+ Y.lp.code.gitrepository.edit.setup();
1461+ }, window);
1462+ });
1463+ </script>
1464+</div>
1465+
1466+</body>
1467+</html>