mirror of https://github.com/zulip/zulip.git
activity: Add view to see the ledger entries for a customer plan.
Adds a link to the plan ledger view in the current plan information shown in the support views. Link is not shown if the plan is 100% sponsored, e.g., the Community plan. Adds a formatted header area to the activity table template so that it's easy to add useful information to these activity views.
This commit is contained in:
parent
704423787b
commit
d54ca85de2
|
@ -42,11 +42,18 @@ class RemoteActivityPlanData:
|
|||
rate: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class ActivityHeaderEntry:
|
||||
name: str
|
||||
value: str | Markup
|
||||
|
||||
|
||||
def make_table(
|
||||
title: str,
|
||||
cols: Sequence[str],
|
||||
rows: Sequence[Any],
|
||||
*,
|
||||
header: list[ActivityHeaderEntry] | None = None,
|
||||
totals: Any | None = None,
|
||||
stats_link: Markup | None = None,
|
||||
has_row_class: bool = False,
|
||||
|
@ -58,7 +65,9 @@ def make_table(
|
|||
|
||||
rows = list(map(fix_row, rows))
|
||||
|
||||
data = dict(title=title, cols=cols, rows=rows, totals=totals, stats_link=stats_link)
|
||||
data = dict(
|
||||
title=title, cols=cols, rows=rows, header=header, totals=totals, stats_link=stats_link
|
||||
)
|
||||
|
||||
content = loader.render_to_string(
|
||||
"corporate/activity/activity_table.html",
|
||||
|
|
|
@ -208,6 +208,10 @@ class ActivityTest(ZulipTestCase):
|
|||
result = self.client_get(f"/user_activity/{iago.id}/")
|
||||
self.assertEqual(result.status_code, 200)
|
||||
|
||||
with self.assert_database_query_count(8):
|
||||
result = self.client_get(f"/activity/plan_ledger/{plan.id}/")
|
||||
self.assertEqual(result.status_code, 200)
|
||||
|
||||
def test_get_remote_server_guest_and_non_guest_count(self) -> None:
|
||||
RemoteRealmAuditLog.objects.bulk_create([RemoteRealmAuditLog(**data) for data in data_list])
|
||||
server_id = 1
|
||||
|
|
|
@ -25,6 +25,7 @@ from corporate.views.installation_activity import (
|
|||
get_installation_activity,
|
||||
get_integrations_activity,
|
||||
)
|
||||
from corporate.views.plan_activity import get_plan_ledger
|
||||
from corporate.views.portico import (
|
||||
app_download_link_redirect,
|
||||
apps_view,
|
||||
|
@ -105,6 +106,7 @@ i18n_urlpatterns: Any = [
|
|||
path("user_activity/<user_profile_id>/", get_user_activity),
|
||||
path("activity/remote", get_remote_server_activity),
|
||||
path("activity/remote/support", remote_servers_support, name="remote_servers_support"),
|
||||
path("activity/plan_ledger/<plan_id>/", get_plan_ledger),
|
||||
]
|
||||
|
||||
v1_api_and_json_patterns = [
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
from typing import Any
|
||||
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.shortcuts import render
|
||||
|
||||
from corporate.lib.activity import ActivityHeaderEntry, format_optional_datetime, make_table
|
||||
from corporate.models import Customer, CustomerPlan, LicenseLedger
|
||||
from zerver.decorator import require_server_admin
|
||||
|
||||
|
||||
def get_plan_billing_entity_name(customer: Customer) -> str:
|
||||
if customer.realm:
|
||||
return customer.realm.name
|
||||
elif customer.remote_realm:
|
||||
return customer.remote_realm.name
|
||||
assert customer.remote_server is not None
|
||||
return customer.remote_server.hostname
|
||||
|
||||
|
||||
@require_server_admin
|
||||
def get_plan_ledger(request: HttpRequest, plan_id: int) -> HttpResponse:
|
||||
plan = CustomerPlan.objects.get(id=plan_id)
|
||||
ledger_entries = LicenseLedger.objects.filter(plan=plan).order_by("-event_time")
|
||||
|
||||
name = get_plan_billing_entity_name(plan.customer)
|
||||
title = f"{name}"
|
||||
cols = [
|
||||
"Event time (UTC)",
|
||||
"Renewal",
|
||||
"License count",
|
||||
"Renewal count",
|
||||
]
|
||||
|
||||
def row(record: LicenseLedger) -> list[Any]:
|
||||
return [
|
||||
format_optional_datetime(record.event_time),
|
||||
record.is_renewal,
|
||||
record.licenses,
|
||||
record.licenses_at_next_renewal,
|
||||
]
|
||||
|
||||
rows = list(map(row, ledger_entries))
|
||||
|
||||
header_entries = []
|
||||
header_entries.append(
|
||||
ActivityHeaderEntry(name="Plan name", value=CustomerPlan.name_from_tier(plan.tier))
|
||||
)
|
||||
header_entries.append(
|
||||
ActivityHeaderEntry(
|
||||
name="Next invoice (UTC)", value=format_optional_datetime(plan.next_invoice_date, True)
|
||||
)
|
||||
)
|
||||
|
||||
content = make_table(title, cols, rows, header=header_entries)
|
||||
|
||||
return render(
|
||||
request,
|
||||
"corporate/activity/activity.html",
|
||||
context=dict(
|
||||
data=content,
|
||||
title=title,
|
||||
is_home=False,
|
||||
),
|
||||
)
|
|
@ -4,6 +4,14 @@
|
|||
{% include "corporate/activity/remote_activity_key.html" %}
|
||||
{% endif %}
|
||||
|
||||
{% if data.header %}
|
||||
<div class="activity-header-information">
|
||||
{% for entry in data.header %}
|
||||
<p class="activity-header-entry"><b>{{ entry.name }}</b>: {{ entry.value }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{{ data.rows|length}} rows
|
||||
<table class="table sortable table-striped table-bordered analytics-table">
|
||||
|
||||
|
|
|
@ -33,5 +33,6 @@
|
|||
<b>Annual recurring revenue</b>: ${{ dollar_amount(plan_data.annual_recurring_revenue) }}<br />
|
||||
<b>Start of next billing cycle</b>: {{ plan_data.next_billing_cycle_start.strftime('%d %B %Y') }}<br />
|
||||
{% endif %}
|
||||
<a target="_blank" rel="noopener noreferrer" href="/activity/plan_ledger/{{ plan_data.current_plan.id }}/">License ledger entries</a><br />
|
||||
{% endif %}
|
||||
</div>
|
||||
|
|
|
@ -54,6 +54,7 @@ not_yet_fully_covered = [
|
|||
# TODO: This is a work in progress and therefore without
|
||||
# tests yet.
|
||||
"corporate/views/installation_activity.py",
|
||||
"corporate/views/plan_activity.py",
|
||||
"corporate/views/realm_activity.py",
|
||||
"corporate/views/remote_billing_page.py",
|
||||
"corporate/views/support.py",
|
||||
|
|
|
@ -463,6 +463,7 @@ tr.admin td:first-child {
|
|||
padding-bottom: 25px;
|
||||
}
|
||||
|
||||
.activity-header-information,
|
||||
.push-notification-status,
|
||||
.realm-management-actions,
|
||||
.next-plan-container,
|
||||
|
@ -473,6 +474,17 @@ tr.admin td:first-child {
|
|||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.activity-header-information {
|
||||
border: 2px solid hsl(330deg 3% 40%);
|
||||
background-color: hsl(60deg 12% 90%);
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.activity-header-entry {
|
||||
margin: 0;
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
.push-notification-status,
|
||||
.realm-management-actions {
|
||||
border: 2px solid hsl(186deg 76% 36%);
|
||||
|
|
Loading…
Reference in New Issue