Skip to content

Commit 0654894

Browse files
authored
Webhook behavior customization and delivery records (#1338)
Signed-off-by: tdruez <tdruez@nexb.com>
1 parent 2663ec0 commit 0654894

14 files changed

+590
-85
lines changed

CHANGELOG.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ v34.7.2 (unreleased)
1313
related to the Pipeline run.
1414
https://github.com/nexB/scancode.io/pull/1330
1515

16+
- Expands on the existing WebhookSubscription model by adding a few fields to
17+
configure the behavior of the Webhooks, and moves some of the fields to a new
18+
WebhookDelivery model, which captures the results of a WebhookSubscription
19+
"delivery".
20+
https://github.com/nexB/scancode.io/issues/1325
21+
1622
v34.7.1 (2024-07-15)
1723
--------------------
1824

scanpipe/api/serializers.py

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
from scanpipe.models import Project
3737
from scanpipe.models import ProjectMessage
3838
from scanpipe.models import Run
39+
from scanpipe.models import WebhookSubscription
3940
from scanpipe.pipes import count_group_by
4041

4142
scanpipe_app = apps.get_app_config("scanpipe")
@@ -143,6 +144,18 @@ class Meta:
143144
]
144145

145146

147+
class WebhookSubscriptionSerializer(serializers.ModelSerializer):
148+
class Meta:
149+
model = WebhookSubscription
150+
fields = [
151+
"target_url",
152+
"trigger_on_each_run",
153+
"include_summary",
154+
"include_results",
155+
"is_active",
156+
]
157+
158+
146159
class ProjectSerializer(
147160
ExcludeFromListViewMixin,
148161
SerializerExcludeFieldsMixin,
@@ -158,6 +171,7 @@ class ProjectSerializer(
158171
execute_now = serializers.BooleanField(
159172
write_only=True,
160173
help_text="Execute pipeline now",
174+
required=False,
161175
)
162176
upload_file = serializers.FileField(write_only=True, required=False)
163177
upload_file_tag = serializers.CharField(write_only=True, required=False)
@@ -167,6 +181,7 @@ class ProjectSerializer(
167181
style={"base_template": "textarea.html"},
168182
)
169183
webhook_url = serializers.CharField(write_only=True, required=False)
184+
webhooks = WebhookSubscriptionSerializer(many=True, write_only=True, required=False)
170185
next_run = serializers.CharField(source="get_next_run", read_only=True)
171186
runs = RunSerializer(many=True, read_only=True)
172187
input_sources = InputSourceSerializer(
@@ -192,6 +207,7 @@ class Meta:
192207
"upload_file_tag",
193208
"input_urls",
194209
"webhook_url",
210+
"webhooks",
195211
"created_date",
196212
"is_archived",
197213
"notes",
@@ -289,6 +305,7 @@ def create(self, validated_data):
289305
pipeline = validated_data.pop("pipeline", [])
290306
execute_now = validated_data.pop("execute_now", False)
291307
webhook_url = validated_data.pop("webhook_url", None)
308+
webhooks = validated_data.pop("webhooks", [])
292309

293310
project = super().create(validated_data)
294311

@@ -298,12 +315,15 @@ def create(self, validated_data):
298315
for url in input_urls:
299316
project.add_input_source(download_url=url)
300317

301-
if webhook_url:
302-
project.add_webhook_subscription(webhook_url)
303-
304318
for pipeline_name in pipeline:
305319
project.add_pipeline(pipeline_name, execute_now)
306320

321+
if webhook_url:
322+
project.add_webhook_subscription(target_url=webhook_url)
323+
324+
for webhook_data in webhooks:
325+
project.add_webhook_subscription(**webhook_data)
326+
307327
return project
308328

309329

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Generated by Django 5.0.7 on 2024-07-24 05:22
2+
3+
import django.db.models.deletion
4+
import scanpipe.models
5+
import uuid
6+
from django.db import migrations, models
7+
8+
9+
class Migration(migrations.Migration):
10+
11+
dependencies = [
12+
('scanpipe', '0063_run_selected_steps'),
13+
]
14+
15+
operations = [
16+
migrations.CreateModel(
17+
name='WebhookDelivery',
18+
fields=[
19+
('uuid', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='UUID')),
20+
('target_url', models.URLField(help_text='Stores a copy of the Webhook target URL in case the subscription object is deleted.', max_length=1024, verbose_name='Target URL')),
21+
('sent_date', models.DateTimeField(auto_now_add=True, help_text='The date and time when the Webhook was sent.')),
22+
('payload', models.JSONField(blank=True, default=dict, help_text='The JSON payload that was sent to the target URL.')),
23+
('response_status_code', models.PositiveIntegerField(blank=True, help_text='The HTTP status code received in response to the Webhook request.', null=True)),
24+
('response_text', models.TextField(blank=True, help_text='The text response received from the target URL.')),
25+
('delivery_error', models.TextField(blank=True, help_text='Any error messages encountered during the Webhook delivery.')),
26+
('project', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='webhookdeliveries', to='scanpipe.project')),
27+
('run', models.ForeignKey(blank=True, editable=False, help_text='The Pipeline Run associated with this delivery.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='webhook_deliveries', to='scanpipe.run')),
28+
('webhook_subscription', models.ForeignKey(blank=True, editable=False, help_text='The Webhook subscription associated with this delivery.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='deliveries', to='scanpipe.webhooksubscription')),
29+
],
30+
options={
31+
'verbose_name': 'webhook delivery',
32+
'verbose_name_plural': 'webhook deliveries',
33+
'ordering': ['-sent_date'],
34+
},
35+
bases=(scanpipe.models.UpdateMixin, models.Model),
36+
),
37+
]
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Generated by Django 5.0.7 on 2024-07-24 05:24
2+
3+
from django.db import migrations
4+
5+
6+
def migrate_webhook_delivery_data(apps, schema_editor):
7+
WebhookSubscription = apps.get_model("scanpipe", "WebhookSubscription")
8+
WebhookDelivery = apps.get_model("scanpipe", "WebhookDelivery")
9+
10+
subscription_qs = WebhookSubscription.objects.all().select_related("project")
11+
12+
for subscription in subscription_qs:
13+
project = subscription.project
14+
15+
# Asserting the run FK in case this project has a single run
16+
project_runs = project.runs.all()
17+
run = project_runs[0] if len(project_runs) == 1 else None
18+
19+
WebhookDelivery.objects.create(
20+
project=project,
21+
run=run,
22+
webhook_subscription=subscription,
23+
target_url=subscription.target_url,
24+
sent_date=subscription.created_date,
25+
payload={},
26+
response_status_code=subscription.response_status_code,
27+
response_text=subscription.response_text,
28+
delivery_error=subscription.delivery_error,
29+
)
30+
31+
32+
class Migration(migrations.Migration):
33+
dependencies = [
34+
("scanpipe", "0064_webhookdelivery"),
35+
]
36+
37+
operations = [
38+
migrations.RunPython(
39+
migrate_webhook_delivery_data,
40+
reverse_code=migrations.RunPython.noop,
41+
),
42+
]
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# Generated by Django 5.0.7 on 2024-07-24 05:24
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('scanpipe', '0065_webhookdelivery_data'),
10+
]
11+
12+
operations = [
13+
migrations.AlterModelOptions(
14+
name='webhooksubscription',
15+
options={'ordering': ['-created_date']},
16+
),
17+
migrations.RemoveField(
18+
model_name='webhooksubscription',
19+
name='delivery_error',
20+
),
21+
migrations.RemoveField(
22+
model_name='webhooksubscription',
23+
name='response_status_code',
24+
),
25+
migrations.RemoveField(
26+
model_name='webhooksubscription',
27+
name='response_text',
28+
),
29+
migrations.AddField(
30+
model_name='webhooksubscription',
31+
name='include_results',
32+
field=models.BooleanField(default=False, help_text='Include the entire results data in the payload.'),
33+
),
34+
migrations.AddField(
35+
model_name='webhooksubscription',
36+
name='include_summary',
37+
field=models.BooleanField(default=False, help_text='Include the entire summary data in the payload.'),
38+
),
39+
migrations.AddField(
40+
model_name='webhooksubscription',
41+
name='is_active',
42+
field=models.BooleanField(default=True, help_text='Indicates whether the Webhook is currently active and should be triggered.'),
43+
),
44+
migrations.AddField(
45+
model_name='webhooksubscription',
46+
name='trigger_on_each_run',
47+
field=models.BooleanField(default=False, help_text='Trigger the Webhook after each individual pipeline run instead of only after all runs are completed.'),
48+
),
49+
migrations.AlterField(
50+
model_name='webhooksubscription',
51+
name='created_date',
52+
field=models.DateTimeField(auto_now_add=True, help_text='The date and time when the Webhook subscription was created.'),
53+
),
54+
migrations.AlterField(
55+
model_name='webhooksubscription',
56+
name='target_url',
57+
field=models.URLField(help_text='The URL to which the POST request will be sent when the Webhook is triggered.', max_length=1024, verbose_name='Target URL'),
58+
),
59+
]

0 commit comments

Comments
 (0)