Skip to content

Commit

Permalink
Merge branch 'main' into allauth-progress
Browse files Browse the repository at this point in the history
  • Loading branch information
ericnewcomer committed Mar 1, 2025
2 parents ed87f62 + 0623e1b commit 48134e5
Show file tree
Hide file tree
Showing 16 changed files with 231 additions and 159 deletions.
2 changes: 2 additions & 0 deletions temba/api/tests/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@ def as_user(user, expected_results: list, expected_queries: int = None):
for field, msg in errors.items():
self.assertResponseError(response, field, msg, status_code=400)
elif callable(raw):
if not raw(response.json()):
print(response.json())
self.assertTrue(raw(response.json()))
else:
self.assertEqual(raw, response.json())
Expand Down
16 changes: 5 additions & 11 deletions temba/api/v2/tests/test_campaign_events.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
from unittest.mock import call

from django.urls import reverse
from django.utils import timezone

from temba.api.v2.serializers import format_datetime
from temba.campaigns.models import Campaign, CampaignEvent
from temba.contacts.models import ContactField, ContactGroup
from temba.tests import matchers, mock_mailroom
from temba.tests import mock_mailroom

from . import APITest

Expand Down Expand Up @@ -249,16 +251,8 @@ def test_endpoint(self, mr_mocks):
self.assertEqual(event2.message, None)
self.assertEqual(event2.flow, flow)

# make sure we queued a mailroom task to schedule this event
self.assertEqual(
{
"org_id": self.org.id,
"type": "schedule_campaign_event",
"queued_on": matchers.Datetime(),
"task": {"campaign_event_id": event2.id, "org_id": self.org.id},
},
mr_mocks.queued_batch_tasks[-1],
)
# make sure we called mailroom to schedule this event
self.assertEqual(call(self.org, event2), mr_mocks.calls["campaign_schedule_event"][-1])

