Merge ~ines-almeida/launchpad:add-bug-webhooks/add-interfaces into launchpad:master
- Git
- lp:~ines-almeida/launchpad
- add-bug-webhooks/add-interfaces
- Merge into master
Proposed by
Ines Almeida
Status: | Merged |
---|---|
Approved by: | Ines Almeida |
Approved revision: | ba52f95c33b98c3a1806cf3c3a68075944346594 |
Merge reported by: | Otto Co-Pilot |
Merged at revision: | not available |
Proposed branch: | ~ines-almeida/launchpad:add-bug-webhooks/add-interfaces |
Merge into: | launchpad:master |
Prerequisite: | ~ines-almeida/launchpad:add-bug-webhooks/update-webhook-model |
Diff against target: |
845 lines (+285/-84) 15 files modified
lib/lp/bugs/interfaces/bugtarget.py (+4/-0) lib/lp/registry/browser/distribution.py (+24/-12) lib/lp/registry/browser/distributionsourcepackage.py (+15/-1) lib/lp/registry/browser/product.py (+14/-1) lib/lp/registry/configure.zcml (+4/-48) lib/lp/registry/interfaces/distribution.py (+4/-1) lib/lp/registry/interfaces/distributionsourcepackage.py (+21/-8) lib/lp/registry/interfaces/product.py (+2/-1) lib/lp/registry/model/distribution.py (+6/-0) lib/lp/registry/model/distributionsourcepackage.py (+6/-0) lib/lp/registry/model/product.py (+6/-0) lib/lp/services/features/flags.py (+8/-0) lib/lp/services/webhooks/interfaces.py (+2/-0) lib/lp/services/webhooks/tests/test_browser.py (+120/-5) lib/lp/services/webhooks/tests/test_webservice.py (+49/-7) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Colin Watson (community) | Approve | ||
Review via email: mp+442544@code.launchpad.net |
Commit message
Add interfaces to add new webhooks for bugtask targets
Webhooks can now be added to a project, a distribution or a distribution source package.
Description of the change
This will add the user interfaces to add new webhooks for the new targets (to be used with the new `bug` and `bug:comment` events)
All interfaces are hidden under a new feature flag, so these pages won't be exposed until the feature flag is set.
This MP is based of another open MP: https:/
The webhooks are not hooked to anything in this MP - that will be handled in another MP
To post a comment you must log in.
Revision history for this message
Colin Watson (cjwatson) : | # |
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/bugs/interfaces/bugtarget.py b/lib/lp/bugs/interfaces/bugtarget.py |
2 | index 57bc0fe..540079f 100644 |
3 | --- a/lib/lp/bugs/interfaces/bugtarget.py |
4 | +++ b/lib/lp/bugs/interfaces/bugtarget.py |
5 | @@ -15,6 +15,7 @@ __all__ = [ |
6 | "ISeriesBugTarget", |
7 | "BUG_POLICY_ALLOWED_TYPES", |
8 | "BUG_POLICY_DEFAULT_TYPES", |
9 | + "BUG_WEBHOOKS_FEATURE_FLAG", |
10 | ] |
11 | |
12 | |
13 | @@ -156,6 +157,9 @@ BUG_POLICY_DEFAULT_TYPES = { |
14 | } |
15 | |
16 | |
17 | +BUG_WEBHOOKS_FEATURE_FLAG = "bugs.webhooks.enabled" |
18 | + |
19 | + |
20 | @exported_as_webservice_entry(as_of="beta") |
21 | class IHasBugs(Interface): |
22 | """An entity which has a collection of bug tasks.""" |
23 | diff --git a/lib/lp/registry/browser/distribution.py b/lib/lp/registry/browser/distribution.py |
24 | index 3fb8a36..b990f6f 100644 |
25 | --- a/lib/lp/registry/browser/distribution.py |
26 | +++ b/lib/lp/registry/browser/distribution.py |
27 | @@ -83,6 +83,7 @@ from lp.bugs.browser.structuralsubscription import ( |
28 | StructuralSubscriptionTargetTraversalMixin, |
29 | expose_structural_subscription_data_to_js, |
30 | ) |
31 | +from lp.bugs.interfaces.bugtarget import BUG_WEBHOOKS_FEATURE_FLAG |
32 | from lp.buildmaster.interfaces.processor import IProcessorSet |
33 | from lp.code.browser.vcslisting import TargetDefaultVCSNavigationMixin |
34 | from lp.registry.browser import RegistryEditFormView, add_subscribe_link |
35 | @@ -141,6 +142,7 @@ from lp.services.webapp.authorization import check_permission |
36 | from lp.services.webapp.batching import BatchNavigator |
37 | from lp.services.webapp.breadcrumb import Breadcrumb |
38 | from lp.services.webapp.interfaces import ILaunchBag |
39 | +from lp.services.webhooks.browser import WebhookTargetNavigationMixin |
40 | from lp.soyuz.browser.archive import EnableProcessorsMixin |
41 | from lp.soyuz.browser.packagesearch import PackageSearchViewBase |
42 | from lp.soyuz.enums import ArchivePurpose |
43 | @@ -155,6 +157,7 @@ class DistributionNavigation( |
44 | StructuralSubscriptionTargetTraversalMixin, |
45 | PillarNavigationMixin, |
46 | TargetDefaultVCSNavigationMixin, |
47 | + WebhookTargetNavigationMixin, |
48 | ): |
49 | |
50 | usedfor = IDistribution |
51 | @@ -388,6 +391,18 @@ class DistributionNavigationMenu(NavigationMenu, DistributionLinksMixin): |
52 | usedfor = IDistribution |
53 | facet = "overview" |
54 | |
55 | + links = ( |
56 | + "edit", |
57 | + "admin", |
58 | + "pubconf", |
59 | + "subscribe_to_bug_mail", |
60 | + "edit_bug_mail", |
61 | + "sharing", |
62 | + "new_oci_project", |
63 | + "search_oci_project", |
64 | + "webhooks", |
65 | + ) |
66 | + |
67 | @enabled_with_permission("launchpad.Admin") |
68 | def admin(self): |
69 | text = "Administer" |
70 | @@ -419,18 +434,14 @@ class DistributionNavigationMenu(NavigationMenu, DistributionLinksMixin): |
71 | link.enabled = not oci_projects.is_empty() |
72 | return link |
73 | |
74 | - @cachedproperty |
75 | - def links(self): |
76 | - return [ |
77 | - "edit", |
78 | - "admin", |
79 | - "pubconf", |
80 | - "subscribe_to_bug_mail", |
81 | - "edit_bug_mail", |
82 | - "sharing", |
83 | - "new_oci_project", |
84 | - "search_oci_project", |
85 | - ] |
86 | + @enabled_with_permission("launchpad.Edit") |
87 | + def webhooks(self): |
88 | + return Link( |
89 | + "+webhooks", |
90 | + "Manage webhooks", |
91 | + icon="edit", |
92 | + enabled=bool(getFeatureFlag(BUG_WEBHOOKS_FEATURE_FLAG)), |
93 | + ) |
94 | |
95 | |
96 | class DistributionOverviewMenu(ApplicationMenu, DistributionLinksMixin): |
97 | @@ -623,6 +634,7 @@ class DistributionBugsMenu(PillarBugsMenu): |
98 | def links(self): |
99 | links = ["bugsupervisor", "cve", "filebug"] |
100 | add_subscribe_link(links) |
101 | + links.append("webhooks") |
102 | return links |
103 | |
104 | |
105 | diff --git a/lib/lp/registry/browser/distributionsourcepackage.py b/lib/lp/registry/browser/distributionsourcepackage.py |
106 | index a55097b..e0d960a 100644 |
107 | --- a/lib/lp/registry/browser/distributionsourcepackage.py |
108 | +++ b/lib/lp/registry/browser/distributionsourcepackage.py |
109 | @@ -42,6 +42,7 @@ from lp.bugs.browser.structuralsubscription import ( |
110 | StructuralSubscriptionTargetTraversalMixin, |
111 | expose_structural_subscription_data_to_js, |
112 | ) |
113 | +from lp.bugs.interfaces.bugtarget import BUG_WEBHOOKS_FEATURE_FLAG |
114 | from lp.bugs.interfaces.bugtask import BugTaskStatus |
115 | from lp.bugs.interfaces.bugtasksearch import BugTaskSearchParams |
116 | from lp.code.browser.vcslisting import TargetDefaultVCSNavigationMixin |
117 | @@ -54,6 +55,7 @@ from lp.registry.interfaces.distributionsourcepackage import ( |
118 | from lp.registry.interfaces.person import IPersonSet |
119 | from lp.registry.interfaces.series import SeriesStatus |
120 | from lp.services.database.decoratedresultset import DecoratedResultSet |
121 | +from lp.services.features import getFeatureFlag |
122 | from lp.services.helpers import shortlist |
123 | from lp.services.propertycache import cachedproperty |
124 | from lp.services.webapp import ( |
125 | @@ -76,6 +78,7 @@ from lp.services.webapp.menu import ( |
126 | ) |
127 | from lp.services.webapp.publisher import LaunchpadView |
128 | from lp.services.webapp.sorting import sorted_dotted_numbers |
129 | +from lp.services.webhooks.browser import WebhookTargetNavigationMixin |
130 | from lp.soyuz.browser.sourcepackagerelease import linkify_changelog |
131 | from lp.soyuz.interfaces.archive import IArchiveSet |
132 | from lp.soyuz.interfaces.distributionsourcepackagerelease import ( |
133 | @@ -170,6 +173,15 @@ class DistributionSourcePackageLinksMixin: |
134 | get_data = "?field.status=OPEN" |
135 | return Link(base_path + get_data, "Open Questions", site="answers") |
136 | |
137 | + @enabled_with_permission("launchpad.Edit") |
138 | + def webhooks(self): |
139 | + return Link( |
140 | + "+webhooks", |
141 | + "Manage webhooks", |
142 | + icon="edit", |
143 | + enabled=bool(getFeatureFlag(BUG_WEBHOOKS_FEATURE_FLAG)), |
144 | + ) |
145 | + |
146 | |
147 | class DistributionSourcePackageOverviewMenu( |
148 | ApplicationMenu, DistributionSourcePackageLinksMixin |
149 | @@ -193,6 +205,7 @@ class DistributionSourcePackageBugsMenu( |
150 | def links(self): |
151 | links = ["filebug"] |
152 | add_subscribe_link(links) |
153 | + links.append("webhooks") |
154 | return links |
155 | |
156 | |
157 | @@ -214,6 +227,7 @@ class DistributionSourcePackageNavigation( |
158 | QuestionTargetTraversalMixin, |
159 | TargetDefaultVCSNavigationMixin, |
160 | StructuralSubscriptionTargetTraversalMixin, |
161 | + WebhookTargetNavigationMixin, |
162 | ): |
163 | |
164 | usedfor = IDistributionSourcePackage |
165 | @@ -285,7 +299,7 @@ class DistributionSourcePackageActionMenu( |
166 | def links(self): |
167 | links = ["publishing_history", "change_log"] |
168 | add_subscribe_link(links) |
169 | - links.append("edit") |
170 | + links.extend(["edit", "webhooks"]) |
171 | return links |
172 | |
173 | def publishing_history(self): |
174 | diff --git a/lib/lp/registry/browser/product.py b/lib/lp/registry/browser/product.py |
175 | index 56c03a3..907d7f1 100644 |
176 | --- a/lib/lp/registry/browser/product.py |
177 | +++ b/lib/lp/registry/browser/product.py |
178 | @@ -111,6 +111,7 @@ from lp.bugs.browser.structuralsubscription import ( |
179 | StructuralSubscriptionTargetTraversalMixin, |
180 | expose_structural_subscription_data_to_js, |
181 | ) |
182 | +from lp.bugs.interfaces.bugtarget import BUG_WEBHOOKS_FEATURE_FLAG |
183 | from lp.bugs.interfaces.bugtask import RESOLVED_BUGTASK_STATUSES |
184 | from lp.charms.browser.hascharmrecipes import HasCharmRecipesMenuMixin |
185 | from lp.code.browser.branchref import BranchRef |
186 | @@ -190,6 +191,7 @@ from lp.services.webapp.interfaces import UnsafeFormGetSubmissionError |
187 | from lp.services.webapp.menu import NavigationMenu |
188 | from lp.services.webapp.url import urlsplit |
189 | from lp.services.webapp.vhosts import allvhosts |
190 | +from lp.services.webhooks.browser import WebhookTargetNavigationMixin |
191 | from lp.services.worlddata.helpers import browser_languages |
192 | from lp.services.worlddata.interfaces.country import ICountry |
193 | from lp.snappy.browser.hassnaps import HasSnapsMenuMixin |
194 | @@ -210,6 +212,7 @@ class ProductNavigation( |
195 | StructuralSubscriptionTargetTraversalMixin, |
196 | PillarNavigationMixin, |
197 | TargetDefaultVCSNavigationMixin, |
198 | + WebhookTargetNavigationMixin, |
199 | ): |
200 | |
201 | usedfor = IProduct |
202 | @@ -512,6 +515,15 @@ class ProductEditLinksMixin(StructuralSubscriptionMenuMixin): |
203 | ) and product.canAdministerOCIProjects(self.user) |
204 | return link |
205 | |
206 | + @enabled_with_permission("launchpad.Edit") |
207 | + def webhooks(self): |
208 | + return Link( |
209 | + "+webhooks", |
210 | + "Manage webhooks", |
211 | + icon="edit", |
212 | + enabled=bool(getFeatureFlag(BUG_WEBHOOKS_FEATURE_FLAG)), |
213 | + ) |
214 | + |
215 | |
216 | class IProductEditMenu(Interface): |
217 | """A marker interface for the 'Change details' navigation menu.""" |
218 | @@ -537,6 +549,7 @@ class ProductActionNavigationMenu(NavigationMenu, ProductEditLinksMixin): |
219 | "sharing", |
220 | "search_oci_project", |
221 | "new_oci_project", |
222 | + "webhooks", |
223 | ] |
224 | add_subscribe_link(links) |
225 | return links |
226 | @@ -648,7 +661,7 @@ class ProductBugsMenu(PillarBugsMenu, ProductEditLinksMixin): |
227 | def links(self): |
228 | links = ["filebug", "bugsupervisor", "cve"] |
229 | add_subscribe_link(links) |
230 | - links.append("configure_bugtracker") |
231 | + links.extend(["configure_bugtracker", "webhooks"]) |
232 | return links |
233 | |
234 | |
235 | diff --git a/lib/lp/registry/configure.zcml b/lib/lp/registry/configure.zcml |
236 | index 22178f0..b2dc87f 100644 |
237 | --- a/lib/lp/registry/configure.zcml |
238 | +++ b/lib/lp/registry/configure.zcml |
239 | @@ -550,48 +550,14 @@ |
240 | <class |
241 | class="lp.registry.model.distributionsourcepackage.DistributionSourcePackage"> |
242 | <allow interface="lp.bugs.interfaces.bugsummary.IBugSummaryDimension"/> |
243 | - <allow interface="lp.bugs.interfaces.bugtarget.IBugTarget"/> |
244 | <allow |
245 | interface="lp.translations.interfaces.customlanguagecode.IHasCustomLanguageCodes"/> |
246 | |
247 | <allow |
248 | - attributes=" |
249 | - __eq__ |
250 | - __getitem__ |
251 | - __ne__ |
252 | - _getOfficialTagClause |
253 | - binary_names |
254 | - bug_count |
255 | - bugtasks |
256 | - current_publishing_records |
257 | - currentrelease |
258 | - delete |
259 | - development_version |
260 | - display_name |
261 | - displayname |
262 | - distribution |
263 | - distro |
264 | - drivers |
265 | - findRelatedArchivePublications |
266 | - findRelatedArchives |
267 | - getBranches |
268 | - getMergeProposals |
269 | - getReleasesAndPublishingHistory |
270 | - getUsedBugTagsWithOpenCounts |
271 | - getVersion |
272 | - get_distroseries_packages |
273 | - is_official |
274 | - latest_overall_publication |
275 | - name |
276 | - official_bug_tags |
277 | - personHasDriverRights |
278 | - publishing_history |
279 | - releases |
280 | - sourcepackagename |
281 | - subscribers |
282 | - summary |
283 | - title |
284 | - upstream_product"/> |
285 | + interface="lp.registry.interfaces.distributionsourcepackage.IDistributionSourcePackageView"/> |
286 | + <require |
287 | + permission="launchpad.Edit" |
288 | + interface="lp.registry.interfaces.distributionsourcepackage.IDistributionSourcePackageEdit"/> |
289 | |
290 | <!-- IStructuralSubscriptionTarget --> |
291 | |
292 | @@ -615,16 +581,6 @@ |
293 | bug_reporting_guidelines |
294 | enable_bugfiling_duplicate_search |
295 | "/> |
296 | - |
297 | - <!-- IHasGitRepositories --> |
298 | - |
299 | - <allow |
300 | - interface="lp.code.interfaces.hasgitrepositories.IHasGitRepositories" /> |
301 | - |
302 | - <!-- IHasCodeImports --> |
303 | - |
304 | - <allow |
305 | - interface="lp.code.interfaces.hasbranches.IHasCodeImports" /> |
306 | </class> |
307 | <adapter |
308 | provides="lp.registry.interfaces.distribution.IDistribution" |
309 | diff --git a/lib/lp/registry/interfaces/distribution.py b/lib/lp/registry/interfaces/distribution.py |
310 | index 7fdc5ff..ef966c7 100644 |
311 | --- a/lib/lp/registry/interfaces/distribution.py |
312 | +++ b/lib/lp/registry/interfaces/distribution.py |
313 | @@ -102,6 +102,7 @@ from lp.services.fields import ( |
314 | Summary, |
315 | Title, |
316 | ) |
317 | +from lp.services.webhooks.interfaces import IWebhookTarget |
318 | from lp.soyuz.interfaces.buildrecords import IHasBuildRecords |
319 | from lp.translations.interfaces.hastranslationimports import ( |
320 | IHasTranslationImports, |
321 | @@ -1064,7 +1065,9 @@ class IDistributionView( |
322 | """Return the vulnerability in this distribution with the given id.""" |
323 | |
324 | |
325 | -class IDistributionEditRestricted(IOfficialBugTagTargetRestricted): |
326 | +class IDistributionEditRestricted( |
327 | + IOfficialBugTagTargetRestricted, IWebhookTarget |
328 | +): |
329 | """IDistribution properties requiring launchpad.Edit permission.""" |
330 | |
331 | @mutator_for(IDistributionView["bug_sharing_policy"]) |
332 | diff --git a/lib/lp/registry/interfaces/distributionsourcepackage.py b/lib/lp/registry/interfaces/distributionsourcepackage.py |
333 | index cba5873..1f116fc 100644 |
334 | --- a/lib/lp/registry/interfaces/distributionsourcepackage.py |
335 | +++ b/lib/lp/registry/interfaces/distributionsourcepackage.py |
336 | @@ -27,27 +27,22 @@ from lp.code.interfaces.hasbranches import ( |
337 | from lp.code.interfaces.hasgitrepositories import IHasGitRepositories |
338 | from lp.registry.interfaces.distribution import IDistribution |
339 | from lp.registry.interfaces.role import IHasDrivers |
340 | +from lp.services.webhooks.interfaces import IWebhookTarget |
341 | from lp.soyuz.enums import ArchivePurpose |
342 | |
343 | |
344 | @exported_as_webservice_entry(as_of="beta") |
345 | -class IDistributionSourcePackage( |
346 | +class IDistributionSourcePackageView( |
347 | IHeadingContext, |
348 | IBugTarget, |
349 | IHasBranches, |
350 | IHasMergeProposals, |
351 | IHasOfficialBugTags, |
352 | - IStructuralSubscriptionTarget, |
353 | - IQuestionTarget, |
354 | IHasDrivers, |
355 | IHasGitRepositories, |
356 | IHasCodeImports, |
357 | ): |
358 | - """Represents a source package in a distribution. |
359 | - |
360 | - Create IDistributionSourcePackages by invoking |
361 | - `IDistribution.getSourcePackage()`. |
362 | - """ |
363 | + """`IDistributionSourcePackage` attributes that require launchpad.View.""" |
364 | |
365 | distribution = exported( |
366 | Reference(IDistribution, title=_("The distribution.")) |
367 | @@ -209,3 +204,21 @@ class IDistributionSourcePackage( |
368 | |
369 | :return: True if a persistent object was removed, otherwise False. |
370 | """ |
371 | + |
372 | + |
373 | +class IDistributionSourcePackageEdit(IWebhookTarget): |
374 | + """`IDistributionSourcePackage` attributes that require launchpad.Edit.""" |
375 | + |
376 | + |
377 | +@exported_as_webservice_entry(as_of="beta") |
378 | +class IDistributionSourcePackage( |
379 | + IDistributionSourcePackageView, |
380 | + IDistributionSourcePackageEdit, |
381 | + IStructuralSubscriptionTarget, |
382 | + IQuestionTarget, |
383 | +): |
384 | + """Represents a source package in a distribution. |
385 | + |
386 | + Create IDistributionSourcePackages by invoking |
387 | + `IDistribution.getSourcePackage()`. |
388 | + """ |
389 | diff --git a/lib/lp/registry/interfaces/product.py b/lib/lp/registry/interfaces/product.py |
390 | index e28f68c..a3e3e45 100644 |
391 | --- a/lib/lp/registry/interfaces/product.py |
392 | +++ b/lib/lp/registry/interfaces/product.py |
393 | @@ -130,6 +130,7 @@ from lp.services.fields import ( |
394 | Title, |
395 | URIField, |
396 | ) |
397 | +from lp.services.webhooks.interfaces import IWebhookTarget |
398 | from lp.services.webservice.apihelpers import ( |
399 | patch_collection_property, |
400 | patch_reference_property, |
401 | @@ -1099,7 +1100,7 @@ class IProductView( |
402 | """ |
403 | |
404 | |
405 | -class IProductEditRestricted(IOfficialBugTagTargetRestricted): |
406 | +class IProductEditRestricted(IOfficialBugTagTargetRestricted, IWebhookTarget): |
407 | """`IProduct` properties which require launchpad.Edit permission.""" |
408 | |
409 | @mutator_for(IProductView["bug_sharing_policy"]) |
410 | diff --git a/lib/lp/registry/model/distribution.py b/lib/lp/registry/model/distribution.py |
411 | index cf812a7..045fb98 100644 |
412 | --- a/lib/lp/registry/model/distribution.py |
413 | +++ b/lib/lp/registry/model/distribution.py |
414 | @@ -172,6 +172,7 @@ from lp.services.helpers import backslashreplace, shortlist |
415 | from lp.services.propertycache import cachedproperty, get_property_cache |
416 | from lp.services.webapp.interfaces import ILaunchBag |
417 | from lp.services.webapp.url import urlparse |
418 | +from lp.services.webhooks.model import WebhookTargetMixin |
419 | from lp.services.worlddata.model.country import Country |
420 | from lp.soyuz.enums import ( |
421 | ArchivePurpose, |
422 | @@ -238,6 +239,7 @@ class Distribution( |
423 | TranslationPolicyMixin, |
424 | InformationTypeMixin, |
425 | SharingPolicyMixin, |
426 | + WebhookTargetMixin, |
427 | ): |
428 | """A distribution of an operating system, e.g. Debian GNU/Linux.""" |
429 | |
430 | @@ -2312,6 +2314,10 @@ class Distribution( |
431 | .one() |
432 | ) |
433 | |
434 | + @property |
435 | + def valid_webhook_event_types(self): |
436 | + return ["bug:0.1", "bug:comment:0.1"] |
437 | + |
438 | |
439 | @implementer(IDistributionSet) |
440 | class DistributionSet: |
441 | diff --git a/lib/lp/registry/model/distributionsourcepackage.py b/lib/lp/registry/model/distributionsourcepackage.py |
442 | index 0b6a1ce..5235ff7 100644 |
443 | --- a/lib/lp/registry/model/distributionsourcepackage.py |
444 | +++ b/lib/lp/registry/model/distributionsourcepackage.py |
445 | @@ -46,6 +46,7 @@ from lp.services.database.decoratedresultset import DecoratedResultSet |
446 | from lp.services.database.interfaces import IStore |
447 | from lp.services.database.stormbase import StormBase |
448 | from lp.services.propertycache import cachedproperty |
449 | +from lp.services.webhooks.model import WebhookTargetMixin |
450 | from lp.soyuz.enums import ArchivePurpose, PackagePublishingStatus |
451 | from lp.soyuz.model.archive import Archive |
452 | from lp.soyuz.model.distributionsourcepackagerelease import ( |
453 | @@ -88,6 +89,7 @@ class DistributionSourcePackage( |
454 | HasCustomLanguageCodesMixin, |
455 | HasMergeProposalsMixin, |
456 | HasDriversMixin, |
457 | + WebhookTargetMixin, |
458 | ): |
459 | """This is a "Magic Distribution Source Package". It is not an |
460 | SQLObject, but instead it represents a source package with a particular |
461 | @@ -561,6 +563,10 @@ class DistributionSourcePackage( |
462 | if dsp is None: |
463 | cls._new(distribution, sourcepackagename) |
464 | |
465 | + @property |
466 | + def valid_webhook_event_types(self): |
467 | + return ["bug:0.1", "bug:comment:0.1"] |
468 | + |
469 | |
470 | @implementer(transaction.interfaces.ISynchronizer) |
471 | class ThreadLocalLRUCache(LRUCache, local): |
472 | diff --git a/lib/lp/registry/model/product.py b/lib/lp/registry/model/product.py |
473 | index 4bbf796..3c95e99 100644 |
474 | --- a/lib/lp/registry/model/product.py |
475 | +++ b/lib/lp/registry/model/product.py |
476 | @@ -164,6 +164,7 @@ from lp.services.propertycache import cachedproperty, get_property_cache |
477 | from lp.services.statistics.interfaces.statistic import ILaunchpadStatisticSet |
478 | from lp.services.webapp.interfaces import ILaunchBag |
479 | from lp.services.webapp.snapshot import notify_modified |
480 | +from lp.services.webhooks.model import WebhookTargetMixin |
481 | from lp.translations.enums import TranslationPermission |
482 | from lp.translations.interfaces.customlanguagecode import ( |
483 | IHasCustomLanguageCodes, |
484 | @@ -265,6 +266,7 @@ class Product( |
485 | HasAliasMixin, |
486 | HasCustomLanguageCodesMixin, |
487 | SharingPolicyMixin, |
488 | + WebhookTargetMixin, |
489 | ): |
490 | """A Product.""" |
491 | |
492 | @@ -1593,6 +1595,10 @@ class Product( |
493 | .is_empty() |
494 | ) |
495 | |
496 | + @property |
497 | + def valid_webhook_event_types(self): |
498 | + return ["bug:0.1", "bug:comment:0.1"] |
499 | + |
500 | |
501 | def get_precached_products( |
502 | products, |
503 | diff --git a/lib/lp/services/features/flags.py b/lib/lp/services/features/flags.py |
504 | index 4b19185..5d42d9d 100644 |
505 | --- a/lib/lp/services/features/flags.py |
506 | +++ b/lib/lp/services/features/flags.py |
507 | @@ -295,6 +295,14 @@ flag_info = sorted( |
508 | "", |
509 | "", |
510 | ), |
511 | + ( |
512 | + "bugs.webhooks.enabled", |
513 | + "boolean", |
514 | + "If true, allow adding webhooks to bug updates and comments", |
515 | + "", |
516 | + "", |
517 | + "", |
518 | + ), |
519 | ] |
520 | ) |
521 | |
522 | diff --git a/lib/lp/services/webhooks/interfaces.py b/lib/lp/services/webhooks/interfaces.py |
523 | index 4f0a1e8..306cffd 100644 |
524 | --- a/lib/lp/services/webhooks/interfaces.py |
525 | +++ b/lib/lp/services/webhooks/interfaces.py |
526 | @@ -48,6 +48,8 @@ from lp.services.webservice.apihelpers import ( |
527 | ) |
528 | |
529 | WEBHOOK_EVENT_TYPES = { |
530 | + "bug:0.1": "Bug creation/change", |
531 | + "bug:comment:0.1": "Bug comment", |
532 | "bzr:push:0.1": "Bazaar push", |
533 | "charm-recipe:build:0.1": "Charm recipe build", |
534 | "ci:build:0.1": "CI build", |
535 | diff --git a/lib/lp/services/webhooks/tests/test_browser.py b/lib/lp/services/webhooks/tests/test_browser.py |
536 | index b3f28fe..7542cae 100644 |
537 | --- a/lib/lp/services/webhooks/tests/test_browser.py |
538 | +++ b/lib/lp/services/webhooks/tests/test_browser.py |
539 | @@ -10,6 +10,7 @@ import transaction |
540 | from testtools.matchers import MatchesAll, MatchesStructure, Not |
541 | from zope.component import getUtility |
542 | |
543 | +from lp.bugs.interfaces.bugtarget import BUG_WEBHOOKS_FEATURE_FLAG |
544 | from lp.charms.interfaces.charmrecipe import ( |
545 | CHARM_RECIPE_ALLOW_CREATE, |
546 | CHARM_RECIPE_WEBHOOKS_FEATURE_FLAG, |
547 | @@ -174,13 +175,54 @@ class CharmRecipeTestHelpers: |
548 | return [obj] |
549 | |
550 | |
551 | +class BugUpdateTestHelpersBase: |
552 | + |
553 | + # Overriding this since product webhooks don't have breadcrumbs |
554 | + _webhook_listing = soupmatchers.HTMLContains(add_webhook_tag) |
555 | + |
556 | + event_type = "bug:0.1" |
557 | + expected_event_types = [ |
558 | + ("bug:0.1", "Bug change"), |
559 | + ("bug:comment:0.1", "Bug comment"), |
560 | + ] |
561 | + |
562 | + def getTraversalStack(self, obj): |
563 | + return [obj] |
564 | + |
565 | + |
566 | +class ProductTestHelpers(BugUpdateTestHelpersBase): |
567 | + def makeTarget(self): |
568 | + self.useFixture(FeatureFixture({BUG_WEBHOOKS_FEATURE_FLAG: "on"})) |
569 | + owner = self.factory.makePerson() |
570 | + return self.factory.makeProduct(owner=owner) |
571 | + |
572 | + |
573 | +class DistributionTestHelpers(BugUpdateTestHelpersBase): |
574 | + def makeTarget(self): |
575 | + self.useFixture(FeatureFixture({BUG_WEBHOOKS_FEATURE_FLAG: "on"})) |
576 | + owner = self.factory.makePerson() |
577 | + return self.factory.makeDistribution(owner=owner) |
578 | + |
579 | + |
580 | +class DistributionSourcePackageTestHelpers(BugUpdateTestHelpersBase): |
581 | + def get_target_owner(self): |
582 | + return self.target.distribution.owner |
583 | + |
584 | + def makeTarget(self): |
585 | + self.useFixture(FeatureFixture({BUG_WEBHOOKS_FEATURE_FLAG: "on"})) |
586 | + return self.factory.makeDistributionSourcePackage() |
587 | + |
588 | + |
589 | class WebhookTargetViewTestHelpers: |
590 | def setUp(self): |
591 | super().setUp() |
592 | self.target = self.makeTarget() |
593 | - self.owner = self.target.owner |
594 | + self.owner = self.get_target_owner() |
595 | login_person(self.owner) |
596 | |
597 | + def get_target_owner(self): |
598 | + return self.target.owner |
599 | + |
600 | def makeView(self, name, **kwargs): |
601 | # XXX cjwatson 2020-02-06: We need to give the view a |
602 | # LaunchpadPrincipal rather than just a person, since otherwise bits |
603 | @@ -212,6 +254,7 @@ class WebhookTargetViewTestHelpers: |
604 | class TestWebhooksViewBase(WebhookTargetViewTestHelpers): |
605 | |
606 | layer = DatabaseFunctionalLayer |
607 | + _webhook_listing = webhook_listing_constants |
608 | |
609 | def makeHooksAndMatchers(self, count): |
610 | hooks = [ |
611 | @@ -257,7 +300,7 @@ class TestWebhooksViewBase(WebhookTargetViewTestHelpers): |
612 | self.assertThat( |
613 | self.makeView("+webhooks")(), |
614 | MatchesAll( |
615 | - webhook_listing_constants, |
616 | + self._webhook_listing, |
617 | Not(soupmatchers.HTMLContains(webhook_listing_tag)), |
618 | ), |
619 | ) |
620 | @@ -268,7 +311,7 @@ class TestWebhooksViewBase(WebhookTargetViewTestHelpers): |
621 | self.assertThat( |
622 | self.makeView("+webhooks")(), |
623 | MatchesAll( |
624 | - webhook_listing_constants, |
625 | + self._webhook_listing, |
626 | soupmatchers.HTMLContains(webhook_listing_tag, *link_matchers), |
627 | Not(soupmatchers.HTMLContains(batch_nav_tag)), |
628 | ), |
629 | @@ -280,7 +323,7 @@ class TestWebhooksViewBase(WebhookTargetViewTestHelpers): |
630 | self.assertThat( |
631 | self.makeView("+webhooks")(), |
632 | MatchesAll( |
633 | - webhook_listing_constants, |
634 | + self._webhook_listing, |
635 | soupmatchers.HTMLContains( |
636 | webhook_listing_tag, batch_nav_tag, *link_matchers[:5] |
637 | ), |
638 | @@ -343,6 +386,29 @@ class TestWebhooksViewCharmRecipe( |
639 | pass |
640 | |
641 | |
642 | +class TestWebhooksViewProductBugUpdate( |
643 | + ProductTestHelpers, TestWebhooksViewBase, TestCaseWithFactory |
644 | +): |
645 | + |
646 | + pass |
647 | + |
648 | + |
649 | +class TestWebhooksViewDistributionBugUpdate( |
650 | + DistributionTestHelpers, TestWebhooksViewBase, TestCaseWithFactory |
651 | +): |
652 | + |
653 | + pass |
654 | + |
655 | + |
656 | +class TestWebhooksViewDistributionSourcePackageBugUpdate( |
657 | + DistributionSourcePackageTestHelpers, |
658 | + TestWebhooksViewBase, |
659 | + TestCaseWithFactory, |
660 | +): |
661 | + |
662 | + pass |
663 | + |
664 | + |
665 | class TestWebhookAddViewBase(WebhookTargetViewTestHelpers): |
666 | |
667 | layer = DatabaseFunctionalLayer |
668 | @@ -492,16 +558,42 @@ class TestWebhookAddViewCharmRecipe( |
669 | pass |
670 | |
671 | |
672 | +class TestWebhookAddViewProductBugUpdate( |
673 | + ProductTestHelpers, TestWebhookAddViewBase, TestCaseWithFactory |
674 | +): |
675 | + |
676 | + pass |
677 | + |
678 | + |
679 | +class TestWebhookAddViewDistributionBugUpdate( |
680 | + DistributionTestHelpers, TestWebhookAddViewBase, TestCaseWithFactory |
681 | +): |
682 | + |
683 | + pass |
684 | + |
685 | + |
686 | +class TestWebhookAddViewDistributionSourcePackageBugUpdate( |
687 | + DistributionSourcePackageTestHelpers, |
688 | + TestWebhookAddViewBase, |
689 | + TestCaseWithFactory, |
690 | +): |
691 | + |
692 | + pass |
693 | + |
694 | + |
695 | class WebhookViewTestHelpers: |
696 | def setUp(self): |
697 | super().setUp() |
698 | self.target = self.makeTarget() |
699 | - self.owner = self.target.owner |
700 | + self.owner = self.get_target_owner() |
701 | self.webhook = self.factory.makeWebhook( |
702 | target=self.target, delivery_url="http://example.com/original" |
703 | ) |
704 | login_person(self.owner) |
705 | |
706 | + def get_target_owner(self): |
707 | + return self.target.owner |
708 | + |
709 | def makeView(self, name, **kwargs): |
710 | view = create_view(self.webhook, name, principal=self.owner, **kwargs) |
711 | # To test the breadcrumbs we need a correct traversal stack. |
712 | @@ -729,3 +821,26 @@ class TestWebhookDeleteViewCharmRecipe( |
713 | ): |
714 | |
715 | pass |
716 | + |
717 | + |
718 | +class TestWebhookDeleteViewProductBugUpdate( |
719 | + ProductTestHelpers, TestWebhookDeleteViewBase, TestCaseWithFactory |
720 | +): |
721 | + |
722 | + pass |
723 | + |
724 | + |
725 | +class TestWebhookDeleteViewDistributionBugUpdate( |
726 | + DistributionTestHelpers, TestWebhookDeleteViewBase, TestCaseWithFactory |
727 | +): |
728 | + |
729 | + pass |
730 | + |
731 | + |
732 | +class TestWebhookDeleteViewDistributionSourcePackageBugUpdate( |
733 | + DistributionSourcePackageTestHelpers, |
734 | + TestWebhookDeleteViewBase, |
735 | + TestCaseWithFactory, |
736 | +): |
737 | + |
738 | + pass |
739 | diff --git a/lib/lp/services/webhooks/tests/test_webservice.py b/lib/lp/services/webhooks/tests/test_webservice.py |
740 | index 7f84a14..a6f9c0b 100644 |
741 | --- a/lib/lp/services/webhooks/tests/test_webservice.py |
742 | +++ b/lib/lp/services/webhooks/tests/test_webservice.py |
743 | @@ -45,17 +45,20 @@ class TestWebhook(TestCaseWithFactory): |
744 | |
745 | def setUp(self): |
746 | super().setUp() |
747 | - target = self.factory.makeGitRepository() |
748 | - self.owner = target.owner |
749 | + self.target = self.factory.makeGitRepository() |
750 | + self.owner = self.get_target_owner() |
751 | with person_logged_in(self.owner): |
752 | self.webhook = self.factory.makeWebhook( |
753 | - target=target, delivery_url="http://example.com/ep" |
754 | + target=self.target, delivery_url="http://example.com/ep" |
755 | ) |
756 | self.webhook_url = api_url(self.webhook) |
757 | self.webservice = webservice_for_person( |
758 | self.owner, permission=OAuthPermission.WRITE_PRIVATE |
759 | ) |
760 | |
761 | + def get_target_owner(self): |
762 | + return self.target.owner |
763 | + |
764 | def test_get(self): |
765 | representation = self.webservice.get( |
766 | self.webhook_url, api_version="devel" |
767 | @@ -262,11 +265,11 @@ class TestWebhookDelivery(TestCaseWithFactory): |
768 | |
769 | def setUp(self): |
770 | super().setUp() |
771 | - target = self.factory.makeGitRepository() |
772 | - self.owner = target.owner |
773 | + self.target = self.factory.makeGitRepository() |
774 | + self.owner = self.get_target_owner() |
775 | with person_logged_in(self.owner): |
776 | self.webhook = self.factory.makeWebhook( |
777 | - target=target, delivery_url="http://example.com/ep" |
778 | + target=self.target, delivery_url="http://example.com/ep" |
779 | ) |
780 | self.webhook_url = api_url(self.webhook) |
781 | self.delivery = self.webhook.ping() |
782 | @@ -275,6 +278,9 @@ class TestWebhookDelivery(TestCaseWithFactory): |
783 | self.owner, permission=OAuthPermission.WRITE_PRIVATE |
784 | ) |
785 | |
786 | + def get_target_owner(self): |
787 | + return self.target.owner |
788 | + |
789 | def test_get(self): |
790 | representation = self.webservice.get( |
791 | self.delivery_url, api_version="devel" |
792 | @@ -355,12 +361,15 @@ class TestWebhookTargetBase: |
793 | def setUp(self): |
794 | super().setUp() |
795 | self.target = self.makeTarget() |
796 | - self.owner = self.target.owner |
797 | + self.owner = self.get_target_owner() |
798 | self.target_url = api_url(self.target) |
799 | self.webservice = webservice_for_person( |
800 | self.owner, permission=OAuthPermission.WRITE_PRIVATE |
801 | ) |
802 | |
803 | + def get_target_owner(self): |
804 | + return self.target.owner |
805 | + |
806 | def test_webhooks(self): |
807 | with person_logged_in(self.owner): |
808 | for ep in ("http://example.com/ep1", "http://example.com/ep2"): |
809 | @@ -511,3 +520,36 @@ class TestWebhookTargetCharmRecipe(TestWebhookTargetBase, TestCaseWithFactory): |
810 | } |
811 | ): |
812 | return self.factory.makeCharmRecipe(registrant=owner, owner=owner) |
813 | + |
814 | + |
815 | +class TestWebhookTargetProduct(TestWebhookTargetBase, TestCaseWithFactory): |
816 | + |
817 | + event_type = "bug:0.1" |
818 | + |
819 | + def makeTarget(self): |
820 | + owner = self.factory.makePerson() |
821 | + return self.factory.makeProduct(owner=owner) |
822 | + |
823 | + |
824 | +class TestWebhookTargetDistribution( |
825 | + TestWebhookTargetBase, TestCaseWithFactory |
826 | +): |
827 | + |
828 | + event_type = "bug:0.1" |
829 | + |
830 | + def makeTarget(self): |
831 | + owner = self.factory.makePerson() |
832 | + return self.factory.makeDistribution(owner=owner) |
833 | + |
834 | + |
835 | +class TestWebhookTargetDistributionSourcePackage( |
836 | + TestWebhookTargetBase, TestCaseWithFactory |
837 | +): |
838 | + |
839 | + event_type = "bug:0.1" |
840 | + |
841 | + def makeTarget(self): |
842 | + return self.factory.makeDistributionSourcePackage() |
843 | + |
844 | + def get_target_owner(self): |
845 | + return self.target.distribution.owner |