Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
11 changes: 10 additions & 1 deletion openwisp_radius/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ def get_api_urls(api_views=None):
if not api_views:
api_views = views
if app_settings.RADIUS_API:
return [
api_urls = [
path("freeradius/authorize/", api_views.authorize, name="authorize"),
path("freeradius/postauth/", api_views.postauth, name="postauth"),
path("freeradius/accounting/", api_views.accounting, name="accounting"),
Expand Down Expand Up @@ -89,5 +89,14 @@ def get_api_urls(api_views=None):
name="radius_accounting_list",
),
]
if api_views.monitoring_accounting is not None:
api_urls.append(
path(
"radius/monitoring/sessions/",
api_views.monitoring_accounting,
name="monitoring_accounting_list",
),
)
return api_urls
else:
return []
7 changes: 7 additions & 0 deletions openwisp_radius/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -847,3 +847,10 @@ class RadiusAccountingView(ProtectedAPIMixin, FilterByOrganizationManaged, ListA


radius_accounting = RadiusAccountingView.as_view()

try:
from ..integrations.monitoring.views import MonitoringAccountingView

monitoring_accounting = MonitoringAccountingView.as_view()
except ImportError:
monitoring_accounting = None
2 changes: 1 addition & 1 deletion openwisp_radius/integrations/monitoring/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def get_extra_context(self, pk=None):
ctx.update(
{
"radius_accounting_api_endpoint": reverse(
"radius:radius_accounting_list"
"radius:monitoring_accounting_list"
),
"radius_accounting": reverse(
f"admin:{RadiusAccounting._meta.app_label}"
Expand Down
50 changes: 50 additions & 0 deletions openwisp_radius/integrations/monitoring/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
from django.utils import formats, timezone
from rest_framework import serializers
from swapper import load_model

RadiusAccounting = load_model("openwisp_radius", "RadiusAccounting")


class MonitoringRadiusAccountingSerializer(serializers.ModelSerializer):
"""
Read-only serializer for RADIUS accounting in monitoring integration
that formats datetime fields server-side using Django's localization
for consistency with Django admin datetime formatting.
"""

start_time = serializers.SerializerMethodField()
stop_time = serializers.SerializerMethodField()

def _format_datetime(self, dt):
"""
Format a datetime using Django's localization settings.
Handles both naive and timezone-aware datetimes.
"""
if dt is None:
return None
if timezone.is_aware(dt):
dt = timezone.localtime(dt)
return formats.date_format(dt, "DATETIME_FORMAT")

def get_start_time(self, obj):
"""Format start_time using Django's localization settings"""
return self._format_datetime(obj.start_time)

def get_stop_time(self, obj):
"""Format stop_time using Django's localization settings"""
return self._format_datetime(obj.stop_time)

class Meta:
model = RadiusAccounting
fields = [
"session_id",
"unique_id",
"username",
"input_octets",
"output_octets",
"calling_station_id",
"called_station_id",
"start_time",
"stop_time",
]
read_only_fields = fields
Comment on lines +1 to +50
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new MonitoringRadiusAccountingSerializer lacks test coverage. The repository has comprehensive API test coverage, and serializers are typically tested alongside their views. Consider adding tests that verify:

  1. The start_time and stop_time fields are correctly formatted using Django's DATETIME_FORMAT
  2. The formatting works correctly with different timezone settings
  3. None values are handled correctly for start_time and stop_time
  4. The serialized output contains all expected fields

Copilot uses AI. Check for mistakes.
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,6 @@
const deviceMac = encodeURIComponent($("#id_mac_address").val()),
apiEndpoint = `${radiusAccountingApiEndpoint}?called_station_id=${deviceMac}`;

function getFormattedDateTimeString(dateTimeString) {
// Strip the timezone from the dateTimeString.
// This is done to show the time in server's timezone
// because RadiusAccounting admin also shows the time in server's timezone.
let strippedDateTime = new Date(dateTimeString.replace(/[-+]\d{2}:\d{2}$/, ""));
return strippedDateTime.toLocaleString();
}

function fetchRadiusSessions() {
if ($("#radius-session-tbody").children().length) {
// Don't fetch if RADIUS sessions are already present
Expand Down Expand Up @@ -58,11 +50,8 @@
);

response.forEach((element, index) => {
element.start_time = getFormattedDateTimeString(element.start_time);
if (!element.stop_time) {
element.stop_time = `<strong>${onlineMsg}</strong>`;
} else {
element.stop_time = getFormattedDateTimeString(element.stop_time);
}
$("#radius-session-tbody").append(
`<tr class="form-row has_original dynamic-radiussession_set" id="radiussession_set-${index}">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ <h2>{% trans "RADIUS Sessions" %}</h2>
<script>
const radiusAccountingApiEndpoint = "{{ radius_accounting_api_endpoint }}";
const radiusAccountingAdminPath = "{{ radius_accounting }}";
const djangoLocale = "{{ LANGUAGE_CODE }}";
</script>
{% endif %}
{% endblock %}
29 changes: 29 additions & 0 deletions openwisp_radius/integrations/monitoring/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.generics import ListAPIView
from swapper import load_model

from openwisp_radius.api.freeradius_views import (
AccountingFilter,
AccountingViewPagination,
)
from openwisp_users.api.mixins import FilterByOrganizationManaged, ProtectedAPIMixin

from .serializers import MonitoringRadiusAccountingSerializer

RadiusAccounting = load_model("openwisp_radius", "RadiusAccounting")


class MonitoringAccountingView(
ProtectedAPIMixin, FilterByOrganizationManaged, ListAPIView
):
"""
API view for RADIUS accounting in monitoring integration.
Uses server-side datetime formatting for consistency with Django admin.
"""

throttle_scope = "radius_accounting_list"
serializer_class = MonitoringRadiusAccountingSerializer
pagination_class = AccountingViewPagination
filter_backends = (DjangoFilterBackend,)
filterset_class = AccountingFilter
queryset = RadiusAccounting.objects.all().order_by("-start_time")
Comment on lines +1 to +29
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new MonitoringAccountingView lacks test coverage. The repository has comprehensive API test coverage (see openwisp_radius/tests/test_api/ directory), and similar views like RadiusAccountingView have dedicated tests (test_radius_accounting in test_api.py). Consider adding tests that verify:

  1. The datetime formatting is working correctly with Django's localization
  2. The view properly filters by organization
  3. The view handles authentication/authorization correctly
  4. The formatted datetime strings match the expected Django admin format

Copilot uses AI. Check for mistakes.
8 changes: 8 additions & 0 deletions tests/openwisp2/sample_radius/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@
from openwisp_radius.api.views import (
ValidatePhoneTokenView as BaseValidatePhoneTokenView,
)
from openwisp_radius.integrations.monitoring.views import (
MonitoringAccountingView as BaseMonitoringAccountingView,
)


class AuthorizeView(BaseAuthorizeView):
Expand Down Expand Up @@ -98,6 +101,10 @@ class RadiusAccountingView(BaseRadiusAccountingView):
pass


class MonitoringAccountingView(BaseMonitoringAccountingView):
pass


authorize = AuthorizeView.as_view()
postauth = PostAuthView.as_view()
accounting = AccountingView.as_view()
Expand All @@ -116,3 +123,4 @@ class RadiusAccountingView(BaseRadiusAccountingView):
change_phone_number = ChangePhoneNumberView.as_view()
download_rad_batch_pdf = DownloadRadiusBatchPdfView.as_view()
radius_accounting = RadiusAccountingView.as_view()
monitoring_accounting = MonitoringAccountingView.as_view()
Loading