Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
1dd21ae
inital draft of using a materialized view to query user report data
RuthTurk Aug 28, 2025
e48582b
clean up materalized view migration and add SKIP_HEAVY_MIGRATION, add…
RuthTurk Sep 1, 2025
a163c57
Draft implementation of optimized usage data calculation for user report
rajpatel24 Sep 3, 2025
f9a90b3
Optimization for the cross database values
rajpatel24 Sep 9, 2025
4f6f0f8
add documentation for /user-reports endpoint
RuthTurk Sep 10, 2025
c688f58
Refactor billing and usage snapshot table
rajpatel24 Sep 11, 2025
9780930
Improvements
rajpatel24 Sep 11, 2025
9649670
Merge refactored endpoint logic into original implementation
rajpatel24 Sep 11, 2025
9304d87
add 'account_restricted' field
RuthTurk Sep 12, 2025
20896e5
Refactor snapshot and celery task logic
rajpatel24 Sep 12, 2025
061f595
Add comments
rajpatel24 Sep 12, 2025
dbd88e6
Merge branch 'main' of github.com:kobotoolbox/kpi into dev-899-optimi…
RuthTurk Sep 13, 2025
1227002
fix migration dependency errors
RuthTurk Sep 13, 2025
33f3bde
only run 0070 if Stripe is enabled and add error messages to the endp…
RuthTurk Sep 15, 2025
4d87545
Cleanup for filters
rajpatel24 Sep 15, 2025
38e1965
add tests
RuthTurk Sep 15, 2025
75e5a66
fix spelling
RuthTurk Sep 15, 2025
757b22f
skip tests if stripe is not enabled
RuthTurk Sep 15, 2025
3f421d1
fix tests and add openapi schema generated by management command
RuthTurk Sep 16, 2025
a54b789
remove newline at end of schema_v2.json
RuthTurk Sep 16, 2025
bf3bc31
Merge branch 'main' of github.com:kobotoolbox/kpi into dev-899-optimi…
RuthTurk Sep 19, 2025
817905d
clean up and organize code
RuthTurk Sep 19, 2025
9be8ae7
fix darker issues
RuthTurk Sep 19, 2025
8d7441b
move everything for this endpoint into a new django app called user_r…
RuthTurk Sep 22, 2025
e33b454
Merge branch 'main' of github.com:kobotoolbox/kpi into dev-899-optimi…
RuthTurk Sep 22, 2025
859243e
remove newline
RuthTurk Sep 22, 2025
bc57655
Update logic to retry failed Celery job from where it left off
rajpatel24 Oct 2, 2025
881712b
Fix linter issues
rajpatel24 Oct 2, 2025
71334ff
Minor fixes
rajpatel24 Oct 3, 2025
e698dde
Refactor user report snapshot task with resumable runs and add migrat…
rajpatel24 Oct 6, 2025
be0906b
Fix redis locks and simplify codes
noliveleger Oct 6, 2025
ccb0664
Refactor models and migrations to follow conventions and remove redun…
rajpatel24 Oct 7, 2025
0d3418b
Merge branch 'main' of github.com:kobotoolbox/kpi into dev-899-optimi…
RuthTurk Oct 8, 2025
c58e1b8
update audit logs tag to server logs and fix error message with corre…
RuthTurk Oct 8, 2025
a556695
Remove filtering logic from the branch
rajpatel24 Oct 8, 2025
e867928
fix darker and orval issues
RuthTurk Oct 8, 2025
7f87d61
rename materialized view 'user_reports_mv' to 'user_reports_userrepor…
RuthTurk Oct 8, 2025
fa632e4
don't include anonymous users in reports, create utility function to …
RuthTurk Oct 9, 2025
ffb30d7
Merge branch 'main' of github.com:kobotoolbox/kpi into dev-899-optimi…
RuthTurk Oct 9, 2025
6485dc3
fix tests
RuthTurk Oct 9, 2025
1232d5e
Update materialized view field names for consistency
rajpatel24 Oct 10, 2025
6e4fd44
Minor fixes
rajpatel24 Oct 10, 2025
aa84ad4
Fix linter issues
rajpatel24 Oct 10, 2025
b8bd62d
Add organization balance calculations to celery task and materialized…
rajpatel24 Oct 10, 2025
57212ef
Make QueryParse work with integer,float in JSONBfield
noliveleger Oct 10, 2025
76a38a1
Merge branch 'dev-899-optimize-queries-for-subscriptions' of github.c…
noliveleger Oct 10, 2025
f7cdc9a
Add unit test for process_value of QueryParser
noliveleger Oct 11, 2025
214b7ad
Rename 'organizations' to 'organization' and simplified field names
rajpatel24 Oct 12, 2025
a851aac
Fix failing tests and refactoring
rajpatel24 Oct 13, 2025
5713681
Merge branch 'main' of github.com:kobotoolbox/kpi into dev-899-optimi…
RuthTurk Oct 13, 2025
a8342bc
fix tests and add require_stripe to refresh_user_report_snapshots
RuthTurk Oct 14, 2025
10ba98b
Merge branch 'main' of github.com:kobotoolbox/kpi into dev-899-optimi…
RuthTurk Oct 14, 2025
1034ed2
use f string instead of replace for filtering out anonymous users
RuthTurk Oct 14, 2025
eed9c41
Remove redundant fields from the materialized view
rajpatel24 Oct 15, 2025
67d4d96
Refactor user reports logic to ensure compatibility with stripe-disab…
rajpatel24 Oct 15, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions kobo/apps/audit_log/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@


