Merge ~cjwatson/launchpad:oci-project-basic-views into launchpad:master
- Git
- lp:~cjwatson/launchpad
- oci-project-basic-views
- Merge into 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) |
||||
Related bugs: |
|
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
Description of the change
To post a comment you must log in.
- 8ea7924... by Colin Watson
-
Use OCIProjectNameS
et.getOrCreateB yName 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
1 | diff --git a/lib/lp/registry/browser/configure.zcml b/lib/lp/registry/browser/configure.zcml |
2 | index 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}" |
51 | diff --git a/lib/lp/registry/browser/distribution.py b/lib/lp/registry/browser/distribution.py |
52 | index 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) |
72 | diff --git a/lib/lp/registry/browser/ociproject.py b/lib/lp/registry/browser/ociproject.py |
73 | new file mode 100644 |
74 | index 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 |
202 | diff --git a/lib/lp/registry/browser/tests/test_ociproject.py b/lib/lp/registry/browser/tests/test_ociproject.py |
203 | new file mode 100644 |
204 | index 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'.") |
360 | diff --git a/lib/lp/registry/configure.zcml b/lib/lp/registry/configure.zcml |
361 | index 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 |
390 | diff --git a/lib/lp/registry/interfaces/ociproject.py b/lib/lp/registry/interfaces/ociproject.py |
391 | index 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.")) |
444 | diff --git a/lib/lp/registry/interfaces/ociprojectname.py b/lib/lp/registry/interfaces/ociprojectname.py |
445 | index 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.""" |
467 | diff --git a/lib/lp/registry/model/ociproject.py b/lib/lp/registry/model/ociproject.py |
468 | index 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 |
536 | diff --git a/lib/lp/registry/model/ociprojectname.py b/lib/lp/registry/model/ociprojectname.py |
537 | index 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) |
551 | diff --git a/lib/lp/registry/templates/ociproject-index.pt b/lib/lp/registry/templates/ociproject-index.pt |
552 | new file mode 100644 |
553 | index 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> |
606 | diff --git a/lib/lp/registry/tests/test_ociproject.py b/lib/lp/registry/tests/test_ociproject.py |
607 | index 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): |
627 | diff --git a/lib/lp/testing/factory.py b/lib/lp/testing/factory.py |
628 | index 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, |