Add Sortable 'Last Contacted At' Column to Leads Table (Closes #486, LO-111) - #647
Add Sortable 'Last Contacted At' Column to Leads Table (Closes #486, LO-111)#647GeethaBurigalla wants to merge 1 commit into
Conversation
…8#486, LO-111) Backend: - CampaignLead.last_sent_at: new field + migration, tracking the timestamp of the most recent message/call actually delivered to a lead on a given campaign enrollment. - Stamped on successful email send (alongside last_sent_message_id), successful SMS send, and call attempts (both automated Twilio calls and the manual-task fallback when no Twilio creds are configured — both represent a real contact attempt). Not stamped on a genuine send failure. - LeadViewSet.get_queryset annotates last_contacted_at via Max('campaigns__last_sent_at') and supports ?ordering=last_contacted_at / -last_contacted_at, with never-contacted leads sorted last in both directions. - LeadSerializer exposes last_contacted_at as a SerializerMethodField rather than a plain DateTimeField, so it degrades to None for unannotated Lead instances (e.g. the object returned from create/update) instead of raising AttributeError. Frontend (leads.html): - New 'Last Contacted' column header with a 3-state sort toggle (desc -> asc -> unsorted) and an icon reflecting current state. - New cell rendering the formatted timestamp or 'Never'. - Wires 'ordering' into getFilterParams so sort composes with existing filters; resets on Clear Filters. Verified: all 85 existing backend tests pass; manual sanity check confirms annotation, both sort directions (nulls last), and serializer null-safety.
📝 WalkthroughWalkthroughCampaign sends now persist the latest contact timestamp. The leads API aggregates and sorts by that timestamp, and the leads table displays it with a three-state sort control. ChangesLast Contacted Tracking
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant LeadsTable
participant LeadViewSet
participant CampaignLead
LeadsTable->>LeadViewSet: Request ordering=last_contacted_at
LeadViewSet->>CampaignLead: Aggregate Max(last_sent_at)
CampaignLead-->>LeadViewSet: Latest contact timestamp
LeadViewSet-->>LeadsTable: Ordered leads with last_contacted_at
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
backend/campaigns/models.py (1)
135-138: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winConsider adding a database index on
last_sent_at.The
Max('campaigns__last_sent_at')annotation inLeadViewSet.get_querysetruns on every leads list request. A composite index on(lead_id, last_sent_at)would let the database satisfy theMAXsubquery via an index-only scan instead of scanning all CampaignLead rows per lead.♻️ Optional: add db_index to last_sent_at
last_sent_at = models.DateTimeField( - null=True, blank=True, + null=True, blank=True, db_index=True, help_text='Timestamp of the most recent message/call actually sent to the lead on this campaign.', )🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@backend/campaigns/models.py` around lines 135 - 138, Update the last_sent_at field in the CampaignLead model to add a database index, supporting the Max('campaigns__last_sent_at') annotation used by LeadViewSet.get_queryset. If the project supports composite indexes, add the appropriate (lead_id, last_sent_at) index through the model’s Meta configuration and generate the corresponding migration.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@backend/campaigns/models.py`:
- Around line 135-138: Update the last_sent_at field in the CampaignLead model
to add a database index, supporting the Max('campaigns__last_sent_at')
annotation used by LeadViewSet.get_queryset. If the project supports composite
indexes, add the appropriate (lead_id, last_sent_at) index through the model’s
Meta configuration and generate the corresponding migration.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 9d118a5f-e939-42e1-86ae-f999c977ee88
📒 Files selected for processing (6)
backend/campaigns/migrations/0011_campaignlead_last_sent_at.pybackend/campaigns/models.pybackend/campaigns/tasks.pybackend/leads/serializers.pybackend/leads/views.pyfrontend/leads.html
Summary
Adds a
last_contacted_atcolumn to the leads table, sortable via the API,so users can see and sort by when a lead was most recently contacted.
Closes #486 (LO-111)
Backend
CampaignLead.last_sent_at: new field + migration, tracking the timestampof the most recent message/call actually delivered to a lead on a given
campaign enrollment.
last_sent_message_id)fallback when no Twilio credentials are configured, since both represent
the lead actually being contacted. Not stamped on a genuine send failure.
LeadViewSet.get_querysetannotateslast_contacted_atviaMax('campaigns__last_sent_at')and supports?ordering=last_contacted_at/?ordering=-last_contacted_at, withnever-contacted leads sorted last in both directions.
LeadSerializerexposeslast_contacted_atas aSerializerMethodFieldrather than a plain
DateTimeField, so it degrades toNoneforunannotated
Leadinstances (e.g. the object returned from create/update)instead of raising
AttributeError.Frontend (
leads.html)(desc → asc → unsorted) and an icon reflecting the current state.
orderingintogetFilterParamsso sort composes with existingfilters; resets on Clear Filters.
Testing
Ran the full
leadsandcampaignstest suites locally — all 85 tests pass.Also manually verified the annotation/ordering/serializer behavior end-to-end
(descending puts most-recent first, ascending reverses it, nulls always last,
serializer returns
Nonesafely for plain instances).Aside
Noticed
beautifulsoup4is imported incampaigns/tasks.pybut isn't listedin
requirements.txt— not part of this PR, just flagging in case it's wortha quick separate fix.
Summary by CodeRabbit