django-api-usage 0.1.0__tar.gz → 0.1.2__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.0/src/django_api_usage.egg-info → django_api_usage-0.1.2}/PKG-INFO +1 -1
- {django_api_usage-0.1.0 → django_api_usage-0.1.2}/pyproject.toml +4 -1
- {django_api_usage-0.1.0 → django_api_usage-0.1.2}/src/django_api_usage/__init__.py +1 -1
- django_api_usage-0.1.2/src/django_api_usage/admin.py +164 -0
- django_api_usage-0.1.2/src/django_api_usage/templates/admin/django_api_usage/endpointstat/change_list.html +24 -0
- django_api_usage-0.1.2/src/django_api_usage/templates/admin/django_api_usage/endpointstat/confirm_clear.html +27 -0
- {django_api_usage-0.1.0 → django_api_usage-0.1.2/src/django_api_usage.egg-info}/PKG-INFO +1 -1
- {django_api_usage-0.1.0 → django_api_usage-0.1.2}/src/django_api_usage.egg-info/SOURCES.txt +3 -0
- django_api_usage-0.1.2/tests/test_admin.py +172 -0
- django_api_usage-0.1.0/src/django_api_usage/admin.py +0 -56
- {django_api_usage-0.1.0 → django_api_usage-0.1.2}/LICENSE +0 -0
- {django_api_usage-0.1.0 → django_api_usage-0.1.2}/README.md +0 -0
- {django_api_usage-0.1.0 → django_api_usage-0.1.2}/setup.cfg +0 -0
- {django_api_usage-0.1.0 → django_api_usage-0.1.2}/src/django_api_usage/apps.py +0 -0
- {django_api_usage-0.1.0 → django_api_usage-0.1.2}/src/django_api_usage/buffers.py +0 -0
- {django_api_usage-0.1.0 → django_api_usage-0.1.2}/src/django_api_usage/checks.py +0 -0
- {django_api_usage-0.1.0 → django_api_usage-0.1.2}/src/django_api_usage/conf.py +0 -0
- {django_api_usage-0.1.0 → django_api_usage-0.1.2}/src/django_api_usage/deprecation.py +0 -0
- {django_api_usage-0.1.0 → django_api_usage-0.1.2}/src/django_api_usage/drf.py +0 -0
- {django_api_usage-0.1.0 → django_api_usage-0.1.2}/src/django_api_usage/maintenance.py +0 -0
- {django_api_usage-0.1.0 → django_api_usage-0.1.2}/src/django_api_usage/management/__init__.py +0 -0
- {django_api_usage-0.1.0 → django_api_usage-0.1.2}/src/django_api_usage/management/commands/__init__.py +0 -0
- {django_api_usage-0.1.0 → django_api_usage-0.1.2}/src/django_api_usage/management/commands/api_usage_flush.py +0 -0
- {django_api_usage-0.1.0 → django_api_usage-0.1.2}/src/django_api_usage/management/commands/api_usage_report.py +0 -0
- {django_api_usage-0.1.0 → django_api_usage-0.1.2}/src/django_api_usage/middleware.py +0 -0
- {django_api_usage-0.1.0 → django_api_usage-0.1.2}/src/django_api_usage/migrations/0001_initial.py +0 -0
- {django_api_usage-0.1.0 → django_api_usage-0.1.2}/src/django_api_usage/migrations/__init__.py +0 -0
- {django_api_usage-0.1.0 → django_api_usage-0.1.2}/src/django_api_usage/models.py +0 -0
- {django_api_usage-0.1.0 → django_api_usage-0.1.2}/src/django_api_usage/resolvers.py +0 -0
- {django_api_usage-0.1.0 → django_api_usage-0.1.2}/src/django_api_usage/tasks.py +0 -0
- {django_api_usage-0.1.0 → django_api_usage-0.1.2}/src/django_api_usage.egg-info/dependency_links.txt +0 -0
- {django_api_usage-0.1.0 → django_api_usage-0.1.2}/src/django_api_usage.egg-info/requires.txt +0 -0
- {django_api_usage-0.1.0 → django_api_usage-0.1.2}/src/django_api_usage.egg-info/top_level.txt +0 -0
- {django_api_usage-0.1.0 → django_api_usage-0.1.2}/tests/test_checks.py +0 -0
- {django_api_usage-0.1.0 → django_api_usage-0.1.2}/tests/test_commands.py +0 -0
- {django_api_usage-0.1.0 → django_api_usage-0.1.2}/tests/test_drf.py +0 -0
- {django_api_usage-0.1.0 → django_api_usage-0.1.2}/tests/test_middleware.py +0 -0
- {django_api_usage-0.1.0 → django_api_usage-0.1.2}/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.2
|
|
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
|
|
@@ -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.2"
|
|
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"
|
|
@@ -41,6 +41,9 @@ Issues = "https://github.com/codesyntax/django-api-usage/issues"
|
|
|
41
41
|
[tool.setuptools.packages.find]
|
|
42
42
|
where = ["src"]
|
|
43
43
|
|
|
44
|
+
[tool.setuptools.package-data]
|
|
45
|
+
django_api_usage = ["templates/admin/django_api_usage/endpointstat/*.html"]
|
|
46
|
+
|
|
44
47
|
[tool.black]
|
|
45
48
|
line-length = 88
|
|
46
49
|
target-version = ["py310"]
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""Read-only admin for usage counters, with two maintenance buttons.
|
|
2
|
+
|
|
3
|
+
* **Flush now** moves counters buffered in the cache to the database, so they
|
|
4
|
+
show up here immediately when ``BUFFER_BACKEND = "cache"``.
|
|
5
|
+
* **Clear statistics** empties the aggregated counters. Endpoints (and their
|
|
6
|
+
deprecation metadata) are kept on purpose.
|
|
7
|
+
|
|
8
|
+
The changelist also shows the **sum of the ``count`` column** for the rows that
|
|
9
|
+
match the current filters.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from django.contrib import admin, messages
|
|
13
|
+
from django.db.models import Sum
|
|
14
|
+
from django.http import HttpResponseRedirect
|
|
15
|
+
from django.template.response import TemplateResponse
|
|
16
|
+
from django.urls import path, reverse
|
|
17
|
+
from django.utils.text import format_lazy
|
|
18
|
+
from django.utils.translation import gettext_lazy as _
|
|
19
|
+
|
|
20
|
+
from .buffers import flush
|
|
21
|
+
from .models import Consumer, Endpoint, EndpointStat
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@admin.register(Endpoint)
|
|
25
|
+
class EndpointAdmin(admin.ModelAdmin):
|
|
26
|
+
list_display = (
|
|
27
|
+
"app_label",
|
|
28
|
+
"route_name",
|
|
29
|
+
"method",
|
|
30
|
+
"site_id",
|
|
31
|
+
"deprecated",
|
|
32
|
+
"sunset_date",
|
|
33
|
+
"replacement",
|
|
34
|
+
"owner",
|
|
35
|
+
)
|
|
36
|
+
list_filter = ("deprecated", "app_label", "method", "site_id")
|
|
37
|
+
search_fields = ("route_name", "replacement", "owner", "notes")
|
|
38
|
+
list_editable = ("deprecated", "sunset_date", "replacement", "owner")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@admin.register(EndpointStat)
|
|
42
|
+
class EndpointStatAdmin(admin.ModelAdmin):
|
|
43
|
+
list_display = ("date", "endpoint", "client_type", "status_class", "count")
|
|
44
|
+
list_filter = ("date", "client_type", "status_class")
|
|
45
|
+
list_select_related = ("endpoint",)
|
|
46
|
+
date_hierarchy = "date"
|
|
47
|
+
# Search across the related endpoint (case-insensitive "contains").
|
|
48
|
+
search_fields = (
|
|
49
|
+
"endpoint__route_name",
|
|
50
|
+
"endpoint__app_label",
|
|
51
|
+
"endpoint__method",
|
|
52
|
+
"endpoint__replacement",
|
|
53
|
+
)
|
|
54
|
+
search_help_text = _(
|
|
55
|
+
"Search by endpoint route, app, method or replacement (for example "
|
|
56
|
+
"'artikuluak' or 'gida')."
|
|
57
|
+
)
|
|
58
|
+
change_list_template = "admin/django_api_usage/endpointstat/change_list.html"
|
|
59
|
+
|
|
60
|
+
def has_add_permission(self, request):
|
|
61
|
+
return False
|
|
62
|
+
|
|
63
|
+
def has_change_permission(self, request, obj=None):
|
|
64
|
+
return False
|
|
65
|
+
|
|
66
|
+
# -- changelist total ---------------------------------------------------
|
|
67
|
+
|
|
68
|
+
def changelist_view(self, request, extra_context=None):
|
|
69
|
+
"""Add the sum of ``count`` over the *filtered* queryset to the context."""
|
|
70
|
+
response = super().changelist_view(request, extra_context=extra_context)
|
|
71
|
+
try:
|
|
72
|
+
changelist = response.context_data["cl"]
|
|
73
|
+
except (AttributeError, KeyError):
|
|
74
|
+
return response
|
|
75
|
+
response.context_data["usage_total"] = (
|
|
76
|
+
changelist.queryset.aggregate(total=Sum("count"))["total"] or 0
|
|
77
|
+
)
|
|
78
|
+
return response
|
|
79
|
+
|
|
80
|
+
# -- custom maintenance views -------------------------------------------
|
|
81
|
+
|
|
82
|
+
def get_urls(self):
|
|
83
|
+
app_label, model_name = self.opts.app_label, self.opts.model_name
|
|
84
|
+
custom = [
|
|
85
|
+
path(
|
|
86
|
+
"flush/",
|
|
87
|
+
self.admin_site.admin_view(self.flush_view),
|
|
88
|
+
name=f"{app_label}_{model_name}_flush",
|
|
89
|
+
),
|
|
90
|
+
path(
|
|
91
|
+
"clear/",
|
|
92
|
+
self.admin_site.admin_view(self.clear_view),
|
|
93
|
+
name=f"{app_label}_{model_name}_clear",
|
|
94
|
+
),
|
|
95
|
+
]
|
|
96
|
+
return custom + super().get_urls()
|
|
97
|
+
|
|
98
|
+
def _changelist_url(self):
|
|
99
|
+
app_label, model_name = self.opts.app_label, self.opts.model_name
|
|
100
|
+
return reverse(f"admin:{app_label}_{model_name}_changelist")
|
|
101
|
+
|
|
102
|
+
def flush_view(self, request):
|
|
103
|
+
"""Move buffered counters from the cache into the database."""
|
|
104
|
+
if request.method != "POST":
|
|
105
|
+
return HttpResponseRedirect(self._changelist_url())
|
|
106
|
+
written = flush()
|
|
107
|
+
if written:
|
|
108
|
+
self.message_user(
|
|
109
|
+
request,
|
|
110
|
+
format_lazy(_("Flushed {count} buffered counter(s)."), count=written),
|
|
111
|
+
messages.SUCCESS,
|
|
112
|
+
)
|
|
113
|
+
else:
|
|
114
|
+
self.message_user(
|
|
115
|
+
request,
|
|
116
|
+
_(
|
|
117
|
+
"Nothing was buffered: with BUFFER_BACKEND='db' counters are "
|
|
118
|
+
"already in the database."
|
|
119
|
+
),
|
|
120
|
+
messages.INFO,
|
|
121
|
+
)
|
|
122
|
+
return HttpResponseRedirect(self._changelist_url())
|
|
123
|
+
|
|
124
|
+
def clear_view(self, request):
|
|
125
|
+
"""Empty the aggregated counters (a confirmation page is shown first)."""
|
|
126
|
+
if request.method != "POST":
|
|
127
|
+
context = {
|
|
128
|
+
**self.admin_site.each_context(request),
|
|
129
|
+
"title": _("Clear statistics"),
|
|
130
|
+
"opts": self.opts,
|
|
131
|
+
"total": EndpointStat.objects.count(),
|
|
132
|
+
}
|
|
133
|
+
return TemplateResponse(
|
|
134
|
+
request,
|
|
135
|
+
"admin/django_api_usage/endpointstat/confirm_clear.html",
|
|
136
|
+
context,
|
|
137
|
+
)
|
|
138
|
+
deleted = EndpointStat.objects.all().delete()[0]
|
|
139
|
+
self.message_user(
|
|
140
|
+
request,
|
|
141
|
+
format_lazy(_("Deleted {count} statistic row(s)."), count=deleted),
|
|
142
|
+
messages.SUCCESS,
|
|
143
|
+
)
|
|
144
|
+
return HttpResponseRedirect(self._changelist_url())
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@admin.register(Consumer)
|
|
148
|
+
class ConsumerAdmin(admin.ModelAdmin):
|
|
149
|
+
list_display = (
|
|
150
|
+
"kind",
|
|
151
|
+
"ref_short",
|
|
152
|
+
"user_agent_family",
|
|
153
|
+
"request_count",
|
|
154
|
+
"first_seen",
|
|
155
|
+
"last_seen",
|
|
156
|
+
"contact",
|
|
157
|
+
)
|
|
158
|
+
list_filter = ("kind",)
|
|
159
|
+
search_fields = ("contact", "notes")
|
|
160
|
+
list_editable = ("contact",)
|
|
161
|
+
|
|
162
|
+
@admin.display(description="Reference")
|
|
163
|
+
def ref_short(self, obj):
|
|
164
|
+
return obj.ref_hash[:12]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{% extends "admin/change_list.html" %}
|
|
2
|
+
{% load i18n %}
|
|
3
|
+
|
|
4
|
+
{% block object-tools-items %}
|
|
5
|
+
<li>
|
|
6
|
+
<form method="post" action="{% url 'admin:django_api_usage_endpointstat_flush' %}">
|
|
7
|
+
{% csrf_token %}
|
|
8
|
+
<button type="submit" class="button">{% translate "Flush now" %}</button>
|
|
9
|
+
</form>
|
|
10
|
+
</li>
|
|
11
|
+
<li>
|
|
12
|
+
<a href="{% url 'admin:django_api_usage_endpointstat_clear' %}" class="button">{% translate "Clear statistics" %}</a>
|
|
13
|
+
</li>
|
|
14
|
+
{{ block.super }}
|
|
15
|
+
{% endblock %}
|
|
16
|
+
|
|
17
|
+
{% block result_list %}
|
|
18
|
+
{{ block.super }}
|
|
19
|
+
{% if usage_total is not None %}
|
|
20
|
+
<p class="help" style="text-align: right; padding: 0.5em 1em; font-weight: bold;">
|
|
21
|
+
{% blocktranslate with total=usage_total %}TOTAL COUNT: {{ total }}{% endblocktranslate %}
|
|
22
|
+
</p>
|
|
23
|
+
{% endif %}
|
|
24
|
+
{% endblock %}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{% extends "admin/base_site.html" %}
|
|
2
|
+
{% load i18n %}
|
|
3
|
+
|
|
4
|
+
{% block breadcrumbs %}
|
|
5
|
+
<div class="breadcrumbs">
|
|
6
|
+
<a href="{% url 'admin:index' %}">{% translate "Home" %}</a>
|
|
7
|
+
› <a href="{% url 'admin:django_api_usage_endpointstat_changelist' %}">{% translate "Endpoint stats" %}</a>
|
|
8
|
+
› {% translate "Clear statistics" %}
|
|
9
|
+
</div>
|
|
10
|
+
{% endblock %}
|
|
11
|
+
|
|
12
|
+
{% block content %}
|
|
13
|
+
<p>
|
|
14
|
+
{% blocktranslate count counter=total %}
|
|
15
|
+
This will delete the only aggregated statistic row. Endpoints and their
|
|
16
|
+
deprecation metadata are kept.
|
|
17
|
+
{% plural %}
|
|
18
|
+
This will delete all {{ counter }} aggregated statistic rows. Endpoints
|
|
19
|
+
and their deprecation metadata are kept.
|
|
20
|
+
{% endblocktranslate %}
|
|
21
|
+
</p>
|
|
22
|
+
<form method="post">
|
|
23
|
+
{% csrf_token %}
|
|
24
|
+
<input type="submit" value="{% translate 'Yes, clear statistics' %}">
|
|
25
|
+
<a href="{% url 'admin:django_api_usage_endpointstat_changelist' %}">{% translate "Cancel" %}</a>
|
|
26
|
+
</form>
|
|
27
|
+
{% endblock %}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: django-api-usage
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
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
|
|
@@ -25,6 +25,9 @@ src/django_api_usage/management/commands/api_usage_flush.py
|
|
|
25
25
|
src/django_api_usage/management/commands/api_usage_report.py
|
|
26
26
|
src/django_api_usage/migrations/0001_initial.py
|
|
27
27
|
src/django_api_usage/migrations/__init__.py
|
|
28
|
+
src/django_api_usage/templates/admin/django_api_usage/endpointstat/change_list.html
|
|
29
|
+
src/django_api_usage/templates/admin/django_api_usage/endpointstat/confirm_clear.html
|
|
30
|
+
tests/test_admin.py
|
|
28
31
|
tests/test_checks.py
|
|
29
32
|
tests/test_commands.py
|
|
30
33
|
tests/test_drf.py
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
from django.contrib.auth.models import User
|
|
2
|
+
from django.urls import reverse
|
|
3
|
+
|
|
4
|
+
from django_api_usage.models import Endpoint, EndpointStat
|
|
5
|
+
|
|
6
|
+
from .base import UsageTestCase
|
|
7
|
+
|
|
8
|
+
CHANGELIST = "admin:django_api_usage_endpointstat_changelist"
|
|
9
|
+
FLUSH_URL = "admin:django_api_usage_endpointstat_flush"
|
|
10
|
+
CLEAR_URL = "admin:django_api_usage_endpointstat_clear"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AdminButtonsTest(UsageTestCase):
|
|
14
|
+
def setUp(self):
|
|
15
|
+
super().setUp()
|
|
16
|
+
self.admin = User.objects.create_superuser(
|
|
17
|
+
"admin", "admin@example.com", "password"
|
|
18
|
+
)
|
|
19
|
+
self.client.force_login(self.admin)
|
|
20
|
+
|
|
21
|
+
def _make_stat(self, count=1):
|
|
22
|
+
endpoint = Endpoint.objects.create(
|
|
23
|
+
app_label="api", route_name="artikuluak-list", method="GET"
|
|
24
|
+
)
|
|
25
|
+
return EndpointStat.objects.create(
|
|
26
|
+
endpoint=endpoint, date="2026-10-08", count=count
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
def test_buttons_are_rendered(self):
|
|
30
|
+
response = self.client.get(reverse(CHANGELIST))
|
|
31
|
+
|
|
32
|
+
self.assertEqual(response.status_code, 200)
|
|
33
|
+
content = response.content.decode()
|
|
34
|
+
self.assertIn(reverse(FLUSH_URL), content)
|
|
35
|
+
self.assertIn(reverse(CLEAR_URL), content)
|
|
36
|
+
|
|
37
|
+
def test_flush_reports_when_nothing_is_buffered(self):
|
|
38
|
+
response = self.client.post(reverse(FLUSH_URL), follow=True)
|
|
39
|
+
|
|
40
|
+
self.assertEqual(response.status_code, 200)
|
|
41
|
+
self.assertContains(response, "already in the database")
|
|
42
|
+
|
|
43
|
+
def test_flush_moves_buffered_counters_to_the_database(self):
|
|
44
|
+
# A request fills the cache buffer.
|
|
45
|
+
self.client.get("/ping/")
|
|
46
|
+
self.assertFalse(EndpointStat.objects.exists())
|
|
47
|
+
|
|
48
|
+
response = self.client.post(reverse(FLUSH_URL), follow=True)
|
|
49
|
+
|
|
50
|
+
self.assertEqual(response.status_code, 200)
|
|
51
|
+
self.assertTrue(
|
|
52
|
+
EndpointStat.objects.filter(endpoint__route_name="ping").exists()
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
def test_flush_rejects_get(self):
|
|
56
|
+
response = self.client.get(reverse(FLUSH_URL))
|
|
57
|
+
|
|
58
|
+
self.assertRedirects(response, reverse(CHANGELIST))
|
|
59
|
+
|
|
60
|
+
def test_clear_asks_for_confirmation_first(self):
|
|
61
|
+
self._make_stat()
|
|
62
|
+
|
|
63
|
+
response = self.client.get(reverse(CLEAR_URL))
|
|
64
|
+
|
|
65
|
+
self.assertEqual(response.status_code, 200)
|
|
66
|
+
self.assertTrue(EndpointStat.objects.exists())
|
|
67
|
+
|
|
68
|
+
def test_clear_deletes_statistics_but_keeps_endpoints(self):
|
|
69
|
+
self._make_stat()
|
|
70
|
+
|
|
71
|
+
response = self.client.post(reverse(CLEAR_URL), follow=True)
|
|
72
|
+
|
|
73
|
+
self.assertEqual(response.status_code, 200)
|
|
74
|
+
self.assertFalse(EndpointStat.objects.exists())
|
|
75
|
+
self.assertTrue(Endpoint.objects.exists())
|
|
76
|
+
|
|
77
|
+
def test_total_sums_the_count_column(self):
|
|
78
|
+
api = Endpoint.objects.create(app_label="api", route_name="a", method="GET")
|
|
79
|
+
gida = Endpoint.objects.create(app_label="gida", route_name="b", method="GET")
|
|
80
|
+
EndpointStat.objects.create(
|
|
81
|
+
endpoint=api, date="2026-10-08", client_type="anon", count=5
|
|
82
|
+
)
|
|
83
|
+
EndpointStat.objects.create(
|
|
84
|
+
endpoint=gida, date="2026-10-08", client_type="user", count=7
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
response = self.client.get(reverse(CHANGELIST))
|
|
88
|
+
|
|
89
|
+
self.assertEqual(response.context_data["usage_total"], 12)
|
|
90
|
+
self.assertContains(response, "TOTAL COUNT")
|
|
91
|
+
|
|
92
|
+
def test_total_respects_the_selected_filters(self):
|
|
93
|
+
api = Endpoint.objects.create(app_label="api", route_name="a", method="GET")
|
|
94
|
+
gida = Endpoint.objects.create(app_label="gida", route_name="b", method="GET")
|
|
95
|
+
EndpointStat.objects.create(
|
|
96
|
+
endpoint=api, date="2026-10-08", client_type="anon", count=5
|
|
97
|
+
)
|
|
98
|
+
EndpointStat.objects.create(
|
|
99
|
+
endpoint=gida, date="2026-10-08", client_type="user", count=7
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
response = self.client.get(reverse(CHANGELIST), {"client_type": "user"})
|
|
103
|
+
|
|
104
|
+
self.assertEqual(response.context_data["usage_total"], 7)
|
|
105
|
+
|
|
106
|
+
def test_search_box_is_rendered(self):
|
|
107
|
+
response = self.client.get(reverse(CHANGELIST))
|
|
108
|
+
|
|
109
|
+
self.assertContains(response, 'id="searchbar"')
|
|
110
|
+
|
|
111
|
+
def test_search_filters_rows_by_endpoint_text(self):
|
|
112
|
+
api = Endpoint.objects.create(
|
|
113
|
+
app_label="api", route_name="artikuluak-list", method="GET"
|
|
114
|
+
)
|
|
115
|
+
gida = Endpoint.objects.create(
|
|
116
|
+
app_label="gida", route_name="gida-sailak-list", method="GET"
|
|
117
|
+
)
|
|
118
|
+
EndpointStat.objects.create(
|
|
119
|
+
endpoint=api, date="2026-10-08", client_type="anon", count=5
|
|
120
|
+
)
|
|
121
|
+
EndpointStat.objects.create(
|
|
122
|
+
endpoint=gida, date="2026-10-08", client_type="anon", count=7
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
response = self.client.get(reverse(CHANGELIST), {"q": "artikuluak"})
|
|
126
|
+
|
|
127
|
+
self.assertEqual(response.context_data["usage_total"], 5)
|
|
128
|
+
self.assertEqual(len(response.context_data["cl"].result_list), 1)
|
|
129
|
+
|
|
130
|
+
def test_search_matches_the_app_label(self):
|
|
131
|
+
api = Endpoint.objects.create(
|
|
132
|
+
app_label="api", route_name="artikuluak-list", method="GET"
|
|
133
|
+
)
|
|
134
|
+
gida = Endpoint.objects.create(
|
|
135
|
+
app_label="gida", route_name="gida-sailak-list", method="GET"
|
|
136
|
+
)
|
|
137
|
+
EndpointStat.objects.create(
|
|
138
|
+
endpoint=api, date="2026-10-08", client_type="anon", count=5
|
|
139
|
+
)
|
|
140
|
+
EndpointStat.objects.create(
|
|
141
|
+
endpoint=gida, date="2026-10-08", client_type="anon", count=7
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
response = self.client.get(reverse(CHANGELIST), {"q": "gida"})
|
|
145
|
+
|
|
146
|
+
self.assertEqual(response.context_data["usage_total"], 7)
|
|
147
|
+
|
|
148
|
+
def test_search_combines_with_list_filters(self):
|
|
149
|
+
api = Endpoint.objects.create(
|
|
150
|
+
app_label="api", route_name="artikuluak-list", method="GET"
|
|
151
|
+
)
|
|
152
|
+
EndpointStat.objects.create(
|
|
153
|
+
endpoint=api, date="2026-10-08", client_type="anon", count=5
|
|
154
|
+
)
|
|
155
|
+
EndpointStat.objects.create(
|
|
156
|
+
endpoint=api, date="2026-10-08", client_type="user", count=3
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
response = self.client.get(
|
|
160
|
+
reverse(CHANGELIST), {"q": "artikuluak", "client_type": "user"}
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
self.assertEqual(response.context_data["usage_total"], 3)
|
|
164
|
+
|
|
165
|
+
def test_clear_is_not_available_without_admin_permission(self):
|
|
166
|
+
self._make_stat()
|
|
167
|
+
self.client.force_login(User.objects.create_user("plain", password="x"))
|
|
168
|
+
|
|
169
|
+
response = self.client.post(reverse(CLEAR_URL))
|
|
170
|
+
|
|
171
|
+
self.assertIn(response.status_code, (302, 403))
|
|
172
|
+
self.assertTrue(EndpointStat.objects.exists())
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
"""Read-only admin for usage counters, plus editable deprecation metadata."""
|
|
2
|
-
|
|
3
|
-
from django.contrib import admin
|
|
4
|
-
|
|
5
|
-
from .models import Consumer, Endpoint, EndpointStat
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
@admin.register(Endpoint)
|
|
9
|
-
class EndpointAdmin(admin.ModelAdmin):
|
|
10
|
-
list_display = (
|
|
11
|
-
"app_label",
|
|
12
|
-
"route_name",
|
|
13
|
-
"method",
|
|
14
|
-
"site_id",
|
|
15
|
-
"deprecated",
|
|
16
|
-
"sunset_date",
|
|
17
|
-
"replacement",
|
|
18
|
-
"owner",
|
|
19
|
-
)
|
|
20
|
-
list_filter = ("deprecated", "app_label", "method", "site_id")
|
|
21
|
-
search_fields = ("route_name", "replacement", "owner", "notes")
|
|
22
|
-
list_editable = ("deprecated", "sunset_date", "replacement", "owner")
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
@admin.register(EndpointStat)
|
|
26
|
-
class EndpointStatAdmin(admin.ModelAdmin):
|
|
27
|
-
list_display = ("date", "endpoint", "client_type", "status_class", "count")
|
|
28
|
-
list_filter = ("date", "client_type", "status_class")
|
|
29
|
-
list_select_related = ("endpoint",)
|
|
30
|
-
date_hierarchy = "date"
|
|
31
|
-
|
|
32
|
-
def has_add_permission(self, request):
|
|
33
|
-
return False
|
|
34
|
-
|
|
35
|
-
def has_change_permission(self, request, obj=None):
|
|
36
|
-
return False
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
@admin.register(Consumer)
|
|
40
|
-
class ConsumerAdmin(admin.ModelAdmin):
|
|
41
|
-
list_display = (
|
|
42
|
-
"kind",
|
|
43
|
-
"ref_short",
|
|
44
|
-
"user_agent_family",
|
|
45
|
-
"request_count",
|
|
46
|
-
"first_seen",
|
|
47
|
-
"last_seen",
|
|
48
|
-
"contact",
|
|
49
|
-
)
|
|
50
|
-
list_filter = ("kind",)
|
|
51
|
-
search_fields = ("contact", "notes")
|
|
52
|
-
list_editable = ("contact",)
|
|
53
|
-
|
|
54
|
-
@admin.display(description="Reference")
|
|
55
|
-
def ref_short(self, obj):
|
|
56
|
-
return obj.ref_hash[:12]
|
|
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
|
|
File without changes
|
{django_api_usage-0.1.0 → django_api_usage-0.1.2}/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.0 → django_api_usage-0.1.2}/src/django_api_usage/migrations/0001_initial.py
RENAMED
|
File without changes
|
{django_api_usage-0.1.0 → django_api_usage-0.1.2}/src/django_api_usage/migrations/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_api_usage-0.1.0 → django_api_usage-0.1.2}/src/django_api_usage.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{django_api_usage-0.1.0 → django_api_usage-0.1.2}/src/django_api_usage.egg-info/requires.txt
RENAMED
|
File without changes
|
{django_api_usage-0.1.0 → django_api_usage-0.1.2}/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
|