django-api-usage 0.1.3__tar.gz → 0.1.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {django_api_usage-0.1.3/src/django_api_usage.egg-info → django_api_usage-0.1.5}/PKG-INFO +22 -4
- {django_api_usage-0.1.3 → django_api_usage-0.1.5}/README.md +21 -3
- {django_api_usage-0.1.3 → django_api_usage-0.1.5}/pyproject.toml +1 -1
- {django_api_usage-0.1.3 → django_api_usage-0.1.5}/src/django_api_usage/__init__.py +1 -1
- {django_api_usage-0.1.3 → django_api_usage-0.1.5}/src/django_api_usage/admin.py +82 -5
- {django_api_usage-0.1.3 → django_api_usage-0.1.5/src/django_api_usage.egg-info}/PKG-INFO +22 -4
- {django_api_usage-0.1.3 → django_api_usage-0.1.5}/src/django_api_usage.egg-info/SOURCES.txt +1 -0
- django_api_usage-0.1.5/tests/test_csv_export.py +209 -0
- {django_api_usage-0.1.3 → django_api_usage-0.1.5}/LICENSE +0 -0
- {django_api_usage-0.1.3 → django_api_usage-0.1.5}/setup.cfg +0 -0
- {django_api_usage-0.1.3 → django_api_usage-0.1.5}/src/django_api_usage/apps.py +0 -0
- {django_api_usage-0.1.3 → django_api_usage-0.1.5}/src/django_api_usage/buffers.py +0 -0
- {django_api_usage-0.1.3 → django_api_usage-0.1.5}/src/django_api_usage/checks.py +0 -0
- {django_api_usage-0.1.3 → django_api_usage-0.1.5}/src/django_api_usage/conf.py +0 -0
- {django_api_usage-0.1.3 → django_api_usage-0.1.5}/src/django_api_usage/deprecation.py +0 -0
- {django_api_usage-0.1.3 → django_api_usage-0.1.5}/src/django_api_usage/drf.py +0 -0
- {django_api_usage-0.1.3 → django_api_usage-0.1.5}/src/django_api_usage/maintenance.py +0 -0
- {django_api_usage-0.1.3 → django_api_usage-0.1.5}/src/django_api_usage/management/__init__.py +0 -0
- {django_api_usage-0.1.3 → django_api_usage-0.1.5}/src/django_api_usage/management/commands/__init__.py +0 -0
- {django_api_usage-0.1.3 → django_api_usage-0.1.5}/src/django_api_usage/management/commands/api_usage_flush.py +0 -0
- {django_api_usage-0.1.3 → django_api_usage-0.1.5}/src/django_api_usage/management/commands/api_usage_report.py +0 -0
- {django_api_usage-0.1.3 → django_api_usage-0.1.5}/src/django_api_usage/middleware.py +0 -0
- {django_api_usage-0.1.3 → django_api_usage-0.1.5}/src/django_api_usage/migrations/0001_initial.py +0 -0
- {django_api_usage-0.1.3 → django_api_usage-0.1.5}/src/django_api_usage/migrations/0002_endpoint_route_path.py +0 -0
- {django_api_usage-0.1.3 → django_api_usage-0.1.5}/src/django_api_usage/migrations/__init__.py +0 -0
- {django_api_usage-0.1.3 → django_api_usage-0.1.5}/src/django_api_usage/models.py +0 -0
- {django_api_usage-0.1.3 → django_api_usage-0.1.5}/src/django_api_usage/resolvers.py +0 -0
- {django_api_usage-0.1.3 → django_api_usage-0.1.5}/src/django_api_usage/tasks.py +0 -0
- {django_api_usage-0.1.3 → django_api_usage-0.1.5}/src/django_api_usage/templates/admin/django_api_usage/endpointstat/change_list.html +0 -0
- {django_api_usage-0.1.3 → django_api_usage-0.1.5}/src/django_api_usage/templates/admin/django_api_usage/endpointstat/confirm_clear.html +0 -0
- {django_api_usage-0.1.3 → django_api_usage-0.1.5}/src/django_api_usage.egg-info/dependency_links.txt +0 -0
- {django_api_usage-0.1.3 → django_api_usage-0.1.5}/src/django_api_usage.egg-info/requires.txt +0 -0
- {django_api_usage-0.1.3 → django_api_usage-0.1.5}/src/django_api_usage.egg-info/top_level.txt +0 -0
- {django_api_usage-0.1.3 → django_api_usage-0.1.5}/tests/test_admin.py +0 -0
- {django_api_usage-0.1.3 → django_api_usage-0.1.5}/tests/test_checks.py +0 -0
- {django_api_usage-0.1.3 → django_api_usage-0.1.5}/tests/test_commands.py +0 -0
- {django_api_usage-0.1.3 → django_api_usage-0.1.5}/tests/test_drf.py +0 -0
- {django_api_usage-0.1.3 → django_api_usage-0.1.5}/tests/test_middleware.py +0 -0
- {django_api_usage-0.1.3 → django_api_usage-0.1.5}/tests/test_models.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: django-api-usage
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.5
|
|
4
4
|
Summary: Lightweight, privacy-first usage metering for Django APIs, with an optional deprecation lifecycle layer.
|
|
5
5
|
Author-email: CodeSyntax <teknika@codesyntax.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -37,11 +37,11 @@ Requires-Dist: black==26.5.1; extra == "dev"
|
|
|
37
37
|
Requires-Dist: ruff==0.16.10; extra == "dev"
|
|
38
38
|
Dynamic: license-file
|
|
39
39
|
|
|
40
|
-

