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

Fixes #93 - Add 'changes_display_dict' property to 'LogEntry' model to display diff in a more human readable format #94

Merged
merged 31 commits into from
Sep 13, 2017

Conversation

audiolion
Copy link
Contributor

@audiolion audiolion commented Jan 29, 2017

'changes_display_dict' currently handles fields with choices, long textfields and charfields, and datetime fields.

  • Fields with choices display in the diff with their human readable text.
  • Textfields and Charfields longer than 140 characters are truncated with an ellipsis appended.
  • Datetime fields are rendered with the format 'Aug. 21, 2017, 02:25pm'
  • Support for Postgres's ArrayField and django-multiselectfield for those not on Postgres

A new kwarg was added to AuditlogModelRegistry called mapping_fields. The kwarg allows the user to map the fields in the model to a more human readable or intuitive name. If a field isn't mapped it will default to the name as defined in the model. Partial mapping is supported, all fields do not need to be mapped to use the feature.

Tests added for 'changes_display_dict'
Tests added for 'mapping_fields' property

Updated Documentation

Resolves #93

@audiolion audiolion changed the title Issue 93 - Display human readable text for CharFields with choices. Fixes #93 - Add 'changes_display_dict' property to 'LogEntry' model to display diff in a more human readable format Feb 2, 2017
@audiolion audiolion force-pushed the issue_93 branch 3 times, most recently from c0643d1 to 1468c6d Compare February 2, 2017 21:57
@jjkester
Copy link
Collaborator

jjkester commented Feb 9, 2017

Thanks for your contribution. Because the changes are bit more major than usual I'll mark this for version 0.5.0.

I do not have a lot of time to look through the changes now, so I'll come back to it later.

@jjkester jjkester added this to the 0.5.0 milestone Feb 9, 2017
@audiolion
Copy link
Contributor Author

audiolion commented Feb 16, 2017

so if you want I can break this PR up into a few separate changes, one would be to add the mapping fields attribute. This allow the diff to take a database defined name sku and map it to something more readable Product No.. This is an opt in feature (to provide field mappings in your auditlog register function) and we can include a boolean argument to the diff function to turn off the field mappings if you want to get the actual database field names.

Different PR will be for adding a mapping of choices attributes to human displayable text and other potential differences, where you would prefer to take a different approach, I will open a different issue.

@audiolion audiolion force-pushed the issue_93 branch 2 times, most recently from e547ded to 14ef242 Compare February 20, 2017 17:44
@audiolion
Copy link
Contributor Author

audiolion commented Feb 20, 2017

PR now only adds changes display dict functionality. If you are not in favor of this approach you can close the PR and I will just use my custom fork. It is my opinion that due to the need for using model meta attribute introspection to solve this problem, template tags are an inefficient solution for this problem.

From TwoScoops of Django Best Practices:

14.2.4 When to Write Template Tags

These days, we’re very cautious about adding new template tags. We consider two things before writing them:

  • Anything that causes a read/write of data might be better placed in a model or object method.
  • Since we implement a consistent naming standard across our projects, we can add an abstract base class model to our core.models module. Can a method or property in our project’s
    abstract base class model do the same work as a custom template tag?

Implementing this as a templatetag would violate both bullets, it is both reading data and can be implemented in in a base class model

@audiolion
Copy link
Contributor Author

With the updates you can uses changes_display_dict to get:
image

The current use of changes_dict does not map to human readable names and provides the actual database values like this:
image

The information from changes_dict is great for the programmer, what database field was changed, what database value was changed, however if you are displaying it to your end users or non-technical users who are using the admin site the human readable version helps a lot.

@jjkester
Copy link
Collaborator

OK, you convinced me. I have no time today to actually look at everything but I will take a look soon.

.coveragerc Outdated
@@ -0,0 +1,13 @@
[run]
Copy link
Collaborator

Choose a reason for hiding this comment

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

I would like to have the coverage in another pull request.

.gitignore Outdated
@@ -7,3 +7,4 @@
.project
.pydevproject
local_settings.py
.coverage
Copy link
Collaborator

Choose a reason for hiding this comment

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

Occurrence of coverage

README.md Outdated
@@ -27,3 +27,16 @@ Contribute
----------

If you have great ideas for Auditlog, or if you like to improve something, feel free to fork this repository and/or create a pull request. I'm open for suggestions. If you like to discuss something with me (about Auditlog), please open an issue.

