Skip to content
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

Integrate DJ2.1 "view" permissions for both model and historical model #529

Closed
wants to merge 55 commits into from
Closed

Integrate DJ2.1 "view" permissions for both model and historical model #529

wants to merge 55 commits into from

Conversation

erikvw
Copy link
Contributor

@erikvw erikvw commented Feb 16, 2019

In some scenarios, it is not ideal to allow users to revert a model instance to a previous version.
Django 2.1 introduced view permissions which, if integrated into DSH, would give an administrator the ability to allow a user to add/change a model but not revert to a previous instance through the model's history.

Description

The SimpleHistoryAdmin reviews the historical model instance permissions instead of just the model instance by overriding the has_xxxx_permission methods in ModelAdmin.

  • enable this feature through settings (SIMPLE_HISTORY_PERMISSIONS_ENABLED);

  • honor superuser privileges;

  • user/group permissions granted for the historical model cannot exceed those of the model. That is, granting view and change permissions for the historical model but only view permission for the model means the user has view permission only for the historical model;

  • text on the changelist and form becomes context sensitive (change permission vs view permission);

  • setting SIMPLE_HISTORY_REVERT_DISABLED=True limits all users (other than superuser) to view access. (this works for all Django versions)

  • the [Revert] button is changed to a [Close] button if the user has:
    -- if Django >= 2.1, feature is disabled, and has view on model;
    -- if Django >= 2.1, feature is enable and has view model permission and view historical model permission;
    -- if settings. SIMPLE_HISTORY_REVERT_DISABLED = True (regardless of Django version).

How Has This Been Tested?

See tests in test_admin_with_permissions.py

A few tests in test_admin.py required very minor modifications.

Screenshots (if appropriate):

For a user with "view" only permission on a historical object, the changelist looks like this:
20_permissions

and the form looks like this:
30_permissions

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist:

  • I have run the make format command to format my code
  • My change requires a change to the documentation.
  • I have updated the documentation accordingly.
  • I have read the CONTRIBUTING document.
  • I have added tests to cover my changes.
  • I have added my name and/or github handle to AUTHORS.rst
  • I have added my change to CHANGES.rst
  • All new and existing tests passed.

@codecov-io
Copy link

codecov-io commented Feb 18, 2019

Codecov Report

Merging #529 into master will decrease coverage by 0.24%.
The diff coverage is 94.82%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master     #529      +/-   ##
==========================================
- Coverage    97.8%   97.55%   -0.25%     
==========================================
  Files          17       17              
  Lines         866      942      +76     
  Branches      122      131       +9     
==========================================
+ Hits          847      919      +72     
- Misses          8       12       +4     
  Partials       11       11
Impacted Files Coverage Δ
simple_history/admin.py 96.7% <94.82%> (-1.41%) ⬇️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 80469cc...74d1b7f. Read the comment docs.

@dwasyl
Copy link

dwasyl commented Jun 15, 2019

Since this wasn't fully merged in yet, I jus wanted to add in 2 cents and say that rather than just hiding the Revert button the 'read only' view of the historical record might want to follow the pattern that Django has for view permission which is to render all of the fields as if there are in the readonly_fields list. That way the widgets aren't rendered which could leave some users confused UI wise.

Edit: aren't instead of are.

@rossmechanic
Copy link
Collaborator

@erikvw I like the idea suggested by @dwasyl . Thoughts?

author erikvw <ew2789@gmail.com> 1550266705 -0600
committer erikvw <ew2789@gmail.com> 1560873977 -0500

add Close button

conditionally show text

move titles and show close to methods

just use has_change_permission

use user permissions instead of just overriding method

add additional tests
SIMPLE_HISTORY_REVERT_ENABLED
refactor history_view

reduce complexity ...

add dj2.1 note

further refactor history_view, allow superuser to override all perms and settings, update tests

fix py2 incompatibles, fix revert logic with view/change

revert_disabled must return false if superuser

format

minor

format

improve coverage, update docs

improve coverage, update docs

add test

update test

update CHANGES

add doscstring

update docs

separate tests, refactor

format

revert runtests

format

change title to inline if statement

minor edit to docs

format, pep8
@erikvw
Copy link
Contributor Author

erikvw commented Jun 18, 2019

@rossmechanic and @dwasyl ... to want to follow the Django pattern of adding the fields to the "readonly_fields" is a good instinct. The difference, however, is that the options are no longer rendered (e.g. for a radio field, etc). Although a matter of opinion/preference, my experience has been that an auditor prefers not only to see the selected response but also to see what the available options were. So I would prefer to leave it at is. What do you think?

@dwasyl
Copy link

dwasyl commented Jun 18, 2019

@erikvw @rossmechanic For what it's worth, I understand that and actually find it to be a problem with history records (since many of mine link to other models via ForeignKey or ManyToMany relationships which are never quite captured. The choices that you'll see in a historical record are coloured by the currently available relations or choices. Since it's not truly an accurate historical representation my thought is that displaying it as a widget makes it more confusing since an end-user could think those were the options at the time the record was made (which may or may not be true).

