Merge ~cjwatson/launchpad:oci-project-basic-views into launchpad:master

Proposed by Colin Watson
Status: Merged
Approved by: Colin Watson
Approved revision: 22aa91ef40fedc9dfd4f1f2eea18b312dd411444
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~cjwatson/launchpad:oci-project-basic-views
Merge into: launchpad:master
Prerequisite: ~cjwatson/launchpad:person-oci-project
Diff against target: 639 lines (+434/-18)
12 files modified
lib/lp/registry/browser/configure.zcml (+39/-0)
lib/lp/registry/browser/distribution.py (+5/-1)
lib/lp/registry/browser/ociproject.py (+124/-0)
lib/lp/registry/browser/tests/test_ociproject.py (+152/-0)
lib/lp/registry/configure.zcml (+7/-6)
lib/lp/registry/interfaces/ociproject.py (+12/-5)
lib/lp/registry/interfaces/ociprojectname.py (+5/-2)
lib/lp/registry/model/ociproject.py (+24/-2)
lib/lp/registry/model/ociprojectname.py (+7/-0)
lib/lp/registry/templates/ociproject-index.pt (+49/-0)
lib/lp/registry/tests/test_ociproject.py (+9/-1)
lib/lp/testing/factory.py (+1/-1)
Reviewer Review Type Date Requested Status
Tom Wardill (community) Approve
Review via email: mp+376106@code.launchpad.net

Commit message

Add basic index and edit views for OCIProject

To post a comment you must log in.
8ea7924... by Colin Watson

Use OCIProjectNameSet.getOrCreateByName in the factory too

22aa91e... by Colin Watson

Add constraint to IOCIProject.name

There's already a constraint on IOCIProjectName.name, but duplicating it
here avoids an OOPS when editing an OCIProject in the web UI.

