-
Notifications
You must be signed in to change notification settings - Fork 1
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
Network manager funnel performance #391
base: main
Are you sure you want to change the base?
Changes from all commits
233e896
b73e31f
0c02dc3
2ec973a
be2ba7d
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,17 @@ | ||
# Generated by Django 4.2.5 on 2024-09-18 10:16 | ||
|
||
from django.db import migrations, models | ||
|
||
|
||
class Migration(migrations.Migration): | ||
dependencies = [ | ||
("opportunity", "0058_paymentinvoice_payment_invoice"), | ||
] | ||
|
||
operations = [ | ||
migrations.AddField( | ||
model_name="opportunityaccess", | ||
name="invited_date", | ||
field=models.DateTimeField(auto_now_add=True, null=True), | ||
), | ||
] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
from django.db.models import Avg, Count, DurationField, ExpressionWrapper, F, OuterRef, Q, Subquery | ||
|
||
from commcare_connect.opportunity.models import UserVisit, VisitValidationStatus | ||
from commcare_connect.program.models import ManagedOpportunity, Program | ||
|
||
|
||
def get_annotated_managed_opportunity(program: Program): | ||
filter_for_valid__visit_date = ~Q( | ||
opportunityaccess__uservisit__status__in=[ | ||
VisitValidationStatus.over_limit, | ||
VisitValidationStatus.trial, | ||
] | ||
) | ||
|
||
earliest_visits = ( | ||
UserVisit.objects.filter( | ||
opportunity_access=OuterRef("opportunityaccess"), | ||
) | ||
.exclude(status__in=[VisitValidationStatus.over_limit, VisitValidationStatus.trial]) | ||
.order_by("visit_date") | ||
.values("visit_date")[:1] | ||
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. nit: You can use |
||
) | ||
|
||
managed_opportunities = ( | ||
ManagedOpportunity.objects.filter(program=program) | ||
.order_by("start_date") | ||
.annotate( | ||
workers_invited=Count("opportunityaccess"), | ||
workers_passing_assessment=Count( | ||
"opportunityaccess__assessment", | ||
filter=Q( | ||
opportunityaccess__assessment__passed=True, | ||
opportunityaccess__assessment__opportunity=F("opportunityaccess__opportunity"), | ||
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. is this filter necessary? shouldn't the opportunity access guarantee its the right opportunity already? |
||
), | ||
), | ||
workers_starting_delivery=Count( | ||
"opportunityaccess__uservisit__user", | ||
filter=filter_for_valid__visit_date, | ||
distinct=True, | ||
), | ||
percentage_conversion=F("workers_starting_delivery") / F("workers_invited") * 100, | ||
average_time_to_convert=Avg( | ||
ExpressionWrapper( | ||
Subquery(earliest_visits) - F("opportunityaccess__invited_date"), output_field=DurationField() | ||
), | ||
filter=filter_for_valid__visit_date, | ||
), | ||
) | ||
.prefetch_related( | ||
"opportunityaccess_set", | ||
"opportunityaccess_set__uservisit_set", | ||
"opportunityaccess_set__assessment_set", | ||
) | ||
) | ||
|
||
return managed_opportunities |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
from datetime import timedelta | ||
|
||
from django_celery_beat.utils import now | ||
|
||
from commcare_connect.opportunity.models import VisitValidationStatus | ||
from commcare_connect.opportunity.tests.factories import AssessmentFactory, OpportunityAccessFactory, UserVisitFactory | ||
from commcare_connect.organization.models import Organization | ||
from commcare_connect.program.helpers import get_annotated_managed_opportunity | ||
from commcare_connect.program.tests.factories import ManagedOpportunityFactory, ProgramFactory | ||
from commcare_connect.users.tests.factories import OrganizationFactory, UserFactory | ||
|
||
|
||
def test_get_annotated_managed_opportunity(program_manager_org: Organization): | ||
program = ProgramFactory.create(organization=program_manager_org) | ||
nm_org = OrganizationFactory.create() | ||
opp = ManagedOpportunityFactory.create(program=program, organization=nm_org) | ||
users = UserFactory.create_batch(5) | ||
for index, user in enumerate(users): | ||
access = OpportunityAccessFactory.create(opportunity=opp, user=user, invited_date=now()) | ||
AssessmentFactory.create(opportunity=opp, user=user, opportunity_access=access) | ||
visit_status = VisitValidationStatus.pending if index < 3 else VisitValidationStatus.trial | ||
UserVisitFactory.create( | ||
user=user, | ||
opportunity=opp, | ||
status=visit_status, | ||
opportunity_access=access, | ||
visit_date=now() + timedelta(1), | ||
) | ||
|
||
opps = get_annotated_managed_opportunity(program) | ||
for opp in opps: | ||
assert nm_org.slug == opp.organization.slug | ||
assert opp.workers_passing_assessment == 5 | ||
assert opp.workers_starting_delivery == 3 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
{% extends "program/base.html" %} | ||
{% load static %} | ||
{% load i18n %} | ||
{% load django_tables2 %} | ||
{% block title %}{{ request.org }} - Programs{% endblock %} | ||
|
||
{% block breadcrumbs_inner %} | ||
{{ block.super }} | ||
<li class="breadcrumb-item" aria-current="page">{{ program.name }}</li> | ||
<li class="breadcrumb-item active">{% translate "Dashboard" %}</li> | ||
{% endblock %} | ||
{% block content %} | ||
<div class="container bg-white shadow pb-2"> | ||
<div class="mt-5 py-3"> | ||
<h1> {% trans "Dashboard" %}</h1> | ||
</div> | ||
<section class="mt-4 shadow mb-5"> | ||
<ul class="nav nav-tabs fw-bold bg-primary-subtle" role="tablist"> | ||
<li class="nav-item" role="presentation"> | ||
<button class="nav-link active" id="funnel-performance-tab" data-bs-toggle="tab" | ||
data-bs-target="#funnel-performance-tab-pane" | ||
type="button" role="tab" aria-controls="funnel-performance" aria-selected="true"> | ||
<i class="bi bi-filter-square"></i> {% trans "Funnel Performance" %} | ||
</button> | ||
</li> | ||
|
||
</ul> | ||
<div class="tab-content"> | ||
<div class="tab-pane fade show active" id="funnel-performance-tab-pane" role="tabpanel" | ||
aria-labelledby="funnel-performance-tab" | ||
tabindex="0" hx-on::after-request="refreshTooltips()"> | ||
<div class="pb-4" id="funnel-performance-table-containers" | ||
hx-get="{% url 'program:funnel_performance_table' request.org.slug program.id %}" hx-trigger="load" | ||
hx-swap="outerHTML"> | ||
{% include "tables/table_placeholder.html" with num_cols=6 %} | ||
</div> | ||
</div> | ||
</div> | ||
</section> | ||
</div> | ||
{% endblock content %} |
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.
can this use the filter you defined above?