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

Add history diff column to admin change history table #1128

Merged
merged 13 commits into from
May 6, 2024

Conversation

raunaq-sailo
Copy link

@raunaq-sailo raunaq-sailo commented Feb 21, 2023

Description

Main changes:

  • Added a "Changes" column to SimpleHistoryAdmin's object history table, listing the changes between each historical record of the object; see the docs under "Customizing the History Admin Templates" for overriding its template context (c9963aa)
  • Renamed the (previously internal) admin template simple_history/_object_history_list.html to simple_history/object_history_list.html, and added the field SimpleHistoryAdmin.object_history_list_template for overriding it (16b7de7)
  • Deprecated the undocumented template tag simple_history_admin_list.display_list(); it will be removed in version 3.8 (16b7de7)
  • Added SimpleHistoryAdmin.get_history_queryset() for overriding which QuerySet is used to list the historical records (18f00bd)
  • Added SimpleHistoryAdmin.get_history_list_display() which returns history_list_display by default, and made the latter into an actual field (dc3a34e)
  • ModelDelta and ModelChange (in simple_history.models) are now immutable dataclasses; their signatures remain unchanged (28afb4d)
  • ModelDelta's changes and changed_fields are now sorted alphabetically by field name. Also, if ModelChange is for an M2M field, its old and new lists are sorted by the related object. This should help prevent flaky tests. (a842b98)
  • diff_against() has a new keyword argument, foreign_keys_are_objs; see usage in the docs under "History Diffing" (a842b98)

Related Issue

Closes #886, closes #1030, closes #1308.

Motivation and Context

See the linked issues.

How Has This Been Tested?

Through the tests added in the changed test files.

Screenshots (if appropriate):

image

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 pre-commit run command to format and lint.
  • 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
Copy link

codecov bot commented Feb 21, 2023

Codecov Report

Attention: Patch coverage is 97.59615% with 5 lines in your changes are missing coverage. Please review.

Project coverage is 96.83%. Comparing base (4d39103) to head (733f4e0).

Files Patch % Lines
simple_history/models.py 94.87% 3 Missing and 1 partial ⚠️
simple_history/admin.py 97.05% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1128      +/-   ##
==========================================
- Coverage   96.88%   96.83%   -0.06%     
==========================================
  Files          23       24       +1     
  Lines        1285     1452     +167     
  Branches      212      237      +25     
==========================================
+ Hits         1245     1406     +161     
- Misses         21       24       +3     
- Partials       19       22       +3     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@PetrDlouhy
Copy link

I tested this PR and it seems to me, that it works correctly and is very helpful. Here is a preview of what it does:
Snímek obrazovky_2023-09-11_12-35-02

I checked, that it does show the diff values for my models and also that it doesn't make unnecessary DB queries (the number of queries is constant and not dependent on number of history records).

When I look at the code, i think there is added unnecessary comment line:

<!-- {{ action.history_diff }}-->

which is probably some relic from development.

And obviously the failed test would need to be corrected.
@raunaq-sailo Could you please remove the comment and fix the failing tests?

Copy link
Member

@ddabble ddabble left a comment

Choose a reason for hiding this comment

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

This is very much a neat feature to have! 😄 A few small changes:

simple_history/admin.py Outdated Show resolved Hide resolved
@ddabble ddabble changed the title added history diff Add history diff column to admin change history table Sep 15, 2023
@ddabble ddabble mentioned this pull request Sep 28, 2023
2 tasks
@ddabble ddabble linked an issue Sep 28, 2023 that may be closed by this pull request
2 tasks
@ddabble
Copy link
Member

ddabble commented Sep 28, 2023

@raunaq-sailo Just as a heads-up, I'm considering implementing the changes I suggested myself, if no activity has happened within a couple weeks :)

ddabble added a commit to ddabble/django-simple-history that referenced this pull request Feb 18, 2024
ddabble added a commit to ddabble/django-simple-history that referenced this pull request Feb 18, 2024
ddabble
ddabble previously approved these changes Feb 18, 2024
Copy link
Contributor

@tim-schilling tim-schilling left a comment

Choose a reason for hiding this comment

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

I think this is fine, but I would like to raise a slight change in implementation. Rather than always showing the change at the end, would it make more sense to include this as a default option in history_list_display? That way, it would allow the user to decide if they want to even include the changes or move it to a different column?

One of the problems with doing that is that we'd have to define some name for this column that would shadow potential fields on the actual historical model. Maybe something like Historical changes? I'm thinking Changes by itself might be too generic.

I don't feel super strong about it, but it seems like a reasonable customization we could implement. And it would avoid having an explicit call to set_history_delta_changes.