Revision history for this message
Tom Wardill (twom) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/lib/lp/registry/browser/configure.zcml b/lib/lp/registry/browser/configure.zcml
2index 018a53c..e277e94 100644
3--- a/lib/lp/registry/browser/configure.zcml
4+++ b/lib/lp/registry/browser/configure.zcml
5@@ -602,6 +602,45 @@
6 provides="zope.traversing.interfaces.IPathAdapter"
7 for="lp.registry.interfaces.sourcepackage.ISourcePackage"
8 />
9+ <browser:defaultView
10+ name="+index"
11+ for="lp.registry.interfaces.ociproject.IOCIProject"
12+ />
13+ <browser:url
14+ for="lp.registry.interfaces.ociproject.IOCIProject"
15+ path_expression="string:+oci/${name}"
16+ attribute_to_parent="pillar"
17+ />
18+ <browser:navigation
19+ module="lp.registry.browser.ociproject"
20+ classes="OCIProjectNavigation"
21+ />
22+ <browser:page
23+ name="+index"
24+ for="lp.registry.interfaces.ociproject.IOCIProject"
25+ class="lp.services.webapp.LaunchpadView"
26+ permission="launchpad.View"
27+ template="../templates/ociproject-index.pt"
28+ />
29+ <browser:page
30+ name="+edit"
31+ for="lp.registry.interfaces.ociproject.IOCIProject"
32+ class="lp.registry.browser.ociproject.OCIProjectEditView"
33+ permission="launchpad.Edit"
34+ template="../../app/templates/generic-edit.pt"
35+ />
36+ <browser:menus
37+ module="lp.registry.browser.ociproject"
38+ classes="
39+ OCIProjectFacets
40+ OCIProjectNavigationMenu"
41+ />
42+ <adapter
43+ name="fmt"
44+ factory="lp.registry.browser.ociproject.OCIProjectFormatterAPI"
45+ provides="zope.traversing.interfaces.IPathAdapter"
46+ for="lp.registry.interfaces.ociproject.IOCIProject"
47+ />
48 <browser:url
49 for="lp.registry.interfaces.commercialsubscription.ICommercialSubscription"
50 path_expression="string:+commercialsubscription/${id}"
51diff --git a/lib/lp/registry/browser/distribution.py b/lib/lp/registry/browser/distribution.py
52index 453823c..a17ff8d 100644
53--- a/lib/lp/registry/browser/distribution.py
54+++ b/lib/lp/registry/browser/distribution.py
55@@ -1,4 +1,4 @@
56-# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
57+# Copyright 2009-2019 Canonical Ltd. This software is licensed under the
58 # GNU Affero General Public License version 3 (see the file LICENSE).
59
60 """Browser views for distributions."""
61@@ -155,6 +155,10 @@ class DistributionNavigation(
62 def traverse_sources(self, name):
63 return self.context.getSourcePackage(name)
64
65+ @stepthrough('+oci')
66+ def traverse_oci(self, name):
67+ return self.context.getOCIProject(name)
68+
69 @stepthrough('+milestone')
70 def traverse_milestone(self, name):
71 return self.context.getMilestone(name)
72diff --git a/lib/lp/registry/browser/ociproject.py b/lib/lp/registry/browser/ociproject.py
73new file mode 100644
74index 0000000..4d760a4
75--- /dev/null
76+++ b/lib/lp/registry/browser/ociproject.py
77@@ -0,0 +1,124 @@
78+# Copyright 2019 Canonical Ltd. This software is licensed under the
79+# GNU Affero General Public License version 3 (see the file LICENSE).
80+
81+"""Views, menus, and traversal related to `OCIProject`s."""
82+
83+from __future__ import absolute_import, print_function, unicode_literals
84+
85+__metaclass__ = type
86+__all__ = [
87+ 'OCIProjectBreadcrumb',
88+ 'OCIProjectFacets',
89+ 'OCIProjectNavigation',
90+ ]
91+
92+from zope.component import getUtility
93+from zope.interface import implementer
94+
95+from lp.app.browser.launchpadform import (
96+ action,
97+ LaunchpadEditFormView,
98+ )
99+from lp.app.browser.tales import CustomizableFormatter
100+from lp.app.interfaces.headings import IHeadingBreadcrumb
101+from lp.code.browser.vcslisting import TargetDefaultVCSNavigationMixin
102+from lp.registry.interfaces.ociproject import (
103+ IOCIProject,
104+ IOCIProjectSet,
105+ )
106+from lp.services.webapp import (
107+ canonical_url,
108+ enabled_with_permission,
109+ Link,
110+ Navigation,
111+ NavigationMenu,
112+ StandardLaunchpadFacets,
113+ )
114+from lp.services.webapp.breadcrumb import Breadcrumb
115+from lp.services.webapp.interfaces import IMultiFacetedBreadcrumb
116+
117+
118+class OCIProjectFormatterAPI(CustomizableFormatter):
119+ """Adapt `IOCIProject` objects to a formatted string."""
120+
121+ _link_summary_template = '%(displayname)s'
122+
123+ def _link_summary_values(self):
124+ displayname = self._context.display_name
125+ return {'displayname': displayname}
126+
127+
128+class OCIProjectNavigation(TargetDefaultVCSNavigationMixin, Navigation):
129+
130+ usedfor = IOCIProject
131+
132+
133+@implementer(IHeadingBreadcrumb, IMultiFacetedBreadcrumb)
134+class OCIProjectBreadcrumb(Breadcrumb):
135+ """Builds a breadcrumb for an `IOCIProject`."""
136+
137+ @property
138+ def text(self):
139+ return '%s OCI project' % self.context.name
140+
141+
142+class OCIProjectFacets(StandardLaunchpadFacets):
143+
144+ usedfor = IOCIProject
145+ enable_only = [
146+ 'overview',
147+ 'branches',
148+ ]
149+
150+
151+class OCIProjectNavigationMenu(NavigationMenu):
152+ """Navigation menu for OCI projects."""
153+
154+ usedfor = IOCIProject
155+
156+ facet = 'overview'
157+
158+ links = ('edit',)
159+
160+ @enabled_with_permission('launchpad.Edit')
161+ def edit(self):
162+ return Link('+edit', 'Edit OCI project', icon='edit')
163+
164+
165+class OCIProjectEditView(LaunchpadEditFormView):
166+ """Edit an OCI project."""
167+
168+ schema = IOCIProject
169+ field_names = [
170+ 'distribution',
171+ 'name',
172+ ]
173+
174+ @property
175+ def label(self):
176+ return 'Edit %s OCI project' % self.context.name
177+
178+ page_title = 'Edit'
179+
180+ def validate(self, data):
181+ super(OCIProjectEditView, self).validate(data)
182+ distribution = data.get('distribution')
183+ name = data.get('name')
184+ if distribution and name:
185+ oci_project = getUtility(IOCIProjectSet).getByDistributionAndName(
186+ distribution, name)
187+ if oci_project is not None and oci_project != self.context:
188+ self.setFieldError(
189+ 'name',
190+ 'There is already an OCI project in %s with this name.' % (
191+ distribution.display_name))
192+
193+ @action('Update OCI project', name='update')
194+ def update_action(self, action, data):
195+ self.updateContextFromData(data)
196+
197+ @property
198+ def next_url(self):
199+ return canonical_url(self.context)
200+
201+ cancel_url = next_url
202diff --git a/lib/lp/registry/browser/tests/test_ociproject.py b/lib/lp/registry/browser/tests/test_ociproject.py
203new file mode 100644
204index 0000000..57f1061
205--- /dev/null
206+++ b/lib/lp/registry/browser/tests/test_ociproject.py
207@@ -0,0 +1,152 @@
208+# Copyright 2019 Canonical Ltd. This software is licensed under the
209+# GNU Affero General Public License version 3 (see the file LICENSE).
210+
211+"""Test OCI project views."""
212+
213+from __future__ import absolute_import, print_function, unicode_literals
214+
215+__metaclass__ = type
216+__all__ = []
217+
218+from datetime import datetime
219+
220+import pytz
221+
222+from lp.services.database.constants import UTC_NOW
223+from lp.services.webapp import canonical_url
224+from lp.services.webapp.escaping import structured
225+from lp.testing import (
226+ BrowserTestCase,
227+ person_logged_in,
228+ test_tales,
229+ TestCaseWithFactory,
230+ )
231+from lp.testing.layers import DatabaseFunctionalLayer
232+from lp.testing.matchers import MatchesTagText
233+from lp.testing.pages import (
234+ extract_text,
235+ find_main_content,
236+ find_tags_by_class,
237+ )
238+from lp.testing.publication import test_traverse
239+from lp.testing.views import create_initialized_view
240+
241+
242+class TestOCIProjectFormatterAPI(TestCaseWithFactory):
243+
244+ layer = DatabaseFunctionalLayer
245+
246+ def test_link(self):
247+ oci_project = self.factory.makeOCIProject()
248+ markup = structured(
249+ '<a href="/%s/+oci/%s">%s</a>',
250+ oci_project.pillar.name, oci_project.name,
251+ oci_project.display_name).escapedtext
252+ self.assertEqual(
253+ markup,
254+ test_tales('oci_project/fmt:link', oci_project=oci_project))
255+
256+
257+class TestOCIProjectNavigation(TestCaseWithFactory):
258+
259+ layer = DatabaseFunctionalLayer
260+
261+ def test_canonical_url(self):
262+ distribution = self.factory.makeDistribution(name="mydistro")
263+ oci_project = self.factory.makeOCIProject(
264+ pillar=distribution, ociprojectname="myociproject")
265+ self.assertEqual(
266+ "http://launchpad.test/mydistro/+oci/myociproject",
267+ canonical_url(oci_project))
268+
269+ def test_traversal(self):
270+ oci_project = self.factory.makeOCIProject()
271+ obj, _, _ = test_traverse(
272+ "http://launchpad.test/%s/+oci/%s" %
273+ (oci_project.pillar.name, oci_project.name))
274+ self.assertEqual(oci_project, obj)
275+
276+
277+class TestOCIProjectView(BrowserTestCase):
278+
279+ layer = DatabaseFunctionalLayer
280+
281+ def test_index(self):
282+ distribution = self.factory.makeDistribution(displayname="My Distro")
283+ oci_project = self.factory.makeOCIProject(
284+ pillar=distribution, ociprojectname="oci-name")
285+ self.assertTextMatchesExpressionIgnoreWhitespace("""\
286+ OCI project oci-name for My Distro
287+ .*
288+ OCI project information
289+ Distribution: My Distro
290+ Name: oci-name
291+ """, self.getMainText(oci_project))
292+
293+
294+class TestOCIProjectEditView(BrowserTestCase):
295+
296+ layer = DatabaseFunctionalLayer
297+
298+ def test_edit_oci_project(self):
299+ oci_project = self.factory.makeOCIProject()
300+ new_distribution = self.factory.makeDistribution(
301+ owner=oci_project.pillar.owner)
302+
303+ browser = self.getViewBrowser(
304+ oci_project, user=oci_project.pillar.owner)
305+ browser.getLink("Edit OCI project").click()
306+ browser.getControl(name="field.distribution").value = [
307+ new_distribution.name]
308+ browser.getControl(name="field.name").value = "new-name"
309+ browser.getControl("Update OCI project").click()
310+
311+ content = find_main_content(browser.contents)
312+ self.assertEqual(
313+ "OCI project new-name for %s" % new_distribution.display_name,
314+ extract_text(content.h1))
315+ self.assertThat(
316+ "Distribution:\n%s\nEdit OCI project" % (
317+ new_distribution.display_name),
318+ MatchesTagText(content, "distribution"))
319+ self.assertThat(
320+ "Name:\nnew-name\nEdit OCI project",
321+ MatchesTagText(content, "name"))
322+
323+ def test_edit_oci_project_sets_date_last_modified(self):
324+ # Editing an OCI project sets the date_last_modified property.
325+ date_created = datetime(2000, 1, 1, tzinfo=pytz.UTC)
326+ oci_project = self.factory.makeOCIProject(date_created=date_created)
327+ self.assertEqual(date_created, oci_project.date_last_modified)
328+ with person_logged_in(oci_project.pillar.owner):
329+ view = create_initialized_view(
330+ oci_project, name="+edit", principal=oci_project.pillar.owner)
331+ view.update_action.success({"name": "changed"})
332+ self.assertSqlAttributeEqualsDate(
333+ oci_project, "date_last_modified", UTC_NOW)
334+
335+ def test_edit_oci_project_already_exists(self):
336+ oci_project = self.factory.makeOCIProject(ociprojectname="one")
337+ self.factory.makeOCIProject(
338+ pillar=oci_project.pillar, ociprojectname="two")
339+ pillar_display_name = oci_project.pillar.display_name
340+ browser = self.getViewBrowser(
341+ oci_project, user=oci_project.pillar.owner)
342+ browser.getLink("Edit OCI project").click()
343+ browser.getControl(name="field.name").value = "two"
344+ browser.getControl("Update OCI project").click()
345+ self.assertEqual(
346+ "There is already an OCI project in %s with this name." % (
347+ pillar_display_name),
348+ extract_text(find_tags_by_class(browser.contents, "message")[1]))
349+
350+ def test_edit_oci_project_invalid_name(self):
351+ oci_project = self.factory.makeOCIProject()
352+ browser = self.getViewBrowser(
353+ oci_project, user=oci_project.pillar.owner)
354+ browser.getLink("Edit OCI project").click()
355+ browser.getControl(name="field.name").value = "invalid name"
356+ browser.getControl("Update OCI project").click()
357+ self.assertStartsWith(
358+ extract_text(find_tags_by_class(browser.contents, "message")[1]),
359+ "Invalid name 'invalid name'.")
360diff --git a/lib/lp/registry/configure.zcml b/lib/lp/registry/configure.zcml
361index c95a7b2..2f01072 100644
362--- a/lib/lp/registry/configure.zcml
363+++ b/lib/lp/registry/configure.zcml
364@@ -746,18 +746,19 @@
365 interface="lp.registry.interfaces.ociproject.IOCIProjectEdit"
366 set_schema="lp.registry.interfaces.ociproject.IOCIProjectEditableAttributes" />
367 </class>
368- <securedutility
369- class="lp.registry.model.ociproject.OCIProject"
370- provides="lp.registry.interfaces.ociproject.IOCIProject">
371- <allow
372- interface="lp.registry.interfaces.ociproject.IOCIProject"/>
373- </securedutility>
374+ <subscriber
375+ for="lp.registry.interfaces.ociproject.IOCIProject zope.lifecycleevent.interfaces.IObjectModifiedEvent"
376+ handler="lp.registry.model.ociproject.oci_project_modified" />
377 <securedutility
378 class="lp.registry.model.ociproject.OCIProjectSet"
379 provides="lp.registry.interfaces.ociproject.IOCIProjectSet">
380 <allow
381 interface="lp.registry.interfaces.ociproject.IOCIProjectSet"/>
382 </securedutility>
383+ <adapter
384+ for="lp.registry.interfaces.ociproject.IOCIProject"
385+ provides="lp.services.webapp.interfaces.IBreadcrumb"
386+ factory="lp.registry.browser.ociproject.OCIProjectBreadcrumb"/>
387
388 <!-- OCIProjectSeries -->
389 <class
390diff --git a/lib/lp/registry/interfaces/ociproject.py b/lib/lp/registry/interfaces/ociproject.py
391index 0750767..e0b3c00 100644
392--- a/lib/lp/registry/interfaces/ociproject.py
393+++ b/lib/lp/registry/interfaces/ociproject.py
394@@ -14,6 +14,7 @@ __all__ = [
395 from lazr.restful.fields import (
396 CollectionField,
397 Reference,
398+ ReferenceChoice,
399 )
400 from zope.interface import (
401 Attribute,
402@@ -23,9 +24,11 @@ from zope.schema import (
403 Datetime,
404 Int,
405 Text,
406+ TextLine,
407 )
408
409 from lp import _
410+from lp.app.validators.name import name_validator
411 from lp.bugs.interfaces.bugtarget import IBugTarget
412 from lp.code.interfaces.hasgitrepositories import IHasGitRepositories
413 from lp.registry.interfaces.distribution import IDistribution
414@@ -54,7 +57,6 @@ class IOCIProjectView(IHasGitRepositories, Interface):
415 # Really IOCIProjectSeries
416 value_type=Reference(schema=Interface))
417
418- name = Attribute(_("Name"))
419 display_name = Attribute(_("Display name for this OCI project."))
420
421
422@@ -64,12 +66,17 @@ class IOCIProjectEditableAttributes(IBugTarget):
423 These attributes need launchpad.View to see, and launchpad.Edit to change.
424 """
425
426- distribution = Reference(
427- IDistribution,
428- title=_("The distribution that this OCI project is associated with."))
429+ distribution = ReferenceChoice(
430+ title=_("The distribution that this OCI project is associated with."),
431+ schema=IDistribution, vocabulary="Distribution",
432+ required=True, readonly=False)
433+ name = TextLine(
434+ title=_("Name"), required=True, readonly=False,
435+ constraint=name_validator,
436+ description=_("The name of this OCI project."))
437 ociprojectname = Reference(
438 IOCIProjectName,
439- title=_("The name of this OCI project."),
440+ title=_("The name of this OCI project, as an `IOCIProjectName`."),
441 required=True,
442 readonly=True)
443 description = Text(title=_("The description for this OCI project."))
444diff --git a/lib/lp/registry/interfaces/ociprojectname.py b/lib/lp/registry/interfaces/ociprojectname.py
445index 3c34ffc..e2ed738 100644
446--- a/lib/lp/registry/interfaces/ociprojectname.py
447+++ b/lib/lp/registry/interfaces/ociprojectname.py
448@@ -39,13 +39,16 @@ class IOCIProjectNameSet(Interface):
449 """A set of `OCIProjectName`."""
450
451 def __getitem__(name):
452- """Retrieve a `OCIProjectName` by name."""
453+ """Retrieve an `OCIProjectName` by name."""
454
455 def getByName(name):
456- """Return a `OCIProjectName` by its name.
457+ """Return an `OCIProjectName` by its name.
458
459 :raises NoSuchOCIProjectName: if the `OCIProjectName` can't be found.
460 """
461
462 def new(name):
463 """Create a new `OCIProjectName`."""
464+
465+ def getOrCreateByName(name):
466+ """Return an `OCIProjectName` by its name, creating it if necessary."""
467diff --git a/lib/lp/registry/model/ociproject.py b/lib/lp/registry/model/ociproject.py
468index f0975b3..7dcfdea 100644
469--- a/lib/lp/registry/model/ociproject.py
470+++ b/lib/lp/registry/model/ociproject.py
471@@ -19,7 +19,9 @@ from storm.locals import (
472 Reference,
473 Unicode,
474 )
475+from zope.component import getUtility
476 from zope.interface import implementer
477+from zope.security.proxy import removeSecurityProxy
478
479 from lp.bugs.model.bugtarget import BugTargetBase
480 from lp.registry.interfaces.distribution import IDistribution
481@@ -27,10 +29,14 @@ from lp.registry.interfaces.ociproject import (
482 IOCIProject,
483 IOCIProjectSet,
484 )
485+from lp.registry.interfaces.ociprojectname import IOCIProjectNameSet
486 from lp.registry.interfaces.series import SeriesStatus
487 from lp.registry.model.ociprojectname import OCIProjectName
488 from lp.registry.model.ociprojectseries import OCIProjectSeries
489-from lp.services.database.constants import DEFAULT
490+from lp.services.database.constants import (
491+ DEFAULT,
492+ UTC_NOW,
493+ )
494 from lp.services.database.interfaces import (
495 IMasterStore,
496 IStore,
497@@ -38,6 +44,17 @@ from lp.services.database.interfaces import (
498 from lp.services.database.stormbase import StormBase
499
500
501+def oci_project_modified(oci_project, event):
502+ """Update the date_last_modified property when an OCIProject is modified.
503+
504+ This method is registered as a subscriber to `IObjectModifiedEvent`
505+ events on OCI projects.
506+ """
507+ # This attribute is normally read-only; bypass the security proxy to
508+ # avoid that.
509+ removeSecurityProxy(oci_project).date_last_modified = UTC_NOW
510+
511+
512 @implementer(IOCIProject)
513 class OCIProject(BugTargetBase, StormBase):
514 """See `IOCIProject` and `IOCIProjectSet`."""
515@@ -70,6 +87,11 @@ class OCIProject(BugTargetBase, StormBase):
516 def name(self):
517 return self.ociprojectname.name
518
519+ @name.setter
520+ def name(self, value):
521+ self.ociprojectname = getUtility(IOCIProjectNameSet).getOrCreateByName(
522+ value)
523+
524 @property
525 def pillar(self):
526 """See `IBugTarget`."""
527@@ -79,7 +101,7 @@ class OCIProject(BugTargetBase, StormBase):
528 def display_name(self):
529 """See `IOCIProject`."""
530 return "OCI project %s for %s" % (
531- self.ociprojectname.name, self.pillar.name)
532+ self.ociprojectname.name, self.pillar.display_name)
533
534 bugtargetname = display_name
535 bugtargetdisplayname = display_name
536diff --git a/lib/lp/registry/model/ociprojectname.py b/lib/lp/registry/model/ociprojectname.py
537index 9085916..5591bd8 100644
538--- a/lib/lp/registry/model/ociprojectname.py
539+++ b/lib/lp/registry/model/ociprojectname.py
540@@ -70,3 +70,10 @@ class OCIProjectNameSet:
541 project_name = OCIProjectName(name=name)
542 store.add(project_name)
543 return project_name
544+
545+ def getOrCreateByName(self, name):
546+ """See `IOCIProjectNameSet`."""
547+ try:
548+ return self.getByName(name)
549+ except NoSuchOCIProjectName:
550+ return self.new(name)
551diff --git a/lib/lp/registry/templates/ociproject-index.pt b/lib/lp/registry/templates/ociproject-index.pt
552new file mode 100644
553index 0000000..8a9a6a1
554--- /dev/null
555+++ b/lib/lp/registry/templates/ociproject-index.pt
556@@ -0,0 +1,49 @@
557+<html
558+ xmlns="http://www.w3.org/1999/xhtml"
559+ xmlns:tal="http://xml.zope.org/namespaces/tal"
560+ xmlns:metal="http://xml.zope.org/namespaces/metal"
561+ xmlns:i18n="http://xml.zope.org/namespaces/i18n"
562+ metal:use-macro="view/macro:page/main_side"
563+ i18n:domain="launchpad"
564+>
565+
566+<body>
567+ <metal:registering fill-slot="registering">
568+ Created by
569+ <tal:registrant replace="structure context/registrant/fmt:link"/>
570+ on
571+ <tal:created-on replace="structure context/date_created/fmt:date"/>
572+ and last modified on
573+ <tal:last-modified replace="structure context/date_last_modified/fmt:date"/>
574+ </metal:registering>
575+
576+ <metal:side fill-slot="side">
577+ <div tal:replace="structure context/@@+global-actions"/>
578+ </metal:side>
579+
580+ <metal:heading fill-slot="heading">
581+ <h1 tal:content="context/display_name"/>
582+ </metal:heading>
583+
584+ <div metal:fill-slot="main">
585+ <h2>OCI project information</h2>
586+ <div class="two-column-list">
587+ <dl id="distribution" tal:define="distribution context/distribution">
588+ <dt>Distribution:</dt>
589+ <dd>
590+ <a tal:attributes="href distribution/fmt:url"
591+ tal:content="distribution/display_name"/>
592+ <a tal:replace="structure view/menu:overview/edit/fmt:icon"/>
593+ </dd>
594+ </dl>
595+ <dl id="name">
596+ <dt>Name:</dt>
597+ <dd>
598+ <span tal:content="context/name"/>
599+ <a tal:replace="structure view/menu:overview/edit/fmt:icon"/>
600+ </dd>
601+ </dl>
602+ </div>
603+ </div>
604+</body>
605+</html>
606diff --git a/lib/lp/registry/tests/test_ociproject.py b/lib/lp/registry/tests/test_ociproject.py
607index 174b12b..42c5d7d 100644
608--- a/lib/lp/registry/tests/test_ociproject.py
609+++ b/lib/lp/registry/tests/test_ociproject.py
610@@ -71,7 +71,15 @@ class TestOCIProject(TestCaseWithFactory):
611 oci_project_name = self.factory.makeOCIProjectName(name='test-name')
612 oci_project = self.factory.makeOCIProject(
613 ociprojectname=oci_project_name)
614- self.assertEqual(oci_project.name, 'test-name')
615+ self.assertEqual('test-name', oci_project.name)
616+
617+ def test_display_name(self):
618+ oci_project_name = self.factory.makeOCIProjectName(name='test-name')
619+ oci_project = self.factory.makeOCIProject(
620+ ociprojectname=oci_project_name)
621+ self.assertEqual(
622+ 'OCI project test-name for %s' % oci_project.pillar.display_name,
623+ oci_project.display_name)
624
625
626 class TestOCIProjectSet(TestCaseWithFactory):
627diff --git a/lib/lp/testing/factory.py b/lib/lp/testing/factory.py
628index 39d2725..0c72d31 100644
629--- a/lib/lp/testing/factory.py
630+++ b/lib/lp/testing/factory.py
631@@ -4901,7 +4901,7 @@ class BareLaunchpadObjectFactory(ObjectFactory):
632 def makeOCIProjectName(self, name=None):
633 if name is None:
634 name = self.getUniqueString(u"oci-project-name")
635- return getUtility(IOCIProjectNameSet).new(name)
636+ return getUtility(IOCIProjectNameSet).getOrCreateByName(name)
637
638 def makeOCIProject(self, registrant=None, pillar=None,
639 ociprojectname=None, date_created=DEFAULT,

Subscribers

People subscribed via source and target branches

to status/vote changes: