Skip to content

Commit

Permalink
Add coverage
Browse files Browse the repository at this point in the history
Fixes #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
  • Loading branch information
audiolion committed Feb 20, 2017
1 parent cd662fc commit 14ef242
Show file tree
Hide file tree
Showing 13 changed files with 279 additions and 5 deletions.
13 changes: 13 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[run]
branch = true

[report]
omit =
*site-packages*
*auditlog_tests*
*auditlog/migrations*
src/runtests.py
show_missing = True
exclude_lines =
pragma: no cover
raise NotImplementedError
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@
.project
.pydevproject
local_settings.py
.coverage
2 changes: 2 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ language: python
python:
- "2.7"
- "3.4"
- "3.5"
env:
- DJANGO_VERSION=1.8.*
- DJANGO_VERSION=1.9.*
- DJANGO_VERSION=1.10.*
- DJANGO_VERSION=1.11a1
install:
- "pip install -r requirements.txt"
- "pip install -r requirements_test.txt"
- "pip install Django==$DJANGO_VERSION"
script: "python src/runtests.py"
sudo: false
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
-----------------------

Before you submit a pull request, check that it meets these guidelines:

1. The pull request should include tests.
2. If the pull request adds functionality, the docs should be updated. Put
your new functionality into a function with a docstring, and update relevant
documentation.
3. The pull request should work for Python 2.7, 3.4+, and with Django>=1.8. Check
https://travis-ci.org/jjkester/django-auditlog/pull_requests
and make sure that the tests pass for all supported Python versions.
44 changes: 44 additions & 0 deletions docs/source/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,19 @@ For example, to exclude the field ``last_updated``, use::

Excluding fields

**Mapping fields**

If you have field names on your models that aren't intuitive or user friendly you can include a dictionary of field mappings
during the `register()` call.

.. code-block:: python
auditlog.register(MyModel, mapping_fields={'sku': 'Product No.', 'version': 'Product Revision'})
.. versionadded:: 0.5.0

You do not need to map all the fields of the model, any fields not mapped will be displayed as they are defined in the model.

Actors
------

Expand Down Expand Up @@ -95,6 +108,37 @@ your models is equally easy as any other field::
``False``, this defaults to ``True``. If your model has a custom primary key that is not an integer value,
:py:attr:`pk_indexable` needs to be set to ``False``. Keep in mind that this might slow down queries.

The :py:class:`AuditlogHistoryField` provides easy access to :py:class:`LogEntry` instances related to the model instance. Here is an example of how to use it:

.. code-block:: html

<div class="table-responsive">
<table class="table table-striped table-bordered">
<thead>
<tr>
<th>Field</th>
<th>From</th>
<th>To</th>
</tr>
</thead>
<tbody>
{% for key, value in mymodel.history.latest.changes_dict.iteritems %}
<tr>
<td>{{ key }}</td>
<td>{{ value.0|default:"None" }}</td>
<td>{{ value.1|default:"None" }}</td>
</tr>
{% empty %}
<p>No history for this item has been logged yet.</p>
{% endfor %}
</tbody>
</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.

Check out the internals for the full list of attributes you can use to get associated :py:class:`LogEntry` instances.

Many-to-many relationships
--------------------------

Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
Django>=1.8
django-jsonfield>=1.0.0
python-dateutil==2.6.0
2 changes: 2 additions & 0 deletions requirements_test.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
coverage==4.2
django-multiselectfield==0.1.5
5 changes: 4 additions & 1 deletion src/auditlog/diff.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,10 @@ 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))
if model_fields:
diff[model_fields['mapping_fields'].get(field.name, field.name)] = (smart_text(old_value), smart_text(new_value))
else:
diff[field.name] = (smart_text(old_value), smart_text(new_value))

if len(diff) == 0:
diff = None
Expand Down
41 changes: 41 additions & 0 deletions src/auditlog/models.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from __future__ import unicode_literals

import json
import ast
from datetime import datetime

from django.conf import settings
from django.contrib.contenttypes.fields import GenericRelation
Expand All @@ -12,6 +14,7 @@
from django.utils.translation import ugettext_lazy as _

from jsonfield.fields import JSONField
from dateutil import parser


class LogEntryManager(models.Manager):
Expand Down Expand Up @@ -229,6 +232,43 @@ def changes_str(self, colon=': ', arrow=smart_text(' \u2192 '), separator='; '):

return separator.join(substrings)

