Skip to content

[change:api] Include reset period in UserRadiusUsageView response #669#670

Open
PRoIISHAAN wants to merge 5 commits intoopenwisp:masterfrom
PRoIISHAAN:api-change
Open

[change:api] Include reset period in UserRadiusUsageView response #669#670
PRoIISHAAN wants to merge 5 commits intoopenwisp:masterfrom
PRoIISHAAN:api-change

Conversation

@PRoIISHAAN
Copy link

@PRoIISHAAN PRoIISHAAN commented Jan 17, 2026

Checklist

  • I have read the OpenWISP Contributing Guidelines.
  • I have manually tested the changes proposed in this pull request.
  • I have written new test cases for new code and/or updated existing tests for changes to existing code.
  • I have updated the documentation.

Reference to Existing Issue

Closes #669 >.

Description of Changes

Modified API response to return reset attribute in API

@coderabbitai
Copy link

coderabbitai bot commented Jan 17, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a reset SerializerMethodField to UserGroupCheckSerializer and includes it in Meta.fields. Implements get_reset(self, obj) which selects the appropriate counter class via app_settings.CHECK_ATTRIBUTE_COUNTERS_MAP, calls the counter's get_reset_timestamps to obtain an end_time, and returns that timestamp. get_reset catches SkipCheck, ValueError, and KeyError and returns None in those cases. Tests were updated to expect the new reset field in serialized outputs.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant View
    participant Serializer
    participant AppSettings as app_settings
    participant CounterModule as Counter

    Client->>View: GET UserRadiusUsage
    View->>Serializer: serialize(checks)
    Serializer->>AppSettings: lookup CHECK_ATTRIBUTE_COUNTERS_MAP[type]
    AppSettings-->>Serializer: counter_class
    Serializer->>CounterModule: counter_class.get_reset_timestamps(obj)
    CounterModule-->>Serializer: {start_time, end_time} or raise/None
    Serializer-->>View: serialized data (includes reset)
    View-->>Client: HTTP 200 with JSON (includes reset)
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title follows the required format with [change:api] prefix and clearly describes the main change: adding reset period to UserRadiusUsageView response, matching the linked issue #669.
Description check ✅ Passed The description covers most template sections but lacks detail. The contributor completed contributing guidelines and manual testing checklist items, but did not update tests or documentation as noted. Description of changes is minimal.
Linked Issues check ✅ Passed The changes successfully implement the objective from issue #669: a reset field was added to UserGroupCheckSerializer with logic to compute reset timestamps using existing reset calculation logic, returning the next counter reset datetime.
Out of Scope Changes check ✅ Passed All changes are scoped to implementing the reset field in UserGroupCheckSerializer and corresponding test updates, directly addressing issue #669 requirements without unrelated modifications.
Bug Fixes ✅ Passed This PR adds a new feature rather than fixing a bug, so the custom check for bug fix requirements does not apply.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Tip

Migrating from UI to YAML configuration.

Use the @coderabbitai configuration command in a PR comment to get a dump of all your UI settings in YAML format. You can then edit this YAML file and upload it to the root of your repository to configure CodeRabbit programmatically.

coderabbitai[bot]
coderabbitai bot previously approved these changes Jan 17, 2026
Copy link
Member

@nemesifier nemesifier left a comment

Choose a reason for hiding this comment

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

Thanks @PRoIISHAAN 👍

We need to add a test for this, see also my comment below.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
openwisp_radius/tests/test_api/test_api.py (1)

1094-1115: Test assertions for reset field only verify presence, not correctness.

The assertions use checks[0]["reset"] as the expected value, which is self-referential and only confirms the field exists. Unlike result which is tested with concrete expected values (e.g., 0, 261), the reset field's actual value is never validated.

Consider at minimum:

  1. Asserting that reset is not None when a counter exists
  2. Verifying the type is a valid datetime (e.g., assertIsInstance or regex match for ISO format)
  3. Or mocking time to get deterministic expected values
💡 Example improvement
from datetime import datetime

# Add type assertion for reset field
self.assertIsNotNone(checks[0]["reset"])
# Optionally verify it's a valid ISO datetime string
datetime.fromisoformat(checks[0]["reset"].replace("Z", "+00:00"))
🧹 Nitpick comments (1)
openwisp_radius/api/serializers.py (1)

330-341: LGTM! Consider extracting shared counter instantiation logic.

The implementation correctly mirrors the existing get_result pattern and handles exceptions appropriately. The static analysis hint (TRY300) about moving the return to an else block is a style preference—since get_result uses the same pattern, consistency is more valuable here.

For a future improvement, you could extract the shared counter instantiation logic (lines 307-313 and 332-337) into a helper method to reduce duplication, but this is optional.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8e1cf73 and 5d5e786.

