-
-
Notifications
You must be signed in to change notification settings - Fork 218
[feature] Add support to reversion to the REST API #994
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
8690016
to
3fdfcaf
Compare
3fdfcaf
to
98ca619
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good work! The code is clean and well-structured.
However, this PR seems to be missing its main goal: ensuring that every versioned model admin has a corresponding versioned REST API that logs revisions—tracking who changed what—each time a modification is made.
Please check my comments below for further details.
84a68d6
to
df5c3fa
Compare
I’ve added REST API revision support using django-reversion. Here’s a summary What’s Added
New Endpoints:
How It Works:
Issues Noticed
QuestionGiven the unexpected behavior in signals and added complexity: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks on the right track.
I will do a manual round of testing soon, in the meantime see my comments below.
We'll need to add this to pki
, geo
and connection
.
'controller/<str:model_slug>/revision/<str:pk>/restore/', | ||
api_views.revision_restore, | ||
name='revision_restore', | ||
), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What if we shorten this to just model
? I think it would be clear enough and more concise. It's a small thing but nonetheless over time these simplifications help to keep maintenance sane.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have another doubt on this, how are we enforcing that we are enabling this only on the models that support reversions in this app? Eg: Device, Template, Vpn, etc. Would this work for any model?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Currently, empty revisions are being stored with only user and comment entries, which does not offer anything.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updates
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you point to the lines of code that are generating the additional queries?
Have you found out what additional queries are being generated that before weren't?
If we can see the query it would be faster to verify whether all the queries are needed, the increase seems pretty significant.
34dbfcf
to
47e1c70
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you point to the lines of code that are generating the additional queries?
Have you found out what additional queries are being generated that before weren't?
Here's a diff of the queries generated by the test_patch_deviceconnectoin_detail
,
https://www.diffchecker.com/ToUeQeyZ/
From my analysis, it appears that django-rivision creates a version for all related objects (here - DeviceConnection, Config and Device). This is inline with VersionAdmin behaviour.
Updates
|
Updates
With this approach, only registered models will store revisions, while unregistered models can still include the mixin without any side effects. This allows in the future to safely add the mixin to new POST, PUT, or PATCH requests without needing to worry about whether the model is registered with reversion. |
36b017e
to
92b7954
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@dee077 can you please run openwisp-qa-format
here again with the latest version of openwisp-utils? See openwisp/openwisp-utils#456.
Implemented three endpoints: 1. List all revisions with optional filtering by model. 2. Inspect a specific revision by its ID. 3. Restore a revision using its ID. Fixes openwisp#894
…d serializer for more clarity
92b7954
to
fd1faab
Compare
… to geo_api:device_location
fd1faab
to
3afb759
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did a round of manual testing with the Templates
model. Changing the model fields from the API endpoints creates revisions as expected. Kudos!
Is it possible to list all revisions of a particular object? Similar to this view
https://demo.openwisp.io/admin/config/template/e450012e-a482-48ad-9b2b-f0faa93b9cac/history/
If not, it really hurts the functionality of the REST API endpoints.
This end point is missing from the issue description. Let's discuss it with @nemesifier first before proceeding on this front.
def get_queryset(self): | ||
model = self.kwargs.get("model").lower() | ||
queryset = self.queryset.filter(content_type__model=model) | ||
revision_id = self.request.query_params.get("revision_id") | ||
if revision_id: | ||
queryset = queryset.filter(revision_id=revision_id) | ||
return self.queryset.filter(content_type__model=model) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The way this method is written breaks the convention of Django/DRF. It is also inconsistent with OpenWISP.
def get_queryset(self): | |
model = self.kwargs.get("model").lower() | |
queryset = self.queryset.filter(content_type__model=model) | |
revision_id = self.request.query_params.get("revision_id") | |
if revision_id: | |
queryset = queryset.filter(revision_id=revision_id) | |
return self.queryset.filter(content_type__model=model) | |
def get_queryset(self): | |
queryset = super().get_queryset() | |
model = self.kwargs.get("model", "").lower() | |
queryset = queryset.filter(content_type__model=model) | |
if revision_id := self.request.query_params.get("revision_id"): | |
queryset = queryset.filter(revision_id=revision_id) | |
return queryset |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right now, it is not filtering the queryset with the revision_id
, since the return
statement filters the queryset with just content_type__model
.
And, there's no test which verifies the filtering. This is bad.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Filtering using revision_id
should be done with django_filters instead.
https://django-filter.readthedocs.io/en/stable/guide/rest_framework.html#quickstart
You can check out other API views for details.
def get_queryset(self): | ||
model = self.kwargs.get("model").lower() | ||
return self.queryset.filter(content_type__model=model) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
def get_queryset(self): | |
model = self.kwargs.get("model").lower() | |
return self.queryset.filter(content_type__model=model) | |
def get_queryset(self): | |
model = self.kwargs.get("model").lower() | |
return super().get_querset().filter(content_type__model=model) |
Maybe, we can create a Mixin class which filters the queryset like this.
def get_queryset(self): | ||
model = self.kwargs.get("model").lower() | ||
return self.queryset.filter(content_type__model=model) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here!
with transaction.atomic(): | ||
with reversion.create_revision(): | ||
for version in versions: | ||
version.revert() | ||
reversion.set_user(request.user) | ||
reversion.set_comment( | ||
f"Restored to previous revision: {self.kwargs.get('pk')}" | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why don't we use Revision.revert() instead of reverting individual versions?
Checklist
Reference to Existing Issue
Closes #894.
Description of Changes
Introduces three new REST API endpoints for managing revisions:
List all revisions with optional filtering by model:
GET /controller/reversion/?model=<model_name>
Inspect a specific revision using its ID:
GET /controller/reversion/<revision_id>/
Restore a revision based on its ID:
POST /controller/reversion/<revision_id>/restore/
Let me know if any changes are needed in the response data whether anything should be included, excluded, or modified. Also, please share any suggestions for adding more filters or adjusting the response format for the restore endpoint