@property
def changes_display_dict(self):
"""
:return: The changes recorded in this log entry intended for display to users as a dictionary object.
"""
model = self.content_type.model_class()
changes_display_dict = {}
for field_name, values in iteritems(self.changes_dict):
field = model._meta.get_field(field_name)
values_display = []
if field.choices:
choices_dict = dict(field.choices)
for value in values:
try:
value = ast.literal_eval(value)
if type(value) is [].__class__:
values_display.append(', '.join([choices_dict.get(val, 'None') for val in value]))
else:
values_display.append(choices_dict.get(value, 'None'))
except ValueError:
values_display.append(choices_dict.get(value, 'None'))
else:
for value in values:
try:
value = parser.parse(value)
value = value.strftime("%b %d, %Y %I:%M %p")
except ValueError:
pass

if len(value) > 140:
value = "{}...".format(value[:140])

values_display.append(value)

changes_display_dict[field_name] = values_display
return changes_display_dict


class AuditlogHistoryField(GenericRelation):
"""
Expand Down Expand Up @@ -257,6 +297,7 @@ def __init__(self, pk_indexable=True, **kwargs):
kwargs['content_type_field'] = 'content_type'
super(AuditlogHistoryField, self).__init__(**kwargs)


# South compatibility for AuditlogHistoryField
try:
from south.modelsinspector import add_introspection_rules
Expand Down
4 changes: 3 additions & 1 deletion src/auditlog/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def __init__(self, create=True, update=True, delete=True, custom=None):
if custom is not None:
self._signals.update(custom)

def register(self, model=None, include_fields=[], exclude_fields=[]):
def register(self, model=None, include_fields=[], exclude_fields=[], mapping_fields={}):
"""
Register a model with auditlog. Auditlog will then track mutations on this model's instances.
Expand All @@ -44,6 +44,7 @@ def registrar(cls):
self._registry[cls] = {
'include_fields': include_fields,
'exclude_fields': exclude_fields,
'mapping_fields': mapping_fields,
}
self._connect_signals(cls)

Expand Down Expand Up @@ -110,6 +111,7 @@ def get_model_fields(self, model):
return {
'include_fields': self._registry[model]['include_fields'],
'exclude_fields': self._registry[model]['exclude_fields'],
'mapping_fields': self._registry[model]['mapping_fields'],
}


Expand Down
52 changes: 52 additions & 0 deletions src/auditlog_tests/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
from auditlog.models import AuditlogHistoryField
from auditlog.registry import auditlog

from multiselectfield import MultiSelectField


@auditlog.register()
class SimpleModel(models.Model):
Expand Down Expand Up @@ -84,6 +86,18 @@ class SimpleExcludeModel(models.Model):
history = AuditlogHistoryField()


class SimpleMappingModel(models.Model):
"""
A simple model used for register's mapping_fields kwarg
"""

sku = models.CharField(max_length=100)
vtxt = models.CharField(max_length=100)
not_mapped = models.CharField(max_length=100)

history = AuditlogHistoryField()


class AdditionalDataIncludedModel(models.Model):
"""
A model where get_additional_data is defined which allows for logging extra
Expand Down Expand Up @@ -120,11 +134,49 @@ class DateTimeFieldModel(models.Model):
history = AuditlogHistoryField()


class ChoicesFieldModel(models.Model):
"""
A model with a CharField restricted to a set of choices.
This model is used to test the changes_display_dict method.
"""
RED = 'r'
YELLOW = 'y'
GREEN = 'g'

STATUS_CHOICES = (
(RED, 'Red'),
(YELLOW, 'Yellow'),
(GREEN, 'Green'),
)

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

history = AuditlogHistoryField()


class CharfieldTextfieldModel(models.Model):
"""
A model with a max length CharField and a Textfield.
This model is used to test the changes_display_dict
method's ability to truncate long text.
"""

longchar = models.CharField(max_length=255)
longtextfield = models.TextField()

history = AuditlogHistoryField()


auditlog.register(AltPrimaryKeyModel)
auditlog.register(ProxyModel)
auditlog.register(RelatedModel)
auditlog.register(ManyRelatedModel)
auditlog.register(ManyRelatedModel.related.through)
auditlog.register(SimpleExcludeModel, exclude_fields=['text'])
auditlog.register(SimpleMappingModel, mapping_fields={'sku': 'Product No.', 'vtxt': 'Version'})
auditlog.register(AdditionalDataIncludedModel)
auditlog.register(DateTimeFieldModel)
auditlog.register(ChoicesFieldModel)
auditlog.register(CharfieldTextfieldModel)
3 changes: 2 additions & 1 deletion src/auditlog_tests/test_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
'django.contrib.contenttypes',
'auditlog',
'auditlog_tests',
'multiselectfield',
]

MIDDLEWARE_CLASSES = (
Expand All @@ -19,7 +20,7 @@
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': 'auditlog_tests.db',
'NAME': 'auditlog_test.db',
}
}

Expand Down
Loading

0 comments on commit 14ef242

Please sign in to comment.