📒 Files selected for processing (2)
  • openwisp_radius/api/serializers.py
  • openwisp_radius/tests/test_api/test_api.py
🧰 Additional context used
🧬 Code graph analysis (1)
openwisp_radius/api/serializers.py (3)
openwisp_radius/counters/base.py (2)
  • reset (39-40)
  • get_reset_timestamps (81-89)
openwisp_radius/models.py (1)
  • RadiusGroupCheck (49-52)
openwisp_radius/counters/exceptions.py (1)
  • SkipCheck (13-18)
🪛 Ruff (0.14.13)
openwisp_radius/api/serializers.py

339-339: Consider moving this statement to an else block

(TRY300)

🔇 Additional comments (2)
openwisp_radius/tests/test_api/test_api.py (2)

1064-1075: LGTM!

The test correctly verifies that reset is None when the counter attribute doesn't exist in CHECK_ATTRIBUTE_COUNTERS_MAP.


1245-1265: LGTM!

This test appropriately mirrors the test_user_group_check_serializer_counter_does_not_exist test in TestApi for the transaction test case.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

@PRoIISHAAN PRoIISHAAN requested a review from nemesifier January 17, 2026 15:15
@coveralls
Copy link

coveralls commented Jan 17, 2026

Coverage Status

coverage: 97.466% (+0.006%) from 97.46%
when pulling e61b5ba on PRoIISHAAN:api-change
into eef0edf on openwisp:master.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
openwisp_radius/tests/test_api/test_api.py (1)

1307-1457: ⚠️ Potential issue | 🟠 Major

Tautological reset assertions don't validate the feature — tests will always pass regardless of the actual value.

"reset": checks[0]["reset"] asserts that a key equals itself, which is trivially true. For Max-Daily-Session and Max-Daily-Session-Traffic (both daily counters), reset should be a non-None ISO 8601 datetime string representing the next midnight. The test suite would pass even if get_reset returned a completely wrong value, or even if it always returned None.

✅ Proposed fix — assert non-None and validate format

Replace the tautological assertions with explicit value checks. For example, for each subtest:

-                "reset": checks[0]["reset"],
+                "reset": checks[0]["reset"],  # keep for dict equality, but add:

Then add explicit assertions after each assertDictEqual:

+            self.assertIsNotNone(checks[0]["reset"])
+            self.assertIsNotNone(checks[1]["reset"])

Or, more thoroughly, validate the format using a datetime.fromisoformat / dateutil.parser.parse check:

from django.utils.dateparse import parse_datetime
self.assertIsNotNone(parse_datetime(checks[0]["reset"]))
self.assertIsNotNone(parse_datetime(checks[1]["reset"]))

