Merge ~andrey-fedoseev/launchpad:bug-presense into launchpad:master
- Git
- lp:~andrey-fedoseev/launchpad
- bug-presense
- Merge into master
Proposed by
Andrey Fedoseev
Status: | Needs review |
---|---|
Proposed branch: | ~andrey-fedoseev/launchpad:bug-presense |
Merge into: | launchpad:master |
Diff against target: |
769 lines (+691/-0) 7 files modified
lib/lp/bugs/configure.zcml (+25/-0) lib/lp/bugs/interfaces/bugpresence.py (+103/-0) lib/lp/bugs/model/bugpresence.py (+150/-0) lib/lp/bugs/security.py (+33/-0) lib/lp/bugs/tests/test_bugpresence.py (+337/-0) lib/lp/code/model/gitrepository.py (+10/-0) lib/lp/testing/factory.py (+33/-0) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Launchpad code reviewers | Pending | ||
Review via email: mp+431710@code.launchpad.net |
Commit message
Add `BugPresence` model
Description of the change
It represents a range of versions or git commits in which the bug was present.
To post a comment you must log in.
Unmerged commits
- 460e925... by Andrey Fedoseev
-
Add `BugPresence` model
It represents a range of versions or git commits in which the bug was present.
-
docs:0 (build) lint:0 (build) mypy:0 (build) 1 → 3 of 3 results First • Previous • Next • Last
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | diff --git a/lib/lp/bugs/configure.zcml b/lib/lp/bugs/configure.zcml |
2 | index 6eaa58c..52977cd 100644 |
3 | --- a/lib/lp/bugs/configure.zcml |
4 | +++ b/lib/lp/bugs/configure.zcml |
5 | @@ -1113,4 +1113,29 @@ |
6 | > |
7 | <allow interface="zope.schema.interfaces.IVocabularyFactory"/> |
8 | </securedutility> |
9 | + |
10 | + <!-- BugPresence --> |
11 | + <class |
12 | + class="lp.bugs.model.bugpresence.BugPresence"> |
13 | + <require |
14 | + permission="launchpad.View" |
15 | + interface="lp.bugs.interfaces.bugpresence.IBugPresenceView"/> |
16 | + <require |
17 | + permission="launchpad.Edit" |
18 | + interface="lp.bugs.interfaces.bugpresence.IBugPresenceEdit" /> |
19 | + </class> |
20 | + |
21 | + <!-- BugPresenceSet --> |
22 | + <class |
23 | + class="lp.bugs.model.bugpresence.BugPresenceSet"> |
24 | + <allow |
25 | + interface="lp.bugs.interfaces.bugpresence.IBugPresenceSet"/> |
26 | + </class> |
27 | + <securedutility |
28 | + class="lp.bugs.model.bugpresence.BugPresenceSet" |
29 | + provides="lp.bugs.interfaces.bugpresence.IBugPresenceSet"> |
30 | + <allow |
31 | + interface="lp.bugs.interfaces.bugpresence.IBugPresenceSet"/> |
32 | + </securedutility> |
33 | + |
34 | </configure> |
35 | diff --git a/lib/lp/bugs/interfaces/bugpresence.py b/lib/lp/bugs/interfaces/bugpresence.py |
36 | new file mode 100644 |
37 | index 0000000..4a34970 |
38 | --- /dev/null |
39 | +++ b/lib/lp/bugs/interfaces/bugpresence.py |
40 | @@ -0,0 +1,103 @@ |
41 | +from lazr.restful.fields import Reference |
42 | +from zope.interface import Attribute, Interface |
43 | +from zope.schema import Choice, Int, TextLine |
44 | + |
45 | +from lp import _ |
46 | +from lp.services.fields import BugField |
47 | + |
48 | +__all__ = ["IBugPresence", "IBugPresenceSet"] |
49 | + |
50 | + |
51 | +class IBugPresenceView(Interface): |
52 | + """IBugPresence attributes that require launchpad.View permission.""" |
53 | + |
54 | + id = Int() |
55 | + bug = BugField(title=_("Bug"), readonly=True) |
56 | + bug_id = Int() |
57 | + |
58 | + project = Choice(title=_("Project"), required=False, vocabulary="Product") |
59 | + project_id = Attribute("The product ID") |
60 | + source_package_name = Choice( |
61 | + title=_("Package"), required=False, vocabulary="SourcePackageName" |
62 | + ) |
63 | + source_package_name_id = Attribute("The source_package_name ID") |
64 | + distribution = Choice( |
65 | + title=_("Distribution"), required=False, vocabulary="Distribution" |
66 | + ) |
67 | + distribution_id = Attribute("The distribution ID") |
68 | + |
69 | + broken_version = TextLine( |
70 | + required=False, title=_("Version that introduced the bug") |
71 | + ) |
72 | + fixed_version = TextLine( |
73 | + required=False, title=_("Version that fixed the bug") |
74 | + ) |
75 | + |
76 | + git_repository = Choice( |
77 | + title=_("Git repository"), required=False, vocabulary="GitRepository" |
78 | + ) |
79 | + git_repository_id = Attribute("The git repository ID") |
80 | + broken_git_commit_sha1 = TextLine( |
81 | + required=False, |
82 | + title=_("Git commit that introduced the bug"), |
83 | + max_length=40, |
84 | + ) |
85 | + fixed_git_commit_sha1 = TextLine( |
86 | + required=False, title=_("Git commit that fixed the bug"), max_length=40 |
87 | + ) |
88 | + |
89 | + target = Reference( |
90 | + title=_("Target"), |
91 | + required=True, |
92 | + schema=Interface, # IBugTarget|IGitRepository |
93 | + ) |
94 | + |
95 | + |
96 | +class IBugPresenceEdit(Interface): |
97 | + """IBugPresence attributes that require launchpad.Edit permission.""" |
98 | + |
99 | + def destroySelf(): |
100 | + """Delete the specified bug presence.""" |
101 | + |
102 | + |
103 | +class IBugPresence(IBugPresenceView, IBugPresenceEdit): |
104 | + """ |
105 | + Represents a range of versions or git commits in which the bug was present. |
106 | + """ |
107 | + |
108 | + |
109 | +class IBugPresenceSet(Interface): |
110 | + def new( |
111 | + bug, |
112 | + target, |
113 | + broken_version, |
114 | + fixed_version, |
115 | + broken_git_commit_sha1, |
116 | + fixed_git_commit_sha1, |
117 | + ): |
118 | + """Create new BugPresence instance. |
119 | + :param bug: a bug to create a bug presence for |
120 | + :param target: a project, a distribution, a distribution package or |
121 | + a git repository |
122 | + :param broken_version: version in which the bug was introduced |
123 | + :param fixed_version: version in which the bug was fixed |
124 | + :param broken_git_commit_sha1: git commit in which the bug |
125 | + was introduced (for git repository) |
126 | + :param fixed_git_commit_sha1: git commit in which the bug |
127 | + was fixed (for git repository) |
128 | + """ |
129 | + pass |
130 | + |
131 | + def getByBug(bug): |
132 | + """Get all BugPresence instances for the given bug. |
133 | + :param bug: a bug to get the bug presence instances from |
134 | + :return: a collection of BugPresence instances |
135 | + """ |
136 | + pass |
137 | + |
138 | + def getByTarget(target): |
139 | + """Get all BugPresence instances for the given target. |
140 | + :param target: a target to get the bug presence instances for |
141 | + :return: a collection of BugPresence instances |
142 | + """ |
143 | + pass |
144 | diff --git a/lib/lp/bugs/model/bugpresence.py b/lib/lp/bugs/model/bugpresence.py |
145 | new file mode 100644 |
146 | index 0000000..b21371e |
147 | --- /dev/null |
148 | +++ b/lib/lp/bugs/model/bugpresence.py |
149 | @@ -0,0 +1,150 @@ |
150 | +from storm.properties import Int, Unicode |
151 | +from storm.references import Reference |
152 | +from storm.store import Store |
153 | +from zope.interface import implementer |
154 | + |
155 | +from lp.bugs.interfaces.bugpresence import IBugPresence, IBugPresenceSet |
156 | +from lp.code.interfaces.gitrepository import IGitRepository |
157 | +from lp.registry.interfaces.distributionsourcepackage import ( |
158 | + IDistributionSourcePackage, |
159 | +) |
160 | +from lp.registry.interfaces.product import IProduct |
161 | +from lp.services.database.interfaces import IStore |
162 | +from lp.services.database.stormbase import StormBase |
163 | + |
164 | +__all__ = ["BugPresence", "BugPresenceSet"] |
165 | + |
166 | + |
167 | +@implementer(IBugPresence) |
168 | +class BugPresence(StormBase): |
169 | + """See `IBugPresence`.""" |
170 | + |
171 | + __storm_table__ = "BugPresence" |
172 | + |
173 | + id = Int(primary=True) |
174 | + |
175 | + bug_id = Int(name="bug", allow_none=False) |
176 | + bug = Reference(bug_id, "Bug.id") |
177 | + |
178 | + project_id = Int(name="project", allow_none=True) |
179 | + project = Reference(project_id, "Product.id") |
180 | + |
181 | + source_package_name_id = Int(name="source_package_name", allow_none=True) |
182 | + source_package_name = Reference( |
183 | + source_package_name_id, "SourcePackageName.id" |
184 | + ) |
185 | + |
186 | + distribution_id = Int(name="distribution", allow_none=True) |
187 | + distribution = Reference(distribution_id, "Distribution.id") |
188 | + |
189 | + broken_version = Unicode(name="broken_version", allow_none=True) |
190 | + fixed_version = Unicode(name="fixed_version", allow_none=True) |
191 | + |
192 | + git_repository_id = Int(name="git_repository", allow_none=True) |
193 | + git_repository = Reference(git_repository_id, "GitRepository.id") |
194 | + |
195 | + broken_git_commit_sha1 = Unicode( |
196 | + name="broken_git_commit_sha1", allow_none=True |
197 | + ) |
198 | + fixed_git_commit_sha1 = Unicode( |
199 | + name="fixed_git_commit_sha1", allow_none=True |
200 | + ) |
201 | + |
202 | + def __init__( |
203 | + self, |
204 | + bug, |
205 | + target, |
206 | + broken_version=None, |
207 | + fixed_version=None, |
208 | + broken_git_commit_sha1=None, |
209 | + fixed_git_commit_sha1=None, |
210 | + ): |
211 | + self.bug = bug |
212 | + self.target = target |
213 | + self.broken_version = broken_version |
214 | + self.fixed_version = fixed_version |
215 | + self.broken_git_commit_sha1 = broken_git_commit_sha1 |
216 | + self.fixed_git_commit_sha1 = fixed_git_commit_sha1 |
217 | + |
218 | + @property |
219 | + def target(self): |
220 | + if self.project: |
221 | + return self.project |
222 | + elif self.source_package_name: |
223 | + return self.distribution.getSourcePackage(self.source_package_name) |
224 | + elif self.git_repository: |
225 | + return self.git_repository |
226 | + else: |
227 | + raise AssertionError("Could not determine BugPresence target") |
228 | + |
229 | + @target.setter |
230 | + def target(self, target): |
231 | + if IProduct.providedBy(target): |
232 | + self.project = target |
233 | + elif IDistributionSourcePackage.providedBy(target): |
234 | + self.distribution = target.distribution |
235 | + self.source_package_name = target.sourcepackagename |
236 | + elif IGitRepository.providedBy(target): |
237 | + self.git_repository = target |
238 | + else: |
239 | + raise AssertionError("Invalid BugPresence target.") |
240 | + |
241 | + def destroySelf(self): |
242 | + Store.of(self).remove(self) |
243 | + |
244 | + |
245 | +@implementer(IBugPresenceSet) |
246 | +class BugPresenceSet: |
247 | + """See `IBugPresenceSet`.""" |
248 | + |
249 | + def new( |
250 | + self, |
251 | + bug, |
252 | + target, |
253 | + broken_version=None, |
254 | + fixed_version=None, |
255 | + broken_git_commit_sha1=None, |
256 | + fixed_git_commit_sha1=None, |
257 | + ): |
258 | + |
259 | + is_version = bool(broken_version or fixed_version) |
260 | + is_git_range = bool(broken_git_commit_sha1 or fixed_git_commit_sha1) |
261 | + |
262 | + if is_version == is_git_range: |
263 | + raise ValueError( |
264 | + "Either broken_version/fixed_version or " |
265 | + "broken_git_commit_sha1/fixed_git_commit_sha1 " |
266 | + "must be specified" |
267 | + ) |
268 | + |
269 | + if target is None: |
270 | + raise ValueError("target must be specified") |
271 | + |
272 | + if is_git_range and not IGitRepository.providedBy(target): |
273 | + raise ValueError("target must be a git repository") |
274 | + |
275 | + return BugPresence( |
276 | + bug=bug, |
277 | + target=target, |
278 | + broken_version=broken_version, |
279 | + fixed_version=fixed_version, |
280 | + broken_git_commit_sha1=broken_git_commit_sha1, |
281 | + fixed_git_commit_sha1=fixed_git_commit_sha1, |
282 | + ) |
283 | + |
284 | + def getByBug(self, bug): |
285 | + return IStore(BugPresence).find(BugPresence, bug=bug) |
286 | + |
287 | + def getByTarget(self, target): |
288 | + conditions = {} |
289 | + if IProduct.providedBy(target): |
290 | + conditions["project"] = target |
291 | + elif IDistributionSourcePackage.providedBy(target): |
292 | + conditions["distribution"] = target.distribution |
293 | + conditions["source_package_name"] = target.sourcepackagename |
294 | + elif IGitRepository.providedBy(target): |
295 | + conditions["git_repository"] = target |
296 | + else: |
297 | + raise AssertionError("Invalid BugPresence target.") |
298 | + |
299 | + return IStore(BugPresence).find(BugPresence, **conditions) |
300 | diff --git a/lib/lp/bugs/security.py b/lib/lp/bugs/security.py |
301 | index 11d74e9..15ae315 100644 |
302 | --- a/lib/lp/bugs/security.py |
303 | +++ b/lib/lp/bugs/security.py |
304 | @@ -17,6 +17,7 @@ from lp.bugs.interfaces.bug import IBug |
305 | from lp.bugs.interfaces.bugactivity import IBugActivity |
306 | from lp.bugs.interfaces.bugattachment import IBugAttachment |
307 | from lp.bugs.interfaces.bugnomination import IBugNomination |
308 | +from lp.bugs.interfaces.bugpresence import IBugPresence |
309 | from lp.bugs.interfaces.bugsubscription import IBugSubscription |
310 | from lp.bugs.interfaces.bugsubscriptionfilter import IBugSubscriptionFilter |
311 | from lp.bugs.interfaces.bugsupervisor import IHasBugSupervisor |
312 | @@ -455,3 +456,35 @@ class BugTargetOwnerOrBugSupervisorOrAdmins(AuthorizationBase): |
313 | or user.inTeam(self.obj.owner) |
314 | or user.in_admin |
315 | ) |
316 | + |
317 | + |
318 | +class ViewBugPresence(DelegatedAuthorization): |
319 | + """ |
320 | + A person that can view a Bug can also view a related BugPresence. |
321 | + """ |
322 | + |
323 | + permission = "launchpad.View" |
324 | + usedfor = IBugPresence |
325 | + |
326 | + def __init__(self, obj): |
327 | + super().__init__(obj, obj.bug, "launchpad.View") |
328 | + |
329 | + def checkAuthenticated(self, user): |
330 | + r = super().checkAuthenticated(user) |
331 | + return r |
332 | + |
333 | + def checkUnauthenticated(self): |
334 | + r = super().checkUnauthenticated() |
335 | + return r |
336 | + |
337 | + |
338 | +class EditBugPresence(DelegatedAuthorization): |
339 | + """ |
340 | + A person that can edit a Bug can also edit a related BugPresence. |
341 | + """ |
342 | + |
343 | + permission = "launchpad.Edit" |
344 | + usedfor = IBugPresence |
345 | + |
346 | + def __init__(self, obj): |
347 | + super().__init__(obj, obj.bug, "launchpad.Edit") |
348 | diff --git a/lib/lp/bugs/tests/test_bugpresence.py b/lib/lp/bugs/tests/test_bugpresence.py |
349 | new file mode 100644 |
350 | index 0000000..e328dff |
351 | --- /dev/null |
352 | +++ b/lib/lp/bugs/tests/test_bugpresence.py |
353 | @@ -0,0 +1,337 @@ |
354 | +import transaction |
355 | +from psycopg2.errors import CheckViolation |
356 | +from zope.component import getUtility |
357 | +from zope.security import checkPermission |
358 | + |
359 | +from lp.app.enums import InformationType |
360 | +from lp.bugs.interfaces.bugpresence import IBugPresenceSet |
361 | +from lp.bugs.model.bugpresence import BugPresence |
362 | +from lp.registry.model.distributionsourcepackage import ( |
363 | + DistributionSourcePackage, |
364 | +) |
365 | +from lp.services.database.interfaces import IStore |
366 | +from lp.testing import ( |
367 | + TestCaseWithFactory, |
368 | + anonymous_logged_in, |
369 | + person_logged_in, |
370 | +) |
371 | +from lp.testing.layers import DatabaseFunctionalLayer, ZopelessDatabaseLayer |
372 | + |
373 | + |
374 | +class TestBugPresence(TestCaseWithFactory): |
375 | + |
376 | + layer = ZopelessDatabaseLayer |
377 | + |
378 | + def test_target(self): |
379 | + project = self.factory.makeProduct() |
380 | + distribution = self.factory.makeDistribution() |
381 | + spn = self.factory.makeSourcePackageName() |
382 | + |
383 | + for target, attrs in ( |
384 | + (project, {"project": project}), |
385 | + ( |
386 | + DistributionSourcePackage(distribution, spn), |
387 | + {"distribution": distribution, "source_package_name": spn}, |
388 | + ), |
389 | + ): |
390 | + bug_presence = BugPresence( |
391 | + bug=self.factory.makeBug(), |
392 | + target=target, |
393 | + broken_version="1", |
394 | + ) |
395 | + self.assertEqual(target, bug_presence.target) |
396 | + for attr_name, value in attrs.items(): |
397 | + self.assertEqual(value, getattr(bug_presence, attr_name)) |
398 | + |
399 | + git_repository = self.factory.makeGitRepository() |
400 | + bug_presence = BugPresence( |
401 | + bug=self.factory.makeBug(), |
402 | + target=git_repository, |
403 | + broken_git_commit_sha1="1" * 40, |
404 | + ) |
405 | + self.assertEqual(git_repository, bug_presence.target) |
406 | + self.assertEqual(git_repository, bug_presence.git_repository) |
407 | + |
408 | + def test_constraints(self): |
409 | + project = self.factory.makeProduct() |
410 | + distro_package = self.factory.makeDistributionSourcePackage() |
411 | + git_repository = self.factory.makeGitRepository() |
412 | + |
413 | + store = IStore(BugPresence) |
414 | + store.commit() |
415 | + |
416 | + # no version range, nor git commit range |
417 | + for target in project, distro_package, git_repository: |
418 | + BugPresence(bug=self.factory.makeBug(), target=target) |
419 | + self.assertRaises(CheckViolation, store.flush) |
420 | + store.rollback() |
421 | + |
422 | + # invalid version target |
423 | + BugPresence( |
424 | + bug=self.factory.makeBug(), |
425 | + target=git_repository, |
426 | + broken_version="1", |
427 | + ) |
428 | + self.assertRaises(CheckViolation, store.flush) |
429 | + store.rollback() |
430 | + |
431 | + # invalid git range target |
432 | + for target in project, distro_package: |
433 | + BugPresence( |
434 | + bug=self.factory.makeBug(), |
435 | + target=target, |
436 | + broken_git_commit_sha1="1" * 40, |
437 | + ) |
438 | + self.assertRaises(CheckViolation, store.flush) |
439 | + store.rollback() |
440 | + |
441 | + # valid version range |
442 | + for broken_version, fixed_version in ( |
443 | + ("1", None), |
444 | + (None, "2"), |
445 | + ("1", "2"), |
446 | + ): |
447 | + for target in project, distro_package: |
448 | + bp = BugPresence( |
449 | + bug=self.factory.makeBug(), |
450 | + target=target, |
451 | + broken_version=broken_version, |
452 | + fixed_version=fixed_version, |
453 | + ) |
454 | + store.commit() |
455 | + bp = store.get(BugPresence, bp.id) |
456 | + self.assertEqual(broken_version, bp.broken_version) |
457 | + self.assertEqual(fixed_version, bp.fixed_version) |
458 | + |
459 | + # valid git commit range |
460 | + for broken_git_commit_sha1, fixed_git_commit_sha1 in ( |
461 | + ("1" * 40, None), |
462 | + (None, "2" * 40), |
463 | + ("1" * 40, "2" * 40), |
464 | + ): |
465 | + bp = BugPresence( |
466 | + bug=self.factory.makeBug(), |
467 | + target=git_repository, |
468 | + broken_git_commit_sha1=broken_git_commit_sha1, |
469 | + fixed_git_commit_sha1=fixed_git_commit_sha1, |
470 | + ) |
471 | + store.commit() |
472 | + bp = store.get(BugPresence, bp.id) |
473 | + self.assertEqual(broken_git_commit_sha1, bp.broken_git_commit_sha1) |
474 | + self.assertEqual(fixed_git_commit_sha1, bp.fixed_git_commit_sha1) |
475 | + |
476 | + def test_referenced_git_repository_can_be_deleted(self): |
477 | + git_repository = self.factory.makeGitRepository() |
478 | + bug = self.factory.makeBug() |
479 | + bug_presence_to_delete = self.factory.makeBugPresence( |
480 | + bug=bug, |
481 | + target=git_repository, |
482 | + broken_git_commit_sha1="1" * 40, |
483 | + ) |
484 | + bug_presence_to_keep = self.factory.makeBugPresence(bug=bug) |
485 | + git_repository.destroySelf(break_references=True) |
486 | + transaction.commit() |
487 | + bug_presences = list(getUtility(IBugPresenceSet).getByBug(bug)) |
488 | + self.assertIn(bug_presence_to_keep, bug_presences) |
489 | + self.assertNotIn(bug_presence_to_delete, bug_presences) |
490 | + |
491 | + |
492 | +class TestBugPresenceSecurity(TestCaseWithFactory): |
493 | + |
494 | + layer = DatabaseFunctionalLayer |
495 | + |
496 | + def test_view_permission(self): |
497 | + project = self.factory.makeProduct() |
498 | + |
499 | + # Anyone can view a public bug presence |
500 | + public_bug = self.factory.makeBug( |
501 | + target=project, information_type=InformationType.PUBLIC |
502 | + ) |
503 | + bp = BugPresence(bug=public_bug, target=project, broken_version="1") |
504 | + with anonymous_logged_in(): |
505 | + self.assertTrue(checkPermission("launchpad.View", bp)) |
506 | + |
507 | + # Anonymous and random users can't see a private bug presence |
508 | + owner = self.factory.makePerson() |
509 | + project = self.factory.makeProduct( |
510 | + information_type=InformationType.PROPRIETARY, owner=owner |
511 | + ) |
512 | + with person_logged_in(owner): |
513 | + private_bug = self.factory.makeBug( |
514 | + target=project, |
515 | + information_type=InformationType.PROPRIETARY, |
516 | + owner=owner, |
517 | + ) |
518 | + bp = BugPresence(bug=private_bug, target=project, broken_version="1") |
519 | + with anonymous_logged_in(): |
520 | + self.assertFalse(checkPermission("launchpad.View", bp)) |
521 | + with person_logged_in(self.factory.makePerson()): |
522 | + self.assertFalse(checkPermission("launchpad.View", bp)) |
523 | + |
524 | + # Private bug owners can see a private bug presence |
525 | + with person_logged_in(owner): |
526 | + self.assertTrue(checkPermission("launchpad.View", bp)) |
527 | + |
528 | + def test_edit_permission(self): |
529 | + project = self.factory.makeProduct() |
530 | + |
531 | + public_bug = self.factory.makeBug( |
532 | + target=project, information_type=InformationType.PUBLIC |
533 | + ) |
534 | + bp = BugPresence(bug=public_bug, target=project, broken_version="1") |
535 | + # Anonymous can't edit a public bug presence |
536 | + with anonymous_logged_in(): |
537 | + self.assertFalse(checkPermission("launchpad.Edit", bp)) |
538 | + |
539 | + # Any authenticated user can edit a public bug presence |
540 | + with person_logged_in(self.factory.makePerson()): |
541 | + self.assertTrue(checkPermission("launchpad.Edit", bp)) |
542 | + |
543 | + # Anonymous and random users can't edit a private bug presence |
544 | + owner = self.factory.makePerson() |
545 | + project = self.factory.makeProduct( |
546 | + information_type=InformationType.PROPRIETARY, owner=owner |
547 | + ) |
548 | + with person_logged_in(owner): |
549 | + private_bug = self.factory.makeBug( |
550 | + target=project, |
551 | + information_type=InformationType.PROPRIETARY, |
552 | + owner=owner, |
553 | + ) |
554 | + bp = BugPresence(bug=private_bug, target=project, broken_version="1") |
555 | + with anonymous_logged_in(): |
556 | + self.assertFalse(checkPermission("launchpad.Edit", bp)) |
557 | + with person_logged_in(self.factory.makePerson()): |
558 | + self.assertFalse(checkPermission("launchpad.Edit", bp)) |
559 | + |
560 | + # Private bug owners can edit a private bug presence |
561 | + with person_logged_in(owner): |
562 | + self.assertTrue(checkPermission("launchpad.Edit", bp)) |
563 | + |
564 | + |
565 | +class TestBugPresenceSet(TestCaseWithFactory): |
566 | + |
567 | + layer = DatabaseFunctionalLayer |
568 | + |
569 | + def test_new__version_range(self): |
570 | + project = self.factory.makeProduct() |
571 | + distro_package = self.factory.makeDistributionSourcePackage() |
572 | + |
573 | + bug_presence_set = getUtility(IBugPresenceSet) |
574 | + |
575 | + # version range |
576 | + for broken_version, fixed_version in ( |
577 | + ("1", None), |
578 | + (None, "2"), |
579 | + ("1", "2"), |
580 | + ): |
581 | + for target in project, distro_package: |
582 | + bug = self.factory.makeBug() |
583 | + bp = bug_presence_set.new( |
584 | + bug=bug, |
585 | + target=target, |
586 | + broken_version=broken_version, |
587 | + fixed_version=fixed_version, |
588 | + ) |
589 | + self.assertEqual(bug, bp.bug) |
590 | + self.assertEqual(target, bp.target) |
591 | + self.assertEqual(broken_version, bp.broken_version) |
592 | + self.assertEqual(fixed_version, bp.fixed_version) |
593 | + |
594 | + def test_new__git_commit_range(self): |
595 | + git_repository = self.factory.makeGitRepository() |
596 | + |
597 | + bug_presence_set = getUtility(IBugPresenceSet) |
598 | + |
599 | + # version range |
600 | + for broken_git_commit_sha1, fixed_git_commit_sha1 in ( |
601 | + ("1" * 40, None), |
602 | + (None, "2" * 40), |
603 | + ("1" * 40, "2" * 40), |
604 | + ): |
605 | + bug = self.factory.makeBug() |
606 | + bp = bug_presence_set.new( |
607 | + bug=bug, |
608 | + target=git_repository, |
609 | + broken_git_commit_sha1=broken_git_commit_sha1, |
610 | + fixed_git_commit_sha1=fixed_git_commit_sha1, |
611 | + ) |
612 | + self.assertEqual(bug, bp.bug) |
613 | + self.assertEqual(git_repository, bp.target) |
614 | + self.assertEqual(broken_git_commit_sha1, bp.broken_git_commit_sha1) |
615 | + self.assertEqual(fixed_git_commit_sha1, bp.fixed_git_commit_sha1) |
616 | + |
617 | + def test_new__invalid_arguments(self): |
618 | + project = self.factory.makeProduct() |
619 | + bug = self.factory.makeBug() |
620 | + bug_presence_set = getUtility(IBugPresenceSet) |
621 | + |
622 | + # no version range nor git commit range is specified |
623 | + self.assertRaises( |
624 | + ValueError, bug_presence_set.new, bug=bug, target=project |
625 | + ) |
626 | + |
627 | + # both version range and commit range is specified |
628 | + self.assertRaises( |
629 | + ValueError, |
630 | + bug_presence_set.new, |
631 | + bug=bug, |
632 | + target=project, |
633 | + broken_version="1", |
634 | + fixed_git_commit_sha1="2", |
635 | + ) |
636 | + |
637 | + # target is not a git repository for git commit range |
638 | + self.assertRaises( |
639 | + ValueError, |
640 | + bug_presence_set.new, |
641 | + bug=bug, |
642 | + target=project, |
643 | + fixed_git_commit_sha1="2", |
644 | + ) |
645 | + |
646 | + # target is missing |
647 | + self.assertRaises( |
648 | + ValueError, |
649 | + bug_presence_set.new, |
650 | + bug=bug, |
651 | + target=None, |
652 | + broken_version="1", |
653 | + ) |
654 | + |
655 | + def test_getByBug(self): |
656 | + project = self.factory.makeProduct() |
657 | + bug = self.factory.makeBug() |
658 | + another_bug = self.factory.makeBug() |
659 | + bug_presence_set = getUtility(IBugPresenceSet) |
660 | + |
661 | + self.assertEqual([], list(bug_presence_set.getByBug(bug))) |
662 | + self.assertEqual([], list(bug_presence_set.getByBug(another_bug))) |
663 | + |
664 | + bp_1 = bug_presence_set.new( |
665 | + bug=bug, target=project, broken_version="1" |
666 | + ) |
667 | + bp_2 = bug_presence_set.new(bug=bug, target=project, fixed_version="2") |
668 | + bp_3 = bug_presence_set.new( |
669 | + bug=another_bug, target=project, broken_version="3" |
670 | + ) |
671 | + self.assertEqual({bp_1, bp_2}, set(bug_presence_set.getByBug(bug))) |
672 | + self.assertEqual({bp_3}, set(bug_presence_set.getByBug(another_bug))) |
673 | + |
674 | + |
675 | +class TestBugPresenceFactory(TestCaseWithFactory): |
676 | + |
677 | + layer = ZopelessDatabaseLayer |
678 | + |
679 | + def test_makeBugPresence__no_arguments(self): |
680 | + bp = self.factory.makeBugPresence() |
681 | + self.assertIsNotNone(bp.target) |
682 | + self.assertIsNotNone(bp.broken_version) |
683 | + self.assertIsNotNone(bp.fixed_version) |
684 | + |
685 | + def test_makeBugPresence__git_repository(self): |
686 | + git_repository = self.factory.makeGitRepository() |
687 | + bp = self.factory.makeBugPresence(target=git_repository) |
688 | + self.assertEqual(git_repository, bp.git_repository) |
689 | + self.assertIsNotNone(bp.broken_git_commit_sha1) |
690 | + self.assertIsNotNone(bp.fixed_git_commit_sha1) |
691 | diff --git a/lib/lp/code/model/gitrepository.py b/lib/lp/code/model/gitrepository.py |
692 | index bd09864..dee8312 100644 |
693 | --- a/lib/lp/code/model/gitrepository.py |
694 | +++ b/lib/lp/code/model/gitrepository.py |
695 | @@ -48,6 +48,7 @@ from lp.app.errors import ( |
696 | from lp.app.interfaces.informationtype import IInformationType |
697 | from lp.app.interfaces.launchpad import ILaunchpadCelebrities |
698 | from lp.app.interfaces.services import IService |
699 | +from lp.bugs.interfaces.bugpresence import IBugPresenceSet |
700 | from lp.charms.interfaces.charmrecipe import ICharmRecipeSet |
701 | from lp.code.adapters.branch import BranchMergeProposalNoPreviewDiffDelta |
702 | from lp.code.enums import ( |
703 | @@ -1955,6 +1956,15 @@ class GitRepository( |
704 | ) |
705 | for recipe in recipes |
706 | ) |
707 | + bug_presences = getUtility(IBugPresenceSet).getByTarget(self) |
708 | + deletion_operations.extend( |
709 | + DeletionCallable( |
710 | + bug_presence, |
711 | + msg("This bug presence refers to this repository."), |
712 | + bug_presence.destroySelf, |
713 | + ) |
714 | + for bug_presence in bug_presences |
715 | + ) |
716 | if not getUtility(ISnapSet).findByGitRepository(self).is_empty(): |
717 | alteration_operations.append( |
718 | DeletionCallable( |
719 | diff --git a/lib/lp/testing/factory.py b/lib/lp/testing/factory.py |
720 | index b95c374..e43d9fc 100644 |
721 | --- a/lib/lp/testing/factory.py |
722 | +++ b/lib/lp/testing/factory.py |
723 | @@ -65,6 +65,7 @@ from lp.blueprints.interfaces.specification import ISpecificationSet |
724 | from lp.blueprints.interfaces.sprint import ISprintSet |
725 | from lp.bugs.interfaces.apportjob import IProcessApportBlobJobSource |
726 | from lp.bugs.interfaces.bug import CreateBugParams, IBugSet |
727 | +from lp.bugs.interfaces.bugpresence import IBugPresenceSet |
728 | from lp.bugs.interfaces.bugtask import ( |
729 | BugTaskImportance, |
730 | BugTaskStatus, |
731 | @@ -2652,6 +2653,38 @@ class LaunchpadObjectFactory(ObjectFactory): |
732 | ) |
733 | ) |
734 | |
735 | + def makeBugPresence( |
736 | + self, |
737 | + bug=None, |
738 | + target=None, |
739 | + broken_version=None, |
740 | + fixed_version=None, |
741 | + broken_git_commit_sha1=None, |
742 | + fixed_git_commit_sha1=None, |
743 | + ): |
744 | + if bug is None: |
745 | + bug = self.makeBug() |
746 | + if target is None: |
747 | + target = self.makeProduct() |
748 | + |
749 | + if IGitRepository.providedBy(target): |
750 | + if not broken_git_commit_sha1 and not fixed_git_commit_sha1: |
751 | + broken_git_commit_sha1 = self.getUniqueHexString(40) |
752 | + fixed_git_commit_sha1 = self.getUniqueHexString(40) |
753 | + |
754 | + elif not broken_version and not fixed_version: |
755 | + broken_version = self.getUniqueString("version") |
756 | + fixed_version = self.getUniqueString("version") |
757 | + |
758 | + return getUtility(IBugPresenceSet).new( |
759 | + bug=bug, |
760 | + target=target, |
761 | + broken_version=broken_version, |
762 | + fixed_version=fixed_version, |
763 | + broken_git_commit_sha1=broken_git_commit_sha1, |
764 | + fixed_git_commit_sha1=fixed_git_commit_sha1, |
765 | + ) |
766 | + |
767 | def makeSignedMessage( |
768 | self, |
769 | msgid=None, |