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