Pull Request Guidelines
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can you put this in another pull request? Thanks!

@@ -229,6 +233,62 @@ def changes_str(self, colon=': ', arrow=smart_text(' \u2192 '), separator='; '):

return separator.join(substrings)

@property
def changes_display_dict(self):
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can you inline-comment this? Than it is better understandable what it exactly does.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

will do

@@ -257,6 +317,7 @@ def __init__(self, pk_indexable=True, **kwargs):
kwargs['content_type_field'] = 'content_type'
super(AuditlogHistoryField, self).__init__(**kwargs)


Copy link
Collaborator

Choose a reason for hiding this comment

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

Newline not needed here

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yeah I think that was my editor adding it automatically on save ill remove

)

status = models.CharField(max_length=1, choices=STATUS_CHOICES)
multiselect = MultiSelectField(max_length=3, choices=STATUS_CHOICES, max_choices=3)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why are you explicitly testing with this third party library?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I use this library and wanted auditlog to be compatible with it

Copy link
Collaborator

Choose a reason for hiding this comment

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

Right. If this is added to the tests I would also want the built-in PostgreSQL equivalent/replacement of it (https://docs.djangoproject.com/en/1.10/ref/contrib/postgres/fields/#arrayfield) since it is likely that people store lists that way as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yeah I am stuck on 1.8 so using it, I will add arrayfield support though, good idea!

@@ -19,7 +20,7 @@
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': 'auditlog_tests.db',
'NAME': 'auditlog_test.db',
Copy link
Collaborator

Choose a reason for hiding this comment

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

The 'app' is still called auditlog_tests, so why this change?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

not needed change, ill revert

@jjkester
Copy link
Collaborator

The fixes do not seem to be pushed to this branch, so I cannot merge it. Can you do this so we can get the functionality in?

Fixes jazzband#93 - Add 'changes_display_dict' property to 'LogEntry' model to display diff in a more human readable format

'changes_display_dict' currently handles fields with choices, long textfields and charfields, and datetime fields.
Fields with choices display in the diff with their human readable text.
Textfields and Charfields longer than 140 characters are truncated with an ellipsis appended.
Datetime fields are rendered with the format 'Aug. 21, 2017, 02:25pm'

A new kwarg was added to 'AuditlogModelRegistry' called 'mapping_fields'. The kwarg allows the user to map the
fields in the model to a more human readable or intuitive name. If a field isn't mapped it will default to the
name as defined in the model. Partial mapping is supported, all fields do not need to be mapped to use the feature.

Tests added for 'changes_display_dict'
Tests added for 'mapping_fields' property

Added python 3.5 support

Updated Documentation

Update .travis.yml to use requirements_test.txt as well in build
… dateutil's parser and would get formatted as datetime output. Introspection is used to see if the internal field type is DateTime, Date, or Time and then try to parse it and format it appropriately. This adds functionality in that DateTime, Date and Time each now have their separate formats.
of the 'mapping_fields' dictionary keys and values. The purpose of
this dictionary is so that the LogEntry model's 'changes_display_dict'
method can reverse a human readable field name returned from 'changes'
so that it can do extra checks on that field. Without this we would get
a FieldDoesNotExist error when trying to lookup fields on the model in
the 'changes_display_dict' method so that we could introspect attributes
about them when determining how to display human readable changes.
@codecov-io
Copy link

codecov-io commented Sep 6, 2017

Codecov Report

❗ No coverage uploaded for pull request base (master@ac8638d). Click here to learn what that means.
The diff coverage is 94%.

Impacted file tree graph

@@            Coverage Diff            @@
##             master      #94   +/-   ##
=========================================
  Coverage          ?   69.15%           
=========================================
  Files             ?       18           
  Lines             ?      483           
  Branches          ?        0           
=========================================
  Hits              ?      334           
  Misses            ?      149           
  Partials          ?        0
Impacted Files Coverage Δ
src/auditlog/registry.py 86.27% <100%> (ø)
src/auditlog/models.py 81.21% <93.75%> (ø)

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 ac8638d...27ae05e. Read the comment docs.

@audiolion audiolion dismissed jjkester’s stale review September 6, 2017 19:38

Review comments have been addressed

@audiolion
Copy link
Contributor Author

audiolion commented Sep 6, 2017

Hey @jjkester addressed all the review comments and got the diff coverage to 100%.

To support the request for Postgres's ArrayField I had to modify the test setup to run with multiple databases. In the process I found a bug where Auditlog saved LogEntry's to the default database always. I fixed this in 1aca8073 so that LogEntry's are saved to the database of the model they are attached to.

Also after this PR merges there will now be a base commit with coverage report to compare PR's to so we will get actual information from codecov reports.

from auditlog.registry import auditlog
model_fields = auditlog.get_model_fields(model._meta.model)
reversed_field = model_fields['reverse_mapping_fields'].get(field_name)
field = model._meta.get_field(reversed_field)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Again, a FieldDoesNotExist can be raised, for example when a model is changed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok, with the update to mapping_fields not changing the diff.py output, I didn't need the reverse_mapping_fields. I have now removed that section, and in the case of FieldDoesNotExist fall back on the field_name auditlog has stored.

if "DateTime" in field.get_internal_type():
try:
value = parser.parse(value)
value = value.strftime("%b %d, %Y %I:%M %p")
Copy link
Collaborator

Choose a reason for hiding this comment

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

I would rather see use of Django's l10n stuff for this, so that the format adapts to different settings.

Copy link
Contributor Author

@audiolion audiolion Sep 11, 2017

Choose a reason for hiding this comment

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

Alright, modified to now rely on USE_L10N first and fallback on settings.DATETIME_FORMAT, settings.DATE_FORMAT, and settings.TIME_FORMAT which have defaults and can be set if the user desires.

Added test cases which demonstrate this.

elif "Date" in field.get_internal_type():
try:
value = parser.parse(value)
value = value.strftime("%b %d, %Y")
Copy link
Collaborator

Choose a reason for hiding this comment

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

Again, l10n is preferred.

elif "Time" in field.get_internal_type():
try:
value = parser.parse(value)
value = value.strftime("%I:%M %p")
Copy link
Collaborator

Choose a reason for hiding this comment

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

Again, l10n is preferred.

@@ -135,7 +135,7 @@ def model_instance_diff(old, new):
new_value = get_field_value(new, field)

if old_value != new_value:
diff[field.name] = (smart_text(old_value), smart_text(new_value))
diff[model_fields['mapping_fields'].get(field.name, field.name)] = (smart_text(old_value), smart_text(new_value))
Copy link
Collaborator

Choose a reason for hiding this comment

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

I would not use the mapping here. I think keeping the database representation as close to internal representation as possible is a good idea, especially because later on we are using database values to retrieve model fields. Changing the mapping would completely destroy that capability for the changed fields. The actual field names are less likely to change.

The mapping could be applied when the dictionary for display is built up (see my comment there for details).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah yes I agree, I reverted this change and now only change the name in the changes_display_dict property


values_display.append(value)

changes_display_dict[field_name] = values_display
Copy link
Collaborator

Choose a reason for hiding this comment

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

Instead of the field name we might use the mapping here. Also, we might consider going with the verbose name if present. So in order of importance: mapping - verbose name - name (the last is only guaranteed to be present).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok made this change, the order is mapping -> verbose name -> default verbose name. Django assigns a default verbose_name if none is provided so it is guaranteed to be always there.

If this isn’t given, Django will use a munged version of the class name: CamelCase becomes camel case.
ref

…e changes_display_dict function. Remove usage of reverse_fields as it is no longer required. Default to field.verbose_name in the cases where there is no mapping_field, and if the field cannot be found default to the auditlog stored field_name
@audiolion audiolion dismissed jjkester’s stale review September 11, 2017 17:26

addressed concerns, awaiting new review

jjkester
jjkester previously approved these changes Sep 13, 2017
Copy link
Collaborator

@jjkester jjkester left a comment

Choose a reason for hiding this comment

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

You might change the docs a bit, otherwise it looks good

</table>
</div>

If you want to display the changes in a more human readable format use the :py:class:`LogEntry`'s :py:attr:`changes_display_dict` instead. The :py:attr:`changes_display_dict` will translate ``choices`` fields into their human readable form, display timestamps in the form ``Jun. 31, 2017 12:55pm``, and truncate text greater than 140 characters to 140 characters with an ellipsis appended.
Copy link
Collaborator

Choose a reason for hiding this comment

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

maybe be explicit about the l10n support

@audiolion audiolion merged commit 45760c6 into jazzband:master Sep 13, 2017
@audiolion audiolion deleted the issue_93 branch September 13, 2017 14:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Have changes_dict show get_<field_name>_display
3 participants