Flattening them to the readonly_fields view that Django is using for the View permission avoids all of that by saying here's what the historical record is and doesn't leave room for misinterpretation. But that's just my 2 cents.

@erikvw
Copy link
Contributor Author

erikvw commented Jun 18, 2019

@rossmechanic @dwasyl You make a good case. Would you like me to explore this change?

@rossmechanic
Copy link
Collaborator

@erikvw I agree with @dwasyl . If it's an easy change, feel free to add it to this, but I'm also happy to get this merged and let @dwasyl put up a follow-on PR to make the fields read only. Up to you

@erikvw
Copy link
Contributor Author

erikvw commented Jun 19, 2019

Would be nice to merge and treat @dwasyl 's suggestion as a separate PR.

return self.fetch_history_list_display_callables(queryset)

@property
def history_manager(self):
Copy link
Collaborator

Choose a reason for hiding this comment

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

We have a util that already does this. but fine with this for now so we can get this in

<a href="../">{% trans "History" %}</a> &rsaquo;
{% blocktrans with original_opts.verbose_name as verbose_name %}Revert {{verbose_name}}{% endblocktrans %}
{% if has_revert_permission %}
{% blocktrans with original_opts.verbose_name as verbose_name %}Revert {{ verbose_name }}{% endblocktrans %}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Indentation looks off here.

Copy link
Collaborator

@rossmechanic rossmechanic left a comment

Choose a reason for hiding this comment

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

Hey @erikvw ! Finally got around to reviewing this. A few questions around how we structured perms. Also want to avoid some of the refactoring work that was done, so we can get this feature in and possibly look at refactoring work in a different PR. Thanks!

historical_permission = request.user.has_perm(
"%s.%s" % (historical_opts.app_label, historical_codename)
)
return permission and historical_permission
Copy link
Collaborator

Choose a reason for hiding this comment

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

so in this case, for view, the user has to succeed on the base model's has_view_permission function as well as have the app_label.view_historicalmodelname permission?

else:
try:
view_permission = self.has_view_permission(request, obj)
except AttributeError:
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is this just checking that we're on version >= 2.1? Would rather have an explicit check of the django version

else:
try:
has_permission = self.has_view_or_change_permission(request, obj)
except AttributeError:
Copy link
Collaborator

Choose a reason for hiding this comment

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

Same as above. Would prefer to have an explicit check for django version. Makes it a lot easier to remove as we deprecate certain versions down the line

historical_opts = self.history_manager.model._meta
historical_codename = get_permission_codename(action, historical_opts)
try:
func = getattr(self, "has_%s_permission" % action)
Copy link
Collaborator

Choose a reason for hiding this comment

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

not sure that I necessarily want to check for this permission. For example, in the case of reverting:
Say you have Model and HistoricalModel,
if I need to have has_change_permission as well as change_historicalmodel permission, then what's the point of this change_historicalmodel permission? I think the only reason that permission would make sense would be if we wanted users who could revert objects, but not change them from scratch. In that case, you would want a user to be able to revert using only the change_historicalmodel permission. I think you shouldn't need both, you should only need the historical permission

object_history_template = "simple_history/object_history.html"
object_history_form_template = "simple_history/object_history_form.html"

def get_urls(self):
"""Returns the additional urls used by the Reversion admin."""
"""Returns the additional urls used by the Reversion admin.
Copy link
Collaborator

Choose a reason for hiding this comment

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

why these changes?

return obj

def get_object_history(self, object_id):
"""Returns a queryset of historical instances.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Let's reformat these docstrings so if they can fit on one line then they're on one line

@@ -42,53 +121,39 @@ def get_urls(self):
return history_urls + urls

def history_view(self, request, object_id, extra_context=None):
"""The 'history' admin view for this model."""
"""Overridden `history_view`.
Copy link
Collaborator

Choose a reason for hiding this comment

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

I appreciate this refactor, but maybe let's do this in a separate PR? Hard to review in the context of the larger PR

@property
def change_history(self):
try:
change_history = settings.SIMPLE_HISTORY_EDIT
Copy link
Collaborator

Choose a reason for hiding this comment

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

can we change these try except AttributeErrors to getattr(settings, "SIMPLE_HISTORY_EDIT")

@@ -207,6 +344,13 @@ def render_history_view(self, request, template, context, **kwargs):
return render(request, template, context, **kwargs)

def save_model(self, request, obj, form, change):
"""Set special model attribute to user for reference after save"""
"""Set special model attribute to user for reference after save.
Copy link
Collaborator

Choose a reason for hiding this comment

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

same as above on docstrings

@rossmechanic
Copy link
Collaborator

Hey @erikvw any update on this?

@rossmechanic
Copy link
Collaborator

@erikvw lmk if you want me to take this over

@erikvw
Copy link
Contributor Author

erikvw commented Aug 5, 2019 via email

@rossmechanic
Copy link
Collaborator

Sounds good @erikvw ! Just lmk

@jeking3
Copy link
Contributor

jeking3 commented Sep 17, 2021

This needs to be rebased.

This pull request was closed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
accepted Issue accepted for completion
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

7 participants