# update the message event to be a flow event
self.assertPost(
Expand Down
103 changes: 48 additions & 55 deletions temba/api/v2/tests/test_definitions.py
Original file line number Diff line number Diff line change
@@ -1,100 +1,93 @@
from django.urls import reverse

from temba.campaigns.models import Campaign
from temba.campaigns.models import Campaign, CampaignEvent
from temba.contacts.models import ContactField
from temba.flows.models import Flow
from temba.tests import mock_mailroom
from temba.triggers.models import Trigger

from . import APITest


class DefinitionsEndpointTest(APITest):
def test_endpoint(self):
@mock_mailroom
def test_endpoint(self, mr_mocks):
endpoint_url = reverse("api.v2.definitions") + ".json"

self.assertGetNotPermitted(endpoint_url, [None, self.agent])
self.assertPostNotAllowed(endpoint_url)
self.assertDeleteNotAllowed(endpoint_url)

self.import_file("test_flows/subflow.json")
flow = Flow.objects.get(name="Parent Flow")

# all flow dependencies and we should get the child flow
self.assertGet(
endpoint_url + f"?flow={flow.uuid}",
[self.editor],
raw=lambda j: {f["name"] for f in j["flows"]} == {"Child Flow", "Parent Flow"},
)

# export just the parent flow
self.assertGet(
endpoint_url + f"?flow={flow.uuid}&dependencies=none",
[self.editor],
raw=lambda j: {f["name"] for f in j["flows"]} == {"Parent Flow"},
# create a flow with subflow dependencies
flow1 = self.create_flow("Parent Flow")
flow2 = self.create_flow("Child Flow 1")
flow3 = self.create_flow("Child Flow 2")
flow1.flow_dependencies.add(flow2, flow3)

# that's used in a campaign
field = self.create_field("registered", "Registered", ContactField.TYPE_DATETIME)
group = self.create_group("Others", [])
campaign1 = Campaign.create(self.org, self.admin, "Reminders", group)
CampaignEvent.create_flow_event(self.org, self.admin, campaign1, field, 1, "D", flow1, -1)

# and has a trigger
Trigger.create(
self.org, self.editor, Trigger.TYPE_KEYWORD, flow1, keywords=["test"], match_type=Trigger.MATCH_FIRST_WORD
)

# import the clinic app which has campaigns
self.import_file("test_flows/the_clinic.json")

# our catchall flow, all alone
flow = Flow.objects.get(name="Catch All")
# nothing specified, nothing exported
self.assertGet(
endpoint_url + f"?flow={flow.uuid}&dependencies=none",
endpoint_url,
[self.editor],
raw=lambda j: len(j["flows"]) == 1 and len(j["campaigns"]) == 0 and len(j["triggers"]) == 0,
raw=lambda j: len(j["flows"]) == 0 and len(j["campaigns"]) == 0 and len(j["triggers"]) == 0,
)

# with its trigger dependency
# flow + all dependencies by default
self.assertGet(
endpoint_url + f"?flow={flow.uuid}",
[self.editor],
raw=lambda j: len(j["flows"]) == 1 and len(j["campaigns"]) == 0 and len(j["triggers"]) == 1,
)

# our registration flow, all alone
flow = Flow.objects.get(name="Register Patient")
self.assertGet(
endpoint_url + f"?flow={flow.uuid}&dependencies=none",
[self.editor],
raw=lambda j: len(j["flows"]) == 1 and len(j["campaigns"]) == 0 and len(j["triggers"]) == 0,
)

# touches a lot of stuff
self.assertGet(
endpoint_url + f"?flow={flow.uuid}",
endpoint_url + f"?flow={flow1.uuid}",
[self.editor],
raw=lambda j: len(j["flows"]) == 6 and len(j["campaigns"]) == 1 and len(j["triggers"]) == 2,
raw=lambda j: {f["name"] for f in j["flows"]} == {"Parent Flow", "Child Flow 1", "Child Flow 2"}
and len(j["campaigns"]) == 1
and len(j["triggers"]) == 1,
)

# ignore campaign dependencies
# flow + all dependencies explicitly
self.assertGet(
endpoint_url + f"?flow={flow.uuid}&dependencies=flows",
endpoint_url + f"?flow={flow1.uuid}&dependencies=all",
[self.editor],
raw=lambda j: len(j["flows"]) == 2 and len(j["campaigns"]) == 0 and len(j["triggers"]) == 1,
raw=lambda j: {f["name"] for f in j["flows"]} == {"Parent Flow", "Child Flow 1", "Child Flow 2"}
and len(j["campaigns"]) == 1
and len(j["triggers"]) == 1,
)

# add our missed call flow
missed_call = Flow.objects.get(name="Missed Call")
# flow + no dependencies
self.assertGet(
endpoint_url + f"?flow={flow.uuid}&flow={missed_call.uuid}&dependencies=all",
endpoint_url + f"?flow={flow1.uuid}&dependencies=none",
[self.editor],
raw=lambda j: len(j["flows"]) == 7 and len(j["campaigns"]) == 1 and len(j["triggers"]) == 3,
raw=lambda j: {f["name"] for f in j["flows"]} == {"Parent Flow"}
and len(j["campaigns"]) == 0
and len(j["triggers"]) == 0,
)

campaign = Campaign.objects.get(name="Appointment Schedule")
# flow + just flow dependencies (includes triggers)
self.assertGet(
endpoint_url + f"?campaign={campaign.uuid}&dependencies=none",
endpoint_url + f"?flow={flow1.uuid}&dependencies=flows",
[self.editor],
raw=lambda j: len(j["flows"]) == 0 and len(j["campaigns"]) == 1 and len(j["triggers"]) == 0,
raw=lambda j: {f["name"] for f in j["flows"]} == {"Parent Flow", "Child Flow 1", "Child Flow 2"}
and len(j["campaigns"]) == 0
and len(j["triggers"]) == 1,
)

# campaign + all dependencies
self.assertGet(
endpoint_url + f"?campaign={campaign.uuid}",
endpoint_url + f"?campaign={campaign1.uuid}",
[self.editor],
raw=lambda j: len(j["flows"]) == 6 and len(j["campaigns"]) == 1 and len(j["triggers"]) == 2,
raw=lambda j: len(j["flows"]) == 3 and len(j["campaigns"]) == 1 and len(j["triggers"]) == 1,
)

# test an invalid value for dependencies
self.assertGet(
endpoint_url + f"?flow={flow.uuid}&dependencies=xx",
endpoint_url + f"?flow={flow1.uuid}&dependencies=xx",
[self.editor],
errors={None: "dependencies must be one of none, flows, all"},
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Generated by Django 5.1.4 on 2025-02-28 15:24

from django.db import migrations, models

import temba.utils.uuid


class Migration(migrations.Migration):

dependencies = [
("campaigns", "0066_alter_campaignevent_message"),
]

operations = [
migrations.AddField(
model_name="campaignevent",
name="fire_uuid",
field=models.UUIDField(null=True),
),
migrations.AlterField(
model_name="campaignevent",
name="fire_uuid",
field=models.UUIDField(default=temba.utils.uuid.uuid4, null=True),
),
migrations.AddField(
model_name="campaignevent",
name="status",
field=models.CharField(choices=[("S", "Scheduling"), ("R", "Ready")], default="R", max_length=1),
),
]
24 changes: 24 additions & 0 deletions temba/campaigns/migrations/0068_backfill_event_fire_uuid.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Generated by Django 5.1.4 on 2025-02-28 20:53

from django.db import migrations

from temba.utils.uuid import uuid4


def backfill_event_fire_uuid(apps, schema_editor): # pragma: no cover
CampaignEvent = apps.get_model("campaigns", "CampaignEvent")

for event in CampaignEvent.objects.filter(fire_uuid=None).only("id"):
event.fire_uuid = uuid4()
event.save(update_fields=("fire_uuid",))


class Migration(migrations.Migration):

dependencies = [
("campaigns", "0067_campaignevent_fire_uuid_campaignevent_status"),
]

operations = [
migrations.RunPython(backfill_event_fire_uuid, migrations.RunPython.noop),
]
20 changes: 20 additions & 0 deletions temba/campaigns/migrations/0069_alter_campaignevent_fire_uuid.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Generated by Django 5.1.4 on 2025-02-28 20:55

from django.db import migrations, models

import temba.utils.uuid


class Migration(migrations.Migration):

dependencies = [
("campaigns", "0068_backfill_event_fire_uuid"),
]

operations = [
migrations.AlterField(
model_name="campaignevent",
name="fire_uuid",
field=models.UUIDField(default=temba.utils.uuid.uuid4),
),
]
25 changes: 14 additions & 11 deletions temba/campaigns/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from temba.orgs.models import Org
from temba.utils import json, on_transaction_commit
from temba.utils.models import TembaModel, TembaUUIDMixin, TranslatableField
from temba.utils.uuid import uuid4


class Campaign(TembaModel):
Expand Down Expand Up @@ -256,6 +257,10 @@ class CampaignEvent(TembaUUIDMixin, SmartModel):
TYPE_MESSAGE = "M"
TYPE_CHOICES = ((TYPE_FLOW, "Flow Event"), (TYPE_MESSAGE, "Message Event"))

STATUS_SCHEDULING = "S"
STATUS_READY = "R"
STATUS_CHOICES = ((STATUS_SCHEDULING, _("Scheduling")), (STATUS_READY, _("Ready")))

UNIT_MINUTES = "M"
UNIT_HOURS = "H"
UNIT_DAYS = "D"
Expand All @@ -273,17 +278,18 @@ class CampaignEvent(TembaUUIDMixin, SmartModel):
START_MODES_CHOICES = ((MODE_INTERRUPT, "Interrupt"), (MODE_SKIP, "Skip"), (MODE_PASSIVE, "Passive"))

campaign = models.ForeignKey(Campaign, on_delete=models.PROTECT, related_name="events")

event_type = models.CharField(max_length=1, choices=TYPE_CHOICES, default=TYPE_FLOW)
status = models.CharField(max_length=1, choices=STATUS_CHOICES, default=STATUS_READY)

# TODO switch contact fires to reference event by this instead of it's ID/UUID so we can invalidate fires without
# recreating events
fire_uuid = models.UUIDField(default=uuid4)

# the contact specific date value this is event is based on
relative_to = models.ForeignKey(ContactField, on_delete=models.PROTECT, related_name="campaign_events")

# offset from that date value (positive is after, negative is before)
offset = models.IntegerField(default=0)

# the unit for the offset, e.g. days, weeks
unit = models.CharField(max_length=1, choices=UNIT_CHOICES, default=UNIT_DAYS)
offset = models.IntegerField(default=0) # offset from that date value (positive is after, negative is before)
unit = models.CharField(max_length=1, choices=UNIT_CHOICES, default=UNIT_DAYS) # the unit for the offset
delivery_hour = models.IntegerField(default=-1) # can also specify the hour during the day

# the flow that will be triggered by this event
flow = models.ForeignKey(Flow, on_delete=models.PROTECT, related_name="campaign_events")
Expand All @@ -294,9 +300,6 @@ class CampaignEvent(TembaUUIDMixin, SmartModel):
# when sending single message events, we store the message here (as well as on the flow) for convenience
message = TranslatableField(max_length=Msg.MAX_TEXT_LEN, null=True)

# can also specify the hour during the day that the even should be triggered
delivery_hour = models.IntegerField(default=-1)

@classmethod
def create_message_event(
cls,
Expand Down Expand Up @@ -451,7 +454,7 @@ def offset_display(self):
return _("on")

def schedule_async(self):
on_transaction_commit(lambda: mailroom.queue_schedule_campaign_event(self))
on_transaction_commit(lambda: mailroom.get_client().campaign_schedule_event(self.campaign.org, self))

def recreate(self):
"""
Expand Down
Loading

0 comments on commit 48134e5

Please sign in to comment.