simple_history/admin.py Outdated Show resolved Hide resolved
@ddabble
Copy link
Member

ddabble commented Feb 19, 2024

I think this is fine, but I would like to raise a slight change in implementation. Rather than always showing the change at the end, would it make more sense to include this as a default option in history_list_display? That way, it would allow the user to decide if they want to even include the changes or move it to a different column?

One of the problems with doing that is that we'd have to define some name for this column that would shadow potential fields on the actual historical model. Maybe something like Historical changes? I'm thinking Changes by itself might be too generic.

Sure, so make history_list_display into a field (like Django's list_display) with a default value of something like ["historical_changes"], where historical_changes is the name of a method that returns the changes for each history record/row? If so, we could also do the same for the other default columns, to allow for a similar customization as list_display does (so that the current default column layout would be roughly translated to history_list_display = ["history_object", "history_date", "history_type", "history_user", "history_change_reason", "historical_changes"]) - though that would have to be in another PR, of course 🙂 Otherwise, I'm not entirely sure how a user would "move [Changes] to a different column" 🤔

@tim-schilling
Copy link
Contributor

Oh, I thought history_list_display was already a thing. I don't want to have perfect get in the way of good though, so let's make this improvement.

@ddabble
Copy link
Member

ddabble commented Mar 10, 2024

@tim-schilling I realized some additional changes needed to be made to properly display foreign keys and M2M objects, and so most of the recent commits were made to facilitate that - incl. making some optimizations and usage improvements along the way. It's absolutely possible, however, to display them without some of those changes, so let me know if I should split it into multiple PRs 🙂 (I'll squash some of the commits before merging.)

I also realized that making history_list_display contain the current default column layout - like we discussed - would require several extra changes, which I think is better to include in a separate PR that I can open after this one. Is that okay with you?

ddabble added a commit to raunaq-sailo/django-simple-history that referenced this pull request Mar 11, 2024
See discussion:
jazzband#1128 (comment)

Also prefixed the method with `_`, to mark it as not being part of the
public API.
@ddabble
Copy link
Member

ddabble commented May 2, 2024

@tim-schilling I mainly added some (presumably final) polish commits - including fd72e7a and 38ec0c2, which improve how really long strings are displayed - and added tests for how safe strings are handled. Again, please let me know if I should split it into multiple PRs 🙂

