-
Notifications
You must be signed in to change notification settings - Fork 65
feat: add campaign audit log and benchmarks #431
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| # Generated by Django 4.2.30 on 2026-06-22 06:55 | ||
|
|
||
| from django.db import migrations | ||
|
|
||
|
|
||
| class Migration(migrations.Migration): | ||
|
|
||
| dependencies = [ | ||
| ('campaigns', '0009_campaign_cached_counters'), | ||
| ('campaigns', '0009_campaignlead_bounce_metadata'), | ||
| ] | ||
|
|
||
| operations = [ | ||
| ] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| # Generated by Django 4.2.30 on 2026-06-22 06:55 | ||
|
|
||
| from django.conf import settings | ||
| from django.db import migrations, models | ||
| import django.db.models.deletion | ||
| import uuid | ||
|
|
||
|
|
||
| class Migration(migrations.Migration): | ||
|
|
||
| dependencies = [ | ||
| ('tenants', '0002_organization_enable_ai_personalization_and_more'), | ||
| migrations.swappable_dependency(settings.AUTH_USER_MODEL), | ||
| ('campaigns', '0010_merge_20260622_0655'), | ||
| ] | ||
|
|
||
| operations = [ | ||
| migrations.CreateModel( | ||
| name='AuditLog', | ||
| fields=[ | ||
| ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), | ||
| ('created_at', models.DateTimeField(auto_now_add=True)), | ||
| ('updated_at', models.DateTimeField(auto_now=True)), | ||
| ('action', models.CharField(max_length=255)), | ||
| ('target_type', models.CharField(blank=True, default='', max_length=64)), | ||
| ('target_id', models.CharField(blank=True, default='', max_length=64)), | ||
| ('ip_address', models.GenericIPAddressField(blank=True, null=True)), | ||
| ('metadata', models.JSONField(blank=True, default=dict)), | ||
| ('actor', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='audit_logs', to=settings.AUTH_USER_MODEL)), | ||
| ('organization', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tenants.organization')), | ||
| ], | ||
| options={ | ||
| 'ordering': ['-created_at'], | ||
| }, | ||
| ), | ||
| ] |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,7 +1,7 @@ | ||||||||||||||||||||||||||||||||||||||||||
| from rest_framework import serializers | ||||||||||||||||||||||||||||||||||||||||||
| from django.db.models import Q | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| from .models import Campaign, CampaignLead, ConnectedEmailAccount, SequenceStep, EmailTemplate | ||||||||||||||||||||||||||||||||||||||||||
| from .models import AuditLog, Campaign, CampaignLead, ConnectedEmailAccount, SequenceStep, EmailTemplate | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| DELAY_UNIT_TO_MINUTES = { | ||||||||||||||||||||||||||||||||||||||||||
| 'minutes': 1, | ||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -232,6 +232,39 @@ class Meta: | |||||||||||||||||||||||||||||||||||||||||
| model = EmailTemplate | ||||||||||||||||||||||||||||||||||||||||||
| fields = ['id', 'name', 'subject', 'body', 'category', 'usage_count', 'created_at'] | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| class AuditLogSerializer(serializers.ModelSerializer): | ||||||||||||||||||||||||||||||||||||||||||
| actor = serializers.SerializerMethodField() | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| class Meta: | ||||||||||||||||||||||||||||||||||||||||||
| model = AuditLog | ||||||||||||||||||||||||||||||||||||||||||
| fields = [ | ||||||||||||||||||||||||||||||||||||||||||
| 'id', | ||||||||||||||||||||||||||||||||||||||||||
| 'action', | ||||||||||||||||||||||||||||||||||||||||||
| 'target_type', | ||||||||||||||||||||||||||||||||||||||||||
| 'target_id', | ||||||||||||||||||||||||||||||||||||||||||
| 'actor', | ||||||||||||||||||||||||||||||||||||||||||
| 'ip_address', | ||||||||||||||||||||||||||||||||||||||||||
| 'metadata', | ||||||||||||||||||||||||||||||||||||||||||
| 'created_at', | ||||||||||||||||||||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+241
to
+250
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use an immutable tuple for serializer Ruff flags this mutable class attribute; switching to a tuple keeps the same DRF behavior and satisfies RUF012. Proposed fix- fields = [
+ fields = (
'id',
'action',
'target_type',
'target_id',
'actor',
'ip_address',
'metadata',
'created_at',
- ]
+ )📝 Committable suggestion
Suggested change
🧰 Tools🪛 Ruff (0.15.17)[warning] 241-250: Mutable default value for class attribute (RUF012) 🤖 Prompt for AI AgentsSource: Linters/SAST tools |
||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| def get_actor(self, obj): | ||||||||||||||||||||||||||||||||||||||||||
| if not obj.actor: | ||||||||||||||||||||||||||||||||||||||||||
| return None | ||||||||||||||||||||||||||||||||||||||||||
| full_name = ' '.join( | ||||||||||||||||||||||||||||||||||||||||||
| part for part in [ | ||||||||||||||||||||||||||||||||||||||||||
| getattr(obj.actor, 'first_name', ''), | ||||||||||||||||||||||||||||||||||||||||||
| getattr(obj.actor, 'last_name', ''), | ||||||||||||||||||||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||||||||||||||||||||
| if part | ||||||||||||||||||||||||||||||||||||||||||
| ).strip() | ||||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||||
| 'id': str(obj.actor_id), | ||||||||||||||||||||||||||||||||||||||||||
| 'email': obj.actor.email, | ||||||||||||||||||||||||||||||||||||||||||
| 'name': full_name or obj.actor.email, | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| class CampaignLeadSerializer(serializers.ModelSerializer): | ||||||||||||||||||||||||||||||||||||||||||
| class Meta: | ||||||||||||||||||||||||||||||||||||||||||
| model = CampaignLead | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
Add choices constraint to validate action field values.
The
actionfield currently accepts any string up to 255 characters without validation. According to the PR objectives, there are seven specific campaign actions (create, update, delete, enroll, launch, pause, resume), which downstream code records as strings like 'campaign_created', 'campaign_updated', etc. Without a choices constraint or validation, typos in action names won't be caught at write time, leading to inconsistent data that makes filtering and querying audit logs unreliable.✨ Proposed fix to add choices validation
🧰 Tools
🪛 ast-grep (0.44.0)
[info] 55-55: use help_text to document model columns
Context: models.CharField(max_length=64, blank=True, default='')
Note: Security best practice.
(model-help-text)
[warning] 55-55: always specify max_length for a Charfield
Context: models.CharField(max_length=64, blank=True, default='')
Note: Security best practice.
(model-charfield-max-length)
🤖 Prompt for AI Agents