Alternatively, compute the expected next-midnight value and assert it directly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openwisp_radius/tests/test_api/test_api.py` around lines 1307 - 1457, The
tests currently assert `"reset": checks[0]["reset"]` which is tautological and
doesn't validate reset values; update the subtests that check `checks[0]` and
`checks[1]` (the entries for "Max-Daily-Session" and
"Max-Daily-Session-Traffic") to instead assert that `checks[i]["reset"]` is not
None and is a valid ISO8601 datetime (e.g., use Django's `parse_datetime` /
`datetime.fromisoformat` to parse and assert parsing succeeded), or compute the
expected next-midnight datetime and assert equality—replace the self-referential
`"reset": checks[i]["reset"]` entries with these explicit assertions.
openwisp_radius/api/serializers.py (1)

309-344: 🧹 Nitpick | 🔵 Trivial

Extract a shared _get_counter(obj) helper to avoid double counter instantiation.

Both get_result (Line 312–316) and get_reset (Line 336–340) independently instantiate the same Counter object for the same obj. Each instantiation may trigger database access (e.g., in consumed() and get_reset_timestamps()), so serializing a single check item fires the counter constructor twice.

♻️ Proposed refactor
+    def _get_counter(self, obj):
+        Counter = app_settings.CHECK_ATTRIBUTE_COUNTERS_MAP[obj.attribute]
+        return Counter(
+            user=self.context["user"],
+            group=self.context["group"],
+            group_check=obj,
+        )
+
     def get_result(self, obj):
         try:
-            Counter = app_settings.CHECK_ATTRIBUTE_COUNTERS_MAP[obj.attribute]
-            counter = Counter(
-                user=self.context["user"],
-                group=self.context["group"],
-                group_check=obj,
-            )
+            counter = self._get_counter(obj)
             consumed = counter.consumed()
             value = int(obj.value)
             if consumed > value:
                 consumed = value
             return consumed
         except (SkipCheck, ValueError, KeyError):
             return None

     def get_reset(self, obj):
         try:
-            Counter = app_settings.CHECK_ATTRIBUTE_COUNTERS_MAP[obj.attribute]
-            counter = Counter(
-                user=self.context["user"],
-                group=self.context["group"],
-                group_check=obj,
-            )
+            counter = self._get_counter(obj)
             _, end_time = counter.get_reset_timestamps()
             return end_time
         except (SkipCheck, ValueError, KeyError):
             return None

Note: _get_counter itself should remain free of a try/except, since callers already handle KeyError and SkipCheck.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openwisp_radius/api/serializers.py` around lines 309 - 344, Add a private
helper method _get_counter(self, obj) that looks up the Counter class from
app_settings.CHECK_ATTRIBUTE_COUNTERS_MAP[obj.attribute] and returns
Counter(user=self.context["user"], group=self.context["group"], group_check=obj)
(do not wrap it in try/except). Replace the duplicated Counter instantiation in
get_result(self, obj) and get_reset(self, obj) to call self._get_counter(obj)
and keep the existing exception handling in those methods intact; leave get_type
as-is (it only needs the Counter class lookup).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@openwisp_radius/api/serializers.py`:
- Around line 309-344: Add a private helper method _get_counter(self, obj) that
looks up the Counter class from
app_settings.CHECK_ATTRIBUTE_COUNTERS_MAP[obj.attribute] and returns
Counter(user=self.context["user"], group=self.context["group"], group_check=obj)
(do not wrap it in try/except). Replace the duplicated Counter instantiation in
get_result(self, obj) and get_reset(self, obj) to call self._get_counter(obj)
and keep the existing exception handling in those methods intact; leave get_type
as-is (it only needs the Counter class lookup).

In `@openwisp_radius/tests/test_api/test_api.py`:
- Around line 1307-1457: The tests currently assert `"reset":
checks[0]["reset"]` which is tautological and doesn't validate reset values;
update the subtests that check `checks[0]` and `checks[1]` (the entries for
"Max-Daily-Session" and "Max-Daily-Session-Traffic") to instead assert that
`checks[i]["reset"]` is not None and is a valid ISO8601 datetime (e.g., use
Django's `parse_datetime` / `datetime.fromisoformat` to parse and assert parsing
succeeded), or compute the expected next-midnight datetime and assert
equality—replace the self-referential `"reset": checks[i]["reset"]` entries with
these explicit assertions.

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5d5e786 and f2976bc.

📒 Files selected for processing (2)
  • openwisp_radius/api/serializers.py
  • openwisp_radius/tests/test_api/test_api.py
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: Python==3.12 | django~=4.2.0
  • GitHub Check: Python==3.12 | django~=5.2.0
  • GitHub Check: Python==3.10 | django~=5.2.0
  • GitHub Check: Python==3.13 | django~=5.1.0
  • GitHub Check: Python==3.10 | django~=5.1.0
  • GitHub Check: Python==3.11 | django~=5.1.0
  • GitHub Check: Python==3.10 | django~=4.2.0
  • GitHub Check: Python==3.13 | django~=5.2.0
  • GitHub Check: Python==3.11 | django~=5.2.0
  • GitHub Check: Python==3.12 | django~=5.1.0
  • GitHub Check: Python==3.11 | django~=4.2.0
🧰 Additional context used
🧬 Code graph analysis (1)
openwisp_radius/api/serializers.py (3)
openwisp_radius/counters/base.py (2)
  • reset (39-40)
  • get_reset_timestamps (81-89)
openwisp_radius/models.py (1)
  • RadiusGroupCheck (49-52)
openwisp_radius/counters/exceptions.py (1)
  • SkipCheck (13-18)

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
openwisp_radius/tests/test_api/test_api.py (1)

1577-1598: ⚠️ Potential issue | 🟡 Minor

Tests should validate actual reset timestamp values, not compare to themselves.

The assertions use "reset": checks[0]["reset"] and "reset": checks[1]["reset"], which compares the value to itself. This verifies the field exists but doesn't validate the correctness of the computed reset timestamp.

For meaningful test coverage, consider asserting against expected values. For daily counters, the reset should be the Unix timestamp of the next day's start.

💚 Suggested test improvement
+from datetime import datetime, timedelta
+from django.utils import timezone
+
+def _get_expected_daily_reset():
+    """Returns expected Unix timestamp for next daily reset."""
+    now = timezone.now()
+    tomorrow = datetime(now.year, now.month, now.day) + timedelta(days=1)
+    return int(tomorrow.timestamp())
+
 # In test assertions:
 self.assertDictEqual(
     dict(checks[0]),
     {
         "attribute": "Max-Daily-Session",
         "op": ":=",
         "value": "10800",
         "result": 0,
         "type": "seconds",
-        "reset": checks[0]["reset"],
+        "reset": self._get_expected_daily_reset(),
     },
 )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openwisp_radius/tests/test_api/test_api.py` around lines 1577 - 1598, The