(The failing tests are unrelated to this PR; I'm looking into them.)

@tim-schilling
Copy link
Contributor

@ddabble I think it looks good. There was a lot in those changes and admittedly I didn't do a great job of reviewing. It looks like the code is well written, there are tests and they are passing. Not having code coverage hurts us here because that would confirm that the code is actually running in the tests.

Let's make this the last of the large, multi-commit PRs though. They're really tough to do a good job when I'm not super-duper familiar with the codebase.

tim-schilling
tim-schilling previously approved these changes May 3, 2024
@tim-schilling
Copy link
Contributor

@ddabble when you think it's ready, I'll integrate it with our application to see how things perform.

@ddabble
Copy link
Member

ddabble commented May 3, 2024

Not having code coverage hurts us here because that would confirm that the code is actually running in the tests.

True.. From the workflow logs (under "Upload coverage"), it looks like we're being rate-limited by Codecov, since the repo doesn't have any upload token 😕 (see #1305)

Let's make this the last of the large, multi-commit PRs though. They're really tough to do a good job when I'm not super-duper familiar with the codebase.

Alright, that's fair 😅 Thanks a bunch for reviewing regardless! I'll clean up the commits for merging, as mentioned above (followed by re-requesting a review) 🙂

when you think it's ready, I'll integrate it with our application to see how things perform.

I'm assuming you mean as a final system test before merging..?

@tim-schilling
Copy link
Contributor

I'm assuming you mean as a final system test before merging..?

Yeah, that's what I was thinking. Let me do that now.

@tim-schilling
Copy link
Contributor

Dang this is cool! So amazing 🚀

@tim-schilling
Copy link
Contributor

Also, I didn't run into any issues. All good.

ddabble and others added 13 commits May 5, 2024 16:52
This is a more accurate name for the contents of the queryset.

Also did a small cleanup of `_object_history_list.html`, incl. removing
the `scope` attribute of a `<td>` tag, as it's been deprecated for those
tags (see
https://developer.mozilla.org/en-US/docs/Web/HTML/Element/td#scope).
Including renaming it to remove the `_` prefix (indicating that it's
internal / not meant for usage by library users), and adding an
`object_history_list_template` field to the admin class.
This rename also changed the order of messages in the translation files.

Also deprecated the template tag `simple_history_admin_list.display_list`,
as it's now entirely unused within the project.
These should help with often having to check which values are returned
by `m2m_field_name()` and `m2m_reverse_field_name()` - since they're
both undocumented.
Also, if Django changes the mentioned internal methods, it'll be easier
to only update these two functions instead of all the places they
are / will be used.
This gives them useful methods like `__eq__()`, `__repr__()` and
`__hash__()` for free :)

Note that they have `frozen=True` (mainly to allow safe `__hash__()`
implementations), as I think the vast majority of users are in practice
treating these classes as read-only; modifying them serve no purpose
within this library, and should therefore be an incredibly niche use
case (except when comparing against expected mock objects while testing,
but then `dataclasses.replace()` can be used).
* Refactored building the change lists
  * Extracted the code for M2M fields and other fields into two helper
    methods
  * Derive `changed_fields` from `changes`
* Refactored getting M2M fields to avoid querying the DB (1 or 2 times)
  for a `reference_history_m2m_item`
This makes it easier to get the related objects from the diff objects,
and facilitates prefetching the related objects when listing the diffs
- which is needed for the upcoming changes to the admin history page
adding a "Changes" column.

The returned `ModelDelta` fields are now alphabetically ordered, to
prevent flakiness of one of the added tests.

The `old` and `new` M2M lists of `ModelChange` are now also sorted,
which will prevent inconsistent ordering of the `Place` objects in
`AdminSiteTest.test_history_list_contains_diff_changes_for_m2m_fields()`
(will be added as part of the previously mentioned upcoming admin
history changes).

Lastly, cleaned up some `utils` imports in `models.py`.
...when called with `foreign_keys_are_objs=True`.
This special dataclass (`DeletedObject`) will signal to the user that
the related object was deleted, and provides useful info - i.e. the
object's PK and model - and a verbose `__str__()` implementation.
(An alternative solution of returning `None` for deleted objects, would
introduce ambiguity if the `ForeignKey` field is nullable.)
...to `HistoricalQuerySet`.
This convenience method will make it easier to avoid database queries
- e.g. when diffing and listing the results, which will be done as part
of the upcoming changes to the admin history page adding a "Changes"
column.

No documentation was added, after the following discussion:
jazzband#1128 (comment)
Also prefixed the method with `_`, to mark it as not being part of the
public API.
...i.e. the code that's used to display the difference between the
actual and expected values for most of the `assert[...]` methods of
`TestCase` when the assertion fails.

This will be used to better display the diffs on the admin history page
in an upcoming commit - with some modifications.

The code was copied from
https://github.com/python/cpython/blob/v3.12.0/Lib/unittest/util.py#L8-L52,
with the following changes:
* Placed the code inside a class, to group the functions and their
  "setting" variables from other code - which also lets them easily be
  overridden by users
* Removed the `_` prefix from the functions and variables
* Added type hints
* Formatted with Black

Lastly, the code was copied instead of simply imported from `unittest`,
because the functions are undocumented and underscore-prefixed, which
means that they're prone to being changed (drastically) or even removed,
and so I think maintaining it will be easier and more stable by
copy-pasting it - which additionally facilitates modification.
This lists the changes between each historical record of the object.

Effort has been made to make it easy for library users to override most
parts of this, and to let them customize only the details they want to
without having to copy or replace our implementation.
An overview of how to override it has been added to `admin.rst`.

Most of the code was added to `template_utils.py`, to avoid cluttering
`SimpleHistoryAdmin`.

Also updated the Norwegian Bokmål translations,
and updated all the docs screenshots, to make them have Django 5.0's UI
and to showcase the new column.

Co-authored-by: Gur Raunaq Singh <raunaqsoni.dev123@gmail.com>
Co-authored-by: Tim Schilling <schilling711@gmail.com>
This removes the need for the `nosec` comments in files inside those
directories.

Also, the existing `-x` arg doesn't seem to work anymore, so this fixes
that.
@ddabble
Copy link
Member

ddabble commented May 5, 2024

Finished cleaning up the commits, so the PR is ready for merging :)
(A few minor changes done while rebasing)

@ddabble ddabble requested a review from tim-schilling May 5, 2024 20:37
Copy link
Contributor

@tim-schilling tim-schilling left a comment

Choose a reason for hiding this comment

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

This looks good. I really appreciate your attention to the docs, code comments and organization.

@ddabble ddabble merged commit b8c1a0c into jazzband:master May 6, 2024
19 of 22 checks passed
@hfroot
Copy link

hfroot commented May 27, 2024

Not to spam, but I think I love you 😍 this feature is awesome

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
5 participants