@extend_schema(
tags=['Audit logs (superusers)'],
tags=['Server logs (superusers)'],
)
@extend_schema_view(
list=extend_schema(
Expand Down Expand Up @@ -94,7 +94,7 @@ class AuditLogViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):

@extend_schema_view(
list=extend_schema(
tags=['Audit logs (superusers)'],
tags=['Server logs (superusers)'],
description=read_md('audit_log', 'access_logs/list'),
responses=open_api_200_ok_response(
AccessLogResponse,
Expand Down Expand Up @@ -157,7 +157,7 @@ class AccessLogViewSet(AuditLogViewSet):
require_auth=False,
validate_payload=False,
),
tags=['Audit logs (superusers)'],
tags=['Server logs (superusers)'],
)
)
class AllProjectHistoryLogViewSet(AuditLogViewSet):
Expand Down Expand Up @@ -188,7 +188,7 @@ class AllProjectHistoryLogViewSet(AuditLogViewSet):
require_auth=False,
validate_payload=False,
),
tags=['Audit logs (superusers)'],
tags=['Server logs (superusers)'],
)
@extend_schema(
methods=['POST'],
Expand All @@ -201,7 +201,7 @@ class AllProjectHistoryLogViewSet(AuditLogViewSet):
require_auth=False,
validate_payload=False,
),
tags=['Audit logs (superusers)'],
tags=['Server logs (superusers)'],
)
@action(detail=False, methods=['GET', 'POST'])
def export(self, request, *args, **kwargs):
Expand Down Expand Up @@ -469,7 +469,7 @@ def list(self, request, *args, **kwargs):