test currently validates "reset" by comparing it to itself (using
checks[0]["reset"] / checks[1]["reset"]) which only verifies presence; replace
those self-referential assertions with computed expected reset timestamps for
the next day's start and assert equality (or a small allowable delta) against
checks[i]["reset"] for the entries with attribute names "Max-Daily-Session" and
"Max-Daily-Session-Traffic"; compute the expected reset as the Unix timestamp
for midnight of the next day in the same timezone/UTC logic used by the
production code and use that value in the dict passed to assertDictEqual (or
assertAlmostEqual/assertInRange if timezone/precision requires tolerance) so the
test verifies the actual reset calculation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@openwisp_radius/api/serializers.py`:
- Around line 333-344: The get_reset method currently returns a raw Unix
timestamp from counter.get_reset_timestamps(); update get_reset (in the
serializer method get_reset) to convert the integer end_time into an ISO 8601
datetime string before returning (e.g., use datetime.fromtimestamp/end_time with
UTC and .isoformat() or append 'Z' for UTC) so API consumers receive a
human-readable ISO datetime; preserve the existing exception handling
(SkipCheck, ValueError, KeyError) and still return None on failure.

---

Outside diff comments:
In `@openwisp_radius/tests/test_api/test_api.py`:
- Around line 1577-1598: The test currently validates "reset" by comparing it to
itself (using checks[0]["reset"] / checks[1]["reset"]) which only verifies
presence; replace those self-referential assertions with computed expected reset
timestamps for the next day's start and assert equality (or a small allowable
delta) against checks[i]["reset"] for the entries with attribute names
"Max-Daily-Session" and "Max-Daily-Session-Traffic"; compute the expected reset
as the Unix timestamp for midnight of the next day in the same timezone/UTC
logic used by the production code and use that value in the dict passed to
assertDictEqual (or assertAlmostEqual/assertInRange if timezone/precision
requires tolerance) so the test verifies the actual reset calculation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 909fdb63-6c61-48ff-ba0e-dc068ad1695e

📥 Commits

Reviewing files that changed from the base of the PR and between f2976bc and e61b5ba.

📒 Files selected for processing (2)
  • openwisp_radius/api/serializers.py
  • openwisp_radius/tests/test_api/test_api.py
📜 Review details
🔇 Additional comments (3)
openwisp_radius/api/serializers.py (1)

300-307: LGTM!

The addition of the reset field to UserGroupCheckSerializer follows the existing patterns established by result and type fields. The implementation correctly:

  • Uses SerializerMethodField consistent with other computed fields
  • Includes the field in Meta.fields
  • Handles exceptions appropriately
openwisp_radius/tests/test_api/test_api.py (2)

1068-1078: LGTM!

The test correctly verifies that when a counter does not exist for the attribute (ChilliSpot-Max-Input-Octets), the reset field is None. This matches the expected behavior from get_reset returning None on KeyError.


1738-1748: LGTM!

The test correctly verifies the reset: None behavior in the transaction test context.

Comment on lines +333 to +344
def get_reset(self, obj):
try:
Counter = app_settings.CHECK_ATTRIBUTE_COUNTERS_MAP[obj.attribute]
counter = Counter(
user=self.context["user"],
group=self.context["group"],
group_check=obj,
)
_, end_time = counter.get_reset_timestamps()
return end_time
except (SkipCheck, ValueError, KeyError):
return None
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider converting Unix timestamp to ISO datetime string for API consistency.

The get_reset method returns a raw integer Unix timestamp from get_reset_timestamps(), while typical REST API datetime fields return ISO 8601 formatted strings. API consumers might expect a human-readable datetime format rather than a Unix timestamp.

If this is intentional for performance or client-side handling reasons, consider documenting the format in the API specification.

♻️ Optional: Convert to ISO datetime string
+from datetime import datetime
+
 def get_reset(self, obj):
     try:
         Counter = app_settings.CHECK_ATTRIBUTE_COUNTERS_MAP[obj.attribute]
         counter = Counter(
             user=self.context["user"],
             group=self.context["group"],
             group_check=obj,
         )
         _, end_time = counter.get_reset_timestamps()
+        if end_time is not None:
+            return datetime.fromtimestamp(end_time, tz=timezone.utc).isoformat()
         return end_time
     except (SkipCheck, ValueError, KeyError):
         return None
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openwisp_radius/api/serializers.py` around lines 333 - 344, The get_reset
method currently returns a raw Unix timestamp from
counter.get_reset_timestamps(); update get_reset (in the serializer method
get_reset) to convert the integer end_time into an ISO 8601 datetime string
before returning (e.g., use datetime.fromtimestamp/end_time with UTC and
.isoformat() or append 'Z' for UTC) so API consumers receive a human-readable
ISO datetime; preserve the existing exception handling (SkipCheck, ValueError,
KeyError) and still return None on failure.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[change:api] Include reset period in UserRadiusUsageView response

3 participants