|
|
40
|
+

|
|
41
41
|

|
|
42
42
|

|
|
43
|
-

|
|
44
|
-

|
|
43
|
+

|
|
44
|
+

|
|
45
45
|
|
|
46
46
|
# django-api-usage
|
|
47
47
|
|
|
@@ -120,6 +120,24 @@ API_USAGE = {
|
|
|
120
120
|
}
|
|
121
121
|
```
|
|
122
122
|
|
|
123
|
+
## Admin
|
|
124
|
+
|
|
125
|
+
The package ships a read-only admin for the three models, built with plain
|
|
126
|
+
Django — **no dependency beyond Django itself** (no `django-object-actions` or
|
|
127
|
+
similar):
|
|
128
|
+
|
|
129
|
+
* **Endpoint stats** changelist: a search box (by endpoint path, route name, app,
|
|
130
|
+
method or replacement), filters by date, profile and role, and the **sum of
|
|
131
|
+
the `count` column** for the rows matching the current filters.
|
|
132
|
+
* **Flush now**: moves counters buffered in the cache into the database, so they
|
|
133
|
+
appear immediately when you run with `BUFFER_BACKEND = "cache"`.
|
|
134
|
+
* **Clear statistics**: empties the aggregated counters behind a confirmation
|
|
135
|
+
page (endpoints and their deprecation metadata are kept).
|
|
136
|
+
* **Export selected rows to CSV** on all three admins. Relations are flattened
|
|
137
|
+
into one analysis-friendly table (a stat row carries its endpoint's path, app
|
|
138
|
+
and method), so the file can be fed straight to pandas or a spreadsheet. Use
|
|
139
|
+
the "select all" link to export every row matching the current filters.
|
|
140
|
+
|
|
123
141
|
## Deprecation lifecycle
|
|
124
142
|
|
|
125
143
|
`Endpoint` carries `deprecated`, `sunset_date`, `replacement` and `owner`, so the
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-

|
|
1
|
+

|
|
2
2
|

|
|
3
3
|

|
|
4
|
-

|
|
5
|
-

|
|
4
|
+

|
|
5
|
+

|
|
6
6
|
|
|
7
7
|
# django-api-usage
|
|
8
8
|
|
|
@@ -81,6 +81,24 @@ API_USAGE = {
|
|
|
81
81
|
}
|
|
82
82
|
```
|
|
83
83
|
|
|
84
|
+
## Admin
|
|
85
|
+
|
|
86
|
+
The package ships a read-only admin for the three models, built with plain
|
|
87
|
+
Django — **no dependency beyond Django itself** (no `django-object-actions` or
|
|
88
|
+
similar):
|
|
89
|
+
|
|
90
|
+
* **Endpoint stats** changelist: a search box (by endpoint path, route name, app,
|
|
91
|
+
method or replacement), filters by date, profile and role, and the **sum of
|
|
92
|
+
the `count` column** for the rows matching the current filters.
|
|
93
|
+
* **Flush now**: moves counters buffered in the cache into the database, so they
|
|
94
|
+
appear immediately when you run with `BUFFER_BACKEND = "cache"`.
|
|
95
|
+
* **Clear statistics**: empties the aggregated counters behind a confirmation
|
|
96
|
+
page (endpoints and their deprecation metadata are kept).
|
|
97
|
+
* **Export selected rows to CSV** on all three admins. Relations are flattened
|
|
98
|
+
into one analysis-friendly table (a stat row carries its endpoint's path, app
|
|
99
|
+
and method), so the file can be fed straight to pandas or a spreadsheet. Use
|
|
100
|
+
the "select all" link to export every row matching the current filters.
|
|
101
|
+
|
|
84
102
|
## Deprecation lifecycle
|
|
85
103
|
|
|
86
104
|
`Endpoint` carries `deprecated`, `sunset_date`, `replacement` and `owner`, so the
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "django-api-usage"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.5"
|
|
8
8
|
description = "Lightweight, privacy-first usage metering for Django APIs, with an optional deprecation lifecycle layer."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -6,14 +6,21 @@
|
|
|
6
6
|
deprecation metadata) are kept on purpose.
|
|
7
7
|
|
|
8
8
|
The changelist also shows the **sum of the ``count`` column** for the rows that
|
|
9
|
-
match the current filters
|
|
9
|
+
match the current filters, and every admin ships an **export to CSV** action for
|
|
10
|
+
the selected rows (all the rows matching the filters can be selected with the
|
|
11
|
+
"select all" link).
|
|
12
|
+
|
|
13
|
+
Everything is plain Django: no extra dependency beyond Django itself.
|
|
10
14
|
"""
|
|
11
15
|
|
|
16
|
+
import csv
|
|
17
|
+
|
|
12
18
|
from django.contrib import admin, messages
|
|
13
19
|
from django.db.models import Sum
|
|
14
|
-
from django.http import HttpResponseRedirect
|
|
20
|
+
from django.http import HttpResponse, HttpResponseRedirect
|
|
15
21
|
from django.template.response import TemplateResponse
|
|
16
22
|
from django.urls import path, reverse
|
|
23
|
+
from django.utils import timezone
|
|
17
24
|
from django.utils.text import format_lazy
|
|
18
25
|
from django.utils.translation import gettext_lazy as _
|
|
19
26
|
|
|
@@ -21,8 +28,53 @@ from .buffers import flush
|
|
|
21
28
|
from .models import Consumer, Endpoint, EndpointStat
|
|
22
29
|
|
|
23
30
|
|
|
31
|
+
class CsvExportMixin:
|
|
32
|
+
"""Admin action to download the selected rows as CSV.
|
|
33
|
+
|
|
34
|
+
``csv_columns`` is an iterable of ``(header, queryset lookup)`` pairs. The
|
|
35
|
+
lookups are resolved by the database in a single query, so related rows can
|
|
36
|
+
be flattened into one analysis-friendly table (for example a stat row plus
|
|
37
|
+
its endpoint's path and app label).
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
#: ``(header, lookup)`` pairs. The header is written as-is (keep it stable
|
|
41
|
+
#: and ASCII: the file is meant to be read by pandas or a spreadsheet).
|
|
42
|
+
csv_columns = ()
|
|
43
|
+
#: Prefix of the downloaded filename; the date is appended.
|
|
44
|
+
csv_filename_prefix = "django-api-usage"
|
|
45
|
+
|
|
46
|
+
@admin.action(description=_("Export selected rows to CSV"))
|
|
47
|
+
def export_as_csv(self, request, queryset):
|
|
48
|
+
columns = list(self.csv_columns)
|
|
49
|
+
rows = queryset.values_list(*(lookup for _, lookup in columns))
|
|
50
|
+
|
|
51
|
+
filename = f"{self.csv_filename_prefix}-{timezone.localdate():%Y-%m-%d}.csv"
|
|
52
|
+
response = HttpResponse(content_type="text/csv; charset=utf-8")
|
|
53
|
+
response["Content-Disposition"] = f'attachment; filename="{filename}"'
|
|
54
|
+
# Byte order mark, so Excel detects UTF-8 (content may be Basque).
|
|
55
|
+
response.write("\ufeff")
|
|
56
|
+
|
|
57
|
+
writer = csv.writer(response)
|
|
58
|
+
writer.writerow([header for header, _ in columns])
|
|
59
|
+
writer.writerows(rows)
|
|
60
|
+
return response
|
|
61
|
+
|
|
62
|
+
|
|
24
63
|
@admin.register(Endpoint)
|
|
25
|
-
class EndpointAdmin(admin.ModelAdmin):
|
|
64
|
+
class EndpointAdmin(CsvExportMixin, admin.ModelAdmin):
|
|
65
|
+
actions = ("export_as_csv",)
|
|
66
|
+
csv_columns = (
|
|
67
|
+
("app_label", "app_label"),
|
|
68
|
+
("route_path", "route_path"),
|
|
69
|
+
("route_name", "route_name"),
|
|
70
|
+
("method", "method"),
|
|
71
|
+
("site_id", "site_id"),
|
|
72
|
+
("deprecated", "deprecated"),
|
|
73
|
+
("sunset_date", "sunset_date"),
|
|
74
|
+
("replacement", "replacement"),
|
|
75
|
+
("owner", "owner"),
|
|
76
|
+
("notes", "notes"),
|
|
77
|
+
)
|
|
26
78
|
list_display = (
|
|
27
79
|
"app_label",
|
|
28
80
|
"route_path",
|
|
@@ -40,7 +92,21 @@ class EndpointAdmin(admin.ModelAdmin):
|
|
|
40
92
|
|
|
41
93
|
|
|
42
94
|
@admin.register(EndpointStat)
|
|
43
|
-
class EndpointStatAdmin(admin.ModelAdmin):
|
|
95
|
+
class EndpointStatAdmin(CsvExportMixin, admin.ModelAdmin):
|
|
96
|
+
actions = ("export_as_csv",)
|
|
97
|
+
# Flattened on purpose: the CSV is meant for analysis (pandas, spreadsheet)
|
|
98
|
+
# without having to join endpoints by hand.
|
|
99
|
+
csv_columns = (
|
|
100
|
+
("date", "date"),
|
|
101
|
+
("app_label", "endpoint__app_label"),
|
|
102
|
+
("route_path", "endpoint__route_path"),
|
|
103
|
+
("route_name", "endpoint__route_name"),
|
|
104
|
+
("method", "endpoint__method"),
|
|
105
|
+
("site_id", "endpoint__site_id"),
|
|
106
|
+
("client_type", "client_type"),
|
|
107
|
+
("status_class", "status_class"),
|
|
108
|
+
("count", "count"),
|
|
109
|
+
)
|
|
44
110
|
list_display = ("date", "endpoint", "client_type", "status_class", "count")
|
|
45
111
|
list_filter = ("date", "client_type", "status_class")
|
|
46
112
|
list_select_related = ("endpoint",)
|
|
@@ -147,7 +213,18 @@ class EndpointStatAdmin(admin.ModelAdmin):
|
|
|
147
213
|
|
|
148
214
|
|
|
149
215
|
@admin.register(Consumer)
|
|
150
|
-
class ConsumerAdmin(admin.ModelAdmin):
|
|
216
|
+
class ConsumerAdmin(CsvExportMixin, admin.ModelAdmin):
|
|
217
|
+
actions = ("export_as_csv",)
|
|
218
|
+
csv_columns = (
|
|
219
|
+
("kind", "kind"),
|
|
220
|
+
("ref_hash", "ref_hash"),
|
|
221
|
+
("user_agent_family", "user_agent_family"),
|
|
222
|
+
("request_count", "request_count"),
|
|
223
|
+
("first_seen", "first_seen"),
|
|
224
|
+
("last_seen", "last_seen"),
|
|
225
|
+
("contact", "contact"),
|
|
226
|
+
("notes", "notes"),
|
|
227
|
+
)
|
|
151
228
|
list_display = (
|
|
152
229
|
"kind",
|
|
153
230
|
"ref_short",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: django-api-usage
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.5
|
|
4
4
|
Summary: Lightweight, privacy-first usage metering for Django APIs, with an optional deprecation lifecycle layer.
|
|
5
5
|
Author-email: CodeSyntax <teknika@codesyntax.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -37,11 +37,11 @@ Requires-Dist: black==26.5.1; extra == "dev"
|
|
|
37
37
|
Requires-Dist: ruff==0.16.10; extra == "dev"
|
|
38
38
|
Dynamic: license-file
|
|
39
39
|
|
|
40
|
-

|
|
40
|
+

|
|
41
41
|

|
|
42
42
|

|
|
43
|
-

|
|
44
|
-

|
|
43
|
+

|
|
44
|
+

|
|
45
45
|
|
|
46
46
|
# django-api-usage
|
|
47
47
|
|
|
@@ -120,6 +120,24 @@ API_USAGE = {
|
|
|
120
120
|
}
|
|
121
121
|
```
|
|
122
122
|
|
|
123
|
+
## Admin
|
|
124
|
+
|
|
125
|
+
The package ships a read-only admin for the three models, built with plain
|
|
126
|
+
Django — **no dependency beyond Django itself** (no `django-object-actions` or
|
|
127
|
+
similar):
|
|
128
|
+
|
|
129
|
+
* **Endpoint stats** changelist: a search box (by endpoint path, route name, app,
|
|
130
|
+
method or replacement), filters by date, profile and role, and the **sum of
|
|
131
|
+
the `count` column** for the rows matching the current filters.
|
|
132
|
+
* **Flush now**: moves counters buffered in the cache into the database, so they
|
|
133
|
+
appear immediately when you run with `BUFFER_BACKEND = "cache"`.
|
|
134
|
+
* **Clear statistics**: empties the aggregated counters behind a confirmation
|
|
135
|
+
page (endpoints and their deprecation metadata are kept).
|
|
136
|
+
* **Export selected rows to CSV** on all three admins. Relations are flattened
|
|
137
|
+
into one analysis-friendly table (a stat row carries its endpoint's path, app
|
|
138
|
+
and method), so the file can be fed straight to pandas or a spreadsheet. Use
|
|
139
|
+
the "select all" link to export every row matching the current filters.
|
|
140
|
+
|
|
123
141
|
## Deprecation lifecycle
|
|
124
142
|
|
|
125
143
|
`Endpoint` carries `deprecated`, `sunset_date`, `replacement` and `owner`, so the
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"""Tests for the "export selected rows to CSV" admin action."""
|
|
2
|
+
|
|
3
|
+
import csv
|
|
4
|
+
import io
|
|
5
|
+
|
|
6
|
+
from django.contrib.auth.models import User
|
|
7
|
+
from django.urls import reverse
|
|
8
|
+
|
|
9
|
+
from django_api_usage.models import Consumer, Endpoint, EndpointStat
|
|
10
|
+
|
|
11
|
+
from .base import UsageTestCase
|
|
12
|
+
|
|
13
|
+
ENDPOINT_CHANGELIST = "admin:django_api_usage_endpoint_changelist"
|
|
14
|
+
STAT_CHANGELIST = "admin:django_api_usage_endpointstat_changelist"
|
|
15
|
+
CONSUMER_CHANGELIST = "admin:django_api_usage_consumer_changelist"
|
|
16
|
+
|
|
17
|
+
BOM = b"\xef\xbb\xbf"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class CsvExportTest(UsageTestCase):
|
|
21
|
+
def setUp(self):
|
|
22
|
+
super().setUp()
|
|
23
|
+
self.admin = User.objects.create_superuser("admin", "a@example.com", "pw")
|
|
24
|
+
self.client.force_login(self.admin)
|
|
25
|
+
|
|
26
|
+
# -- helpers ------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
def export(self, changelist, objects, action="export_as_csv"):
|
|
29
|
+
return self.client.post(
|
|
30
|
+
reverse(changelist),
|
|
31
|
+
{
|
|
32
|
+
"action": action,
|
|
33
|
+
"select_across": "0",
|
|
34
|
+
"index": "0",
|
|
35
|
+
"_selected_action": [str(obj.pk) for obj in objects],
|
|
36
|
+
},
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
def rows(self, response):
|
|
40
|
+
"""Return the CSV as a list of rows (BOM stripped)."""
|
|
41
|
+
content = response.content.decode("utf-8-sig")
|
|
42
|
+
return list(csv.reader(io.StringIO(content)))
|
|
43
|
+
|
|
44
|
+
def make_endpoint(self, route_path="api/3.0/herriak/", app_label="api"):
|
|
45
|
+
return Endpoint.objects.create(
|
|
46
|
+
app_label=app_label,
|
|
47
|
+
route_path=route_path,
|
|
48
|
+
route_name="town-list",
|
|
49
|
+
method="GET",
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
def make_stat(self, endpoint, count=1, client_type="anon"):
|
|
53
|
+
return EndpointStat.objects.create(
|
|
54
|
+
endpoint=endpoint,
|
|
55
|
+
date="2026-10-08",
|
|
56
|
+
client_type=client_type,
|
|
57
|
+
status_class="2xx",
|
|
58
|
+
count=count,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
def assert_csv_response(self, response):
|
|
62
|
+
self.assertEqual(response.status_code, 200)
|
|
63
|
+
self.assertEqual(response["Content-Type"], "text/csv; charset=utf-8")
|
|
64
|
+
self.assertIn("attachment;", response["Content-Disposition"])
|
|
65
|
+
self.assertIn(".csv", response["Content-Disposition"])
|
|
66
|
+
# The BOM makes Excel detect UTF-8.
|
|
67
|
+
self.assertTrue(response.content.startswith(BOM))
|
|
68
|
+
|
|
69
|
+
# -- action availability ------------------------------------------------
|
|
70
|
+
|
|
71
|
+
def test_action_is_offered_in_the_changelist(self):
|
|
72
|
+
endpoint = self.make_endpoint()
|
|
73
|
+
self.make_stat(endpoint)
|
|
74
|
+
|
|
75
|
+
response = self.client.get(reverse(STAT_CHANGELIST))
|
|
76
|
+
|
|
77
|
+
self.assertContains(response, "Export selected rows to CSV")
|
|
78
|
+
self.assertContains(response, 'value="export_as_csv"')
|
|
79
|
+
|
|
80
|
+
def test_delete_selected_is_still_available(self):
|
|
81
|
+
self.make_endpoint()
|
|
82
|
+
|
|
83
|
+
changelist = self.client.get(reverse(ENDPOINT_CHANGELIST))
|
|
84
|
+
|
|
85
|
+
self.assertContains(changelist, "delete_selected")
|
|
86
|
+
|
|
87
|
+
# -- stats export -------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
def test_stats_export_header_is_stable(self):
|
|
90
|
+
endpoint = self.make_endpoint()
|
|
91
|
+
stat = self.make_stat(endpoint)
|
|
92
|
+
|
|
93
|
+
response = self.export(STAT_CHANGELIST, [stat])
|
|
94
|
+
|
|
95
|
+
self.assert_csv_response(response)
|
|
96
|
+
self.assertEqual(
|
|
97
|
+
self.rows(response)[0],
|
|
98
|
+
[
|
|
99
|
+
"date",
|
|
100
|
+
"app_label",
|
|
101
|
+
"route_path",
|
|
102
|
+
"route_name",
|
|
103
|
+
"method",
|
|
104
|
+
"site_id",
|
|
105
|
+
"client_type",
|
|
106
|
+
"status_class",
|
|
107
|
+
"count",
|
|
108
|
+
],
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
def test_stats_export_flattens_the_endpoint(self):
|
|
112
|
+
endpoint = self.make_endpoint(route_path="^api/3.0/herriak/$")
|
|
113
|
+
stat = self.make_stat(endpoint, count=7)
|
|
114
|
+
|
|
115
|
+
response = self.export(STAT_CHANGELIST, [stat])
|
|
116
|
+
|
|
117
|
+
body = self.rows(response)[1]
|
|
118
|
+
self.assertEqual(body[0], "2026-10-08")
|
|
119
|
+
self.assertEqual(body[1], "api")
|
|
120
|
+
self.assertEqual(body[2], "^api/3.0/herriak/$")
|
|
121
|
+
self.assertEqual(body[3], "town-list")
|
|
122
|
+
self.assertEqual(body[4], "GET")
|
|
123
|
+
self.assertEqual(body[6], "anon")
|
|
124
|
+
self.assertEqual(body[7], "2xx")
|
|
125
|
+
self.assertEqual(body[8], "7")
|
|
126
|
+
|
|
127
|
+
def test_stats_export_only_includes_the_selected_rows(self):
|
|
128
|
+
endpoint = self.make_endpoint()
|
|
129
|
+
selected = self.make_stat(endpoint, count=1)
|
|
130
|
+
self.make_stat(endpoint, count=99, client_type="user")
|
|
131
|
+
|
|
132
|
+
response = self.export(STAT_CHANGELIST, [selected])
|
|
133
|
+
|
|
134
|
+
rows = self.rows(response)
|
|
135
|
+
self.assertEqual(len(rows), 2) # header + one row
|
|
136
|
+
self.assertEqual(rows[1][8], "1")
|
|
137
|
+
|
|
138
|
+
def test_stats_export_filename_contains_the_date(self):
|
|
139
|
+
endpoint = self.make_endpoint()
|
|
140
|
+
stat = self.make_stat(endpoint)
|
|
141
|
+
|
|
142
|
+
response = self.export(STAT_CHANGELIST, [stat])
|
|
143
|
+
|
|
144
|
+
self.assertIn('filename="django-api-usage-', response["Content-Disposition"])
|
|
145
|
+
|
|
146
|
+
# -- endpoints and consumers export -------------------------------------
|
|
147
|
+
|
|
148
|
+
def test_endpoints_export(self):
|
|
149
|
+
endpoint = self.make_endpoint()
|
|
150
|
+
Endpoint.objects.filter(pk=endpoint.pk).update(
|
|
151
|
+
deprecated=True, replacement="api/4.0/herriak/"
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
response = self.export(ENDPOINT_CHANGELIST, [endpoint])
|
|
155
|
+
|
|
156
|
+
self.assert_csv_response(response)
|
|
157
|
+
rows = self.rows(response)
|
|
158
|
+
self.assertIn("deprecated", rows[0])
|
|
159
|
+
self.assertIn("replacement", rows[0])
|
|
160
|
+
self.assertIn("True", rows[1])
|
|
161
|
+
self.assertIn("api/4.0/herriak/", rows[1])
|
|
162
|
+
|
|
163
|
+
def test_consumers_export(self):
|
|
164
|
+
consumer = Consumer.objects.create(
|
|
165
|
+
kind="ip_hash",
|
|
166
|
+
ref_hash="a" * 64,
|
|
167
|
+
user_agent_family="firefox",
|
|
168
|
+
request_count=12,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
response = self.export(CONSUMER_CHANGELIST, [consumer])
|
|
172
|
+
|
|
173
|
+
self.assert_csv_response(response)
|
|
174
|
+
rows = self.rows(response)
|
|
175
|
+
self.assertEqual(rows[0][0], "kind")
|
|
176
|
+
self.assertEqual(rows[0][1], "ref_hash")
|
|
177
|
+
self.assertEqual(rows[1][0], "ip_hash")
|
|
178
|
+
self.assertEqual(rows[1][1], "a" * 64)
|
|
179
|
+
|
|
180
|
+
# -- edge cases ---------------------------------------------------------
|
|
181
|
+
|
|
182
|
+
def test_export_without_selection_is_not_a_csv(self):
|
|
183
|
+
"""Django warns and redirects instead of downloading an empty file."""
|
|
184
|
+
response = self.client.post(
|
|
185
|
+
reverse(STAT_CHANGELIST),
|
|
186
|
+
{"action": "export_as_csv", "select_across": "0", "index": "0"},
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
self.assertEqual(response.status_code, 302)
|
|
190
|
+
self.assertNotEqual(response.get("Content-Type"), "text/csv; charset=utf-8")
|
|
191
|
+
|
|
192
|
+
def test_export_all_matching_rows_with_select_across(self):
|
|
193
|
+
"""One checked row + "select all" exports every row matching the filters."""
|
|
194
|
+
endpoint = self.make_endpoint()
|
|
195
|
+
first = self.make_stat(endpoint, count=1)
|
|
196
|
+
self.make_stat(endpoint, count=2, client_type="user")
|
|
197
|
+
|
|
198
|
+
response = self.client.post(
|
|
199
|
+
reverse(STAT_CHANGELIST),
|
|
200
|
+
{
|
|
201
|
+
"action": "export_as_csv",
|
|
202
|
+
"select_across": "1",
|
|
203
|
+
"index": "0",
|
|
204
|
+
"_selected_action": [str(first.pk)],
|
|
205
|
+
},
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
self.assert_csv_response(response)
|
|
209
|
+
self.assertEqual(len(self.rows(response)), 3) # header + two rows
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_api_usage-0.1.3 → django_api_usage-0.1.5}/src/django_api_usage/management/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_api_usage-0.1.3 → django_api_usage-0.1.5}/src/django_api_usage/migrations/0001_initial.py
RENAMED
|
File without changes
|
|
File without changes
|
{django_api_usage-0.1.3 → django_api_usage-0.1.5}/src/django_api_usage/migrations/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_api_usage-0.1.3 → django_api_usage-0.1.5}/src/django_api_usage.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{django_api_usage-0.1.3 → django_api_usage-0.1.5}/src/django_api_usage.egg-info/requires.txt
RENAMED
|
File without changes
|
{django_api_usage-0.1.3 → django_api_usage-0.1.5}/src/django_api_usage.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|