@extend_schema(
tags=['Audit logs (superusers)'],
tags=['Server logs (superusers)'],
)
@extend_schema_view(
list=extend_schema(
Expand Down
22 changes: 20 additions & 2 deletions kobo/apps/stripe/utils/billing_dates.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,42 @@
from zoneinfo import ZoneInfo

from dateutil.relativedelta import relativedelta
from django.conf import settings
from django.db.models import F, Max, Q, Window
from django.utils import timezone

from kobo.apps.organizations.models import Organization
from kobo.apps.organizations.types import BillingDates
from kobo.apps.stripe.constants import ACTIVE_STRIPE_STATUSES
from kobo.apps.stripe.utils.import_management import requires_stripe
from kobo.apps.user_reports.typing_aliases import OrganizationIterator


@requires_stripe
def get_current_billing_period_dates_by_org(
orgs: list[Organization] = None, **kwargs
orgs: OrganizationIterator = None, **kwargs
) -> dict[str, BillingDates]:

now = timezone.now().replace(tzinfo=ZoneInfo('UTC'))
first_of_this_month = datetime(now.year, now.month, 1, tzinfo=ZoneInfo('UTC'))
first_of_next_month = first_of_this_month + relativedelta(months=1)

if not settings.STRIPE_ENABLED:
results = {}
if orgs is not None:
for org in orgs:
results[org.id] = {
'start': first_of_this_month,
'end': first_of_next_month
}
return results

for org in Organization.objects.all():
results[org.id] = {
'start': first_of_this_month,
'end': first_of_next_month
}
return results

# check 1: look for active subscriptions
all_active_billing_dates = get_current_billing_period_dates_for_active_plans(orgs)

Expand Down
Empty file.
6 changes: 6 additions & 0 deletions kobo/apps/user_reports/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from django.apps import AppConfig


class UserReportsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'kobo.apps.user_reports'
212 changes: 212 additions & 0 deletions kobo/apps/user_reports/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
from django.db import migrations, models
from django.db.models import Q

import kpi.models.abstract_models


class Migration(migrations.Migration):

initial = True

dependencies = []

operations = [
migrations.CreateModel(
name='BillingAndUsageSnapshotRun',
fields=[
(
'id',
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name='ID'
)
),
('uid', kpi.fields.kpi_uid.KpiUidField(_null=False, uid_prefix='busr')),
(
'status',
models.CharField(
choices=[
('in_progress', 'In Progress'),
('completed', 'Completed'),
('aborted', 'Aborted')
],
default='in_progress',
max_length=32
)
),
('last_processed_org_id', models.CharField(blank=True, null=True)),
('details', models.JSONField(blank=True, null=True)),
('singleton', models.BooleanField(default=True, editable=False)),
(
'date_created',
models.DateTimeField(
default=kpi.models.abstract_models._get_default_datetime
),
),
(
'date_modified',
models.DateTimeField(
default=kpi.models.abstract_models._get_default_datetime
),
),
],
options={
'ordering': ['-date_created'],
},
),
migrations.CreateModel(
name='BillingAndUsageSnapshot',
fields=[
(
'id',
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name='ID'
)
),
(
'effective_user_id',
models.IntegerField(blank=True, null=True, db_index=True)
),
(
'last_snapshot_run',
models.ForeignKey(
on_delete=models.deletion.CASCADE,
related_name='snapshots',
to='user_reports.billingandusagesnapshotrun',
),
),
(
'organization',
models.OneToOneField(
on_delete=models.deletion.CASCADE,
to='organizations.organization',
),
),
('total_storage_bytes', models.BigIntegerField(default=0)),
('total_submission_count_all_time', models.BigIntegerField(default=0)),
(
'total_submission_count_current_period',
models.BigIntegerField(default=0)
),
('billing_period_start', models.DateTimeField(blank=True, null=True)),
('billing_period_end', models.DateTimeField(blank=True, null=True)),
(
'date_created',
models.DateTimeField(
default=kpi.models.abstract_models._get_default_datetime
),
),
(
'date_modified',
models.DateTimeField(
default=kpi.models.abstract_models._get_default_datetime
),
),
('submission_limit', models.BigIntegerField(blank=True, null=True)),
('storage_bytes_limit', models.BigIntegerField(blank=True, null=True)),
('asr_seconds_limit', models.BigIntegerField(blank=True, null=True)),
('mt_characters_limit', models.BigIntegerField(blank=True, null=True)),
],
),
migrations.AddIndex(
model_name='billingandusagesnapshot',
index=models.Index(fields=['effective_user_id'], name='idx_bau_user'),
),
migrations.AddIndex(
model_name='billingandusagesnapshot',
index=models.Index(fields=['date_created'], name='idx_bau_created'),
),
# Add index for runs
migrations.AddIndex(
model_name='billingandusagesnapshotrun',
index=models.Index(
fields=['status', 'date_modified'],
name='idx_bau_run_status_expires'
),
),
migrations.AddConstraint(
model_name='billingandusagesnapshotrun',
constraint=models.UniqueConstraint(
fields=('singleton',),
condition=Q(status='in_progress'),
name='uniq_run_in_progress'
),
),
migrations.AddConstraint(
model_name='billingandusagesnapshot',
constraint=models.UniqueConstraint(
fields=('organization',),
name='uniq_snapshot_per_org'
),
),
# Register the materialized-view model state (unmanaged) so Django knows
# the model exists for ORM queries, but doesn't try to create a migration
# file for it. The actual materialized view is created by the subsequent
# migration `0002_create_user_reports_mv.py`.
migrations.SeparateDatabaseAndState(
database_operations=[],
state_operations=[
migrations.CreateModel(
name='UserReports',
fields=[
(
'id',
models.BigIntegerField(primary_key=True, serialize=False)
),
(
'extra_details_uid',
models.CharField(max_length=255, null=True, blank=True)
),
('username', models.CharField(max_length=150)),
('first_name', models.CharField(max_length=150)),
('last_name', models.CharField(max_length=150)),
('email', models.CharField(max_length=254)),
('is_superuser', models.BooleanField()),
('is_staff', models.BooleanField()),
('is_active', models.BooleanField()),
('date_joined', models.CharField(max_length=64)),
(
'last_login',
models.CharField(max_length=64, null=True, blank=True)
),
('validated_email', models.BooleanField()),
('validated_password', models.BooleanField()),
('mfa_is_active', models.BooleanField()),
('sso_is_active', models.BooleanField()),
('accepted_tos', models.BooleanField()),
('social_accounts', models.JSONField(null=True, blank=True)),
('organizations', models.JSONField(null=True, blank=True)),
('metadata', models.JSONField(null=True, blank=True)),
('subscriptions', models.JSONField(null=True, blank=True)),
('asset_count', models.IntegerField(default=0)),
('deployed_asset_count', models.IntegerField(default=0)),
(
'current_period_start',
models.DateTimeField(null=True, blank=True)
),
(
'current_period_end',
models.DateTimeField(null=True, blank=True)
),
(
'organization_id',
models.IntegerField(null=True, blank=True)
),
(
'service_usage',
models.JSONField(null=True, blank=True),
),
],
options={
'managed': False,
'db_table': 'user_reports_userreportsmv',
},
),
],
),
]
Loading