mirror of
				https://github.com/zulip/zulip.git
				synced 2025-10-31 03:53:50 +00:00 
			
		
		
		
	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:
		
				
					committed by
					
						 Tim Abbott
						Tim Abbott
					
				
			
			
				
	
			
			
			
						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 = [ | ||||
|   | ||||
							
								
								
									
										64
									
								
								corporate/views/plan_activity.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								corporate/views/plan_activity.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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%); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user